From 342a223044a0836b09ebe22a158cd0bc98487bef Mon Sep 17 00:00:00 2001 From: Dharit Shah Date: Fri, 3 Apr 2026 23:10:49 -0400 Subject: [PATCH 1/3] fix(fetch): use OpenAPI 3.0 compatible schema keywords for Gemini support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Fetch model's JSON schema used keywords incompatible with LLM providers that only support OpenAPI 3.0 (e.g. Google Gemini 2.5 Pro), causing 400 INVALID_ARGUMENT errors: 1. max_length field used gt=0/lt=1000000 (Pydantic Field constraints), which generated exclusiveMinimum/exclusiveMaximum — not recognized by Gemini. Changed to ge=1/le=999999 (identical semantics for integers), which emits the supported minimum/maximum keywords. 2. url field used AnyUrl, which generated format: "uri" and minLength: 1 — Gemini only supports "enum" and "date-time" for string format. Added WithJsonSchema override to emit a plain string schema while preserving AnyUrl runtime validation. Fixes #1624 Made-with: Cursor --- src/fetch/src/mcp_server_fetch/server.py | 12 ++++++--- src/fetch/tests/test_server.py | 33 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) 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..ff87b34d34 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -5,6 +5,7 @@ from mcp.shared.exceptions import McpError from mcp_server_fetch.server import ( + Fetch, extract_content_from_html, get_robots_txt_url, check_may_autonomously_fetch_url, @@ -13,6 +14,38 @@ ) +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" + + class TestGetRobotsTxtUrl: """Tests for get_robots_txt_url function.""" From 14e4107e4088e5a76e7f9d02f4add5ac19dfff2b Mon Sep 17 00:00:00 2001 From: Dharit Shah Date: Tue, 21 Apr 2026 10:55:54 -0400 Subject: [PATCH 2/3] test(fetch): add runtime validation tests for url field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that AnyUrl runtime validation is preserved after the WithJsonSchema override — invalid URLs still raise ValidationError, valid URLs parse successfully. Addresses review feedback on #3812. --- src/fetch/tests/test_server.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/fetch/tests/test_server.py b/src/fetch/tests/test_server.py index ff87b34d34..be2f9a3349 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -3,6 +3,7 @@ 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, @@ -45,6 +46,20 @@ def test_url_schema_omits_unsupported_keywords(self): 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(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(url="https://example.com/page") + assert str(fetch.url) == "https://example.com/page" + class TestGetRobotsTxtUrl: """Tests for get_robots_txt_url function.""" From 787326203d05e55fd2a0d7b0bb6f79731c3fecb6 Mon Sep 17 00:00:00 2001 From: Dharit Shah Date: Tue, 21 Apr 2026 10:59:59 -0400 Subject: [PATCH 3/3] test(fetch): use model_validate to satisfy pyright in url tests Pyright doesn't recognize Pydantic's Annotated + Field(default=...) pattern as making __init__ args optional, so Fetch(url="...") flags max_length/start_index/raw as missing. Switch to Fetch.model_validate which accepts a dict and avoids the false positive. Same behavior, same coverage. --- src/fetch/tests/test_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fetch/tests/test_server.py b/src/fetch/tests/test_server.py index be2f9a3349..577fa965f2 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -53,11 +53,11 @@ def test_url_runtime_validation_still_rejects_invalid_urls(self): must still be rejected at parse time with a ValidationError. """ with pytest.raises(ValidationError): - Fetch(url="not-a-url") + 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(url="https://example.com/page") + fetch = Fetch.model_validate({"url": "https://example.com/page"}) assert str(fetch.url) == "https://example.com/page"