Skip to content

fix: Add support for list-type JSON Schema fields in modeling.py #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 1, 2025
Merged
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
33 changes: 32 additions & 1 deletion src/mcpadapt/langchain_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,38 @@ def _generate_tool_class(
# TODO: this could be better and handle nested objects...
tool_params = []
for k, v in properties.items():
tool_params.append(f"{k}: {JSON_SCHEMA_TO_PYTHON_TYPES[v['type']]}")
# Handle case where 'type' is missing but 'anyOf' is present (for multiple types)
if "type" in v:
if isinstance(v["type"], list):
# Handle list-type (multiple allowed types in JSON Schema)
types = []
for t in v["type"]:
if t != "null": # Exclude null types
types.append(JSON_SCHEMA_TO_PYTHON_TYPES[t])

if len(types) > 1:
python_type = " | ".join(types)
else:
python_type = types[0] if types else "str" # Default to str
else:
python_type = JSON_SCHEMA_TO_PYTHON_TYPES[v["type"]]
elif "anyOf" in v:
# Extract types from anyOf
types = []
for option in v["anyOf"]:
if "type" in option and option["type"] != "null":
types.append(JSON_SCHEMA_TO_PYTHON_TYPES[option["type"]])

if len(types) > 1:
python_type = " | ".join(types)
else:
python_type = types[0] if types else "str" # Default to str
else:
# Default to str if no type information is available
python_type = "str"

tool_params.append(f"{k}: {python_type}")

tool_params = ", ".join(tool_params)

argument = "{" + ", ".join(f"'{k}': {k}" for k in properties.keys()) + "}"
Expand Down
18 changes: 17 additions & 1 deletion src/mcpadapt/utils/modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,23 @@ def get_field_type(field_name: str, field_schema: Dict[str, Any], required: set)
else:
# Simple types
json_type = field_schema.get("type", "string")
field_type = json_type_mapping.get(json_type, Any) # type: ignore

# Handle list-type (multiple allowed types in JSON Schema)
if isinstance(json_type, list):
# Convert to Union type (consistent with anyOf handling)
types = []
for t in json_type:
if t != "null": # Exclude null types as in anyOf handling
mapped_type = json_type_mapping.get(t, Any)
types.append(mapped_type)

if len(types) > 1:
field_type = Union[tuple(types)] # type: ignore
else:
field_type = types[0] if types else Any
else:
# Original code for simple types
field_type = json_type_mapping.get(json_type, Any) # type: ignore

# Handle optionality and default values
default = field_schema.get("default")
Expand Down
51 changes: 51 additions & 0 deletions tests/test_langchain_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@
from mcpadapt.langchain_adapter import LangChainAdapter


@pytest.fixture
def json_schema_array_type_server_script():
"""
Create a server with a tool that uses array notation for type fields.
This tests handling of JSON Schema 'type': ['string', 'number'] syntax.
"""
return dedent(
'''
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("JSON Schema Array Type Test Server")

@mcp.tool()
def multi_type_tool(
id: str | int, # This becomes {"type": ["string", "number"]} in JSON Schema
name: str | None = None, # Tests nullable with array type
) -> str:
"""Test tool with a parameter that accepts multiple types using array notation"""
id_type = type(id).__name__
return f"Received ID: {id} (type: {id_type}), Name: {name}"

mcp.run()
'''
)


@pytest.fixture
def echo_server_script():
return dedent(
Expand Down Expand Up @@ -112,6 +138,31 @@ def test_basic_sync_sse(echo_sse_server):
assert len(tools) == 1


def test_json_schema_array_type_handling(json_schema_array_type_server_script):
"""
Test that MCPAdapt correctly handles JSON Schema with array notation for types.
This ensures our fix for 'unhashable type: list' error is working.
"""
with MCPAdapt(
StdioServerParameters(
command="uv",
args=["run", "python", "-c", json_schema_array_type_server_script],
),
LangChainAdapter(),
) as tools:
# Verify the tool was successfully loaded
assert len(tools) == 1
assert tools[0].name == "multi_type_tool"

# Test with string type
result_string = tools[0].invoke({"id": "abc123", "name": "test"})
assert "Received ID: abc123 (type: str)" in result_string

# Test with integer type
result_int = tools[0].invoke({"id": 42, "name": "test"})
assert "Received ID: 42 (type: int)" in result_int


def test_tool_name_with_dashes():
mcp_server_script = dedent(
'''
Expand Down
37 changes: 37 additions & 0 deletions tests/utils/test_modeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Tests for the modeling module, specifically focused on JSON Schema handling.
"""

from mcpadapt.utils.modeling import create_model_from_json_schema


def test_direct_modeling_with_list_type():
"""
Test the modeling module directly with a schema using list-type notation.
This test is specifically designed to verify handling of list-type JSON Schema fields.
"""
# Create a schema with list-type field
schema = {
"type": "object",
"properties": {
"multi_type_field": {
"type": ["string", "number"],
"description": "Field that accepts multiple types",
},
"nullable_field": {
"type": ["string", "null"],
"description": "Field that is nullable",
},
},
}

# Create model from schema - should not raise TypeError
model = create_model_from_json_schema(schema)

# Verify the model works as expected with string
instance = model(multi_type_field="test")
assert instance.multi_type_field == "test"

# Verify the model works as expected with number
instance = model(multi_type_field=42)
assert instance.multi_type_field == 42