Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/fetch/src/mcp_server_fetch/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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,
Expand Down
62 changes: 62 additions & 0 deletions src/fetch/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
Loading