|
11 | 11 | import pydantic_core |
12 | 12 | from pydantic import BaseModel, ConfigDict, Field, PydanticUserError, WithJsonSchema, create_model |
13 | 13 | from pydantic.fields import FieldInfo |
14 | | -from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue, JsonSchemaWarningKind |
15 | | -from pydantic_core import CoreSchema |
| 14 | +from pydantic.json_schema import GenerateJsonSchema, JsonSchemaWarningKind |
16 | 15 | from typing_extensions import is_typeddict |
17 | 16 | from typing_inspection.introspection import ( |
18 | 17 | UNKNOWN, |
|
30 | 29 | logger = get_logger(__name__) |
31 | 30 |
|
32 | 31 |
|
33 | | -class ExternalSchemaRefError(Exception): |
34 | | - """A tool schema contains a `$ref` that is not a same-document reference. |
35 | | -
|
36 | | - Deliberately not a `ValueError`: schema generation treats a `ValueError` as |
37 | | - an unserializable type and degrades gracefully, but an external `$ref` is a |
38 | | - hard error that must surface at tool registration. |
39 | | - """ |
40 | | - |
41 | | - |
42 | 32 | class StrictJsonSchema(GenerateJsonSchema): |
43 | | - """Render tool schemas, raising on pydantic warnings and external `$ref`s. |
| 33 | + """A JSON schema generator that raises exceptions instead of emitting warnings. |
44 | 34 |
|
45 | | - Warnings (e.g. a non-serializable type) become errors so they surface at tool |
46 | | - registration instead of silently producing a degenerate schema. External |
47 | | - `$ref`s, which pydantic never emits itself but a user can inject via |
48 | | - `Field(json_schema_extra=...)`, are an SSRF / fetch-DoS vector and are |
49 | | - rejected for the same reason (SEP-2106). |
50 | | -
|
51 | | - See: https://modelcontextprotocol.io/seps/2106-json-schema-2020-12#security-implications |
| 35 | + This is used to detect non-serializable types during schema generation. |
52 | 36 | """ |
53 | 37 |
|
54 | 38 | def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None: |
| 39 | + # Raise an exception instead of emitting a warning |
55 | 40 | raise ValueError(f"JSON schema warning: {kind} - {detail}") |
56 | 41 |
|
57 | | - def generate(self, schema: CoreSchema, mode: Any = "validation") -> JsonSchemaValue: |
58 | | - json_schema = super().generate(schema, mode) |
59 | | - external = sorted(_find_external_refs(json_schema)) |
60 | | - if external: |
61 | | - raise ExternalSchemaRefError( |
62 | | - f"Tool schema contains external $ref(s) that MUST NOT be dereferenced (SEP-2106): " |
63 | | - f"{', '.join(external)}. Only same-document references (e.g. '#/$defs/Foo') are allowed." |
64 | | - ) |
65 | | - return json_schema |
66 | | - |
67 | | - |
68 | | -def _find_external_refs(node: Any) -> set[str]: |
69 | | - external: set[str] = set() |
70 | | - if isinstance(node, dict): |
71 | | - mapping = cast("dict[str, Any]", node) |
72 | | - ref = mapping.get("$ref") |
73 | | - if isinstance(ref, str) and not ref.startswith("#"): |
74 | | - external.add(ref) |
75 | | - for value in mapping.values(): |
76 | | - external |= _find_external_refs(value) |
77 | | - elif isinstance(node, list): |
78 | | - for item in cast("list[Any]", node): |
79 | | - external |= _find_external_refs(item) |
80 | | - return external |
81 | | - |
82 | 42 |
|
83 | 43 | class ArgModelBase(BaseModel): |
84 | 44 | """A model representing the arguments to a function.""" |
|
0 commit comments