Appearance
Channels 技术参考
本文是为开发者准备的 Channels 协议参考,说明如何构建将外部事件推送到 Claude Code 会话的 MCP 服务器(Channel)。涵盖能力声明、通知格式、双向回复工具、发送方验证(防止提示注入)、权限中继(让你可以在手机上远程批准工具调用),并附有完整的 webhook.ts 示例代码。
Channels 处于研究预览阶段,需要 Claude Code v2.1.80+。需要 claude.ai 登录,不支持 Console 和 API Key 认证。Team/Enterprise 组织需管理员明确启用。
Channel 是一个将外部事件推送到 Claude Code 会话的 MCP 服务器,使 Claude 能够响应终端之外发生的事情。
单向 Channel:转发告警、Webhook 或监控事件供 Claude 响应。双向 Channel:类似聊天桥接,还暴露一个回复工具,让 Claude 可以发送消息。具有可信发送方路径的 Channel 还可选择加入权限中继,让你可以远程批准或拒绝工具使用。
概览
Channel 是运行在与 Claude Code 同一台机器上的 MCP 服务器。Claude Code 将其作为子进程启动,通过 stdio 通信。Channel 服务器是外部系统与 Claude Code 会话之间的桥梁:
- 聊天平台(Telegram、Discord):插件在本地运行,轮询平台 API 获取新消息。无需暴露 URL。
- Webhook(CI、监控):服务器监听本地 HTTP 端口,外部系统 POST 到该端口,服务器将负载推送给 Claude。
前提条件
只需要 @modelcontextprotocol/sdk 包和兼容 Node.js 的运行时(Bun、Node 或 Deno 均可)。
服务器需要:
- 声明
claude/channel能力,以便 Claude Code 注册通知监听器 - 在事件发生时发送
notifications/claude/channel通知 - 通过 stdio transport 连接(Claude Code 将你的服务器作为子进程启动)
研究预览期间,自定义 Channel 不在官方批准名单上。使用 --dangerously-load-development-channels 在本地测试。
示例:构建 Webhook 接收器
这个演示构建一个单文件服务器,监听 HTTP 请求并将其转发到 Claude Code 会话。任何能发 HTTP POST 的系统(CI 流水线、监控告警、curl 命令)都可以向 Claude 推送事件。
最小实现
typescript
#!/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' },
{
capabilities: { experimental: { 'claude/channel': {} } },
instructions: '来自 webhook channel 的事件以 <channel source="webhook" ...> 格式到达。它们是单向的:读取并响应,不需要回复。',
},
)
await mcp.connect(new StdioServerTransport())
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')
},
})在 .mcp.json 中注册:
json
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}测试(研究预览期间需要开发标志):
bash
claude --dangerously-load-development-channels server:webhook
# 另一个终端模拟 Webhook
curl -X POST localhost:8788 -d "main 分支构建失败:https://ci.example.com/run/1234"研究预览期间测试
期间每个 Channel 必须在批准名单中才能注册。开发标志为特定条目绕过名单(需要确认提示):
bash
# 测试开发中的插件
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace
# 测试裸 .mcp.json 服务器(尚无插件包装)
claude --dangerously-load-development-channels server:webhook服务器选项
Channel 在 Server 构造器中设置这些选项:
| 字段 | 类型 | 说明 |
|---|---|---|
capabilities.experimental['claude/channel'] | object | 必填,始终为 {},注册通知监听器 |
capabilities.experimental['claude/channel/permission'] | object | 可选,始终为 {},声明此 Channel 可接收权限中继请求 |
capabilities.tools | object | 双向 Channel 才需要,标准 MCP 工具能力声明 |
instructions | string | 建议填写,添加到 Claude 的系统提示,告知事件格式和如何回复 |
一个双向 Channel 的构造器示例:
typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
const mcp = new Server(
{ name: 'your-channel', version: '0.0.1' },
{
capabilities: {
experimental: { 'claude/channel': {} },
tools: {}, // 单向 Channel 省略这行
},
instructions: '消息以 <channel source="your-channel"...> 到达。用 reply 工具回复。',
},
)通知格式
通过 mcp.notification() 推送事件,method 为 notifications/claude/channel:
| 字段 | 类型 | 说明 |
|---|---|---|
content | string | 事件正文,作为 <channel> 标签的内容 |
meta | Record<string, string> | 可选,每个键值对变为 <channel> 标签的属性(键只能包含字母、数字、下划线,含连字符等其他字符的键会被静默丢弃) |
typescript
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' },
},
})Claude 收到的事件(source 属性由服务器名称自动设置):
xml
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>暴露回复工具(双向 Channel)
双向 Channel 需要暴露一个标准 MCP 工具让 Claude 可以发送回复,包含三个组成部分:
- 服务器构造器 capabilities 中的
tools: {}条目 - 定义工具 schema 和实现发送逻辑的工具处理器
- 服务器构造器中的
instructions字符串,告知 Claude 何时调用工具以及如何使用
typescript
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'reply',
description: '通过此 Channel 发送消息回复',
inputSchema: {
type: 'object',
properties: {
chat_id: { type: 'string', description: '要回复的会话 ID' },
text: { type: 'string', description: '要发送的消息' },
},
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 }
// 发送回复的实际逻辑(POST 到平台 API 等)
send(`Reply to ${chat_id}: ${text}`)
return { content: [{ type: 'text', text: 'sent' }] }
}
throw new Error(`unknown tool: ${req.params.name}`)
})验证入站消息(防止提示注入)
未验证的 Channel 是提示注入的入口:任何能访问你端点的人都能向 Claude 发送文本。聊天平台或公开端点的 Channel 必须在发送通知前验证发送方。
typescript
const allowed = new Set(loadAllowlist()) // 从你的 access.json 或等效方式加载
// 在消息处理器中,发送前检查:
if (!allowed.has(message.from.id)) { // 基于发送方身份
return // 静默丢弃
}
await mcp.notification({ ... })重要:基于发送方身份(
message.from.id)而非聊天室身份(message.chat.id)验证。在群聊中,两者不同——仅验证聊天室会让允许名单中群组的任何成员都能向 Claude 注入消息。
中继权限提示
当 Claude 调用需要批准的工具时,本地终端会打开对话框,会话等待。双向 Channel 可以选择接收相同提示,并将其中继到另一台设备(如手机)。本地终端和远程端均保持有效,先到达的回复被采用。
权限中继只覆盖工具使用审批(Bash、Write、Edit 等)。项目信任和 MCP 服务器同意对话框不中继,只在本地终端显示。
中继流程
- Claude Code 生成短请求 ID,通知你的服务器
- 你的服务器将提示和 ID 转发到聊天应用
- 远程用户用该 ID 回复是或否
- 你的入站处理器解析回复为裁决,Claude Code 应用(ID 必须匹配)
权限请求字段
出站通知方法:notifications/claude/channel/permission_request
| 字段 | 说明 |
|---|---|
request_id | 5 个小写字母(不含 l,避免与 1 混淆),关联裁决用 |
tool_name | 工具名称(如 Bash、Write) |
description | 可读的操作摘要(与本地对话框相同的文本) |
input_preview | 工具参数的 JSON 字符串(截断到 200 字符) |
回复裁决方法:notifications/claude/channel/permission,包含 request_id(回传 ID)和 behavior('allow' 或 'deny')。
添加权限中继到聊天桥接
声明权限能力:
typescript
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // 选择加入权限中继
},
tools: {},
},处理权限请求通知:
typescript
import { z } from 'zod'
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 想运行 ${params.tool_name}:${params.description}\n\n` +
`回复 "yes ${params.request_id}" 或 "no ${params.request_id}"`,
)
})拦截入站裁决(在正常聊天转发之前):
typescript
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 new Response('verdict recorded')
}
// 不匹配裁决格式 → 继续作为正常聊天消息处理只有你的 Channel 验证了发送方才应该声明 claude/channel/permission 能力——能通过 Channel 回复的人就能批准 Claude 的工具调用。
将 Channel 打包为插件
要让 Channel 可安装和共享,将其封装在插件中并发布到市场。用户通过 /plugin install 安装,然后用 --channels plugin:<name>@<marketplace> 启用。
发布到你自己市场的 Channel 仍需 --dangerously-load-development-channels 运行,因为它不在官方批准名单上。要加入名单,提交插件到官方市场。Team/Enterprise 管理员也可以将你的插件加入组织自己的 allowedChannelPlugins 列表。
相关文档
- Channels — 安装和使用 Telegram、Discord、iMessage 或 fakechat 演示
- GitHub 上的参考实现 — 包含配对流程、回复工具和文件附件的完整代码
- MCP — Channel 服务器实现的底层协议
- 插件 — 将 Channel 打包为可安装插件
常见问题
Q: Channel 和普通 MCP 服务器有什么区别?
普通 MCP 服务器是被动的——Claude 在任务中主动查询它们(如读取数据、执行搜索)。Channel 是主动的——外部系统可以向正在运行的 Claude Code 会话推送事件,Claude 会主动响应。技术上,Channel 就是声明了 claude/channel 能力的 MCP 服务器。
Q: 如何防止 Channel 成为提示注入入口?
在调用 mcp.notification() 之前,基于发送方身份(不是聊天室)验证消息来源,只有在允许名单中的发送方才能推送消息。注意群聊场景中 from.id 和 chat.id 的区别。
Q: 权限中继的 request_id 为什么不含字母 l?
request_id 是 5 个来自 a-z(不含 l)的小写字母,避免字母 l 和数字 1 在手机小屏幕上视觉混淆,提高远程批准时的准确率。