Metadata-Version: 2.4
Name: aleth-mcp
Version: 0.2.0
Summary: MCP face for the memory engine: the 9 memory verbs as MCP tools over the thin wire client (ALB-2, go/no-go Step 1). Any MCP-speaking agent gets persistent memory with one config entry.
Author: Aleth Authors
License: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aleth-client>=1.1
Requires-Dist: mcp>=1.0
Dynamic: license-file

# aleth-mcp

The MCP face for the memory engine: the ten memory verbs, `remember`,
`recall`, `show`, `search`, `retract`, `forget`, `supersede`, `connect`,
`status`, `nodes`: exposed as MCP tools, as a thin shell over the
`aleth-client` wire core.

Three of those correct a belief and they are NOT interchangeable:
`retract` says it was wrong, `supersede` says it was true and something
newer replaced it, `forget` destroys it.

Any MCP-speaking agent gets persistent, associative, erasable memory with
one config entry:

```json
{
  "mcpServers": {
    "memory": { "command": "aleth-mcp" }
  }
}
```

## What it is (and is not)

- **Thin.** Every tool is one client call; results are the frozen wire
  models as plain dicts. No state of its own, no extra verbs.
- **Lazy.** The server starts and lists tools even before the engine is up;
  the first call connects. Connection problems come back in-band
  (`{"error": ...}`), so the agent sees them instead of a dead server.
- **Engine-agnostic on the wire.** It speaks the frozen memory wire, so it
  works against the standalone memory server and the full daemon alike --
  set `MEMORY_DISCOVERY_DIR` if the engine's discovery file lives somewhere
  non-default.
- **`forget` really forgets.** Permanent erasure (graph + storage + vacuum),
  distinct from `retract` (kept but flagged). That split is the product's
  thesis, not a convenience.

## Install / run

```bash
pip install aleth-mcp
aleth-mcp          # stdio MCP server (bare invocation -- how hosts launch it)
```

## Register with an AI host

The `install` subcommand writes the server entry into a host's own config file
(merge-only, backed up, idempotent). The bare `aleth-mcp` invocation is
unchanged, only the `install` / `uninstall` / `hosts` subcommands trigger the
writer.

```bash
aleth-mcp hosts                 # list hosts + which are detected here
aleth-mcp install claude-code   # add "memory" to ./.mcp.json
aleth-mcp install --all         # install into every detected host
aleth-mcp install cursor --project --instructions   # config + rules file
aleth-mcp install codex --dry-run                   # show the diff, write nothing
aleth-mcp uninstall claude-code                     # remove exactly our entry
```

Flags: `--all` (detected hosts only), `--project` / `--user` (scope, where the
host supports both), `--dry-run` (unified diff, no write), `--instructions`
(also inject the memory instruction block), `--hooks` (also wire automatic
capture, see below).

Behaviour: existing config is parsed and only our `memory` entry is
added/replaced (unknown keys preserved); a timestamped `.bak` is written next
to the file before every change; re-running with identical content writes
nothing. JSON hosts are reserialized (2-space indent; all other keys survive).
TOML and comment-bearing files are edited by surgical text splice. If a
JSONC-capable file (VS Code, Zed) actually contains comments, the writer
**fails soft**: it refuses to touch the file and prints the exact snippet to
paste, rather than dropping your comments.

### What gets written where

| Host | Config file (default scope) | Container key | Source |
|------|-----------------------------|---------------|--------|
| `claude-code` | `./.mcp.json` (project) or `~/.claude.json` (`--user`) | `mcpServers` | own-knowledge |
| `claude-desktop` | `%APPDATA%\Claude\claude_desktop_config.json` / `~/Library/Application Support/Claude/...` / `~/.config/Claude/...` | `mcpServers` | own-knowledge |
| `cursor` | `~/.cursor/mcp.json` (global) or `./.cursor/mcp.json` (`--project`) | `mcpServers` | own-knowledge |
| `codex` | `~/.codex/config.toml` | `[mcp_servers.memory]` (TOML) | [OpenAI docs](https://developers.openai.com/codex/config-reference) |
| `vscode` | `./.vscode/mcp.json` | `servers` (note: not `mcpServers`; stdio `type` required) | [VS Code docs](https://code.visualstudio.com/docs/agents/reference/mcp-configuration) |
| `gemini-cli` | `~/.gemini/settings.json` | `mcpServers` | own-knowledge |
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | own-knowledge |
| `zed` | `~/.config/zed/settings.json` / `%APPDATA%\Zed\settings.json` | `context_servers` (`{command:{...}}` shape) | [Zed docs](https://zed.dev/docs/ai/mcp) |
| `cline` | `<VS Code User>/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` | `mcpServers` | own-knowledge + [cline/cline](https://github.com/cline/cline) |

Instruction-line injection (`--instructions`) writes a short, marker-fenced
memory usage block into the host's instruction file where one exists:
`CLAUDE.md` (claude-code), `AGENTS.md` (codex), `.cursor/rules/memory.mdc`
(cursor), `GEMINI.md` (gemini-cli).

## Automatic capture

An agent that has to remember to call `remember` will sometimes not, and what
it did not store is gone. Capture closes that gap at the host, not in the
engine: the host runs a command at each lifecycle event, and four of those
events are wired to `aleth-mcp capture`.

| Lifecycle moment | Command | What it does |
|-------------------|---------|--------------|
| the user sends a prompt | `aleth-mcp capture prompt` | spools what the user said, verbatim |
| a tool finishes | `aleth-mcp capture tool` | spools one line per action: the tool and the argument that names it, never the tool's output |
| the turn ends | `aleth-mcp capture stop` | stores the user's words (origin `user`) and the agent's record (its final message plus the action list, origin `agent`), links the two, and links the turn to the previous one |
| the session ends | `aleth-mcp capture session-end` | a bounded last attempt for anything still spooled |

### Wiring it into a host

`aleth-mcp capture hook` is the same thing behind one name. It reads the event
out of the payload and routes it, so a host config names one command four times
instead of four different ones. `--hooks` writes that wiring for you:

```bash
aleth-mcp install claude-code --hooks    # ./.claude/settings.json
aleth-mcp install codex --hooks          # ~/.codex/hooks.json
aleth-mcp install gemini-cli --hooks     # ~/.gemini/settings.json
aleth-mcp install cursor --hooks         # ./.cursor/hooks.json
aleth-mcp install --all --hooks          # every host detected here
aleth-mcp uninstall cursor --hooks       # remove exactly our hooks
```

Same discipline as the server entry: merge only, timestamped `.bak`, idempotent,
your other hooks and settings untouched, and a file with comments in it is left
alone with the snippet printed for you to paste. `--project` / `--user` pick the
scope; the default is the one that makes sense per host, so Cursor's hooks go in
the repo while Codex's go in your home directory. Ship a project's `.cursor/` or
`.codex/` hooks file in version control and everyone working in that repo gets
capture.

The Claude Code plugin (`packaging/claude-code-plugin/`) and the Gemini CLI
extension (`packaging/gemini-cli-extension/`) already carry their hooks, so
installing either of those needs no `--hooks` at all.

What the record is, and is not:

- **It is the record.** Nothing is summarised or rewritten by a model. The
  user's words are stored as said; the agent's record is its own final message
  and the list of what it ran. Recall gets what happened, not a reading of it.
- **It is selective only where selection is mechanical.** A go-signal ("ok",
  "continue"), a slash command or a host-injected payload is not stored as the
  user's words; the actions it triggered still are. The agent's own `memory_*`
  calls are not recorded as actions, because they are writes already. Long
  texts are clipped, never dropped.
- **It never costs a turn.** Every path exits 0 and reports a failure as one
  line on stderr. With the engine down, the turn waits in a local spool and is
  stored at the next `stop`, of this session or any other; a window that dies
  mid-turn leaves a spool that the next `stop` anywhere sweeps up.
- **Off is one variable.** `ALETH_CAPTURE=0` in the shell the host runs in.
  The spool lives under `ALETH_CAPTURE_DIR`, else Claude Code's per-plugin data
  directory, else `~/.aleth/capture`. The engine's own settings are untouched.
- **Forgetting still works.** Something said in passing is in the store;
  `memory_forget` erases it for real, as it always did.
- **Nothing goes to stdout.** Some hosts parse a hook's stdout, so every word
  the capture has to say goes to stderr and every path exits 0. A hook never
  costs you a turn.

The distilled fact, the correction (`supersede` / `retract`) and the erasure
stay the agent's job; the plugin's skill and the extension's context file say
so at every session start.

Claude Code's payload was measured on a real run. Codex, Gemini CLI and Cursor
are wired from their published references and have not been observed on a live
install; if one of them renames a field the record gets shorter, never broken.

## Testing

`tests/test_install.py` drives the host-config writers against real temp
dirs (HOME / APPDATA redirected to `tmp_path`, no mocked file IO, no test
touches a real config dir). The handler layer is exercised end-to-end
against a real in-process engine + RPC server + wire client, no mocks,
the stdio transport the only layer not under test.

```bash
python -m pytest tests/ -q
```
