Skip to main content
Glama
README.md
# memory-agents-mcp

Unified MCP server for LAN agents (Hermes, Codex, Claude Code, ...) to share
memory: native agent memory (`memory_remember`/`memory_recall`/
`memory_forget`/`memory_list`, backed by a `memories` table in Postgres +
pgvector, embeddings via Ollama) plus a thin adapter over the existing
Obsidian-vault `memory-service` (`kb_search`/`kb_recall`/`kb_read_page`/
`kb_write_page`/`kb_ingest`/`kb_reindex`, SCHEMA.md v1.5 zone/role rules
enforced before every write).

Deployed on CT106 (`192.168.1.110:8199`, systemd) with Claude Code and
Hermes connected — see `docs/reports/phase4-deployment.md` for the full
tool catalog, connection commands, and a bug found/fixed during deploy.
Progress by phase: `openspec/changes/add-unified-memory-mcp/tasks.md`.
Runbooks: deploy/migrate the server — `docs/runbooks/deploy.md`; add a new
agent — `docs/runbooks/add-agent.md`.

Design rationale: `docs/adr/0001-unified-memory-mcp.md`.
Specs: `openspec/changes/add-unified-memory-mcp/specs/mcp-server-core/spec.md`,
`openspec/changes/add-unified-memory-mcp/specs/agent-memory/spec.md`,
`openspec/changes/add-unified-memory-mcp/specs/kb-adapter/spec.md`.

## Run it

```bash
uv sync
cp config.example.toml config.toml   # then fill in real bearer tokens and Postgres DSN
uv run python -m migrations.migrate --config config.toml   # create/upgrade the memories table
uv run python -m memory_agents_mcp --config config.toml
```

Transport is streamable-HTTP over the LAN (no stdio). Logs go to stderr and
the rotating file configured by `log_file` — never stdout.

## Config

One TOML file (`config.example.toml` documents the schema). Key points:

- `[tokens]` is a table of `bearer-token -> agent_id`. This is the **only**
  source of agent identity — a tool ever accepting an `agent_id` parameter
  ignores it. Unknown or missing tokens are rejected with 401 before any
  tool runs.
- No sessions, no activation, no TTL for agent access: a token is valid
  until removed from the config file, regardless of how long the agent has
  been idle. (`expires_at` is a per-memory-record expiry field, not an
  access mechanism — implemented in Phase 2.)
- `[embedding]` pins the model name and vector dimension (`bge-m3` / 1024).
  A mismatch between this pin and what Ollama actually reports is a
  startup config error.
- `[postgres]`, `[ollama]`, `[memory_service]` point at the shared infra
  (`agent_memory` DB on `.111`, Ollama on `.110`, memory-service on
  `.200:8101`) backing the `memory_*` and `kb_*` tools respectively.
- `[transport] allowed_hosts` — Host-header values LAN agents connect
  through (e.g. the deploy host's real `ip:port`). Required once `host` !=
  `127.0.0.1`: MCP's DNS-rebinding protection stays on and only accepts
  these (localhost is always allowed too).

## Agent memory (`memory_*` tools)

- `memory_remember(type, title, content, confidence=0.8, provenance=...,
  tags=[], visibility="shared", project=None, expires_at=None)` — embeds
  `title + content` via Ollama and inserts one row. `agent_id` always comes
  from the caller's bearer token, never a parameter. Visibility defaults to
  `shared` (agents kept forgetting to pass it and got silently-private
  records); pass `visibility="private"` explicitly for a private one.
- `memory_recall(query, type=None, agent_id=None, project=None, since=None,
  as_of=None, limit=10)` — semantic search. No `agent_id` filter: caller's
  own rows (private + shared) plus everyone else's `shared` rows. Explicit
  `agent_id`: only that agent's `shared` rows — their private rows are never
  reachable this way. Always excludes expired rows
  (`expires_at IS NULL OR expires_at > now()`); expiry never gates the rest
  of an agent's memory.
- `memory_forget(id)` — deletes by id, only if the row's `agent_id` matches
  the caller; otherwise rejected and the row is untouched.
- `memory_list(project=None, limit=20)` — caller's own recent memories (both
  visibilities), no semantic query.

Schema: `migrations/001_memories.sql` (apply with
`python -m migrations.migrate`). Field shape ported from memanto's
`MemoryRecord` — 13 memory types, confidence 0–1, provenance enum, tags,
optional `expires_at` (ADR 0001, "Решение: судьба memanto").

## Knowledge base (`kb_*` tools)

Thin adapter over the existing `memory-service` HTTP API (unrelated,
unmodified service backing the user's Obsidian vault) — this project owns no
vault data itself.

- `kb_search(query, max_results=None, min_similarity=None, filters=None)` /
  `kb_recall(topic, max_pages=None, min_similarity=None)` /
  `kb_read_page(path)` — transparent passthrough (`POST /search`,
  `POST /recall`, `GET /pages`).
- `kb_write_page(path, content, reason, role="agent", page_type=None)` —
  checks SCHEMA.md v1.5 zone/role rules (`memory_agents_mcp/kb_zones.py`,
  the one module to edit when those rules change) *before* any HTTP call;
  rejects edits touching the Sergey-only `## Заметки` section for any
  role; auto-logs a `POST /events` entry the caller cannot skip.
- `kb_ingest(path, reason, role="agent")` — `POST /ingest` + auto-event.
- `kb_reindex(force=False)` — `POST /reindex`; registered as a tool **only**
  when `admin = true` in config, per spec ("absent from the tool list",
  not merely rejected).

## Failure behavior

Dependency failures (Postgres/Ollama/memory-service unreachable, or an
embedding-dimension mismatch) are **soft-fail**: logged loudly to stderr and
the log file, but the server keeps running. This is a deliberate fix for a
prior bug (memanto's hooks swallowing failures with zero signal) — "never
break the host" must never mean "never be heard."

## Memory Viewer (local GUI)

`viewer/` is a local NiceGUI app over this server: run `viewer.bat` (or
`uv run python -m viewer`) from the repo root — the UI opens in the
browser on the port from `viewer/config.toml` (default 8299; copy
`viewer/config.example.toml` and fill in the token/DSN first).

Tabs: **Память агентов** (semantic search via `memory_recall` + a no-SQL
field-filter browser over Postgres that also shows private rows, record
card, admin delete with confirmation), **Vault** (`kb_search`/`kb_recall`/
`kb_read_page` with markdown preview, `kb_write_page`/`kb_ingest` forms),
**Тулы** (every MCP tool as a form, built from `tools/list`, raw JSON
responses), **Мониторинг** (health of MCP/Postgres/Ollama/memory-service,
the `tool_calls` journal, agent activity feed, CT106 logs over SSH),
**Настройки** (all endpoints/token/DSN editable at runtime, applied
without restart).

The server side journals every tool call into the `tool_calls` table
(migration 003, soft-fail, retention `tool_log_retention_days`, default
30 days).

## Tests

```bash
uv run pytest
```