Skip to content

Commit 9b8d0aa

Browse files
committed
Inline the tool schema generator into func_metadata
func_metadata already defined StrictJsonSchema; keep the SEP-2106 external-$ref rejection there instead of a separate _schema_generator module.
1 parent b0786d1 commit 9b8d0aa

6 files changed

Lines changed: 81 additions & 108 deletions

File tree

src/mcp/server/mcpserver/tools/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
from pydantic import BaseModel, Field
88

99
from mcp.server.mcpserver.exceptions import ToolError
10-
from mcp.server.mcpserver.utilities._schema_generator import StrictJsonSchema
1110
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
12-
from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, func_metadata
11+
from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, StrictJsonSchema, func_metadata
1312
from mcp.shared._callable_inspection import is_async_callable
1413
from mcp.shared.exceptions import UrlElicitationRequiredError
1514
from mcp.shared.tool_name_validation import validate_and_warn_tool_name

src/mcp/server/mcpserver/utilities/_schema_generator.py

Lines changed: 0 additions & 63 deletions
This file was deleted.

src/mcp/server/mcpserver/utilities/func_metadata.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import pydantic_core
1212
from pydantic import BaseModel, ConfigDict, Field, PydanticUserError, WithJsonSchema, create_model
1313
from pydantic.fields import FieldInfo
14+
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue, JsonSchemaWarningKind
15+
from pydantic_core import CoreSchema
1416
from typing_extensions import is_typeddict
1517
from typing_inspection.introspection import (
1618
UNKNOWN,
@@ -21,14 +23,58 @@
2123
)
2224

2325
from mcp.server.mcpserver.exceptions import InvalidSignature
24-
from mcp.server.mcpserver.utilities._schema_generator import ExternalSchemaRefError, StrictJsonSchema
2526
from mcp.server.mcpserver.utilities.logging import get_logger
2627
from mcp.server.mcpserver.utilities.types import Audio, Image
2728
from mcp.types import CallToolResult, ContentBlock, TextContent
2829

2930
logger = get_logger(__name__)
3031

3132

33+
class ExternalSchemaRefError(ValueError):
34+
"""A tool schema contains a `$ref` that is not a same-document reference."""
35+
36+
37+
class StrictJsonSchema(GenerateJsonSchema):
38+
"""Render tool schemas, raising on pydantic warnings and external `$ref`s.
39+
40+
Warnings (e.g. a non-serializable type) become errors so they surface at tool
41+
registration instead of silently producing a degenerate schema. External
42+
`$ref`s -- which pydantic never emits itself, but a user can inject via
43+
`Field(json_schema_extra=...)` -- are an SSRF / fetch-DoS vector and are
44+
rejected for the same reason (SEP-2106).
45+
46+
See: https://modelcontextprotocol.io/seps/2106-json-schema-2020-12#security-implications
47+
"""
48+
49+
def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:
50+
raise ValueError(f"JSON schema warning: {kind} - {detail}")
51+
52+
def generate(self, schema: CoreSchema, mode: Any = "validation") -> JsonSchemaValue:
53+
json_schema = super().generate(schema, mode)
54+
external = sorted(_find_external_refs(json_schema))
55+
if external:
56+
raise ExternalSchemaRefError(
57+
f"Tool schema contains external $ref(s) that MUST NOT be dereferenced (SEP-2106): "
58+
f"{', '.join(external)}. Only same-document references (e.g. '#/$defs/Foo') are allowed."
59+
)
60+
return json_schema
61+
62+
63+
def _find_external_refs(node: Any) -> set[str]:
64+
external: set[str] = set()
65+
if isinstance(node, dict):
66+
mapping = cast("dict[str, Any]", node)
67+
ref = mapping.get("$ref")
68+
if isinstance(ref, str) and not ref.startswith("#"):
69+
external.add(ref)
70+
for value in mapping.values():
71+
external |= _find_external_refs(value)
72+
elif isinstance(node, list):
73+
for item in cast("list[Any]", node):
74+
external |= _find_external_refs(item)
75+
return external
76+
77+
3278
class ArgModelBase(BaseModel):
3379
"""A model representing the arguments to a function."""
3480

tests/server/mcpserver/test_func_metadata.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pydantic import BaseModel, Field
1414

1515
from mcp.server.mcpserver.exceptions import InvalidSignature
16-
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
16+
from mcp.server.mcpserver.utilities.func_metadata import ExternalSchemaRefError, StrictJsonSchema, func_metadata
1717
from mcp.types import CallToolResult
1818

1919

@@ -1191,3 +1191,34 @@ def func_with_metadata() -> Annotated[int, Field(gt=1)]: ... # pragma: no branc
11911191

11921192
assert meta.output_schema is not None
11931193
assert meta.output_schema["properties"]["result"] == {"exclusiveMinimum": 1, "title": "Result", "type": "integer"}
1194+
1195+
1196+
def test_strict_json_schema_allows_same_document_refs():
1197+
class Inner(BaseModel):
1198+
x: int
1199+
1200+
class Model(BaseModel):
1201+
inner: Inner
1202+
1203+
schema = Model.model_json_schema(schema_generator=StrictJsonSchema)
1204+
assert "$defs" in schema
1205+
assert schema["properties"]["inner"]["$ref"] == "#/$defs/Inner"
1206+
1207+
1208+
def test_strict_json_schema_rejects_external_ref_in_property():
1209+
class Model(BaseModel):
1210+
profile: Annotated[dict[str, Any], Field(json_schema_extra={"$ref": "https://evil.example/s.json"})]
1211+
1212+
with pytest.raises(ExternalSchemaRefError, match="https://evil.example/s.json"):
1213+
Model.model_json_schema(schema_generator=StrictJsonSchema)
1214+
1215+
1216+
def test_strict_json_schema_rejects_external_ref_nested_in_list():
1217+
class Model(BaseModel):
1218+
items: Annotated[
1219+
list[str],
1220+
Field(json_schema_extra={"prefixItems": [{"$ref": "https://evil.example/a.json"}]}),
1221+
]
1222+
1223+
with pytest.raises(ExternalSchemaRefError, match="https://evil.example/a.json"):
1224+
Model.model_json_schema(schema_generator=StrictJsonSchema)

tests/server/mcpserver/test_schema_generator.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

tests/server/mcpserver/test_tool_manager.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
from mcp.server.mcpserver import Context, MCPServer
1111
from mcp.server.mcpserver.exceptions import ToolError
1212
from mcp.server.mcpserver.tools import Tool, ToolManager
13-
from mcp.server.mcpserver.utilities._schema_generator import ExternalSchemaRefError
14-
from mcp.server.mcpserver.utilities.func_metadata import ArgModelBase, FuncMetadata
13+
from mcp.server.mcpserver.utilities.func_metadata import ArgModelBase, ExternalSchemaRefError, FuncMetadata
1514
from mcp.types import CallToolResult, TextContent, ToolAnnotations
1615

1716

0 commit comments

Comments
 (0)