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.

Setup

Two pieces, both opt-in: one on the URL (?stateful), one on the Claude Code command line.

Stateless is the default

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:

json
{ "mcpServers": { "chamade": { "type": "http", "url": "https://mcp.dev.chamade.io/mcp/?stateful", "headers": { "Authorization": "Bearer chmd_..." } } } }

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.

bash
# Launch Claude Code with chamade channels loaded claude --dangerously-load-development-channels server:chamade # Resume the current conversation after a restart claude --dangerously-load-development-channels server:chamade --continue # Unattended / auto-approve permissions claude --dangerously-skip-permissions --dangerously-load-development-channels server:chamade
Required every launch

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.

Transport-agnostic

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

  1. Client opens an MCP session with ?stateful in the URL and sends initialize.
  2. Chamade's hybrid session manager sees the query param, routes to a persistent session, issues an Mcp-Session-Id response header, and declares experimental.claude/channel: {} in server capabilities.
  3. 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.
  4. 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/channel JSON-RPC notification.
  5. Channel-aware clients surface the event content to the agent, which reacts using the normal MCP tools (chamade_call_say, chamade_dm_chat, etc.).
  6. 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.
Stateful sessions die on chamade restart

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.

No polling needed

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

Hosted STT/TTS required for this exact flow

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:

You Join this Teams meeting: https://teams.microsoft.com/l/meetup-join/...
Tool chamade_call_join → call_id: "abc123", state: "connecting"
Chamade Transcript: [Alice] OK let's start the sprint review.
Chamade Transcript: [Bob] I finished the auth migration yesterday.
Agent Alice started the sprint review. Bob reports the auth migration is done. Let me take notes.
Tool chamade_call_say → "Got it, I'm noting that the auth migration is complete."

Messaging workflow

Incoming DMs are also pushed automatically:

Chamade New message from Alice on Discord: "Can you check the test results?"
Agent Alice is asking about test results. Let me check and reply.
Tool chamade_dm_typing → OK
Tool 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 Incoming call: SIP +33 1 23 45 67 89, state: "ringing"
Agent Incoming phone call. Let me answer.
Tool 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 sourcePushed 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
LatencyDepends on poll intervalReal-time (<1 s)
DM messagesAgent polls chamade_inbox (optionally long-poll up to 55 s)Pushed automatically
Incoming callsAgent polls chamade_call_listPushed automatically
ClientsAny MCP clientClaude Code (and any client that opts into experimental.claude/channel)
ConfigSameSame — 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.