Turn a folder of markdown into an MCP server any agent can reason over.
Wormhole is a single PHP file that serves your wiki as Model Context Protocol tools, behind a per-consumer access gate. Point an agent at the URL; it searches, batches, and reasons over your pages as if they were its own. Your files never move.
Not sure what this is yet? Start with the overview + live demo. This page is the technical reference.
01Quick start
Connect any MCP client to the public Dune Archive demo — token-less, one line:
# public demo — no token needed claude mcp add dune-archive https://www.davidbicho.com/wormhole/dune-demo/mcp.php --transport http
Then ask your agent anything about Dune. It calls the wormhole, reads the linked markdown, and answers.
Done? Disconnect anytime — no commitment. Connecting adds one line to your MCP config; nothing is installed, nothing runs in the background. Remove it just as fast:
# removes the config line — gone, no leftovers claude mcp remove dune-archive
Check any server's version + capabilities without a handshake:
curl https://www.davidbicho.com/wormhole/dune-demo/mcp.php?version # → {"name":"bichovault-wormhole","version":"0.8.0","tools":6, # "embedding":{"provider":"gemini","model":"gemini-embedding-001"}}
02The tools
Six MCP tools. The server serves raw markdown through the gate; the consuming agent does the reasoning — so it stays cheap on the host side.
| Tool | Does |
|---|---|
| search_and_fetch | The primary tool. Ranks the shared pages for a query and returns the top matches' full content in one call — no follow-up fetch. Semantic if L2 is on, else keyword. |
| get_pages | Fetch many pages in one call by slug. Use instead of repeated get_page — collapses the round-trip cascade that makes big wikis slow. |
| list_shared_pages | Every page shared with you + a one-line summary each, so the agent picks the right page first try. |
| get_page | Fetch one page by slug. |
| search_pages | Keyword search returning slug + title + snippet (no bodies). |
| cost_estimate | Estimates what enabling semantic search (L2) would cost for this wiki — one-time index + per-changed-page — before you turn it on. Embeds nothing. |
| lint | Read-only quality report: dead [[links]], near-dupes (via the L2 index), structure (stubs, oversized, orphans, missing titles). Touches nothing. |
03Three levels of speed
The bottleneck on a large wiki isn't the server (~150 ms/call) — it's the round-trip cascade: every tool call is a full LLM turn. The levels cut the number of turns.
Batch fetch
get_pages pulls many pages in one call instead of one turn per link. A 10-hop crawl becomes 1–2 turns.
Search + fetch
search_and_fetch returns the best matches' full content directly — one turn instead of search-then-fetch-each.
Semantic index
An optional embedding index beside the wiki (.wh-index.json) ranks by meaning, not keyword. Lazy-built, mtime-cached, .md untouched.
04Who pays for embeddings
Level 2 has two embedding operations, paid by different parties — "whoever thinks, pays". Provider-neutral: pick Gemini, OpenAI, or Voyage.
Embedding the wiki
Building the index happens once, for every consumer — a cost of running the node (like a mail server's disk). The host's key lives in wh-config.php.
Embedding the query
Each search embeds the query — the asker's own work. A consumer passes embed_key to pay with their own key. Omit it and the host covers it.
// consumer pays for their own query embedding (key = same provider as the index) { "name": "search_and_fetch", "arguments": { "query": "tidsfrist för anmälan", "embed_key": "YOUR-KEY" } }
Query and index must share an embedding space, so the consumer's key must be the same
provider as the index — ?version reports which. Keys are never stored or logged.
What it actually costs
Embeddings are cheap and the index is a one-time build (only changed pages re-embed after that). Indicative one-time cost to index a whole wiki:
| Wiki size | ≈ tokens | Gemini | OpenAI | Voyage |
|---|---|---|---|---|
| 25 pages | ~11k | $0.002 | $0.0002 | $0.0007 |
| 100 pages | ~150k | $0.02 | $0.003 | $0.009 |
| 1,000 pages | ~1.5M | $0.22 | $0.03 | $0.09 |
| 10,000 pages | ~15M | $2.25 | $0.30 | $0.90 |
| 100,000 pages | ~150M | $22 | $3.00 | $9.00 |
cost_estimate tool measures
your wiki and returns the exact one-time index cost + per-changed-page cost, before you turn
L2 on. Pure arithmetic — it embeds nothing.// exact estimate for THIS wiki, before enabling L2 { "name": "cost_estimate", "arguments": {} } // → {"pages":25,"approx_tokens":10762,"one_time_index_usd":0.0016, // "per_changed_page_usd":0.00006, ...}
05The access gate
A manifest (_wormhole-access.md) at the host decides what leaves the machine.
Default-deny. Two modes, layered filters.
| Mechanism | Effect |
|---|---|
public: "*" | Token-less mode — anyone with the URL reads the opened pages. No public: block = nothing reachable without a token. |
| bearer token | Per-consumer key (new-token.php mints them); maps a token → a name → the pages opened for that name. |
deny: globs | Filename excludes: "private*", "*ekonomi*". * matches / — a deny errs toward hiding too much. |
deny-content: | Hides any page whose text mentions a word (e.g. kontonummer) — a content safety net. |
_-prefix | Any _-prefixed file is never shared (the gate, tokens, drafts). |
tokens.php, _wormhole-access.md, wh-config.php)
are web-denied by the shipped .htaccess.06Run your own server
Two ways. Both drop the server beside your wiki — your files are never moved, only read.
Let your agent install it. Paste one line to Claude (or any agent with file + shell access):
"Install the Wormhole server for my markdown wiki — follow https://www.davidbicho.com/wormhole/INSTALL-WITH-CLAUDE.md"
It downloads the server, drops it in your vault, walks you through the gate, tests it,
and hands back a URL. To enable semantic search, drop a wh-config.php with your own
provider + key (see wh-config.example.txt).
// wh-config.php — YOUR provider + YOUR key (you pay for embedding your wiki) return [ 'provider' => 'gemini', 'key' => 'YOUR-API-KEY' ]; // providers: gemini · openai · voyage
07Version history
Every server carries WORMHOLE_VERSION, shown in the file header,
the MCP handshake (serverInfo.version), and GET ?version.
cost_estimate tool measures the wiki and returns the one-time index cost + per-changed-page cost per provider, before enabling L2.embed_key to pay for their own query embedding; host pays for the index. ?version reports the index provider/model.wh-config.php. Legacy geminiKey still works.get_pages, L1 search_and_fetch, L2 semantic index, the lint tool, per-page summaries.list_shared_pages / search_pages / get_page — keyword, one page at a time.