@muninhq/mcp
v0.2.0
Published
Local stdio MCP server over the Munin engine — your machine, your keys, your choice of provider. Part of the open-core local product; the hosted/managed product and the verticals are separate and closed.
Downloads
223
Readme
@muninhq/mcp — local MCP server over your Munin memory
A stdio MCP server that lets your own LLM client (Claude Desktop, Cursor, anything MCP-capable) retrieve cited, permission-filtered context from a Munin memory. Your machine, your keys, your choice of provider — the server runs as a child process of your client, opens no network listener, and adds no auth of its own (the OS process boundary is the trust boundary).
One-command setup
The server reads its configuration from a per-user MUNIN_HOME (default
~/.munin) — one munin.env file plus its pgdata/ and encrypted blobs/.
Nothing is hand-edited in the common case.
The fastest way in is the guided wizard — it provisions your home, helps you choose a model, adds files, wires your AI client, and verifies the connection:
munin setupmunin setup defaults to "local store · cloud AI": your files, blobs and memory
stay on this machine, and only the passages a question needs go to the cloud provider
you choose, under your own key (munin set-key openai is the smooth one-key path —
it does extraction and embeddings, so no Ollama is needed). Prefer zero egress?
Choose Local (Ollama) in the wizard (or munin init --local) and nothing leaves
your machine — install Ollama and pull its two models once:
ollama pull qwen2.5:7b && ollama pull bge-m3.
The examples below use the installed
munincommand (npm install -g munin-mcp). From a dev checkout, prefix each withpnpm --filter munin-mcp(e.g.pnpm --filter munin-mcp munin setup).
Or step by step
The wizard just orchestrates these — run them directly if you prefer:
# 1. bootstrap a portable home (~/.munin): munin.env (mode 0600), data dirs, tenant
munin init
# 2. choose your model: a cloud key (smooth), or zero-egress local
munin set-key openai # or `munin set-key anthropic`, or `munin use-local`
# 3. put documents in, then build the local knowledge graph (both run in-process)
munin ingest /path/to/your/docs
munin extract
# 4. wire your AI client — prints the block by default; --write merges it safely
munin mcp connect --client claude-desktop --write
# (Cursor: --client cursor; project-local: add --project <dir>)
# 5. confirm it is wired, then restart your client
munin mcp doctormunin ingest / munin extract default --tenant to the home's tenant and
--tags to personal, so the common case needs no extra flags. The local store
is a single-process database, but you never need to stop anything: when your AI
client (or Studio) has the memory open through the shared munin serve host,
the data verbs detect it and route through that host automatically; with
nothing running they open the store directly.
Choosing (or switching) your model
munin setup sets this up interactively; you can also point the home at a provider
key — or back at fully-local — in one command at any time:
# Claude for extraction + answers; embeddings stay local (Ollama bge-m3):
ANTHROPIC_API_KEY=sk-ant-… munin set-key anthropic
# OpenAI for extraction + answers AND cloud embeddings (re-ingest to re-embed):
OPENAI_API_KEY=sk-… munin set-key openai
# Or go fully local (zero egress) — Ollama does extraction + embeddings:
munin use-localThe two postures are: local store · cloud AI (files stay local; the text a
question needs goes to your chosen provider under your key) and fully local
(Ollama, zero egress — heavier on the machine, best on ~16 GB+ RAM). set-key
writes the key to munin.env at mode 0600 (never printed), picks sensible models,
and sets the posture to MUNIN_ALLOW_CLOUD_PROVIDERS=true (local store + cloud AI;
egress acknowledged); use-local re-engages MUNIN_LOCAL_MODE=true. Prefer the
provider env var over --key <value> so the key stays out of your shell history. Run
munin mcp doctor afterwards to confirm the posture.
Seeing and managing what's in the memory
munin docs # list documents (newest first)munin docs lists the documents in the memory with the ids you can read via
munin_get_document. The munin_status tool also surfaces the most-recent few
from inside your AI client. To remove a document and everything derived from it
(paragraphs, embeddings, extracted entities/edges, the raw blob), run the CLI:
munin forget <documentId> --commit --confirm-title "<title>"munin forget is a dry run by default; --commit --confirm-title performs the
hard delete in a single transaction and removes the blob, leaving no orphans.
Like ingest/extract, munin docs and munin forget are doorman-aware: with a
shared munin serve host running they read (and erase) through it; otherwise
they open the local store directly. If the store is ever locked by something
that is not a reachable host, munin serve --stop frees it (a shared host
restarts automatically the next time your AI client uses it).
munin mcp connect
Prints the mcpServers block (and the target path) by default — paste it
yourself, or re-run with --write. --write is safe: it refuses symlinks
and unparseable configs, backs up the existing file, writes atomically,
and merges — it sets only mcpServers.munin and never touches your other
servers or top-level keys. Re-running is a no-op. The emitted block carries
no secrets — only a MUNIN_HOME pointer and a hardened PATH; the settings
(including the blob key) stay in munin.env.
For an installed Munin, the emitted block runs the published munin-mcp bin
directly (no pnpm, no --dir, no checkout path):
{
"mcpServers": {
"munin": {
"command": "/Users/you/.nvm/versions/node/v22.22.0/bin/node",
"args": [
"/Users/you/project/node_modules/@muninhq/mcp/dist/main.js"
],
"env": {
"MUNIN_HOME": "/Users/you/.munin",
"PATH": "/Users/you/.nvm/versions/node/v22.22.0/bin:/usr/bin:/bin"
}
}
}
}(From a dev repository checkout the block instead runs pnpm --dir
<checkout>/packages/mcp --silent start — same shape, repo launch target.)
Node-version pinning (the nvm fix). connect pins the exact Node it is
running under (Node 22, by construction) and leads PATH with that Node's bin
dir. A bare command: "pnpm"/"node" would let your client resolve the binary —
and under nvm pick up an old Node (v18): the launcher then crashes (File is
not defined, undici) and the client just shows "Server disconnected". So run
connect under Node 22 (the repo's pinned version) and the emitted block can't
grab a broken Node. (In the checkout form --silent matters too: pnpm's script
banner would otherwise corrupt the stdio JSON-RPC channel.)
Honest v1 boundaries
connectemits the installed-bin launcher.munin-mcpships as a compiled Node bin (dist/main.js,#!/usr/bin/env node— notsx, no checkout needed to run the server), and the package carries its own engine + migrations + WASM-backed PGlite as real dependencies, so an installedmunin-mcpruns standalone. When run from an install,munin mcp connectresolves that installed bin and emits a<node> <…>/@muninhq/mcp/dist/main.jslauncher (the clean-install acceptance test asserts exactly this — no--dir, no repo path); from a dev checkout it emits thepnpm --dir <repo>/packages/mcpform. What remains is the actual npm publish (and then a one-linenpx munin-mcp).- Blob encryption is mandatory only under the local/prod posture, and the AES
key is co-located with the data. Raw original bytes are encrypted at rest with
AES-256-GCM, and under
MUNIN_LOCAL_MODE(and in production) plaintext blob storage is refused — but a non-local configuration can store plaintext by design, so the encryption guarantee is the local/prod posture's, not an unconditional one. The AES key lives in the same0600 munin.envas the store, so at-rest encryption protects a stolen disk only if that key file is separately protected, and is weakened on a synced/cloud-backed folder (Dropbox / iCloud) where the key travels with the data. KeepMUNIN_HOMEon local disk for the encryption to mean what it says. - MCP reads are blob-free, so your memory relocates to any path. All five
tools read paragraph text from the database (
pgdata/), never from blobs — so copying/pointingMUNIN_HOMEkeeps reading and chatting at any path. Raw blobs (original bytes, used only by ingest/erase) still store absolutefile://URIs, so full blob fidelity — erase / original-byte re-read — survives only a same-path move or a re-ingest until tenant-relative blob keys land (a deferred engine change).munin mcp doctorand the launcher surface this posture. - Don't point two engines at one home. PGlite is single-process: a synced
home or two clients opening the same
pgdatarisks corruption. Sync/share for transport only.
The five tools
| tool | what it does |
| --- | --- |
| munin_retrieve_context | ranked, cited context for a question — the calling LLM synthesises (one embedding call, no server-side answer model) |
| munin_ask | full grounded answer with [n] citations, or an honest no_evidence |
| munin_gather_entity | everything about one subject by identity; same-name collisions return candidates + pick tokens |
| munin_get_document | the full source behind a citation (title + ordered paragraphs) |
| munin_status | document/paragraph/entity counts, pending extraction, active tenant + configuration, and a sample of the most-recent documents |
Verifiable citations. Every source the retrieval tools return carries a
sourceId (e.g. P11), and each result includes a short, stable
citationGuidance field instructing the calling model to cite the sourceId
inline after each claim (e.g. "…rooted in doughnut economics [P11].") and to
say so when the sources don't support something. munin_ask returns [n]
markers mapped to a citations[] array (each with documentId, paragraphId
and the quoted text) and guidance to preserve those markers when relaying
the answer. The grounding shows up in the answer, not by reading the logs.
Advanced / dev
- Hosted-Postgres store (instead of the local PGlite home): point the server
at a Postgres connection string and supply the config/tenant directly — set
MUNIN_CONFIG_PACKAGE=@muninhq/config-generic-baseline(or@muninhq/config-personal) +MUNIN_TENANT_ID=<id>+DATABASE_URL=<conn>in the clientenvblock (noMUNIN_HOMEneeded for a hosted store). - Inspector:
npx @modelcontextprotocol/inspector pnpm --filter @muninhq/mcp --silent start(setMUNIN_HOMEor the config/tenant env first). - Without a home: the launcher fails fast and friendly ("run
munin init") unless you supplyMUNIN_CONFIG_PACKAGE(and the rest) another way — e.g. the hosted demo above.
Troubleshooting
- Logs are on stderr (stdout belongs to JSON-RPC). Claude Desktop:
~/Library/Logs/Claude/mcp-server-munin.log. Verbosity:MUNIN_MCP_LOG_LEVEL=debug. - "No Munin home … run
munin init" — the launcher found nomunin.envatMUNIN_HOMEand noMUNIN_CONFIG_PACKAGEin its env. Runmunin init, thenmunin mcp connect. - "Multiple tenants exist" — set
MUNIN_TENANT_ID(or letmunin initpin one).munin mcp doctorshows what resolved. - Empty results — check
munin_status(pending extraction?) and that the embedding provider matches the one the corpus was embedded with. munin.envis authoritative — it is loaded with override-on, so a value set only in the clientenvblock is intentionally overridden by the home file.
Open core
Part of the Munin open-core local product, released under AGPL-3.0-only (see LICENSE and the repository NOTICE). The hosted / managed product, team and device-sync features, brokered LLM access, and the vertical configurations (e.g. MAT / HR) are a separate, closed commercial product and are not licensed under the AGPL.
