参考
Channels reference
Channels reference 这一页讲的,就是 Channels reference 这件事在 Claude Code 里到底怎么用。
页面信息
这页不是官方原文,而是顺着官方文档结构做的中文解释版。命令、参数、配置名这些硬东西尽量保留,解释部分则尽量讲成人能照着做的话。
如果你碰到特别敏感的配置、权限或企业环境差异,最好顺手点上面的“查看原始文档”再核一遍。
这一页先讲明白
这页主要讲 Channels reference:Build an MCP server that pushes webhooks, alerts, and chat messages into a Claude Code session. Reference for the channel contract: capability declaration, notification events, reply tools, sender gating, and permission relay.
你可以把它当成"Reference"这块里专门管这一摊事的说明书。
你可以把"Channels reference"理解成 Reference 这一栏里的一把专门工具。这页不是让你背书,而是教你什么时候该把这把工具拿出来。
原文这页大多会按 Overview、What you need、Example: build a webhook receiver、Test during the research preview 这些环节往下讲。
翻成人话,大概就是:Example: build a webhook receiver
第一,先别一上来全开全配。先按最小一步试通,确认没跑偏,再继续往下加。
第二,命令、配置名、参数名这些硬东西尽量保留原样。人话解释是帮你听懂,不是帮你改关键字。
第三,照着原文这几个环节挨个过:Overview -> What you need -> Example: build a webhook receiver -> Test during the research preview。像下地先看水路、再试机器、再正式开干,一步一步最稳。
原页关键片段:Example: build a webhook receiver 1
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk 原页关键片段:Example: build a webhook receiver 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
// Create the MCP server and declare it as a channel
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
// this key is what makes it a channel — Claude Code registers a listener for it
capabilities: { experimental: { 'claude/channel': {} } },
// added to Claude's system prompt so it knows how to handle these events
instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
},
)
// Connect to Claude Code over stdio (Claude Code spawns this process)
await mcp.connect(new StdioServerTransport())
// Start an HTTP server that forwards every POST to Claude
Bun.serve({
port: 8788, // any open port works
// localhost-only: nothing outside this machine can POST
hostname: '127.0.0.1',
async fetch(req) {
const body = await req.text()
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: body, // becomes the body of the <channel> tag
// each key becomes a tag attribute, e.g. <channel path="/" method="POST">
meta: { path: new URL(req.url).pathname, method: req.method },
},
})
return new Response('ok')
},
}) 原页关键片段:Example: build a webhook receiver 3
这一段说完,最后还得写到配置里才算真的生效。
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
} 原页关键片段:Test during the research preview
想把这条规矩固定住,就把下面这块老老实实写进去。
# Testing a plugin you're developing
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace
# Testing a bare .mcp.json server (no plugin wrapper yet)
claude --dangerously-load-development-channels server:webhook 原页关键片段:Server options
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
const mcp = new Server(
{ name: 'your-channel', version: '0.0.1' },
{
capabilities: {
experimental: { 'claude/channel': {} }, // registers the channel listener
tools: {}, // omit for one-way channels
},
// added to Claude's system prompt so it knows how to handle your events
instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
},
) 原页关键片段:Notification format 1
先别急着往下翻,下面这条命令跑完,心里才有底。
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: 'build failed on main: https://ci.example.com/run/1234',
meta: { severity: 'high', run_id: '1234' },
},
}) 原页关键片段:Notification format 2
这一段要真抓重点,通常就抓下面这块原文。
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel> 原页关键片段:Expose a reply tool 1
先看下面这块原始片段,等会儿再回头看解释会顺得多。
capabilities: {
experimental: { 'claude/channel': {} },
tools: {}, // enables tool discovery
}, 原页关键片段:Expose a reply tool 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
// Add this import at the top of webhook.ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
// Claude queries this at startup to discover what tools your server offers
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'reply',
description: 'Send a message back over this channel',
// inputSchema tells Claude what arguments to pass
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'],
},
}],
}))
// Claude calls this when it wants to invoke a tool
mcp.setRequestHandler(CallToolRequestSchema, async req => {
if (req.params.name === 'reply') {
const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
// send() is your outbound: POST to your chat platform, or for local
// testing the SSE broadcast shown in the full example below.
send(`Reply to ${chat_id}: ${text}`)
return { content: [{ type: 'text', text: 'sent' }] }
}
throw new Error(`unknown tool: ${req.params.name}`)
}) 原页关键片段:Expose a reply tool 3
先看下面这块原始片段,等会儿再回头看解释会顺得多。
instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.' 原页关键片段:Gate inbound messages
这会儿轮到改配置了,字段名和关键字别自己乱换。
const allowed = new Set(loadAllowlist()) // from your access.json or equivalent
// inside your message handler, before emitting:
if (!allowed.has(message.from.id)) { // sender, not room
return // drop silently
}
await mcp.notification({ ... }) 原页关键片段:Add relay to a chat bridge 1
先看下面这块原始片段,等会儿再回头看解释会顺得多。
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in to permission relay
},
tools: {},
}, Documentation Index
这里不是让你背"Documentation Index"这个词,而是让你看它真干活时怎么使。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
Overview
看到这里,就把"Overview"当成一件真要上手的活来看。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
What you need
这一段不只是挂个标题,它是在说明"What you need"这一块到底负责什么。
看这段时要特别盯工具和权限边界,别为了省事一把全开。
Example: build a webhook receiver
别把这段只当成标题看,它其实是在给"Example: build a webhook receiver"划边界。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
Example: build a webhook receiver 1
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk Example: build a webhook receiver 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
// Create the MCP server and declare it as a channel
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
// this key is what makes it a channel — Claude Code registers a listener for it
capabilities: { experimental: { 'claude/channel': {} } },
// added to Claude's system prompt so it knows how to handle these events
instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
},
)
// Connect to Claude Code over stdio (Claude Code spawns this process)
await mcp.connect(new StdioServerTransport())
// Start an HTTP server that forwards every POST to Claude
Bun.serve({
port: 8788, // any open port works
// localhost-only: nothing outside this machine can POST
hostname: '127.0.0.1',
async fetch(req) {
const body = await req.text()
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: body, // becomes the body of the <channel> tag
// each key becomes a tag attribute, e.g. <channel path="/" method="POST">
meta: { path: new URL(req.url).pathname, method: req.method },
},
})
return new Response('ok')
},
}) Example: build a webhook receiver 3
这一段说完,最后还得写到配置里才算真的生效。
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
} Example: build a webhook receiver 4
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
claude --dangerously-load-development-channels server:webhook Example: build a webhook receiver 5
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234" Example: build a webhook receiver 6
先看下面这块原始片段,等会儿再回头看解释会顺得多。
<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel> Test during the research preview
这一块主要是在说"Test during the research preview"真到手上该怎么用,哪里最容易踩坑。
看这段时要特别盯工具和权限边界,别为了省事一把全开。
Test during the research preview
想把这条规矩固定住,就把下面这块老老实实写进去。
# Testing a plugin you're developing
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace
# Testing a bare .mcp.json server (no plugin wrapper yet)
claude --dangerously-load-development-channels server:webhook Server options
看到这里,就把"Server options"当成一件真要上手的活来看。
看这段时要特别盯工具和权限边界,别为了省事一把全开。
Server options
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
const mcp = new Server(
{ name: 'your-channel', version: '0.0.1' },
{
capabilities: {
experimental: { 'claude/channel': {} }, // registers the channel listener
tools: {}, // omit for one-way channels
},
// added to Claude's system prompt so it knows how to handle your events
instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
},
) Notification format
这一段主要是在把"Notification format"讲实,不是只摆个标题给你看。
如果你打算把外接能力往里挂,这里提到的 hooks、MCP、skills、memory 都要分清各自负责哪一摊。
Notification format 1
先别急着往下翻,下面这条命令跑完,心里才有底。
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: 'build failed on main: https://ci.example.com/run/1234',
meta: { severity: 'high', run_id: '1234' },
},
}) Notification format 2
这一段要真抓重点,通常就抓下面这块原文。
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel> Expose a reply tool
这一段更像在讲判断条件,什么时候该上,什么时候先别急。把触发条件看清,比背标题更重要。
看这段时要特别盯工具和权限边界,别为了省事一把全开。
Expose a reply tool 1
先看下面这块原始片段,等会儿再回头看解释会顺得多。
capabilities: {
experimental: { 'claude/channel': {} },
tools: {}, // enables tool discovery
}, Expose a reply tool 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
// Add this import at the top of webhook.ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
// Claude queries this at startup to discover what tools your server offers
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'reply',
description: 'Send a message back over this channel',
// inputSchema tells Claude what arguments to pass
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'],
},
}],
}))
// Claude calls this when it wants to invoke a tool
mcp.setRequestHandler(CallToolRequestSchema, async req => {
if (req.params.name === 'reply') {
const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
// send() is your outbound: POST to your chat platform, or for local
// testing the SSE broadcast shown in the full example below.
send(`Reply to ${chat_id}: ${text}`)
return { content: [{ type: 'text', text: 'sent' }] }
}
throw new Error(`unknown tool: ${req.params.name}`)
}) Expose a reply tool 3
先看下面这块原始片段,等会儿再回头看解释会顺得多。
instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.' Expose a reply tool 4
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
// --- Outbound: write to any curl -N listeners on /events --------------------
// A real bridge would POST to your chat platform instead.
const listeners = new Set<(chunk: string) => void>()
function send(text: string) {
const chunk = text.split('\n').map(l => `data: ${l}\n`).join('') + '\n'
for (const emit of listeners) emit(chunk)
}
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
capabilities: {
experimental: { 'claude/channel': {} },
tools: {},
},
instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.',
},
)
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}`)
})
await mcp.connect(new StdioServerTransport())
let nextId = 1
Bun.serve({
port: 8788,
hostname: '127.0.0.1',
idleTimeout: 0, // don't close idle SSE streams
async fetch(req) {
const url = new URL(req.url)
// GET /events: SSE stream so curl -N can watch Claude's replies live
if (req.method === 'GET' && url.pathname === '/events') {
const stream = new ReadableStream({
start(ctrl) {
ctrl.enqueue(': connected\n\n') // so curl shows something immediately
const emit = (chunk: string) => ctrl.enqueue(chunk)
listeners.add(emit)
req.signal.addEventListener('abort', () => listeners.delete(emit))
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
})
}
// POST: forward to Claude as a channel event
const body = await req.text()
const chat_id = String(nextId++)
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: body,
meta: { chat_id, path: url.pathname, method: req.method },
},
})
return new Response('ok')
},
}) Gate inbound messages
这一段主要是在把"Gate inbound messages"讲实,不是只摆个标题给你看。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
Gate inbound messages
这会儿轮到改配置了,字段名和关键字别自己乱换。
const allowed = new Set(loadAllowlist()) // from your access.json or equivalent
// inside your message handler, before emitting:
if (!allowed.has(message.from.id)) { // sender, not room
return // drop silently
}
await mcp.notification({ ... }) Relay permission prompts
看到这里,就把"Relay permission prompts"当成一件真要上手的活来看。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
How relay works
看懂这里,后面出怪事时你心里会更有底。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
Permission request fields
这一段就是给你查规矩的,像看说明书那样一项项对着来。
看这段时要特别盯工具和权限边界,别为了省事一把全开。
Add relay to a chat bridge
看到这里,就把"Add relay to a chat bridge"当成一件真要上手的活来看。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
Add relay to a chat bridge 1
先看下面这块原始片段,等会儿再回头看解释会顺得多。
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in to permission relay
},
tools: {},
}, Add relay to a chat bridge 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
import { z } from 'zod'
// setNotificationHandler routes by z.literal on the method field,
// so this schema is both the validator and the dispatch key
const PermissionRequestSchema = z.object({
method: z.literal('notifications/claude/channel/permission_request'),
params: z.object({
request_id: z.string(), // five lowercase letters, include verbatim in your prompt
tool_name: z.string(), // e.g. "Bash", "Write"
description: z.string(), // human-readable summary of this call
input_preview: z.string(), // tool args as JSON, truncated to ~200 chars
}),
})
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
// send() is your outbound: POST to your chat platform, or for local
// testing the SSE broadcast shown in the full example below.
send(
`Claude wants to run ${params.tool_name}: ${params.description}\n\n` +
// the ID in the instruction is what your inbound handler parses in Step 3
`Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
)
}) Add relay to a chat bridge 3
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
// matches "y abcde", "yes abcde", "n abcde", "no abcde"
// [a-km-z] is the ID alphabet Claude Code uses (lowercase, skips 'l')
// /i tolerates phone autocorrect; lowercase the capture before sending
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
async function onInbound(message: PlatformMessage) {
if (!allowed.has(message.from.id)) return // gate on sender first
const m = PERMISSION_REPLY_RE.exec(message.text)
if (m) {
// m[1] is the verdict word, m[2] is the request ID
// emit the verdict notification back to Claude Code instead of chat
await mcp.notification({
method: 'notifications/claude/channel/permission',
params: {
request_id: m[2].toLowerCase(), // normalize in case of autocorrect caps
behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
},
})
return // handled as verdict, don't also forward as chat
}
// didn't match verdict format: fall through to the normal chat path
await mcp.notification({
method: 'notifications/claude/channel',
params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
})
} Full example
别把这段只当成标题看,它其实是在给"Full example"划边界。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
Full example 1
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
// --- Outbound: write to any curl -N listeners on /events --------------------
// A real bridge would POST to your chat platform instead.
const listeners = new Set<(chunk: string) => void>()
function send(text: string) {
const chunk = text.split('\n').map(l => `data: ${l}\n`).join('') + '\n'
for (const emit of listeners) emit(chunk)
}
// Sender allowlist. For the local walkthrough we trust the single X-Sender
// header value "dev"; a real bridge would check the platform's user ID.
const allowed = new Set(['dev'])
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in to permission relay
},
tools: {},
},
instructions:
'Messages arrive as <channel source="webhook" chat_id="...">. ' +
'Reply with the reply tool, passing the chat_id from the tag.',
},
)
// --- reply tool: Claude calls this to send a message back -------------------
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}`)
})
// --- permission relay: Claude Code (not Claude) calls this when a dialog opens
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}"`,
)
})
await mcp.connect(new StdioServerTransport())
// --- HTTP on :8788: GET /events streams outbound, POST routes inbound -------
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
let nextId = 1
Bun.serve({
port: 8788,
hostname: '127.0.0.1',
idleTimeout: 0, // don't close idle SSE streams
async fetch(req) {
const url = new URL(req.url)
// GET /events: SSE stream so curl -N can watch replies and prompts live
if (req.method === 'GET' && url.pathname === '/events') {
const stream = new ReadableStream({
start(ctrl) {
ctrl.enqueue(': connected\n\n') // so curl shows something immediately
const emit = (chunk: string) => ctrl.enqueue(chunk)
listeners.add(emit)
req.signal.addEventListener('abort', () => listeners.delete(emit))
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
})
}
// everything else is inbound: gate on sender first
const body = await req.text()
const sender = req.headers.get('X-Sender') ?? ''
if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })
// check for verdict format before treating as chat
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 new Response('verdict recorded')
}
// normal chat: forward to Claude as a channel event
const chat_id = String(nextId++)
await mcp.notification({
method: 'notifications/claude/channel',
params: { content: body, meta: { chat_id, path: url.pathname } },
})
return new Response('ok')
},
}) Full example 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
claude --dangerously-load-development-channels server:webhook Full example 3
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
curl -N localhost:8788/events Full example 4
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788 Full example 5
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788 Package as a plugin
这一段主要是在把"Package as a plugin"讲实,不是只摆个标题给你看。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
See also
这一段主要是在把"See also"讲实,不是只摆个标题给你看。
这里还牵扯作用域,意思就是这条规则到底管当前项目、你个人,还是只管这一趟会话。
照着做一遍
如果你不想来回翻,就先照这几步顺着做。
每做完一步就看一下结果,再决定要不要继续往下。
第 1 步:Example: build a webhook receiver 1
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk 第 2 步:Example: build a webhook receiver 2
这一段不是只让你理解意思,下面这条命令就是现在要跑的。
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
// Create the MCP server and declare it as a channel
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
// this key is what makes it a channel — Claude Code registers a listener for it
capabilities: { experimental: { 'claude/channel': {} } },
// added to Claude's system prompt so it knows how to handle these events
instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
},
)
// Connect to Claude Code over stdio (Claude Code spawns this process)
await mcp.connect(new StdioServerTransport())
// Start an HTTP server that forwards every POST to Claude
Bun.serve({
port: 8788, // any open port works
// localhost-only: nothing outside this machine can POST
hostname: '127.0.0.1',
async fetch(req) {
const body = await req.text()
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: body, // becomes the body of the <channel> tag
// each key becomes a tag attribute, e.g. <channel path="/" method="POST">
meta: { path: new URL(req.url).pathname, method: req.method },
},
})
return new Response('ok')
},
}) 第 3 步:Example: build a webhook receiver 3
这一段说完,最后还得写到配置里才算真的生效。
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
} 第 4 步:Test during the research preview
想把这条规矩固定住,就把下面这块老老实实写进去。
# Testing a plugin you're developing
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace
# Testing a bare .mcp.json server (no plugin wrapper yet)
claude --dangerously-load-development-channels server:webhook 一眼看懂这一页
先把这页到底在讲什么看明白,再去碰具体命令和配置,最不容易绕晕。
Channels reference
|
v
这是 Reference 里的一摊要紧活
|
v
先弄懂,再下手 文末提醒
这站会按官方 docs 的导航和内容变化继续重生成,原站加页、删页、改页时,这里会跟着更新。
人话解释会尽量顺着原页往下讲,但命令、参数名、配置名这些硬东西还是保留原样,免得你抄过去跑不起来。