From 6b912e671ae103d3e155b4740e31287a164cf353 Mon Sep 17 00:00:00 2001 From: Jack Yuan Date: Fri, 21 Nov 2025 11:00:04 -0500 Subject: [PATCH 1/4] fix: fix normalized tool property default type incorrect for objects and arrays --- src/strands/tools/tools.py | 3 ++- tests/strands/tools/test_tools.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/strands/tools/tools.py b/src/strands/tools/tools.py index 48b969bc3..b2cc19c4e 100644 --- a/src/strands/tools/tools.py +++ b/src/strands/tools/tools.py @@ -88,7 +88,8 @@ def _normalize_property(prop_name: str, prop_def: Any) -> dict[str, Any]: if "$ref" in normalized_prop: return normalized_prop - normalized_prop.setdefault("type", "string") + if "anyOf" not in normalized_prop: + normalized_prop.setdefault("type", "string") normalized_prop.setdefault("description", f"Property {prop_name}") return normalized_prop diff --git a/tests/strands/tools/test_tools.py b/tests/strands/tools/test_tools.py index 60460f464..e20274523 100644 --- a/tests/strands/tools/test_tools.py +++ b/tests/strands/tools/test_tools.py @@ -509,3 +509,18 @@ async def test_stream(identity_tool, alist): tru_events = await alist(stream) exp_events = [ToolResultEvent(({"tool_use": 1}, 2))] assert tru_events == exp_events + + +def test_normalize_schema_with_anyof(): + """Test that anyOf properties don't get default type.""" + schema = { + "type": "object", + "properties": { + "optional_field": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}]}, + "regular_field": {}, + }, + } + normalized = normalize_schema(schema) + + assert "type" not in normalized["properties"]["optional_field"] + assert normalized["properties"]["regular_field"]["type"] == "string" From a6f0f0bf454c92281daca4534bb9676bbaf5af34 Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 5 Dec 2025 16:24:06 -0500 Subject: [PATCH 2/4] fix: skip default type for anyOf properties in registry validation Apply the same fix from tools._normalize_property to registry.validate_tool_spec. Properties with anyOf (union/optional types) should not get type: 'string' added as it causes models to return string-encoded JSON instead of proper arrays/objects. --- src/strands/tools/registry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strands/tools/registry.py b/src/strands/tools/registry.py index c80b80f64..8c5006e0c 100644 --- a/src/strands/tools/registry.py +++ b/src/strands/tools/registry.py @@ -602,7 +602,8 @@ def validate_tool_spec(self, tool_spec: ToolSpec) -> None: if "$ref" in prop_def: continue - if "type" not in prop_def: + # Skip setting default type for anyOf properties (union/optional types) + if "type" not in prop_def and "anyOf" not in prop_def: prop_def["type"] = "string" if "description" not in prop_def: prop_def["description"] = f"Property {prop_name}" From 8d73922be9ff7ca1cfc27dde39aeac0d9b42679e Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 5 Dec 2025 16:25:46 -0500 Subject: [PATCH 3/4] test: add unit tests for registry anyOf and $ref property handling --- tests/strands/tools/test_registry.py | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/strands/tools/test_registry.py b/tests/strands/tools/test_registry.py index c700016f6..9f4611b29 100644 --- a/tests/strands/tools/test_registry.py +++ b/tests/strands/tools/test_registry.py @@ -389,3 +389,70 @@ async def track_load_tools(*args, **kwargs): # Verify add_consumer was called with the registry ID mock_provider.add_consumer.assert_called_once_with(registry._registry_id) + + +def test_validate_tool_spec_with_anyof_property(): + """Test that validate_tool_spec does not add type: 'string' to anyOf properties. + + This is important for MCP tools that use anyOf for optional/union types like + Optional[List[str]]. Adding type: 'string' causes models to return string-encoded + JSON instead of proper arrays/objects. + """ + tool_spec = { + "name": "test_tool", + "description": "A test tool", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "regular_field": {}, # Should get type: "string" + "anyof_field": { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ] + }, + }, + } + }, + } + + registry = ToolRegistry() + registry.validate_tool_spec(tool_spec) + + props = tool_spec["inputSchema"]["json"]["properties"] + + # Regular field should get default type: "string" + assert props["regular_field"]["type"] == "string" + assert props["regular_field"]["description"] == "Property regular_field" + + # anyOf field should NOT get type: "string" added + assert "type" not in props["anyof_field"], "anyOf property should not have type added" + assert "anyOf" in props["anyof_field"], "anyOf should be preserved" + assert props["anyof_field"]["description"] == "Property anyof_field" + + +def test_validate_tool_spec_with_ref_property(): + """Test that validate_tool_spec does not modify $ref properties.""" + tool_spec = { + "name": "test_tool", + "description": "A test tool", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "ref_field": {"$ref": "#/$defs/SomeType"}, + }, + } + }, + } + + registry = ToolRegistry() + registry.validate_tool_spec(tool_spec) + + props = tool_spec["inputSchema"]["json"]["properties"] + + # $ref field should not be modified + assert props["ref_field"] == {"$ref": "#/$defs/SomeType"} + assert "type" not in props["ref_field"] + assert "description" not in props["ref_field"] From ae32ad5bb0aa40e7559718a4091bd968ecfd7ac5 Mon Sep 17 00:00:00 2001 From: Murat Kaan Meral Date: Fri, 5 Dec 2025 16:36:17 -0500 Subject: [PATCH 4/4] fix: handle all JSON Schema composition keywords (anyOf, oneOf, allOf, not) - Add COMPOSITION_KEYWORDS constant for reusability - Skip default type for properties with any composition keyword - Add comprehensive unit tests for all composition keywords --- src/strands/tools/registry.py | 6 +-- src/strands/tools/tools.py | 7 +++- tests/strands/tools/test_registry.py | 55 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/strands/tools/registry.py b/src/strands/tools/registry.py index 8c5006e0c..b66c28557 100644 --- a/src/strands/tools/registry.py +++ b/src/strands/tools/registry.py @@ -23,7 +23,7 @@ from ..experimental.tools import ToolProvider from ..types.tools import AgentTool, ToolSpec from .loader import load_tool_from_string, load_tools_from_module -from .tools import PythonAgentTool, normalize_schema, normalize_tool_spec +from .tools import COMPOSITION_KEYWORDS, PythonAgentTool, normalize_schema, normalize_tool_spec logger = logging.getLogger(__name__) @@ -602,8 +602,8 @@ def validate_tool_spec(self, tool_spec: ToolSpec) -> None: if "$ref" in prop_def: continue - # Skip setting default type for anyOf properties (union/optional types) - if "type" not in prop_def and "anyOf" not in prop_def: + has_composition = any(kw in prop_def for kw in COMPOSITION_KEYWORDS) + if "type" not in prop_def and not has_composition: prop_def["type"] = "string" if "description" not in prop_def: prop_def["description"] = f"Property {prop_name}" diff --git a/src/strands/tools/tools.py b/src/strands/tools/tools.py index b2cc19c4e..55ff0d8b2 100644 --- a/src/strands/tools/tools.py +++ b/src/strands/tools/tools.py @@ -17,6 +17,10 @@ logger = logging.getLogger(__name__) +# JSON Schema composition keywords that define type constraints. +# Properties with these should not get a default type: "string" added. +COMPOSITION_KEYWORDS = ("anyOf", "oneOf", "allOf", "not") + class InvalidToolUseNameException(Exception): """Exception raised when a tool use has an invalid name.""" @@ -88,7 +92,8 @@ def _normalize_property(prop_name: str, prop_def: Any) -> dict[str, Any]: if "$ref" in normalized_prop: return normalized_prop - if "anyOf" not in normalized_prop: + has_composition = any(kw in normalized_prop for kw in COMPOSITION_KEYWORDS) + if not has_composition: normalized_prop.setdefault("type", "string") normalized_prop.setdefault("description", f"Property {prop_name}") return normalized_prop diff --git a/tests/strands/tools/test_registry.py b/tests/strands/tools/test_registry.py index 9f4611b29..9ae51dcfe 100644 --- a/tests/strands/tools/test_registry.py +++ b/tests/strands/tools/test_registry.py @@ -432,6 +432,61 @@ def test_validate_tool_spec_with_anyof_property(): assert props["anyof_field"]["description"] == "Property anyof_field" +def test_validate_tool_spec_with_composition_keywords(): + """Test that validate_tool_spec does not add type: 'string' to composition keyword properties. + + JSON Schema composition keywords (anyOf, oneOf, allOf, not) define type constraints. + Properties using these should not get a default type added. + """ + tool_spec = { + "name": "test_tool", + "description": "A test tool", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "regular_field": {}, # Should get type: "string" + "oneof_field": { + "oneOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + }, + "allof_field": { + "allOf": [ + {"minimum": 0}, + {"maximum": 100}, + ] + }, + "not_field": {"not": {"type": "null"}}, + }, + } + }, + } + + registry = ToolRegistry() + registry.validate_tool_spec(tool_spec) + + props = tool_spec["inputSchema"]["json"]["properties"] + + # Regular field should get default type: "string" + assert props["regular_field"]["type"] == "string" + + # Composition keyword fields should NOT get type: "string" added + assert "type" not in props["oneof_field"], "oneOf property should not have type added" + assert "oneOf" in props["oneof_field"], "oneOf should be preserved" + + assert "type" not in props["allof_field"], "allOf property should not have type added" + assert "allOf" in props["allof_field"], "allOf should be preserved" + + assert "type" not in props["not_field"], "not property should not have type added" + assert "not" in props["not_field"], "not should be preserved" + + # All should have descriptions + for field in ["oneof_field", "allof_field", "not_field"]: + assert props[field]["description"] == f"Property {field}" + + def test_validate_tool_spec_with_ref_property(): """Test that validate_tool_spec does not modify $ref properties.""" tool_spec = {