conversation-search
by gebeer
README.md
# conversation-search
FTS5 full-text search over Claude Code conversation history. Indexes JSONL transcripts from `~/.claude/projects/` into a persistent SQLite FTS5 database and exposes them as searchable memory. Available as both an MCP server and a CLI tool.
Based on [Searchable Agent Memory in a Single File](https://eric-tramel.github.io/blog/2026-02-07-searchable-agent-memory/) by Eric Tramel.
## Development process
The initial implementation was produced by Claude Code following a structured pipeline:
1. **PRD** (`ai-docs/features/001-conversation-search-mcp.md`) — requirements and design decisions
2. **Build spec** (`specs/conversation-search-mcp.md`) — generated from the PRD, containing exact function signatures, filtering rules, acceptance criteria, and validation commands
3. **Implementation** (`conversation_search.py`) — written by Claude Code using the build spec as instructions
The search backend was later migrated from in-memory bm25s to SQLite FTS5 (`specs/fts5-migration.md`), peer-reviewed by Claude (Opus 4.6) and Codex (GPT-5.4), and executed as 5 sequential subagent tasks.
## How it works
Claude Code stores conversation transcripts as JSONL files under `~/.claude/projects/<encoded-dir>/`. This server:
1. Discovers matching project directories via glob pattern
2. Parses JSONL into turns (user message + assistant response + tool calls)
3. Builds an SQLite FTS5 index stored at `~/.cache/conversation-search/index.db`
4. On warm start, only reparses files whose mtime/size has changed (sub-second startup)
5. Watches the filesystem for changes and reindexes (60s debounce)
6. Serves 4 MCP tools via stdio (`serve`) or SSE (`daemon`)
## Requirements
- [`uv`](https://docs.astral.sh/uv/) (Python >= 3.10 is resolved automatically)
No venv or manual install needed. `uv run` handles `mcp`, `uvicorn`, and `watchdog` automatically. No `bm25s` or other search library is required — SQLite FTS5 is part of the Python standard library.
## Installation
Add to your MCP configuration — either project-level (`.mcp.json`) or global (`~/.claude.json` under the `mcpServers` key):
```json
{
"mcpServers": {
"conversation-search": {
"command": "uvx",
"args": [
"--from", "git+https://github.com/gebeer/conversation-search.git",
"conversation-search", "connect"
]
}
}
}
```
`connect` starts a shared daemon on first use and reuses it across sessions (see [Daemon Mode](#daemon-mode-recommended-for-multiple-sessions)). For standalone stdio mode (one index per session), replace `connect` with `serve`.
The `--pattern` flag controls which project directories under `~/.claude/projects/` are indexed. It defaults to `*` (all projects) if omitted. Patterns containing `/` are treated as filesystem paths and auto-converted to the encoded directory name format.
| Pattern | Scope |
|---------|-------|
| `*` | All projects |
| `~/repos/openclaw` | Single project |
| `~/repos/*` | All repos projects |
| `~/repos/open*` | Projects starting with "open" |
| `--pattern="-home-claude-repos-*"` | Encoded format (requires `=` syntax for leading `-`) |
Restart Claude Code after changing MCP configuration.
## CLI Usage
The tool can also be used directly from the command line for scripting and debugging:
```bash
uvx --from git+https://github.com/gebeer/conversation-search.git \
conversation-search search --query "heartbeat" --limit 5
conversation-search list --project "claude" --limit 10
conversation-search read-turn --session-id "<uuid>" --turn 5
conversation-search read-conv --session-id "<uuid>" --offset 0 --limit 10
```
After the first `uvx` invocation, the `conversation-search` command is cached and can be called directly. Alternatively, use `uv run conversation_search.py <command>` from a local clone.
All CLI commands output pretty-printed JSON to stdout. Index progress is printed to stderr. Use `2>/dev/null` to suppress progress output when piping.
## Daemon Mode (Recommended for Multiple Sessions)
When running multiple Claude Code sessions simultaneously, use daemon mode to share a single
SQLite FTS5 index instead of opening separate DB connections per session.
### Setup
The default [installation](#installation) config already uses `connect`, which enables daemon mode automatically. On first session start, `connect` launches the daemon in the background. Subsequent sessions reuse it. The daemon exits after 15 minutes of inactivity.
### Manual daemon control
```bash
# Start daemon in foreground (useful for debugging)
conversation-search daemon
# Custom port and idle timeout
conversation-search daemon --port 9300 --idle-timeout 1800
# Stop daemon
kill $(cat ~/.cache/conversation-search/daemon.pid)
```
### Configuration
| Flag | Default | Description |
|------|---------|-------------|
| `--port` | 9237 | Localhost port for the SSE server |
| `--idle-timeout` | 900 | Seconds of inactivity before daemon exits |
Both flags work on `daemon` and `connect` subcommands.
### How it works
```
Claude Code session A ──┐
Claude Code session B ──┼── connect (stdio↔SSE bridge) ──► daemon (SSE on localhost:9237)
Claude Code session C ──┘ │
• one FTS5 index (~10 MB)
• one filesystem watcher
• one reindex loop
```
**Without daemon:** N sessions each open the same SQLite DB (WAL mode handles concurrent reads).
**With daemon:** Single writer/watcher; all sessions share one connection via SSE.
## Tools
### `search_conversations`
FTS5 full-text search across all indexed turns. All terms are implicitly ANDed.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | `str` | required | FTS5 search query. All terms must match (implicit AND). |
| `limit` | `int` | `10` | Max results |
| `session_id` | `str \| None` | `None` | Filter to one session |
| `project` | `str \| None` | `None` | Substring filter on project name |
Returns ranked results with `session_id`, `turn_number`, `score`, `snippet` (context window with `[[match]]` markers), `timestamp`.
#### Query syntax
| Syntax | Example | Meaning |
|--------|---------|---------|
| Keywords | `heartbeat timer` | Both must match (implicit AND) |
| Phrase | `"systemd timer"` | Exact phrase |
| Boolean | `heartbeat AND NOT clawd` | Boolean operators |
| Prefix | `buffer*` | Prefix matching |
| OR | `heartbeat OR cron` | Either term |
| Grouping | `(timer OR cron) AND heartbeat` | Grouped boolean |
| Literal | `literal:foo.bar()` | Code-like query, skips FTS5 syntax parsing |
### `list_conversations`
Browse indexed sessions with metadata.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project` | `str \| None` | `None` | Substring filter on project name |
| `limit` | `int` | `50` | Max results |
Returns sessions sorted by `last_timestamp` desc, with `summary`, `turn_count`, `cwd`, `git_branch`.
### `read_turn`
Full-fidelity retrieval of a single turn. Re-parses the source JSONL (not the index).
| Parameter | Type | Description |
|-----------|------|-------------|
| `session_id` | `str` | Session UUID |
| `turn_number` | `int` | Zero-based turn index |
Returns complete `user_text`, `assistant_text`, and `tools_used` with rendered tool details.
### `read_conversation`
Paginated reading of consecutive turns from a session.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `session_id` | `str` | required | Session UUID |
| `offset` | `int` | `0` | Starting turn |
| `limit` | `int` | `10` | Number of turns |
## Usage pattern
The 4 tools are automatically exposed to the assistant via MCP — no extra instructions in `CLAUDE.md`, `AGENTS.md`, or similar files are needed. The server also provides `instructions` metadata through the MCP protocol to guide the assistant on effective usage.
Search wide, then read deep:
```
search_conversations("ProcessWire login redirect") -> find relevant turns
read_turn(session_id, turn_number) -> get full context
read_conversation(session_id, offset, limit) -> read surrounding turns
```
FTS5 requires all query terms to match (implicit AND). Use specific keywords for best results. For either-or matching, use explicit `OR`. For code-like queries with special characters, use the `literal:` prefix.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues