From jack-software
Step-by-step procedure for building a Claude Code channel integration. Use when creating a new channel plugin that connects Claude Code to external systems via MCP, named pipes, webhooks, or chat platforms.
npx claudepluginhub jack-michaud/faire --plugin jack-softwareThis skill uses the workspace's default tool permissions.
A Claude Code channel is an MCP server that pushes events into a Claude Code session so Claude can react to things happening outside the terminal. Channels can be one-way (alerts, webhooks) or two-way (chat bridges with reply tools).
Customizes ClaudeClaw by adding channels (Telegram, Slack, email), triggers, integrations, router mods. Interactive with plugin-to-developer migration: GitHub fork, clone, npm install, service setup.
Guides MCP server integration into Claude Code plugins via .mcp.json or plugin.json, covering stdio, SSE, HTTP for external tools like filesystems and APIs.
Guides users through installing Claude Code plugins for WhatsApp, Telegram, Discord, iMessage, Fakechat to enable external messaging channels. Shows exact commands, prerequisites like Bun.
Share bugs, ideas, or general feedback.
A Claude Code channel is an MCP server that pushes events into a Claude Code session so Claude can react to things happening outside the terminal. Channels can be one-way (alerts, webhooks) or two-way (chat bridges with reply tools).
This procedure walks through building, packaging, and testing a channel integration from scratch.
claude --version)@modelcontextprotocol/sdk npm packageCreate a new plugin directory with the channel server and config files:
my-channel/
package.json # Dependencies
server.ts # MCP channel server
plugin.json # Plugin manifest
.mcp.json # MCP server config
Initialize the project:
mkdir my-channel && cd my-channel
bun init -y
bun add @modelcontextprotocol/sdk
The server has three essential parts:
claude/channel capabilityMinimal one-way server:
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
const mcp = new Server(
{ name: 'my-channel', version: '0.1.0' },
{
// claude/channel capability is REQUIRED — this registers the notification listener
capabilities: { experimental: { 'claude/channel': {} } },
// Instructions go into Claude's system prompt
instructions: 'Events arrive as <channel source="my-channel" ...>. Act on them.',
},
)
await mcp.connect(new StdioServerTransport())
// Push events to Claude:
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: 'The event body text',
meta: { key: 'value' }, // Each key becomes a <channel> tag attribute
},
})
If Claude needs to send messages back, expose an MCP tool:
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
// Add tools capability to the Server constructor:
// capabilities: { experimental: { 'claude/channel': {} }, tools: {} }
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'reply',
description: 'Send a reply back through this channel',
inputSchema: {
type: 'object',
properties: {
chat_id: { type: 'string', description: 'Conversation to reply in' },
text: { type: 'string', description: 'Message to send' },
},
required: ['chat_id', 'text'],
},
}],
}))
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === 'reply') {
const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
// Send the reply through your transport (pipe, HTTP, chat API, etc.)
await sendReply(chat_id, text)
return { content: [{ type: 'text', text: 'sent' }] }
}
throw new Error(`unknown tool: ${req.params.name}`)
})
Update instructions to tell Claude about the reply tool:
instructions: 'Messages arrive as <channel source="my-channel" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.'
Choose your transport based on the use case:
| Transport | Use Case | Example |
|---|---|---|
| Named pipes (mkfifo) | Local IPC between processes | fifo-pipe-channel |
| HTTP server | Webhooks, CI alerts | webhook-channel |
| Platform API polling | Chat platforms (Telegram, Discord) | telegram-channel |
| WebSocket | Real-time bidirectional | custom chat bridges |
For named pipes (see resources/fifo-pipe-example.md for full implementation):
import { execSync } from 'child_process'
// Create pipes
execSync('mkfifo /path/to/inbound.fifo')
execSync('mkfifo /path/to/outbound.fifo')
// Read loop: watch inbound pipe, push to Claude
async function readLoop() {
while (true) {
const file = Bun.file('/path/to/inbound.fifo')
const stream = file.stream()
const reader = stream.getReader()
// Read lines and call mcp.notification() for each
}
}
For HTTP (webhooks):
Bun.serve({
port: 8788,
hostname: '127.0.0.1',
async fetch(req) {
const body = await req.text()
await mcp.notification({
method: 'notifications/claude/channel',
params: { content: body, meta: { path: new URL(req.url).pathname } },
})
return new Response('ok')
},
})
plugin.json:
{
"name": "my-channel",
"version": "0.1.0",
"description": "Description of what this channel does",
"author": { "name": "Your Name" },
"mcpServers": "./.mcp.json"
}
.mcp.json:
{
"mcpServers": {
"my-channel": {
"command": "bun",
"args": ["${CLAUDE_PLUGIN_ROOT}/server.ts"]
}
}
}
package.json:
{
"name": "claude-channel-my-channel",
"version": "0.1.0",
"type": "module",
"bin": "./server.ts",
"scripts": { "start": "bun install --no-summary && bun server.ts" },
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }
}
Add an entry to .claude-plugin/marketplace.json:
{
"name": "my-channel",
"source": "./my-channel",
"description": "...",
"version": "0.1.0",
"category": "communication"
}
# Install the plugin
claude plugin install my-channel@your-marketplace
# Start Claude with the development channel flag
claude --dangerously-load-development-channels plugin:my-channel@your-marketplace
# Select "I am using this for local development" when prompted
Test the inbound path by sending data through your transport. Verify:
← my-channel · ... in the session)mcp.notification() to prevent prompt injection127.0.0.1, not 0.0.0.0claude/channel/permission capability to forward tool approval prompts remotely. Only do this if your channel authenticates the sender.Events arrive in Claude's context as XML tags:
<channel source="my-channel" key1="val1" key2="val2">
The event body content
</channel>
source is set automatically from the server namemeta keys become tag attributes (letters, digits, underscores only)content becomes the tag bodyThe instructions field in the Server constructor is injected into Claude's system prompt. It should tell Claude:
chat_id from the inbound tag)Custom channels need --dangerously-load-development-channels during the research preview. Format: plugin:<name>@<marketplace> or server:<name> for bare MCP servers.
external_plugins/fakechat in claude-plugins-officialfifo-pipe-channel/ in this repo