Wormhole · developer reference · v0.8.0

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.

ToolDoes
search_and_fetchThe 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_pagesFetch 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_pagesEvery page shared with you + a one-line summary each, so the agent picks the right page first try.
get_pageFetch one page by slug.
search_pagesKeyword search returning slug + title + snippet (no bodies).
cost_estimateEstimates what enabling semantic search (L2) would cost for this wiki — one-time index + per-changed-page — before you turn it on. Embeds nothing.
lintRead-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.

Level 0

Batch fetch

get_pages pulls many pages in one call instead of one turn per link. A 10-hop crawl becomes 1–2 turns.

Level 1

Search + fetch

search_and_fetch returns the best matches' full content directly — one turn instead of search-then-fetch-each.

Level 2

Semantic index

An optional embedding index beside the wiki (.wh-index.json) ranks by meaning, not keyword. Lazy-built, mtime-cached, .md untouched.

L2 is opt-in and additive: with no embedding key configured, search falls back to keyword ranking. The index is a derived cache — delete it and the wiki still works.

04Who pays for embeddings

Level 2 has two embedding operations, paid by different parties — "whoever thinks, pays". Provider-neutral: pick Gemini, OpenAI, or Voyage.

The host pays

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.

The asker pays

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≈ tokensGeminiOpenAIVoyage
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
Don't guess — ask the server. The 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.

MechanismEffect
public: "*"Token-less mode — anyone with the URL reads the opened pages. No public: block = nothing reachable without a token.
bearer tokenPer-consumer key (new-token.php mints them); maps a token → a name → the pages opened for that name.
deny: globsFilename 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.
_-prefixAny _-prefixed file is never shared (the gate, tokens, drafts).
The gate always runs at the host — the sharer owns what leaves their machine. Secret files (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.

0.8.0Cost estimate. A cost_estimate tool measures the wiki and returns the one-time index cost + per-changed-page cost per provider, before enabling L2.
0.7.0Split embedding cost. Consumers can pass embed_key to pay for their own query embedding; host pays for the index. ?version reports the index provider/model.
0.6.0Provider-neutral L2. The host supplies their own provider + key (gemini / openai / voyage) in wh-config.php. Legacy geminiKey still works.
0.5.0Fast + lint. L0 get_pages, L1 search_and_fetch, L2 semantic index, the lint tool, per-page summaries.
0.1.0Original server: list_shared_pages / search_pages / get_page — keyword, one page at a time.