文档索引
获取完整文档索引:https://code.claude.com/docs/llms.txt 使用此文件发现所有可用页面,然后进一步探索。
通道参考
构建一个 MCP 服务器,将 webhooks、提醒和聊天消息推送到 Claude Code 会话中。通道契约参考:能力声明、通知事件、回复工具、发送者网关和权限中继。
通道是一种 MCP 服务器,可以将事件推送到 Claude Code 会话中,使 Claude 能够对终端外部发生的事件做出反应。
你可以构建单向或双向通道。单向通道转发提醒、webhook 或监控事件供 Claude 处理。双向通道(如聊天桥接)还会暴露回复工具,使 Claude 能够发回消息。具有可信发送者路径的通道还可以选择中继权限提示,以便你远程批准或拒绝工具使用。
本页涵盖:
- 概述:通道的工作原理
- 前提条件:要求和一般步骤
- 示例:构建 webhook 接收器:最小化单向演练
- 服务器选项:构造函数字段
- 通知格式:事件载荷和传递行为
- 暴露回复工具:让 Claude 发回消息
- 网关入站消息:防止提示注入的发送者检查
- 中继权限提示:将工具审批提示转发到远程通道
要使用现有通道而非自行构建,请参阅通道。Telegram、Discord、iMessage 和 fakechat 已包含在研究预览中。
概述
通道是一个 MCP 服务器,运行在与 Claude Code 相同的机器上。Claude Code 将其作为子进程生成,并通过 stdio 进行通信。你的通道服务器是外部系统和 Claude Code 会话之间的桥梁:
- 聊天平台(Telegram、Discord):你的插件在本地运行并轮询平台的 API 获取新消息。当有人给你的机器人发私信时,插件接收消息并转发给 Claude。无需暴露 URL。
- Webhooks(CI、监控):你的服务器监听本地 HTTP 端口。外部系统 POST 到该端口,你的服务器将载荷推送给 Claude。
前提条件
唯一硬性要求是 @modelcontextprotocol/sdk 包和兼容 Node.js 的运行时。Bun、Node 和 Deno 都可以。研究预览中的预构建插件使用 Bun,但你的通道不必如此。
你的服务器需要:
- 声明
claude/channel能力,以便 Claude Code 注册通知监听器 - 当事件发生时发出
notifications/claude/channel事件 - 通过 stdio 传输连接(Claude Code 将你的服务器作为子进程生成)
服务器选项和通知格式部分详细介绍了这些内容。完整演练请参阅示例:构建 webhook 接收器。
在研究预览期间,自定义通道不在批准的允许列表中。使用 --dangerously-load-development-channels 在本地测试。详情请参阅研究预览期间测试。
示例:构建 webhook 接收器
本演练构建一个单文件服务器,监听 HTTP 请求并将其转发到你的 Claude Code 会话中。完成后,任何能发送 HTTP POST 的工具(如 CI 流水线、监控提醒或 curl 命令)都可以向 Claude 推送事件。
本示例使用 Bun 作为运行时,因其内置 HTTP 服务器和 TypeScript 支持。你可以改用 Node 或 Deno;唯一要求是 MCP SDK。
创建项目
创建新目录并安装 MCP SDK:
mkdir webhook-channel && cd webhook-channel bun add @modelcontextprotocol/sdk编写通道服务器
创建名为
webhook.ts的文件。这就是你的整个通道服务器:它通过 stdio 连接到 Claude Code,并在端口 8788 上监听 HTTP POST。当请求到达时,它将请求体作为通道事件推送给 Claude。#!/usr/bin/env bun import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' // 创建 MCP 服务器并将其声明为通道 const mcp = new Server( { name: 'webhook', version: '0.0.1' }, { // 此键使其成为通道 — Claude Code 为其注册监听器 capabilities: { experimental: { 'claude/channel': {} } }, // 添加到 Claude 的系统提示中,以便它知道如何处理这些事件 instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.', }, ) // 通过 stdio 连接到 Claude Code(Claude Code 生成此进程) await mcp.connect(new StdioServerTransport()) // 启动 HTTP 服务器,将每个 POST 转发给 Claude Bun.serve({ port: 8788, // 任何开放端口都可以 // 仅限 localhost:此机器外部无法 POST hostname: '127.0.0.1', async fetch(req) { const body = await req.text() await mcp.notification({ method: 'notifications/claude/channel', params: { content: body, // 成为 <channel> 标签的正文 // 每个键成为标签属性,例如 <channel path="/" method="POST"> meta: { path: new URL(req.url).pathname, method: req.method }, }, }) return new Response('ok') }, })该文件按顺序执行三件事:
- 服务器配置:创建带有
claude/channel能力的 MCP 服务器,这是告诉 Claude Code 这是一个通道的标识。instructions字符串进入 Claude 的系统提示:告诉 Claude 预期什么事件、是否需要回复、以及如果需要回复如何路由。 - Stdio 连接:通过 stdin/stdout 连接到 Claude Code。这是任何 MCP 服务器的标准做法:Claude Code 将其作为子进程生成。
- HTTP 监听器:在端口 8788 上启动本地 Web 服务器。每个 POST 请求体通过
mcp.notification()作为通道事件转发给 Claude。content成为事件正文,每个meta条目成为<channel>标签上的属性。监听器需要访问mcp实例,因此在同一进程中运行。对于更大的项目,你可以将其拆分为单独的模块。
- 服务器配置:创建带有
向 Claude Code 注册你的服务器
将服务器添加到你的 MCP 配置中,以便 Claude Code 知道如何启动它。对于同目录下的项目级
.mcp.json,使用相对路径。对于~/.claude.json中的用户级配置,使用完整绝对路径,以便从任何项目都能找到服务器:{ "mcpServers": { "webhook": { "command": "bun", "args": ["./webhook.ts"] } } }Claude Code 在启动时读取你的 MCP 配置,并将每个服务器作为子进程生成。
测试
在研究预览期间,自定义通道不在允许列表中,因此使用开发标志启动 Claude Code:
claude --dangerously-load-development-channels server:webhookClaude Code 启动时会读取你的 MCP 配置,将你的
webhook.ts作为子进程生成,HTTP 监听器会自动在你配置的端口(本例中为 8788)上启动。你不需要自己运行服务器。如果看到"blocked by org policy",你的组织管理员需要先启用通道。
在另一个终端中,通过向你的服务器发送 HTTP POST 来模拟 webhook。本示例向端口 8788(或你配置的端口)发送 CI 失败提醒:
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"载荷以
<channel>标签的形式到达你的 Claude Code 会话:<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel>在你的 Claude Code 终端中,你会看到 Claude 接收消息并开始响应:读取文件、运行命令或执行消息所需的任何操作。这是单向通道,因此 Claude 在你的会话中操作但不会通过 webhook 发回任何内容。要添加回复,请参阅暴露回复工具。
如果事件未到达,诊断取决于
curl的返回:curl成功但 Claude 未收到:在你的会话中运行/mcp检查服务器状态。"Failed to connect" 通常意味着服务器文件中的依赖或导入错误;检查~/.claude/debug/<session-id>.txt中的调试日志获取 stderr 跟踪。curl失败并显示"connection refused":端口尚未绑定或之前运行的残留进程占用了端口。lsof -i :<port>显示监听内容;在重启会话前kill残留进程。
fakechat 服务器扩展了此模式,添加了 Web UI、文件附件和双向聊天的回复工具。
研究预览期间测试
在研究预览期间,每个通道必须在批准的允许列表上才能注册。开发标志在确认提示后绕过特定条目的允许列表。本示例展示了两种条目类型:
# 测试你正在开发的插件
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace
# 测试裸 .mcp.json 服务器(尚无插件封装)
claude --dangerously-load-development-channels server:webhook
绕过是逐条目的。将此标志与 --channels 结合使用不会将绕过扩展到 --channels 条目。在研究预览期间,批准的允许列表由 Anthropic 策展,因此你的通道在构建和测试期间保持使用开发标志。
此标志仅跳过允许列表。channelsEnabled 组织策略仍然适用。不要使用它运行来自不受信任来源的通道。
服务器选项
通道在 Server 构造函数中设置这些选项。instructions 和 capabilities.tools 字段是标准 MCP;capabilities.experimental['claude/channel'] 和 capabilities.experimental['claude/channel/permission'] 是通道特定的新增项:
| 字段 | 类型 | 描述 |
|---|---|---|
capabilities.experimental['claude/channel'] | object | 必需。始终为 {}。存在即注册通知监听器。 |
capabilities.experimental['claude/channel/permission'] | object | 可选。始终为 {}。声明此通道可以接收权限中继请求。声明后,Claude Code 会将工具审批提示转发到你的通道,以便你远程批准或拒绝。参阅中继权限提示。 |
capabilities.tools | object | 仅限双向。始终为 {}。标准 MCP 工具能力。参阅暴露回复工具。 |
instructions | string | 推荐。添加到 Claude 的系统提示中。告诉 Claude 预期什么事件、<channel> 标签属性的含义、是否需要回复、如果需要使用哪个工具以及传递回哪个属性(如 chat_id)。 |
要创建单向通道,省略 capabilities.tools。本示例展示了设置了通道能力、工具和指令的双向配置:
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
const mcp = new Server(
{ name: 'your-channel', version: '0.0.1' },
{
capabilities: {
experimental: { 'claude/channel': {} }, // 注册通道监听器
tools: {}, // 单向通道省略此项
},
// 添加到 Claude 的系统提示中,以便它知道如何处理你的事件
instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
},
)
要推送事件,调用 mcp.notification() 并使用方法 notifications/claude/channel。参数在下一节中。
通知格式
你的服务器发出 notifications/claude/channel 并带有两个参数:
| 字段 | 类型 | 描述 |
|---|---|---|
content | string | 事件正文。作为 <channel> 标签的正文传递。 |
meta | Record<string, string> | 可选。每个条目成为 <channel> 标签上的属性,用于路由上下文(如聊天 ID、发送者名称或提醒严重级别)。键必须是标识符:仅限字母、数字和下划线。包含连字符或其他字符的键会被静默丢弃。 |
你的服务器通过在 Server 实例上调用 mcp.notification() 来推送事件。本示例推送带有两个 meta 键的 CI 失败提醒:
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' },
},
})
事件以 <channel> 标签的形式到达 Claude 的上下文中。source 属性由你的服务器配置名称自动设置:
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>
通知不会被确认。mcp.notification() 上的 await 在消息写入传输时解析,而非在 Claude 处理它时。如果会话尚未将你的服务器加载为通道,或组织策略阻止了它,事件会被静默丢弃,不会向你的服务器返回错误。
如果你需要传递确认,请在服务器中跟踪事件状态并暴露一个 Claude 可以调用的回复工具来报告状态。
事件排入会话并按顺序处理。如果在 Claude 忙碌时到达多个通知,它们会在下一轮一起传递,Claude 作为一组处理它们。要并发处理独立的事件流,请运行单独的会话。
暴露回复工具
如果你的通道是双向的(如聊天桥接而非提醒转发器),请暴露一个标准 MCP 工具,Claude 可以调用它来发回消息。工具注册本身没有通道特定的内容。回复工具有三个组件:
Server构造函数能力中的tools: {}条目,以便 Claude Code 发现该工具- 定义工具模式并实现发送逻辑的工具处理器
Server构造函数中的instructions字符串,告诉 Claude 何时以及如何调用该工具
要将这些添加到上方的 webhook 接收器:
启用工具发现
在
webhook.ts的Server构造函数中,向能力添加tools: {},以便 Claude Code 知道你的服务器提供工具:capabilities: { experimental: { 'claude/channel': {} }, tools: {}, // 启用工具发现 },注册回复工具
将以下内容添加到
webhook.ts。import放在文件顶部与其他导入一起;两个处理器放在Server构造函数和mcp.connect()之间。这注册了一个 Claude 可以用chat_id和text调用的reply工具:// 在 webhook.ts 顶部添加此导入 import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js' // Claude 在启动时查询此以发现你的服务器提供的工具 mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: 'reply', description: 'Send a message back over this channel', // inputSchema 告诉 Claude 传递什么参数 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 在想要调用工具时调用此处理器 mcp.setRequestHandler(CallToolRequestSchema, async req => { if (req.params.name === 'reply') { const { chat_id, text } = req.params.arguments as { chat_id: string; text: string } // send() 是你的出站:POST 到你的聊天平台,或用于本地 // 测试的下方完整示例中显示的 SSE 广播。 send(`Reply to ${chat_id}: ${text}`) return { content: [{ type: 'text', text: 'sent' }] } } throw new Error(`unknown tool: ${req.params.name}`) })更新 instructions
更新
Server构造函数中的instructions字符串,以便 Claude 知道通过工具路由回复。本示例告诉 Claude 传递入站标签中的chat_id:instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.'
以下是带有双向支持的完整 webhook.ts。出站回复通过 GET /events 使用 Server-Sent Events(SSE)进行流式传输,因此 curl -N localhost:8788/events 可以实时查看它们;入站聊天到达 POST /:
#!/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'
// --- 出站:写入 /events 上的任何 curl -N 监听器 --------------------
// 真正的桥接会 POST 到你的聊天平台。
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, // 不关闭空闲 SSE 流
async fetch(req) {
const url = new URL(req.url)
// GET /events:SSE 流,curl -N 可以实时查看 Claude 的回复
if (req.method === 'GET' && url.pathname === '/events') {
const stream = new ReadableStream({
start(ctrl) {
ctrl.enqueue(': connected\n\n') // 以便 curl 立即显示内容
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:作为通道事件转发给 Claude
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')
},
})
fakechat 服务器展示了带有文件附件和消息编辑的更完整示例。
网关入站消息
未设网关的通道是提示注入的载体。任何能访问你端点的人都可以在 Claude 面前放置文本。监听聊天平台或公共端点的通道在发出任何内容之前需要真正的发送者检查。
在调用 mcp.notification() 之前,根据允许列表检查发送者。本示例丢弃来自不在集合中的发送者的任何消息:
const allowed = new Set(loadAllowlist()) // 来自你的 access.json 或等效文件
// 在你的消息处理器中,发出之前:
if (!allowed.has(message.from.id)) { // 发送者,非房间
return // 静默丢弃
}
await mcp.notification({ ... })
根据发送者身份而非聊天或房间身份进行网关控制:示例中的 message.from.id 而非 message.chat.id。在群聊中,这些是不同的,根据房间进行网关控制会让允许列表群组中的任何人向会话注入消息。
Telegram 和 Discord 通道以相同方式根据发送者允许列表进行网关控制。它们通过配对引导列表:用户给机器人发私信,机器人回复配对码,用户在 Claude Code 会话中批准,其平台 ID 就被添加。参阅任一实现了解完整配对流程。iMessage 通道采用不同方法:它在启动时从消息数据库中检测用户自己的地址并自动放行,其他发送者通过句柄添加。
中继权限提示
权限中继需要 Claude Code v2.1.81 或更高版本。早期版本忽略 claude/channel/permission 能力。
当 Claude 调用需要审批的工具时,本地终端对话框打开,会话等待。双向通道可以选择同时接收相同的提示并在另一台设备上中继给你。两者都保持活跃:你可以在终端或手机上回答,Claude Code 应用先到达的答案并关闭另一个。
中继涵盖工具使用审批(如 Bash、Write 和 Edit)。项目信任和 MCP 服务器同意对话框不中继;它们仅出现在本地终端。
中继工作原理
当权限提示打开时,中继循环有四个步骤:
- Claude Code 生成一个简短的请求 ID 并通知你的服务器
- 你的服务器将提示和 ID 转发到你的聊天应用
- 远程用户回复 yes 或 no 以及该 ID
- 你的入站处理器将回复解析为裁决,Claude Code 仅在 ID 匹配开放请求时应用它
本地终端对话框在此过程中保持打开。如果终端上有人在远程裁决到达之前回答,则应用该答案并丢弃待处理的远程请求。
权限请求字段
来自 Claude Code 的出站通知是 notifications/claude/channel/permission_request。与通道通知一样,传输是标准 MCP 但方法和模式是 Claude Code 扩展。params 对象有四个字符串字段,你的服务器将其格式化为外发提示:
| 字段 | 描述 |
|---|---|
request_id | 从 a-z(不含 l)中抽取的五个小写字母,因此在手机上输入时不会被误读为 1 或 I。在外发提示中包含它,以便在回复中回显。Claude Code 仅接受带有它签发的 ID 的裁决。本地终端对话框不显示此 ID,因此你的出站处理器是获取它的唯一方式。 |
tool_name | Claude 想要使用的工具名称,例如 Bash 或 Write。 |
description | 此特定工具调用功能的人类可读摘要,与本地终端对话框显示的文本相同。对于 Bash 调用,这是 Claude 对命令的描述,或未提供时的命令本身。 |
input_preview | 工具参数的 JSON 字符串,截断到 200 个字符。对于 Bash 这是命令;对于 Write 这是文件路径和内容的前缀。如果你只有一行消息的空间,可以从提示中省略它。你的服务器决定显示什么。 |
你的服务器发回的裁决是 notifications/claude/channel/permission,包含两个字段:request_id 回显上述 ID,behavior 设置为 'allow' 或 'deny'。Allow 允许工具调用继续;deny 拒绝它,与在本地对话中回答 No 相同。两种裁决都不影响未来的调用。
将中继添加到聊天桥接
将权限中继添加到双向通道需要三个组件:
Server构造函数中experimental能力下的claude/channel/permission: {}条目,以便 Claude Code 知道转发提示notifications/claude/channel/permission_request的通知处理器,格式化提示并通过你的平台 API 发送- 入站消息处理器中的检查,识别
yes <id>或no <id>并发出notifications/claude/channel/permission裁决而非将文本转发给 Claude
仅在你的通道验证发送者时才声明此能力,因为任何能通过你的通道回复的人都可以批准或拒绝你会话中的工具使用。
要将这些添加到暴露回复工具中组装的双向聊天桥接:
声明权限能力
在你的
Server构造函数中,在experimental下添加claude/channel/permission: {}与claude/channel并列:capabilities: { experimental: { 'claude/channel': {}, 'claude/channel/permission': {}, // 选择加入权限中继 }, tools: {}, },处理传入请求
在你的
Server构造函数和mcp.connect()之间注册通知处理器。当权限对话框打开时,Claude Code 用四个请求字段调用它。你的处理器为你的平台格式化提示并包含使用 ID 回复的说明:import { z } from 'zod' // setNotificationHandler 通过 z.literal 在 method 字段上路由, // 因此此 schema 既是验证器也是分发键 const PermissionRequestSchema = z.object({ method: z.literal('notifications/claude/channel/permission_request'), params: z.object({ request_id: z.string(), // 五个小写字母,原样包含在你的提示中 tool_name: z.string(), // 例如 "Bash"、"Write" description: z.string(), // 此调用的人类可读摘要 input_preview: z.string(), // 工具参数的 JSON,截断到约 200 字符 }), }) mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => { // send() 是你的出站:POST 到你的聊天平台,或用于本地 // 测试的下方完整示例中显示的 SSE 广播。 send( `Claude wants to run ${params.tool_name}: ${params.description}\n\n` + // 说明中的 ID 是你的入站处理器在步骤 3 中解析的内容 `Reply "yes ${params.request_id}" or "no ${params.request_id}"`, ) })在入站处理器中拦截裁决
你的入站处理器是从你的平台接收消息的循环或回调:与你根据发送者进行网关控制和发出
notifications/claude/channel将聊天转发给 Claude 的地方相同。在聊天转发调用之前添加一个检查,识别裁决格式并发出权限通知。正则表达式匹配 Claude Code 生成的 ID 格式:五个字母,永不为
l。/i标志容忍手机自动更正大写回复;发送前将捕获的 ID 转为小写。// 匹配 "y abcde"、"yes abcde"、"n abcde"、"no abcde" // [a-km-z] 是 Claude Code 使用的 ID 字母表(小写,跳过 'l') // /i 容忍手机自动更正;发送前将捕获转为小写 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 // 先根据发送者进行网关控制 const m = PERMISSION_REPLY_RE.exec(message.text) if (m) { // m[1] 是裁决词,m[2] 是请求 ID // 将裁决通知发回 Claude Code 而非聊天 await mcp.notification({ method: 'notifications/claude/channel/permission', params: { request_id: m[2].toLowerCase(), // 规范化以防自动更正大写 behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny', }, }) return // 作为裁决处理,不要也作为聊天转发 } // 未匹配裁决格式:落入正常的聊天路径 await mcp.notification({ method: 'notifications/claude/channel', params: { content: message.text, meta: { chat_id: String(message.chat.id) } }, }) }
Claude Code 也保持本地终端对话框打开,因此你可以在任一处回答,先到达的答案被应用。远程回复未精确匹配预期格式时以两种方式之一失败,两种情况下对话框都保持打开:
- 不同格式:你的入站处理器的正则表达式匹配失败,因此像
approve it或没有 ID 的yes这样的文本作为普通消息落入 Claude。 - 格式正确但 ID 错误:你的服务器发出裁决,但 Claude Code 找不到具有该 ID 的开放请求并静默丢弃。
完整示例
下方组装的 webhook.ts 结合了本页的所有三个扩展:回复工具、发送者网关和权限中继。如果你从这里开始,你还需要初始演练中的项目设置和 .mcp.json 条目。
为了使两个方向都可以从 curl 测试,HTTP 监听器提供两个路径:
GET /events:保持 SSE 流打开并将每条出站消息作为data:行推送,因此curl -N可以实时查看 Claude 的回复和权限提示到达。POST /:入站侧,与之前相同的处理器,现在在聊天转发分支之前插入了裁决格式检查。
#!/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'
// --- 出站:写入 /events 上的任何 curl -N 监听器 --------------------
// 真正的桥接会 POST 到你的聊天平台。
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)
}
// 发送者允许列表。对于本地演练,我们信任单个 X-Sender
// 头值 "dev";真正的桥接会检查平台的用户 ID。
const allowed = new Set(['dev'])
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // 选择加入权限中继
},
tools: {},
},
instructions:
'Messages arrive as <channel source="webhook" chat_id="...">. ' +
'Reply with the reply tool, passing the chat_id from the tag.',
},
)
// --- 回复工具:Claude 调用此工具发回消息 -------------------
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}`)
})
// --- 权限中继:Claude Code(非 Claude)在对话框打开时调用此处理器
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 在 :8788 上:GET /events 流式传出,POST 路由传入 -------
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, // 不关闭空闲 SSE 流
async fetch(req) {
const url = new URL(req.url)
// GET /events:SSE 流,curl -N 可以实时查看回复和提示
if (req.method === 'GET' && url.pathname === '/events') {
const stream = new ReadableStream({
start(ctrl) {
ctrl.enqueue(': connected\n\n') // 以便 curl 立即显示内容
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' },
})
}
// 其他所有都是入站:先根据发送者进行网关控制
const body = await req.text()
const sender = req.headers.get('X-Sender') ?? ''
if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })
// 在作为聊天处理之前检查裁决格式
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')
}
// 普通聊天:作为通道事件转发给 Claude
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')
},
})
在三个终端中测试裁决路径。第一个是你的 Claude Code 会话,使用开发标志启动以便它生成 webhook.ts:
claude --dangerously-load-development-channels server:webhook
在第二个中,流式传出侧以便你可以实时查看 Claude 的回复和任何权限提示:
curl -N localhost:8788/events
在第三个中,发送一条会让 Claude 尝试运行命令的消息:
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788
本地权限对话框在你的 Claude Code 终端中打开。片刻后提示出现在 /events 流中,包括五字母 ID。从远程侧批准:
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788
本地对话框关闭,工具运行。Claude 的回复通过 reply 工具返回并也出现在流中。
此文件中的三个通道特定部分:
Server构造函数中的能力:claude/channel注册通知监听器,claude/channel/permission选择加入权限中继,tools让 Claude 发现回复工具。- 出站路径:
reply工具处理器是 Claude 调用对话响应的;PermissionRequestSchema通知处理器是 Claude Code 在权限对话框打开时调用的。两者都调用send()通过/events广播,但它们由系统的不同部分触发。 - HTTP 处理器:
GET /events保持 SSE 流打开以便 curl 可以实时查看出站;POST是入站,根据X-Sender头进行网关控制。yes <id>或no <id>正文作为裁决通知发送给 Claude Code,永远不会到达 Claude;其他所有内容作为通道事件转发给 Claude。
打包为插件
要使你的通道可安装和可共享,请将其封装为插件并发布到市场。用户使用 /plugin install 安装,然后使用 --channels plugin:<name>@<marketplace> 为每个会话启用。
发布到你自己市场的通道仍需要 --dangerously-load-development-channels 才能运行,因为它不在批准的允许列表上。默认允许列表是 claude-plugins-official 中的通道插件,由 Anthropic 自行策展。应用内提交表单将插件添加到社区市场,该市场不在通道允许列表上。
如果你正在与 Anthropic 合作伙伴联系人合作,请联系他们协调官方市场上市。在 Team 和 Enterprise 计划上,管理员可以将你的插件包含在组织自己的 allowedChannelPlugins 列表中,该列表替换默认的 Anthropic 允许列表。