wrolpi/modules/ai/test/test_mcp_parity.py
Roland c6d2ec783e
ai: wrolpi_mcp exposes only the compact tool set
The per-kind MCP wrappers (search/search_videos/search_archives/
search_docs, get_video/get_archive/get_doc, get_video_captions/
get_video_comments/get_archive_text) duplicated search_files, get_file,
and read_content and cost an external client ten redundant definitions
on every request.  They are gone.  search_default_zims folds into
search_zim (zim_id optional), get_inventory_items into get_inventory
(slug optional), and read_file is exposed for the first time.  16 tools,
matching what the built-in assistant sees plus the read-only listings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 20:10:18 -06:00

39 lines
1.8 KiB
Python

"""Guards for the wrolpi_mcp thin proxy.
The MCP server's venv (with the `mcp` package) is separate from the API's, so these are source
scans: the tool set is deliberate (no per-kind duplicates of the generic tools), and the proxy
must only reach the tested /api/ai blueprint (plus the whole-API statistics read)."""
import re
from wrolpi.vars import PROJECT_DIR
MCP_SERVER = PROJECT_DIR / 'wrolpi_mcp/server.py'
# The MCP mirrors the compact catalog the built-in assistant uses, plus the read-only listings
# (collections, zims, tags, downloads, map, statistics, status) that only external clients need.
EXPECTED_TOOLS = {
'search_files', 'get_file', 'read_content',
'search_zim', 'get_zim_entry', 'list_zim_files',
'list_collections', 'list_tags', 'list_files', 'read_file',
'list_downloads', 'get_map_overview', 'search_places',
'get_statistics', 'get_inventory', 'get_status',
}
def test_mcp_tool_set():
text = MCP_SERVER.read_text()
tools = set(re.findall(r'@mcp\.tool[^\n]*\)\nasync def (\w+)\(', text))
assert tools == EXPECTED_TOOLS
def test_mcp_is_a_thin_proxy():
"""The MCP calls only /api/ai (and the raw statistics read); the formatting layer is gone."""
text = MCP_SERVER.read_text()
endpoints = set(re.findall(r'["\'](/api/[a-z_/{}]+)', text))
allowed_non_ai = {'/api/statistics'}
offenders = {e for e in endpoints if not e.startswith('/api/ai') and e not in allowed_non_ai}
assert not offenders, f'MCP tools must proxy /api/ai, found: {sorted(offenders)}'
# The formatting/truncation layer moved into modules/ai; it must not come back here.
for name in ('_wrolpi_link', '_fmt_file_group', 'html_to_text', 'BeautifulSoup'):
assert name not in text, f'{name} belongs in modules/ai, not the MCP proxy'