wrolpi/modules/ai/test/test_mcp_parity.py
Roland 6d48e3cdb0
ai: wrolpi_mcp becomes a thin proxy over /api/ai
Every MCP tool is now one /api/ai call with minimal text rendering;
the formatting, link building, and truncation logic it used to carry
untested lives (tested) in modules/ai.  Tool names are unchanged for
external clients; a source-scan test guards the names and that the
proxy only reaches /api/ai.  New lean blueprint endpoints back the two
tools that had no equivalent: GET archives/{id} (detail + snapshot
history) and GET videos/{id}/comments (paged author lines).  Paged
content is read up to the old 50k-char bound.  get_inventory_items now
takes the inventory slug (inventories are config-only; the old
numeric-id endpoint no longer exists).  This also brings wrolpi_mcp
into version control (venv/ is already gitignored).

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

40 lines
1.7 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: external clients depend on the tool names, and the proxy must only reach the tested
/api/ai blueprint (plus the two whole-API reads that predate it)."""
import re
from wrolpi.vars import PROJECT_DIR
MCP_SERVER = PROJECT_DIR / 'wrolpi_mcp/server.py'
# The tool names external clients (Claude etc.) already use. Renaming one breaks them.
EXPECTED_TOOLS = {
'search', 'search_videos', 'search_archives', 'search_docs',
'search_zim', 'search_default_zims',
'get_video', 'get_video_captions', 'get_video_comments',
'get_archive', 'get_archive_text',
'get_doc', 'get_zim_entry',
'list_collections', 'list_zim_files',
'get_statistics', 'get_inventory', 'get_inventory_items', 'get_status',
}
def test_mcp_tool_names_unchanged():
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'