diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index da46348ea8..c70d1a5335 100644 --- a/tests/tools/test_mcp_tool.py +++ b/tests/tools/test_mcp_tool.py @@ -120,6 +120,72 @@ class TestSchemaConversion: assert schema["parameters"] == {"type": "object", "properties": {}} + def test_definitions_refs_are_rewritten_to_defs(self): + from tools.mcp_tool import _convert_mcp_schema + + mcp_tool = _make_mcp_tool( + name="submit", + description="Submit a payload", + input_schema={ + "type": "object", + "properties": { + "input": {"$ref": "#/definitions/Payload"}, + }, + "required": ["input"], + "definitions": { + "Payload": { + "type": "object", + "properties": { + "query": {"type": "string"}, + }, + "required": ["query"], + } + }, + }, + ) + + schema = _convert_mcp_schema("forms", mcp_tool) + + assert schema["parameters"]["properties"]["input"]["$ref"] == "#/$defs/Payload" + assert "$defs" in schema["parameters"] + assert "definitions" not in schema["parameters"] + + def test_nested_definition_refs_are_rewritten_recursively(self): + from tools.mcp_tool import _convert_mcp_schema + + mcp_tool = _make_mcp_tool( + name="nested", + description="Nested schema", + input_schema={ + "type": "object", + "properties": { + "items": { + "type": "array", + "items": {"$ref": "#/definitions/Entry"}, + }, + }, + "definitions": { + "Entry": { + "type": "object", + "properties": { + "child": {"$ref": "#/definitions/Child"}, + }, + }, + "Child": { + "type": "object", + "properties": { + "value": {"type": "string"}, + }, + }, + }, + }, + ) + + schema = _convert_mcp_schema("forms", mcp_tool) + + assert schema["parameters"]["properties"]["items"]["items"]["$ref"] == "#/$defs/Entry" + assert schema["parameters"]["$defs"]["Entry"]["properties"]["child"]["$ref"] == "#/$defs/Child" + def test_tool_name_prefix_format(self): from tools.mcp_tool import _convert_mcp_schema diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index efef5ea91a..58bd6cd112 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -2019,14 +2019,37 @@ def _make_check_fn(server_name: str): # --------------------------------------------------------------------------- def _normalize_mcp_input_schema(schema: dict | None) -> dict: - """Normalize MCP input schemas for LLM tool-calling compatibility.""" + """Normalize MCP input schemas for LLM tool-calling compatibility. + + MCP servers can emit plain JSON Schema with ``definitions`` / + ``#/definitions/...`` references. Kimi / Moonshot rejects that form and + requires local refs to point into ``#/$defs/...`` instead. Normalize the + common draft-07 shape here so MCP tool schemas remain portable across + OpenAI-compatible providers. + """ if not schema: return {"type": "object", "properties": {}} - if schema.get("type") == "object" and "properties" not in schema: - return {**schema, "properties": {}} + def _rewrite_local_refs(node): + if isinstance(node, dict): + normalized = {} + for key, value in node.items(): + out_key = "$defs" if key == "definitions" else key + normalized[out_key] = _rewrite_local_refs(value) + ref = normalized.get("$ref") + if isinstance(ref, str) and ref.startswith("#/definitions/"): + normalized["$ref"] = "#/$defs/" + ref[len("#/definitions/"):] + return normalized + if isinstance(node, list): + return [_rewrite_local_refs(item) for item in node] + return node - return schema + normalized = _rewrite_local_refs(schema) + + if normalized.get("type") == "object" and "properties" not in normalized: + return {**normalized, "properties": {}} + + return normalized def sanitize_mcp_name_component(value: str) -> str: