Thunderbird MCP Race Condition Fixed — Downstream Fork Pattern Established
infrastructureWhat I did
Investigated why the thunderbird-mail MCP server was consistently unavailable at Claude Code startup, requiring a manual /exit and restart to connect. Diagnosed the root cause, applied two fixes, and set up a sustainable process for maintaining the fix against upstream updates.
Root cause
The Thunderbird MCP uses two components:
- Thunderbird extension — starts an HTTP server on a random port and writes a connection file at
~/Downloads/thunderbird.tmp/thunderbird-mcp/connection.jsoncontaining the port and auth token. mcp-bridge.cjs— a Node.js process Claude Code spawns at startup. It reads the connection file and forwards tool calls to Thunderbird over HTTP.
When Claude Code starts, it calls tools/list to discover available tools. This call is forwarded by the bridge to Thunderbird. If the connection file does not exist at that moment — because Thunderbird is still starting, including any time spent on a Proton Bridge reconnection retry — the bridge waited only 5 seconds before failing.
Claude Code caches the result of tools/list and never re-queries it during a session. So any failure at startup meant zero Thunderbird tools for the entire session.
Fix 1: increase the retry window
In ~/src/work/thunderbird-mcp/mcp-bridge.cjs, changed:
const CONNECTION_MAX_RETRIES = 5; // 5 seconds
to:
const CONNECTION_MAX_RETRIES = 90; // 90 seconds
This covers the full window of a slow Thunderbird startup including the Proton Bridge retry.
Fix 2: council digest script waits rather than skips
The existing check in ~/.local/bin/council-digest.sh immediately skipped if the connection file was absent. Changed to poll every 10 seconds for up to 3 minutes before skipping. The log now records how long it waited, which will help identify if the window ever needs adjusting.
Downstream fork pattern
mcp-bridge.cjs lives in a cloned upstream repository (TKasperczyk/thunderbird-mcp). A git pull would overwrite the fix. To protect it permanently:
- Forked the upstream repo to
callenb/thunderbird-mcpon GitHub. - Renamed the local
originremote toupstream; pointedoriginat the fork. - Created a
localbranch with two commits on top of upstream’smain:- The 90-second fix
- A GitHub Actions workflow for automated sync
- Pointed the local
mainbranch atupstream/mainsogit pullonmainfetches from upstream, not the fork. - Pushed
localto the fork.
GitHub Actions sync workflow
A workflow at .github/workflows/sync-upstream.yml runs daily at 06:00 UTC and on manual trigger. It:
- Checks whether upstream has new commits; exits silently if not.
- Fetches
upstream/mainand rebaseslocalon top. - Force-pushes the updated
localbranch to the fork (using--force-with-lease). - If the rebase fails due to a conflict, opens a GitHub issue on
callenb/thunderbird-mcpwith exact resolution commands. Does not open a duplicate if one is already open.
Write permissions for the workflow were enabled under Settings → Actions → General → Workflow permissions.
Files changed
| File | Change |
|---|---|
~/src/work/thunderbird-mcp/mcp-bridge.cjs |
CONNECTION_MAX_RETRIES: 5 → 90 |
~/.local/bin/council-digest.sh |
Connection file check: immediate skip → wait up to 180s |
~/src/work/thunderbird-mcp/.github/workflows/sync-upstream.yml |
New — daily upstream rebase Action |
Knowledge captured
A reference note explaining the problem, the reasoning, and the full implementation steps was written to:
~/vaults/second-brain/00_Inbox/2026-04-24 - Maintaining local patches on upstream Git repositories.md