llm-wiki-mcp
# llm-wiki-mcp
Serve an LLM-maintained markdown wiki to agents over [MCP](https://modelcontextprotocol.io).
If you keep a knowledge base in the shape Andrej Karpathy
[described](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) —
immutable raw sources, a wiki of markdown pages an LLM maintains, and a schema
doc describing the conventions — this makes it queryable from any MCP client,
without the agent having to walk your directory tree.
```sh
uvx llm-wiki-mcp --wiki ~/my-wiki
```
It is strictly read-only. It never writes to your wiki.
## Why not just point the agent at the folder?
Because an agent given a directory reads the wrong things in the wrong order. It
globs, opens files whole, and burns context rediscovering structure you already
wrote down. This server front-loads the parts that are cheap and decisive: a
curated index, one-line descriptions for every page, a tag vocabulary, and a
best-match lookup that returns one page instead of forty snippets.
It also passes your wiki's own conventions doc through as the MCP server
instructions — so a remote agent that has never opened your repo still follows
your rules about linking, attribution, and what the page types mean.
## Install
```sh
uv tool install llm-wiki-mcp # or: pipx install llm-wiki-mcp
uv tool install 'llm-wiki-mcp[ask]' # plus server-side synthesis
```
Register it with a client — for Claude Code:
```sh
claude mcp add my-wiki -- llm-wiki-mcp --wiki ~/my-wiki
```
Or in `claude_desktop_config.json` / any MCP client config:
```json
{
"mcpServers": {
"my-wiki": {
"command": "llm-wiki-mcp",
"args": ["--wiki", "/absolute/path/to/wiki"]
}
}
}
```
Check what it found before wiring anything up:
```sh
llm-wiki-mcp --wiki ~/my-wiki --info
```
## What it expects
Almost nothing. A directory of markdown files.
| | | |
|--|--|--|
| `index.md` | reserved | Your curated catalog. Served by `get_index`; read first by convention. |
| `log.md` | reserved | Append-only history. Served by `get_log`. |
| `raw/` | optional | Immutable source documents. Excluded from search; reachable via `read_source`. Rename with `--raw-dir`. |
| `CLAUDE.md` | optional | Your conventions doc — served as the MCP instructions. `AGENTS.md`, `.llm-wiki.md`, and `CONVENTIONS.md` also work. |
| `overview.md` | optional | A high-level orientation page. `get_overview` is only registered if it exists. |
| everything else | your pages | Any directory structure you like. |
**Page types are discovered, not configured.** A page's type is its frontmatter
`type` if it declares one, and otherwise the directory it lives in. So
`concepts/`, `people/`, `notes/` — whatever you use — become filters
automatically. Filters tolerate singular and plural (`--page_type concept` finds
`concepts/`).
Frontmatter is plain YAML. Only `type` and `title` really matter; `description`
and `tags` are what make the cheap tools useful:
```yaml
---
title: Grounding Data
description: Structured facts a publisher exposes to agents, rather than prose
type: concept
tags: [structured-news, publishing]
---
```
`description` is load-bearing. It's what `list_pages` shows and what `find_page`
ranks on, so a page without one is half-invisible. Keep it to one line.
## Tools
| Tool | What it's for |
|--|--|
| `get_index` | The curated catalog. Read first. |
| `find_page(topic, …)` | The single best page, in full. *"What does the wiki say about X?"* |
| `search_wiki(query, …)` | Every mention across the corpus. *"Where is X discussed?"* |
| `read_page(name)` | A page by slug, path, or `[[wikilink]]`. |
| `list_pages(page_type?, tags?, match?)` | Pages with descriptions; filter by type and tags (`and`/`or`). |
| `list_types()` | The page types in use, with counts. |
| `list_tags(page_type?)` | The tag vocabulary with counts. Read before filtering by tag. |
| `get_log(since?, limit?)` | Change history. `get_index` says what the wiki holds; this says how it got there. |
| `read_source(path)` | A raw source document, text only. |
| `validate_wiki(path?, limit?)` | Conformance report. |
| `get_overview()` | Only if `overview.md` exists. |
| `ask(question)` | Only if `ANTHROPIC_API_KEY` is set. |
Tools that would always fail aren't registered at all — a tool that returns "not
configured" costs the client context and invites a wasted call.
### `find_page` vs `search_wiki`
`search_wiki` is ripgrep: every hit, as snippets. `find_page` returns one whole
page, plus the runners-up by name so an agent can pivot.
Ranking weights title and slug — the page's *identity* — far above tags and
description, and length-normalizes them. This matters more than it sounds: a
page about a person almost never repeats their name in its own description, so
without that weighting an entity's own page loses to every source that cites it.
Short query tokens must match exactly (otherwise `the` matches au**the**nticity);
longer ones match by containment in either direction, so plurals find singulars.
## Validation
A wiki written by an agent across many sessions doesn't fail by crashing — it
drifts. A page never makes it into the index. A link's target gets renamed. A
description picks up a colon and stops being valid YAML, so every consumer that
isn't a hand-rolled parser goes blind to it.
```sh
llm-wiki-mcp --wiki ~/my-wiki --validate
llm-wiki-mcp --wiki ~/my-wiki --validate --level error # what a hook should run
```
**Errors** mean *malformed* — broken for any consumer: invalid YAML, frontmatter
opened and never closed, duplicate slugs that make a wikilink ambiguous.
**Warnings** mean *degraded* — still serves, but worse: no type, title,
description or tags; a page missing from the index; an index entry pointing at a
page that doesn't exist; a missing source; a wikilink split across lines by
hard-wrapping. **Notes** are informational: over-long descriptions, and
unresolved `[[red links]]` — aggregated per target with a citation count, so the
top of that list is a ranked backlog of pages worth writing.
Absent metadata is deliberately not an error. This server infers a title from
the filename and a type from the directory, so a page without them still works —
and a checker that fails on what its own server handles fine is a checker people
turn off. A minimal wiki with no index, no log, and bare markdown pages passes a
hook gating on errors.
Exit codes: `0` clean, `1` errors, `2` couldn't run. `--strict` fails on warnings
too. A ready-made git hook is in [`examples/pre-commit`](examples/pre-commit).
Two deliberate choices worth knowing about. Red links are **notes, not
warnings** — in a living wiki most are pages you haven't written yet, and one
finding per mention buries everything else. And a referenced source that is
absent *but gitignored* is a note explaining why, not a warning: keeping large
PDFs out of git is a normal policy, and a check that's mostly false positives
teaches you to ignore the whole category.
## Transports
**stdio** by default — what most MCP clients expect, and the client is the
parent process, so there's nothing to authenticate.
**HTTP** for serving a wiki to agents on other machines:
```sh
LLM_WIKI_TOKEN=$(openssl rand -base64 32) llm-wiki-mcp --http --port 8848
```
Requires a bearer token; refuses to start without one. Install the `http` extra.
DNS-rebinding protection is off by default (the typical client is a CLI agent
behind a token on a trusted network, not a browser) — set
`LLM_WIKI_ALLOWED_HOSTS` to turn on the allowlist.
## The `ask` tool
With `ANTHROPIC_API_KEY` set and the `ask` extra installed, `ask(question)` runs
a turn-capped sub-agent over the same read-only tools and returns an answer
citing `[[wikilinks]]`. The index is pinned into a cached system prefix, so
repeated questions reuse it.
Use it when you want a finished answer rather than raw pages. Everything else
works without it, and without any API key.
## Configuration
Every flag has an environment variable. Flags win.
| Env | Flag | Default |
|--|--|--|
| `LLM_WIKI_ROOT` | `--wiki` | current directory |
| `LLM_WIKI_RAW_DIR` | `--raw-dir` | `raw` |
| `LLM_WIKI_TOKEN` | — | *(required for `--http`)* |
| `LLM_WIKI_HOST` / `LLM_WIKI_PORT` | `--host` / `--port` | `127.0.0.1` / `8848` |
| `LLM_WIKI_OVERVIEW_FILE` | — | `overview.md` |
| `LLM_WIKI_SCHEMA_FILES` | — | `CLAUDE.md,AGENTS.md,.llm-wiki.md,CONVENTIONS.md` |
| `LLM_WIKI_LIST_MAX_DESCRIBED` | — | `250` |
| `LLM_WIKI_LOG_MAX_ENTRIES` | — | `10` |
| `LLM_WIKI_DESCRIPTION_MAX` | — | `400` |
| `ANTHROPIC_API_KEY` | — | unset (`ask` disabled) |
## Relation to OKF
Google Cloud's [Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing)
describes a very similar artifact: markdown plus YAML frontmatter, `type`
required, `index.md` and `log.md` reserved. A Karpathy-format wiki that fills in
`description` is already close to an OKF bundle at the metadata layer. The
notable divergence is links — OKF uses relative markdown links, this expects
`[[wikilinks]]`, which is what Obsidian and most LLM-maintained wikis actually
use. NRK's [okf-mcp](https://github.com/nrkno/okf-mcp) serves OKF bundles and is
worth a look if that's your format.
## Development
```sh
uv sync --all-extras
uv run pytest
```
## License
GNU General Public License v2.0. See [LICENSE](LICENSE).
TDQS
Scored across 11 tools
Every tool has a distinct purpose. get_index provides an overview, search_wiki and find_page are explicitly differentiated for full-text search vs. single-page retrieval, and read_page, list_pages, list_types, list_tags, get_log, read_source, validate_wiki, and ask each serve unique functions without overlap.
All tool names follow a consistent verb_noun pattern (e.g., get_index, search_wiki, find_page, read_page, list_pages, list_types, list_tags, get_log, read_source, validate_wiki). The only minor deviation is 'ask', but it is still a single verb and fits the pattern of action-oriented names.
With 11 tools, the set is well-scoped. Each tool covers a specific aspect of wiki interaction (index, search, retrieval, listing, logging, raw source access, validation, Q&A) without being excessive or sparse.
The tool set is complete for a read-only wiki interface. It provides everything needed to navigate, search, retrieve, validate, and query a wiki corpus. While write tools are absent, the domain inferred from the tool descriptions is clearly read-only, so no gaps exist.