Skip to main content
Glama
oaustegard

muninn-mcp

by oaustegard
README.md
# muninn-mcp

Remote MCP server for Muninn's memory. **Read-only. Stage 1.**

Green half of the migration in
[`muninn-utilities/docs/mcp-migration.md`](https://github.com/oaustegard/muninn-utilities/blob/main/docs/mcp-migration.md) ยง5.
Blue โ€” the Python skill talking to Turso directly โ€” stays installed and functional
throughout. Data is single-homed; blue-green applies to the **access path**, not
the stack, because a memory database cannot be forked and merged: supersede
chains, `refs` provenance and `is_superseded` flags encode order.

## Status

| Stage | | |
|---|---|---|
| 0 | write provenance (`source` column) | โœ… shipped, muninn-utilities #98 |
| 1 | green read-only + parity harness | ๐Ÿ”จ this repo โ€” gate at 67/74, 0 regressions |
| 2 | reads promoted to production | |
| 3 | writes, shadowed | |
| 4 | cutover | |
| 5 | collapse the double implementation | |

Five tools, all read-only. Write tools do not exist here โ€” not disabled,
**absent** โ€” and must not appear until the parity harness is green on the
production corpus.

| Tool | |
|---|---|
| `recall` | search memories by text and tags |
| `memory_get` | one memory by id, its reference chain, or a decision's alternatives |
| `muninn_config` | the profile / ops / journal config store |
| `muninn_docs` | the deferred reference, for clients that don't read resources |
| `boot` | the composed boot payload |

Plus resources: `muninn://reference/{recall,types,vocabulary}`,
`muninn://utilities` and `muninn://utilities/{name}` (17 pages), and
`muninn://boot`. ยง8's progressive disclosure โ€” ~30KB of reference is addressable
without any of it entering a tool schema.

## Layout

Same shape as [`sage`](https://github.com/oaustegard/sage)'s `mcp/`, deliberately โ€”
one architecture across sage-mcp, muninn-mcp and later muninnd.

- `src/turso.ts` โ€” retrieval layer. Ported from `remembering/scripts/turso.py`.
- `src/expansion.ts` โ€” blue's multi-stage expansion, the layer above the SQL.
- `src/queries.ts` โ€” the by-id and config reads.
- `src/boot.ts` โ€” `_format_boot_output`, byte-equal against blue.
- `src/tools.ts` โ€” tool bodies and output formatting.
- `src/docs.ts` + `src/docs-generated.ts` โ€” the ยง8 registry. Generated by
  `scripts/build-docs.mjs` from muninn-utilities; run `npm run build:docs`.
- `src/resources.ts` โ€” both doors onto that registry, plus `muninn://boot`.
- `src/server.ts` โ€” `buildServer()`: registration only, Zod schemas.
- `src/index.ts` โ€” bearer-token Worker entry, and the shared dispatch module.
- `src/mcp-oauth.ts` โ€” the OAuth entry claude.ai connectors need. `wrangler` main.
- `harness/` โ€” the blue-vs-green parity gate. See [`harness/README.md`](harness/README.md).

Docs: [`deploying.md`](docs/deploying.md) to get it running,
[`operating.md`](docs/operating.md) to run the gate and read its verdicts.

## The gate

ยง5 is explicit that the harness, not the Worker, is Stage 1's deliverable. It
lives in `harness/`: 64 frozen `recall` shapes run through blue-Python and
green-TypeScript against the same database, normalized to **id order** and
diffed.

```bash
export TURSO_URL=... TURSO_TOKEN=...   # both sides read these two names
npm run harness                        # capture blue, capture green, diff
```

Id order, not row contents: `access_count` and `last_accessed` are written *by
the act of reading*, and `composite_score` embeds `julianday('now')`, so neither
is comparable across two captures. Order is stable under both and is what ยง5
actually asks for.

No credentials to hand? `npm run harness:fts` verifies the FTS vectors, and both
capture scripts take `--dry-run`.

## Parity, not unit tests

`src/fts-golden.json` was **generated by running the live Python**
(`_escape_fts5_server`) over 33 inputs. Those are blue's actual outputs, frozen โ€”
not hand-written expectations. Regenerate them from Python and any divergence
fails CI โ€” `npm run harness:fts` is the regenerator, and it needs no credentials.

That file matters more than it looks. FTS escaping is the one place where a
difference between blue and green changes *which memories come back* instead of
failing loudly, which is why the migration doc calls it the highest-value test of
the whole port.

The ranking itself is **server-side SQL in both worlds** โ€” BM25, recency decay,
priority and confidence are computed by Turso, not by application code. `turso.ts`
transcribes SQL strings; it does not reimplement retrieval. That is what keeps the
port tractable.

## Protocol

`@modelcontextprotocol/server` v2, serving the **2026-07-28** revision and the
2025-era handshake from one route and one factory. `tools/list` carries a public
cache hint (SEP-2549); nothing else may, since recall results are private memory.

## Develop

```bash
npm install
npm test               # 620 assertions across 7 suites
npx tsc --noEmit       # typecheck
npm run dev            # local worker
npm run build:docs     # regenerate the muninn:// resource content
npm run harness        # the blue-vs-green gate (needs live credentials)
npm run harness:selftest   # prove the gate still refuses a real reordering
```

Point `TURSO_URL` at a **branch** database for Stage 1:

```bash
turso db create muninn-green --from-db <production>
```

Full deployment walkthrough โ€” KV namespace, secrets, the claude.ai connector, and
what the design concentrates in one place โ€” is in [`docs/deploying.md`](docs/deploying.md).