メインコンテンツへスキップ
チャネルはリサーチプレビュー段階にあり、Claude Code v2.1.80 以降が必要です。claude.ai ログインが必要です。Console と API キー認証はサポートされていません。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 互換ランタイムです。BunNodeDeno すべて動作します。リサーチプレビューの事前構築プラグインは Bun を使用していますが、チャネルはそうである必要はありません。 サーバーは以下を実行する必要があります:
  1. claude/channel 機能を宣言して、Claude Code が通知リスナーを登録するようにする
  2. 何かが発生したときに notifications/claude/channel イベントを発行する
  3. stdio トランスポート経由で接続する(Claude Code はサーバーをサブプロセスとして生成)
サーバーオプション通知フォーマットセクションでは、これらのそれぞれについて詳しく説明しています。完全なウォークスルーについては、例:webhook レシーバーを構築を参照してください。 リサーチプレビュー中、カスタムチャネルは承認許可リストにありません。ローカルでテストするには --dangerously-load-development-channels を使用してください。詳細については、リサーチプレビュー中のテストを参照してください。

例:webhook レシーバーを構築

このウォークスルーでは、HTTP リクエストをリッスンして Claude Code セッションに転送する単一ファイルサーバーを構築します。終了時には、CI パイプライン、監視アラート、または curl コマンドなど、HTTP POST を送信できるものはすべて、Claude にイベントをプッシュできます。 この例では、組み込み HTTP サーバーと TypeScript サポートのために Bun をランタイムとして使用しています。代わりに Node または Deno を使用できます。唯一の要件は MCP SDK です。
1

プロジェクトを作成

新しいディレクトリを作成して MCP SDK をインストールします:
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk
2

チャネルサーバーを記述

webhook.ts というファイルを作成します。これはチャネルサーバー全体です:stdio 経由で Claude Code に接続し、ポート 8788 で 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'

// MCP サーバーを作成してチャネルとして宣言
const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    // このキーがチャネルにする — Claude Code はそれのリスナーを登録
    capabilities: { experimental: { 'claude/channel': {} } },
    // Claude のシステムプロンプトに追加されるため、これらのイベントの処理方法を知っている
    instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
  },
)

// stdio 経由で Claude Code に接続(Claude Code はこのプロセスを生成)
await mcp.connect(new StdioServerTransport())

// すべての 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 にこれがチャネルであることを伝えます。instructions 文字列は Claude のシステムプロンプトに入ります:Claude に期待するイベント、返信するかどうか、返信する場合はどのツールを使用するか、どの属性を返送するか(chat_id など)を伝えます。
  • Stdio 接続:stdin/stdout 経由で Claude Code に接続します。これは任意の MCP サーバーの標準です:Claude Code はそれをサブプロセスとして生成します。
  • HTTP リスナー:ポート 8788 でローカル Web サーバーを開始します。すべての POST 本文は mcp.notification() 経由でチャネルイベントとして Claude に転送されます。content はイベント本文になり、各 meta エントリは <channel> タグの属性になります。リスナーは mcp インスタンスへのアクセスが必要なため、同じプロセスで実行されます。より大きなプロジェクトの場合は、別のモジュールに分割できます。
3

Claude Code にサーバーを登録

Claude Code がそれを開始する方法を知るように、MCP 設定にサーバーを追加します。同じディレクトリのプロジェクトレベル .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:webhook
Claude Code が起動すると、MCP 設定を読み込み、webhook.ts をサブプロセスとして生成し、HTTP リスナーは設定したポート(この例では 8788)で自動的に開始されます。サーバーを自分で実行する必要はありません。‘ブロックされた組織ポリシー’が表示される場合は、Team または Enterprise 管理者が最初にチャネルを有効化する必要があります。別のターミナルで、HTTP POST でメッセージを送信して webhook をシミュレートします。この例は、CI 失敗アラートをポート 8788(または設定したポート)に送信します:
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
ペイロードは 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 を実行してサーバーのステータスを確認します。「接続に失敗」は通常、サーバーファイルの依存関係またはインポートエラーを意味します。~/.claude/debug/<session-id>.txt のデバッグログで stderr トレースを確認してください。
  • 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 コンストラクタでこれらのオプションを設定します。instructionscapabilities.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推奨。Claude のシステムプロンプトに追加されます。Claude に期待するイベント、<channel> タグ属性の意味、返信するかどうか、返信する場合はどのツールを使用するか、どの属性を返送するか(chat_id など)を伝えます。
一方向チャネルを作成するには、capabilities.tools を省略します。この例は、チャネル機能、ツール、および設定された命令を含む双方向セットアップを示しています:
import { Server } from '@modelcontextprotocol/sdk/server/index.js'

const mcp = new Server(
  { name: 'your-channel', version: '0.0.1' },
  {
    capabilities: {
      experimental: { 'claude/channel': {} },  // チャネルリスナーを登録
      tools: {},  // 一方向チャネルの場合は省略
    },
    // Claude のシステムプロンプトに追加されるため、イベントの処理方法を知っている
    instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
  },
)
イベントをプッシュするには、メソッド notifications/claude/channelmcp.notification() を呼び出します。パラメータは次のセクションにあります。

通知フォーマット

サーバーは notifications/claude/channel を 2 つのパラメータで発行します:
フィールドタイプ説明
contentstringイベント本文。<channel> タグの本文として配信されます。
metaRecord<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 がメッセージを返送するために呼び出せる標準 MCP ツールを公開します。ツール登録に関するチャネル固有のものはありません。返信ツールには 3 つのコンポーネントがあります:
  1. Server コンストラクタ機能に tools: {} エントリがあり、Claude Code がツールを検出できるようにする
  2. ツールのスキーマを定義し、送信ロジックを実装するツールハンドラー
  3. Claude に何時どのようにツールを呼び出すかを伝える Server コンストラクタの instructions 文字列
これらを上記の webhook レシーバーに追加するには:
1

ツール検出を有効化

webhook.tsServer コンストラクタで、Claude Code がサーバーがツールを提供することを知るように、機能に tools: {} を追加します:
capabilities: {
  experimental: { 'claude/channel': {} },
  tools: {},  // ツール検出を有効化
},
2

返信ツールを登録

以下を webhook.ts に追加します。import はファイルの上部の他のインポートと一緒に移動します。2 つのハンドラーは Server コンストラクタと mcp.connect() の間に移動します。これは、Claude が chat_idtext で呼び出せる 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.idmessage.chat.id ではありません。グループチャットでは、これらは異なり、ルームでゲートすると、許可リストに登録されたグループ内の誰もがセッションにメッセージを注入できます。 TelegramDiscord チャネルは同じ方法で送信者許可リストでゲートします。ペアリングでリストをブートストラップします:ユーザーがボットに DM を送信し、ボットはペアリングコードで返信し、ユーザーが Claude Code セッションで承認し、プラットフォーム ID が追加されます。完全なペアリングフローについては、いずれかの実装を参照してください。iMessage チャネルは異なるアプローチを取ります:起動時にメッセージデータベースからユーザー自身のアドレスを検出し、それらを自動的に通します。他の送信者はハンドルで追加されます。

権限プロンプトをリレー

権限リレーには Claude Code v2.1.81 以降が必要です。以前のバージョンは claude/channel/permission 機能を無視します。
Claude が承認が必要なツールを呼び出すと、ローカルターミナルダイアログが開き、セッションが待機します。双方向チャネルは、同じプロンプトを並行して受け取り、別のデバイスでそれをリレーすることを選択できます。両方がライブのままです:ターミナルまたは電話で答えることができ、Claude Code は最初に到着した答えを適用し、もう一方を閉じます。 リレーは BashWriteEdit などのツール使用承認をカバーします。プロジェクト信頼と MCP サーバー同意ダイアログはリレーされません。これらはローカルターミナルにのみ表示されます。

リレーの仕組み

権限プロンプトが開くと、リレーループには 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_ida-z から l なしで描画された 5 つの小文字。電話で入力するときに 1 または I として読まれることはありません。発信プロンプトに含めて、返信で反映できるようにします。Claude Code は発行した ID を持つ判定のみを受け入れます。ローカルターミナルダイアログはこの ID を表示しないため、アウトバウンドハンドラーはそれを学ぶ唯一の方法です。
tool_nameClaude が使用したいツールの名前、例えば Bash または Write
descriptionこの特定のツール呼び出しが何をするかの人間が読める要約。ローカルターミナルダイアログが表示するのと同じテキスト。Bash 呼び出しの場合、これは Claude のコマンドの説明、または何も与えられていない場合はコマンド自体です。
input_previewツールの引数を JSON 文字列として、200 文字に切り詰めたもの。Bash の場合はコマンド。Write の場合はファイルパスとコンテンツのプレフィックス。1 行のメッセージの余地しかない場合はプロンプトから省略します。サーバーは何を表示するかを決定します。
サーバーが返送する判定は notifications/claude/channel/permission で、2 つのフィールド:上記の ID を反映する request_id と、'allow' または 'deny' に設定された behavior。Allow はツール呼び出しを続行させます。Deny はそれを拒否し、ローカルダイアログで No と答えるのと同じです。どちらの判定も将来の呼び出しに影響しません。

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

双方向チャネルに権限リレーを追加するには、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 として、約 200 文字に切り詰め
  }),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  // send() はアウトバウンド:チャットプラットフォームに POST、またはローカル
  // テストの場合は下の完全な例に示されている SSE ブロードキャスト。
  send(
    `Claude wants to run ${params.tool_name}: ${params.description}\n\n` +
    // 命令の ID はステップ 3 でインバウンドハンドラーが解析するもの
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})
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) } },
  })
}
Claude Code はローカルターミナルダイアログも開いたままにするため、どちらかの場所で答えることができ、最初に到着した答えが適用されます。期待されたフォーマットと正確に一致しないリモート返信は、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 /:インバウンド側、以前と同じハンドラー、チャット転送ブランチの前に判定フォーマットチェックが挿入されました。
権限リレー付きの完全な 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'

// --- アウトバウンド:/events の任意の curl -N リスナーに書き込み ---
// 実際のブリッジはチャットプラットフォームに POST します。
const listeners = new Set<(chunk: string) => void>()
function send(text: string) {
  const chunk = text.split('\n').map(l => `data: ${l}\n`).join('') + '\n'
  for (const emit of listeners) emit(chunk)
}

// 送信者許可リスト。ローカルウォークスルーの場合、単一の X-Sender
// ヘッダー値「dev」を信頼します。実際のブリッジはプラットフォームのユーザー ID をチェックします。
const allowed = new Set(['dev'])

const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    capabilities: {
      experimental: {
        'claude/channel': {},
        'claude/channel/permission': {},  // 権限リレーにオプトイン
      },
      tools: {},
    },
    instructions:
      'Messages arrive as <channel source="webhook" chat_id="...">. ' +
      'Reply with the reply tool, passing the chat_id from the tag.',
  },
)

// --- 返信ツール:Claude がメッセージを返送するために呼び出す ---
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Send a message back over this channel',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'The conversation to reply in' },
        text: { type: 'string', description: 'The message to send' },
      },
      required: ['chat_id', 'text'],
    },
  }],
}))

mcp.setRequestHandler(CallToolRequestSchema, async req => {
  if (req.params.name === 'reply') {
    const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
    send(`Reply to ${chat_id}: ${text}`)
    return { content: [{ type: 'text', text: 'sent' }] }
  }
  throw new Error(`unknown tool: ${req.params.name}`)
})

// --- 権限リレー:ダイアログが開くと Claude Code(Claude ではない)がこれを呼び出す
const PermissionRequestSchema = z.object({
  method: z.literal('notifications/claude/channel/permission_request'),
  params: z.object({
    request_id: z.string(),
    tool_name: z.string(),
    description: z.string(),
    input_preview: z.string(),
  }),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  send(
    `Claude wants to run ${params.tool_name}: ${params.description}\n\n` +
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})

await mcp.connect(new StdioServerTransport())

// --- HTTP 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
2 番目では、アウトバウンド側をストリーミングして、Claude の返信と権限プロンプトがライブで到着するのを見ることができます:
curl -N localhost:8788/events
3 番目では、Claude がコマンドを実行しようとするメッセージを送信します:
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788
ローカル権限ダイアログが Claude Code ターミナルで開きます。少し後、プロンプトが /events ストリームに表示され、5 文字の ID を含みます。リモート側から承認します:
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788
ローカルダイアログが閉じ、ツールが実行されます。Claude の返信は reply ツール経由で戻り、ストリームにも到着します。 このファイルの 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 が必要です。追加されるようにするには、公式マーケットプレイスに提出してください。チャネルプラグインは承認される前にセキュリティレビューを受けます。Team および Enterprise プランでは、管理者は代わりにプラグインを組織の独自の allowedChannelPlugins リストに含めることができます。これはデフォルトの Anthropic 許可リストを置き換えます。

関連項目

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