mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
137 lines
5.3 KiB
Python
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
|