チャネルリファレンス
webhook、アラート、チャットメッセージを Claude Code セッションにプッシュする MCP サーバーを構築します。チャネルコントラクトのリファレンス:機能宣言、通知イベント、返信ツール、送信者ゲーティング、権限リレー。
チャネルは、Claude Code セッションにイベントをプッシュする MCP サーバーで、Claude がターミナルの外で発生していることに反応できるようにします。
一方向または双方向のチャネルを構築できます。一方向チャネルは、アラート、webhook、または監視イベントを転送して Claude が対応できるようにします。チャットブリッジのような双方向チャネルは、Claude がメッセージを返送できるように返信ツールを公開することもできます。信頼できる送信者パスを持つチャネルは、権限プロンプトをリレーすることを選択して、ツール使用をリモートで承認または拒否できます。
このページでは以下をカバーしています:
- 概要:チャネルの仕組み
- 必要なもの:要件と一般的な手順
- 例:webhook レシーバーを構築:最小限の一方向ウォークスルー
- サーバーオプション:コンストラクタフィールド
- 通知フォーマット:イベントペイロードと配信動作
- 返信ツールを公開:Claude がメッセージを返送できるようにする
- インバウンドメッセージをゲート:プロンプトインジェクションを防ぐための送信者チェック
- 権限プロンプトをリレー:ツール承認プロンプトをリモートチャネルに転送
既存のチャネルを使用する場合は、チャネルを参照してください。Telegram、Discord、iMessage、および fakechat はリサーチプレビューに含まれています。
概要
チャネルは、Claude Code と同じマシン上で実行されるMCP サーバーです。Claude Code はそれをサブプロセスとして生成し、stdio 経由で通信します。チャネルサーバーは、外部システムと Claude Code セッション間のブリッジです:
- チャットプラットフォーム(Telegram、Discord):プラグインはローカルで実行され、プラットフォームの API をポーリングして新しいメッセージを取得します。誰かがボットに DM を送信すると、プラグインはメッセージを受け取り、Claude に転送します。公開する URL は不要です。
- Webhook(CI、監視):サーバーはローカル HTTP ポートでリッスンします。外部システムがそのポートに POST し、サーバーはペイロードを Claude にプッシュします。
必要なもの
唯一のハード要件は、@modelcontextprotocol/sdk パッケージと Node.js 互換ランタイムです。Bun、Node、Deno すべて動作します。リサーチプレビューの事前構築プラグインは Bun を使用していますが、チャネルはそうである必要はありません。
サーバーは以下を実行する必要があります:
claude/channel機能を宣言して、Claude Code が通知リスナーを登録するようにする- 何かが発生したときに
notifications/claude/channelイベントを発行する - stdio トランスポート経由で接続する
サーバーオプションと通知フォーマットセクションでは、これらのそれぞれについて詳しく説明しています。完全なウォークスルーについては、例:webhook レシーバーを構築を参照してください。
リサーチプレビュー中、カスタムチャネルは承認許可リストにありません。ローカルでテストするには --dangerously-load-development-channels を使用してください。詳細については、リサーチプレビュー中のテストを参照してください。
例:webhook レシーバーを構築
このウォークスルーでは、HTTP リクエストをリッスンして Claude Code セッションに転送する単一ファイルサーバーを構築します。終了時には、CI パイプライン、監視アラート、または curl コマンドなど、HTTP POST を送信できるものはすべて、Claude にイベントをプッシュできます。
この例では、組み込み HTTP サーバーと TypeScript サポートのために Bun をランタイムとして使用しています。代わりに Node または Deno を使用できます。唯一の要件は MCP SDK です。
プロジェクトを作成
このページの後半にある権限リレーの例は zod を直接インポートするため、MCP SDK と一緒にインストールされます。新しいディレクトリを作成して両方をインストールします:
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk zod
チャネルサーバーを記述
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 Code はサーバーが接続するときこれを 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())
// すべての POST を Claude に転送する HTTP サーバーを開始
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')
},
})
ファイルは順番に 3 つのことを実行します:
- サーバー設定:
claude/channelをその機能に含む MCP サーバーを作成します。これが Claude Code にこれがチャネルであることを伝えます。Claude Code はサーバーが接続するときにinstructions文字列を Claude にコンテキストとして配信します:Claude に期待するイベント、返信するかどうか、返信する場合はどのように返信をルーティングするかを伝えます。 - Stdio 接続:stdin/stdout 経由で Claude Code に接続します。これは任意の MCP サーバー の標準です。
- HTTP リスナー:ポート 8788 でローカル Web サーバーを開始します。すべての POST 本文は
mcp.notification()経由でチャネルイベントとして Claude に転送されます。contentはイベント本文になり、各metaエントリは<channel>タグの属性になります。リスナーはmcpインスタンスへのアクセスが必要なため、同じプロセスで実行されます。より大きなプロジェクトの場合は、別のモジュールに分割できます。
Claude Code にサーバーを登録
Claude Code がそれを開始する方法を知るように、MCP 設定にサーバーを追加します。同じディレクトリのプロジェクトレベル .mcp.json の場合は、相対パスを使用します。~/.claude.json のユーザーレベル設定の場合は、サーバーが任意のプロジェクトから見つかるように完全な絶対パスを使用します:
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}
Claude Code は起動時に MCP 設定を読み込み、各サーバーをサブプロセスとして生成します。
テスト
リサーチプレビュー中、カスタムチャネルは許可リストにないため、開発フラグで Claude Code を開始します:
claude --dangerously-load-development-channels server:webhook
Claude Code は最初に、読み込んでいる開発チャネルをリストする全画面警告ダイアログを表示します。ローカル開発に使用しています を選択して続行するか、終了 を選択して終了します。
このプロジェクトで初めてセッションを開始するとき、Claude Code は .mcp.json から新しいサーバーを使用する前に同意を求めます。ダイアログは「このプロジェクトで見つかった新しい MCP サーバー:webhook」と報告します。このMCPサーバーを使用 を選択して続行します。
同意した後、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。
'組織ポリシーによってブロックされています'が表示される場合は、組織管理者が最初に チャネルを有効化 する必要があります。
別のターミナルで、HTTP POST でメッセージを送信して webhook をシミュレートします。この例は、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>
ターミナルはイベントを 1 行の概要として、← 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が「接続が拒否されました」で失敗:ポートはまだバインドされていないか、以前の実行からの古いプロセスがそれを保持しています。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 または 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 を省略します。この例は、チャネル機能、ツール、および設定された命令を含む双方向セットアップを示しています:
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 Code はサーバーが接続するときにこれを Claude に配信するため、イベントの処理方法を知っている
instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
},
)
通知フォーマット
サーバーは notifications/claude/channel を 2 つのパラメータで発行します:
| フィールド | タイプ | 説明 |
|---|---|---|
content |
string |
イベント本文。<channel> タグの本文として配信されます。 |
meta |
Record<string, string> |
オプション。各エントリは <channel> タグの属性になり、chat ID、送信者名、またはアラート重大度などのルーティングコンテキストを提供します。キーは識別子である必要があります:文字、数字、アンダースコアのみ。ハイフンまたは他の文字を含むキーはサイレントにドロップされます。 |
サーバーは Server インスタンスで mcp.notification() を呼び出してイベントをプッシュします。この例は、2 つのメタキーを持つ 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' },
},
})
イベントは 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 は通知を確認しません。mcp.notification() の await は、メッセージがトランスポートに書き込まれるときに解決され、Claude が処理したときではありません。セッションがチャネルとしてサーバーを読み込んでいない場合、または組織ポリシーがそれをブロックしている場合、Claude Code はイベントをサイレントにドロップし、サーバーにエラーを返しません。
配信確認が必要な場合は、サーバーでイベント状態を追跡し、Claude が状態を報告するために呼び出せる返信ツールを公開します。
イベントはセッションにキューイングされ、順番に処理されます。Claude がビジーの間に複数の通知が到着した場合、次のターンで一緒に配信され、Claude はそれらをグループとして処理します。独立したイベントストリームを同時に処理するには、別のセッションを実行します。
返信ツールを公開
チャネルが双方向の場合、アラートフォワーダーではなくチャットブリッジのような場合、Claude がメッセージを返送するために呼び出せる標準 MCP ツールを公開します。ツール登録に関するチャネル固有のものはありません。返信ツールには 3 つのコンポーネントがあります:
Serverコンストラクタ機能にtools: {}エントリがあり、Claude Code がツールを検出できるようにする- ツールのスキーマを定義し、送信ロジックを実装するツールハンドラー
- Claude に何時どのようにツールを呼び出すかを伝える
Serverコンストラクタのinstructions文字列
これらを上記の webhook レシーバーに追加するには:
ツール検出を有効化
webhook.ts の Server コンストラクタで、Claude Code がサーバーがツールを提供することを知るように、機能に tools: {} を追加します:
capabilities: {
experimental: { 'claude/channel': {} },
tools: {}, // ツール検出を有効化
},
返信ツールを登録
以下を webhook.ts に追加します。import はファイルの上部の他のインポートと一緒に移動します。2 つのハンドラーは 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}`)
})
命令を更新
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 です。アウトバウンド返信は Server-Sent Events(SSE)を使用して GET /events でストリーミングされるため、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({ ... })
チャットまたはルーム ID ではなく、送信者の ID でゲートします:例では message.from.id、message.chat.id ではありません。グループチャットでは、これらは異なり、ルームでゲートすると、許可リストに登録されたグループ内の誰もがセッションにメッセージを注入できます。
Telegram と Discord チャネルは同じ方法で送信者許可リストでゲートします。ペアリングでリストをブートストラップします。完全なペアリングフローについては、いずれかの実装を参照してください。iMessage チャネルは異なるアプローチを取ります:起動時にメッセージデータベースからユーザー自身のアドレスを検出し、それらを自動的に通します。他の送信者はハンドルで追加されます。
権限プロンプトをリレー
Claude が承認が必要なツールを呼び出すと、ローカルターミナルダイアログが開き、セッションが待機します。双方向チャネルは、同じプロンプトを並行して受け取り、別のデバイスでそれをリレーすることを選択できます。両方がライブのままです:ターミナルまたは電話で答えることができ、Claude Code は最初に到着した答えを適用し、もう一方を閉じます。
リレーは Bash、Write、Edit などのツール使用承認をカバーします。プロジェクト信頼と MCP サーバー同意ダイアログはリレーされません。これらはローカルターミナルにのみ表示されます。
Claude Code v2.1.234 以降は、セッション用にチャネルとして登録したサーバーにのみ権限リクエストを送信するため、リレーはセッションオプトインと組織コントロールと同じ背後にあります。リレーはまた、--channels または開発フラグでサーバーをオプトインし、サーバーが権限機能を宣言することを必要とします。
リレーの仕組み
権限プロンプトが開くと、リレーループには 4 つのステップがあります:
- Claude Code は短いリクエスト ID を生成し、サーバーに通知
- サーバーはプロンプトと ID をチャットアプリに転送
- リモートユーザーは yes または no と ID で返信
- インバウンドハンドラーは返信を判定に解析し、Claude Code は ID が開いているリクエストと一致する場合のみそれを適用
ローカルターミナルダイアログはこのすべてを通じて開いたままです。ターミナルの誰かがリモート判定が到着する前に答えた場合、その答えが代わりに適用され、保留中のリモートリクエストはドロップされます。
権限リクエストフィールド
Claude Code からのアウトバウンド通知は notifications/claude/channel/permission_request です。チャネル通知のように、トランスポートは標準 MCP ですが、メソッドとスキーマは Claude Code 拡張です。params オブジェクトには、サーバーが発信プロンプトにフォーマットする 4 つの文字列フィールドがあります:
| フィールド | 説明 |
|---|---|
request_id |
a-z から l なしで描画された 5 つの小文字。電話で入力するときに 1 または I として読まれることはありません。発信プロンプトに含めて、返信で反映できるようにします。Claude Code は発行した ID を持つ判定のみを受け入れます。ローカルターミナルダイアログはこの ID を表示しないため、アウトバウンドハンドラーはそれを学ぶ唯一の方法です。 |
tool_name |
Claude が使用したいツールの名前、例えば Bash または Write。 |
description |
この特定のツール呼び出しが何をするかの人間が読める要約。コマンド自体ではありません。Bash 呼び出しの場合、これは Claude のコマンドの説明です。モデルが説明を与えない場合、フィールドは定数 Run shell command であり、ゼロのコマンド詳細を持ちます。余地がある場合は input_preview をレンダリングします。 |
input_preview |
ツールの引数を JSON 形式の表示テキストとして、トップレベルフィールドごとにキー付けされたもの。Bash の場合はコマンド。Write の場合はファイルパスとコンテンツ。1 行のメッセージの余地しかない場合はプロンプトから省略します。サーバーは何を表示するかを決定します。 |
Claude Code v2.1.211 以降のクライアントは、リレーする前に description と input_preview をサニタイズします。受け取るテキストに 3 つの変更を予想してください:
- 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 以降のクライアントは、循環構造や極めて大きな配列など、安全にシリアライズできない input_preview フィールド値の代わりに、マーカー (value unserializable) をリレーします。フィールドのキーは依然として受け取り、プレビューの他のフィールドは変わりません。
Claude Code v2.1.234 以降のクライアントはまた、description と input_preview の認証情報をマスクします。API キーまたは個人アクセストークンなど、認識可能なプロバイダー認証情報トークンの代わりに [REDACTED] を受け取ります。フィールドをレンダリングするときにマスキングの 3 つの効果を予想してください:
- Claude Code は
input_preview内のキー名とその値をマスクします。表示するキー名は入力のキー名と一致しない場合があります。 - Claude Code はシェル構文、パス文字、または URL 文字を含むスパンをマスクしません。マスクは承認されるコマンド、ファイルパス、または宛先を隠すことはできません。
- Claude Code は認識可能なプレフィックスを持たないシークレット、またはホワイトスペースにまたがるシークレット(秘密鍵ブロックなど)をマスクしません。両方ともサーバーにマスクされていない状態で到達します。
マスキングはフィールドを受け取る人を変更しません。マスクされていない状態で残るものは、--channels または開発フラグでオプトインしたサーバーにのみ送信されます。両方のフィールドをクライアントフリートを制御しない限り信頼されないものとして扱います。
サーバーが返送する判定は notifications/claude/channel/permission で、2 つのフィールド:上記の ID を反映する request_id と、'allow' または 'deny' に設定された behavior。Allow はツール呼び出しを続行させます。Deny はそれを拒否し、ローカルダイアログで No と答えるのと同じです。どちらの判定も将来の呼び出しに影響しません。
チャットブリッジにリレーを追加
双方向チャネルに権限リレーを追加するには、3 つのコンポーネントが必要です:
Serverコンストラクタのexperimental機能の下にclaude/channel/permission: {}エントリがあり、Claude Code がプロンプトを転送することを知るようにするnotifications/claude/channel/permission_requestの通知ハンドラーがプロンプトをフォーマットしてプラットフォーム API 経由で送信- インバウンドメッセージハンドラーの確認が
yes <id>またはno <id>を認識し、テキストを Claude に転送する代わりにnotifications/claude/channel/permission判定を発行
チャネルが送信者を認証する場合のみ機能を宣言してください。チャネル経由で返信できる誰もがセッションのツール使用を承認または拒否できるためです。
これらを返信ツールを公開で組み立てられたような双方向チャットブリッジに追加するには:
権限機能を宣言
Server コンストラクタで、experimental の下に claude/channel と一緒に claude/channel/permission: {} を追加します:
capabilities: {
experimental: {
'claude/channel': {},
'claude/channel/permission': {}, // 権限リレーにオプトイン
},
tools: {},
},
受信リクエストを処理
Server コンストラクタと mcp.connect() の間に通知ハンドラーを登録します。権限ダイアログが開くと、Claude Code は4 つのリクエストフィールドで呼び出します。ハンドラーはプロンプトをプラットフォーム用にフォーマットし、ID で返信するための命令を含めます:
import { z } from 'zod'
// setNotificationHandler はメソッドフィールドで z.literal にルーティングするため、
// このスキーマはバリデータとディスパッチキーの両方です
const PermissionRequestSchema = z.object({
method: z.literal('notifications/claude/channel/permission_request'),
params: z.object({
request_id: z.string(), // 5 つの小文字、プロンプトに逐語的に含める
tool_name: z.string(), // 例:'Bash'、'Write'
description: z.string(), // この呼び出しが何をするかの要約。信頼されないものとして扱う。
input_preview: z.string(), // ツール引数を JSON 形式のテキストとして。信頼されないものとして扱う。
}),
})
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
// send() はアウトバウンド:チャットプラットフォームに POST、またはローカル
// テストの場合は下の完全な例に示されている SSE ブロードキャスト。
send(
`Claude wants to run ${params.tool_name}: ${params.description}\n` +
// input_preview は実際の引数を持ちます。余地がある場合はレンダリングします:
// Bash の場合、説明だけは「Run shell command」で、ゼロのコマンド詳細かもしれません
`${params.input_preview}\n\n` +
// 命令の ID はステップ 3 でインバウンドハンドラーが解析するもの
`Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
)
})
インバウンドハンドラーで判定をインターセプト
インバウンドハンドラーは、プラットフォームからメッセージを受け取るループまたはコールバック:送信者でゲートし、notifications/claude/channel を発行してチャットを Claude に転送する同じ場所。判定フォーマットを認識し、チャット転送呼び出しの代わりに権限通知を発行するチェックを追加します。
正規表現は Claude Code が生成する ID フォーマットと一致します:5 文字、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) } },
})
}
期待されたフォーマットと正確に一致しないリモート返信は、2 つの方法のいずれかで失敗し、どちらの場合もローカルターミナルダイアログは開いたままです:
- 異なるフォーマット:インバウンドハンドラーの正規表現が一致しないため、'approve it'または ID なしの'yes'のようなテキストは通常のメッセージとして Claude にフォールスルーします。
- 正しいフォーマット、間違った ID:サーバーは判定を発行しますが、Claude Code はその ID を持つ開いているリクエストを見つけず、サイレントにドロップします。
完全な例
以下の組み立てられた webhook.ts は、このページからの 3 つの拡張をすべて組み合わせます:返信ツール、送信者ゲーティング、権限リレー。ここから始める場合は、初期ウォークスルーからプロジェクト設定と .mcp.json エントリも必要です。
curl から両方向をテスト可能にするために、HTTP リスナーは 2 つのパスを提供します:
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` +
`${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 はアウトバウンドをストリーム、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')
},
})
判定パスを 3 つのターミナルでテストします。最初は Claude Code セッションで、開発フラグで開始されるため、webhook.ts を生成します:
claude --dangerously-load-development-channels server:webhook
このウォークスルーは権限ダイアログ自体をテストするため、セッションが開いたら、Shift+Tab を押してステータスバーが ⏸ manual mode on を表示するまで。オートモードでは分類器は reply 呼び出しの代わりに決定し、リモート側が答えるためのダイアログは開きません。
2 番目では、アウトバウンド側をストリーミングして、Claude の返信と権限プロンプトがライブで到着するのを見ることができます:
curl -N localhost:8788/events
3 番目では、Claude がコマンドを実行しようとするメッセージを送信します:
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788
ファイルをリストすることは読み取り専用なので、Claude は承認なしでそれを実行します。権限ダイアログは Claude が reply ツールを呼び出して答えを返送するときに開きます。ローカルダイアログは Claude Code ターミナルで開き、少し後、mcp__webhook__reply のプロンプトが /events ストリームに表示され、5 文字の ID を含みます。リモート側から承認します:
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788
ローカルダイアログが閉じ、reply ツールが実行され、Claude の返信がストリームに到着します。
このファイルの 3 つのチャネル固有の部分:
Serverコンストラクタの機能:claude/channelは通知リスナーを登録し、claude/channel/permissionは権限リレーにオプトイン、toolsは Claude がツールを検出できるようにします。- アウトバウンドパス:
replyツールハンドラーは Claude が会話応答のために呼び出すもの。PermissionRequestSchema通知ハンドラーは権限ダイアログが開くと Claude Code が呼び出すもの。両方とも/events経由でブロードキャストするためにsend()を呼び出しますが、システムの異なる部分によってトリガーされます。 - 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 許可リストを置き換えます。