תיעוד 162
אסמכתת ערוצים
בנו שרת MCP שדוחף webhooks, התראות והודעות צ'אט לתוך סשן של Claude Code. אסמכתא לחוזה הערוץ: הצהרת יכולת, אירועי התראה, כלי תשובה, סינון שולחים, והעברת בקשות הרשאה.
[!NOTE]
ערוצים נמצאים בתצוגה מקדימה למחקר. ארגוני Team ו-Enterprise חייבים להפעיל אותם במפורש.
ערוץ הוא שרת MCP שדוחף אירועים לתוך סשן של Claude Code, כדי ש-Claude יוכל להגיב לדברים שקורים מחוץ לטרמינל.
אפשר לבנות ערוץ חד כיווני או דו כיווני. ערוצים חד כיווניים מעבירים התראות, webhooks או אירועי ניטור, כדי ש-Claude יפעל לפיהם. ערוצים דו כיווניים, כמו גשרי צ'אט, גם חושפים כלי תשובה כדי ש-Claude יוכל לשלוח הודעות בחזרה. ערוץ עם נתיב שולח מהימן יכול גם להצטרף להעברת בקשות הרשאה, כדי לאשר או לדחות שימוש בכלים מרחוק.
הדף הזה מכסה:
- סקירה: איך ערוצים עובדים
- מה צריך: דרישות וצעדים כלליים
- דוגמה: בניית מקלט webhook: הדרכה מינימלית לערוץ חד כיווני
- אפשרויות השרת: שדות הבנאי
- מבנה ההתראה: מטען האירוע והתנהגות המסירה
- חשיפת כלי תשובה: לאפשר ל-Claude לשלוח הודעות בחזרה
- סינון הודעות נכנסות: בדיקות שולח כדי למנוע הזרקת הנחיות
- העברת בקשות הרשאה: העברת בקשות אישור לכלי אל ערוצים מרוחקים
כדי להשתמש בערוץ קיים במקום לבנות אחד, ראו Channels. 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 transport
הסעיפים אפשרויות השרת ומבנה ההתראה מכסים כל אחד מאלה בפירוט. ראו דוגמה: בניית מקלט webhook להדרכה מלאה.
במהלך התצוגה המקדימה למחקר, ערוצים מותאמים אינם ברשימת ההיתר המאושרת. השתמשו ב---dangerously-load-development-channels כדי לבדוק מקומית. ראו בדיקה במהלך התצוגה המקדימה למחקר לפרטים.
#דוגמה: בניית מקלט webhook
ההדרכה הזו בונה שרת בקובץ יחיד שמאזין לבקשות HTTP ומעביר אותן לתוך סשן Claude Code. בסוף, כל דבר שיכול לשלוח HTTP POST, כמו צינור CI, התראת ניטור או פקודת curl, יכול לדחוף אירועים ל-Claude.
הדוגמה הזו משתמשת ב-Bun כסביבת ריצה, בזכות שרת HTTP מובנה ותמיכה ב-TypeScript. אפשר להשתמש ב-Node או ב-Deno במקום. הדרישה היחידה היא MCP SDK.
1. צרו את הפרויקט
דוגמאות העברת ההרשאות מייבאות את zod ישירות, ולכן הוא מותקן יחד עם MCP SDK. צרו ספרייה חדשה והתקינו את שניהם:
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk zod2. כתבו את שרת הערוץ
צרו קובץ בשם webhook.ts. זה כל שרת הערוץ: הוא מתחבר ל-Claude Code דרך stdio, ומאזין לבקשות HTTP POST בפורט 8788. כשמגיעה בקשה, הוא דוחף את הגוף ל-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': {} } },
// Claude Code delivers this to Claude as context when the server connects, 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')
},
})הקובץ מגדיר את השרת, מתחבר דרך stdio, ומתחיל מאזין HTTP, בסדר הזה:
- הגדרת השרת: יוצרת את שרת MCP עם
claude/channelביכולות שלו, וזה מה שאומר ל-Claude Code שזה ערוץ. Claude Code מעביר את מחרוזתinstructionsל-Claude כהקשר כשהשרת מתחבר: ספרו ל-Claude אילו אירועים לצפות להם, אם להשיב, ואיך לנתב תשובות אם צריך. - חיבור stdio: מתחבר ל-Claude Code דרך stdin/stdout. זה סטנדרטי לכל שרת MCP.
- מאזין HTTP: מפעיל שרת אינטרנט מקומי בפורט 8788. כל גוף POST מועבר ל-Claude כאירוע ערוץ דרך
mcp.notification(). ה-contentהופך לגוף האירוע, וכל רשומתmetaהופכת למאפיין בתג<channel>. המאזין צריך גישה למופעmcp, ולכן הוא רץ באותו תהליך. בפרויקט גדול יותר אפשר לפצל את זה למודולים נפרדים.
3. רשמו את השרת אצל Claude Code
הוסיפו את השרת להגדרת MCP כדי ש-Claude Code ידע איך להפעיל אותו. עבור .mcp.json ברמת הפרויקט באותה ספרייה, השתמשו בנתיב יחסי. עבור הגדרה ברמת המשתמש ב-~/.claude.json, השתמשו בנתיב המוחלט המלא כדי שאפשר יהיה למצוא את השרת מכל פרויקט:
.mcp.json:
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}Claude Code קורא את הגדרת MCP בהפעלה ומפעיל כל שרת כתהליך בן.
4. בדקו את זה
במהלך התצוגה המקדימה למחקר, ערוצים מותאמים אינם ברשימת ההיתר, אז הפעילו את Claude Code עם דגל הפיתוח:
claude --dangerously-load-development-channels server:webhookClaude Code מציג קודם תיבת אזהרה במסך מלא עם רשימת ערוצי הפיתוח שאתם טוענים. בחרו I am using this for local development כדי להמשיך, או Exit כדי לצאת.
בפעם הראשונה שמתחילים סשן בפרויקט הזה, Claude Code גם מבקש הסכמה לפני שימוש בשרת החדש מ-.mcp.json. תיבת הדו שיח מדווחת "New MCP server found in this project: webhook". בחרו Use this MCP server כדי להמשיך.
אחרי האישור, Claude Code מפעיל את webhook.ts כתהליך בן, ומאזין ה-HTTP מתחיל אוטומטית בפורט שהגדרתם, 8788 בדוגמה הזו. אין צורך להריץ את השרת בעצמכם.
הודעה עמומה מתחת לבאנר ההפעלה מאשרת שהערוץ נרשם: Channels (experimental) messages from server:webhook inject directly in this session · restart without --dangerously-load-development-channels to stop.
אם מופיע "blocked by org policy", מנהל הארגון צריך להפעיל ערוצים קודם.
בטרמינל נפרד, דמו webhook על ידי שליחת HTTP POST עם הודעה לשרת. הדוגמה הזו שולחת התראת כשל CI לפורט 8788 (או לכל פורט שהגדרתם):
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"המטען מגיע להקשר של Claude כתג <channel>:
<channel source="webhook" path="/" method="POST">
build failed on main: https://ci.example.com/run/1234
</channel>הטרמינל מציג את האירוע כסיכום בשורה אחת, ← webhook: build failed on main: https://ci.example.com/run/1234, במקום התג הגולמי. אחר כך תראו ש-Claude מתחיל להגיב: קורא קבצים, מריץ פקודות, או כל מה שההודעה דורשת. זה ערוץ חד כיווני, אז Claude פועל בסשן שלכם אבל לא שולח שום דבר בחזרה דרך ה-webhook. כדי להוסיף תשובות, ראו חשיפת כלי תשובה.
אם האירוע לא מגיע, האבחון תלוי במה ש-curl החזיר:
curlמצליח אבל שום דבר לא מגיע ל-Claude: הריצו/mcpבסשן כדי לבדוק את מצב השרת. מצבfailedבדרך כלל אומר שגיאת תלות או ייבוא בקובץ השרת. כדי לראות את מעקב stderr, הפעילו מחדש עםclaude --debug --dangerously-load-development-channels server:webhookובדקו את יומן הדיבוג ב-~/.claude/debug/<session-id>.txt.curlנכשל עם "connection refused": הפורט עדיין לא קשור, או שתהליך ישן מהרצה קודמת מחזיק אותו.lsof -i :<port>מראה מה מאזין. בצעוkillלתהליך הישן לפני הפעלה מחדש של הסשן.
שרת fakechat מרחיב את הדפוס הזה עם ממשק אינטרנט, קבצים מצורפים, וכלי תשובה לצ'אט דו כיווני.
#בדיקה במהלך התצוגה המקדימה למחקר
במהלך התצוגה המקדימה למחקר, כל ערוץ חייב להיות ברשימת ההיתר המאושרת כדי להירשם. דגל הפיתוח עוקף את רשימת ההיתר עבור רשומות מסוימות אחרי בקשת אישור. הדוגמה הזו מראה את שני סוגי הרשומות:
# 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הריצו את דגל הפיתוח בסשן אינטראקטיבי, שבו Claude Code יכול להציג את בקשת האישור. אם מעבירים אותו במצב לא אינטראקטיבי עם -p או דרך Agent SDK, Claude Code מתעלם מהדגל והערוץ לא נרשם.
העקיפה היא לפי רשומה. שילוב הדגל הזה עם --channels לא מרחיב את העקיפה לרשומות של --channels. במהלך התצוגה המקדימה למחקר, הערוץ שלכם אינו ברשימת ההיתר המאושרת, ולכן הוא נשאר על דגל הפיתוח בזמן הבנייה והבדיקה.
[!NOTE]
הדגל הזה מדלג רק על רשימת ההיתר. מדיניות הארגון
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 או false | אופציונלי. הגדירו ל-{} כדי להצהיר שהערוץ הזה יכול לקבל בקשות להעברת הרשאה. כשמוצהר, Claude Code מעביר בקשות אישור לכלי אל הערוץ, כדי שאפשר יהיה לאשר או לדחות אותן מרחוק. כדי לא להצטרף, השמיטו את המפתח או הגדירו אותו ל-false. לפני v2.1.234, Claude Code התייחס ל-false כמוצהר. ראו העברת בקשות הרשאה. |
capabilities.tools | object | רק לדו כיווני. תמיד {}. יכולת כלי MCP סטנדרטית. ראו חשיפת כלי תשובה. |
instructions | string | מומלץ. Claude Code מעביר את זה ל-Claude כהקשר כשהשרת מתחבר. ספרו ל-Claude אילו אירועים לצפות להם, מה המשמעות של מאפייני תג <channel>, אם להשיב, ואם כן באיזה כלי להשתמש ואיזה מאפיין להחזיר (כמו chat_id). |
כדי ליצור ערוץ חד כיווני, השמיטו את capabilities.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
},
// Claude Code delivers this to Claude as context when the server connects, so it knows how to handle your events
instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
},
)#מבנה ההתראה
השרת פולט notifications/claude/channel עם שני פרמטרים:
| שדה | סוג | תיאור |
|---|---|---|
content | string | גוף האירוע. נמסר כגוף של תג <channel>. |
meta | Record<string, string> | אופציונלי. כל רשומה הופכת למאפיין על תג <channel> עבור הקשר ניתוב כמו מזהה צ'אט, שם שולח, או חומרת התראה. מפתחות חייבים להיות מזהים: אותיות, ספרות וקווים תחתונים בלבד. מפתחות שמכילים מקפים או תווים אחרים מושמטים בשקט. |
השרת דוחף אירועים על ידי קריאה ל-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>. המאפיין source נקבע אוטומטית לפי השם המוגדר של השרת:
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>Claude Code לא מאשר קבלה של התראות. ה-await על mcp.notification() מסתיים כשההודעה נכתבת לטרנספורט, לא כש-Claude עיבד אותה. אם הסשן לא טען את השרת כערוץ, או שמדיניות הארגון חוסמת אותו, Claude Code משמיט את האירועים בשקט ולא מחזיר שגיאה לשרת.
אם צריך אישור מסירה, עקבו אחרי מצב האירוע בשרת וחשפו כלי תשובה ש-Claude יכול לקרוא לו כדי לדווח סטטוס בחזרה.
אירועים נכנסים לתור בסשן ומעובדים לפי הסדר. אם כמה התראות מגיעות בזמן ש-Claude עסוק, הן נמסרות יחד בתור הבא ו-Claude מטפל בהן כקבוצה. כדי לעבד זרמי אירועים בלתי תלויים במקביל, הריצו סשנים נפרדים.
#חשיפת כלי תשובה
אם הערוץ שלך דו כיווני, כמו גשר צ'אט ולא כמו מעביר התראות, חשוף כלי MCP סטנדרטי ש Claude יכול לקרוא לו כדי לשלוח הודעות בחזרה. אין ברישום הכלי שום דבר ייחודי לערוץ. לכלי תשובה יש שלושה רכיבים:
- רשומת
tools: {}ביכולות של בנאיServerשלך כדי ש Claude Code יגלה את הכלי - מטפלים לכלי שמגדירים את הסכמה של הכלי ומממשים את לוגיקת השליחה
- מחרוזת
instructionsבבנאיServerשלך שאומרת ל Claude מתי ואיך לקרוא לכלי
כדי להוסיף אותם למקלט ה webhook שלמעלה:
1. הפעלת גילוי כלים
בבנאי Server שלך בקובץ webhook.ts, הוסף tools: {} ליכולות כדי ש Claude Code יידע שהשרת שלך מציע כלים:
capabilities: {
experimental: { 'claude/channel': {} },
tools: {}, // enables tool discovery
},2. רישום כלי התשובה
הוסף את הקטע הבא ל webhook.ts. ה import הולך לראש הקובץ יחד עם שאר הייבואים. שני המטפלים הולכים בין בנאי 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}`)
})3. עדכון ההוראות
עדכן את מחרוזת 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 /:
webhook.ts מלא עם כלי reply
#!/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 מציג דוגמה מלאה יותר עם קבצים מצורפים ועריכת הודעות.
#שער להודעות נכנסות
ערוץ בלי שער הוא וקטור להזרקת פרומפט. כל מי שיכול להגיע לנקודת הקצה שלך יכול לשים טקסט מול Claude. ערוץ שמאזין לפלטפורמת צ'אט או לנקודת קצה ציבורית צריך בדיקת שולח אמיתית לפני שהוא פולט משהו.
בדוק את השולח מול רשימת היתר לפני הקריאה ל mcp.notification(). הדוגמה הזו משמיטה כל הודעה משולח שאינו בקבוצה:
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. בצ'אטים קבוצתיים אלה שונים, ושער על החדר יאפשר לכל אחד בקבוצה שברשימת ההיתר להזריק הודעות לסשן.
ערוצי Telegram וDiscord שמים שער על רשימת היתר של שולחים באותו אופן. הם מאתחלים את הרשימה באמצעות צימוד. ראה אחד מהמימושים לזרימת הצימוד המלאה. ערוץ iMessage נוקט גישה שונה: הוא מזהה את הכתובות של המשתמש עצמו ממסד הנתונים של Messages בעת ההפעלה ומעביר אותן אוטומטית, ושולחים אחרים מתווספים לפי handle.
#העברת בקשות הרשאה
כש Claude קורא לכלי שצריך אישור, נפתח דיאלוג המסוף המקומי והסשן ממתין. ערוץ דו כיווני יכול לבחור להצטרף כדי לקבל את אותה בקשה במקביל ולהעביר אותה אליך במכשיר אחר. שניהם נשארים פעילים: אפשר לענות במסוף או בטלפון, ו Claude Code מיישם את התשובה שמגיעה ראשונה וסוגר את האחרת.
ההעברה מכסה אישורי שימוש בכלים כמו Bash, Write ו Edit. דיאלוגי אמון בפרויקט והסכמה לשרת MCP לא מועברים. אלה מופיעים רק במסוף המקומי.
Claude Code v2.1.234 ואילך שולח בקשות הרשאה רק לשרתים שהוא רשם כערוצים עבור הסשן, כך שההעברה יושבת מאחורי אותה הצטרפות לסשן ובקרות ארגון כמו מסירת הודעות. ההעברה גם דורשת שתצטרף את השרת עם --channels או דגל הפיתוח, ודורשת שהשרת יצהיר על יכולת ההרשאה.
#איך ההעברה עובדת
כשנפתחת בקשת הרשאה, ללולאת ההעברה ארבעה צעדים:
- Claude Code מייצר מזהה בקשה קצר ומודיע לשרת שלך
- השרת שלך מעביר את הבקשה ואת המזהה לאפליקציית הצ'אט שלך
- המשתמש המרוחק עונה בכן או לא ועם המזהה הזה
- המטפל הנכנס שלך מפרק את התשובה להכרעה, ו Claude Code מיישם אותה רק אם המזהה תואם לבקשה פתוחה
דיאלוג המסוף המקומי נשאר פתוח לאורך כל זה. אם מישהו במסוף עונה לפני שההכרעה המרוחקת מגיעה, התשובה הזו מיושמת במקומה והבקשה המרוחקת הממתינה מושמטת.
#שדות בקשת הרשאה
ההתראה היוצאת מ Claude Code היא notifications/claude/channel/permission_request. כמו התראת הערוץ, התעבורה היא MCP סטנדרטי אבל המתודה והסכמה הם הרחבות של Claude Code. לאובייקט params יש ארבעה שדות מחרוזת שהשרת שלך מעצב לתוך הבקשה היוצאת:
| שדה | תיאור |
|---|---|
request_id | חמש אותיות קטנות שנמשכות מ a עד z בלי l, כך שהיא אף פעם לא נקראת כ 1 או I כשמקלידים בטלפון. כלול אותו בבקשה היוצאת שלך כדי שאפשר יהיה להדהד אותו בתשובה. Claude Code מקבל הכרעה רק אם היא נושאת מזהה שהוא הנפיק. דיאלוג המסוף המקומי לא מציג את המזהה הזה, כך שהמטפל היוצא שלך הוא הדרך היחידה ללמוד אותו. |
tool_name | שם הכלי ש Claude רוצה להשתמש בו, לדוגמה Bash או Write. |
description | סיכום קריא לבני אדם של מה שהקריאה הספציפית הזו לכלי עושה, אף פעם לא הפקודה עצמה. עבור קריאת Bash זה התיאור של Claude לפקודה. כשהמודל לא נותן תיאור, השדה הוא הקבוע Run shell command והוא נושא אפס פירוט פקודה. רנדר את input_preview כשיש לך מקום. |
input_preview | הארגומנטים של הכלי כטקסט תצוגה בצורת JSON, לפי מפתח לכל שדה ברמה העליונה. עבור Bash זו הפקודה. עבור Write, נתיב הקובץ והתוכן. השמט אותו מהבקשה שלך אם יש לך מקום רק להודעה של שורה אחת. השרת שלך מחליט מה להציג. |
לקוחות ב Claude Code v2.1.211 או מאוחר יותר מסננים את description ואת input_preview לפני ההעברה. צפה לשלושה שינויים בטקסט שאתה מקבל:
- Claude Code מנטרל תווי עקיפת כיוון, תווים בלתי נראים, וחיקויים של מרכאות וסוגריים זוויתיים.
- Claude Code מקפל כל רצף של רווח לבן לרווח יחיד.
- Claude Code מעביר טקסט בשלמותו עד 3,500 נקודות קוד. לערך ארוך יותר, אתה מקבל את תחילתו ואת סופו סביב סמן ספירה
⋯ N code points elided ⋯. סוף של פקודה ארוכה עדיין מגיע למאשר.
עבור input_preview, Claude Code מחיל את מגבלת 3,500 על כל שדה ברמה העליונה של הארגומנטים בנפרד ושומר על המרכאות המבניות של ה JSON עצמו. לקוחות לפני v2.1.211 מעבירים את description גולמי וחותכים את input_preview ל 200 יחידות UTF-16 עם אליפסיס בסוף.
לקוחות ב Claude Code v2.1.234 או מאוחר יותר מעבירים את הסמן (value unserializable) במקום ערך שדה input_preview שהם לא יכולים לעשות לו סריאליזציה בבטחה, כמו מבנה מעגלי או מערך גדול במיוחד. אתה עדיין מקבל את המפתח של השדה, ושאר השדות בתצוגה המקדימה לא משתנים.
לקוחות ב Claude Code v2.1.234 או מאוחר יותר גם מסווים פרטי גישה ב description וב input_preview. אתה מקבל [REDACTED] במקום אסימון פרטי גישה של ספק שניתן לזהות, כמו מפתח API או אסימון גישה אישי. צפה לשלוש השפעות של המיסוך כשאתה מרנדר את השדות:
- Claude Code מסווה שמות מפתח בתוך
input_previewוגם את הערכים שלהם. שם מפתח שאתה מציג עשוי לא להתאים לשם המפתח בקלט. - Claude Code אף פעם לא מסווה מקטע שמכיל תחביר מעטפת, תווי נתיב, או תווי URL. מיסוך לא יכול להסתיר את הפקודה, נתיב הקובץ, או היעד שמאושרים.
- Claude Code לא מסווה סוד שאין לו קידומת שניתן לזהות, או סוד שחוצה רווח לבן, כמו בלוק מפתח פרטי. שניהם מגיעים לשרת שלך בלי מיסוך.
המיסוך לא משנה מי מקבל את השדות. כל מה שנשאר בלי מיסוך הולך רק לשרתים שהצטרפת אליהם עם --channels או דגל הפיתוח. התייחס לשני השדות כלא מהימנים אלא אם אתה שולט בצי הלקוחות.
ההכרעה שהשרת שלך שולח בחזרה היא notifications/claude/channel/permission עם שני שדות: request_id שמהדהד את המזהה שלמעלה, ו behavior שמוגדר ל 'allow' או 'deny'. allow נותן לקריאת הכלי להמשיך. deny דוחה אותה. אף הכרעה לא משפיעה על קריאות עתידיות.
#הוספת ממסר לגשר צ'אט
הוספת ממסר הרשאות לערוץ דו כיווני דורשת שלושה רכיבים:
- רשומת
claude/channel/permission: {}תחת יכולותexperimentalבבנאיServer, כדי ש Claude Code ידע להעביר הנחיות - מטפל בהתראות עבור
notifications/claude/channel/permission_requestשמעצב את ההנחיה ושולח אותה דרך ה API של הפלטפורמה - בדיקה במטפל ההודעות הנכנסות שמזהה
yes <id>אוno <id>ופולטת פסק דיןnotifications/claude/channel/permissionבמקום להעביר את הטקסט ל Claude
הצהר על היכולת רק אם הערוץ שלך מאמת את השולח, כי כל מי שיכול להשיב דרך הערוץ יכול לאשר או לדחות שימוש בכלים בסשן שלך.
כדי להוסיף אותם לגשר צ'אט דו כיווני כמו זה שהורכב בחשיפת כלי תשובה:
1. הצהרה על יכולת ההרשאה
בבנאי Server, הוסף claude/channel/permission: {} לצד claude/channel תחת experimental:
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // opt in to permission relay
},
tools: {},
},2. טיפול בבקשה הנכנסת
רשום מטפל בהתראות בין בנאי Server לבין mcp.connect(). Claude Code קורא לו עם ארבעת שדות הבקשה כשנפתחת תיבת הרשאה. המטפל מעצב את ההנחיה לפלטפורמה וכולל הוראות לתשובה עם המזהה:
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(), // summary of this call. Treat as untrusted.
input_preview: z.string(), // tool args as JSON-shaped text. Treat as untrusted.
}),
})
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` +
// input_preview carries the actual arguments; render it when you
// have room: for Bash the description alone may be just
// "Run shell command" with zero command detail
`${params.input_preview}\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}"`,
)
})3. יירוט פסק הדין במטפל הנכנס
מטפל הנכנסות הוא הלולאה או ה callback שמקבל הודעות מהפלטפורמה: אותו מקום שבו מסננים לפי שולח ופולטים notifications/claude/channel כדי להעביר צ'אט ל Claude. הוסף בדיקה לפני קריאת העברת הצ'אט, שמזהה את פורמט פסק הדין ופולטת את התראת ההרשאה במקום זאת.
הביטוי הרגולרי תואם לפורמט המזהה ש Claude Code מייצר: חמש אותיות, אף פעם לא l. הדגל /i סובל תיקון אוטומטי בטלפון שמעביר את התשובה לאותיות גדולות. המר את המזהה שנלכד לאותיות קטנות לפני השליחה בחזרה.
// 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) } },
})
}אם לתשובה מרחוק אין השפעה ותיבת הדו שיח במסוף המקומי נשארת פתוחה, בדוק את הסיבות האלה:
- הפורמט שגוי: הביטוי הרגולרי במטפל הנכנס לא מצליח להתאים, ולכן טקסט כמו
approve itאוyesבלי מזהה ממשיך כהודעה רגילה ל Claude. - המזהה שגוי: השרת פולט פסק דין, אבל Claude Code לא מוצא בקשה פתוחה עם המזהה הזה ומשליך אותו בשקט.
- המזהה שימש בהנחיה קודמת: השרת פולט פסק דין עם המזהה מהבקשה, אבל להנחיה קודמת בסשן היה אותו מזהה. ברגע שמזהה חוזר, Claude Code משליך כל פסק דין ערוץ שנושא אותו, כדי שתשובה שנועדה להנחיה אחת לא תוכל לאשר את האחרת. מזהה הוא חמש אותיות אקראיות, ולכן חזרה נדירה. ענה במסוף במקום זאת.
#דוגמה מלאה
קובץ webhook.ts המורכב למטה משלב את שלוש ההרחבות מהדף הזה: כלי התשובה, סינון שולח, וממסר הרשאות. אם מתחילים כאן, צריך גם את הגדרת הפרויקט ואת רשומת .mcp.json מההדרכה הראשונית.
כדי ששני הכיוונים יהיו ניתנים לבדיקה מ curl, מאזין ה HTTP מגיש שני נתיבים:
GET /events: מחזיק זרם SSE פתוח ודוחף כל הודעה יוצאת כשורתdata:, כך שcurl -Nיכול לצפות בתשובות של Claude ובהנחיות הרשאה בזמן אמת.POST /: הצד הנכנס, אותו מטפל כמו קודם, עכשיו עם בדיקת פורמט פסק הדין שמוכנסת לפני ענף העברת הצ'אט.
webhook.ts מלא עם העברת הרשאות:
#!/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` +
`${params.input_preview}\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:
claude --dangerously-load-development-channels server:webhookההדרכה הזו בודקת את תיבת ההרשאה עצמה, אז אחרי שהסשן נפתח, לחץ Shift+Tab עד ששורת המצב מציגה ⏸ manual mode on. במצב אוטומטי המסווג היה מחליט על קריאת reply במקומך, ולא הייתה נפתחת תיבה שהצד המרוחק יכול לענות עליה.
בשני, הזרם את הצד היוצא כדי לראות את התשובות של Claude וכל הנחיות הרשאה כשהן נשלחות:
curl -N localhost:8788/eventsבשלישי, שלח הודעה שתגרום ל Claude לנסות להריץ פקודה:
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788רשימת קבצים היא לקריאה בלבד, ולכן Claude מריץ אותה בלי אישור. תיבת ההרשאה נפתחת כש Claude קורא לכלי reply כדי לשלוח את התשובה בחזרה. התיבה המקומית נפתחת במסוף Claude Code, ורגע אחר כך ההנחיה עבור mcp__webhook__reply מופיעה בזרם /events, כולל המזהה בן חמש האותיות. אשר אותו מהצד המרוחק:
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788התיבה המקומית נסגרת, כלי reply רץ, והתשובה של Claude מופיעה בזרם.
שלושת החלקים שספציפיים לערוץ בקובץ הזה:
- יכולות בבנאי
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, פנו אליו כדי לתאם רישום בשוק הרשמי. בתוכניות Team ו Enterprise, מנהל יכול במקום זאת לכלול את התוסף ברשימת allowedChannelPlugins של הארגון, שמחליפה את רשימת ההיתר של Anthropic כברירת מחדל.
#ראה גם
- ערוצים להתקנה ולשימוש ב Telegram, Discord, iMessage, או הדגמת fakechat, ולהפעלת ערוצים לארגון Team או Enterprise
- מימושי ערוץ עובדים לקוד שרת מלא עם תהליכי צימוד, כלי תשובה, וקבצים מצורפים
- MCP לפרוטוקול הבסיסי ששרתי ערוץ מממשים
- תוספים לאריזת הערוץ כדי שמשתמשים יוכלו להתקין אותו עם
/plugin install