Skip to content

About

给 AI Agent 的长期记忆服务,通过 MCP 与 Python SDK 接入。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

qcdoc-mem

Persistent long-term memory for AI agents, served over MCP.

Connect an MCP client, pass a user_id, and the agent remembers: facts are extracted from conversations, stored as plain files on disk, and handed back for injection into later sessions. No database to operate, no vector store, no embedding pipeline.

agent ──► memory_remember_conversation ──► LLM extraction ──► facts/*.md + memory.json
agent ◄── memory_context / memory_search ◄── budgeted injection ◄──┘

python mcp tests license


Why

LLMs are stateless. Every session starts blank, so the user re-introduces themselves, re-states preferences, and re-corrects the same mistakes. qcdoc-mem is the memory subsystem that fixes that, packaged as a standalone service:

  • It lives outside your agent. One process serves any number of agents over MCP or an in-process SDK. Your client only needs the tool names.
  • It decides what to remember. You hand over the conversation; an LLM extracts durable facts, scores their confidence, drops the noise, dedupes near-identical entries, and merges fragments over time.
  • It is inspectable. Memory is Markdown and JSON on disk. You can read it, grep it, diff it, edit it, back it up with rsync, and recover it without a database.

What you get

10 MCP tools recall, explicit saves, automatic conversation extraction, fact CRUD, erasure, flush, status. See docs/tools.md.
Two write paths memory_remember for facts you can already state (no LLM needed); memory_remember_conversation to hand over a session and let extraction decide.
Three-layer isolation per user_id, per agent_name, with an explicit strict_user_scope switch for multi-tenant deployments.
Lifecycle management confidence gates, two-level dedup, max_facts capacity eviction, staleness review, and fragment consolidation. See docs/architecture.md.
Budgeted injection the memory block is ranked and truncated to a token budget, with correction facts in a protected reserve.
No infrastructure Markdown facts + a JSON summary + an embedded SQLite FTS5 index that is derived and rebuildable. See docs/operations.md.
Swappable backend the whole memory system sits behind a MemoryManager contract. See docs/extending.md.

Quick start

Requires Python 3.12+.

uv sync                      # or: pip install -e .
uv run qcdoc-mem-mcp         # streamable HTTP on http://127.0.0.1:8130/mcp

Or, for a client that spawns the server itself:

uv run qcdoc-mem-mcp --transport stdio

Point your MCP client at it:

{
  "mcpServers": {
    "qcdoc-mem": {
      "type": "http",
      "url": "http://127.0.0.1:8130/mcp"
    }
  }
}

Optional extras:

uv sync --extra cjk          # jieba word segmentation, better Chinese/Japanese/Korean recall

No built-in authentication. The server binds to loopback by default for exactly that reason. If you expose it, put authentication in front of it -- anyone who can reach the port can read and write every user's memory.

Give it an LLM

Reads, explicit saves, and fact CRUD work with no model. Automatic extraction from conversations needs one. Point qcdoc-mem at any OpenAI-compatible endpoint:

# qcdoc-mem.config.yaml
memory:
  backend_config:
    model:
      provider: openai                      # anything init_chat_model supports
      model: deepseek-chat
      base_url: https://api.deepseek.com/v1

Run qcdoc-mem-mcp and call memory_status: it reports llm_configured: true when the model resolved. A misconfigured model does not crash startup -- reads keep working and extraction fails at call time with a clear error.

The API key comes from the environment, never from the file -- the config file is not interpolated, so a ${VAR} written there would be sent as the key verbatim. Either export QCDOC_MEM_LLM_API_KEY, or omit api_key entirely and let the provider read its own variable (OPENAI_API_KEY, ANTHROPIC_API_KEY, ...). See docs/configuration.md.


The 10 tools

Tool One line Needs LLM
memory_context Load memory as injection-ready text. Call once at the start of a task. no
memory_search Find facts by query. Call before asking the user to repeat themselves. no
memory_get Return the whole memory document (summaries + facts). For inspection/export. no
memory_remember Store one durable fact. Idempotent on exact content. no
memory_remember_conversation Hand a session over for automatic extraction. yes
memory_update_fact Edit a fact by id; omitted fields keep their value. no
memory_delete_fact Delete one fact by id. no
memory_forget Erase a user's memory and cancel its pending extractions. no
memory_flush Force pending extractions to run now. no
memory_status Report backend, mode, storage path, whether a model is configured. no

Plus one MCP resource: qcdoc-mem://status.

Every tool takes optional user_id / agent_name; set default_user_id on the server and a single-tenant client can omit them entirely.

Full reference -- every parameter, every return shape, when to call it and in which phase of the agent loop: docs/tools.md.

Errors

Tools never raise at the model. They return {"error": "...", "kind": "..."}, where kind is one of:

kind Meaning
invalid_request The caller's input was wrong (empty query, unknown role, bad confidence).
unsupported The configured backend does not implement that operation.
backend_error Storage or the backend failed.

How it works

write path (asynchronous, batched)
  client ── memory_remember_conversation ──► debounce queue
            ──► prompt assembly (current memory + conversation + hints)
            ──► one LLM call ──► JSON with 6 decision types
            ──► deterministic gates ──► dedup ──► capacity eviction ──► files

read path (synchronous, per turn)
  client ── memory_context ──► load bucket ──► rank ──► token budget ──► text
  client ── memory_search  ──► FTS5 (or lexical relevance) ──► ranked facts

The design in one paragraph: one conversation turn produces at most one LLM call, and that call is asked to do six jobs at once -- update the six summary sections, add new facts, reinforce confirmed facts, remove contradicted facts, re-adjudicate aged facts, and merge fragments. Everything the model proposes then passes through deterministic code: a write gate that rejects anything not scope=user + durable + descriptive, a confidence floor, exact and near-duplicate dedup, and a capacity cap. The model proposes; the code decides.

Read docs/architecture.md for the full mechanism -- data model, both pipelines step by step, every gate, the concurrency protocol, and the isolation model.


Using it in an agent loop

qcdoc-mem does not hook your agent -- it cannot see your turns. The client owns three moments:

Phase Call Why
Session start memory_context Prepend the returned text to the system prompt.
Before asking the user to repeat memory_search Only if facts were not already injected.
Session end memory_remember_conversation then memory_flush Capture the session; the flush matters because extraction is batched.

The server's MCP instructions tell the model when to make these calls, so a client that surfaces them gets the behaviour with no integration code. If you want automatic capture/injection at the framework level (LangGraph middleware, turn hooks), you write that layer -- see docs/integration.md for recipes and for the automatic-capture behaviour that this package deliberately leaves to you.


SDK

The same operations in-process, no MCP:

from qcdoc_mem.contract import get_memory_manager
from qcdoc_mem.service import MemoryService
from qcdoc_mem.settings import configure_defaults

server_settings = configure_defaults()          # env + optional config file
service = MemoryService(get_memory_manager(), server_settings)

service.remember("prefers uv over pip", user_id="alice", category="preference")
print(service.context(user_id="alice")["context"])
print(service.search("uv", user_id="alice")["results"])

Or talk to the backend contract directly, bypassing the facade:

from qcdoc_mem.contract import HostHooks, MemorySettings, build_memory_manager

manager = build_memory_manager(
    MemorySettings(backend_config={"storage_path": "/var/lib/qcdoc-mem"}),
    HostHooks(),
)
print(manager.get_context("alice"))

Documentation

Start at docs/README.md, which maps every document and gives lookup tables by task ("store a fact now", "why is my read empty", "point it at DeepSeek", ...).

Document What is in it
docs/README.md The index: reading paths by role, and "I want to..." lookups.
docs/tools.md Every MCP tool: parameters, returns, when to call it, which phase of the loop, worked examples, error handling.
docs/architecture.md Principles, data model, the write and read pipelines step by step, gates, dedup, eviction, staleness, consolidation, concurrency, isolation, observability.
docs/integration.md Putting it in an agent loop: phases, recipes, multi-tenant identity, and the automatic-capture layer this package does not ship.
docs/configuration.md Full config reference: precedence, every environment variable, every CLI flag, every backend field with defaults and bounds.
docs/operations.md Storage layout, backup and recovery, migration, concurrency limits, troubleshooting, known limitations.
docs/extending.md Swapping the backend, the storage class, the retrieval adapter, the prompts, and the signal patterns.
examples/README.md Five runnable examples: in-process SDK, agent loop, multi-tenant isolation, a real MCP client, and live LLM extraction.

Where memory lives

<data-dir>/
├── .retrieval/memory-fts5.sqlite3          derived FTS5 index (rebuildable)
└── users/{user_id}/
    ├── memory.json                         six summary sections + revision
    ├── .memory.lock                        cross-process lock
    └── agents/{agent_name}/
        ├── facts/{ab}/{fact_id}.md         one Markdown file per fact
        └── .metadata/                      access counters, eviction audit

Data root resolution: memory.backend_config.storage_path > $QCDOC_MEM_DATA_DIR > ~/.qcdoc-mem. Details, including what is safe to delete, are in docs/operations.md.


Development

uv run pytest                        # 58 tests
uv run pytest tests/test_mcp_server.py -q
uv run python examples/run_all.py    # every offline example, ~5s
uv run python scripts/check_doc_links.py   # docs links and anchors

The MCP tests drive the real tool-dispatch path over FastMCP's in-memory transport, so nothing binds a port. 04_mcp_client.py is the exception: it starts a real server on a free port and drives it over HTTP, then shuts it down.

Credits

The memory backend under src/qcdoc_mem/backends/qcdoc_mem/ is derived from the memory subsystem of deer-flow, at backend/packages/harness/deerflow/agents/memory/, adapted into this standalone service. The adaptation is mechanical: module paths, class names, the environment-variable prefix, the configuration filename and the default storage root were renamed, and the host-coupled layers were replaced by this package's own contract/, service.py, settings.py and mcp/ packages. The storage, extraction, retrieval and lifecycle logic itself originates upstream.

That project is MIT licensed. Its copyright notice is reproduced in THIRD-PARTY-NOTICES.md, which ships with the package.

License

MIT -- see LICENSE. Third-party copyright notices are in THIRD-PARTY-NOTICES.md.

About

给 AI Agent 的长期记忆服务,通过 MCP 与 Python SDK 接入。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages