Skip to content

Commit ee8773e

Browse files
committed
fix(fetch): handle malformed input without crashing
Change `raise_exceptions=True` to `raise_exceptions=False` in server.run() so that malformed JSON-RPC messages on stdin are handled gracefully instead of terminating the server process. This aligns mcp-server-fetch with other reference servers (e.g. mcp-server-time) that use the default raise_exceptions=False. Fixes #3359
1 parent c14c28a commit ee8773e

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

src/fetch/src/mcp_server_fetch/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,4 @@ async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
285285

286286
options = server.create_initialization_options()
287287
async with stdio_server() as (read_stream, write_stream):
288-
await server.run(read_stream, write_stream, options, raise_exceptions=True)
288+
await server.run(read_stream, write_stream, options, raise_exceptions=False)

src/fetch/tests/test_server.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for the fetch MCP server."""
22

33
import pytest
4+
import asyncio
45
from unittest.mock import AsyncMock, patch, MagicMock
56
from mcp.shared.exceptions import McpError
67

@@ -9,6 +10,7 @@
910
get_robots_txt_url,
1011
check_may_autonomously_fetch_url,
1112
fetch_url,
13+
serve,
1214
DEFAULT_USER_AGENT_AUTONOMOUS,
1315
)
1416

@@ -324,3 +326,60 @@ async def test_fetch_with_proxy(self):
324326

325327
# Verify AsyncClient was called with proxy
326328
mock_client_class.assert_called_once_with(proxy="http://proxy.example.com:8080")
329+
330+
331+
class TestServeRaiseExceptions:
332+
"""Tests that the server handles malformed input gracefully."""
333+
334+
@pytest.mark.asyncio
335+
async def test_serve_does_not_crash_on_malformed_input(self):
336+
"""Test that serve() uses raise_exceptions=False so malformed input
337+
does not crash the server process.
338+
339+
Regression test for https://github.com/modelcontextprotocol/servers/issues/3359
340+
"""
341+
from anyio import create_memory_object_stream
342+
343+
# Create a stream pair to pass as stdio replacements
344+
send_stream, recv_stream = create_memory_object_stream[bytes](0)
345+
_, write_stream = create_memory_object_stream[bytes](0)
346+
347+
mock_server_run = AsyncMock()
348+
349+
with patch("mcp_server_fetch.server.stdio_server") as mock_stdio:
350+
with patch("mcp_server_fetch.server.Server") as MockServer:
351+
mock_server_instance = MagicMock()
352+
mock_server_instance.create_initialization_options = MagicMock(
353+
return_value={}
354+
)
355+
mock_server_instance.run = mock_server_run
356+
357+
# The decorator methods (list_tools, list_prompts, etc.) return
358+
# a decorator that registers the handler. We just need them to
359+
# accept and return the decorated function unchanged.
360+
def make_decorator(*args, **kwargs):
361+
def decorator(fn):
362+
return fn
363+
return decorator
364+
365+
mock_server_instance.list_tools = make_decorator
366+
mock_server_instance.list_prompts = make_decorator
367+
mock_server_instance.call_tool = make_decorator
368+
mock_server_instance.get_prompt = make_decorator
369+
370+
MockServer.return_value = mock_server_instance
371+
372+
mock_stdio.return_value.__aenter__ = AsyncMock(
373+
return_value=(recv_stream, write_stream)
374+
)
375+
mock_stdio.return_value.__aexit__ = AsyncMock(return_value=None)
376+
377+
await serve()
378+
379+
# Verify that server.run was called with raise_exceptions=False
380+
mock_server_run.assert_called_once()
381+
_, kwargs = mock_server_run.call_args
382+
assert kwargs.get("raise_exceptions") is False, (
383+
"server.run must be called with raise_exceptions=False "
384+
"to prevent crashes on malformed input"
385+
)

0 commit comments

Comments
 (0)