@cchez/memory-mcp
v1.0.0
Published
AI memory MCP server with Qdrant vector storage and Ollama/OpenAI embeddings
Maintainers
Readme
Memory MCP
A self-contained AI memory store for agents and developers. Stores coding rules, architecture decisions, team discussions, and project facts as semantic vectors. Any MCP-compatible agent can search and save memories across sessions.
Stack: Qdrant (vector DB) + Ollama bge-m3 (embedding) + MCP stdio server — all containerised, zero cloud dependencies.
Architecture
memory-mcp/
├── db/
│ ├── docker-compose.yml # Qdrant + Ollama containers
│ ├── .env.example # Environment variable template
│ ├── qdrant_data/ # Persisted vector data (gitignored)
│ └── ollama_data/ # Persisted model cache (gitignored)
├── src/
│ ├── index.ts # MCP stdio server
│ ├── server.ts # HTTP bulk-ingest server
│ ├── embedding.ts # Embedding provider abstraction (Ollama / OpenAI)
│ ├── qdrant.ts # Qdrant client — dual-collection, auto-routing
│ └── tools/
│ ├── store.ts # store_memory tool
│ ├── search.ts # search_memory tool
│ ├── delete.ts # delete_memory tool
│ ├── list.ts # list_memories tool
│ ├── correct.ts # correct_memory tool — supersession chain
│ └── episode.ts # capture_episode tool — opt-in task summaries
├── scripts/
│ ├── migrate.ts # One-time migration: old collection → coding/workspace
│ └── migrate-reembed.ts # Re-embed all data when switching embedding model
└── skills/
├── memory-search/ # Agent skill: when and how to search memory
├── memory-save/ # Agent skill: when and how to save to memory
└── memory-correct/ # Agent skill: when and how to correct memoryCollections
Memories are auto-routed to one of two Qdrant collections based on memory_type:
| Collection | memory_type values | Content |
|---|---|---|
| coding | rule, decision, preference | Coding constraints, architecture choices, tool preferences |
| workspace | fact, summary | Team discussions, Slack summaries, project facts, config values |
Quick Start
Step 1 — Start the database and embedding model
cd db
docker compose up -dThis starts:
- Qdrant on
http://localhost:6333(vector database) - Ollama on
http://localhost:11434(embedding server, auto-pullsbge-m3on first run)
First run takes 1–2 minutes while bge-m3 (~1.1 GB) downloads. Check progress:
docker compose logs -f ollamaVerify both are ready:
curl http://localhost:6333/collections # Qdrant
curl http://localhost:11434/api/tags # Ollama — should list bge-m3Note: If you have Ollama running locally on port 11434, stop it first to avoid port conflicts:
killall ollama 2>/dev/null || true
Step 2 — Configure environment
cp db/.env.example .envThe defaults work out of the box with the docker-compose setup. Edit .env only if you need to customise:
QDRANT_URL=http://localhost:6333
CODING_COLLECTION=coding
WORKSPACE_COLLECTION=workspace
EMBEDDING_PROVIDER=ollama # or "openai"
EMBEDDING_DIMENSIONS=1024 # must match the model (bge-m3 = 1024)
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=bge-m3
# Only needed if EMBEDDING_PROVIDER=openai
OPENAI_API_KEY=sk-...
INGEST_API_KEY=your-secret-key # for HTTP bulk-ingest auth
PORT=3000Step 3 — Install dependencies
Only needed for local development from this repo:
npm installStep 4 — Wire up the MCP server
For normal usage, run the MCP server via npm with npx. Add this to your Claude Code / Claude Desktop MCP config (.claude/settings.json or claude_desktop_config.json):
{
"mcpServers": {
"memory-mcp": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@cchez/memory-mcp@latest"
],
"env": {
"QDRANT_URL": "http://localhost:6333",
"EMBEDDING_PROVIDER": "ollama",
"OLLAMA_BASE_URL": "http://localhost:11434",
"OLLAMA_MODEL": "bge-m3",
"EMBEDDING_DIMENSIONS": "1024",
"CODING_COLLECTION": "coding",
"WORKSPACE_COLLECTION": "workspace"
}
}
}
}For local development before publishing, point the MCP host at the source file instead:
{
"mcpServers": {
"memory-mcp": {
"type": "stdio",
"command": "node",
"args": [
"/absolute/path/to/memory-mcp/node_modules/tsx/dist/cli.mjs",
"/absolute/path/to/memory-mcp/src/index.ts"
],
"env": {
"QDRANT_URL": "http://localhost:6333",
"EMBEDDING_PROVIDER": "ollama",
"OLLAMA_BASE_URL": "http://localhost:11434",
"OLLAMA_MODEL": "bge-m3",
"EMBEDDING_DIMENSIONS": "1024",
"CODING_COLLECTION": "coding",
"WORKSPACE_COLLECTION": "workspace"
}
}
}
}Why
nodeinstead ofnpm run devfor local development? Some MCP hosts don't reliably pass thecwdfield to the spawned process, causing npm to look forpackage.jsonin the wrong directory. Invokingnodewith absolute paths bypasses npm entirely and works regardless of working directory.
Replace both /absolute/path/to/memory-mcp occurrences with the actual path to this repo (e.g. /Users/you/projects/memory-mcp).
Restart Claude Code / Claude Desktop. The MCP tools will be available immediately.
Step 5 — Install agent skills (optional but recommended)
Copy the skills into your Claude skills directory so agents automatically know when to search and save memory:
cp -r skills/memory-search ~/.claude/skills/
cp -r skills/memory-save ~/.claude/skills/
cp -r skills/memory-correct ~/.claude/skills/Restart Claude Code to pick up the new skills.
Publishing to npmjs
The package is configured as @cchez/memory-mcp and exposes one executable bin, memory-mcp, backed by dist/index.js. The package uses a files allowlist so local Qdrant/Ollama data is not published.
Before publishing:
npm run build
npm run pack:dry-runFirst-time publish for a scoped public package:
npm login --registry=https://registry.npmjs.org/
npm publish --access public --registry=https://registry.npmjs.org/After publish, agent configs can use:
{
"command": "npx",
"args": ["-y", "@cchez/memory-mcp@latest"]
}The npm package only runs the MCP server. Qdrant and Ollama still need to be running separately, for example with db/docker-compose.yml from this repo.
Step 6 - Global instruction
# Memory
知识库(memory mcp)是核心资产,通过 memory mcp 访问。
## 搜索 — 先查再做
开始任何任务前,先搜索知识库获取相关上下文:
- 开始写代码或修 bug → 搜索是否有相关编码规则或约束
- 讨论架构或技术选型 → 搜索是否有已有决策
- 遇到项目、系统、集成的名称 → 搜索相关背景和配置事实
- 用户问"我们之前怎么处理 X 的" → 搜索知识库,别靠猜
搜索时设 `score_threshold: 0.5` 过滤低相关结果。不确定搜哪个 collection 就两个都搜(默认行为)。每个话题最多搜两次(换个措辞重试一次),没结果就停,不要死循环。
## 保存 — 三问过滤
发现以下信息时主动保存,不需要用户提醒:
- 编码约束或规则("方法复杂度不能超过 15,否则 CI 挂")
- 架构或技术选型决策("选择了 Qdrant,原因是…")
- 配置事实("Airwallex RFI KYC template ID 是 xxx")
- 解决了一个非显而易见的歧义
用户说"记住这个"时,立即保存。
保存前过三问:① 耐久?3 个月后还成立吗 ② 非显而易见?新人不会的知道吗 ③ 可复用?会影响下次工作吗。三问全是才保存。不保存:任务状态、短暂信息、众所周知的事实。
## memory_type 分类
| memory_type | 保存什么 | 自动路由到 |
|---|---|---|
| `rule` | 编码约束、CI 规则、强制规范 | coding |
| `decision` | 技术选型、架构决策(含原因) | coding |
| `preference` | 团队偏好、软性约定 | coding |
| `fact` | 配置值、团队现状、集成细节 | workspace |
| `summary` | Slack/会议/文档的提炼摘要 | workspace |
collection 不需要手动指定,由 `memory_type` 自动路由。
## source 格式
`slack/<频道名>` / `agent/claude-code` / `confluence/<页面名>` / `manual/<主题>`
## 删除过时记忆
发现知识库中有错误或过期内容时,用 `correct_memory` 写入修订版并 supersede 旧记忆。只有敏感信息、重复垃圾、或确实不应保留审计历史的记录才用 `delete_memory(id, collection)`。MCP Tools Reference
store_memory
Save a piece of knowledge. Content with identical text is automatically deduplicated (idempotent upsert via SHA-256 hash).
| Parameter | Type | Required | Description |
|---|---|---|---|
| content | string | yes | The knowledge to store (1–5 sentences, written for future retrieval) |
| source | string | yes | Origin: slack/channel-name, agent/claude-code, confluence/page-title, manual/... |
| memory_type | enum | yes | rule / decision / preference / fact / summary |
| tags | string[] | no | 2–5 specific tags for filtered retrieval |
| status | enum | active | active / superseded / deprecated |
| confidence | number | no | Confidence score 0–1 |
| episode_id | string | no | Task/debug episode that produced the memory |
| related_ids | string[] | no | Related memory IDs |
| supersedes | string | no | Older memory ID replaced by this memory |
| valid_until | string | no | ISO timestamp for time-sensitive facts |
| last_verified_at | string | no | ISO timestamp for last verification |
Returns: { id, collection }
search_memory
Hybrid search across one or both collections. The default hybrid mode combines vector similarity, exact keyword matching, light recency weighting, and MMR de-duplication. Superseded and deprecated memories are hidden unless explicitly requested.
| Parameter | Type | Default | Description |
|---|---|---|---|
| query | string | — | Natural language query |
| collections | array | both | ["coding"], ["workspace"], or omit for both |
| limit | number | 5 | Max results (1–20) |
| score_threshold | number | — | Min similarity 0–1 (recommended: 0.5) |
| memory_type | enum | — | Filter by type |
| tags | string[] | — | Filter — all specified tags must match |
| source | string | — | Filter by exact source string |
| include_inactive | boolean | false | Include superseded / deprecated memories |
| mode | enum | hybrid | vector, keyword, or hybrid |
| use_recency | boolean | true | Apply recency weighting to time-sensitive memories |
| use_mmr | boolean | true | Reduce duplicate-looking results |
Returns: array of { id, content, source, memory_type, collection, tags, score, vector_score, keyword_score, recency_score, status, created_at, updated_at, ...lifecycle_fields }
correct_memory
Create a corrected replacement for an existing memory. The old memory is marked superseded; the new memory is written as active with supersedes pointing at the old ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | yes | Old memory ID |
| collection | enum | yes | Old memory collection: coding or workspace |
| corrected_content | string | yes | Replacement memory content |
| correction_reason | string | yes | Why the old memory is wrong/stale |
| source | string | no | Defaults to old source |
| memory_type | enum | no | Defaults to old type |
| tags | string[] | no | Defaults to old tags |
| confidence | number | no | Defaults to 0.9 |
capture_episode
Opt-in capture for task/debug episodes. This does not ingest raw conversation or tool traces; callers provide a compact summary plus durable observations that passed a quality filter.
| Parameter | Type | Required | Description |
|---|---|---|---|
| episode_id | string | yes | Task/debug episode identifier |
| source | string | yes | Usually agent/claude-code or another agent source |
| summary | string | yes | What happened, key attempts, final conclusion |
| observations | array | yes | 1–10 structured memories with content, memory_type, optional tags and confidence |
| related_ids | string[] | no | Existing memory IDs related to this episode |
delete_memory
Delete a specific memory entry by ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | yes | ID returned by store_memory |
| collection | enum | yes | coding or workspace |
list_memories
Browse memory entries with pagination — useful for auditing or finding IDs.
| Parameter | Type | Default | Description |
|---|---|---|---|
| collection | enum | — | coding or workspace (required) |
| limit | number | 20 | Max results (1–100) |
| offset | number | 0 | Pagination offset |
| source | string | — | Filter by source |
| memory_type | enum | — | Filter by type |
| include_inactive | boolean | false | Include superseded/deprecated memories for audit |
Example Prompts
Saving memories
Remember this rule: all service methods must have cyclomatic complexity ≤ 15,
otherwise the CI pipeline fails. Source is agent/claude-code, type is rule,
tags: ci, complexity, typescript.Save to memory: we decided to use SHA-256 content hash as the Qdrant point ID
for deduplication. Same content written twice produces one record.
Source: agent/claude-code, type: decision, tags: qdrant, deduplication, architecture.Note down: Airwallex RFI hosted flow KYC template ID is "kyc_rfi_v2_au".
Source: manual/airwallex-config, type: fact, tags: airwallex, rfi, configuration.Searching memories
Search memory for any rules about code complexity limits.What do we know about our Airwallex integration configuration?
Search the workspace collection.Find all architecture decisions we've made. Search coding collection,
memory_type decision, score threshold 0.5.List everything in the coding collection so I can audit what rules are stored.Deleting stale memories
Delete memory id "a3f2b1c9..." from the workspace collection.HTTP Bulk Ingest
For automated pipelines (e.g. daily Slack summarisation jobs):
npm run dev:servercurl -X POST http://localhost:3000/ingest \
-H "Authorization: Bearer <INGEST_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"memories": [
{
"content": "Pod Pay Pilots weekly update 2026-04-22: document upload UI refactor completed. UAT sign-off from Katrina Li.",
"source": "slack/pod-pay-pilots",
"memory_type": "summary",
"tags": ["weekly-update", "document-upload", "uat"]
},
{
"content": "Decision: LaunchDarkly flag AU_discoverability_beanie_events controls Beanie event emission for AU bill pay. Default off in production.",
"source": "slack/pod-pay-pilots-engineers",
"memory_type": "fact",
"tags": ["launchdarkly", "feature-flag", "au-bill-pay"]
}
]
}'Response (all succeeded):
{ "stored": 2, "failed": 0, "succeeded": [{"index": 0, "id": "..."}, {"index": 1, "id": "..."}] }Response (partial failure — HTTP 207):
{ "stored": 1, "failed": 1, "succeeded": [...], "errors": [{"index": 1, "error": "..."}] }Health check:
curl http://localhost:3000/health
# {"status":"ok"}Switching Embedding Models
The embedding model is a hard infrastructure decision — all stored vectors must use the same model. To switch models:
- Update
.env: setOLLAMA_MODEL(orEMBEDDING_PROVIDER=openai) andEMBEDDING_DIMENSIONS - Run the re-embed migration:
# Dry-run first
MIGRATE_DRY_RUN=true node --env-file=.env ./node_modules/tsx/dist/cli.mjs scripts/migrate-reembed.ts
# Execute
MIGRATE_DRY_RUN=false node --env-file=.env ./node_modules/tsx/dist/cli.mjs scripts/migrate-reembed.tsThis drops and recreates all collections at the new vector size, then re-embeds every stored record.
Stop Services
cd db
docker compose down # stop containers, keep data
docker compose down -v # stop containers and delete all data