chist
MCP server for unified full-text search across chat histories from Claude Code, Codex, Cursor CLI, and Antigravity CLI, using SQLite FTS5. Provides read-only tools to search sessions, list conversations, and retrieve session details.
README
chist
日本語版は README.ja.md。
Unified full-text search across your Claude Code, Codex, Cursor CLI, and Antigravity CLI chat history. Ingests every transcript into one SQLite database and lets you search it from a web UI, an MCP server, or the command line.
Ingestion and search run on the Python 3.9 standard library — zero dependencies. Only the MCP server needs an external package.
python3 -m chist ingest # incremental import
python3 -m chist search 'trigram' # full-text search
python3 -m chist show <ext_id> # read a conversation
Why
Chat history is scattered: each CLI keeps its own format in its own directory, and none
of them can see the others. When you already solved something months ago in a different
tool, you can't find it. chist normalizes all four into one schema and one index, so a
single query reaches everything — and project_key (derived from the git remote) groups
the same project across different machines and different tools.
Features
- Four sources, one index — JSONL for Claude Code / Codex / Cursor CLI, and raw
protobuf-in-SQLite for Antigravity CLI (decoded without a
.protodefinition) - Incremental ingest — unchanged files are skipped by mtime+size; changed ones resume from a byte offset, verified against a hash of the first 4 KB
- CJK-capable search — FTS5 with the
trigramtokenizer, so Japanese substrings match (the defaultunicode61tokenizer cannot) - Web UI — Svelte SPA with search, session browsing, and statistics
- MCP server — lets an agent query its own history (
search_history,list_sessions,get_session) - Multi-machine aggregation — each machine ingests locally and pushes deltas to a central server over HTTP
- Secret masking — credentials are masked on display, and always before storage on the aggregation server
Measured performance
macOS, 1,034 files / ~1.86 GB of transcripts:
| Metric | Value |
|---|---|
| Initial ingest (all files) | 14.9 s |
| Second run (no changes) | < 0.1 s |
| Database size | 130 MB (including the FTS index) |
| Search latency | ~0.05 s |
| source | messages | sessions |
|---|---|---|
| claude-code | 49,263 | 769 |
| codex | 1,488 | 10 |
| cursor-cli | 410 | 9 |
| antigravity-cli | 225 | 3 |
Quick start (local, no Docker)
git clone https://github.com/you/chist-server.git
cd chist-server
python3 -m chist ingest
python3 -m chist search 'docker compose'
The database defaults to ~/.local/share/chist/history.db (mode 600). Override it with
--db or CHIST_DB.
Options
search -s/--source filter by source (repeatable)
--host filter by machine (repeatable)
-c/--cwd substring match on the project path
-r/--role user | assistant | system | tool
--since / --until ISO 8601 (e.g. 2026-07-01)
-n/--limit default 20
--raw do not mask secrets
show --thinking include thinking blocks
ingest --limit N cap the number of files (for smoke tests)
prune --before / --after time range (ISO 8601)
-s/--source by source
--session by session (ext_id or id)
--match substring of the body (to undo a bad import)
--dry-run report counts without deleting
-y/--yes skip confirmation (required non-interactively)
--vacuum reclaim file size after deleting
Pruning history
Ingest is append-only, so the database only grows. Measured at 3.8–5.9 KB per message (the trigram index alone is ~1.9× the body), which is roughly 150–300 MB per year in daily use. Capacity is rarely the issue — the real need is removing something imported by mistake, or a conversation you don't want kept.
chist prune --before 2026-01-01 --dry-run # check the count first
chist prune --before 2026-01-01 --yes --vacuum
chist prune --match 'password' --dry-run # undo a bad import
chist prune --session <ext_id> --yes
Conditions combine with AND. With no condition it does nothing — deleting everything is deliberately not offered (remove the database file instead).
On the aggregation server, run the same CLI inside the container:
docker exec chist-server python -m chist prune --before 2026-01-01 --dry-run
Notes:
- Sources without timestamps (Cursor CLI) are still covered by time ranges — the session's end time is used as a fallback, so nothing is silently skipped
- Without
--vacuumthe file does not shrink (space is only reused). VACUUM rewrites the database, so it needs free space equal to its size - Sessions left with no body are removed too (
--keep-sessionskeeps them). Empty sessions outside the delete set are never touched - If the original log files still exist and you clear
ingest_state, a re-ingest brings the messages back. Delete the source files to remove them for good - No prune tool is exposed over MCP — the tools are declared
read_only_hint, and a mistaken deletion cannot be undone, so deletion stays in the CLI
Server deployment
For multiple machines, run the aggregation server in Docker behind an existing reverse proxy. Each machine ingests locally and pushes only the delta.
cp .env.example .env # CHIST_TOKEN, CHIST_HOST, CHIST_BASICAUTH, …
docker compose up -d --build
# on each machine (still dependency-free)
export CHIST_TOKEN=<same token>
python3 -m chist ingest
python3 -m chist push https://chist.example.com
The compose file reads every environment-specific value from .env, publishes no host
port, and joins the proxy's network directly. The Svelte UI is built in a
node:22-alpine stage and baked into the image, so the server does not need Node
installed. See docs/deploy.md.
Web UI
Three tabs — search, sessions, statistics. Opening a URL with ?q=<query> runs that
search immediately, and #search / #sessions / #stats select the tab, so results are
shareable as links. Light/dark themes follow the OS setting by default.
Filtering by machine is available from the web UI, the CLI (--host), and MCP.
In the web UI the machine filter and column appear only when two or more machines have
pushed — with a single machine they stay hidden. The list of machines is read from the
hosts table at runtime, so adding a machine needs no configuration.
MCP server
claude mcp add --transport http chist https://chist.example.com/mcp
Implements MCP 2026-07-28 over stdio or streamable HTTP, read-only and stateless.
Tools: search_history, list_sessions, get_session. See
docs/mcp-setup.md.
For stdio use, the SDK requires Python 3.10+:
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python mcp
.venv/bin/python -m chist.mcp_server
Only chist/mcp_server.py imports mcp. Ingest, search, and the CLI stay
dependency-free on the system Python.
Tests
Standard-library unittest only — the zero-dependency rule holds for the tests too.
python3 -m unittest discover -s tests -t .
Server-side tests need the MCP SDK and skip automatically without it. To run everything, use the image:
docker run --rm -v "$PWD/tests":/app/tests:ro -w /app chist:latest \
python -m unittest discover -s tests -t /app
The suite covers behaviour that has actually broken, or could:
- masking detects secrets and leaves ordinary prose alone (labels with no value, short placeholders, everyday commands)
- session titles are masked, not just bodies — Cursor and Antigravity derive titles from the first user message
- the same ext_id under two sources is ambiguous, and
sourceresolves it - the 3-character trigram floor, and that FTS special characters never raise
- prune refuses to run without a condition, does not sweep up already-empty sessions, covers sources with NULL timestamps, and keeps the FTS index in step
- foreign keys and
ON DELETE CASCADE; deduplication viaUNIQUE (source, ext_id, part) - authentication (match, mismatch, and deny-all when no token is configured), the 64 MiB body cap, and malformed payloads
Security model
Never expose the app directly. Two route groups cannot authenticate themselves:
| Path | Why | Protection |
|---|---|---|
/mcp |
routed by the MCP SDK, bypasses the app's bearer check | proxy forwardAuth → /auth |
/ /api/* |
browsers cannot send a bearer token | proxy basicauth |
/ingest /stats /auth |
— | bearer token in the app |
/health |
— | unauthenticated by design (health checks) |
The shipped docker-compose.yml wires this up as three Traefik routers with an IP
allowlist on each. The server refuses to start without CHIST_TOKEN, so there is no
path to running it unauthenticated.
Secret masking
The local database stores raw text and masks on display (--raw disables it). The
aggregation server masks before storing, so push --raw still cannot put live
credentials into the server database.
Detected: known prefixes (sk-, ghp_, AKIA, AIza, Bearer, JWTs, PEM private
keys); labelled values — TOKEN=… plus password: / token: / secret: / api key: /
access key: and their Japanese equivalents, with the full-width colon accepted; and
credential-bearing commands (curl -u user:pass, htpasswd -nbB user pass).
A bare random string cannot be masked. With no label and no prefix there is nothing to
key on, and entropy-based detection produces too many false positives to be usable. Write
secrets with a label (password: …) and they will be masked.
Masking is best-effort, not a guarantee. It always covers message bodies and session titles (titles matter: Cursor CLI and Antigravity derive them from the first user message, so a prompt like "deploy with TOKEN=…" would otherwise leak through the title). It still has limits:
- bare random strings, as above
cwd/git_branch/project_keyare not masked — filtering and cross-machine project grouping depend on them. Keep secrets out of paths- the local database stores bodies verbatim and masks on display
Treat the database file itself as a secret. The local database is 0600; the server
keeps it inside a docker volume. Back it up on the same assumption. "Masking exists, so
the database is safe" does not hold.
Notes and limitations
- Queries shorter than 3 characters fall back to LIKE. The
trigramtokenizer cannot MATCH fewer than 3 characters; the CLI and UI switch automatically and say so. tool_call/tool_resultare stored but not indexed. Large diffs and file dumps drown out the signal. They are over 70% of all rows.- Resumed sessions keep their original
uuid. Claude Code copies prior history into a new file on resume, soUNIQUE (source, ext_id, part)keeps only one copy. Search never shows duplicates;show <resumed-id>may be missing the earlier part. - Cursor CLI has no timestamps or model names.
tsis NULL and session spans fall back to file mtime. - Codex
reasoningis usually encrypted. Only plaintextsummaryblocks are kept. - Antigravity CLI is decoded without a
.proto. Field numbers were identified from real data, so a format change could silently drop messages — watchchist statsfor unexpected drops.
Documentation
| File | Contents |
|---|---|
docs/deploy.md |
Server deployment and multi-machine aggregation |
docs/mcp-setup.md |
Registering the MCP server with each tool |
db/schema.sql |
Schema of record, with design notes |
License
MIT — see LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。