Wire up the MCP server
memhtml serve mcp exposes the same store over the Model Context Protocol on stdio: fifteen tools and three resources (apps/mcp/src/tools.ts). This tutorial starts the server, lists what a client sees, calls a tool by hand so you can verify the wiring without a client, and then configures a client.
You need a store with something in it, so write a memory first.
Start it
Section titled “Start it”export MEMHTML_ROOT=~/memhtmlmemhtml serve mcpNothing appears on stdout, and that is correct: stdout is the JSON-RPC stream. Anything else written there would corrupt the protocol, so every log goes to stderr. The server then waits for a client to speak first.
memhtml serve mcp holds no database of its own. It spawns the memhtml-mcp entry point with inherited stdio and waits (apps/cli/src/serve.ts:72), so the supervisor holds no handle that could conflict with the child. Interrupting the supervisor kills the child, so Ctrl-C never leaves an orphaned server holding the database open (apps/cli/src/serve.ts:97). On exit you get the one envelope the contract promises:
{ "apiVersion": "1", "type": "serve.exit", "data": { "server": "/path/to/memhtml/apps/mcp/dist/bin.js", "exitCode": 130, "signal": null }}The two apps ship as one build, so the supervisor finds the server by sibling path. For a split deployment that does not keep them side by side, set MEMHTML_MCP_BIN to an explicit path. That variable locates the server and configures no store behavior at all.
See what a client sees
Section titled “See what a client sees”You do not need a client to check the wiring. Speak the protocol on stdin:
{ printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' sleep 1 printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' sleep 2} | memhtml serve mcp 2>/dev/nullThe handshake answers:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"logging":{},"completions":{},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"memhtml","version":"0.2.2"}}}2025-06-18 is the protocol revision this server speaks, and it is the only adapter shipped, so a client negotiating a different revision will not connect. serverInfo.version is the released package version, so expect it to read higher than this transcript.
tools/list publishes the tools in this order:
memory_write memory_write_batch memory_read memory_searchmemory_recall memory_correct memory_link memory_neighborsmemory_resolve memory_archive memory_reinforce memory_listtrace_search trace_links memory_statusThe order is deliberate, because a client publishes it and an agent reads it top-down. memory_write_batch sits second, directly after memory_write, so the tool that memory_write’s own description points at is the very next entry rather than thirteen tools away.
Three resource templates come with them:
{ "resourceTemplates": [ { "uriTemplate": "memhtml://file/{path}", "name": "Memory file", "description": "One memory's title, claim, and body text, by repo-root-relative path. For showing a human the file behind an answer.", "mimeType": "text/plain" }, { "uriTemplate": "memhtml://sleep/{run-id}", "name": "Sleep run report" }, { "uriTemplate": "memhtml://at/{commit}/{path}", "name": "Memory file at a commit" } ]}memhtml://file/{path} funnels through the same use case memory_read does, so fetching it bumps the access plane: it is a chosen open. memhtml://sleep/{run-id} serves one curation run’s committed HTML report.
memhtml://at/{commit}/{path} is the same read as the first one at a finer grain: the bytes of that path as of that commit, so a citation written today still resolves to what it cited after the memory is corrected, archived, or evicted. It reads the git object rather than the working tree, it refuses a branch name or HEAD because a citation must name something that cannot move, and it does not bump the access plane — verifying a receipt is auditing, not choosing. memory_resolve is its companion: one answers what was true, the other where the fact went.
Sleep is absent from the tool surface, because it is a cron and operator action that produces a reviewable branch and an agent should not start one mid-conversation. The other operator commands are absent with it: doctor, publish, index rebuild, sleep merge, and the discrimination gate all stay on the CLI, so reach for memhtml for anything on the operations pages.
Call a tool
Section titled “Call a tool”{ printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' sleep 1 printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"memory_search","arguments":{"query":"drain the VIP"}}}' sleep 3} | memhtml serve mcp 2>/dev/nullThe result carries the answer twice, once as text and once as structuredContent:
{ "jsonrpc": "2.0", "id": 2, "result": { "content": [{ "type": "text", "text": "{\"hits\":[…]}" }], "structuredContent": { "hits": [ { "path": "resources/runbook/drain-the-vip-before-reverting-a-deploy.html", "title": "Drain the VIP before reverting a deploy", "gist": "If a prod rollback is issued, drain the VIP before reverting the deploy.", "memory_type": "procedural", "score": 1, "confidence": 1, "updated_at": "2026-08-12T19:23:15Z", "snippet": "If a prod rollback is issued, drain the VIP before reverting the deploy.", "entities": [], "superseded_by": null } ], "degraded": true, "arms": ["fts", "recency", "salience"], "entity_scope": null, "scope_empty": false, "archived_matches": 0, "archived": [] } }}The fields are the same as memhtml search’s, spelled in snake_case on this surface: memory_type, updated_at, superseded_by, scope_empty, archived_matches, where the CLI’s own envelope is camelCase. Read the key from the surface you are on.
degraded, arms, and scope_empty mean exactly what they mean on the CLI, and they are on the tool result for the same reason: a client that cannot tell a narrow answer from a complete one will present a narrow one as complete.
Configure a client
Section titled “Configure a client”For Claude Code, Codex, Cursor, or OpenCode, one command writes the entry for you:
memhtml integrations install claude # or codex, cursor, opencodememhtml integrations install # every host whose home directory existsIt writes that host’s MCP entry with an absolute command and an explicit MEMHTML_ROOT, and it records what it wrote in a receipt so memhtml integrations uninstall claude removes exactly that. It also installs hooks, an instruction block, and a skill, which is more than a client entry: --hooks none writes the entry, block, and skill and no hook. Wire up your coding agent is the full page, with the per-host files and the restart or trust step each one needs.
Write the entry by hand for any other client, or when you want to see the shape the command produces. An MCP client launches the server as a subprocess, and while the wrapper key differs between clients, the command / args / env triple is the shape they share:
{ "mcpServers": { "memhtml": { "command": "memhtml-mcp", "env": { "MEMHTML_ROOT": "/home/you/memhtml" } } }}memhtml-mcp is the server itself, installed beside memhtml by the same package. "command": "memhtml", "args": ["serve", "mcp"] reaches the same server through a supervisor process and is equally valid — use it when you want the CLI’s own resolution of the entry point, and the direct binary when you would rather not spawn a process to spawn a process.
Set MEMHTML_ROOT explicitly in the client config rather than relying on a shell profile. A client launches the server from its own environment, which is not your interactive shell, and the default ~/memhtml may not be the store you meant. The binary expands a leading ~ itself, so ~/memhtml is a legal value here.
When your memhtml is a symlink into ~/.local/bin and the client does not inherit that on its PATH, give the absolute path to apps/cli/dist/bin.js as command instead.
Run it alongside the CLI
Section titled “Run it alongside the CLI”A CLI command and a running server can share one store. The index is WAL SQLite: it admits one writer at a time and any number of concurrent readers, readers never block the writer, a second writer waits rather than failing, and a wait that outlives busy_timeout is retried with jittered exponential backoff for up to 20 seconds (packages/index/src/database.ts). So running memhtml write while a server serves the same store is supported, and so is the every-ten-minutes index update cron.
The exception is memhtml sleep run, for a git reason rather than a database one. A run holds a checked-out sleep/<date> branch, so a concurrent write commits onto that branch and is then either merged as if it were curation or lost when the branch is dropped. Quiesce writes for the duration of a run.
Share one store between a CLI and a server is the operational version of this, with the concurrency probe you can run yourself. The envelope contract and the tool surface explains why the two surfaces carry the same answers.