رفتن به محتوا

مرجع چنل‌ها

یک چنل، سرورِ MCP‌ای است که رویدادها را به یک نشستِ Claude Code می‌فرستد تا Claude بتواند به اتفاقاتِ بیرونِ ترمینال واکنش نشان دهد.

می‌توانی یک چنلِ یک‌طرفه یا دوطرفه بسازی. چنل‌های یک‌طرفه هشدارها، webhookها یا رویدادهای مانیتورینگ را برای اقدامِ Claude پیش می‌فرستند. چنل‌های دوطرفه مانند پل‌های چت، علاوه بر این یک ابزارِ پاسخ هم در معرض می‌گذارند تا Claude بتواند پیام برگرداند. چنلی که مسیرِ فرستنده‌ی معتمد دارد می‌تواند اختیاراً به بازپخشِ پرامپت‌های دسترسی opt-in کند تا بتوانی استفاده از ابزار را از راه دور تأیید یا رد کنی.

این صفحه این‌ها را پوشش می‌دهد:

برای استفاده از یک چنلِ موجود به‌جای ساختنش، Channels را ببین. Telegram، Discord، iMessage و fakechat در research preview گنجانده شده‌اند.

یک چنل، سرورِ MCP‌ای است که روی همان ماشینِ Claude Code اجرا می‌شود. Claude Code آن را به‌عنوان یک subprocess می‌سازد و از طریقِ stdio با آن ارتباط برقرار می‌کند. سرورِ چنلِ تو پلِ میانِ سیستم‌های بیرونی و نشستِ Claude Code است:

  • پلتفرم‌های چت (Telegram، Discord): پلاگینت محلی اجرا می‌شود و APIِ پلتفرم را برای پیام‌های تازه poll می‌کند. وقتی کسی به باتت DM می‌دهد، پلاگین پیام را دریافت و به Claude پیش‌می‌فرستد. هیچ URLی لازم نیست در معرض گذاشته شود.
  • Webhookها (CI، مانیتورینگ): سرورت روی یک پورتِ HTTPِ محلی گوش می‌دهد. سیستم‌های بیرونی به آن پورت POST می‌کنند و سرورت payload را به Claude می‌فرستد.
نمودارِ معماری که سیستم‌های بیرونی را در حالِ اتصال به سرورِ چنلِ محلیِ تو نشان می‌دهد، که از طریق stdio با Claude Code ارتباط برقرار می‌کند

به چه چیزهایی نیاز داری

Section titled “به چه چیزهایی نیاز داری”

تنها پیش‌نیازِ قطعی، بسته‌ی @modelcontextprotocol/sdk و یک runtimeِ سازگار با Node.js است. Bun، Node و Deno همه کار می‌کنند. پلاگین‌های ازپیش‌ساخته‌ی research preview از Bun استفاده می‌کنند، ولی چنلِ تو مجبور نیست.

سرورت باید:

  1. قابلیتِ claude/channel را اعلام کند تا Claude Code یک شنونده‌ی اعلان ثبت کند
  2. وقتی اتفاقی می‌افتد رویدادهای notifications/claude/channel را منتشر کند
  3. از طریقِ stdio transport متصل شود (Claude Code سرورت را به‌عنوان یک subprocess می‌سازد)

بخش‌های گزینه‌های سرور و قالبِ اعلان هرکدام از این‌ها را با جزئیات پوشش می‌دهند. برای یک راهنمای کامل، مثال: ساختِ یک گیرنده‌ی webhook را ببین.

در طولِ research preview، چنل‌های سفارشی در allowlistِ تأییدشده نیستند. برای تستِ محلی از --dangerously-load-development-channels استفاده کن. برای جزئیات تست در طولِ research preview را ببین.

مثال: ساختِ یک گیرنده‌ی webhook

Section titled “مثال: ساختِ یک گیرنده‌ی webhook”

این راهنما یک سرورِ تک‌فایلی می‌سازد که به درخواست‌های HTTP گوش می‌دهد و آن‌ها را به نشستِ Claude Code تو پیش‌می‌فرستد. در پایان، هر چیزی که بتواند یک HTTP POST بفرستد—مثل یک پایپ‌لاینِ CI، یک هشدارِ مانیتورینگ یا یک دستورِ curl—می‌تواند رویدادها را به Claude بفرستد.

این مثال از Bun به‌عنوان runtime استفاده می‌کند، به‌خاطرِ سرورِ HTTPِ داخلی و پشتیبانیِ TypeScript‌اش. می‌توانی به‌جایش از Node یا Deno استفاده کنی؛ تنها پیش‌نیاز، MCP SDK است.

ساختِ پروژه

یک دایرکتوریِ تازه بساز و MCP SDK را نصب کن:

Terminal window
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk

نوشتنِ سرورِ چنل

فایلی به نامِ webhook.ts بساز. این تمامِ سرورِ چنلِ توست: از طریقِ stdio به Claude Code وصل می‌شود و روی پورتِ ۸۷۸۸ به HTTP POSTها گوش می‌دهد. وقتی درخواستی می‌رسد، بدنه‌ی آن را به‌عنوان یک رویدادِ چنل به Claude می‌فرستد.

webhook.ts
#!/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')
},
})

این فایل سه کار را به ترتیب انجام می‌دهد:

  • پیکربندیِ سرور: سرورِ MCP را با claude/channel در قابلیت‌هایش می‌سازد، که همان چیزی است که به Claude Code می‌گوید این یک چنل است. رشته‌ی instructions به system promptِ Claude می‌رود: به Claude بگو چه رویدادهایی را انتظار بکشد، آیا پاسخ بدهد و در صورتِ لازم چطور پاسخ‌ها را مسیریابی کند.
  • اتصالِ stdio: از طریقِ stdin/stdout به Claude Code وصل می‌شود. این برای هر سرورِ MCP استاندارد است: Claude Code آن را به‌عنوان یک subprocess می‌سازد.
  • شنونده‌ی HTTP: یک وب‌سرورِ محلی روی پورتِ ۸۷۸۸ راه می‌اندازد. بدنه‌ی هر POST از طریقِ mcp.notification() به‌عنوان یک رویدادِ چنل به Claude پیش‌فرستاده می‌شود. مقدارِ content بدنه‌ی رویداد می‌شود و هر ورودیِ meta یک attribute روی تگِ <channel> می‌شود. شنونده به نمونه‌ی mcp دسترسی لازم دارد، پس در همان فرایند اجرا می‌شود. برای یک پروژه‌ی بزرگ‌تر می‌توانی آن را به ماژول‌های جداگانه تقسیم کنی.

ثبتِ سرورت در Claude Code

سرور را به پیکربندیِ MCP‌ات اضافه کن تا Claude Code بداند چطور آن را شروع کند. برای یک .mcp.json در سطحِ پروژه و در همان دایرکتوری، از مسیرِ نسبی استفاده کن. برای پیکربندیِ سطحِ کاربر در ~/.claude.json، از مسیرِ مطلقِ کامل استفاده کن تا سرور از هر پروژه‌ای پیدا شود:

.mcp.json
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}

Claude Code پیکربندیِ MCP‌ات را هنگامِ راه‌اندازی می‌خواند و هر سرور را به‌عنوان یک subprocess می‌سازد.

تستش کن

در طولِ research preview، چنل‌های سفارشی در allowlist نیستند، پس Claude Code را با پرچمِ توسعه شروع کن:

Terminal window
claude --dangerously-load-development-channels server:webhook

اولین باری که در این پروژه نشستی شروع می‌کنی، Claude Code پیش از استفاده از سرورِ تازه از .mcp.json رضایتت را می‌پرسد. دیالوگ گزارش می‌دهد “New MCP server found in this project: webhook”. برای ادامه Use this MCP server را انتخاب کن.

وقتی Claude Code شروع می‌شود، پیکربندیِ MCP‌ات را می‌خواند، webhook.ts را به‌عنوان یک subprocess می‌سازد، و شنونده‌ی HTTP خودکار روی پورتی که پیکربندی کرده‌ای (۸۷۸۸ در این مثال) راه می‌افتد. لازم نیست خودت سرور را اجرا کنی.

یک یادداشتِ کم‌رنگ زیرِ بنرِ راه‌اندازی تأیید می‌کند که چنل ثبت شده: Channels (experimental) messages from server:webhook inject directly in this session · restart without --dangerously-load-development-channels to stop.

اگر “blocked by org policy” دیدی، ادمینِ سازمانت باید اول چنل‌ها را فعال کند.

در یک ترمینالِ جدا، با فرستادنِ یک HTTP POST همراهِ یک پیام به سرورت، یک webhook را شبیه‌سازی کن. این مثال یک هشدارِ شکستِ CI به پورتِ ۸۷۸۸ (یا هر پورتی که پیکربندی کردی) می‌فرستد:

Terminal window
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"

payload در نشستِ Claude Code تو به‌صورت یک تگِ <channel> می‌رسد:

<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” معمولاً یعنی یک خطای وابستگی یا import در فایلِ سرورت؛ لاگِ debug را در ~/.claude/debug/<session-id>.txt برای ردِ stderr بررسی کن.
  • curl با “connection refused” شکست می‌خورد: یا پورت هنوز bind نشده یا فرایندی کهنه از اجرای قبلی آن را نگه داشته. lsof -i :<port> نشان می‌دهد چه چیزی گوش می‌دهد؛ پیش از ری‌استارتِ نشستت فرایندِ کهنه را kill کن.

سرورِ fakechat این الگو را با یک UIِ وب، پیوست‌های فایل و یک ابزارِ پاسخ برای چتِ دوطرفه گسترش می‌دهد.

در طولِ research preview، هر چنل باید در allowlistِ تأییدشده باشد تا ثبت شود. پرچمِ توسعه، allowlist را برای ورودی‌های مشخص پس از یک پرامپتِ تأیید دور می‌زند. این مثال هر دو نوعِ ورودی را نشان می‌دهد:

Terminal window
# 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

این دورزدن per-entry است. ترکیبِ این پرچم با --channels دورزدن را به ورودی‌های --channels گسترش نمی‌دهد. در طولِ research preview، allowlistِ تأییدشده به‌دستِ Anthropic سرپرستی می‌شود، پس چنلت تا وقتی می‌سازی و تست می‌کنی روی پرچمِ توسعه می‌ماند.

یک چنل این گزینه‌ها را در سازنده‌ی 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.toolsobjectفقط دوطرفه. همیشه {}. قابلیتِ ابزارِ MCP استاندارد. در معرض گذاشتنِ ابزارِ پاسخ را ببین.
instructionsstringتوصیه‌شده. به system promptِ Claude افزوده می‌شود. به Claude بگو چه رویدادهایی را انتظار بکشد، attributeهای تگِ <channel> چه معنایی دارند، آیا پاسخ بدهد و اگر بله از کدام ابزار استفاده کند و کدام attribute را برگرداند (مثلِ chat_id).

برای ساختِ یک چنلِ یک‌طرفه، capabilities.tools را حذف کن. این مثال یک راه‌اندازیِ دوطرفه را با قابلیتِ چنل، tools و instructions نشان می‌دهد:

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.',
},
)

برای فرستادنِ یک رویداد، mcp.notification() را با متدِ notifications/claude/channel فراخوانی کن. paramها در بخشِ بعدی‌اند.

سرورت notifications/claude/channel را با دو param منتشر می‌کند:

فیلدنوعتوضیح
contentstringبدنه‌ی رویداد. به‌صورت بدنه‌ی تگِ <channel> تحویل می‌شود.
metaRecord<string, string>اختیاری. هر ورودی یک attribute روی تگِ <channel> می‌شود برای کانتکستِ مسیریابی مثلِ chat ID، نامِ فرستنده یا شدتِ هشدار. کلیدها باید identifier باشند: فقط حروف، ارقام و آندرلاین. کلیدهای دارای خط‌تیره یا کاراکترهای دیگر بی‌صدا حذف می‌شوند.

سرورت رویدادها را با فراخوانیِ mcp.notification() روی نمونه‌ی Server می‌فرستد. این مثال یک هشدارِ شکستِ CI را با دو کلیدِ meta می‌فرستد:

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، پیچیده در یک تگِ <channel> می‌رسد. attributeِ source خودکار از نامِ پیکربندی‌شده‌ی سرورت تنظیم می‌شود:

<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>

اعلان‌ها acknowledge نمی‌شوند. await روی mcp.notification() وقتی resolve می‌شود که پیام روی transport نوشته شود، نه وقتی Claude آن را پردازش کرده باشد. اگر نشست سرورت را به‌عنوان چنل بارگذاری نکرده باشد، یا سیاستِ سازمانی آن را مسدود کند، رویدادها بی‌صدا حذف می‌شوند و هیچ خطایی به سرورت برنمی‌گردد.

اگر به تأییدِ تحویل نیاز داری، وضعیتِ رویداد را در سرورت ردگیری کن و یک ابزارِ پاسخ در معرض بگذار که Claude بتواند برای گزارشِ وضعیت فراخوانی‌اش کند.

رویدادها در نشست صف می‌شوند و به ترتیب پردازش می‌شوند. اگر چند اعلان هنگامی که Claude مشغول است برسند، در نوبتِ بعدی با هم تحویل می‌شوند و Claude آن‌ها را به‌صورت یک گروه رسیدگی می‌کند. برای پردازشِ هم‌زمانِ جریان‌های مستقلِ رویداد، نشست‌های جداگانه اجرا کن.

در معرض گذاشتنِ ابزارِ پاسخ

Section titled “در معرض گذاشتنِ ابزارِ پاسخ”

اگر چنلت دوطرفه است—مثلِ یک پلِ چت و نه یک پیش‌فرستِ هشدار—یک ابزارِ MCP استاندارد در معرض بگذار که Claude بتواند برای فرستادنِ پیام به عقب فراخوانی‌اش کند. هیچ‌چیزِ ثبتِ این ابزار مختصِ چنل نیست. یک ابزارِ پاسخ سه جزء دارد:

  1. یک ورودیِ tools: {} در قابلیت‌های سازنده‌ی Server‌ات تا Claude Code ابزار را کشف کند
  2. handlerهای ابزار که schemaِ ابزار را تعریف و منطقِ فرستادن را پیاده می‌کنند
  3. یک رشته‌ی instructions در سازنده‌ی Server‌ات که به Claude می‌گوید کِی و چطور ابزار را فراخوانی کند

برای افزودنِ این‌ها به گیرنده‌ی webhookِ بالا:

فعال‌کردنِ کشفِ ابزار

در سازنده‌ی Server‌ات در webhook.ts، tools: {} را به قابلیت‌ها اضافه کن تا Claude Code بداند سرورت ابزار ارائه می‌دهد:

capabilities: {
experimental: { 'claude/channel': {} },
tools: {}, // enables tool discovery
},

ثبتِ ابزارِ پاسخ

موارد زیر را به webhook.ts اضافه کن. import بالای فایل کنارِ بقیه‌ی importهایت می‌رود؛ دو handler بینِ سازنده‌ی Server و mcp.connect() می‌روند. این یک ابزارِ reply ثبت می‌کند که Claude می‌تواند با یک chat_id و text فراخوانی‌اش کند:

// 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}`)
})

به‌روزرسانیِ instructions

رشته‌ی instructions در سازنده‌ی Server‌ات را به‌روز کن تا 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 / می‌رسد:

Full webhook.ts with reply tool
#!/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')
},
})

سرورِ fakechat مثالی کامل‌تر با پیوست‌های فایل و ویرایشِ پیام نشان می‌دهد.

فیلترکردنِ پیام‌های ورودی

Section titled “فیلترکردنِ پیام‌های ورودی”

یک چنلِ بدونِ فیلتر یک بردارِ prompt injection است. هرکس که بتواند به endpointت برسد می‌تواند متنی جلوی Claude بگذارد. چنلی که به یک پلتفرمِ چت یا یک endpointِ عمومی گوش می‌دهد، پیش از انتشارِ هر چیزی به یک بررسیِ واقعیِ فرستنده نیاز دارد.

پیش از فراخوانیِ mcp.notification() فرستنده را در برابرِ یک allowlist بررسی کن. این مثال هر پیامی را از فرستنده‌ای که در set نیست حذف می‌کند:

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({ ... })

فیلتر را روی هویتِ فرستنده بگذار، نه هویتِ چت یا اتاق: message.from.id در مثال، نه message.chat.id. در چت‌های گروهی این‌ها فرق می‌کنند، و فیلترکردن روی اتاق به هرکس در یک گروهِ allowlist‌شده اجازه می‌دهد پیام به نشست تزریق کند.

چنل‌های Telegram و Discord به همین شکل روی allowlistِ فرستنده فیلتر می‌کنند. آن‌ها فهرست را با pairing راه‌اندازی می‌کنند: کاربر به بات DM می‌دهد، بات با یک کدِ pairing پاسخ می‌دهد، کاربر آن را در نشستِ Claude Code‌اش تأیید می‌کند، و شناسه‌ی پلتفرمش افزوده می‌شود. برای جریانِ کاملِ pairing هرکدام از پیاده‌سازی‌ها را ببین. چنلِ iMessage رویکردِ متفاوتی دارد: نشانی‌های خودِ کاربر را هنگامِ راه‌اندازی از پایگاه‌داده‌ی Messages تشخیص می‌دهد و خودکار اجازه‌ی عبور می‌دهد، و فرستندگانِ دیگر با handle افزوده می‌شوند.

بازپخشِ پرامپت‌های دسترسی

Section titled “بازپخشِ پرامپت‌های دسترسی”

وقتی Claude ابزاری را فراخوانی می‌کند که به تأیید نیاز دارد، دیالوگِ ترمینالِ محلی باز می‌شود و نشست منتظر می‌ماند. یک چنلِ دوطرفه می‌تواند opt-in کند تا همان پرامپت را به‌موازات دریافت و آن را روی دستگاهِ دیگری به تو بازپخش کند. هر دو زنده می‌مانند: می‌توانی در ترمینال یا روی گوشی‌ات پاسخ بدهی، و Claude Code هر پاسخی که زودتر برسد را اعمال و دیگری را می‌بندد.

بازپخش، تأییدهای استفاده از ابزار مثلِ Bash، Write و Edit را پوشش می‌دهد. دیالوگ‌های اعتمادِ پروژه و رضایتِ سرورِ MCP بازپخش نمی‌شوند؛ آن‌ها فقط در ترمینالِ محلی ظاهر می‌شوند.

بازپخش چطور کار می‌کند

Section titled “بازپخش چطور کار می‌کند”

وقتی یک پرامپتِ دسترسی باز می‌شود، حلقه‌ی بازپخش چهار گام دارد:

  1. Claude Code یک request ID کوتاه می‌سازد و به سرورت اعلان می‌دهد
  2. سرورت پرامپت و ID را به اپِ چتت پیش‌می‌فرستد
  3. کاربرِ راه‌دور با بله یا خیر و همان ID پاسخ می‌دهد
  4. handlerِ ورودی‌ات پاسخ را به یک حکم تجزیه می‌کند، و Claude Code فقط اگر ID با یک درخواستِ باز تطبیق کند آن را اعمال می‌کند

دیالوگِ ترمینالِ محلی در طولِ همه‌ی این‌ها باز می‌ماند. اگر کسی پشتِ ترمینال پیش از رسیدنِ حکمِ راه‌دور پاسخ بدهد، همان پاسخ اعمال و درخواستِ راه‌دورِ معلق حذف می‌شود.

نمودارِ توالی: Claude Code یک اعلانِ permission_request به سرورِ چنل می‌فرستد، سرور پرامپت را قالب‌بندی و به اپِ چت می‌فرستد، انسان با یک حکم پاسخ می‌دهد، و سرور آن پاسخ را به یک اعلانِ دسترسی به Claude Code تجزیه می‌کند

فیلدهای درخواستِ دسترسی

Section titled “فیلدهای درخواستِ دسترسی”

اعلانِ خروجی از Claude Code، notifications/claude/channel/permission_request است. مثلِ اعلانِ چنل، transport همان MCP استاندارد است ولی متد و schema افزوده‌های Claude Code‌اند. شیءِ params چهار فیلدِ رشته‌ای دارد که سرورت در پرامپتِ خروجی قالب‌بندی می‌کند:

فیلدتوضیح
request_idپنج حرفِ کوچک از a-z بدونِ l، تا هیچ‌وقت هنگامِ تایپ روی گوشی به‌جای 1 یا I خوانده نشود. آن را در پرامپتِ خروجی‌ات بگنجان تا بتواند در پاسخ بازتاب شود. Claude Code فقط حکمی را می‌پذیرد که IDِ صادرشده‌ی خودش را حمل کند. دیالوگِ ترمینالِ محلی این ID را نشان نمی‌دهد، پس handlerِ خروجی‌ات تنها راهِ دانستنِ آن است.
tool_nameنامِ ابزاری که Claude می‌خواهد استفاده کند، مثلاً Bash یا Write.
descriptionخلاصه‌ی قابلِ‌خواندنِ آدم از کاری که این فراخوانیِ ابزار انجام می‌دهد، همان متنی که دیالوگِ ترمینالِ محلی نشان می‌دهد. برای یک فراخوانیِ Bash این توضیحِ Claude از دستور است، یا خودِ دستور اگر هیچ توضیحی داده نشده باشد.
input_previewآرگومان‌های ابزار به‌صورت یک رشته‌ی JSON، بریده‌شده به ۲۰۰ کاراکتر. برای Bash این دستور است؛ برای Write مسیرِ فایل و یک پیشوند از محتواست. اگر فقط جای یک پیامِ یک‌خطی داری از پرامپتت حذفش کن. سرورت تصمیم می‌گیرد چه نشان دهد.

حکمی که سرورت برمی‌گرداند notifications/claude/channel/permission با دو فیلد است: request_id که IDِ بالا را بازتاب می‌دهد، و behavior که روی 'allow' یا 'deny' تنظیم می‌شود. Allow اجازه می‌دهد فراخوانیِ ابزار پیش برود؛ deny ردش می‌کند، همان‌طور که در دیالوگِ محلی No بزنی. هیچ‌کدام از این حکم‌ها فراخوانی‌های آینده را تحتِ تأثیر نمی‌گذارند.

افزودنِ بازپخش به یک پلِ چت

Section titled “افزودنِ بازپخش به یک پلِ چت”

افزودنِ بازپخشِ دسترسی به یک چنلِ دوطرفه سه جزء می‌خواهد:

  1. یک ورودیِ claude/channel/permission: {} زیرِ قابلیت‌های experimental در سازنده‌ی Server‌ات تا Claude Code بداند پرامپت‌ها را پیش بفرستد
  2. یک notification handler برای notifications/claude/channel/permission_request که پرامپت را قالب‌بندی و از طریقِ APIِ پلتفرمت می‌فرستد
  3. یک بررسی در handlerِ پیامِ ورودی‌ات که yes <id> یا no <id> را تشخیص می‌دهد و به‌جای پیش‌فرستادنِ متن به Claude، یک حکمِ notifications/claude/channel/permission منتشر می‌کند

قابلیت را فقط اگر چنلت فرستنده را احرازهویت می‌کند اعلام کن، چون هرکس بتواند از طریقِ چنلت پاسخ بدهد می‌تواند استفاده از ابزار را در نشستت تأیید یا رد کند.

برای افزودنِ این‌ها به یک پلِ چتِ دوطرفه مثلِ آنکه در در معرض گذاشتنِ ابزارِ پاسخ سوار شد:

اعلامِ قابلیتِ دسترسی

در سازنده‌ی Server‌ات، claude/channel/permission: {} را کنارِ claude/channel زیرِ experimental اضافه کن:

capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in to permission relay
},
tools: {},
},

رسیدگی به درخواستِ ورودی

یک notification handler بینِ سازنده‌ی Server‌ات و mcp.connect() ثبت کن. Claude Code وقتی یک دیالوگِ دسترسی باز می‌شود آن را با چهار فیلدِ درخواست فراخوانی می‌کند. handlerت پرامپت را برای پلتفرمت قالب‌بندی می‌کند و دستورالعملِ پاسخ‌دادن با ID را می‌گنجاند:

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}"`,
)
})

رهگیریِ حکم در handlerِ ورودی‌ات

handlerِ ورودی‌ات همان حلقه یا callback است که پیام‌ها را از پلتفرمت دریافت می‌کند: همان جایی که روی فرستنده فیلتر می‌کنی و notifications/claude/channel را برای پیش‌فرستادنِ چت به Claude منتشر می‌کنی. پیش از فراخوانیِ پیش‌فرستِ چت، یک بررسی اضافه کن که قالبِ حکم را تشخیص می‌دهد و به‌جایش اعلانِ دسترسی را منتشر می‌کند.

regex با قالبِ IDی که Claude Code می‌سازد تطبیق می‌کند: پنج حرف، هیچ‌وقت l. پرچمِ /i تحملِ بزرگ‌کردنِ پاسخ به‌دستِ autocorrectِ گوشی را دارد؛ پیش از فرستادنِ IDِ گرفته‌شده آن را کوچک کن.

// 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) } },
})
}

Claude Code همچنین دیالوگِ ترمینالِ محلی را باز نگه می‌دارد، پس می‌توانی در هر جا پاسخ بدهی، و اولین پاسخی که برسد اعمال می‌شود. پاسخی از راه دور که دقیقاً با قالبِ موردِانتظار تطبیق نکند به یکی از دو شکل شکست می‌خورد، و در هر دو حالت دیالوگ باز می‌ماند:

  • قالبِ متفاوت: regexِ handlerِ ورودی‌ات تطبیق نمی‌کند، پس متنی مثلِ approve it یا yes بدونِ ID به‌عنوان یک پیامِ معمولی به Claude می‌رسد.
  • قالبِ درست، IDِ اشتباه: سرورت یک حکم منتشر می‌کند، ولی Claude Code هیچ درخواستِ بازی با آن ID نمی‌یابد و بی‌صدا حذفش می‌کند.

webhook.tsِ مونتاژشده‌ی زیر هر سه افزونه‌ی این صفحه را با هم می‌آورد: ابزارِ پاسخ، فیلترِ فرستنده و بازپخشِ دسترسی. اگر از همین‌جا شروع می‌کنی، به راه‌اندازیِ پروژه و ورودیِ .mcp.json از راهنمای آغازین هم نیاز داری.

برای اینکه هر دو جهت از curl قابلِ‌تست باشند، شنونده‌ی HTTP دو مسیر را سرو می‌کند:

  • GET /events: یک جریانِ SSE را باز نگه می‌دارد و هر پیامِ خروجی را به‌صورت یک خطِ data: می‌فرستد، پس curl -N می‌تواند رسیدنِ زنده‌ی پاسخ‌ها و پرامپت‌های دسترسیِ Claude را تماشا کند.
  • POST /: سمتِ ورودی، همان handlerِ قبلی، حالا با بررسیِ قالبِ حکم پیش از شاخه‌ی پیش‌فرستِ چت.
Full webhook.ts with permission relay
#!/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')
},
})

مسیرِ حکم را در سه ترمینال تست کن. اولی نشستِ Claude Code توست، که با پرچمِ توسعه شروع شده تا webhook.ts را بسازد:

Terminal window
claude --dangerously-load-development-channels server:webhook

در دومی، سمتِ خروجی را جریان بده تا بتوانی پاسخ‌های Claude و هر پرامپتِ دسترسی را هنگامِ شلیک‌شدن ببینی:

Terminal window
curl -N localhost:8788/events

در سومی، پیامی بفرست که Claude را وادار به اجرای یک دستور کند:

Terminal window
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788

فهرست‌کردنِ فایل‌ها فقط‌خواندنی است، پس Claude بدونِ تأیید اجرایش می‌کند. دیالوگِ دسترسی وقتی باز می‌شود که Claude ابزارِ reply را فراخوانی می‌کند تا پاسخش را برگرداند. دیالوگِ محلی در ترمینالِ Claude Code تو باز می‌شود، و لحظه‌ای بعد پرامپتِ mcp__webhook__reply در جریانِ /events ظاهر می‌شود، شاملِ IDِ پنج‌حرفی. آن را از سمتِ راه‌دور تأیید کن:

Terminal window
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788

دیالوگِ محلی بسته می‌شود، ابزارِ reply اجرا می‌شود، و پاسخِ Claude در جریان فرود می‌آید.

سه قطعه‌ی مختصِ چنل در این فایل:

  • Capabilities در سازنده‌ی Server: claude/channel شنونده‌ی اعلان را ثبت می‌کند، claude/channel/permission به بازپخشِ دسترسی opt-in می‌کند، tools به Claude اجازه می‌دهد ابزارِ پاسخ را کشف کند.
  • مسیرهای خروجی: handlerِ ابزارِ reply همان چیزی است که Claude برای پاسخ‌های گفت‌وگویی فراخوانی می‌کند؛ notification handlerِ PermissionRequestSchema همان چیزی است که Claude Code وقتی یک دیالوگِ دسترسی باز می‌شود فراخوانی می‌کند. هر دو send() را برای پخش روی /events فراخوانی می‌کنند، ولی با بخش‌های متفاوتِ سیستم تریگر می‌شوند.
  • HTTP handler: GET /events یک جریانِ SSE را باز نگه می‌دارد تا curl بتواند خروجی را زنده تماشا کند؛ POST ورودی است، فیلترشده روی هدرِ X-Sender. بدنه‌ی yes <id> یا no <id> به‌صورت یک اعلانِ حکم به Claude Code می‌رود و هیچ‌وقت به Claude نمی‌رسد؛ هر چیزِ دیگری به‌عنوان یک رویدادِ چنل به Claude پیش‌فرستاده می‌شود.

بسته‌بندی به‌صورتِ یک پلاگین

Section titled “بسته‌بندی به‌صورتِ یک پلاگین”

برای اینکه چنلت قابلِ‌نصب و قابلِ‌اشتراک شود، آن را در یک plugin بپیچ و در یک marketplace منتشرش کن. کاربران با /plugin install نصبش می‌کنند، بعد در هر نشست با --channels plugin:<name>@<marketplace> فعالش می‌کنند.

چنلی که در marketplaceِ خودت منتشر شده، هنوز برای اجرا به --dangerously-load-development-channels نیاز دارد، چون در allowlistِ تأییدشده نیست. allowlistِ پیش‌فرض، همان پلاگین‌های چنل در claude-plugins-official است که Anthropic به صلاحدیدِ خودش سرپرستی می‌کند. فرم‌های ارسالِ درون‌برنامه‌ای پلاگین‌ها را به community marketplace اضافه می‌کنند، که در allowlistِ چنل نیست.

اگر با یک نماینده‌ی شریکِ Anthropic کار می‌کنی، برای هماهنگیِ یک listing در marketplaceِ رسمی با او تماس بگیر. در پلن‌های Team و Enterprise، یک ادمین می‌تواند به‌جایش پلاگینت را در فهرستِ allowedChannelPlugins خودِ سازمان بگنجاند، که جایگزینِ allowlistِ پیش‌فرضِ Anthropic می‌شود.

  • Channels برای نصب و استفاده از Telegram، Discord، iMessage یا دموی fakechat، و برای فعال‌کردنِ چنل‌ها برای یک سازمانِ Team یا Enterprise
  • پیاده‌سازی‌های کارای چنل برای کدِ کاملِ سرور با جریان‌های pairing، ابزارهای پاسخ و پیوست‌های فایل
  • MCP برای پروتکلِ زیرینی که سرورهای چنل پیاده‌اش می‌کنند
  • Plugins برای بسته‌بندیِ چنلت تا کاربران بتوانند با /plugin install نصبش کنند