Skip to content

Commit 8b914ac

Browse files
sakthikannan25Sakthi Kannan
andauthored
fix: Add support for list-type JSON Schema fields in modeling.py (#37)
* fix: Add support for list-type JSON Schema fields in modeling.py Problem: - JSON Schema allows specifying multiple types using array notation: {"type": ["string", "number"]} - This is valid per JSON Schema specification, and common in real-world schemas - The mcpadapt modeling.py module failed with "unhashable type: 'list'" when processing such schemas - Error occurs in get_field_type() when attempting to use a list as a dictionary key Solution: - Enhanced get_field_type() to properly handle list-type JSON Schema types - Added special case to detect when json_type is a list - Implemented conversion of list-type to Python Union types - For single-item lists, extract and use the single type - For multi-item lists, create a Union of all mapped types - Preserves original behavior for all other schema types This fix ensures compatibility with JSON Schema that use the array notation for specifying multiple allowed types for a field, which is a common pattern in the JSON Schema ecosystem. The fix is backwards compatible and follows the expected behavior of properly converting JSON Schema types to their Python equivalents. * test: Add dedicated test for JSON Schema list-type handling This commit adds a comprehensive test suite for validating the fix for JSON Schema array notation in type fields (e.g., "type": ["string", "number"]). Key additions: - New test file tests/test_modeling.py with multiple test scenarios: - Direct test against modeling.py to verify handling of list-type JSON Schema fields - Tests for array-type fields with multiple primitive types - Specific tests for handling of null types in array notation - Inspection utility to examine actual schema structure in MCP tools The tests are designed to: 1. Verify the fix works correctly for all edge cases 2. Provide clear diagnostics when the bug is present 3. Demonstrate proper handling of various JSON Schema type patterns 4. Ensure consistent behavior with the existing anyOf implementation The test handles both the "happy path" (with fix) and failure path (without fix), making it valuable for preventing regressions. It also improves null type handling to be consistent with how the codebase already handles nulls in anyOf constructs. This testing approach validates that our implementation correctly supports the JSON Schema specification, which allows multiple types to be specified either via array notation or anyOf constructs. * fix: Address PR #37 review feedback on JSON Schema list-type handling - Extended JSON Schema array notation fix to langchain_adapter.py - Added support for both array notation ("type": ["string", "number"]) and anyOf structures in LangChain adapter - Reorganized tests per reviewer feedback: * Moved simplified direct test to tests/utils/test_modeling.py * Added E2E test to test_langchain_adapter.py * Removed redundant test file This commit ensures consistent handling of JSON Schema list-type fields across all adapters and addresses all feedback from the PR review. --------- Co-authored-by: Sakthi Kannan <saktr@amazon.com>
1 parent 4deaddd commit 8b914ac

File tree

4 files changed

+137
-2
lines changed

4 files changed

+137
-2
lines changed

src/mcpadapt/langchain_adapter.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,38 @@ def _generate_tool_class(
8484
# TODO: this could be better and handle nested objects...
8585
tool_params = []
8686
for k, v in properties.items():
87-
tool_params.append(f"{k}: {JSON_SCHEMA_TO_PYTHON_TYPES[v['type']]}")
87+
# Handle case where 'type' is missing but 'anyOf' is present (for multiple types)
88+
if "type" in v:
89+
if isinstance(v["type"], list):
90+
# Handle list-type (multiple allowed types in JSON Schema)
91+
types = []
92+
for t in v["type"]:
93+
if t != "null": # Exclude null types
94+
types.append(JSON_SCHEMA_TO_PYTHON_TYPES[t])
95+
96+
if len(types) > 1:
97+
python_type = " | ".join(types)
98+
else:
99+
python_type = types[0] if types else "str" # Default to str
100+
else:
101+
python_type = JSON_SCHEMA_TO_PYTHON_TYPES[v["type"]]
102+
elif "anyOf" in v:
103+
# Extract types from anyOf
104+
types = []
105+
for option in v["anyOf"]:
106+
if "type" in option and option["type"] != "null":
107+
types.append(JSON_SCHEMA_TO_PYTHON_TYPES[option["type"]])
108+
109+
if len(types) > 1:
110+
python_type = " | ".join(types)
111+
else:
112+
python_type = types[0] if types else "str" # Default to str
113+
else:
114+
# Default to str if no type information is available
115+
python_type = "str"
116+
117+
tool_params.append(f"{k}: {python_type}")
118+
88119
tool_params = ", ".join(tool_params)
89120

90121
argument = "{" + ", ".join(f"'{k}': {k}" for k in properties.keys()) + "}"

src/mcpadapt/utils/modeling.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,23 @@ def get_field_type(field_name: str, field_schema: Dict[str, Any], required: set)
157157
else:
158158
# Simple types
159159
json_type = field_schema.get("type", "string")
160-
field_type = json_type_mapping.get(json_type, Any) # type: ignore
160+
161+
# Handle list-type (multiple allowed types in JSON Schema)
162+
if isinstance(json_type, list):
163+
# Convert to Union type (consistent with anyOf handling)
164+
types = []
165+
for t in json_type:
166+
if t != "null": # Exclude null types as in anyOf handling
167+
mapped_type = json_type_mapping.get(t, Any)
168+
types.append(mapped_type)
169+
170+
if len(types) > 1:
171+
field_type = Union[tuple(types)] # type: ignore
172+
else:
173+
field_type = types[0] if types else Any
174+
else:
175+
# Original code for simple types
176+
field_type = json_type_mapping.get(json_type, Any) # type: ignore
161177

162178
# Handle optionality and default values
163179
default = field_schema.get("default")

tests/test_langchain_adapter.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@
77
from mcpadapt.langchain_adapter import LangChainAdapter
88

99

10+
@pytest.fixture
11+
def json_schema_array_type_server_script():
12+
"""
13+
Create a server with a tool that uses array notation for type fields.
14+
This tests handling of JSON Schema 'type': ['string', 'number'] syntax.
15+
"""
16+
return dedent(
17+
'''
18+
from mcp.server.fastmcp import FastMCP
19+
20+
mcp = FastMCP("JSON Schema Array Type Test Server")
21+
22+
@mcp.tool()
23+
def multi_type_tool(
24+
id: str | int, # This becomes {"type": ["string", "number"]} in JSON Schema
25+
name: str | None = None, # Tests nullable with array type
26+
) -> str:
27+
"""Test tool with a parameter that accepts multiple types using array notation"""
28+
id_type = type(id).__name__
29+
return f"Received ID: {id} (type: {id_type}), Name: {name}"
30+
31+
mcp.run()
32+
'''
33+
)
34+
35+
1036
@pytest.fixture
1137
def echo_server_script():
1238
return dedent(
@@ -112,6 +138,31 @@ def test_basic_sync_sse(echo_sse_server):
112138
assert len(tools) == 1
113139

114140

141+
def test_json_schema_array_type_handling(json_schema_array_type_server_script):
142+
"""
143+
Test that MCPAdapt correctly handles JSON Schema with array notation for types.
144+
This ensures our fix for 'unhashable type: list' error is working.
145+
"""
146+
with MCPAdapt(
147+
StdioServerParameters(
148+
command="uv",
149+
args=["run", "python", "-c", json_schema_array_type_server_script],
150+
),
151+
LangChainAdapter(),
152+
) as tools:
153+
# Verify the tool was successfully loaded
154+
assert len(tools) == 1
155+
assert tools[0].name == "multi_type_tool"
156+
157+
# Test with string type
158+
result_string = tools[0].invoke({"id": "abc123", "name": "test"})
159+
assert "Received ID: abc123 (type: str)" in result_string
160+
161+
# Test with integer type
162+
result_int = tools[0].invoke({"id": 42, "name": "test"})
163+
assert "Received ID: 42 (type: int)" in result_int
164+
165+
115166
def test_tool_name_with_dashes():
116167
mcp_server_script = dedent(
117168
'''

tests/utils/test_modeling.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Tests for the modeling module, specifically focused on JSON Schema handling.
3+
"""
4+
5+
from mcpadapt.utils.modeling import create_model_from_json_schema
6+
7+
8+
def test_direct_modeling_with_list_type():
9+
"""
10+
Test the modeling module directly with a schema using list-type notation.
11+
This test is specifically designed to verify handling of list-type JSON Schema fields.
12+
"""
13+
# Create a schema with list-type field
14+
schema = {
15+
"type": "object",
16+
"properties": {
17+
"multi_type_field": {
18+
"type": ["string", "number"],
19+
"description": "Field that accepts multiple types",
20+
},
21+
"nullable_field": {
22+
"type": ["string", "null"],
23+
"description": "Field that is nullable",
24+
},
25+
},
26+
}
27+
28+
# Create model from schema - should not raise TypeError
29+
model = create_model_from_json_schema(schema)
30+
31+
# Verify the model works as expected with string
32+
instance = model(multi_type_field="test")
33+
assert instance.multi_type_field == "test"
34+
35+
# Verify the model works as expected with number
36+
instance = model(multi_type_field=42)
37+
assert instance.multi_type_field == 42

0 commit comments

Comments
 (0)