llm-wiki-mcp
# llm-wiki-mcp
An MCP server for a plain-markdown "LLM wiki" vault — a knowledge base of notes with YAML
frontmatter and Obsidian-style `[[wiki-links]]`, maintained by an LLM librarian. No Obsidian
app or plugin required: the server reads the files directly, so it works headless and
unattended.
## Tools
| Tool | Access | What it does |
|---|---|---|
| `search_wiki(query, limit=20, scope="all")` | read | Every word must appear, in any order; the exact phrase ranks higher. Returns `hits`, `total`, and `scope`; each hit carries `kind` (`wiki`, `raw`, `other`). `scope` narrows to `wiki` or `raw`. |
| `read_note(path)` | read | One note: parsed YAML frontmatter, parse error (if any), and markdown body. |
| `list_recent(n=10)` | read | The n most recently modified notes, newest first (ISO 8601 UTC). |
| `write_note(path, content, mode, dry_run=False)` | gated write | `create` a new `.md` note or `append` to an existing one. **No overwrite mode exists.** |
| `get_links(path)` | read | Outbound `[[wiki-links]]` and backlinks (matched by filename stem, title, or aliases). |
## Prompts
Three prompts turn the vault's conventions into slash commands in any MCP client. Each embeds the
same conventions text that the server sends as instructions and the gardener uses to grade notes
(`src/llm_wiki_mcp/conventions.py`).
| Prompt | Arguments | What it asks the model to do |
|---|---|---|
| `ingest_source` | `path` | Read one immutable `raw/` source and draft a linked wiki page with frontmatter, then create it. |
| `lint_note` | `path` | Check one note against the conventions and give exact fixes, without editing it. |
| `crystallize` | `topic`, `folder="wiki/syntheses"` | Draft a synthesis page from the notes on a topic; preview with `dry_run` before creating it. |
## Safety model
- **Path traversal is blocked.** Every path resolves inside the vault root or the call fails —
absolute paths, `..` segments, and symlink escapes included. Search and listing apply the same
check to every file they visit, so a junction or symlink inside the vault that points outside is
skipped.
- **Writes are append/create only.** There is no overwrite, no delete, and no rename. `create`
refuses existing files; `append` refuses missing ones. `dry_run=true` previews without touching disk.
- **`raw/` is immutable.** The vault's source-material directory rejects all writes.
- **Only `.md` notes are readable or writable.** Files under `.git/`, `.obsidian/`, `.trash/`, and
similar directories are never exposed, by any tool.
- **Malformed notes degrade cleanly.** Broken YAML frontmatter comes back as a
`frontmatter_error` string with the body intact — never a traceback.
- **Links in code don't count.** `[[links]]` inside code fences and inline code are ignored by
the link graph (they're examples, not references).
- **Refusals are messages, not tracebacks.** Every refusal is a tool error whose text tells the model
how to correct the call; unexpected exceptions are masked by the SDK and logged server-side.
## Setup
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
```bash
git clone <this-repo> llm-wiki-mcp
cd llm-wiki-mcp
uv sync
```
Point the server at your vault (the directory that contains `wiki/`):
```bash
# PowerShell
$env:LLM_WIKI_VAULT = "C:\path\to\your\vault"
# bash
export LLM_WIKI_VAULT=/path/to/your/vault
```
Optional: set `LLM_WIKI_LOG_LEVEL` to `DEBUG`, `INFO`, `WARNING`, or `ERROR` (default `INFO`). Logs go
to stderr; stdout is the MCP transport.
Run it standalone (stdio transport):
```bash
uv run llm-wiki-mcp
```
### Register with Claude Code
```bash
claude mcp add llm-wiki -e LLM_WIKI_VAULT="C:\path\to\your\vault" -- uv run --directory "C:\path\to\llm-wiki-mcp" llm-wiki-mcp
```
Then in any Claude Code session: "search the wiki for X", "read wiki/concepts/foo.md",
"append a journal line to wiki/log.md".
## The gardener (scheduled agent)
`llm-wiki-gardener` is a Claude agent (built on `claude-agent-sdk`, installed with the `gardener`
extra) that tends the vault through this MCP server. Its entire tool surface is the five tools
above: no shell, no direct file access. Each pass reads the vault's own conventions note, lints
recent notes against it and the built-in conventions, flags orphans and broken links, proposes
crystallization candidates, and files ONE dated report note at
`wiki/journal/gardener/YYYY-MM-DD.md` through the gated `write_note`. It never edits existing notes.
Safety and cost controls, all enforced in code:
- A `PreToolUse` hook refuses any `write_note` call outside `wiki/journal/gardener/`, whatever the
prompt says. `..` segments and absolute paths are normalized away before the check.
- `permission_mode="dontAsk"`: a tool outside the allowlist is denied instead of waiting on a
prompt nobody will answer. `strict_mcp_config=True`: only this server is loaded. No user
settings are read.
- `--max-budget` (default 5.00 USD) stops a runaway pass; two full passes over about 130 notes measured
1.72 and 2.77 USD on `claude-opus-5`. `--model` picks the model (default `claude-opus-5`).
- One log file per pass under `~/.llm-wiki-gardener/logs/` (`--log-dir`): the assistant's text,
the CLI's stderr, and a summary with outcome, turns, cost, and duration. Exit code 0 only when
the pass succeeded.
```bash
uv sync --extra gardener
uv run llm-wiki-gardener --vault /path/to/vault --dry-run # preview: reads only, writes nothing
uv run llm-wiki-gardener --vault /path/to/vault # one real pass
uv run llm-wiki-gardener --help # --model, --max-budget, --log-dir, --conventions
```
Authentication: the pass runs through the Claude Code CLI, so it uses your Claude login on this
machine. For a context with no interactive login, create a long-lived token with
`claude setup-token` and export it as `CLAUDE_CODE_OAUTH_TOKEN` in the scheduled task's
environment.
To run weekly on Windows, point Task Scheduler at `scripts\run-gardener.cmd`. The wrapper runs the
gardener from a dedicated environment (`%LOCALAPPDATA%\llm-wiki-gardener\venv`, created on first run;
override with `UV_PROJECT_ENVIRONMENT`), so the scheduled job never shares an executable with a running
MCP server and can keep itself in sync with the lockfile. Paths must be absolute: Task Scheduler's
PATH is minimal.
```powershell
schtasks /create /tn llm-wiki-gardener /sc weekly /d SUN /st 21:00 /tr "C:\path\to\llm-wiki-mcp\scripts\run-gardener.cmd --vault C:\path\to\vault"
schtasks /run /tn llm-wiki-gardener # fire it once now, then read the log
```
## Development
```bash
uv run pytest # test suite runs against a synthetic fixture vault (tests/fixtures/vault)
uv run ruff check . # lint
uv run ruff format . # format
```
After changing `__version__`, run `uv sync --reinstall-package llm-wiki-mcp` so the installed metadata
follows; `tests/test_packaging.py` fails until it does.
The suite calls the Python functions directly and, in `tests/test_protocol.py`, drives the server the
way an MCP client does: through the SDK's in-memory transport and once over a real stdio launch.
The fixture vault is fully synthetic — no real notes, names, or content. Keep it that way.
## Design notes
Prior art (what exists, what we adopted, why this was built anyway) is documented in
[`docs/prior-art.md`](docs/prior-art.md). A dated self-review against the project's definition of done
and Claude's connector review criteria is in [`docs/reviews/`](docs/reviews/2026-09-05-review.md).
## License
MIT
TDQS
Scored across 5 tools
The tools are largely distinct: read/search/list_recent cover read operations, write_note covers creation/append, get_links covers graph traversal. However, read_note and get_links both target a single note's content, and could occasionally be confused for retrieving note data, though their outputs differ enough to be clearly separable.
All tool names follow a consistent verb_object pattern (read_note, search_wiki, list_recent, write_note, get_links). Verbs are clear and descriptive, with no mixing of conventions or vague imperative actions like 'process' or 'run'.
5 tools is well-scoped for a wiki/vault MCP server, covering reading, searching, listing, writing, and graph traversal. Each tool serves a distinct purpose without redundancy, and the count is squarely in the ideal range.
The surface covers core note lifecycle well: read, search, write/create/append, and link analysis. Minor gaps exist—there's no update/rename/delete operation, though write_note's gated behavior (explicitly never deleting) suggests this is a deliberate design choice. The immutable source protection and dry_run flag further round out the surface.