Telegram MCP plugin doesn't exit when Claude Code closes (orphaned bun server.ts processes accumulate over uptime)
Summary
The telegram plugin from claude-plugins-official runs an MCP server over stdio (StdioServerTransport). When the Claude Code parent process exits, the MCP server should detect the closed stdin and exit gracefully — but instead it stays alive as an orphaned process (ppid=1) and keeps running indefinitely.
Over multi-day / multi-week uptime, these orphans accumulate to dozens of processes, each consuming 50–80% CPU (the grammy Telegram bot library keeps the event loop hot), driving system load average past 100 on a 10-core machine.
Environment
| | |
|---|---|
| OS | macOS 26.5 (build 25F71) |
| Hardware | Mac Studio, Apple M1 Max (10 core) |
| Claude Code | 2.1.199 |
| bun | 1.3.11 |
| Plugin | claude-plugins-official/telegram v0.0.6 |
| MCP SDK | @modelcontextprotocol/sdk ^1.0.0 (per plugin package.json) |
| System uptime at reproduction | 38 days |
Repro
- Install the plugin:
/plugins install claude-plugins-official/telegram - Configure and use it in a Claude Code session, then exit Claude Code.
- Repeat across sessions over several days without rebooting.
- Observe accumulating
bun server.tsprocesses withppid=1.
Diagnosis
After 38 days of uptime with routine Claude Code use:
$ pgrep -f "bun server.ts" | wc -l
35
$ for pid in $(pgrep -f "bun server.ts" | head -5); do
printf " pid=%s ppid=%s cwd=%s\n" \
$pid \
$(ps -o ppid= -p $pid | tr -d ' ') \
$(lsof -p $pid 2>/dev/null | awk '/cwd/{print $NF; exit}')
done
pid=12124 ppid=1 cwd=/Users/michaelzhao/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6
pid=80498 ppid=1 cwd=/Users/michaelzhao/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6
pid=28804 ppid=1 cwd=/Users/michaelzhao/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6
pid=65370 ppid=1 cwd=/Users/michaelzhao/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6
pid=28643 ppid=1 cwd=/Users/michaelzhao/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6
$ top -l 2 -o cpu -stats pid,command,cpu,time | grep bun | head -8
12124 bun 74.4 849 hrs ← running 35 days as an orphan
32587 bun 55.4 721 hrs
3955 bun 52.1 737 hrs
80498 bun 77.4 48:56:43
... (35 total, aggregate ~700%+ CPU)
$ uptime
load averages: 88.64 100.00 137.19 ← healthy on 10-core = <10
All 35 daemons were reparented to pid 1 (their Claude Code parents exited days/weeks ago) and their cwd is the plugin's cache dir. So the MCP server is not receiving/acting on the stdin EOF that a well-behaved stdio server should exit on.
After killing them:
for pid in $(pgrep -f "bun server.ts"); do
cwd=$(lsof -p $pid 2>/dev/null | awk '/cwd/{print $NF; exit}')
[[ "$cwd" == *"claude-plugins-official/telegram"* ]] && kill -9 $pid
done
Load average dropped from 107 → 39 within 2 minutes.
Where the bug likely lives
An MCP server using StdioServerTransport should exit when stdin closes (this is the contract implied by the stdio transport). Candidates:
- The plugin's
server.ts— doesn't attach aprocess.stdin.on('end'|'close')handler or watchdog, and the grammyBot.start()polling loop keeps the bun event loop alive indefinitely. @modelcontextprotocol/sdk'sStdioServerTransport— may not close/end the underlying stream in a way that triggersbun's event loop to drain.grammy— its long-polling loop is designed to run forever; it doesn't watch for parent death.
Most likely the plugin needs to explicitly wire up "exit when stdin closes" — since grammy will never notice.
Impact
- ~4 GB RAM wasted (18 orphans × ~220 MB when I first checked, up to 35 total)
- CPU load average 100+ on a 10-core machine → constant thermal throttling (
kernel_taskwas consuming 11.8% CPU purely for cooling) - Machine noticeably slow, fans loud, unrelated workflows impacted
Suggested fix
In server.ts, after transport setup, add:
process.stdin.on('end', () => process.exit(0))
process.stdin.on('close', () => process.exit(0))
Or wrap StdioServerTransport so the transport's own EOF handler calls process.exit().
For existing orphans on user machines, a "cleanup on install/update" step in the plugin runner would help too.
Happy to test a fix if there's a candidate build.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗