fix(mcp): keep inlined body schema containers JSON native

The MCP server validates the raw inputSchema against the JSON Schema metaschema on every tool call, where a tuple is not an array, so tools with a required body field became uncallable.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-08-13 20:30:08 +00:00
parent b387ae2737
commit 7e59527359
2 changed files with 39 additions and 5 deletions

View file

@ -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")

View file

@ -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"]