Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MCP Server

lunaris-mcp exposes Lunaris memory to any Model Context Protocol (MCP) agent — Claude Code, OpenAI Codex, and anything else that speaks MCP — over the stdio transport. The agent gets persistent, scope-isolated memory it can write to and recall from across sessions, with the same bi-temporal storage and atomicity guarantees as the rest of Lunaris.

MCP ≠ MemoryProtocol 0.1. This page is about the agent-facing MCP server (a stdio JSON-RPC tool surface). The MemoryProtocol chapter is a separate HTTP/SSE wire protocol for the Lunaris HTTP server. They solve different problems and are not interchangeable.

Install

No Rust toolchain is required for the npx / uvx paths — both download a prebuilt binary for your platform on first run.

# Rust (builds from source → ~/.cargo/bin/lunaris-mcp).
# NOT `cargo install lunaris-mcp` — the crate is publish = false (it links
# lunaris-memory-service, which has a vendor/ path dep) so it is not on
# crates.io. Needs cmake + a C++ compiler for llama.cpp.
cargo install --git https://github.com/pilotspace/lunaris lunaris-mcp

# Node (no Rust toolchain)
npx -y @pilotspace/lunaris-mcp --help

# Python (no Rust toolchain)
uvx lunaris-mcp --help

Supported prebuilt platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64. On any other platform, build from source with the cargo install --git form above — plain cargo install lunaris-mcp always fails (the crate is not on crates.io).

Registry availability. The npx/uvx packages (@pilotspace/lunaris-mcp, lunaris-mcp) are published as part of the npx/uvx distribution wave; until your registry shows them, the always-available paths are the prebuilt lunaris-mcp-<target>.tar.gz binaries attached to each GitHub release (since v0.6.1) or the cargo install --git source build. The npm/PyPI wrappers honour LUNARIS_MCP_BIN_PATH for air-gapped hosts.

Tool surface

20 tools are registered (all implemented) — nine durable-memory tools, four working-memory (scratchpad) tools, five curation tools, and two retention tools:

ToolInputReturns
memory.ingestsource, content, optional t_ref, metadata{ lsn }
memory.recallquery, optional k, filters, as_of{ hits[] }
memory.forgettarget.source_prefix XOR target.episode_id, optional dry_run (defaults to true){ status, dry_run, matched, removed }
memory.list_scopes(none){ scopes[] }
memory.rememberkind (decision|fix|preference|constraint), content, optional why, tags, dedupe_key{ lsn, was_duplicate, source }
memory.record_decisiondecision, rationale, optional alternatives, tags, dedupe_key{ lsn, was_duplicate }
memory.record_editpath, after, optional before, intent, dedupe_key{ lsn, was_duplicate }
memory.status(none)backend capability profile + MQ queue-depth probes for the three worker topics and the never-drained __lunaris_audit__
memory.feedbackmemory_id, sentiment (±), reason (required), optional dedupe_key{ lsn, was_duplicate, activation_applied }
memory.verify_agendaoptional limit{ count, items[] } — episodes the staleness sweep flagged (recorded git anchor no longer matches HEAD for files they reference)
memory.resolveepisode_id, action (keep | invalidate), optional reason, superseded_by{ status, episode_id, invalidated, agenda_removed }
memory.dream_agendaoptional limit, min_cluster_size, max_activation{ status, total_candidates, count, clusters[] }read-only; writes nothing
memory.distillkind (decision | lesson | invariant | gotcha), content, source_episode_ids, optional title, tags, dedupe_key{ status, distilled_episode_id, lsn, archived_count, was_duplicate }
memory.profileoptional limit_per_section{ markdown, counts, total }
memory.scratchpad_writekey, value, optional namespace{ lsn }
memory.scratchpad_readkey, optional namespace{ found, value }
memory.scratchpad_greppattern, optional namespace{ entries[] }
memory.scratchpad_consolidateoptional namespace{ status, promotions, archives }
memory.retentionoptional max_age_ms (omit to READ), hard{ status, configured, max_age_ms, hard }
memory.retention_enforceoptional dry_run (defaults to true){ status, dry_run, configured, max_age_ms, hard, cutoff_ms, matched, removed }

memory.ingest is the general capture path. memory.record_decision and memory.record_edit are structured aliases that write intent-typed episodes (source = "decision:<scope>" / "edit:<scope>") with optional dedupe_key idempotency. memory.status reports the bound scope and backend capabilities (queue_native, graph_native, rerank_native, native_rrf, max_vector_dim, cypher_dialect, …).

The four curation tools are what separate Lunaris from a vector store with an MCP wrapper — they let an agent maintain its memory, not just append to it. memory.verify_agenda surfaces memories the background staleness sweep believes have gone out of date (the git anchor recorded with the episode no longer matches HEAD for the files it references); memory.resolve acts on one — keep prunes the agenda row and leaves the episode live, invalidate soft-deletes it via an MVCC tombstone so it stops appearing in memory.recall. memory.dream_agenda is the read-only planner for distillation: it surfaces clusters of ripe (referenced, not-yet-distilled) raw episodes with activation stats and writes nothing, and memory.distill is the transactional apply step that writes the distilled prose back as a durable, highest-priority episode (source = "distilled:{kind}:<scope>", source_priority = 95). memory.feedback records explicit ± human/agent feedback on one memory with a required reason, writing a strong reinforcement signal to the activation ledger that moves its recall ranking.

The roster is pinned against the real binary by crates/lunaris-mcp/tests/server_boot.rs::server_boots_and_lists_all_tools, which spawns the process and drives initializetools/list.

The four memory.scratchpad_* tools are working memory — transient, key-addressed notes (drafts, plans, in-progress state) under a scratchpad/ namespace, separate from the durable episode log. scratchpad_write/read are key-value put/get, scratchpad_grep lists entries by key-prefix, and scratchpad_consolidate drains the scratchpad queue and promotes/archives notes by activation. scratchpad_consolidate needs a native-queue backend; Moon has one, so on 0.7.0 it is always available. (It still returns { status: "unsupported_backend" } if the connected substrate reports no queue — see Storage.)

memory.forget previews by default: with dry_run omitted it scans, returns { status: "preview", matched: N, removed: 0 }, and writes nothing. Deleting takes an explicit "dry_run": false. This inverts the HTTP POST /v1/forget default (dry_run: false there, for API compatibility) on purpose — the MCP caller is a language model, so the irreversible branch must be the one it has to ask for.

The wire DTOs are identical across MCP clients, and every request DTO carries #[serde(deny_unknown_fields)] — no wire field can override the bound scope.

Progressive disclosure — the retrieval ladder

The server instructions (returned at MCP initialize) and every tool description teach connecting agents to retrieve cheapest-first:

  1. scratchpad_read / scratchpad_grep — exact or prefix key lookup; returns full verbatim values; no model load. Always first for known keys.
  2. memory.recall with the default k = 5 — hybrid semantic + BM25 preview pass. Hits are 200-character snippets (with episode_id, source, score), not full episode text, and the first call in a process stages/loads the GGUF embedder.
  3. Widen only on a miss — raise k, add filters.source_prefix (decision:, edit:, claude-code:), or pass as_of for a bi-temporal point-in-time view.

There is intentionally no fetch-full-episode tool: widen k for more context, or keep full-fidelity values in the scratchpad where reads are verbatim. memory.status / memory.list_scopes are diagnostics, not retrieval.

Scope is bound at startup

lunaris-mcp resolves one scope when it starts and never changes it from wire payloads. Resolution order:

  1. --scope flag / LUNARIS_MCP_SCOPE env var (highest priority).
  2. git remote.origin.url + current branch → blake3 → "git_<hex16>".
  3. Canonical cwd → blake3 → "cwd_<hex16>".

The resolved scope is persisted to ~/.lunaris/scopes.json. To rename it, edit the name field there and restart the host agent (which restarts the lunaris-mcp child). See Multi-Agent & Scope for the scope model.

Storage

Moon is the only backend, and the server resolves it in exactly this order:

#SourceNotes
1--storage / LUNARIS_MCP_STORAGEExplicit always wins.
2~/.lunaris/contextd-moon.urlThe store a running lunaris-contextd advertises, adopted only after a loopback + RESP PING liveness probe (25 ms, LUNARIS_MOON_DISCOVERY_TIMEOUT_MS).
3Refuses to boot, printing the quickstart.

Step 2 is why an MCP server and the lunaris-hook daemon on the same machine land in the same Moon without being configured twice — lunaris-hook has always resolved this way, and lunaris-mcp now does too. An advertised and probed store is not a guessed default: a stale file (contextd crashed, its port recycled) fails the probe and is declined, which lands you in step 3 rather than in somebody else’s Moon. The file is read once, at boot — start contextd first, then the agent.

There is still no default. Through 0.6.x an unset value opened a per-scope SQLite file at ~/.lunaris/<scope>.db; 0.7.0 deleted that backend, and nothing guesses a store in its place — not SQLite, and not a hardcoded moon://127.0.0.1:6380 either. A stdio server surfaces tool errors to its client but not startup logs, so “starts, then fails every call” (or worse, “starts, and quietly writes into an unrelated Moon”) was the worst outcome available.

docker run -d --name lunaris-moon -p 6381:6379 \
  ghcr.io/pilotspace/moon:0.8.5 \
  --shards 1 --protected-mode no --appendonly yes
"env": { "LUNARIS_MCP_STORAGE": "moon://127.0.0.1:6381" }

--shards 1 is mandatory — an ingest is one MULTI/EXEC transaction and a sharded Moon rejects it. All 20 tools work against Moon: native HNSW vector search, BM25 keyword fusion, graph, queues, and search-side bi-temporal reads. See Running an external Moon.

Auto-launched Moon (opt-in build, development only). A source build with cargo build -p lunaris-mcp --features embedded-moon makes lunaris-mcp launch an in-process Moon (rooted at ./.lunaris-moon) when no LUNARIS_MCP_STORAGE override is set, then use it automatically. The feature is off by default and is not compiled into the published npx/uvx/cargo install binaries. An explicit --storage/LUNARIS_MCP_STORAGE still wins; a failed bring-up is now terminal (it used to fall back to SQLite — there is nothing to fall back to).

The first memory.recall stages the GGUF embedder (~150 MB) and reranker to ~/.lunaris/models/ — expect ~30 s on a cold start, fast thereafter. Set LUNARIS_MCP_SKIP_STAGE=1 if models are pre-staged.

Key environment variables

VariableDefaultDescription
LUNARIS_MCP_SCOPEderived from git/cwdForce a specific scope name
LUNARIS_MCP_STORAGE(no default)Storage URL. moon://host:port only. Unset: the server falls back to a live lunaris-contextd store advertised in ~/.lunaris/contextd-moon.url, else refuses to boot
LUNARIS_MOON_DISCOVERY_TIMEOUT_MS25Liveness-probe budget for that discovery file (0 disables discovery)
LUNARIS_GRAPH_ENABLEDoffEnable the graph extraction/write path (Moon graph recall)
LUNARIS_MCP_LOGinfo,rmcp=warntracing-style filter directive (logs to stderr only)
LUNARIS_MCP_SKIP_STAGEunsetSet to 1 to skip GGUF staging on first recall
LUNARIS_MCP_BIN_PATHunset(npx/uvx wrappers) point at a pre-staged binary for air-gapped hosts

stdout is the JSON-RPC transport. lunaris-mcp writes logs to stderr only. Anything printed to stdout (e.g. an echo in your shell profile) corrupts the MCP framing and silently disconnects the host agent.

Per-agent guides

  • Claude Codeclaude mcp add, project-scoped .mcp.json, and the optional lifecycle hooks + context injection.
  • Codex CLI~/.codex/config.toml, plus hooks and the lunaris-contextd warm sidecar.

The exhaustive guides — full hook tables, lunaris-contextd internals, and measured timings — live in the repo: docs/integration/claude-code.md and docs/integration/codex.md.

Status

The stdio transport is the supported path (Wave A). SSE transport with Bearer auth and multi-user server mode are deferred to a later OIDC milestone; running MCP as a feature flag on lunaris-server was evaluated and rejected — see the decision record.