Skip to content
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
15 changes: 11 additions & 4 deletions src/mcpadapt/utils/modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ def get_field_type(field_name: str, field_schema: Dict[str, Any], required: set)

types.append(created_models[ref_name])

field_type = types[0] if len(types) == 1 else Union[tuple(types)] # type: ignore
if len(types) == 0:
field_type = Any
elif len(types) == 1:
field_type = types[0]
else:
field_type = Union[tuple(types)] # type: ignore
default = field_schema.get("default")
is_required = field_name in required and default is None

Expand All @@ -158,10 +163,12 @@ def get_field_type(field_name: str, field_schema: Dict[str, Any], required: set)
mapped_type = json_type_mapping.get(t, Any)
types.append(mapped_type)

if len(types) > 1:
field_type = Union[tuple(types)] # type: ignore
if len(types) == 0:
field_type = Any
elif len(types) == 1:
field_type = types[0]
else:
field_type = types[0] if types else Any
field_type = Union[tuple(types)] # type: ignore
else:
# Original code for simple types
field_type = json_type_mapping.get(json_type, Any) # type: ignore
Expand Down
65 changes: 65 additions & 0 deletions tests/test_crewai_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,68 @@ def test_multiple_content_response_handling(multiple_content_server_script):
# Should return string representation of list of text contents
expected = str(["First response", "Second response", "Third response"])
assert result == expected


@pytest.fixture
def empty_union_server_script():
"""Server that returns a tool with a schema containing only null types in anyOf."""
return dedent(
"""
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.stdio import stdio_server
import anyio

app = Server("empty-union-server")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="problematic_tool",
description="Tool with empty union after null filtering",
inputSchema={
"type": "object",
"properties": {
"nullable_only": {
"anyOf": [
{"type": "null"}
]
},
"type_array_null_only": {
"type": ["null"]
}
},
},
)
]

@app.call_tool()
async def call_tool(name: str, arguments: dict | None) -> list[types.TextContent]:
return [types.TextContent(type="text", text="Success")]

async def arun():
async with stdio_server() as streams:
await app.run(
streams[0], streams[1], app.create_initialization_options()
)

anyio.run(arun)
"""
)


def test_empty_union_type_error(empty_union_server_script):
"""Test that empty unions (anyOf with only null types) do not raise TypeError.

This test reproduced issue #71 where schemas with anyOf containing only
null types cause 'Cannot take a Union of no types' error.
"""
with MCPAdapt(
StdioServerParameters(
command="uv",
args=["run", "python", "-c", empty_union_server_script],
),
CrewAIAdapter(),
) as tools:
assert len(tools) == 1