Skip to main content
Glama
jhonnold

ai-wiki-mcp

by jhonnold
README.md
# ai-wiki-mcp

A small, wiki-aware MCP server over the `ai-wiki/` markdown knowledge base. It
replaces the Obsidian + `cyanheads/obsidian-mcp-server` stack: agents (Claude
Code, Hermes, Open WebUI) operate the wiki over a single streamable-HTTP MCP
endpoint, while the files stay plain markdown on disk.

Why a purpose-built server instead of a generic filesystem MCP:

- **Structured search** — recovers the frontmatter querying Obsidian's dataview
  gave us (lost on leaving Obsidian).
- **Schema-validated writes** — frontmatter crosses the wire as JSON and is
  emitted as real YAML; the old `"['a','b']"` array-mangling bug is impossible by
  construction.
- **Server-side lint** — one `wiki_lint` call runs every structural check instead
  of dozens of agent round-trips.

## Tools

| Tool | Purpose |
|------|---------|
| `wiki_list` | directory snapshot of notes (optional frontmatter) |
| `wiki_read` | read a note; `full` \| `map` \| `section` projections |
| `wiki_search` | full-text (ripgrep) and/or structured frontmatter filter |
| `wiki_write` | create/overwrite a note with validated, structured frontmatter |
| `wiki_edit` | surgical body edit (`replace_section`/`append`/`replace_text`); frontmatter untouched |
| `wiki_set_frontmatter` | structured frontmatter mutation; taxonomy-checked tags |
| `wiki_lint` | all structural checks server-side; pure read |
| `wiki_archive` | move a page to `_archive/`; report inbound links to fix |

### Structured search filter DSL

```
{"type": "concept"}                  # scalar equality
{"confidence": ["high", "medium"]}   # scalar membership
{"tags.contains": "domain/ai"}       # list contains
{"tags.contains_any": [...]}         # list intersects
{"tags.contains_all": [...]}         # list superset
{"missing": ["sources"]}             # keys absent
{"present": ["contested"]}           # keys present
```

## Schema

Validation is driven by `<wiki>/.schema.yaml` (see `.schema.yaml.example`), the
machine-readable source of truth for frontmatter rules and the tag taxonomy. The
human-readable `ai-wiki/SCHEMA.md` is kept in sync (a `wiki_lint` drift check
guards this). The file is stat-reloaded on change, so adding a domain takes effect
without a restart. With no `.schema.yaml`, validation degrades to warn-not-block.

## Configuration

| Env | Default | Meaning |
|-----|---------|---------|
| `WIKI_ROOT` | `/wiki` | wiki tree root (bind-mounted) |
| `AI_WIKI_MCP_HOST` | `0.0.0.0` | bind host |
| `AI_WIKI_MCP_PORT` | `3010` | port; endpoint path is `/mcp` |

## Metrics

Prometheus metrics are served unauthenticated at `GET /metrics` on the same
`AI_WIKI_MCP_PORT` as `/mcp` (no extra port to expose). They cover per-tool call
counts/latency/errors plus scrape-time wiki state — file count and size by layer,
wikilink totals, broken/ambiguous/orphan links, and lint findings by severity. All
metric names are prefixed `ai_wiki_mcp_`. The wiki gauges are recomputed on each
scrape, so keep the scrape interval at 15s or longer.

## Web UI

A read-only viewer is served at `GET /app` on the same `AI_WIKI_MCP_PORT` as `/mcp`
(no extra port to expose; `/` redirects to `/app/`). It gives a navigable file tree, a
force-directed link graph (nodes sized by wikilink degree, broken/orphan markers), search
(client-side fuzzy jump + server full-text and the frontmatter filter DSL), click-through
`[[wikilink]]` navigation, and markdown rendering with a render⇄source toggle. It is a
no-build vanilla-JS SPA reading a small JSON API (`/app/api/{tree,page,graph,index,search,stats}`);
third-party libraries are vendored under `static/vendor/` so it works offline. Read-only:
no write/edit endpoints are exposed.

## Develop

```bash
uv sync
uv run pytest          # offline against tests/fixtures/wiki
uv run ruff check src tests
uv run ai-wiki-mcp     # serve (set WIKI_ROOT first)
```

Docker: `docker build -t ai-wiki-mcp .` then run with `-v <wiki>:/wiki`.

## Releasing

Versioning is semver, single-sourced from `[project].version` in `pyproject.toml`.

- **Every PR must bump the version** (major/minor/patch). The `version-check` workflow
  fails a PR unless its `pyproject.toml` version is a valid `X.Y.Z` strictly greater than
  `main`'s, so each merge produces a fresh tag. Bump with an edit + `uv lock`.
- **Merging to `main`** runs the `release` job (after lint + tests pass): it builds the
  image, publishes it to the Gitea container registry, and pushes a `vX.Y.Z` git tag.

Published image (Gitea built-in registry, direct endpoint):

```bash
docker pull 192.168.10.32:3000/jhonnold/ai-wiki-mcp:<version>   # or :latest
docker run --rm -p 3010:3010 -v <wiki>:/wiki \
  192.168.10.32:3000/jhonnold/ai-wiki-mcp:<version>
```