diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index 38654a18ea..1d1d29bd7f 100644 --- a/tests/tools/test_mcp_tool.py +++ b/tests/tools/test_mcp_tool.py @@ -106,6 +106,18 @@ class TestSchemaConversion: assert schema["parameters"]["type"] == "object" assert schema["parameters"]["properties"] == {} + def test_object_schema_without_properties_gets_normalized(self): + from tools.mcp_tool import _convert_mcp_schema + + mcp_tool = _make_mcp_tool( + name="ask", + description="Ask Crawl4AI", + input_schema={"type": "object"}, + ) + schema = _convert_mcp_schema("crawl4ai", mcp_tool) + + assert schema["parameters"] == {"type": "object", "properties": {}} + def test_tool_name_prefix_format(self): from tools.mcp_tool import _convert_mcp_schema @@ -1893,6 +1905,33 @@ class TestSamplingCallbackText: messages = call_args.kwargs["messages"] assert messages[0] == {"role": "system", "content": "Be helpful"} + def test_server_tools_with_object_schema_are_normalized(self): + """Server-provided tools should gain empty properties for object schemas.""" + fake_client = MagicMock() + fake_client.chat.completions.create.return_value = _make_llm_response() + server_tool = SimpleNamespace( + name="ask", + description="Ask Crawl4AI", + inputSchema={"type": "object"}, + ) + + with patch( + "agent.auxiliary_client.call_llm", + return_value=fake_client.chat.completions.create.return_value, + ) as mock_call: + params = _make_sampling_params(tools=[server_tool]) + asyncio.run(self.handler(None, params)) + + tools = mock_call.call_args.kwargs["tools"] + assert tools == [{ + "type": "function", + "function": { + "name": "ask", + "description": "Ask Crawl4AI", + "parameters": {"type": "object", "properties": {}}, + }, + }] + def test_length_stop_reason(self): """finish_reason='length' maps to stopReason='maxTokens'.""" fake_client = MagicMock() diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index c22b824f35..79482eed56 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -605,7 +605,9 @@ class SamplingHandler: "function": { "name": getattr(t, "name", ""), "description": getattr(t, "description", "") or "", - "parameters": getattr(t, "inputSchema", {}) or {}, + "parameters": _normalize_mcp_input_schema( + getattr(t, "inputSchema", None) + ), }, } for t in server_tools @@ -1213,6 +1215,17 @@ def _make_check_fn(server_name: str): # Discovery & registration # --------------------------------------------------------------------------- +def _normalize_mcp_input_schema(schema: dict | None) -> dict: + """Normalize MCP input schemas for LLM tool-calling compatibility.""" + if not schema: + return {"type": "object", "properties": {}} + + if schema.get("type") == "object" and "properties" not in schema: + return {**schema, "properties": {}} + + return schema + + def _convert_mcp_schema(server_name: str, mcp_tool) -> dict: """Convert an MCP tool listing to the Hermes registry schema format. @@ -1231,10 +1244,7 @@ def _convert_mcp_schema(server_name: str, mcp_tool) -> dict: return { "name": prefixed_name, "description": mcp_tool.description or f"MCP tool {mcp_tool.name} from {server_name}", - "parameters": mcp_tool.inputSchema if mcp_tool.inputSchema else { - "type": "object", - "properties": {}, - }, + "parameters": _normalize_mcp_input_schema(mcp_tool.inputSchema), }