memory-agents-mcp
Unified MCP server for LAN agents (Hermes, Codex, Claude Code, ...) to sharememory: 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 existingObsidian-vault memory-service (kb_search/kb_recall/kb_read_page/kb_write_page/kb_ingest/kb_reindex, SCHEMA.md v1.5 zone/role rulesenforced before every write).
Deployed on CT106 (192.168.1.110:8199, systemd) with Claude Code andHermes connected — see docs/reports/phase4-deployment.md for the fulltool 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 newagent — 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
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 andthe rotating file configured by log_file — never stdout.
Config
One TOML file (config.example.toml documents the schema). Key points:
[tokens]is a table ofbearer-token -> agent_id. This is the onlysource of agent identity — a tool ever accepting anagent_idparameterignores it. Unknown or missing tokens are rejected with 401 before anytool runs.- No sessions, no activation, no TTL for agent access: a token is validuntil removed from the config file, regardless of how long the agent hasbeen idle. (
expires_atis a per-memory-record expiry field, not anaccess 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 astartup config error.[postgres],[ollama],[memory_service]point at the shared infra(agent_memoryDB on.111, Ollama on.110, memory-service on.200:8101) backing thememory_*andkb_*tools respectively.[transport] allowed_hosts— Host-header values LAN agents connectthrough (e.g. the deploy host's realip:port). Required oncehost!=127.0.0.1: MCP's DNS-rebinding protection stays on and only acceptsthese (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)— embedstitle + contentvia Ollama and inserts one row.agent_idalways comesfrom the caller's bearer token, never a parameter. Visibility defaults toshared(agents kept forgetting to pass it and got silently-privaterecords); passvisibility="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. Noagent_idfilter: caller'sown rows (private + shared) plus everyone else'ssharedrows. Explicitagent_id: only that agent'ssharedrows — their private rows are neverreachable this way. Always excludes expired rows(expires_at IS NULL OR expires_at > now()); expiry never gates the restof an agent's memory.memory_forget(id)— deletes by id, only if the row'sagent_idmatchesthe caller; otherwise rejected and the row is untouched.memory_list(project=None, limit=20)— caller's own recent memories (bothvisibilities), no semantic query.
Schema: migrations/001_memories.sql (apply withpython -m migrations.migrate). Field shape ported from memanto'sMemoryRecord — 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 novault 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 anyrole; auto-logs aPOST /eventsentry the caller cannot skip.kb_ingest(path, reason, role="agent")—POST /ingest+ auto-event.kb_reindex(force=False)—POST /reindex; registered as a tool onlywhenadmin = truein config, per spec ("absent from the tool list",not merely rejected).
Failure behavior
Dependency failures (Postgres/Ollama/memory-service unreachable, or anembedding-dimension mismatch) are soft-fail: logged loudly to stderr andthe log file, but the server keeps running. This is a deliberate fix for aprior bug (memanto's hooks swallowing failures with zero signal) — "neverbreak the host" must never mean "never be heard."
Memory Viewer (local GUI)
viewer/ is a local NiceGUI app over this server: run viewer.bat (oruv run python -m viewer) from the repo root — the UI opens in thebrowser on the port from viewer/config.toml (default 8299; copyviewer/config.example.toml and fill in the token/DSN first).
Tabs: Память агентов (semantic search via memory_recall + a no-SQLfield-filter browser over Postgres that also shows private rows, recordcard, 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 JSONresponses), Мониторинг (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, appliedwithout restart).
The server side journals every tool call into the tool_calls table(migration 003, soft-fail, retention tool_log_retention_days, default30 days).
Tests
uv run pytest