From 8ccdaf3876c06bb77c53bc96130cdf5a3e054020 Mon Sep 17 00:00:00 2001 From: Shanto Date: Mon, 12 Oct 2026 16:44:44 +0600 Subject: [PATCH 1/2] Use the declared JSON schema for ClaudeAgent tool inputs --- .../claude/agent_framework_claude/_agent.py | 4 +-- .../claude/tests/test_claude_agent.py | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index fe2983eefdc..b0e1de4e332 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -706,8 +706,8 @@ 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 {} + # Get JSON schema from the pydantic model or the JSON schema the tool was declared with + schema: dict[str, Any] = func_tool.parameters() input_schema: dict[str, Any] = { "type": "object", "properties": schema.get("properties", {}), diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index d8af59a0b92..1bca572199f 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -793,6 +793,31 @@ 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 its parameters.""" + + @tool( + schema={ + "type": "object", + "properties": {"city": {"type": "string", "description": "City name"}}, + "required": ["city"], + } + ) + 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 == { + "type": "object", + "properties": {"city": {"type": "string", "description": "City name"}}, + "required": ["city"], + } + result = await sdk_tool.handler({"city": "Paris"}) + assert result["content"][0]["text"] == "Sunny in Paris" + async def test_tool_handler_success(self) -> None: """Test tool handler executes successfully.""" From dfd9f8003e772d1d652d48801a12754be045ac38 Mon Sep 17 00:00:00 2001 From: Shanto Date: Fri, 16 Oct 2026 16:42:56 +0600 Subject: [PATCH 2/2] Pass the full tool JSON schema to the Claude Agent SDK --- .../claude/agent_framework_claude/_agent.py | 13 +-- .../claude/tests/test_claude_agent.py | 86 ++++++++++++++++--- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index b0e1de4e332..499e1dee974 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -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 the pydantic model or the JSON schema the tool was declared with + # 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] = { - "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"] + input_schema: dict[str, Any] = {**schema, "type": "object", "properties": schema.get("properties") or {}} return SdkMcpTool( name=func_tool.name, diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 1bca572199f..c52940fbc1b 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. +import copy import json from types import SimpleNamespace from typing import Any, cast @@ -794,30 +795,87 @@ def create_person(person: Person) -> str: 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 its parameters.""" + """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={ - "type": "object", - "properties": {"city": {"type": "string", "description": "City name"}}, - "required": ["city"], - } - ) - def get_weather(city: str) -> str: + @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 == { - "type": "object", - "properties": {"city": {"type": "string", "description": "City name"}}, - "required": ["city"], - } + 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."""