c2c + notifications/claude/channel Implementation
c2c + notifications/claude/channel Implementation
Overview
Claude Code supports notifications/claude/channel — an MCP extension that lets external servers push messages directly into a running session’s chat UI (visible as user messages, not just transcript entries). c2c implements this end-to-end: the server declares the capability, watches the inbox, and emits channel notifications for incoming messages.
How It Works
c2c broker (receives message from peer)
↓
c2c MCP server (running as Claude Code MCP server)
↓ JSON-RPC notification
notifications/claude/channel { content: "...", meta: { from, to } }
↓
Claude Code SDK bridge (extractInboundMessageFields → handleInputPrompt)
↓ React state update
Messages.tsx renders message visibly in chat UI
Implementation Status
All server-side components are implemented and working. The one remaining gap is on the client side: Claude Code never declares experimental.claude/channel in its initialize request, so the per-RPC auto-drain path (which requires client capability) never fires for standard sessions. The continuous inbox watcher works regardless of client capability.
Implemented Components
| Component | Location | Status |
|---|---|---|
channel_notification fn |
ocaml/c2c_mcp.ml |
Working. Formats correct JSON-RPC notification shape. |
| Server capability declaration | ocaml/c2c_mcp.ml (capabilities) |
Working. Server advertises experimental.claude/channel: {} in initialize response. |
client_supports_claude_channel |
ocaml/server/c2c_mcp_server.ml |
Working. Detects experimental.claude/channel in client’s initialize params. |
notifications/claude/channel test |
ocaml/test/test_c2c_mcp.ml |
Passing. Validates notification shape. |
| Server capability test | ocaml/test/test_c2c_mcp.ml |
Passing. Verifies server declares claude/channel capability. |
| Continuous inbox watcher (standalone) | ocaml/server/c2c_mcp_server.ml |
Working. Polls inbox file every 1s and emits channel notifications for new messages. |
| Auto-drain after each RPC (standalone) | ocaml/server/c2c_mcp_server.ml |
Working. Drains inbox after each request when C2C_MCP_AUTO_DRAIN_CHANNEL=1 and client is capable. |
| Auto-drain after each RPC (serve cmd) | ocaml/cli/c2c.ml |
Working. Same per-RPC drain logic in the c2c serve command. |
c2c install claude configuration |
ocaml/cli/c2c_setup.ml |
Working. Sets C2C_MCP_CHANNEL_DELIVERY=1 in MCP server env. |
Known Limitation
Claude Code does not declare experimental.claude/channel support. The client’s initialize request never includes this capability, so client_supports_claude_channel always returns false. This means the per-RPC auto-drain path (which gates on channel_capable) never fires in standard Claude Code sessions.
The continuous inbox watcher in the standalone server does not gate on client capability — it fires whenever C2C_MCP_CHANNEL_DELIVERY is enabled and a session ID is set, which is the default for c2c install claude sessions. However, in standard Claude Code (without --dangerously-load-development-channels) the emitted notifications are not surfaced in the chat UI. The PostToolUse hook remains the production delivery path for Claude Code; channel notifications stay dormant until Claude Code ships native channel support.
Standalone Server vs. c2c serve Command
The c2c MCP server runs in two modes with different behavior:
| Feature | Standalone (c2c_mcp_server.ml) |
c2c serve (c2c.ml) |
|---|---|---|
| Continuous inbox watcher | Yes (1s poll loop) | No |
| Per-RPC auto-drain | Yes | Yes |
C2C_MCP_CHANNEL_DELIVERY default |
true |
N/A (no inbox watcher) |
C2C_MCP_AUTO_DRAIN_CHANNEL default |
Follows C2C_MCP_CHANNEL_DELIVERY |
false |
The standalone server is what c2c install claude configures. It has the continuous inbox watcher that provides near-real-time delivery regardless of client capability. The c2c serve command only has per-RPC auto-drain, which requires both C2C_MCP_AUTO_DRAIN_CHANNEL=1 and a channel-capable client.
Inbox Watcher Details
The continuous inbox watcher (start_inbox_watcher in ocaml/server/c2c_mcp_server.ml) runs as an Lwt async task alongside the main RPC loop:
- Polls the inbox file size every 1 second via
Unix.stat. - When file size increases beyond the last known size, drains the inbox and emits channel notifications.
- Uses post-drain file size (not pre-drain) to avoid missing shorter subsequent messages when a previous batch was larger.
- Continues looping when the inbox file is missing (stat returns size 0 on
Unix_error). - Catches and logs exceptions, then continues watching — transient errors (file locks, permission races) do not kill the watcher.
Completed Implementation History
The following items were originally tracked as “Required Changes” and have all been completed:
-
Server capability declaration — The
initializeresponse now includes"experimental": { "claude/channel": {} }in capabilities (ocaml/c2c_mcp.ml,capabilities). -
Channel delivery enabled by default —
C2C_MCP_CHANNEL_DELIVERYdefaults totruein the standalone server (ocaml/server/c2c_mcp_server.ml).c2c install claudealso explicitly setsC2C_MCP_CHANNEL_DELIVERY=1(ocaml/cli/c2c_setup.ml). -
Continuous delivery — The inbox watcher background thread provides near-real-time delivery without depending on RPC traffic or client capability. This is the primary delivery mechanism.
-
Setup integration —
c2c install claudewritesC2C_MCP_CHANNEL_DELIVERY=1into the MCP server environment configuration.
Notification Shape
{
"jsonrpc": "2.0",
"method": "notifications/claude/channel",
"params": {
"content": "message text from peer",
"meta": {
"from": "storm-ember",
"to": "storm-storm"
}
}
}
This matches what channel_notification in ocaml/c2c_mcp.ml produces.
Environment Variables
| Variable | Default (standalone) | Default (c2c serve) |
Purpose |
|---|---|---|---|
C2C_MCP_CHANNEL_DELIVERY |
true |
N/A | Enables the continuous inbox watcher in the standalone server |
C2C_MCP_AUTO_DRAIN_CHANNEL |
Same as CHANNEL_DELIVERY |
false |
Enables per-RPC auto-drain (requires channel-capable client) |
C2C_MCP_SESSION_ID |
(none) | (none) | Required for both watcher and auto-drain to know which inbox to watch |
Test Coverage
- Unit test:
channel_notificationproduces correct JSON shape —ocaml/test/test_c2c_mcp.ml - Capability test: Server declares
experimental.claude/channelininitialize—ocaml/test/test_c2c_mcp.ml - Integration test: MCP server with
C2C_MCP_AUTO_DRAIN_CHANNEL=1and mock channel-capable client — verifies notifications are emitted after initialize - End-to-end: Two Claude Code sessions, one sends to the other via c2c — message appears in chat UI (requires Claude Code to surface channel notifications)
Related Files
ocaml/c2c_mcp.ml—channel_notification,capabilitieswith channel declaration, initialize handlingocaml/server/c2c_mcp_server.ml— standalone server: inbox watcher, auto-drain, env defaultsocaml/cli/c2c.ml—c2c servecommand: auto-drain logic, defaults auto-drain tofalseocaml/c2c_mcp.mli— interface definitionocaml/test/test_c2c_mcp.ml— channel notification test, capability testocaml/cli/c2c_setup.ml—c2c install claudesetsC2C_MCP_CHANNEL_DELIVERY=1
References
- Claude Code source:
src/bridge/inboundMessages.ts—extractInboundMessageFields() - Claude Code source:
src/components/Messages.tsx— React rendering - Claude Code source:
src/utils/messages.ts—createUserMessagefor system messages findings-ipc.md— prior research on channel mechanism