Test Page
+Hello from the prompt
+diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index b42c7b1f6b..e3a3775544 100644 --- a/src/fetch/src/mcp_server_fetch/server.py +++ b/src/fetch/src/mcp_server_fetch/server.py @@ -18,7 +18,7 @@ INTERNAL_ERROR, ) from protego import Protego -from pydantic import BaseModel, Field, AnyUrl +from pydantic import BaseModel, Field, AnyUrl, ValidationError DEFAULT_USER_AGENT_AUTONOMOUS = "ModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)" DEFAULT_USER_AGENT_MANUAL = "ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)" @@ -259,7 +259,10 @@ async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult: if not arguments or "url" not in arguments: raise McpError(ErrorData(code=INVALID_PARAMS, message="URL is required")) - url = arguments["url"] + try: + url = str(Fetch.model_validate({"url": arguments["url"]}).url) + except ValidationError as e: + raise McpError(ErrorData(code=INVALID_PARAMS, message=str(e))) try: content, prefix = await fetch_url(url, user_agent_manual, proxy_url=proxy_url) diff --git a/src/fetch/tests/test_server.py b/src/fetch/tests/test_server.py index 96c1cb38c7..9f84b573d1 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -1,9 +1,16 @@ """Tests for the fetch MCP server.""" +import contextlib + import pytest +from anyio import create_task_group from unittest.mock import AsyncMock, patch, MagicMock from mcp.shared.exceptions import McpError +from mcp.shared.memory import create_client_server_memory_streams +from mcp.client.session import ClientSession +from mcp.types import INVALID_PARAMS, TextContent +import mcp_server_fetch.server as server_module from mcp_server_fetch.server import ( extract_content_from_html, get_robots_txt_url, @@ -13,6 +20,31 @@ ) +@contextlib.asynccontextmanager +async def serve_in_memory(): + """Run the fetch server over in-memory streams and yield a connected ClientSession. + + serve() opens stdio_server() internally, so patch it to hand out the + server side of an in-memory transport instead. + """ + async with create_client_server_memory_streams() as (client_streams, server_streams): + client_read, client_write = client_streams + + @contextlib.asynccontextmanager + async def fake_stdio_server(): + yield server_streams + + with patch.object(server_module, "stdio_server", fake_stdio_server): + async with create_task_group() as tg: + tg.start_soon(server_module.serve) + try: + async with ClientSession(client_read, client_write) as session: + await session.initialize() + yield session + finally: + tg.cancel_scope.cancel() + + class TestGetRobotsTxtUrl: """Tests for get_robots_txt_url function.""" @@ -324,3 +356,48 @@ async def test_fetch_with_proxy(self): # Verify AsyncClient was called with proxy mock_client_class.assert_called_once_with(proxy="http://proxy.example.com:8080") + + +class TestGetPrompt: + """Tests for the get_prompt handler reached through a real client session.""" + + @pytest.mark.asyncio + async def test_invalid_url_raises_invalid_params(self): + """An invalid prompt URL must raise McpError with INVALID_PARAMS, not a generic JSON-RPC error.""" + async with serve_in_memory() as session: + with pytest.raises(McpError) as exc_info: + await session.get_prompt("fetch", {"url": "http://[::1"}) + assert exc_info.value.error.code == INVALID_PARAMS + + @pytest.mark.asyncio + async def test_valid_url_returns_fetched_content(self): + """A valid prompt URL still fetches and returns the page content.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + +
+Hello from the prompt
+