Changelog
v1.0 (2026-02): Query, import, and thread tools over a synced copy of the email-triage database. One database, finally searchable.
v1.3 (2026-04): First corruption incident. Raw SCP of SQLite binaries and a
copyFileSyncfallback both turned out to be corruption vectors on Windows. Dump+rebuild sync replaced them. One database, rebuilt instead of copied.v1.4 (2026-04): A 6-day-old orphaned process held the WAL lock and every fresh launch corrupted the DB trying to swap it. PID+heartbeat lockfile, stdin-EOF death detection, skip-if-recent sync. One database, guarded by a lock.
v1.5 (2026-05): The lock broke reconnects in a new way: zombie parents kept orphans heartbeating for days. Cooperative handoff, parent-PID polling, Windows tasklist fallback. One database, guarded by a smarter lock.
v1.6 (2026-08): The redesign. One shared daemon owns the database; sessions bridge to it over loopback HTTP. The lock, the handoff, the eviction, all made unnecessary rather than more clever. One database, one owner, any number of sessions.
The Problem
My email lives in Office 365. My AI coding sessions live in terminals. The gap between them meant every "what did the client actually agree to?" question ended in manual inbox archaeology.
Email Query MCP closes that gap: 8 tools that let any Claude Code session search a decade of mail by sender, domain, client, or keyword, pull full conversation threads, and download attachments. Read-only against the mailbox. The interesting part was never the queries.
The interesting part was one SQLite file on Windows.
A Taxonomy of Ways One File Can Ruin Your Month
The server keeps a local SQLite copy of the mail index, synced over SSH from a home server. Simple. Except:
| Failure | Cause | Lesson |
|---|---|---|
| SQLITE_CORRUPT on a byte-identical file | Raw SCP of SQLite binaries, Linux → Windows | Checksums lie. Dump SQL, rebuild locally. |
| Live DB truncated to garbage | copyFileSync fallback "succeeding" against an open file | A fallback that can't fail safely isn't a fallback |
| Every fresh launch corrupts the DB | 6-day orphaned process holding the WAL lock | Processes outlive the sessions that spawned them |
| New sessions refuse to start | The lock working exactly as designed | Fixing corruption created an availability problem |
| Sessions randomly "not connected" | Each new session evicting the previous one's server | The lock's final form: last-writer-wins musical chairs |
| Flaky handshakes since day one | The logger writing info lines to stdout, the MCP protocol channel | Found six months later, while fixing something else |
Each fix was correct. Each fix was also a patch on the same underlying mistake: every session spawned its own process, and every process thought it owned the database.
The Fix That Deleted the Problem
v1.6 stops making the lock smarter and removes the contention instead. One long-lived daemon owns the SQLite file, the sync, and the writes, serving MCP over loopback HTTP, stateless per request, so sessions share it freely and a daemon restart is invisible. Each session spawns only a tiny stdio launcher: health-check the daemon, start it if absent, bridge stdio to HTTP.
The port bind is the whole singleton mechanism now. Two daemons can't bind the same port; the loser exits. No heartbeats, no handoff timeouts, no SIGKILL takeovers.
Three design details carry the weight:
- Bind first, sync later. The daemon accepts connections immediately and initializes the database in the background; tool calls await readiness. MCP startup timeouts stopped being possible.
- Stateless per request. No session state in the daemon means a crash-and-respawn mid-conversation loses nothing. The launcher retries once and the session never notices.
- Sync only when idle. The fragile dump+rebuild swap runs when data is stale and no request is in flight, the two conditions under which it can't hurt anyone.
Results
- 8 MCP tools available to every AI session on the machine, concurrently
- N sessions, 1 database owner: the entire lock/handoff/eviction apparatus reduced to a port bind
- Zero eviction fights since v1.6: the "isn't connected in this session" error class is structurally gone
- The mailbox stays read-only: all writes are to the local cache, all owned by one process
Five versions made the lock smarter. The sixth asked why there was contention at all, and the best concurrency control turned out to be having nothing to contend for.