|
1 | 1 | """Tests for the fetch MCP server.""" |
2 | 2 |
|
3 | 3 | import pytest |
| 4 | +import asyncio |
4 | 5 | from unittest.mock import AsyncMock, patch, MagicMock |
5 | 6 | from mcp.shared.exceptions import McpError |
6 | 7 |
|
|
9 | 10 | get_robots_txt_url, |
10 | 11 | check_may_autonomously_fetch_url, |
11 | 12 | fetch_url, |
| 13 | + serve, |
12 | 14 | DEFAULT_USER_AGENT_AUTONOMOUS, |
13 | 15 | ) |
14 | 16 |
|
@@ -324,3 +326,60 @@ async def test_fetch_with_proxy(self): |
324 | 326 |
|
325 | 327 | # Verify AsyncClient was called with proxy |
326 | 328 | 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