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
13 changes: 9 additions & 4 deletions src/claude_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,15 @@ def create_sdk_mcp_server(
# Pre-compute tool schemas once at creation time
def _build_schema(tool_def: SdkMcpTool[Any]) -> dict[str, Any]:
if isinstance(tool_def.input_schema, dict):
if (
"type" in tool_def.input_schema
and "properties" in tool_def.input_schema
and isinstance(tool_def.input_schema["type"], str)
# A pre-built JSON Schema is identified by a string-valued
# "type" (e.g. "object"/"array"). "properties" is optional in
# JSON Schema (open objects, arrays, and non-object schemas
# legitimately omit it), so it must not be required here.
# A {name: type} param mapping never matches: its values are
# Python types, so isinstance(input_schema["type"], str) is
# False even for a param literally named "type".
if "type" in tool_def.input_schema and isinstance(
tool_def.input_schema["type"], str
):
return tool_def.input_schema
properties = {}
Expand Down
45 changes: 45 additions & 0 deletions tests/test_sdk_mcp_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,51 @@ async def validate(args: dict[str, Any]) -> dict[str, Any]:
schema = response.root.tools[0].inputSchema
assert schema == json_schema

@pytest.mark.anyio
@pytest.mark.parametrize(
"json_schema",
[
{"type": "object"},
{"type": "object", "additionalProperties": True},
{"type": "array", "items": {"type": "string"}},
],
)
async def test_json_schema_without_properties_passthrough(
self, json_schema: dict[str, Any]
) -> None:
"""A valid JSON Schema that omits 'properties' (open object, array, ...)
passes through unchanged instead of being mangled into a param mapping."""

@tool("validate", "Validate input", json_schema)
async def validate(args: dict[str, Any]) -> dict[str, Any]:
return {"content": [{"type": "text", "text": "OK"}]}

server_config = create_sdk_mcp_server(name="no-props-test", tools=[validate])
server = server_config["instance"]
list_handler = server.request_handlers[ListToolsRequest]
response = await list_handler(ListToolsRequest(method="tools/list"))
assert response.root.tools[0].inputSchema == json_schema

@pytest.mark.anyio
async def test_param_mapping_with_param_named_type_still_expands(self) -> None:
"""A {name: type} mapping whose value happens to be keyed 'type' must
still expand: its value is a Python type, not a JSON-Schema string."""

@tool("configure", "Configure", {"type": str, "value": int})
async def configure(args: dict[str, Any]) -> dict[str, Any]:
return {"content": [{"type": "text", "text": "OK"}]}

server_config = create_sdk_mcp_server(name="type-param-test", tools=[configure])
server = server_config["instance"]
list_handler = server.request_handlers[ListToolsRequest]
response = await list_handler(ListToolsRequest(method="tools/list"))

schema = response.root.tools[0].inputSchema
assert schema["type"] == "object"
assert schema["properties"]["type"] == {"type": "string"}
assert schema["properties"]["value"] == {"type": "integer"}
assert set(schema["required"]) == {"type", "value"}

@pytest.mark.anyio
async def test_cached_tool_list_is_stable(self) -> None:
@tool("cached", "Test caching", {"x": str})
Expand Down