plugins/claude-code-expert/skills/channels/SKILL.md
# Channels Reference > Build MCP servers that push webhooks, alerts, and chat messages into a Claude Code session. > Requires Claude Code v2.1.80+ (permission relay requires v2.1.81+). > Research preview — requires claude.ai login; Console/API key auth not supported. ## What Is a Channel A channel is an MCP server that pushes events into a Claude Code session so Claude can react to things happening outside the terminal. Claude Code spawns it as a subprocess and communicates over stdio. **One
npx skillsauth add markus41/claude plugins/claude-code-expert/skills/channelsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build MCP servers that push webhooks, alerts, and chat messages into a Claude Code session. Requires Claude Code v2.1.80+ (permission relay requires v2.1.81+). Research preview — requires claude.ai login; Console/API key auth not supported.
A channel is an MCP server that pushes events into a Claude Code session so Claude can react to things happening outside the terminal. Claude Code spawns it as a subprocess and communicates over stdio.
One-way channels: Forward alerts, webhooks, monitoring events for Claude to act on. Two-way channels: Also expose a reply tool so Claude can send messages back. Permission relay: Trusted two-way channels can forward tool approval prompts to remote devices.
External System → Your Channel Server (local) ←stdio→ Claude Code Session
Telegram, Discord, iMessage, and fakechat are included. Custom channels require --dangerously-load-development-channels flag.
@modelcontextprotocol/sdk package#!/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: 'webhook', version: '0.0.1' },
{
// This key makes it a channel — Claude Code registers a listener
capabilities: { experimental: { 'claude/channel': {} } },
// Added to Claude's system prompt
instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. Read them and act, no reply expected.',
},
)
await mcp.connect(new StdioServerTransport())
// HTTP server forwards every POST to Claude
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, method: req.method },
},
})
return new Response('ok')
},
})
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}
# Start with development flag
claude --dangerously-load-development-channels server:webhook
# In another terminal, send a test event
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
Events arrive as <channel> tags:
<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel>
| Field | Type | Description |
|-------|------|-------------|
| capabilities.experimental['claude/channel'] | object | Required. Always {}. Registers the notification listener. |
| capabilities.experimental['claude/channel/permission'] | object | Optional. Enables permission relay for remote tool approval. |
| capabilities.tools | object | Two-way only. Always {}. Enables MCP tool discovery. |
| instructions | string | Recommended. Added to Claude's system prompt. Describe events, reply behavior, and routing. |
Push events via mcp.notification() with method notifications/claude/channel:
| Field | Type | Description |
|-------|------|-------------|
| content | string | Event body — becomes body of <channel> tag |
| meta | Record<string, string> | Optional. Each entry becomes a tag attribute (e.g., chat_id, severity). Keys: letters, digits, underscores only. |
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: 'build failed on main',
meta: { severity: 'high', run_id: '1234' },
},
})
// Arrives as: <channel source="your-channel" severity="high" run_id="1234">build failed on main</channel>
Add a reply tool so Claude can send messages back:
tools: {} to capabilitiesListToolsRequestSchema and CallToolRequestSchema handlersinstructions to tell Claude when/how to replyimport { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'reply',
description: 'Send a message back over this channel',
inputSchema: {
type: 'object',
properties: {
chat_id: { type: 'string', description: 'The conversation to reply in' },
text: { type: 'string', description: 'The 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(`Reply to ${chat_id}: ${text}`)
return { content: [{ type: 'text', text: 'sent' }] }
}
throw new Error(`unknown tool: ${req.params.name}`)
})
An ungated channel is a prompt injection vector. Always check sender identity before emitting notifications:
const allowed = new Set(loadAllowlist())
// Gate on sender identity, NOT room/chat identity
if (!allowed.has(message.from.id)) {
return // drop silently
}
await mcp.notification({ ... })
Gate on message.from.id, not message.chat.id — in group chats these differ, and gating on room would let anyone in an allowlisted group inject messages.
Requires Claude Code v2.1.81+. Lets remote users approve/deny tool use from another device.
yes <id> or no <id>Local terminal dialog stays open — first answer (local or remote) wins.
Five lowercase letters from a-z excluding l (avoids confusion with 1/I).
| Field | Description |
|-------|-------------|
| request_id | Five-letter ID to echo in verdict |
| tool_name | Tool name (e.g., Bash, Write) |
| description | Human-readable summary of tool call |
| input_preview | Tool args as JSON, truncated to 200 chars |
import { z } from 'zod'
// 1. Declare capability
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in
},
tools: {},
},
// 2. Handle incoming permission requests
const PermissionRequestSchema = z.object({
method: z.literal('notifications/claude/channel/permission_request'),
params: z.object({
request_id: z.string(),
tool_name: z.string(),
description: z.string(),
input_preview: z.string(),
}),
})
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
send(
`Claude wants to run ${params.tool_name}: ${params.description}\n\n` +
`Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
)
})
// 3. Parse verdict from inbound messages
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
const m = PERMISSION_REPLY_RE.exec(body)
if (m) {
await mcp.notification({
method: 'notifications/claude/channel/permission',
params: {
request_id: m[2].toLowerCase(),
behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
},
})
return // don't also forward as chat
}
Wrap your channel in a plugin and publish to a marketplace:
/plugin install--channels plugin:<name>@<marketplace>--dangerously-load-development-channelsallowedChannelPlugins list instead| Symptom | Diagnosis |
|---------|-----------|
| curl succeeds but event doesn't reach Claude | Run /mcp to check server status. Check ~/.claude/debug/<session-id>.txt for stderr |
| curl fails with "connection refused" | Port not bound or stale process. lsof -i :<port> to check, kill stale process |
| "blocked by org policy" | Team/Enterprise admin must enable channels first |
| Permission relay verdict ignored | ID doesn't match open request — check format (5 lowercase letters, no l) |
claude/channel experimental capability<channel source="name" ...>content</channel> tagstools: {} capability and reply tool handlersclaude/channel/permission capability and v2.1.81+development
Enhanced plan-authoring skill with Pre-Writing context gathering, task metadata, non-TDD templates, Red Flags, telemetry, and an automated plan linter. Use when you have a spec or requirements for a multi-step task, before touching code.
tools
Documentation intelligence engine with graph-based API docs, algorithm library, and drift detection
tools
Ultraplan cloud planning — kick off a plan in the cloud from your terminal, review and revise in the browser, then execute remotely or send back to CLI
tools
--- name: mcp description: Configure MCP servers for Claude Code — stdio vs HTTP, authentication, Tools/Resources/Prompts distinction, channels (CI webhook, mobile relay, Discord bridge, fakechat), and cost of always-loaded tools. Use this skill whenever adding an MCP server, debugging connection issues, choosing between MCP Tools vs Prompts vs Resources, installing channel servers, or managing .mcp.json. Triggers on: "MCP server", "mcp config", "add Obsidian MCP", "install context7", "channels"