[plugin: telegram] Concurrent sessions race on bun install, MCP server fails to start (EEXIST -> -32000 Connection closed)
Summary
The official telegram plugin (v0.0.6) runs bun install on every MCP server start, inside a shared plugin cache directory. When two or more Claude Code sessions start their telegram MCP server at the same time, the concurrent installs race on binary linking and one of them fails with EEXIST. Because the start script joins install and server with &&, a failed install means the server process never starts at all, and the client reports MCP error -32000: Connection closed.
Users running multiple concurrent sessions see this as "the Telegram bot randomly stops responding."
Environment
- Claude Code
2.1.212 - Plugin
telegram0.0.6fromclaude-plugins-official - macOS (Darwin 25.6.0), bun
- 4 concurrent Claude Code sessions with the telegram channel attached
Root cause
Two files combine into the failure:
.mcp.json — every session launches the server in the _same_ directory:
{
"mcpServers": {
"telegram": {
"command": "bun",
"args": [
"run",
"--cwd",
"${CLAUDE_PLUGIN_ROOT}",
"--shell=bun",
"--silent",
"start"
]
}
}
}
package.json — the start script installs on every launch, and gates the server behind it:
"start": "bun install --no-summary && bun server.ts 2>> $HOME/.claude/channels/telegram/daemon.log"
Chain of events:
2+ sessions start concurrently
-> concurrent `bun install` against the same node_modules
-> error: Failed to link which: EEXIST (install exits non-zero)
-> `&&` short-circuits, `bun server.ts` never runs
-> process exits immediately
-> client: MCP error -32000: Connection closed
Reproduction
A. The install race — 6 concurrent installs in a copy of the plugin directory:
#1 rc=1 error: Failed to link which: EEXIST
#5 rc=1 error: Failed to link which: EEXIST
#2,3,4,6 rc=0
Note this is timing dependent: once bun's global cache is warm the link window shrinks and the race may not reproduce. A warm-cache run is not evidence that the bug is absent.
B. The fatal coupling (deterministic) — force bun install to fail via an unresolvable dependency and observe whether the server starts:
"bun install ... && bun server.ts" -> server does NOT start
"bun install ... || true; bun server.ts" -> server starts
Evidence from MCP logs
~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-plugin-telegram-telegram/*.jsonl
The stderr line is followed 1–3 ms later by the connection failure:
18:54:52.862 Server stderr: error: Failed to link which: EEXIST
18:54:52.863 Connection failed after 434ms: MCP error -32000: Connection closed
Aggregated across sessions on this machine: 9 × Failed to link which: EEXIST, 15 × Connection failed: MCP error -32000: Connection closed.
Independent sessions fail within milliseconds of each other, which is what points at a shared resource rather than per-session flakiness:
2026-07-30T06:25:37.791Z session A
2026-07-30T06:25:37.803Z session B
Suggested fix
The && is the part that turns a recoverable install hiccup into a dead server, but the deeper issue is running bun install in a shared directory on every start. Options, roughly in order of preference:
- Do not install on the hot path. Install once when the plugin version directory is first created, or ship
node_modules. - Guard the install so it only runs when dependencies are absent.
- At minimum, decouple the failure:
bun install --no-summary || true; bun server.ts ...so a losing racer still starts.
Note that a lock is awkward here: --shell=bun has no loops, so a process that fails to acquire the lock has no way to wait — its only option is to skip the install anyway.
Worth noting that bun run already resolves dependencies from the global cache. In testing, the server started fine with a completely empty local node_modules, and that resolution path creates no symlinks and did not race (6 concurrent starts, 0 failures). That suggests the explicit bun install is a redundant safety net rather than a required one.
Workaround
Patch the cached plugin's package.json:
-"start": "bun install --no-summary && bun server.ts 2>> $HOME/..."
+"start": "[ -d node_modules ] || bun install --no-summary || true; bun server.ts 2>> $HOME/..."
With this applied, 6 concurrent starts produced 6/6 servers up, 0 EEXIST, 0 install attempts. This is overwritten whenever the plugin updates.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗