Telegram plugin: inbound MCP channel notifications not delivered to conversation

Open 💬 20 comments Opened Mar 20, 2026 by gokhunguneyhan
💡 Likely answer: A maintainer (localden, collaborator) responded on this thread — see the highlighted reply below.

Description

The Telegram plugin (telegram@claude-plugins-official v0.0.1) receives inbound messages correctly but they are never delivered to the active Claude Code conversation. Outbound messages (via the reply tool) work fine.

Environment

  • Claude Code: 2.1.80
  • OS: macOS (Darwin 24.6.0)
  • Plugin version: 0.0.1 (commit 8908a582)
  • Runtime: Bun

Steps to Reproduce

  1. Install the Telegram plugin (claude plugin install telegram)
  2. Configure bot token and allowlist a user via /telegram:access
  3. Send a message to the bot on Telegram
  4. Observe that the bot shows "typing..." (confirming the message passes the gate and handleInbound runs)
  5. The mcp.notification({ method: 'notifications/claude/channel', ... }) call fires but nothing appears in the Claude Code conversation

What Works

  • Bot token is valid (getMe returns OK)
  • Bot polling is active (single process, no duplicates)
  • Access gate passes (user is on allowlist, "typing..." indicator is sent)
  • Outbound reply tool works — messages are delivered to Telegram
  • MCP server stdio pipes are connected (verified via lsof)

What Doesn't Work

  • notifications/claude/channel sent by the plugin never surfaces in the conversation
  • This is reproducible across multiple restarts (tested 5+ times)
  • Happens regardless of working directory (tested home dir and project dirs)

Root Cause Hypothesis

Claude Code's MCP notification handler does not route notifications/claude/channel events into the active conversation, even though the MCP server is properly connected and tool calls work bidirectionally.

Additional Context

  • Initially worked briefly after first install, then broke after changing the working directory mid-session
  • Killing stale MCP processes and restarting did not resolve the issue
  • /reload-plugins spawns a fresh MCP server with valid stdio connections but inbound still fails

View original on GitHub ↗

20 Comments

github-actions[bot] · 3 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/36429
  2. https://github.com/anthropics/claude-code/issues/36411

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

bawakul · 3 months ago

Same issue here. Additional debugging details:

Environment: macOS Darwin 25.2.0, Claude Code 2.1.80, telegram@claude-plugins-official 0.0.1

What works:

  • Plugin enabled in settings ("telegram@claude-plugins-official": true)
  • MCP server process running (bun server.ts), confirmed polling Telegram API via lsof (active TCP connections to 149.154.x.x)
  • Bot token valid (getMe returns OK)
  • Outbound reply tool works perfectly — messages delivered to Telegram
  • getUpdates returns empty — server is consuming inbound updates
  • Access gate passes (sender in allowFrom, policy allowlist)

What doesn't work:

  • notifications/claude/channel never surfaces in the conversation
  • Tried: /reload-plugins, killing server + reload, full quit + restart, enabling remote-control in /config
  • Server stdio FDs (0, 1, 2) are connected to Claude Code via unix sockets — pipe exists but notifications don't arrive

Reproduction:

  1. Configure telegram plugin with bot token
  2. Pair + approve a sender
  3. Send outbound message via reply tool — works
  4. Send inbound message from Telegram — nothing in conversation

Looks like the MCP notification transport from server→Claude is silently dropping notifications/claude/channel events even though the stdio pipe is connected and tool calls work bidirectionally.

panda850819 · 3 months ago

Same issue here. macOS (Darwin 25.3.0), plugin v0.0.1.

Repro:

  1. Paired a new user via /telegram:access pair <code>
  2. User sent "gm" — bot showed typing indicator, confirming handleInbound ran
  3. Message never surfaced in the Claude Code conversation
  4. Only after I manually replied via the reply tool (triggered by user showing me a screenshot) did the conversation start working

Additional observation:
The root cause appears to be in server.tshandleInbound uses void mcp.notification(...) (fire-and-forget) with zero retry, no delivery confirmation, and no message queue. If the session isn't ready to receive at that exact moment, the message is silently lost.

Suggesting the plugin should at minimum:

  • Queue undelivered messages and retry on a short interval
  • Or send an auto-reply ("message received, processing shortly") as a fallback when the session doesn't respond within N seconds
keitotatsuguchi · 3 months ago

Same issue here. Adding debug evidence that narrows down the root cause further.

Environment: macOS Darwin 24.6.0, --mcp-config (not plugin-enabled), --channels telegram

Setup to eliminate confounders:

  • enabledPlugins: { "telegram@claude-plugins-official": false } — plugin disabled globally
  • Launched via: claude --mcp-config <mcp-telegram-config>.json --channels telegram
  • Confirmed single bun process via ps aux — no polling race condition

File-level debug logging added to server.ts:

Added fs.appendFileSync at each stage in handleInbound:

2026-03-21T03:27:18.685Z handleInbound called: text="hello world", from=XXXXXXXX, chat=XXXXXXXX
2026-03-21T03:27:18.685Z gate result: deliver
2026-03-21T03:27:18.688Z notification sent OK

Key finding: mcp.notification({ method: 'notifications/claude/channel', params: {...} }) resolves successfully (not just fire-and-forget — the Promise completes without error). The MCP stdio transport layer considers the notification delivered. Yet it never surfaces in the conversation.

This confirms the issue is not in the plugin's notification sending logic, but in Claude Code's handling of notifications/claude/channel on the receiving end. The stdio pipe is functional (outbound tool calls like reply work fine from the same session), but inbound notifications are silently dropped.

CodeAunt · 3 months ago

I solved this by starting claude with --channels plugin:telegram@claude-plugins-official

thebrainsystem · 3 months ago

Same issue here. Adding our debugging timeline in case it helps narrow things down.

Environment:

  • macOS Darwin 25.1.0 (Mac Studio M2 Ultra)
  • Claude Code with Opus model
  • telegram@claude-plugins-official v0.0.1
  • Bun runtime

What works:

  • ✅ Plugin installed and MCP server running (single bun run process)
  • ✅ Bot token valid (getMe returns OK)
  • access.json correct: sender in allowFrom, dmPolicy: "pairing"
  • ✅ Outbound reply tool works — messages delivered to Telegram successfully
  • ✅ No webhook set (getWebhookInfo returns empty URL)

What doesn't work:

  • ❌ Inbound messages never surface in the Claude Code conversation
  • getUpdates returns ok: true with empty results (no 409 Conflict), suggesting polling may not be actively running despite the process being alive
  • ❌ Multiple session restarts did not resolve the issue

Debugging performed across two sessions:

  1. Verified bot token, access config, and process status — all healthy
  2. Confirmed no competing consumers (single process, no webhook)
  3. Tested outbound via reply tool — works perfectly
  4. Called getUpdates manually — returns ok: true without 409, indicating the grammy bot.start() async IIFE (line 826 of server.ts) may have failed silently or never started polling
  5. Investigated server.ts initialization flow — the bot.start() runs inside a void (async () => { ... })() block that swallows errors on non-409 failures (line 858: logs to stderr then returns)
  6. After reading through these issues, confirmed the root cause is the tengu_harbor feature gate on Claude Code's side — not a plugin issue

Additional observation:
In our case, getUpdates does NOT return 409 (which would confirm active polling). This might mean bot.start() silently failed during initialization — possibly because the MCP stdio transport isn't fully ready when the IIFE fires, or the feature gate rejection somehow propagates back and prevents polling from starting. Worth investigating if the channel registration rejection affects the bot's willingness to start polling.

Waiting for the server-side flag rollout. Everything on the plugin side appears correctly configured.

dmd · 3 months ago
I solved this by starting claude with --channels plugin:telegram@claude-plugins-official

Is this not how everyone who has this problem is starting it? That's certainly how I'm starting it. I wasn't aware there was any other way.

CodeAunt · 3 months ago
> I solved this by starting claude with --channels plugin:telegram@claude-plugins-official Is this not how everyone who has this problem is starting it? That's certainly how I'm starting it. I wasn't aware there was any other way.

Claude actually directed me to start with just claude to activate tg. It spent all night trying to fix it internally before telling me to just comment '+1' in this issue and wait for fix. Eventually, I figured out the --channels fix myself. Good to know that's the standard way for you.

dmd · 3 months ago

It's literally in the instructions. It's step 4.

AndrzejGolos · 3 months ago

Additional diagnostic data (macOS, Claude Code 2.1.81)

Setup: claude --channels plugin:telegram@claude-plugins-official, Bun runtime, grammy 1.21+

What works:

  • Outbound reply tool → message delivered to Telegram ✅
  • Bot polling active (confirmed via getUpdates returning 409 Conflict) ✅
  • gate() returns deliver for allowlisted senderId ✅
  • handleInbound() executes fully, mcp.notification() called ✅

What fails:

  • notifications/claude/channel notification never appears in Claude Code conversation ❌

Diagnostic steps taken:

  1. Verified single bot process (no zombies): ps aux | grep telegram → 1 process
  2. Verified no webhook set: getWebhookInfourl: ""
  3. Verified polling active: getUpdates → 409 Conflict (server IS polling)
  4. Verified lsof on bot process: stdio unix sockets connected, but zero TCP connections at inspection time (transient HTTP?)
  5. Verified access.json: senderId in allowFrom, pending empty
  6. 3× full Claude Code restarts — no change

Conclusion: The MCP server correctly receives Telegram messages and calls mcp.notification(), but the Claude Code client either doesn't register a handler for notifications/claude/channel or silently drops the notification. The fire-and-forget pattern in handleInbound() (.catch() only) means delivery failure is silent.

Suggested fix: Add message queue with delivery confirmation / retry in the plugin, or fix the notification handler registration on the Claude Code client side.

sbenodiz · 3 months ago

Confirming this issue on Claude Code v2.1.91 (macOS, latest as of 2025-04-03).

Same symptoms as everyone else:

  • Outbound works perfectly — reply tool sends messages to Telegram without issues
  • Bot token is valid (getMe succeeds), no webhook set, single bun process running
  • The Grammy bot is actively polling (getUpdates returns 409 Conflict when called externally, confirming the plugin holds the polling slot)
  • TCP connection to Telegram API is established (lsof confirms active HTTPS connections to 149.154.166.110)
  • Inbound messages are received by the bot (updates are consumed — getUpdates returns empty after user sends messages) but notifications/claude/channel never surfaces in the Claude Code conversation

Tried: /reload-plugins, killing and restarting the MCP process, deleteWebhook, verifying access.json allowlist matches user ID. None helped — the issue is clearly on the Claude Code client side not processing notifications/claude/channel from MCP servers.

DeliLevente99 · 1 month ago

Cross-linking #36837 — the Discord plugin shows the same notifications/claude/channel silent-drop pattern on the client side (it's one of three distinct bugs reported there, alongside Linux stdin lifecycle and discord.js cache staleness). If the macOS notification handler is a shared root cause, a fix likely lands both.

DeliLevente99 · 1 month ago

Follow-up to my earlier cross-link, with a structural data point posted in detail on #36837. On a single Claude Code session (Kali Linux WSL2) with tengu_harbor: true cached and the ledger entry for the official plugin present, the claude-plugins-official channel plugins do not deliver inbound messages — while a community channel plugin (separate custom marketplace, same MCP-server + notifications/claude/channel mechanism) DOES deliver them end-to-end on the same session.

This isolates the silent-drop pattern reported in this thread to the official-marketplace registration path rather than the notification handler itself — consistent with Gate 6 (plugin marketplace source verification) in @soumikbhatta''s #36460 breakdown. Telegram is in the same claude-plugins-official marketplace as Discord, so this likely applies here too.

The presence of telegram in tengu_harbor_ledger is therefore not sufficient — something downstream of the ledger check rejects claude-plugins-official plugins on affected accounts while custom-marketplace plugins go through.

DeliLevente99 · 1 month ago

Correction to my earlier marketplace-isolation comment above. Tested end-to-end on my own Kali Linux WSL2 setup with telegram@claude-plugins-official v0.0.6, launched and paired normally — inbound DMs ARE delivered to the conversation and the assistant responds via the reply tool successfully.

This invalidates the marketplace-isolation hypothesis I posted earlier, which extrapolated from the Discord case in #36837. The symptom reported in this thread appears to be fixed (or at least not reproducible) in the v0.0.6 release — most reports here are on v0.0.1. Upgrading the plugin may resolve it.

Apologies for the noise from the earlier inferred comment; keeping the thread honest.

DeliLevente99 · 1 month ago

Cross-posting workaround confirmation on the same setup, since @localden marked this as the consolidated tracker for notifications/claude/channel silent-drops (closing #36975 in favor of this issue):

--dangerously-load-development-channels plugin:<id> works as a general workaround, but the severity differs by plugin on the same account:

| Channel | --channels | --dangerously-load-development-channels |
|----------|------------------|------------------------------------------|
| Telegram | works | works |
| Slack | works | works |
| Discord | blocked silently | works |

(Telegram tested with telegram@claude-plugins-official v0.0.6.) For users on this thread still blocked on Telegram: two independent things to check — (a) upgrade to v0.0.6 if on the v0.0.1-era report (same symptom may already be cleared by upstream fixes), (b) failing that, the dev-flag workaround.

The Discord side of the same bug is tracked in detail at #36837.

_(Edit: previous version of this comment had its table emojis mangled by a PowerShell stdin encoding issue; switched to plain text for the status column.)_

oskarmodig · 1 month ago

Still reproduces on v2.1.150 with claude --bg — minimal deterministic repro

Confirming notifications/claude/channel is still never surfaced into the conversation for background daemon sessions on Claude Code v2.1.150. Here is a self-contained repro that doesn't depend on any real channel plugin, in case it helps narrow the root cause.

Trivial stdio MCP server (test-channel-plugin.js) that completes the handshake, then emits a notifications/claude/channel 8s later asking the model to reply PONG:

#!/usr/bin/env node
// Minimal stdio MCP server. Handshake, then emit notifications/claude/channel.
let send = (m) => process.stdout.write(JSON.stringify(m) + "\n");

function emit() {
  send({
    jsonrpc: "2.0",
    method: "notifications/claude/channel",
    params: {
      content: "Respond with exactly the word PONG and nothing else.",
      meta: { source: "bg-channel-test", chat_id: "c1", message_id: "m1",
              user: "tester", user_id: "tester", ts: new Date().toISOString() },
    },
  });
}

let buf = "";
process.stdin.on("data", (chunk) => {
  buf += chunk.toString();
  let i;
  while ((i = buf.indexOf("\n")) >= 0) {
    const line = buf.slice(0, i); buf = buf.slice(i + 1);
    if (!line.trim()) continue;
    const msg = JSON.parse(line);
    if (msg.method === "initialize") {
      send({ jsonrpc: "2.0", id: msg.id, result: {
        protocolVersion: "2024-11-05",
        capabilities: { tools: {}, experimental: { "claude/channel": {}, "claude/channel/permission": {} } },
        serverInfo: { name: "bg-channel-test", version: "0.0.1" } } });
    } else if (msg.method === "notifications/initialized") {
      setTimeout(emit, 8000);
    } else if (msg.method === "tools/list") {
      send({ jsonrpc: "2.0", id: msg.id, result: { tools: [] } });
    } else if (msg.id !== undefined) {
      send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "unknown" } });
    }
  }
});

mcp-config.json:

{ "mcpServers": { "bg-channel-test": { "command": "node", "args": ["./test-channel-plugin.js"] } } }

Run as a background daemon:

claude --bg --name canary --mcp-config ./mcp-config.json \
  --allow-dangerously-skip-permissions --permission-mode auto \
  "Wait silently for instructions and follow exactly what you are asked."

Observed (v2.1.150):

  • Handshake completes; the server emits notifications/claude/channel (verified in its own stderr log).
  • The daemon's session jsonl contains only the initial prompt turn → assistant reply "Waiting for instructions." There is no PONG and no trace of the channel content anywhere in the session.
  • /mcp reports 1 MCP server failed.

So the notification is accepted at the transport layer but never delivered into the conversation for --bg sessions. The same trivial server delivers correctly in an interactive/tmux session — it is specifically the headless/--bg path that drops it. This looks like the same root cause as #55896 (stream-json drop); flagging here since this is the consolidated tracker.

localden collaborator · 1 month ago

Porting evidence from #64470 (closing as dup):

@kr-aldama captured getClientCapabilities() from a connected MCP server: the result is {elicitation.form, roots} only — experimental.claude/channel is absent. This is direct confirmation that the host never negotiates the channel capability, which is why notifications/claude/channel messages are dropped.

Also from #64470: still reproduces on plugin v0.0.6 + Claude Code 2.1.145–2.1.158 interactive (the "fixed in 0.0.6" comment above doesn't hold). References claude-cli-internal#23061.

DeliLevente99 · 1 month ago

Replying to @localden's note (comment):

still reproduces on plugin v0.0.6 + Claude Code 2.1.145–2.1.158 interactive (the "fixed in 0.0.6" comment above doesn't hold)

For context, the "fixed in 0.0.6" suggestion came up in two places:
@sbenodiz's comment
("upgrade to v0.0.6 … same symptom may already be cleared by upstream fixes") and
my own follow-up
("appears to be fixed … in the v0.0.6 release"). Here's a data point that I think reconciles
both with the getClientCapabilities() finding.

Same plugin version across all three rows (telegram@claude-plugins-official v0.0.6) —
the only thing that changes is the Claude Code host version:

| Platform | Claude Code | Plugin | Inbound delivered? |
|---------------------|-------------|--------|--------------------|
| @localden's repro | 2.1.145–158 | v0.0.6 | ❌ no |
| Kali Linux (WSL2) | — | v0.0.6 | ✅ yes |
| Nobara (native) | 2.1.170 | v0.0.6 | ✅ yes |

Launched with --channels plugin:telegram@claude-plugins-official. Inbound DMs surface
in the conversation (the ← telegram · <id>: <msg> line appears) and the assistant
replies via the reply tool, end-to-end.

So both are right: the plugin version was never the differentiator (consistent with
"fixed in 0.0.6 doesn't hold"), but it does work for me — because the host is newer.
This points the fix at the Claude Code host, somewhere between 2.1.158 and 2.1.170,
exactly where the getClientCapabilities() negotiation of experimental.claude/channel
would live.

Could anyone still blocked confirm their exact claude --version? Would help pin the
build where the capability negotiation got fixed.

prassanna-ravishankar · 25 days ago

for anyone needing to unblock phone control while this gets fixed, repowire takes a different route that sidesteps the notifications/claude/channel path completely.

instead of a stdio channel plugin, telegram is a first-class peer registered on a local daemon mesh. inbound messages route through the daemon and get injected into the target session, outbound goes back the same way. since it never touches the channel capability negotiation thats failing here, both directions work today.

not a fix for the underlying client bug (@localden's capability-absence finding is the real root cause), but if you just need working telegram or slack control of claude code right now its a way around it. https://github.com/prassanna-ravishankar/repowire

plywoodtom · 24 days ago

Windows native confirmation — working on host 2.1.176

Adding a Windows data point to @DeliLevente99's table (was macOS + Linux only):

| Platform | Claude Code | Plugin | Inbound delivered? |
|---|---|---|---|
| Windows 11 (native, build 26200) | 2.1.176 | v0.0.6 | ✅ yes |

Launched with --channels plugin:telegram@claude-plugins-official. Inbound DMs surface in the conversation and the assistant replies via the reply tool, end-to-end. MCP log shows "Channel notifications registered" (previously this same flag logged "Channel notifications skipped: server plugin:telegram:telegram not in --channels list").

Consistent with the host-side fix landing between 2.1.158 and 2.1.170 — confirms it holds on native Windows too, not just macOS/Linux. Plugin v0.0.6 unchanged.