chatmem MCP server
Commit Nov 5, 2026Captures and stores local LLM chat history in an embedded Postgres database to serve relevant past context back to AI clients via MCP.
Also known as sid077/chatmem
- Stars
- 2
- Tools
- 15
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Nov 5, 2026
- License
- Apache-2.0
What is chatmem MCP?
chatmem captures and stores local LLM chat history in an embedded Postgres database to serve relevant past context back to AI clients via MCP. It exposes 15 tools, such as conversation_id, role, content.
Summary generated from the project's README
About chatmem MCP server
Capturing and storing local LLM chat history in an embedded Postgres database is what chatmem enables for developers using tools like Claude Code, Cursor, or aider. This single-binary utility acts as a local memory layer, using pgvector to store conversations on your machine so that AI clients can retrieve relevant past context on demand. It functions as a self-contained stdio MCP server that manages its own database processes.
Users can manage the lifecycle of their chat history through various commands, including initializing the database, importing existing JSONL transcripts, or installing a background daemon via systemd or launchd to ensure instant startup. The system prioritizes privacy by ensuring message content never leaves the local machine, only sending anonymous telemetry. For enhanced organization, it includes a Notion integration that can automatically synthesize conversations into structured study or debug pages with Mermaid diagrams and fact-based summaries.
What you can do with it
- 1Retrieve past conversation contextAsk your AI client to find specific information from previous chat sessions using full-text search.
- 2Backfill historical chat dataLoad existing chat transcripts from JSON files into your local database to build a complete history.
- 3Automate study note creationTransform long technical discussions into organized Notion pages containing summaries and diagrams.
- 4Maintain a warm database serviceSet up a background service so your chat history is immediately available when you start an MCP client.
Written from the project's README. Every use case is checked against a quote from it.
chatmem MCP tools 15
Detected from the READMEconversation_idrolecontentmodelproviderclient_idtoken_countquerytop_ktoken_budgetsinceuntil
| conversation_id | — | |
| role | — | |
| content | — | |
| model | — | |
| provider | — | |
| client_id | — | |
| token_count | — | |
| query | — | |
| top_k | — | |
| token_budget | — | |
| since | — | |
| until | — |
Show all 15 toolsShow fewer tools
conversation_idslimitafter
| conversation_ids | — | |
| limit | — | |
| after | — |
Automated checks
Ran Nov 14, 2026Full report →Not verified yet. A server needs a score of 80, no serious finding, and enough data for us to check. How checks work · What to fix for more points
- Source repository linked and reachablesid077/chatmem is public and reachablegithub.com/sid077/chatmem ↗Pass
- Package names this repository as its sourceNot checked
- Build provenance (Sigstore attestation)Not checked
- Open source license (OSI)Apache-2.0spdx.org/licenses/Apache-2.0.html ↗Pass
- Commit or release in the last yearLast commit or release 2026-11-05github.com/sid077/chatmem ↗Pass
- OpenSSF ScorecardNot checked
- Package name not close to a popular packageNot checked
- No critical advisory in the package or its dependenciesNot checked
- No hidden instructions to the model15 tool descriptions and the README read; no override phrases, credential paths or hidden charactersPass
- Destructive tools are labelledNo tool named like delete, drop or execPass
- Remote endpoint answers the MCP handshakeNot checked
README
View on GitHubchatmem
Local LLM chat history, served over MCP.
chatmem is a single-binary utility that captures your LLM conversations via Model Context Protocol tools any client can call (Claude Code, Cursor, aider, custom SDKs), stores everything in an embedded Postgres 18 database with pgvector on your machine, and serves relevant past context back to any LLM on demand.
Only anonymous telemetry (install ID, aggregate counters, opt-in crash reports) leaves your machine — message content never does.
Table of contents
- Status
- Quickstart
- MCP tools reference
- Commands
- Data & config paths
- Telemetry
- Architecture
- Repository layout
- Building from source
- Testing
- Development workflow
- Troubleshooting
- Uninstall
- Roadmap
- License
Status
Pre-alpha (v0.0.1-dev). Fully working platforms:
| Platform | pgvector | Distribution | Verified |
|---|---|---|---|
darwin/arm64 |
0.8.5 | Homebrew tap | Live use |
linux/amd64 |
0.8.3 | RPM (zypper/dnf) + DEB + tarball | Debian 12 container + openSUSE Leap 15.6 container |
linux/arm64 |
0.8.3 | RPM (zypper/dnf) + DEB + tarball | Debian 12 container + openSUSE Leap 15.6 container |
darwin/amd64 |
— | — | Needs an Intel-Mac pgvector.dylib |
windows/amd64 |
— | — | Needs a pgvector.dll + service story |
The version skew between macOS and Linux is intentional (Homebrew ships 0.8.5, official Postgres apt ships 0.8.3 for PG18). Both are wire-compatible for our use — same operators, same HNSW support.
Working today:
| Command | Purpose |
|---|---|
chatmem init |
Provision the local database, apply schema, print MCP client config. |
chatmem mcp |
Self-contained stdio MCP server (starts + manages embedded Postgres). |
chatmem daemon |
Long-lived Postgres process. Normally not invoked directly — chatmem install wraps this in a launchd/systemd service so it starts at login and stays warm. |
chatmem install / uninstall |
Install / remove the user-level background service (launchd on macOS, systemd --user on Linux) that runs chatmem daemon. Keeps embedded Postgres warm so MCP clients get instant startup instead of the 6-8s cold boot. |
chatmem start / stop / restart |
Start / stop / restart the background service. |
chatmem status |
Show whether the service is installed + whether PG is listening. |
chatmem doctor |
Print a self-diagnostic report — HOME, EUID, data/cache paths, port availability, telemetry state, ingest reachability, Notion status. Run this first if anything's weird. |
chatmem telemetry {enable,disable,status,dump} |
Manage anonymous telemetry; honors CHATMEM_TELEMETRY=0. |
chatmem notion {connect,status,disconnect,list,resync,sample} |
Manage Notion integration for auto-synthesizing conversations into study/debug pages. See Notion synthesis below. |
chatmem import |
Bulk-load an existing chat transcript (JSONL or JSON array) into chatmem. Great for backfilling conversations that happened outside a chatmem-connected client. See Import an existing chat below. |
Quickstart
# --- macOS or Linuxbrew (Homebrew tap — ships as a cask) ---
brew tap sid077/chatmem
brew install --cask chatmem
# --- openSUSE / SUSE (zypper self-hosted repo) ---
sudo zypper ar https://sid077.github.io/chatmem/chatmem.repo
sudo zypper --gpg-auto-import-keys refresh
sudo zypper in chatmem
# --- Fedora / RHEL (dnf, same repo) ---
sudo dnf config-manager --add-repo https://sid077.github.io/chatmem/chatmem.repo
sudo dnf install chatmem
# --- Debian / Ubuntu (direct .deb download; APT repo TBD) ---
# curl -sSLo /tmp/chatmem.deb https://github.com/sid077/chatmem/releases/latest/download/chatmem_<ver>_<arch>.deb
# sudo apt install /tmp/chatmem.deb
# --- direct download (any Linux) ---
# curl -sSL https://github.com/sid077/chatmem/releases/latest/download/chatmem_Linux_x86_64.tar.gz \
# | tar -xz && sudo mv chatmem /usr/local/bin/
# 2) bootstrap the local database — prints the JSON snippet to paste into your MCP client
chatmem init
# 3) paste into ~/.claude/mcp.json (or ~/.cursor/mcp.json), restart the client
# 4) verify tools appear
# In Claude Code, ask: "list your MCP tools" — you should see
# record_message, search_history, get_conversation from the 'chatmem' server.
Claude Code (terminal claude, 2.1.x+):
claude mcp add --scope user chatmem /opt/homebrew/bin/chatmem mcp
claude mcp list # should show: chatmem ✓ Connected
Claude Desktop app — merge into ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"chatmem": {
"command": "/opt/homebrew/bin/chatmem",
"args": ["mcp"]
}
}
}
Fully quit + relaunch the app after editing.
Codex — merge into ~/.codex/config.toml:
[mcp_servers.chatmem]
command = "/opt/homebrew/bin/chatmem"
args = ["mcp"]
startup_timeout_sec = 60
Cursor / Windsurf / other MCP clients — same JSON block as Claude Desktop but in their MCP config file. Consult the client's docs for the path.
MCP tools reference
All three tools are registered by internal/mcp/server.go. Every write conveys model/provider/client_id explicitly — the daemon never infers them — so multiple LLM clients writing into the same database stay cleanly attributed.
record_message
Store a single chat message. Opens a new conversation when conversation_id is empty (then model/provider/client_id are required).
| Field | Type | Required | Notes |
|---|---|---|---|
conversation_id |
UUID string | if continuing a conversation | Omit to open a new one |
role |
user | assistant | system | tool |
yes | |
content |
string | yes | Message text |
model |
string | when opening a new conversation | e.g. claude-opus-4-7 |
provider |
string | when opening a new conversation | e.g. anthropic, openai |
client_id |
string | when opening a new conversation | e.g. claude-code, cursor, aider |
token_count |
int | no | Optional metadata |
Returns:
{ "message_id": "<uuid>", "conversation_id": "<uuid>" }
search_history
Search stored chat history. MVP uses Postgres full-text ranking (to_tsvector + plainto_tsquery); semantic re-ranking via ONNX embeddings is the next milestone. Returns one hit per conversation (MMR-lite), packed to token_budget. The first 100,000 characters of each message are searchable; longer messages are stored in full but matched only on that prefix.
| Field | Type | Required | Notes |
|---|---|---|---|
query |
string | yes | Free-text query |
top_k |
int | no | Default 10, max 100 |
token_budget |
int | no | Total snippet tokens (default 4000) |
model |
string | no | Filter by model id |
client_id |
string | no | Filter by client id |
since |
RFC3339 string | no | Lower bound on created_at |
until |
RFC3339 string | no | Upper bound on created_at |
conversation_ids |
array of UUIDs | no | Restrict to these conversations |
Returns both a rendered text block (visible to any MCP client) and a structured payload for programmatic use:
Rendered text (Content):
2 hit(s) for "kafka retention"
── hit 1 ──
role: user
conversation: 8a2f…
message: def6…
created: 2026-07-20T10:15:32Z
score: 0.2341
snippet:
kafka retention is set at 7 days for the ingest topic…
Structured (StructuredContent):
{
"hits": [
{ "message_id": "<uuid>", "conversation_id": "<uuid>", "role": "user",
"snippet": "...", "score": 0.147, "created_at": "2026-07-20T10:15:32Z" }
]
}
(Before v0.0.2 Content was a bare hit count — Windsurf/Cascade and any client that ignores StructuredContent would show no snippet text.)
get_conversation
Fetch a conversation and its messages, ordered by created_at ascending (insertion order breaks ties). Cursor-paginated: pass the returned next_after back as after.
| Field | Type | Required | Notes |
|---|---|---|---|
conversation_id |
UUID string | yes | |
limit |
int | no | Default 100, max 500 |
after |
string | no | next_after from the previous page (<RFC3339>|<seq>), or a plain RFC3339 timestamp to return messages strictly after it |
Returns both a rendered text block and a structured payload.
Rendered text (Content):
conversation 8a2f…
model: anthropic / claude-opus-4-7
client: claude-code
started: 2026-07-20T10:15:32Z
updated: 2026-07-20T10:20:15Z
messages: 3
── user @ 2026-07-20T10:15:32Z ──
hello from chatmem
── assistant @ 2026-07-20T10:15:35Z ──
hi back
Structured (StructuredContent):
{
"conversation": {
"id": "<uuid>", "client_id": "...", "model": "...", "provider": "...",
"title": null, "started_at": "...", "updated_at": "..."
},
"messages": [
{ "id": "<uuid>", "role": "user", "content": "...", "token_count": 0, "created_at": "..." }
],
"next_after": null
}
Notion synthesis (v0.3.0 — multi-pass with coverage guarantee)
After each conversation, chatmem publishes a structured, concept-organized Notion page — a study-mode page for learning conversations, or a debug-mode page for problem-solving sessions. The pages are optimized for revision: TL;DR at the top, Mermaid diagrams for anything with structure or flow, citations back to the original messages, full transcript collapsed at the bottom.
v0.3.0 quality guarantees: the LLM extracts atomic facts from every message first, then composes the Summary from that inventory. Chatmem refuses to write to Notion unless the Summary cites ≥ 95% of the messages that carry non-trivia facts. Result: no message silently dropped, even in a 4-hour, 200-message session.
suggest_synthesize=true triggers the client-side LLM to run:
┌─ Phase 1: extract facts ──────────────────────────┐
│ get_extraction_prompt → chunk of unextracted │
│ record_facts → store, return remaining │
│ loop until extraction_complete=true │
└──────────────────────┬────────────────────────────┘
▼
┌─ Phase 2: compose Summary ────────────────────────┐
│ get_synthesis_prompt → transcript + facts + │
│ schema + quality rules │
└──────────────────────┬────────────────────────────┘
▼
┌─ Phase 3: publish (coverage-gated) ───────────────┐
│ synthesize_to_notion → validates + writes to │
│ Notion, OR returns list │
│ of missed msg uuids for │
│ the LLM to fix and retry │
└───────────────────────────────────────────────────┘
Setup (once)
- Create an internal Notion integration at https://www.notion.so/my-integrations. Copy the secret token (starts with
ntn_orsecret_). - Create a Notion page that will hold your chatmem pages. Share it with the integration (⋯ → Add connections → your integration).
- On your machine:
chatmem notion connect ntn_XXXXX --parent <page-url-or-id> chatmem notion status # verify token + parent
Restart your MCP client (Windsurf/Cursor/Claude Code). Auto-synthesis fires when a conversation crosses 20 messages, or the client-side rule triggers on suggest_synthesize=true in record_message responses.
To turn auto-synthesis off, set "auto_synthesize": {"enabled": false} in ~/.local/share/chatmem/notion.json. Fields you leave out of auto_synthesize take their defaults (idle_minutes 10, message_threshold 20, min_messages 4).
The coverage gate requires 95% of messages with non-trivial facts to be cited. synthesize_to_notion's min_coverage can raise that bar but not lower it. record_facts rejects message ids that aren't in the conversation. Each re-synthesis that changes the page bumps the version shown in the page header.
System-prompt rule for your MCP client
Paste this into your Windsurf / Cursor / Claude-Code rules so auto-fire lands:
Whenever a
record_messagetool response includessuggest_synthesize: true, immediately (a) callget_synthesis_promptfor the referencedconversation_id, (b) compose the Summary object per the schema in that prompt, (c) callsynthesize_to_notionwith{conversation_id, summary}. Do this once per conversation per trigger, before continuing the user's turn.
Page types
- Study mode (concept-heavy conversations): TL;DR · Prerequisites · Core Concepts (definition callout + body + example + why-it-matters + citations) · Mermaid diagrams · Key Insights · Code · Further Study · References · Full Transcript.
- Debug mode (fixing a broken thing): TL;DR · Status callout (resolved / partial / unresolved) · Timeline Mermaid diagram · What I Tried (attempts) · Root Cause · Resolution · Prevention · Full Transcript.
Session type is auto-classified per conversation. Diagrams are required (validation enforced) — Mermaid timeline for debug, appropriate diagram type for study when concepts warrant one.
Preview the templates without touching Notion
chatmem notion sample --type=study # prints Summary JSON + rendered blocks JSON
chatmem notion sample --type=debug
Inspect coverage
chatmem notion coverage <conversation_id>
# → total messages, messages with facts, category breakdown,
# msgs without any fact yet
Reliability
Failed Notion writes are persisted to ~/.local/share/chatmem/notion-pending/<conversation-id>.json (owner-only, since it holds the transcript) and retried automatically on the next chatmem mcp start, or manually with chatmem notion resync. Only the newest failed write per conversation is kept, and any later successful synthesis discards it. If a page was created before the failure, the retry updates that page; it never creates a second one.
Long content is split to fit Notion's API limits (2000 characters per text run, 100 blocks per request). Each transcript turn on the page shows at most 100 KB; longer messages end with a note pointing at the full text in chatmem.
Disconnect
chatmem notion disconnect # removes notion.json; published pages are untouched
Import an existing chat (v0.2.1)
Got a transcript from a chat that happened outside chatmem — ChatGPT web export, a Claude.ai conversation you saved, an aider log, etc.? Load it into chatmem with chatmem import. Then either the LLM auto-synthesizes it to Notion (if past the threshold) or you nudge it manually.
Input formats
Two formats accepted; auto-detected by the first non-whitespace character:
JSONL (one JSON object per line):
{"role":"user","content":"what is hnsw"}
{"role":"assistant","content":"a graph-based ANN index"}
{"role":"user","content":"pgvector defaults?"}
JSON array:
[
{"role":"user","content":"what is hnsw"},
{"role":"assistant","content":"a graph-based ANN index"}
]
Extra fields on each message (timestamps, source ids, tool_calls) are silently ignored — chatmem generates its own ids and timestamps.
Commands
# From a file, opening a new conversation:
chatmem import -f ./chatgpt-export.jsonl \
--model gpt-5 --provider openai --client-id chatgpt-web
# Piping from stdin:
cat ./transcript.jsonl | chatmem import --stdin \
--model claude-opus-4-7 --provider anthropic --client-id claude-web
# Appending to an existing chatmem conversation (e.g. resume a partial capture):
chatmem import -f ./followup.jsonl \
--conversation-id 8a2f... \
--model claude-opus-4-7 --provider anthropic --client-id claude-code
chatmem import attaches to a running chatmem instance's Postgres if one is up (Claude Code / Windsurf / etc. session live). Otherwise it starts its own embedded PG briefly. Either way it's safe to run.
On success it prints the new conversation UUID + a hint to synthesize it in your LLM client:
imported 47 messages into conversation 8a2f...
Next steps:
chatmem notion status
# From an LLM session with chatmem:
# "call get_synthesis_prompt for conversation 8a2f..., then synthesize_to_notion"
Converting from real chat exports
- ChatGPT
.zipexport → JSONL:jqrecipe (adjust for your export format):jq -c '.[0].mapping | to_entries | map(select(.value.message != null)) | sort_by(.value.message.create_time) | .[] | {role: .value.message.author.role, content: (.value.message.content.parts | join("\n"))}' \ < conversations.json > out.jsonl - Claude.ai export → JSONL:
jq '.[] | {role: .sender, content: .text}' < conversation.json - Anything text-based: turn each turn into
{"role":"...","content":"..."}and you're done.
Commands
Every subcommand has --help. Run chatmem with no arguments to see the top-level list.
chatmem init
Provisions the persistent data directory, extracts embedded Postgres, installs pgvector into the runtime, and applies the schema. On completion prints a ready-to-paste MCP JSON snippet with the actual binary path (os.Executable()).
Safe to re-run — all provisioning steps are idempotent.
chatmem mcp [--port <n>]
Runs a stdio MCP server for a single client. Starts embedded Postgres for the process lifetime and stops it cleanly on stdin close or SIGTERM.
Concurrent MCP clients cannot share one embedded Postgres in the MVP — each chatmem mcp process opens its own PG on the port supplied. See Roadmap for the daemon+shim architecture.
chatmem daemon [--port <n>]
Runs the long-lived Postgres foundation for the future daemon+shim architecture. Not required for MVP usage. Ends cleanly on SIGINT/SIGTERM.
chatmem telemetry {enable|disable|status}
Manage the anonymous telemetry setting. status prints the effective state, the precedence source (env | config | default), and the install id.
Data & config paths
| Kind | macOS | Linux | Env override |
|---|---|---|---|
| Data | ~/.local/share/chatmem/ |
~/.local/share/chatmem/ |
CHATMEM_HOME |
| Cache | ~/Library/Caches/chatmem/ |
~/.cache/chatmem/ |
CHATMEM_CACHE |
| Install id | <data>/install_id |
<data>/install_id |
— |
| Telemetry cfg | <data>/telemetry.json |
<data>/telemetry.json |
— |
| Postgres data | <data>/pgdata/ |
<data>/pgdata/ |
— |
| Postgres runtime | <cache>/pg-runtime/ |
<cache>/pg-runtime/ |
— |
Telemetry
chatmem can send anonymous usage pings to help track adoption. Message content, query strings, and filenames are never sent, ever. What is sent when enabled:
- install UUID (generated locally on first run, in
<data>/install_id) chatmemversion- counters for the flush window (default 5 minutes): captures, searches, gets, errors
- model + client distributions — e.g.
{"claude-opus-4-7": 12, "gpt-5": 4},{"windsurf": 8, "cursor": 8} - latency percentiles per operation — p50/p95/p99 in ms
Modes
The client operates in three modes depending on config:
| Mode | Behavior |
|---|---|
| Disabled (env or config) | Nothing accumulated. Aggregator.Record* still runs but Flush is a no-op. |
| Enabled, no ingest URL | Accumulate + periodic flush → slog.Info("telemetry flush (local-only, no ingest URL set)", ...). Local observability only. |
| Enabled, ingest URL set | Accumulate + flush → POST to <URL>/v1/ping with 3-attempt exponential backoff. Failed sends persist to <data>/pending/*.json and get drained on the next successful flush (24h TTL). Release binaries have this URL baked in (points at the maintainer's Worker); CHATMEM_TELEMETRY_URL overrides it, and chatmem telemetry status prints the effective value. |
Precedence (highest wins)
CHATMEM_TELEMETRY=0(alsofalse,off) — hard off<data>/telemetry.json({"enabled": true|false}) — persistent choicechatmem telemetry {enable|disable}— writes the above- Default: enabled
chatmem init prompts on first run (interactive TTY only) and writes the config. Non-TTY inits print a notice and leave the default; opt out non-interactively with chatmem telemetry disable or the env var.
Commands
chatmem telemetry status # current state + source + ingest URL
chatmem telemetry enable # persist enabled = true
chatmem telemetry disable # persist enabled = false
chatmem telemetry dump # list <data>/pending/*.json (unshipped pings)
Standing up your own ingest
The client posts to any endpoint that speaks POST /v1/ping with the payload documented in internal/telemetry/client.go:Payload. A ready-to-deploy Cloudflare Worker + D1 lives in server/telemetry-worker/ — one wrangler deploy and you have an endpoint. See that README for the 5-command setup.
chatmem MCP questions
What tools does chatmem MCP provide?
It exposes 15 tools, including conversation_id, role, content, model, provider.
Is my message content private?
Yes, message content never leaves your machine; only anonymous telemetry like install IDs and aggregate counters are sent.
Can I import existing chat transcripts?
Yes, you can bulk-load existing chat transcripts in JSONL or JSON array formats into chatmem.
Does it support Notion integration?
Yes, it can automatically synthesize conversations into structured Notion pages for study or debugging.