channels
# Channels Reference
What this skill does
# 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-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.
### Architecture
```
External System → Your Channel Server (local) ←stdio→ Claude Code Session
```
- **Chat platforms** (Telegram, Discord): Plugin polls platform API, forwards messages to Claude
- **Webhooks** (CI, monitoring): Server listens on local HTTP port, pushes POSTs to Claude
### Built-in Channels (Research Preview)
Telegram, Discord, iMessage, and fakechat are included. Custom channels require `--dangerously-load-development-channels` flag.
## Building a Channel
### Requirements
- `@modelcontextprotocol/sdk` package
- Node.js-compatible runtime (Bun, Node, Deno)
- stdio transport (Claude Code spawns as subprocess)
### Minimal One-Way Channel (Webhook Receiver)
```ts
#!/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')
},
})
```
### Registration (.mcp.json)
```json
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}
```
### Testing
```bash
# 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>
```
## Server Options
| 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. |
## Notification Format
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. |
```ts
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>
```
## Two-Way Channels: Reply Tool
Add a reply tool so Claude can send messages back:
1. Add `tools: {}` to capabilities
2. Register `ListToolsRequestSchema` and `CallToolRequestSchema` handlers
3. Update `instructions` to tell Claude when/how to reply
```ts
import { 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}`)
})
```
## Sender Gating (Security)
**An ungated channel is a prompt injection vector.** Always check sender identity before emitting notifications:
```ts
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.
### Pairing Flows
- **Telegram/Discord**: User DMs bot → bot sends pairing code → user approves in Claude Code → platform ID added to allowlist
- **iMessage**: Detects user's own addresses from Messages DB at startup, auto-allows
## Permission Relay
Requires Claude Code v2.1.81+. Lets remote users approve/deny tool use from another device.
### How It Works
1. Claude Code generates 5-letter request ID, notifies your server
2. Server forwards prompt + ID to chat app
3. User replies `yes <id>` or `no <id>`
4. Server parses reply into verdict notification back to Claude Code
Local terminal dialog stays open — first answer (local or remote) wins.
### Request ID Format
Five lowercase letters from `a-z` excluding `l` (avoids confusion with `1`/`I`).
### Permission Request Fields
| 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 |
### Implementation
```ts
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].tRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.