Merges ~/ck3-knowledge and ~/kcd-knowledge, which were 90% duplicate code and
neither of which had ever been committed anywhere.
Works on any wiki, not just MediaWiki:
- sources.py splits the engine-specific parts (enumerate, fetch) behind a Source
protocol. MediaWikiSource is preferred whenever an api.php exists;
SitemapHtmlSource handles custom engines with no API (verified on Fextralife).
Everything downstream was already source-agnostic.
- scripts/detect_wiki.py probes /api.php, /w/api.php then /sitemap.xml and prints
the config to use. The API path and article URL vary independently — Fandom
serves /api.php but /wiki/{Title}.
- Profiles are plain env files, since config.py was already fully env-driven.
Per-wiki behaviour that must be measured rather than assumed:
- Subpage policy. On the CK3 wiki "A/B" is a mod; on the KCD wiki it is the
sequel, and rejecting subpages deletes 441 pages of wanted content.
- Noise sections. "Other languages" tables were 7% of KCD chunks and matched
English queries by repeating every title in eight languages.
- Variant tagging for wikis covering two editions, with scopes that exclude the
other edition but keep both/unknown pages.
Build performance: length bucketing in embed_efficiently is 2.7x (fastembed pads
each batch to its longest member; 184-char median vs 2000-char max). With noise
sections that is 61 min -> 20 min on a 6,140-page wiki at half the workers.
Fandom portable-infoboxes are transformed to tables before markdownify —
otherwise stat values survive but detach from their labels.
100% branch coverage, ruff clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
#!/usr/bin/env python
|
|
"""Verify the ck3 MCP server end-to-end over real stdio JSON-RPC.
|
|
|
|
DONE = the server starts, completes the MCP handshake, advertises the three
|
|
tools, and returns sensible results for wiki_search and wiki_get_page. Exits
|
|
non-zero on any failure. Uses the same interface Claude Code will use, so this
|
|
confirms the server works without needing a session reload.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
import anyio
|
|
from mcp import ClientSession, StdioServerParameters
|
|
from mcp.client.stdio import stdio_client
|
|
|
|
|
|
def _text(result: object) -> str:
|
|
"""Flatten a CallToolResult into a searchable string."""
|
|
parts: list[str] = []
|
|
structured = getattr(result, "structuredContent", None)
|
|
if structured is not None:
|
|
parts.append(str(structured))
|
|
for block in getattr(result, "content", []) or []:
|
|
if hasattr(block, "text"):
|
|
parts.append(block.text)
|
|
return " ".join(parts)
|
|
|
|
|
|
async def run() -> int:
|
|
params = StdioServerParameters(command=sys.executable, args=["-m", "wikikb.server"])
|
|
ok = True
|
|
async with stdio_client(params) as (read, write), ClientSession(read, write) as session:
|
|
await session.initialize()
|
|
|
|
tools = {t.name for t in (await session.list_tools()).tools}
|
|
want = {"wiki_search", "wiki_get_page", "wiki_list_pages"}
|
|
t_ok = want <= tools
|
|
ok &= t_ok
|
|
print(f"[{'PASS' if t_ok else 'FAIL'}] tools/list -> {sorted(tools)}")
|
|
|
|
search_text = _text(await session.call_tool("wiki_search", {"query": "domain limit rank"}))
|
|
s_ok = (
|
|
any(k in search_text for k in ("Titles", "Attributes"))
|
|
and "domain" in search_text.lower()
|
|
)
|
|
ok &= s_ok
|
|
print(f"[{'PASS' if s_ok else 'FAIL'}] wiki_search -> {search_text[:80]!r}...")
|
|
|
|
page_text = _text(await session.call_tool("wiki_get_page", {"title": "Traits"}))
|
|
g_ok = "Genius" in page_text and "Herculean" in page_text
|
|
ok &= g_ok
|
|
print(
|
|
f"[{'PASS' if g_ok else 'FAIL'}] wiki_get_page('Traits') has Genius+Herculean -> {g_ok}"
|
|
)
|
|
|
|
list_text = _text(await session.call_tool("wiki_list_pages", {"prefix": "Tr"}))
|
|
l_ok = "Traits" in list_text
|
|
ok &= l_ok
|
|
print(f"[{'PASS' if l_ok else 'FAIL'}] wiki_list_pages('Tr') includes Traits -> {l_ok}")
|
|
|
|
print(f"\nMCP VERIFICATION: {'PASS' if ok else 'FAIL'}")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(anyio.run(run))
|