Use this file to discover all available pages before exploring further.
Channels are push-based notification streams that deliver real-time events into Claude Code sessions. They enable your MCP server to forward webhooks, application errors, agent completions, job results, and chat messages directly into Claude’s context — with optional two-way reply support.
This feature implements the Claude Code Channels extension — an experimental MCP protocol extension using notifications/claude/channel.
Service connectors maintain persistent connections to external messaging services. Claude sends messages via channel-contributed tools, and incoming responses arrive as channel notifications.This is the most powerful channel pattern — it turns Claude Code into a full messaging client.
import { Channel, ChannelContext, Tool, ToolContext } from '@frontmcp/sdk';import type { ChannelNotification } from '@frontmcp/sdk';import { z } from '@frontmcp/sdk';// Outbound tool — Claude calls this to send messages@Tool({ name: 'send-whatsapp', description: 'Send a WhatsApp message. Replies will arrive as channel notifications.', inputSchema: { to: z.string().describe('Recipient phone number'), text: z.string().describe('Message text'), },})class SendWhatsAppTool extends ToolContext { async execute(input: { to: string; text: string }) { const client = this.get(WhatsAppClientToken); await client.sendMessage(input.to, input.text); return { sent: true, to: input.to }; }}// Channel — maintains connection, receives responses@Channel({ name: 'whatsapp', description: 'WhatsApp messaging. Send via send-whatsapp tool, replies arrive here.', source: { type: 'service', service: 'whatsapp-business' }, tools: [SendWhatsAppTool], // Auto-registered in tool registry twoWay: true,})class WhatsAppChannel extends ChannelContext { private client: WhatsAppClient; async onConnect(): Promise<void> { this.client = new WhatsAppClient(process.env['WHATSAPP_TOKEN']); // Incoming messages from WhatsApp → push into notification pipeline this.client.on('message', (msg) => { this.pushIncoming({ from: msg.from, text: msg.body, chatId: msg.chatId, }); }); await this.client.connect(); } async onDisconnect(): Promise<void> { await this.client.disconnect(); } async onEvent(payload: unknown): Promise<ChannelNotification> { const msg = payload as { from: string; text: string; chatId: string }; return { content: `${msg.from}: ${msg.text}`, meta: { chat_id: msg.chatId, sender: msg.from }, }; }}
The conversation flow:
Claude calls send-whatsapp({ to: "+1234567890", text: "Hi Alice!" })
Message is delivered to Alice’s WhatsApp
Alice replies “Got it, will review the PR”
onConnect()’s listener fires → pushIncoming() → onEvent() transforms it
Claude sees: <channel source="whatsapp" sender="Alice">Alice: Got it, will review the PR</channel>
Claude can continue the conversation by calling send-whatsapp again
Service connector channels call onConnect() during scope initialization and onDisconnect() during teardown. The connection persists for the lifetime of the server.
Events arrive and are processed normally via onEvent()
Each notification is also stored in an in-memory ring buffer
When a new session connects, replayBufferedEvents(sessionId) sends all buffered events
Replayed events include replayed: "true" in their meta
For events that must survive server restarts, combine replay with a persistent store (Redis, SQLite) in onConnect(). See the replay buffer skill example for the pattern.
The server advertises experimental: { 'claude/channel': {} } in its capabilities. Only sessions whose client capabilities include this extension receive channel notifications.
Sessions can subscribe to specific channels instead of all:
// Subscribe to only deploy-alerts (not error-alerts)scope.notifications.subscribeChannel(sessionId, 'deploy-alerts');// Unsubscribe from a channelscope.notifications.unsubscribeChannel(sessionId, 'deploy-alerts');
Session-targeted events from job/agent completions carry the originating sessionId in the event payload. If your custom source produces session-scoped data, pass targetSessionId to channel.pushNotification() to enforce isolation.