Install
$ agentstack add mcp-fabriziosalmi-l0-memory ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
l0-memory
[](https://github.com/fabriziosalmi/l0-memory/actions/workflows/ci.yml) [](https://github.com/fabriziosalmi/l0-memory/releases) [](server/go.mod) [](LICENSE)
Long-term memory for AI assistants, backed by a single Go binary that speaks the Model Context Protocol over stdio and exposes the same SQLite store via a CLI. Memories are partitioned by scope, can be pinned, linked into a typed graph, and tracked for freshness. A VSCode extension provides a sidebar UI, including a force-directed visualisation of the graph.
The store is local and plaintext. By default there is no network listener and no embeddings — search is pure SQLite FTS5. Point LTM_EMBEDDING_URL at an OpenAI-compatible endpoint (Ollama, LM Studio, vLLM, …) to opt into hybrid retrieval, which blends FTS5 with vector similarity. The Go side has zero CGO dependencies; the binary cross-compiles to every supported platform.
Repository layout
server/ Go MCP server + CLI (the `ltm` binary)
extension/ VSCode extension (TreeView UI + bundled binaries)
extension-browser/ Web Clipper browser extension (Manifest V3)
integrations/ Host and shell integrations (Claude Code, Git hooks)
Install
From a release
Download from the releases page:
ltm--.tar.gz(or.zipon Windows). Extract and placeltmon
your PATH.
l0-memory-.vsix. Install the extension with
code --install-extension l0-memory-.vsix.
The macOS binaries in the release archives are ad-hoc codesigned. See [SECURITY.md](SECURITY.md) for the rationale (Sequoia/Tahoe provenance gate).
From source
make build # server/ltm
make install # build + install ltm to ~/.local/bin
make test # go vet + go test -race
make vsix # cross-compile all binaries + package the extension
make install places ltm at ~/.local/bin/ltm; make sure that directory is on your PATH (e.g. add export PATH="$HOME/.local/bin:$PATH" to your shell rc), otherwise ltm won't be found in a fresh shell.
The default DB path is ~/.long-term-memory/memories.db. Override with LTM_DB=/path/to.db.
Connecting an MCP host
ltm is a stdio MCP process. Every host that speaks MCP can use the same SQLite store, so a memory saved from one tool is visible from another.
Claude Code
make install-mcp
# equivalent to: claude mcp add l0-memory $(pwd)/server/ltm mcp
For automatic recall, also install the optional integration — a SessionStart hook that injects your persona (pinned user scope) and the current project's memory (repo:, pinned first), plus a /checkpoint skill to save state at the end of a session:
make install-claude # see integrations/claude-code/
Claude Desktop
make install-mcp-desktop
# Edits the Claude Desktop config (macOS:
# ~/Library/Application Support/Claude/claude_desktop_config.json,
# Linux: ~/.config/Claude/claude_desktop_config.json), backs up the
# previous file, and points Claude Desktop at the local ltm binary.
# Quit and reopen Claude Desktop to pick up the change.
Cursor / Cline / any other MCP host
Point the host at ltm with args ["mcp"]. Example config snippet:
{
"mcpServers": {
"l0-memory": {
"command": "/absolute/path/to/ltm",
"args": ["mcp"]
}
}
}
MCP tools
| Tool | Required args | Optional args | Behaviour | |--------------------|-------------------------------------|----------------------------------------------|-----------| | memory_save | key, value | scope, tags, origin, origin_agent | Insert or update by (scope, key). origin/origin_agent record provenance. | | memory_get | key | scope, expand | Compact descriptor by default; pass expand:true for the full record. | | memory_search | query | scope, limit, expand | Search over key, value, tags. FTS5 by default; hybrid (FTS5 + vector) when an embedding endpoint is configured. Compact hits with snippet and score. | | memory_list | — | scope, limit | Most recently updated entries, pinned first, archived hidden. | | memory_delete | key | scope | Remove an entry. Cascades to incident links. | | memory_query | key, path | scope | Slice a JSON-valued memory by JSON Pointer (RFC 6901) plus * wildcard. | | memory_pin | key, pinned | scope | Toggle the pinned flag. Pinning implies a verify. | | memory_link | from_key, to_key, rel | from_scope, to_scope | Create a typed edge (from, to, rel). Re-linking is a no-op. | | memory_unlink | from_key, to_key, rel | from_scope, to_scope | Remove a single edge. | | memory_links | key | scope | List every link incident to a memory, in either direction. | | memory_traverse | key | scope, depth, rel, direction | BFS subgraph view. Depth defaults to 1; direction out/in/both. | | memory_rename | old_key, new_key | scope | Atomic rename inside one scope. Cascades through incident links. | | memory_verify | key | scope | Set verified_at = now. Compact views report staleness_days. | | memory_supersede | old_key, new_key, value | scope, tags | Archive the old key, create the new, link new --supersedes→ old. |
Pinned memories are also exposed as MCP resources at memory:////, so an MCP host that supports resources/list + resources/read can attach pinned context without calling memory_get. The server emits notifications/resources/list_changed whenever the pinned set changes (pin, unpin, supersede, delete of a pinned row).
Search semantics
memory_search is backed by SQLite FTS5 with the unicode61 tokenizer.
- Each whitespace-separated token becomes a prefix match, AND'd with the
others. caddy waf matches entries containing both caddy* and waf*, ranked by BM25.
- Tokens that contain non-alphanumeric characters are quoted as phrases, so
queries like 100% or "hello do not break the parser.
- Punctuation in the indexed data (
_,-,.,:,/, etc.) is a
separator at index time, so repo:caddy-waf is matched by caddy, waf, or repo.
- Results order: FTS5 rank, then
updated_at DESC. - A query that fails to parse as FTS5 falls back to a case-insensitive
LIKE substring scan with % and _ treated literally.
Hybrid retrieval
FTS5 only matches literal tokens and their prefixes, so a cross-lingual or paraphrased query that shares no token with its target returns nothing — idee per migliorare la memoria finds no match for a semantically perfect English memory. Set LTM_EMBEDDING_URL (and LTM_EMBEDDING_MODEL) to an OpenAI-compatible /v1/embeddings endpoint — Ollama, LM Studio, vLLM, llmproxy, or OpenAI itself — to turn on hybrid retrieval:
- On every
memory_save, the value is embedded and the vector stored in
the same row. Best-effort: a failed embed never blocks the save, only the vector is skipped.
memory_searchthen runs FTS5 and a flat cosine vector search and
blends the two rankings with Reciprocal Rank Fusion (k=60). Pinned status is only a tie-breaker on equal RRF score, not an override.
- With the env unset (or
LTM_EMBED_DISABLE=1) the vector path is skipped
entirely and search is pure FTS5 — no behavioural change from earlier versions.
Existing memories are not embedded retroactively. After enabling the endpoint, run ltm reembed once to backfill; subsequent saves auto-embed.
| Variable | Default | Description | |-----------------------|---------|-------------| | LTM_EMBEDDING_URL | (empty) | OpenAI-compatible /v1/embeddings base URL. Empty = FTS-only. | | LTM_EMBEDDING_MODEL | (empty) | Embedding model name passed to the endpoint. | | LTM_EMBED_DISABLE | (empty) | 1 forces the vector path off even when a URL is set. | | LTM_EMBED_TIMEOUT | 5s | Per-request timeout (Go duration). |
Compact responses
memory_get returns {key, scope, tags, pinned, archived?, size_bytes, schema | preview, hint, verified_at?, staleness_days?, origin?, created_at, updated_at, compact: true} by default. The value field is omitted. Pass expand:true to get the full record.
memory_search likewise returns SearchHit objects without the value, but with a snippet (FTS5 snippet() with > markers) and a score (-bm25(), larger is more relevant). In most cases the snippet shows what the caller needed without a follow-up memory_get.
The CLI commands (ltm get, ltm search) always return the full record.
Scopes
A memory is identified by (scope, key). The same key can live independently in different scopes — (user, focus) and (repo:l0-memory, focus) are different rows. memory_search, memory_list, and the resource list accept scope to restrict; omit it to query every scope at once. memory_save, memory_get, memory_delete default to user.
Conventional scope names:
user— cross-project notes.repo:— repo-specific context.desktop,code, etc. — host-specific notes when the same store is
shared across multiple MCP hosts.
Pinning and freshness
Three orthogonal signals can be attached to a memory:
- Pinned (
memory_pin {pinned:true}) — surfaced first bymemory_list
and exposed as an MCP resource. Pinning sets verified_at = now.
- Verified (
memory_verify) —verified_atis updated; compact views
expose staleness_days = (now - verified_at) / 1 day. The host can use this to skip or flag old memories.
- Archived — set by
memory_supersede. Archived rows are hidden from
memory_list and memory_search by default but stay queryable (memory_get still returns them). The graph keeps incident links, so a successor can be reached from old references.
Knowledge graph
Edges are typed and directional. The triple (from, to, rel) is unique: re-linking the same triple is a no-op. Edges respect a foreign-key cascade, so deleting a memory drops every edge incident to it. Edges may cross scope boundaries.
ltm save tech:caddy "Caddy server" tech
ltm save repo:caddy-waf "WAF plugin" repo
ltm link repo:caddy-waf depends_on tech:caddy
ltm traverse tech:caddy 2
# {root, depth, nodes:[…], edges:[…]}
memory_traverse runs a BFS from the given node, deduplicates visits, filters by rel if requested, and supports direction out, in, or both.
CLI reference
ltm [--scope ] [args...]
# LTM_SCOPE in the environment has the same effect as --scope.
ltm list [limit] # pinned-first, archived hidden
ltm pinned [limit] # only pinned entries
ltm get
ltm search [limit]
ltm query [path] # JSON Pointer + '*' wildcard
ltm save [tags] # value of "-" reads from stdin
ltm delete
ltm rename # cascades through links
ltm verify # mark "still current"
ltm supersede [tags] # archive old, create new, link supersedes
ltm pin
ltm unpin
ltm link # same-scope edge (cross-scope is via MCP)
ltm unlink
ltm links
ltm traverse [depth] # JSON: {root, depth, nodes, edges}
ltm reembed [--force] # backfill embeddings for hybrid retrieval
ltm path # prints the SQLite DB path
ltm version
ltm serve [port] # local HTTP REST server (default 8080); prints an auth token
ltm doctor # one-shot health check: binary, store, serve, hook
VSCode extension
The sidebar has two panes:
- Pinned — pinned memories. Pin/unpin context actions.
- Memories — full list. The toolbar exposes Add, Search, Filter by
scope, Toggle group by scope, Sort, Open knowledge graph, Refresh, Clear filter, Delete selected. The view title shows the active filters in the form scope:user · q:"caddy" · sort:key · grouped.
Per-item context actions: Open in editor, Verify, Supersede with new key, Rename key, Edit, Link to, Show neighbors, Remove a link, Open graph from here, Pin/Unpin, Delete.
A status bar item on the right ($(database) l0: N, plus $(pinned) K when pinned > 0) shows totals and clicks back to the sidebar.
Knowledge graph viewer
The Open knowledge graph button (toolbar of the Memories pane, or per-item context action) launches a side webview that renders the store as a D3 force-directed graph. Nodes are coloured by scope class; pinned nodes have a stronger outline; the root of a per-item graph has the thickest outline. Click a node to open the memory; double-click to re-root; drag to reposition; scroll to zoom. Depth (1–4) and direction (out/in/both) selectors trigger a re-fetch via memory_traverse. D3 is bundled locally (no CDN).
Settings
| Setting | Default | Description | |--------------------------|-------------|-------------| | l0-memory.binaryPath | "" | Absolute path to ltm. Empty enables auto-discovery. | | l0-memory.dbPath | "" | Override SQLite DB path (sets LTM_DB). | | l0-memory.defaultScope | "user" | user / ask / repo:current. Used when adding from the sidebar. | | l0-memory.groupByScope | false | Group memories under collapsible scope nodes in the Memories tree. | | l0-memory.sortBy | "updated" | updated / created / key / scope. Pinned items always come first. | | l0-memory.autoStartMCP | false | Spawn the MCP server in the background on activation. Usually unnecessary because the host starts it. |
Binary auto-discovery
When l0-memory.binaryPath is empty, the extension searches in this order:
- The bundled binary inside the extension at
bin/-/ltm. - The dev layout (
../server/ltmrelative to the extension folder, when
running from this repository).
- Common install locations:
/usr/local/bin/ltm,
/opt/homebrew/bin/ltm, ~/.local/bin/ltm, ~/go/bin/ltm.
ltmresolved viaPATH.
If none of the above resolves to an executable, the sidebar surfaces an error with two actions: open the binaryPath setting, or open the output channel.
REST API & web clipper
ltm serve exposes the store over a local HTTP/JSON API on 127.0.0.1:8080 (GET /health, GET/POST/DELETE /memories) — the backend for the browser web clipper in extension-browser/, and usable by any local script.
Every route except GET /health requires a bearer token (X-LTM-Token or Authorization: Bearer). Binding to 127.0.0.1 does not stop a malicious web page from calling the server, so the token is what actually gates it — the old Access-Control-Allow-Origin: * let any site read/write the whole store. The token is generated once, stored 0600 at /serve-token (override with LTM_SERVE_TOKEN), and printed on startup; CORS is returned only for chrome-extension:// / moz-extension:// origins.
Quick start: run ltm serve, load extension-browser/ as an unpacked extension (chrome://extensions → Developer mode → Load unpacked), and
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: fabriziosalmi
- Source: fabriziosalmi/l0-memory
- License: MIT
- Homepage: https://fabriziosalmi.github.io/l0-memory/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.