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
12 changes: 12 additions & 0 deletions scripts/gen_surface_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@
{"$ref": "#/$defs/PrimitiveSchemaDefinition"},
{},
),
# JSON Schema 2020-12 allows a boolean wherever a sub-schema is expected, and
# 2026-07-28 already leaves these free-form; accept `true`/`false` property schemas.
(
"$defs/Tool/properties/inputSchema/properties/properties/additionalProperties",
{"additionalProperties": True, "properties": {}, "type": "object"},
{"anyOf": [{"additionalProperties": True, "properties": {}, "type": "object"}, {"type": "boolean"}]},
),
(
"$defs/Tool/properties/outputSchema/properties/properties/additionalProperties",
{"additionalProperties": True, "properties": {}, "type": "object"},
{"anyOf": [{"additionalProperties": True, "properties": {}, "type": "object"}, {"type": "boolean"}]},
),
],
"2026-07-28": [
("$defs/NumberSchema/properties/default/type", "number", ["integer", "number"]),
Expand Down
4 changes: 2 additions & 2 deletions src/mcp-types/mcp_types/_v2025_11_25/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@
extra="allow",
)
schema_: Annotated[str | None, Field(alias="$schema")] = None
properties: dict[str, dict[str, Any]] | None = None
properties: dict[str, dict[str, Any] | bool] | None = None

Check warning on line 1361 in src/mcp-types/mcp_types/_v2025_11_25/__init__.py

View check run for this annotation

Claude / Claude Code Review

nit: `dict[str, Any] | bool` under pydantic smart union lax-coerces bool-ish scalars (1, 0, "true", "yes", "on", 1.0) into booleans, silently rewriting invalid property sub-schemas instead of rejecting them (same union at line 1379 in OutputSchema; WireMo

nit: `dict[str, Any] | bool` under pydantic smart union lax-coerces bool-ish scalars (1, 0, "true", "yes", "on", 1.0) into booleans, silently rewriting invalid property sub-schemas instead of rejecting them (same union at line 1379 in OutputSchema; WireModel is not strict-mode)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a properties value is "false" or 0, Pydantic's non-strict bool arm coerces it and accepts an invalid JSON Schema sub-schema. Generate this union with a strict boolean type, such as StrictBool or a strict boolean literal, in both schemas so only true and false are admitted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp-types/mcp_types/_v2025_11_25/__init__.py, line 1361:

<comment>When a `properties` value is `"false"` or `0`, Pydantic's non-strict `bool` arm coerces it and accepts an invalid JSON Schema sub-schema. Generate this union with a strict boolean type, such as `StrictBool` or a strict boolean literal, in both schemas so only `true` and `false` are admitted.</comment>

<file context>
@@ -1358,7 +1358,7 @@ class InputSchema(WireModel):
     )
     schema_: Annotated[str | None, Field(alias="$schema")] = None
-    properties: dict[str, dict[str, Any]] | None = None
+    properties: dict[str, dict[str, Any] | bool] | None = None
     required: list[str] | None = None
     type: Literal["object"]
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: dict[str, Any] | bool under pydantic smart union lax-coerces bool-ish scalars (1, 0, "true", "yes", "on", 1.0) into booleans, silently rewriting invalid property sub-schemas instead of rejecting them (same union at line 1379 in OutputSchema; WireModel is not strict-mode)

Extended reasoning...

On a 2025-11-25 (or earlier legacy) session, a server advertises an invalid tool schema like "properties": {"flag": "true"} or {"flag": 1}. Before this change the ListToolsResult failed validation with a clear dict_type error; after it, the sieve silently coerces the value to boolean true, so the client (or a server forwarding a third-party schema) sees a mutated schema the peer never sent — contradicting the PR's claim that string/scalar property values are still rejected. Fix would be a strict-bool annotation on the bool arm of both unions (via the generator patch).

Verification: nit. The coercion path is real and unguarded. The new field is a plain smart union — properties: dict[str, dict[str, Any] | bool] | None = None at src/mcp-types/mcp_types/_v2025_11_25/init.py:1361 (InputSchema) and :1379 (OutputSchema) — with no strict annotation on the bool member. Nothing in the validation path enables strict mode: `WireModel.model_config = ConfigDict(populate_by_name=True

required: list[str] | None = None
type: Literal["object"]

Expand All @@ -1376,7 +1376,7 @@
extra="allow",
)
schema_: Annotated[str | None, Field(alias="$schema")] = None
properties: dict[str, dict[str, Any]] | None = None
properties: dict[str, dict[str, Any] | bool] | None = None
required: list[str] | None = None
type: Literal["object"]

Expand Down
16 changes: 16 additions & 0 deletions tests/types/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,22 @@ def test_elicit_request_surface_accepts_loose_property_schemas():
assert isinstance(parsed, types.ElicitRequest)


def test_2025_11_25_tool_schema_surfaces_accept_boolean_sub_schemas():
"""JSON Schema 2020-12 allows `true`/`false` wherever a sub-schema is expected; real servers emit them."""
tool = {
"name": "echo",
"inputSchema": {"type": "object", "properties": {"arg": True}},
"outputSchema": {
"type": "object",
"properties": {"result": True, "hidden": False, "rows": {"type": "array", "items": True}},
"required": ["result"],
},
}
sieved = methods.serialize_server_result("tools/list", "2025-11-25", {"tools": [tool]})
assert sieved["tools"][0]["inputSchema"] == tool["inputSchema"]
assert sieved["tools"][0]["outputSchema"] == tool["outputSchema"]


def test_response_map_keys_mirror_the_request_map_keys():
assert set(methods.SERVER_RESULTS) == set(methods.CLIENT_REQUESTS)
assert set(methods.CLIENT_RESULTS) == set(methods.SERVER_REQUESTS)
Expand Down
Loading