Channel Mode
DMs, inbound calls, and call state changes arrive in your agent's context the moment they happen — no polling loop to write.
What is it?
Channel mode is Chamade's real-time push layer. Every time something happens on a platform you care about — a new DM on Telegram, a caller ringing your SIP number, a batched WhatsApp flush — the MCP server pushes the event on your open session as a notifications/claude/channel message. Channel-aware clients (currently Claude Code) display it directly in the agent's context so it can react immediately.
- Chat messages — DMs from Discord, Telegram, Teams, WhatsApp, Slack, NC Talk
- Incoming calls — ringing SIP calls, new conversations
- Call state changes — joined, active, disconnected, ended
- WhatsApp flush —
dm_deliveredwhen queued messages are sent after the 24 h window reopens - Transcript lines — only when hosted STT is enabled (BYOK). In BYO audio mode, transcripts live in your own STT client and never touch the channel.
Setup
Two pieces, both opt-in: one on the URL (?stateful), one on the Claude Code command line.
Without opting in, the URL https://mcp.dev.chamade.io/mcp/ serves stateless per-request transport: no session ID, no push channel, transparent to chamade restarts. That's deliberate — the restart-proof path is right for every client that doesn't explicitly need push. You only add ?stateful when you actually want real-time events delivered via notifications/claude/channel.
Server side — add ?stateful to the MCP URL
Append the ?stateful query param to the URL in your .mcp.json:
Key presence alone is enough — ?stateful, ?stateful=1, and ?stateful=yes all work the same. Chamade routes the session through the persistent-session manager: an Mcp-Session-Id response header is issued, and the server starts a per-session push bridge as soon as the client opens its SSE GET.
For the @chamade/mcp-server@3 stdio shim, pass --stateful in args — it auto-appends ?stateful to the underlying URL.
Client side — launch Claude Code with the channel flag
Stacked on top of ?stateful, Claude Code needs its own per-launch opt-in to consume the push notifications. Without this flag, chamade still pushes events but Claude Code drops them silently — you'd be running a stateful session for no benefit.
The --dangerously-load-development-channels server:chamade flag is required every time you start Claude Code. It's a command-line argument, not a settings.json entry. Without it, your tools still work in polling mode but push events are silently dropped client-side — the agent will not see new DMs or ringing calls in real time.
The server name after server: must match the key in your .mcp.json. If you have multiple Chamade entries (chamade, chamade-http, etc.), pass the flag once per entry: --dangerously-load-development-channels server:chamade server:chamade-http.
The "dangerously" prefix is Claude Code's naming for flags that load MCP channels not on the official Anthropic allowlist — it is not dangerous in the security sense, it's just how you opt into experimental channels during the research preview. When Chamade is added to the allowlist, the flag will become optional.
The client-side flag is unrelated to transport. Whether you use HTTP direct, the @chamade/mcp-server@3 stdio shim, or mcp-remote directly, you still need --dangerously-load-development-channels server:chamade on the Claude Code launch.
How it works
- Client opens an MCP session with
?statefulin the URL and sendsinitialize. - Chamade's hybrid session manager sees the query param, routes to a persistent session, issues an
Mcp-Session-Idresponse header, and declaresexperimental.claude/channel: {}in server capabilities. - If the client supports channel mode (Claude Code with the launch flag), it opens a
GET /mcp/SSE stream using that session ID. That GET is the canonical "I want server-initiated messages" handshake — it's what triggers chamade to spawn a per-session push bridge subscribed to the user's inbox hub. - When any event is published to the hub (DM webhook, Maquisard bridge, SIP trunk, etc.), the bridge forwards it to the live SSE connection as a
notifications/claude/channelJSON-RPC notification. - Channel-aware clients surface the event content to the agent, which reacts using the normal MCP tools (
chamade_call_say,chamade_dm_chat, etc.). - When the SSE stream closes (TCP drop, transport termination), the bridge releases its hub subscription automatically — an idle probe catches stale connections within ~25 s even if no events are flowing.
A chamade redeploy wipes the persistent sessions. Per the MCP spec a client SHOULD re-initialize on HTTP 404; Claude Code doesn't always do that today, so you may see a brief "MCP disconnected" blip until you relaunch the client. The stateless default doesn't have this problem — use ?stateful only when you actually need push.
Do not call chamade_call_status or chamade_inbox in a loop when using channel mode with a channel-aware client. Events arrive on their own. You can still call those tools manually to catch up after a reconnect, or in polling mode with non-channel-aware clients.
Voice call workflow
The example below pushes call_transcript events and uses chamade_call_say — both require hosted STT/TTS (BYOK). In BYO audio mode, transcripts come from your own STT and the agent speaks via your own TTS; neither touches the MCP channel. DM, call-state, and inbound-call push work identically in both modes.
A typical voice meeting with hosted STT/TTS enabled:
chamade_call_join → call_id: "abc123", state: "connecting"
chamade_call_say → "Got it, I'm noting that the auth migration is complete."
Messaging workflow
Incoming DMs are also pushed automatically:
chamade_dm_typing → OK
chamade_dm_chat → "All 42 tests passing. The auth migration looks good."
Inbound calls
When a phone call or new voice request comes in, the event arrives immediately:
chamade_call_accept → state: "active"
Channel mode vs polling
Clients without channel support aren't stuck — they simply ignore the push notifications and fall back to polling. Every tool that has a push-event equivalent also has a polling mode; see the polling fallback notes in the MCP tools reference (chamade_call_status, chamade_inbox with wait=55, chamade_call_list). Nothing breaks, you just trade real-time latency for a tighter control loop.
| Polling (default MCP) | Channel mode | |
|---|---|---|
| Transcript delivery (hosted STT only) | Agent polls chamade_call_status when hosted STT is enabled; otherwise the agent's own STT client is the source | Pushed automatically when hosted STT is enabled (BYOK — add a provider key in the dashboard); in BYO audio mode transcripts live in your STT client, not the channel |
| Latency | Depends on poll interval | Real-time (<1 s) |
| DM messages | Agent polls chamade_inbox (optionally long-poll up to 55 s) | Pushed automatically |
| Incoming calls | Agent polls chamade_call_list | Pushed automatically |
| Clients | Any MCP client | Claude Code (and any client that opts into experimental.claude/channel) |
| Config | Same | Same — automatic if client supports it |
Non-channel clients work out of the box in polling mode — a
ready-to-paste system prompt that wires up the
chamade_inbox long-poll loop lives on the
MCP page.
