Channel MCP plugin: orphan process after server restart causes silent message loss

Resolved 💬 3 comments Opened Apr 11, 2026 by jialinclaw-crypto Closed Apr 14, 2026

Summary

When a channel MCP server (e.g., the Telegram plugin) dies mid-session, Claude Code respawns it but the new process's stdio is not connected to the MCP transport. The respawned process continues operating (e.g., polling Telegram API via getUpdates) but notifications/claude/channel never reaches Claude. Messages are silently consumed and lost with no error indicator — the status bar still shows the MCP as connected.

Steps to Reproduce

  1. Start Claude Code with --channels plugin:telegram@claude-plugins-official
  2. Send a Telegram message — it arrives normally (← telegram · ...)
  3. Kill the bun child process: kill $(pgrep -P <claude_pid> -f bun)
  4. Claude Code respawns bun automatically
  5. Send another Telegram message — it never appears in Claude

Observed Behavior

  • After step 4, pgrep -f "bun.*telegram" shows a new bun process
  • But ps --ppid <claude_pid> --no-headers | grep bun returns nothing — the new bun is not a child of Claude (reparented, likely spawned through a shell)
  • The new bun's grammy long-polling works correctly (consumes getUpdates), but its stdout socket is not connected to Claude's MCP message reader
  • mcp.notification({ method: 'notifications/claude/channel', ... }) writes JSON-RPC to stdout → nobody reads it → message lost
  • Telegram's update offset advances → message cannot be re-fetched
  • Claude's status bar still shows the MCP server as connected (e.g., 8 MCPs)

Expected Behavior

Either:

  1. The respawned MCP server's stdio should be correctly piped back to Claude's MCP transport handler, OR
  2. If reconnection isn't possible, Claude should detect the broken pipe and report the MCP as disconnected (removing tools from the available set), rather than showing a stale "connected" state

Impact

This bug is particularly severe for channel plugins with inbound notifications (currently only Telegram). Unlike regular MCP tool servers where a broken connection surfaces as a tool call error, channel plugins push messages to Claude via fire-and-forget notifications. When the notification path breaks:

  • No error is surfaced to Claude or the user
  • The orphan process consumes messages from the external service (advancing offsets/cursors)
  • Messages are permanently lost with no retry mechanism

Workaround

External watchdog that checks if the bun process is a child of Claude, and restarts the full service if not:

PANE_PID=$(tmux display-message -t claude-telegram -p '#{pane_pid}')
BUN_PID=$(pgrep -f "bun.*telegram" | head -1)
IS_CHILD=$(ps --ppid "$PANE_PID" --no-headers | grep -c bun)
if [[ -n "$BUN_PID" && $IS_CHILD -eq 0 ]]; then
    kill "$BUN_PID"
    # restart the full Claude Code session
fi

Environment

  • Claude Code: 2.1.101
  • Telegram plugin: 0.0.5
  • OS: Ubuntu 24.04 (WSL2)
  • MCP SDK: @modelcontextprotocol/sdk (StdioServerTransport)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗