Files
hermes-webui/tests/test_issue697_mcp_tool_inventory.py
T
2026-05-05 01:20:32 +00:00

137 lines
5.3 KiB
Python

"""Regression tests for issue #697 — searchable global MCP tool inventory."""
import json
from unittest.mock import MagicMock, patch
from api.routes import (
_handle_mcp_tools_list,
_mcp_schema_summary,
_mcp_tool_summary,
)
def _make_handler():
h = MagicMock()
h.path = "/api/mcp/tools"
h.command = "GET"
return h
def _json_payload(handler):
body = handler.wfile.write.call_args[0][0]
return json.loads(body.decode("utf-8"))
def _read(relative_path: str) -> str:
from pathlib import Path
return (Path(__file__).resolve().parents[1] / relative_path).read_text(encoding="utf-8")
class TestMcpToolInventoryApi:
@patch("api.routes._mcp_runtime_status_by_name")
@patch("api.routes.get_config")
def test_endpoint_returns_sanitized_registered_mcp_tools(self, mock_cfg, mock_runtime):
mock_cfg.return_value = {
"mcp_servers": {
"web-reader": {"url": "http://localhost:3001/mcp", "headers": {"Authorization": "Bearer secret-token"}},
"disabled": {"command": "disabled-cmd", "enabled": False},
}
}
mock_runtime.return_value = {
"web-reader": {
"connected": True,
"tools": [
{
"name": "mcp_web_reader_fetch_page",
"description": "Fetch a page without leaking Authorization: Bearer secret-token",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to fetch", "default": "https://token.example/?key=secret-token"},
"limit": {"type": "integer", "description": "Maximum bytes"},
},
"required": ["url"],
},
}
],
},
"disabled": {"connected": False, "tools": 0},
}
h = _make_handler()
_handle_mcp_tools_list(h)
payload = _json_payload(h)
assert payload["source"] == "mcp_runtime_status"
assert payload["total"] == 1
assert payload["tools"][0]["name"] == "mcp_web_reader_fetch_page"
assert payload["tools"][0]["server"] == "web-reader"
assert payload["tools"][0]["status"] == "active"
assert payload["tools"][0]["active"] is True
assert payload["tools"][0]["enabled"] is True
assert payload["tools"][0]["schema_summary"] == [
{"name": "url", "type": "string", "required": True, "description": "URL to fetch"},
{"name": "limit", "type": "integer", "required": False, "description": "Maximum bytes"},
]
raw = json.dumps(payload)
assert "secret-token" not in raw
assert "default" not in raw
assert "Authorization" not in raw
def test_schema_summary_uses_parameter_names_types_required_and_descriptions_only(self):
schema = {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search text", "examples": ["secret"]},
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tag filters"},
},
"required": ["query"],
}
assert _mcp_schema_summary(schema) == [
{"name": "query", "type": "string", "required": True, "description": "Search text"},
{"name": "tags", "type": "array", "required": False, "description": "Tag filters"},
]
def test_tool_summary_rejects_non_dict_schema_and_redacts_description(self):
summary = _mcp_tool_summary(
"search",
{"description": "use API_KEY=super-secret", "parameters": "not-a-dict"},
{"name": "search", "status": "configured", "enabled": True, "active": False},
)
assert summary["description"] != "use API_KEY=super-secret"
assert "super-secret" not in summary["description"]
assert summary["schema_summary"] == []
class TestMcpToolInventoryUi:
def test_system_settings_contains_searchable_global_mcp_tool_section(self):
html = _read("static/index.html")
assert 'data-i18n="mcp_tools_title"' in html
assert 'id="mcpToolSearch"' in html
assert 'id="mcpToolList"' in html
assert 'oninput="filterMcpTools()"' in html
def test_panels_js_loads_tools_and_filters_name_server_description(self):
js = _read("static/panels.js")
assert "function loadMcpTools" in js
assert "api('/api/mcp/tools')" in js
assert "function filterMcpTools" in js
assert "_filterMcpToolsForSearch" in js
assert "tool.name" in js
assert "tool.server" in js
assert "tool.description" in js
assert "mcp-tool-empty-state" in js
assert "mcp-tool-error-state" in js
def test_mcp_tool_i18n_keys_are_present(self):
i18n = _read("static/i18n.js")
for key in [
"mcp_tools_title",
"mcp_tools_desc",
"mcp_tools_search_placeholder",
"mcp_tools_no_tools",
"mcp_tools_no_matches",
"mcp_tools_load_failed",
"mcp_tools_schema_empty",
]:
assert key in i18n