Skip to content

Commit 9a9f64e

Browse files
committed
fix: pass through JSON Schemas without a "properties" key in create_sdk_mcp_server
tool()/create_sdk_mcp_server document that input_schema may be "A JSON Schema dictionary for full validation", and _build_schema has a passthrough branch for it. But it only recognized a dict as a pre-built JSON Schema when it contained BOTH "type" AND "properties". A valid JSON Schema that omits "properties" - e.g. {"type": "object"}, an open object {"type": "object", "additionalProperties": true}, or an array schema {"type": "array", "items": {...}} - failed that test and was misinterpreted as a {param_name: python_type} mapping, turning its schema keywords into fake required string parameters. The model then saw a corrupted tool schema. Detect a JSON Schema by a string-valued "type" alone. "properties" is optional in JSON Schema, and a {name: type} mapping never matches because its values are Python types (isinstance(input_schema["type"], str) is False even for a param literally named "type"). Adds regression tests for the passthrough cases and a guard test for the "param named type" edge case.
1 parent 5513b20 commit 9a9f64e

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

src/claude_agent_sdk/__init__.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,15 @@ def create_sdk_mcp_server(
402402
# Pre-compute tool schemas once at creation time
403403
def _build_schema(tool_def: SdkMcpTool[Any]) -> dict[str, Any]:
404404
if isinstance(tool_def.input_schema, dict):
405-
if (
406-
"type" in tool_def.input_schema
407-
and "properties" in tool_def.input_schema
408-
and isinstance(tool_def.input_schema["type"], str)
405+
# A pre-built JSON Schema is identified by a string-valued
406+
# "type" (e.g. "object"/"array"). "properties" is optional in
407+
# JSON Schema (open objects, arrays, and non-object schemas
408+
# legitimately omit it), so it must not be required here.
409+
# A {name: type} param mapping never matches: its values are
410+
# Python types, so isinstance(input_schema["type"], str) is
411+
# False even for a param literally named "type".
412+
if "type" in tool_def.input_schema and isinstance(
413+
tool_def.input_schema["type"], str
409414
):
410415
return tool_def.input_schema
411416
properties = {}

tests/test_sdk_mcp_integration.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,51 @@ async def validate(args: dict[str, Any]) -> dict[str, Any]:
10771077
schema = response.root.tools[0].inputSchema
10781078
assert schema == json_schema
10791079

1080+
@pytest.mark.anyio
1081+
@pytest.mark.parametrize(
1082+
"json_schema",
1083+
[
1084+
{"type": "object"},
1085+
{"type": "object", "additionalProperties": True},
1086+
{"type": "array", "items": {"type": "string"}},
1087+
],
1088+
)
1089+
async def test_json_schema_without_properties_passthrough(
1090+
self, json_schema: dict[str, Any]
1091+
) -> None:
1092+
"""A valid JSON Schema that omits 'properties' (open object, array, ...)
1093+
passes through unchanged instead of being mangled into a param mapping."""
1094+
1095+
@tool("validate", "Validate input", json_schema)
1096+
async def validate(args: dict[str, Any]) -> dict[str, Any]:
1097+
return {"content": [{"type": "text", "text": "OK"}]}
1098+
1099+
server_config = create_sdk_mcp_server(name="no-props-test", tools=[validate])
1100+
server = server_config["instance"]
1101+
list_handler = server.request_handlers[ListToolsRequest]
1102+
response = await list_handler(ListToolsRequest(method="tools/list"))
1103+
assert response.root.tools[0].inputSchema == json_schema
1104+
1105+
@pytest.mark.anyio
1106+
async def test_param_mapping_with_param_named_type_still_expands(self) -> None:
1107+
"""A {name: type} mapping whose value happens to be keyed 'type' must
1108+
still expand: its value is a Python type, not a JSON-Schema string."""
1109+
1110+
@tool("configure", "Configure", {"type": str, "value": int})
1111+
async def configure(args: dict[str, Any]) -> dict[str, Any]:
1112+
return {"content": [{"type": "text", "text": "OK"}]}
1113+
1114+
server_config = create_sdk_mcp_server(name="type-param-test", tools=[configure])
1115+
server = server_config["instance"]
1116+
list_handler = server.request_handlers[ListToolsRequest]
1117+
response = await list_handler(ListToolsRequest(method="tools/list"))
1118+
1119+
schema = response.root.tools[0].inputSchema
1120+
assert schema["type"] == "object"
1121+
assert schema["properties"]["type"] == {"type": "string"}
1122+
assert schema["properties"]["value"] == {"type": "integer"}
1123+
assert set(schema["required"]) == {"type", "value"}
1124+
10801125
@pytest.mark.anyio
10811126
async def test_cached_tool_list_is_stable(self) -> None:
10821127
@tool("cached", "Test caching", {"x": str})

0 commit comments

Comments
 (0)