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
15 changes: 5 additions & 10 deletions python/packages/claude/agent_framework_claude/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,16 +706,11 @@ async def handler(args: dict[str, Any]) -> dict[str, Any]:
except Exception as e:
return {"content": [{"type": "text", "text": f"Error: {e}"}]}

# Get JSON schema from pydantic model
schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {}
input_schema: dict[str, Any] = {
"type": "object",
"properties": schema.get("properties", {}),
"required": schema.get("required", []),
}
# Preserve $defs for nested type references (Pydantic uses $defs for nested models)
if "$defs" in schema:
input_schema["$defs"] = schema["$defs"]
# Pass the full JSON schema through (from the pydantic model or the schema the tool was declared with), so
# keywords such as additionalProperties and $defs reach Claude. The SDK only uses a dict as a JSON schema when
# it has a string "type" and "properties", so both are always set.
schema: dict[str, Any] = func_tool.parameters()
input_schema: dict[str, Any] = {**schema, "type": "object", "properties": schema.get("properties") or {}}

return SdkMcpTool(
name=func_tool.name,
Expand Down
83 changes: 83 additions & 0 deletions python/packages/claude/tests/test_claude_agent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.

import copy
import json
from types import SimpleNamespace
from typing import Any, cast
Expand Down Expand Up @@ -793,6 +794,88 @@ def create_person(person: Person) -> str:
# Verify the nested reference exists in properties
assert "person" in input_schema["properties"]

async def test_function_tool_to_sdk_mcp_tool_uses_json_schema(self) -> None:
"""Test that a tool declared with a JSON schema keeps the full schema, including additionalProperties."""
schema: dict[str, Any] = {
"type": "object",
"title": "WeatherInput",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"$ref": "#/$defs/Unit"},
},
"required": ["city"],
"additionalProperties": False,
"$defs": {"Unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}},
}

@tool(schema=schema)
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get the weather."""
return f"Sunny in {city}"

agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(get_weather) # type: ignore[reportPrivateUsage]

assert sdk_tool.input_schema == schema
assert cast(dict[str, Any], sdk_tool.input_schema)["additionalProperties"] is False
result = await sdk_tool.handler({"city": "Paris"})
assert result["content"][0]["text"] == "Sunny in Paris"

def test_function_tool_to_sdk_mcp_tool_keeps_input_model_keywords(self) -> None:
"""Test that top-level keywords of a pydantic input model, such as additionalProperties, are kept."""
from pydantic import BaseModel, ConfigDict

class WeatherInput(BaseModel):
model_config = ConfigDict(extra="forbid")
city: str

@tool(schema=WeatherInput)
def get_weather(city: str) -> str:
"""Get the weather."""
return f"Sunny in {city}"

agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(get_weather) # type: ignore[reportPrivateUsage]

assert sdk_tool.input_schema == WeatherInput.model_json_schema()
assert cast(dict[str, Any], sdk_tool.input_schema)["additionalProperties"] is False

@pytest.mark.parametrize(
("schema", "expected"),
[
(
{"properties": {"city": {"type": "string"}}},
{"type": "object", "properties": {"city": {"type": "string"}}},
),
(
{"type": "object", "additionalProperties": False},
{"type": "object", "properties": {}, "additionalProperties": False},
),
(
{"type": ["object", "null"], "properties": {"city": {"type": "string"}}},
{"type": "object", "properties": {"city": {"type": "string"}}},
),
({"type": "object", "properties": None}, {"type": "object", "properties": {}}),
({}, {"type": "object", "properties": {}}),
],
ids=["no-type", "no-properties", "list-type", "null-properties", "empty"],
)
def test_function_tool_to_sdk_mcp_tool_sets_type_and_properties(
self, schema: dict[str, Any], expected: dict[str, Any]
) -> None:
"""Test that a schema without an object type or properties still reaches the SDK as a JSON schema."""
declared = copy.deepcopy(schema)

@tool(name="lookup", description="Look something up.", schema=schema)
def lookup(**kwargs: Any) -> str:
return "ok"

agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(lookup) # type: ignore[reportPrivateUsage]

assert sdk_tool.input_schema == expected
assert lookup.parameters() == declared

async def test_tool_handler_success(self) -> None:
"""Test tool handler executes successfully."""

Expand Down
Loading