hms-mcp: Holographic Memory as an MCP server¶
hms-mcp exposes a Holographic Memory store to any Model Context Protocol client: Claude Code and Desktop, Cursor, VS Code, Codex, Gemini CLI and the rest (see docs/integrations/editors/). It runs on your machine, makes no network calls, and keeps memories in the same document store as hms-server.
Transports: stdio (one client per process) and streamable HTTP (one server, many clients). Protocol: the initialize handshake revisions 2025-03-26, 2025-06-18 and 2025-11-25, and the stateless 2026-07-28 form, all served by the official Rust SDK (rmcp 3.6).
Install and run¶
cargo build --release --features mcp,security --bin hms-mcp # add local-models for semantic recall (Rust >= 1.94)
./target/release/hms-mcp # stdio
./target/release/hms-mcp --transport http # http://127.0.0.1:8765/mcp
After the first release the launchers fetch a prebuilt binary for you:
Both verify the download against a SHA-256 table shipped inside the package and refuse to run a binary that does not match. HMS_MCP_BINARY=/path/to/hms-mcp makes them run your own build. The release assets are named hms-mcp-<rust target triple>[.exe] under the mcp-v<version> release; integrations/mcp/scripts/build-release.sh builds one and records its checksum.
Tools¶
| Tool | Purpose | Read-only |
|---|---|---|
remember | Store or replace a memory. Optional id, title, tags, metadata, if_version (compare-and-set), create_only. | no |
recall | Memories most relevant to a natural-language query, best first, with full text. | yes |
search | Same retrieval with explicit control: tags, metadata filter, min_score, lexical_weight, semantic_weight, fusion (blend or rrf). Returns previews and the score breakdown. | yes |
get | One memory by id: text, tags, metadata, version, timestamps. | yes |
list | Page through memories in id order by prefix or tag; cursor continues. | yes |
forget | Delete by id or ids, or by prefix with confirm: true. | no |
stats | Counts, recall mode, encryption status, storage size and active limits. | yes |
export | Memories as json, jsonl or markdown, size-bounded, paged by next_cursor. | yes |
Every call returns structuredContent plus the same data as JSON text (export returns the payload as text and the paging metadata as structured content). Tool arguments are validated strictly: unknown fields, wrong types and out-of-range values are errors, never ignored.
A memory is {id, text, bytes, title?, tags, metadata, created_at, updated_at, version}. Writing an existing id replaces the memory. version changes on every write, so a read-modify-write client passes it back as if_version and gets version_conflict if someone else wrote in between (the check and the write are atomic per store). Text round-trips exactly, including leading and trailing whitespace; blank text needs allow_empty.
Errors¶
Tool failures are results with isError: true and structuredContent.error = {code, message, details?}, so a model can read them and correct itself.
| Code | Meaning |
|---|---|
invalid_argument | Failed validation (the message names the field). |
too_large | Text or metadata over its limit. |
not_found | No memory with that id. |
already_exists | create_only and the id exists. |
version_conflict | if_version did not match; details.current_version has the current one. |
store_locked | Another process holds the store; see Sharing below. |
store_unavailable | The store could not be opened with this encryption setting. |
too_many_stores / too_many | A configured cap was reached. |
forbidden | A namespace argument on a server that does not accept one. |
internal | Unexpected; the cause is on the server's stderr, never in the reply. |
An unknown or disabled tool, a malformed request or an unknown resource or prompt is a protocol error (-32602), as the specification asks.
Resources and prompts¶
hms://memory/{id}(id percent-encoded) reads one memory astext/plainwith its metadata in_meta;resources/listpages through the default namespace and also listshms://stats. With--namespace-arg,hms://namespace/{namespace}/memory/{id}reads another store.- Prompts:
recall-context(retrieves memories for a topic and embeds them),save-memory,review-memories. A prompt is offered only when the tool it relies on is enabled.
Where memories live¶
| Setting | Flag / environment | Default |
|---|---|---|
| Root directory | --data-dir / HMS_MCP_DATA_DIR | ~/Library/Application Support/hms-mcp (macOS), $XDG_DATA_HOME/hms-mcp or ~/.local/share/hms-mcp (Linux), %LOCALAPPDATA%\hms-mcp (Windows) |
| One exact directory | --store / HMS_MCP_STORE | unset |
| Namespace strategy | --namespace-strategy global\|project / HMS_MCP_NAMESPACE_STRATEGY | global |
| Global namespace name | --namespace / HMS_MCP_NAMESPACE | default |
| Project directory | --project / HMS_MCP_PROJECT | working directory; its nearest .git ancestor is the root |
| Per-call namespace | --namespace-arg / HMS_MCP_NAMESPACE_ARG | off |
Each namespace is its own engine store at <data-dir>/stores/<namespace>. Namespace names are [a-z0-9_-]{1,64}; the project strategy derives <directory-name>-<8 hex digits of a path hash>, so two projects with the same folder name do not collide. With --namespace-arg every tool takes an optional namespace, which is how a multi-user application keeps one store per user. At most --max-stores (16) are open at once.
Sharing a store¶
An engine store has one writer. A second stdio process on the same store fails at startup with store_locked. Several clients that must share memories (Claude Code and Cursor on one project, say) should use one hms-mcp --transport http and point each at it; the editor pages show the remote form.
Encryption at rest¶
export HMS_MCP_PASSPHRASE='correct horse battery staple'
hms-mcp --encrypt # needs a build with --features security
--encrypt (HMS_MCP_ENCRYPT) encrypts the store with a key derived from the passphrase in the environment variable named by --passphrase-env (default HMS_MCP_PASSPHRASE). The server refuses to start without the variable, never logs it, and a wrong passphrase fails the open. Reopening an encrypted store without --encrypt, or the reverse, fails rather than mixing modes. The end-to-end test stores a distinctive string and confirms it appears in no file under the data directory. What encryption does not cover: process memory while running, and the passphrase itself, which lives in whatever sets the environment.
Recall: what "semantic" means for each build¶
- Default build: recall is lexical. Text is stemmed and ranked with BM25, and
tagsandfilternarrow the candidates. Use the words a memory would contain.stats.recallsayslexical (BM25) with metadata filters. --features mcp,local-modelswith--embed-model DIR --embed-revision SHA(a Qwen3-Embedding directory, seedocs/SERVER.md): memories and queries are embedded on the machine and ranking is hybrid lexical plus semantic.local-modelsneeds Rust 1.94 or newer; themcpfeature alone builds on the 1.89 MSRV. The semantic path compiles but has not been exercised end to end here (no model weights were available), so treat it as unverified until you run it against your weights.
score is a relative rank in 0..=1 (the top hit of a blend is 1.0), not a probability; lexical_score and semantic_score are the raw components.
Exposure and limits¶
| Setting | Flag / environment | Default |
|---|---|---|
| Tools to expose | --tools a,b / HMS_MCP_TOOLS | all |
Read-only (drops remember, forget) | --read-only / HMS_MCP_READ_ONLY | off |
| Resources / prompts off | --no-resources, --no-prompts | on |
| Memory text | --max-text-bytes | 32768 (hard cap 65536, and at most 4096 words, one storage chunk) |
| Metadata | --max-metadata-bytes | 16384 |
| Tags per memory | --max-tags | 32 |
k | --max-top-k | 50 |
list page | --max-page | 200 |
forget batch | --max-forget | 1000 |
export payload | --max-export-bytes | 4 MiB per call, paged |
| Concurrent tool calls | --max-concurrent | 4 |
| Dimensions per store | --dim | 4096, fixed once the store exists |
Every flag has an HMS_MCP_* environment variable (see hms-mcp --help). Long documents are not split for you: a memory that does not fit one chunk is rejected with too_large and the advice to split it, which keeps get exact.
HTTP transport¶
- Loopback by default. A non-loopback
--bindis refused unless--allow-remoteis given andHMS_MCP_TOKENis set. There is no TLS; put a TLS proxy in front if it leaves the machine. HMS_MCP_TOKEN(at least 16 characters, environment only) requiresAuthorization: Bearer <token>on the MCP endpoint, compared in constant time./healthanswersokwithout auth.Hostmust be a loopback name (extend with--allow-host) and a request carrying anOriginheader must match--allow-origin; both reject DNS rebinding and cross-site calls with 403. Requests withoutOrigin(non-browser clients) pass.- Request bodies are capped (
--max-request-bytes, 1 MiB). SIGINT or SIGTERM drains in-flight requests, flushes every store and exits 0. - It prints
hms-mcp listening on http://<addr><path>on stdout once bound (--bind 127.0.0.1:0picks a free port).
Security and logging¶
stdout carries only protocol (stdio) or the one listening line (http). Logs go to stderr at --log level (warn default) and contain operation names, durations and error codes, never memory text, queries, ids or the token or passphrase. An internal failure returns a fixed message to the client and the cause to stderr.
Structure and the memory API¶
Handlers are written against the MemoryBackend trait (src/bin/hms-mcp/memory.rs): remember, get, search, list, forget, stats over a namespace. HmsBackend implements it on the document store. A richer memory-API module can implement the same trait and replace HmsBackend in main without touching tool parsing, validation, resources or prompts. The store gained three read-only methods for this server: HmsCore::get_document, list_documents and document_count.
Testing¶
cargo test --features mcp --bin hms-mcp
cargo build --features mcp,security --bin hms-mcp
cd integrations/mcp/tests && npm install && HMS_MCP_BIN=$PWD/../../../target/debug/hms-mcp HMS_MCP_SECURITY=1 npm test
The end-to-end suite drives the binary with the official TypeScript SDK client: initialize and capabilities, tool listing and annotations, every tool, strict validation at the limit and limit plus one, resources and prompts, 60 concurrent writes with 30 concurrent reads, compare-and-set races, restart persistence over stdio and HTTP, lock contention, namespaces, tool exposure, encryption (no plaintext on disk, wrong passphrase refused, secrets absent from logs), HTTP auth, Host and Origin checks, body limits, graceful shutdown, and protocol negotiation over raw HTTP for all four protocol revisions.