SpyBara
Go Premium

channels-reference.md 2026-11-07 23:58 UTC to 2026-11-08 22:58 UTC

No changes for this document in this history range.

2026
Tue 10 21:57

チャネルリファレンス

webhook、アラート、チャットメッセージを Claude Code セッションにプッシュする MCP サーバーを構築します。チャネルコントラクトのリファレンス:機能宣言、通知イベント、返信ツール、送信者ゲーティング、権限リレー。

[!NOTE]

チャネルはリサーチプレビュー段階にあります。Team および Enterprise 組織は明示的に有効化する必要があります。

チャネルは、Claude Code セッションにイベントをプッシュする MCP サーバーで、Claude がターミナルの外で発生していることに反応できるようにします。

一方向または双方向のチャネルを構築できます。一方向チャネルは、アラート、webhook、または監視イベントを転送して Claude が対応できるようにします。チャットブリッジのような双方向チャネルは、Claude がメッセージを返送できるように返信ツールを公開することもできます。信頼できる送信者パスを持つチャネルは、権限プロンプトをリレーすることを選択して、ツール使用をリモートで承認または拒否できます。

このページでは以下をカバーしています:

既存のチャネルを使用する場合は、チャネルを参照してください。Telegram、Discord、iMessage、および fakechat はリサーチプレビューに含まれています。

概要

チャネルは、Claude Code と同じマシン上で実行されるMCP サーバーです。Claude Code はそれをサブプロセスとして生成し、stdio 経由で通信します。チャネルサーバーは、外部システムと Claude Code セッション間のブリッジです:

  • チャットプラットフォーム(Telegram、Discord):プラグインはローカルで実行され、プラットフォームの API をポーリングして新しいメッセージを取得します。誰かがボットに DM を送信すると、プラグインはメッセージを受け取り、Claude に転送します。公開する URL は不要です。
  • Webhook(CI、監視):サーバーはローカル HTTP ポートでリッスンします。外部システムがそのポートに POST し、サーバーはペイロードを Claude にプッシュします。

外部システムがローカルチャネルサーバーに接続し、stdio 経由で Claude Code と通信するアーキテクチャ図

必要なもの

唯一のハード要件は、@modelcontextprotocol/sdk パッケージと Node.js 互換ランタイムです。Bun、Node、Deno すべて動作します。リサーチプレビューの事前構築プラグインは Bun を使用していますが、チャネルはそうである必要はありません。

サーバーは以下を実行する必要があります:

  1. claude/channel 機能を宣言して、Claude Code が通知リスナーを登録するようにする
  2. 何かが発生したときに notifications/claude/channel イベントを発行する
  3. stdio トランスポート経由で接続する

サーバーオプションと通知フォーマットセクションでは、これらのそれぞれについて詳しく説明しています。完全なウォークスルーについては、例:webhook レシーバーを構築を参照してください。

リサーチプレビュー中、カスタムチャネルは承認許可リストにありません。ローカルでテストするには --dangerously-load-development-channels を使用してください。詳細については、リサーチプレビュー中のテストを参照してください。

例:webhook レシーバーを構築する

このウォークスルーでは、HTTP リクエストを待ち受けて Claude Code セッションに転送する単一ファイルのサーバーを構築します。最終的には、CI パイプライン、監視アラート、curl コマンドなど、HTTP POST を送信できるものであれば何でも Claude にイベントをプッシュできるようになります。

この例では、組み込みの HTTP サーバーと TypeScript サポートを備えた Bun をランタイムとして使用します。代わりに Node や Deno を使用することもできます。唯一の要件は MCP SDK です。

1. プロジェクトを作成する

権限リレーの例では zod を直接インポートするため、MCP SDK と一緒にインストールします。新しいディレクトリを作成し、両方をインストールします。

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

2. チャネルサーバーを作成する

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'

// 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 リスナーの起動をこの順序で行います。

  • サーバー設定:capabilities に 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 インスタンスにアクセスする必要があるため、同じプロセス内で実行されます。大規模なプロジェクトでは、別々のモジュールに分割することもできます。

3. サーバーを Claude Code に登録する

Claude Code がサーバーの起動方法を認識できるように、MCP 設定にサーバーを追加します。同じディレクトリ内のプロジェクトレベルの .mcp.json では、相対パスを使用します。~/.claude.json のユーザーレベルの設定では、どのプロジェクトからでもサーバーを見つけられるように完全な絶対パスを使用します。

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

Claude Code は起動時に MCP 設定を読み込み、各サーバーをサブプロセスとして起動します。

4. テストする

リサーチプレビュー期間中、カスタムチャネルは許可リストに含まれていないため、開発用フラグを付けて Claude Code を起動します。

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

Claude 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 をサブプロセスとして起動し、設定したポート(この例では 8788)で 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 の失敗アラートをポート 8788(または設定したポート)に送信します。

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

ペイロードは <channel> タグとして Claude のコンテキストに届きます。

<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 という 1 行の要約として表示されます。その後、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 サーバーは、このパターンを拡張し、Web UI、ファイル添付、双方向チャットのための返信ツールを追加しています。

リサーチプレビュー中のテスト

リサーチプレビュー中、すべてのチャネルは登録するために承認許可リストにある必要があります。開発フラグは、確認プロンプトの後、特定のエントリの許可リストをバイパスします。この例は両方のエントリタイプを示しています:

# 開発中のプラグインをテスト
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace

# ベアな .mcp.json サーバーをテスト(プラグインラッパーはまだない)
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 を省略します。この例は、チャネル機能、ツール、および設定された命令を含む双方向セットアップを示しています:

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 つのコンポーネントがあります:

  1. Server コンストラクタ機能に tools: {} エントリがあり、Claude Code がツールを検出できるようにする
  2. ツールのスキーマを定義し、送信ロジックを実装するツールハンドラー
  3. Claude に何時どのようにツールを呼び出すかを伝える Server コンストラクタの instructions 文字列

これらを上記の webhook レシーバーに追加するには:

1. ツール検出を有効化

webhook.ts の Server コンストラクタで、Claude Code がサーバーがツールを提供することを知るように、機能に tools: {} を追加します:

capabilities: {
  experimental: { 'claude/channel': {} },
  tools: {},  // ツール検出を有効化
},

2. 返信ツールを登録

以下を 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}`)
})

3. 命令を更新

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 つのステップがあります:

  1. Claude Code は短いリクエスト ID を生成し、サーバーに通知
  2. サーバーはプロンプトと ID をチャットアプリに転送
  3. リモートユーザーは yes または no と ID で返信
  4. インバウンドハンドラーは返信を判定に解析し、Claude Code は ID が開いているリクエストと一致する場合のみそれを適用

ローカルターミナルダイアログはこのすべてを通じて開いたままです。ターミナルの誰かがリモート判定が到着する前に答えた場合、その答えが代わりに適用され、保留中のリモートリクエストはドロップされます。

シーケンス図:Claude Code は権限リクエスト通知をチャネルサーバーに送信し、サーバーはプロンプトと ID をチャットアプリにフォーマットして送信し、人間は判定で返信し、サーバーはその返信を権限通知に解析して Claude Code に戻す

権限リクエストフィールド

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 はそれを拒否します。どちらの判定も将来の呼び出しに影響しません。

チャットブリッジにリレーを追加

双方向チャネルに権限リレーを追加するには、3 つのコンポーネントが必要です:

  1. Server コンストラクタの experimental 機能の下に claude/channel/permission: {} エントリがあり、Claude Code がプロンプトを転送することを知るようにする
  2. notifications/claude/channel/permission_request の通知ハンドラーがプロンプトをフォーマットしてプラットフォーム API 経由で送信
  3. インバウンドメッセージハンドラーの確認が yes <id> または no <id> を認識し、テキストを Claude に転送する代わりに notifications/claude/channel/permission 判定を発行

チャネルが送信者を認証する場合のみ機能を宣言してください。チャネル経由で返信できる誰もがセッションのツール使用を承認または拒否できるためです。

これらを返信ツールを公開で組み立てられたような双方向チャットブリッジに追加するには:

1. 権限機能を宣言

Server コンストラクタで、experimental の下に claude/channel と一緒に claude/channel/permission: {} を追加します:

capabilities: {
  experimental: {
    'claude/channel': {},
    'claude/channel/permission': {},  // 権限リレーにオプトイン
  },
  tools: {},
},

2. 受信リクエストを処理

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

3. インバウンドハンドラーで判定をインターセプト

インバウンドハンドラーは、プラットフォームからメッセージを受け取るループまたはコールバック:送信者でゲートし、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) } },
  })
}

リモート返信が効果を持たず、ローカルターミナルダイアログが開いたままの場合は、次の原因を確認してください:

  • フォーマットが間違っている:インバウンドハンドラーの正規表現が一致しないため、approve it や ID なしの yes のようなテキストは通常のメッセージとして Claude にフォールスルーします。
  • ID が間違っている:サーバーは判定を発行しますが、Claude Code はその ID を持つ開いているリクエストを見つけられず、サイレントにドロップします。
  • ID が以前のプロンプトで使用されていた:サーバーはリクエストの ID を使って判定を発行しますが、セッション内の以前のプロンプトが同じ ID を持っていました。ID が重複すると、Claude Code はその ID を持つチャネル判定をすべてドロップします。これにより、一方のプロンプト向けの返信がもう一方を承認することはありません。ID はランダムな 5 文字なので、重複はまれです。代わりにターミナルで応答してください。

完全な例

以下の組み立てられた 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 パートナー連絡先と協力している場合は、公式マーケットプレイスリストを調整するために彼らに連絡してください。Team および Enterprise プランでは、管理者は代わりにプラグインを組織の独自の allowedChannelPlugins リストに含めることができます。これはデフォルトの Anthropic 許可リストを置き換えます。

関連項目

  • チャネル:Telegram、Discord、iMessage、または fakechat デモをインストールして使用し、Team または Enterprise 組織のチャネルを有効化
  • チャネル実装の動作:ペアリングフロー、返信ツール、ファイル添付を含む完全なサーバーコード
  • MCP:チャネルサーバーが実装する基礎となるプロトコル
  • プラグイン:チャネルをパッケージ化して、ユーザーが /plugin install でインストールできるようにする