From ddb6b61de2b70e88741ecdd3e6516eda676c1f42 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sat, 18 Apr 2026 20:06:25 +0100 Subject: [PATCH] fix(fetch): accept null for optional parameters Closes #2035. Clients like LibreChat send explicit `null` for optional params instead of omitting them. The Fetch model declared `max_length: int`, `start_index: int`, and `raw: bool`, so Pydantic rejected `null` at the type-coercion step before field defaults could apply. Adds a `mode="before"` field_validator that maps `None` back to the field's declared default. Keeps field types as `int`/`bool`, preserves the `gt=0`/`lt=1_000_000`/`ge=0` validators on resolved values, and does not rely on falsy-coalescing (which would have replaced valid `start_index=0` or explicit `raw=false`). Adds 9 tests covering null handling, default preservation, explicit zero/false preservation, and validator behavior. --- src/fetch/src/mcp_server_fetch/server.py | 9 +++- src/fetch/tests/test_server.py | 62 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index b42c7b1f6b..aeca59bd32 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, field_validator 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)" @@ -177,6 +177,13 @@ class Fetch(BaseModel): ), ] + @field_validator("max_length", "start_index", "raw", mode="before") + @classmethod + def _null_to_default(cls, value, info): + if value is None: + return cls.model_fields[info.field_name].default + return value + async def serve( custom_user_agent: str | None = None, diff --git a/src/fetch/tests/test_server.py b/src/fetch/tests/test_server.py index 96c1cb38c7..aac450cdab 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -9,10 +9,72 @@ get_robots_txt_url, check_may_autonomously_fetch_url, fetch_url, + Fetch, DEFAULT_USER_AGENT_AUTONOMOUS, ) +class TestFetchParamsNullHandling: + """Issue #2035: clients like LibreChat pass explicit null for optional params.""" + + def test_all_optional_params_omitted(self): + args = Fetch(url="https://example.com") + assert args.max_length == 5000 + assert args.start_index == 0 + assert args.raw is False + + def test_explicit_null_max_length(self): + args = Fetch(url="https://example.com", max_length=None) + assert args.max_length == 5000 + + def test_explicit_null_start_index(self): + args = Fetch(url="https://example.com", start_index=None) + assert args.start_index == 0 + + def test_explicit_null_raw(self): + args = Fetch(url="https://example.com", raw=None) + assert args.raw is False + + def test_all_nulls_together(self): + args = Fetch( + url="https://example.com", + max_length=None, + start_index=None, + raw=None, + ) + assert args.max_length == 5000 + assert args.start_index == 0 + assert args.raw is False + + def test_explicit_values_are_preserved(self): + args = Fetch( + url="https://example.com", + max_length=100, + start_index=50, + raw=True, + ) + assert args.max_length == 100 + assert args.start_index == 50 + assert args.raw is True + + def test_start_index_zero_preserved(self): + """Regression: `or`-coalescing would incorrectly fall through 0.""" + args = Fetch(url="https://example.com", start_index=0) + assert args.start_index == 0 + + def test_raw_false_preserved(self): + """Regression: explicit False must stay False.""" + args = Fetch(url="https://example.com", raw=False) + assert args.raw is False + + def test_validators_still_apply(self): + """gt=0 on max_length must still reject invalid values after null handling.""" + with pytest.raises(ValueError): + Fetch(url="https://example.com", max_length=0) + with pytest.raises(ValueError): + Fetch(url="https://example.com", start_index=-1) + + class TestGetRobotsTxtUrl: """Tests for get_robots_txt_url function."""