diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f493b8c7..48fd2d0c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3721,20 +3721,20 @@ For more help on a command: return has_titles = any(s.get("title") for s in sessions) if has_titles: - print(f"{'Title':<22} {'Preview':<40} {'Last Active':<13} {'ID'}") - print("─" * 100) + print(f"{'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}") + print("─" * 110) else: print(f"{'Preview':<50} {'Last Active':<13} {'Src':<6} {'ID'}") - print("─" * 90) + print("─" * 95) for s in sessions: last_active = _relative_time(s.get("last_active")) preview = s.get("preview", "")[:38] if has_titles else s.get("preview", "")[:48] if has_titles: - title = (s.get("title") or "—")[:20] - sid = s["id"][:20] - print(f"{title:<22} {preview:<40} {last_active:<13} {sid}") + title = (s.get("title") or "—")[:30] + sid = s["id"] + print(f"{title:<32} {preview:<40} {last_active:<13} {sid}") else: - sid = s["id"][:20] + sid = s["id"] print(f"{preview:<50} {last_active:<13} {s['source']:<6} {sid}") elif action == "export": diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index 38654a18..1d1d29bd 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 c22b824f..79482eed 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), }