diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index b42c7b1f6b..1f1c0cc530 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 AnyUrl, BaseModel, Field, WithJsonSchema 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)" @@ -151,14 +151,18 @@ async def fetch_url( class Fetch(BaseModel): """Parameters for fetching a URL.""" - url: Annotated[AnyUrl, Field(description="URL to fetch")] + url: Annotated[ + AnyUrl, + Field(description="URL to fetch"), + WithJsonSchema({"type": "string", "description": "URL to fetch"}), + ] max_length: Annotated[ int, Field( default=5000, description="Maximum number of characters to return.", - gt=0, - lt=1000000, + ge=1, + le=999999, ), ] start_index: Annotated[ diff --git a/src/fetch/tests/test_server.py b/src/fetch/tests/test_server.py index 96c1cb38c7..577fa965f2 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -3,8 +3,10 @@ import pytest from unittest.mock import AsyncMock, patch, MagicMock from mcp.shared.exceptions import McpError +from pydantic import ValidationError from mcp_server_fetch.server import ( + Fetch, extract_content_from_html, get_robots_txt_url, check_may_autonomously_fetch_url, @@ -13,6 +15,52 @@ ) +class TestFetchToolSchema: + """Tests for the Fetch model JSON schema compatibility.""" + + def test_schema_uses_inclusive_bounds(self): + """Ensure the schema uses minimum/maximum instead of exclusiveMinimum/exclusiveMaximum. + + Some LLM providers (e.g. Google Gemini) only support OpenAPI 3.0 + schema keywords and reject exclusiveMinimum/exclusiveMaximum. + """ + schema = Fetch.model_json_schema() + max_length_schema = schema["properties"]["max_length"] + + assert "exclusiveMinimum" not in max_length_schema + assert "exclusiveMaximum" not in max_length_schema + assert max_length_schema["minimum"] == 1 + assert max_length_schema["maximum"] == 999999 + + def test_url_schema_omits_unsupported_keywords(self): + """Ensure the url field schema avoids format/minLength keywords. + + Some LLM providers (e.g. Google Gemini) only support a limited set + of keywords for string types and reject unsupported ones like + format: "uri" or minLength. + """ + schema = Fetch.model_json_schema() + url_schema = schema["properties"]["url"] + + assert "format" not in url_schema + assert "minLength" not in url_schema + assert url_schema["type"] == "string" + + def test_url_runtime_validation_still_rejects_invalid_urls(self): + """Ensure AnyUrl runtime validation is preserved after the WithJsonSchema override. + + The override only simplifies the emitted JSON schema; invalid URLs + must still be rejected at parse time with a ValidationError. + """ + with pytest.raises(ValidationError): + Fetch.model_validate({"url": "not-a-url"}) + + def test_url_runtime_validation_accepts_valid_urls(self): + """Ensure valid URLs still parse successfully after the WithJsonSchema override.""" + fetch = Fetch.model_validate({"url": "https://example.com/page"}) + assert str(fetch.url) == "https://example.com/page" + + class TestGetRobotsTxtUrl: """Tests for get_robots_txt_url function."""