diff --git a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py index 9a3abe7a9d3..c8722f9dc5f 100644 --- a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py +++ b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py @@ -266,6 +266,8 @@ def resolve_operation_params( _SCHEMA_REF_PREFIX: Final = "#/components/schemas/" _EMPTY_SCHEMA: Final[Mapping[str, Any]] = MappingProxyType({}) +_EMPTY_JSON_OBJECT: Final[Mapping[str, Any]] = {} # mutable-ok: MCP validates the raw schema; only real dicts pass +_EMPTY_JSON_ARRAY: Final[Sequence[str]] = [] # mutable-ok: MCP validates the raw schema; only real lists pass def _inline_schema_refs( @@ -300,7 +302,8 @@ def _inlined_schema_value( if isinstance(value, Mapping): return _inline_schema_refs(value, component_schemas, seen) if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): - return tuple(_inlined_schema_value(item, component_schemas, seen) for item in value) + # mutable-ok: MCP validates the raw schema against the JSON Schema metaschema, where a tuple is not an array + return [_inlined_schema_value(item, component_schemas, seen) for item in value] return value @@ -368,8 +371,8 @@ def build_input_schema(operation: Mapping[str, Any], components: _OpenAPICompone properties["body"] = { "type": "object", "description": request_body.get("description", "Request body"), - "properties": schema.get("properties", _EMPTY_SCHEMA), - "required": tuple(schema.get("required", ())), + "properties": schema.get("properties", _EMPTY_JSON_OBJECT), + "required": schema.get("required", _EMPTY_JSON_ARRAY), } if request_body.get("required", False): required.append("body") diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py index 1c7aefc0057..cfadf07a1c2 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py @@ -452,7 +452,7 @@ class TestBuildInputSchema: assert set(body["properties"]) == {"resourceType", "name", "namespace"} assert body["properties"]["resourceType"]["description"] == "Type of resource to get" assert body["properties"]["namespace"]["default"] == "default" - assert body["required"] == ("resourceType",) + assert body["required"] == ["resourceType"] assert schema["required"] == ["body"] def test_request_body_nested_refs_are_dereferenced(self): @@ -503,6 +503,37 @@ class TestBuildInputSchema: assert body["properties"]["maybe_inner"]["anyOf"][0]["properties"] == inner_props assert "$ref" not in json.dumps(body) + def test_request_body_schema_uses_json_native_containers(self): + """The MCP server validates the raw inputSchema against the JSON Schema + metaschema on every tool call, so a tuple or a mapping proxy in place of + an array or object makes the tool uncallable. + """ + operation = { + "requestBody": { + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Payload"}}}, + }, + } + components = { + "schemas": { + "Payload": { + "type": "object", + "required": ["mode"], + "properties": { + "mode": {"type": "string", "enum": ["fast", "slow"]}, + "name": {"anyOf": [{"type": "string"}, {"type": "null"}]}, + }, + } + } + } + + schema = build_input_schema(operation, components) + + assert json.loads(json.dumps(schema)) == schema + + empty_body = build_input_schema({"requestBody": {"content": {"application/json": {}}}})["properties"]["body"] + assert isinstance(empty_body["properties"], dict) + assert isinstance(empty_body["required"], list) + def test_recursive_request_body_ref_terminates(self): """A self-referencing schema must not recurse forever.""" operation = { @@ -566,7 +597,7 @@ class TestBuildInputSchema: body = schema["properties"]["body"] assert body["description"] == "The payload" assert body["properties"] == {"name": {"type": "string"}} - assert body["required"] == ("name",) + assert body["required"] == ["name"] assert schema["required"] == ["body"]