Skip to main content
Glama
Shared-Memory MCP

Shared-Memory MCP

by 0xC0FFEEBEEF
README.md
# Shared‑Memory MCP for AI Tools

A self‑hosted **shared memory system** that lets multiple AI tools — Claude Code,
Codex, and a local OpenCode‑powered model — share one canonical conversation
history. Everything is stored in a single **PostgreSQL** database and exposed to
the tools over **MCP (stdio)**. The memory is append‑only with *supersede without
delete*: a changed fact or decision simply appends a newer row, so the full
history is preserved and can be queried.

## Features

* **Canonical history** – raw messages, facts, decisions, preferences, events, and
  project states all live in one PostgreSQL database shared by every tool.
* **Idempotent appends** – messages are deduplicated by `source_id`, so retries and
  re‑sends never create duplicates.
* **Full‑text search** – `to_tsvector` / `ts_rank` over the message body, with a GIN
  index. Search spans *all* sessions/providers in one query.
* **Optional semantic search** – when `MEMORY_USE_EMBEDDINGS=1`, each message also
  stores a deterministic local embedding in a real **pgvector** column, and retrieval
  becomes a hybrid FTS + cosine‑similarity search. Off by default.
* **Pagination** – read a session's messages page by page.
* **Current context** – one tool returns the latest facts, decisions, preferences,
  project state, and recent events for a project *across all its sessions*.
* **Append‑only streams** – facts / decisions / preferences / events / project states
  are stored as an immutable log; changing a value just appends a new row.
* **Change tracking** – every write is tagged with a `source_client`, so you can ask
  "what did Codex change?" and get a diff of another client's writes.
* **Credential rejection** – appends that look like secrets (`sk-…` keys) are refused.
* **MCP, not REST** – the server speaks MCP over stdio (the transport Claude Code,
  Codex, and OpenCode use).

## Architecture

```
  +-----------------+     MCP / stdio      +--------------------+
  |  Claude Code    | ------------------> |                    |
  |  Codex          |  (shared-memory)    |  PostgreSQL        |
  |  OpenCode AI    | <------------------ |  (pgvector + FTS)  |
  +-----------------+                      +--------------------+
```

Each tool is a thin wrapper over `memory.crud`. The database is the single source
of truth; there is no in‑server cache to sync. Two clients writing concurrently
see each other's rows the next time they query — there is nothing to "sync", only
one shared store.

## Requirements

* **Python 3.12+**
* **PostgreSQL 16 + pgvector**. This repo ships with a bundled embedded
  PostgreSQL 16.2 (with the `vector` extension) so you can run everything
  locally with **no Docker and no system package installation**. You can also
  point it at any other PostgreSQL via `DATABASE_URL`.

## Quick Start (embedded PostgreSQL, no Docker)

```bash
git clone <this-repo> && cd shared-memory
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# 1. Start the bundled PostgreSQL on port 5433 (idempotent).
scripts/start-pg.sh start

# 2. Create the database + schema. Either path works and produces the same schema:
#    (a) migrations via Alembic (preferred for versioning/dev parity):
#        export DATABASE_URL="postgresql+asyncpg://memory:memory@localhost:5433/memory"
#        alembic upgrade head
#    (b) or just run the MCP server — it boots the schema itself on startup.

# 3. Run the MCP server (stdio).
python -m memory.mcp_server
```

### Environment variables

| Variable | Default | Meaning |
|----------|---------|---------|
| `DATABASE_URL` | `postgresql+asyncpg://memory:memory@localhost:5433/memory` | async SQLAlchemy URL. Override for any PostgreSQL. |
| `MEMORY_USE_EMBEDDINGS` | `0` | `1` enables the optional local pgvector embedding + hybrid search. |
| `PG_POOL_SIZE` / `PG_MAX_OVERFLOW` | `10` / `10` | asyncpg pool sizing. |

## Database setup & migrations

The schema is defined once, in the ORM (`memory/models.py`), and two independent
paths produce an identical database:

* **`memory.database.init_schema()`** – idempotent bootstrap used by the runtime
  and tests. Creates the pgvector extension, all tables, and indexes.
* **Alembic** – `alembic/versions/2026_09_27_initial.py` mirrors the ORM exactly.
  Use it to evolve the schema over time:

```bash
export DATABASE_URL="postgresql+asyncpg://memory:memory@localhost:5433/memory"
alembic upgrade head     # apply migrations
alembic check            # verify no drift from the ORM ("No new upgrade operations detected.")
alembic downgrade -v -1  # step back one migration
```

> **Note:** the runtime app uses **asyncpg** (async), but Alembic's migration
> context is synchronous, so `alembic/env.py` transparently rewrites the URL to the
> pure‑Python **pg8000** driver for migrations only. See `alembic/env.py`.

## Using the MCP server from a client

The server is launched over **stdio** (see `memory/mcp_server.py` entrypoint).
Configure any MCP client to spawn it. Every client starts the server from its
own working directory, so set `PYTHONPATH` to the project root, or the import
`python -m memory.mcp_server` fails with `ModuleNotFoundError`. The examples
below point `DATABASE_URL` at the shared `memory` database so every client sees
the same history.

### Claude Code

Use `-s user` to make it available in every project (there is no
`~/.claude/projects/.claude-mcp.json`); `-s project` writes `.mcp.json` in the
project root instead.

```bash
claude mcp add -s user shared-memory \
  -e DATABASE_URL=postgresql+asyncpg://memory:memory@localhost:5433/memory \
  -e PYTHONPATH=/path/to/llmmemory \
  -- /path/to/llmmemory/.venv/bin/python -m memory.mcp_server
```

### Codex

Codex is configured in `~/.codex/config.toml`, which is **TOML**, not JSON. You
can add the entry by hand:

```toml
[mcp_servers.shared-memory]
command = "/path/to/llmmemory/.venv/bin/python"
args = ["-m", "memory.mcp_server"]
env = { DATABASE_URL = "postgresql+asyncpg://memory:memory@localhost:5433/memory", PYTHONPATH = "/path/to/llmmemory" }
```

or equivalently with the CLI (`codex mcp add shared-memory --env KEY=VALUE ... -- <command>`).

### OpenCode

OpenCode v2 uses `mcp.servers.<name>` in its config, not `mcpServers`. Use the
CLI so the format comes out right:

```bash
opencode mcp add --global shared-memory \
  --env DATABASE_URL=postgresql+asyncpg://memory:memory@localhost:5433/memory \
  --env PYTHONPATH=/path/to/llmmemory \
  -- /path/to/llmmemory/.venv/bin/python -m memory.mcp_server
```

> **OpenCode note:** the user's OpenCode runs inside a network‑isolated sandbox
> that cannot reach the host's PostgreSQL. Reaching it requires a bridge added to
> the sandbox wrapper — outside this project, so it is listed under
> [What still needs the user](#what-still-needs-the-user).

All three clients point at the **same** `DATABASE_URL`, so they share one
canonical history — there is nothing to "sync", only one shared store.

### MCP tool reference

Every tool returns a plain dict wrapped as `{"is_error": false, …}`, or a
structured error `{"is_error": true, "code": …, "error": …}` — **tools never raise
on business errors**, so clients can inspect results programmatically.

| Tool | Purpose |
|------|---------|
| `list_projects()` | List all projects. |
| `create_project(name, description?)` | Create a project. |
| `start_session(provider, source_session_id, title?, project_id?)` | Create/find a session (idempotent by provider+source). |
| `append_message(session_id, source_message_id, content, original_timestamp, role?, order?, payload?, provider?, source_client?)` | Append a message (idempotent by source_id). |
| `read_session(session_id, cursor?, limit?)` | Read a session's messages, paginated. |
| `get_message(message_id)` | Fetch one message. |
| `search_history(query, limit?, source_client?)` | Full‑text (+ optional vector) search across **all** history. |
| `record_fact` / `record_decision` / `record_preference` | Record a sourced, append‑only memory value. |
| `record_event(type, payload?, …)` | Record an event. |
| `get_project_context(project_id?, session_id?, limit?)` | Latest facts/decisions/preferences/project state + recent events for a project. |
| `inspect_project_history(project_id, limit?, after?)` | Full append‑only timeline (facts/decisions/preferences/events/project_states). |
| `record_project_state(project_id, state_json, …)` | Snapshot a project's current state (append‑only). |
| `find_changes(other_client, since?, project_id?, limit?)` | List records written by another `source_client`. |

## Capture: automatic, hook, or manual

There are three ways data gets **into** memory. Be explicit about which one your
integration uses.

* **Automatic capture is not built yet.** Nothing is captured automatically
  today: none of these three clients sends its conversation to an MCP server on
  its own. The server only sees what the model chooses to send through tool
  calls — that is **manual capture**, and it will miss turns.

  Automatic capture needs a small importer per client. The importer reads the
  client's own session store and calls `append_message`, which is idempotent by
  `source_id`, so re‑reading is safe. Planned importers:

  * **Claude Code** – hooks configured in `settings.json` (e.g. `Stop`,
    `SessionEnd`) receive JSON on stdin that includes `session_id` and
    `transcript_path`. Transcripts are JSONL files at
    `~/.claude/projects/<project>/<session-id>.jsonl`.
  * **Codex** – sessions are JSONL files at
    `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`; a `notify` program set in
    `config.toml` runs after each turn.
  * **OpenCode v2** – sessions live in its SQLite database `opencode.db`, in
    tables `session_v2` and `session_message`. The sandboxed install keeps that
    file under the sandbox's own home directory.

* **Hook / on‑demand capture** – the client captures only at specific points
  (when a tool call returns, on session end, when a fact is surfaced). It is
  still client‑code deciding *when* to call `append_message` / `record_fact`.

* **Manual capture** – a human or agent calls `record_fact`, `record_decision`,
  `record_preference`, `record_event`, or `record_project_state` directly. This
  is how structured, high‑signal knowledge (a decision, a preference) gets in
  without a full message stream.

All three write to the same tables, so the data is indistinguishable downstream —
each write is tagged with `source_client` and can be found via
`find_changes` / `inspect_project_history`.

> **Known limitation:** the credential filter refuses a whole message rather than
> blanking out just the secret, so a captured transcript will have gaps wherever
> a message trips it. This is intentional for now; the filter is deliberately
> not changed.

## Two‑client demo

`examples/two_client_demo.py` simulates **Claude** and **Codex** sharing one
database and proves cross‑client visibility:

```bash
scripts/start-pg.sh start
python -m examples.two_client_demo
```

The demo writes to its **own** throwaway `memory_demo` database, read from
`DEMO_DATABASE_URL` (default `postgresql+asyncpg://memory:memory@localhost:5433/memory_demo`),
so it never needs `DATABASE_URL` and never touches the shared `memory` database.
It resets `memory_demo` before running, so re‑running is safe.

It shows Claude writing one session and Codex another on the same project, each
client seeing the other's writes via `find_changes`, and project context + search
spanning both sessions.

## Security

The store contains conversation history and derived knowledge. Two guards are in
place:

* **Credential rejection** – appends whose content looks like a secret
  (e.g. `sk-…` API keys) are refused by `memory.crud_secret`.
* **Never store secrets** – it is the caller's responsibility not to send secrets;
  the guard is a safety net, not a substitute. For production, front the database
  with TLS + auth and audit access.

## Running the tests

The suite runs against a **real PostgreSQL** instance (the bundled embedded one by
default).

```bash
scripts/start-pg.sh start
export LD_LIBRARY_PATH="$PWD/.embedded-pg/pginstall/lib"
export PYTHONPATH="$PWD"
python -m pytest
```

Tests cover: idempotent appends, conflict detection, full message field storage,
pagination, cross‑client visibility, full‑text search (+ `source_client` filter),
fact supersession, decisions/preferences, project context, events, project states,
`find_changes`, credential rejection, and structured error reporting. There is also
`test_embeddings.py`, which exercises the real pgvector semantic search path
(`MEMORY_USE_EMBEDDINGS=1`).

## Project layout

```
memory/            ORM models, async engine, CRUD, MCP server, embeddings
alembic/           schema migrations (mirror the ORM)
scripts/           start-pg.sh / bootstrap-pg.sh for the embedded PostgreSQL
examples/          two_client_demo.py — cross-client write/read demo
tests/             pytest integration suite against real PostgreSQL
```

## What still needs the user

A few things are out of scope for this repo and must be done on the user's side.

* **OpenCode sandbox → host PostgreSQL.** The user's OpenCode runs inside a
  network‑isolated sandbox that cannot reach the host's PostgreSQL. Pointing the
  MCP server's `DATABASE_URL` at a reachable database (or adding a bridge to the
  sandbox wrapper) is outside this project and must be arranged there.

* **Automatic capture.** None of the three clients streams its conversation to
  this server automatically (see [Capture](#capture-automatic-hook-or-manual)).
  The per‑client importers described there still need to be written.

* **`DATABASE_URL` in production.** The examples default to the local embedded
  server on port `5433`. Point `DATABASE_URL` (and `DEMO_DATABASE_URL` for the
  demo) at your real PostgreSQL to run against it.

## Contributing

Please run `pytest` locally and add tests for new features. Keep the ORM
(`memory/models.py`) and the latest Alembic migration in sync, and verify with
`alembic check`.

## About this copy

A public snapshot of my private working repo. More of my projects: [nbdevlab.com](https://www.nbdevlab.com).


## About this copy

A public snapshot of my private working repo. More of my projects: [nbdevlab.com](https://www.nbdevlab.com).

## License

GNU General Public License v3.0 or later, see [`LICENSE`](LICENSE).

<sub>Preparing this public copy from my private repo was done with help from Claude.</sub>