Claude Code ChannelsでWebhook受信サーバーを自作する
Claude CodeのChannelsはMCPサーバーとして自作できます。HTTPのPOSTをstdio経由でセッションに届ける最小構成から、送信元をゲートする設計までを手順で追います。
この記事で作るもの
Claude Code Channelsは、ターミナルの外で起きた出来事をセッションに直接届ける研究プレビュー機能です。TelegramやDiscordのような既存プラグインを使う以外に、自分でMCPサーバーを書いてチャンネルにすることもできます。この記事では、CIの失敗通知や監視アラートのようなHTTPのPOSTを受け取り、Claudeのコンテキストへそのまま流し込む最小構成のwebhook受信サーバーを作ります。
作るのは一方向のチャンネルです。外部システムがPOSTを送るとClaudeがそれを読んで反応しますが、Claude側から呼び出し元へ返信する経路はまだ持ちません。双方向にする場合の考え方は後半で触れます。
前提条件
必要なのは@modelcontextprotocol/sdkパッケージと、Node.js互換のランタイムだけです。Bun・Node・Denoのどれでも動きます。この記事では組み込みのHTTPサーバーとTypeScriptサポートを持つBunを使います。
MCPサーバーをゼロから自作する手順を先に押さえておくと、通常のツール定義とチャンネル固有の宣言との違いが見えやすくなります。チャンネル固有の宣言というのは、この後で書くcapabilities.experimentalの中身を指します。
研究プレビュー期間中、自作チャンネルは承認済みの許可リストに載っていません。ローカルで試すには--dangerously-load-development-channelsフラグが必要です。
手順1: プロジェクトを作る
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk zodzodは必須ではありませんが、後半で返信ツールや権限中継を試す際にそのまま使えるよう一緒に入れておきます。
手順2: チャンネルサーバーを書く
webhook.tsという1ファイルがサーバーの全体です。Claude Codeとstdio経由で接続し、ポート8788でHTTPのPOSTを待ち受けます。
#!/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.',
},
)
// Claude Codeがこのプロセスをspawnし、stdioで接続する
await mcp.connect(new StdioServerTransport())
// すべてのPOSTをClaudeへ転送するHTTPサーバー
Bun.serve({
port: 8788,
hostname: '127.0.0.1', // localhostのみ。外部からは届かない
async fetch(req) {
const body = await req.text()
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: body, // <channel>タグの本文になる
meta: { path: new URL(req.url).pathname, method: req.method }, // 各キーがタグの属性になる
},
})
return new Response('ok')
},
})このファイルがやっていることは3つです。capabilities.experimental['claude/channel']を宣言してChannelsとして登録されるようにする。StdioServerTransportでClaude Codeとの標準入出力接続を確立する。そしてHTTPリスナーが受けたPOSTボディをmcp.notification()でそのままClaudeへ転送する。contentが<channel>タグの本文に、metaの各キーがタグの属性になります。
手順3: MCP設定に登録する
プロジェクト直下の.mcp.jsonに相対パスで登録します(~/.claude.jsonに置く場合は絶対パスにします)。
{
"mcpServers": {
"webhook": { "command": "bun", "args": ["./webhook.ts"] }
}
}Claude Codeは起動時にこの設定を読み、各サーバーをサブプロセスとして立ち上げます。
手順4: 開発用フラグで起動して確認する
研究プレビュー中の自作チャンネルは許可リスト外なので、開発用フラグを付けて起動します。
claude --dangerously-load-development-channels server:webhook読み込む開発用チャンネルを列挙する全画面警告が出るので、ローカル開発用であることを確認して進みます。プロジェクトで初めてこのMCPサーバーを起動するセッションでは、「New MCP server found in this project: webhook」という同意ダイアログも出るので、そちらも承認します。承認後はClaude Codeがwebhook.tsをサブプロセスとして起動し、設定したポートでHTTPリスナーも自動的に立ち上がります。自分でサーバーを起動する必要はありません。
起動バナーの下に「Channels (experimental) messages from server:webhook inject directly in this session」という控えめな表示が出ればチャンネルとして登録できています。別のターミナルからPOSTを送って動作を確認します。
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"Claudeのコンテキストには次の形で届きます。
<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel>ターミナル上は生のタグではなく「← webhook: build failed on main: ...」という1行の要約で表示され、続けてClaudeがファイルを読んだりコマンドを実行したりし始めます。一方向のチャンネルなので、この時点ではClaudeからWebhook側への返信は発生しません。
受信メッセージは必ずゲートしてから公開する
このままのコードは学習用の最小構成であり、送信元を一切確認していません。ゲートしていないチャンネルはプロンプトインジェクションの入口そのものです。あなたのエンドポイントに届く相手なら誰でも、Claudeの目の前にテキストを差し込めます。
公開する前に、送信元IDを許可リストと照合してからmcp.notification()を呼ぶようにします。判定に使うのはチャットやルームのIDではなく、送信者本人のIDです。グループチャットではこの2つが一致しないため、ルーム側で許可すると許可していない相手のメッセージまで通ってしまいます。
const allowed = new Set(loadAllowlist()) // access.jsonなどから読み込む
// mcp.notification()を呼ぶ前に必ず確認する
if (!allowed.has(message.from.id)) { // ルームIDではなく送信者本人のID
return // 許可リスト外は黙って捨てる
}
await mcp.notification({ /* ... */ })TelegramやDiscordの公式チャンネルプラグインも、この送信者許可リストを同じ発想でチェックしています。初回はチャットアプリ側からペアリングコードを送らせ、Claude Codeのセッション側でそのコードを承認することで送信者IDを許可リストに登録する、という手順を踏んでいます。自作のWebhookチャンネルでも、HTTPヘッダーの署名検証やAPIキーなど、送信元を一意に特定できる仕組みを組み合わせて同じ考え方を適用します。
一方向から双方向にするには
Claude側からメッセージを送り返したい場合は、capabilities.toolsに{}を追加してツール探索を有効にし、replyのような標準的なMCPツールを登録します。手を加えるのはServerコンストラクタのcapabilitiesと、ツール一覧・呼び出しのハンドラー、そしてinstructionsの3か所です。instructionsには、受信した<channel>タグのどの属性(chat_idなど)をreplyツールに渡し返すかを書き添え、Claudeが送り主を取り違えないようにします。
よくあるつまずき
curlは成功したのにClaudeに何も届かない場合、セッション内で/mcpを実行してサーバーのステータスを確認します。failedと出ていれば大抵はサーバーファイル内の依存関係やimportエラーです。MCPサーバーが接続できないときの切り分け方は他のMCPサーバーとも共通するので、~/.claude/debug/<session-id>.txtのデバッグログと合わせて確認すると原因を絞りやすくなります。
curlが「connection refused」で失敗する場合、ポートがまだbindされていないか、前回起動時のプロセスがポートを掴んだまま残っています。lsof -i :<port>で何がリッスンしているか確認し、古いプロセスをkillしてからセッションを立て直します。
「blocked by org policy」という表示が出た場合は、組織の管理者がChannelsをまだ有効にしていません。claude.aiのTeam・Enterpriseプランでは、Ownerロールの管理者が有効化するまでChannelsはブロックされたままです。個人のPro・Maxプランではこの組織側の確認自体が発生せず、--channelsでセッションごとに有効化するだけで使えます。
まとめ
Channelsの自作サーバーは、claude/channelをcapabilitiesに宣言したMCPサーバーを書き、stdioでClaude Codeと接続し、外部から受けたイベントをmcp.notification()で転送するという3点に集約されます。研究プレビュー中は--dangerously-load-development-channelsで許可リストを迂回してテストし、公開前には送信元IDを必ずゲートします。双方向にするための返信ツールや、権限確認プロンプトのチャット側への中継は、この最小構成の先にある別の実装領域です。
よくある質問
チャンネルを作るのにMCPプロトコルの深い知識は必要ですか
最小構成であれば、capabilities.experimental['claude/channel']を宣言してstdioで接続し、mcp.notification()でイベントを送るだけで動きます。標準的なMCPサーバーの構築知識があれば十分です。
Bun以外のランタイムでも作れますか
作れます。必要なのは@modelcontextprotocol/sdkパッケージだけで、NodeでもDenoでも動きます。この記事ではHTTPサーバーが組み込みのBunを使っていますが、Node.jsのhttpモジュールやDenoのDeno.serveに置き換えても仕組みは同じです。
自作したチャンネルは他の人にも配布できますか
プラグインとして包んでマーケットプレイスに公開すれば、/plugin installで誰でも導入できるようになります。ただし研究プレビュー中は、自分のマーケットプレイスに公開したチャンネルも承認済み許可リストには載らないため、利用者側は引き続き--dangerously-load-development-channelsが必要です。Team・Enterpriseプランであれば、管理者がallowedChannelPluginsに自社のマーケットプレイスとプラグイン名を登録することで、開発フラグなしで使えるようにもできます。
送信元をゲートしないまま動かすとどうなりますか
エンドポイントに到達できる相手なら誰でもClaudeにテキストを差し込めてしまいます。学習目的でローカルのlocalhostだけに公開している間は実害が出にくいですが、外部に公開する前には送信者IDの照合を必ず追加します。判定に使うIDはチャットやルームの識別子ではなく、送信者本人を一意に特定できる値にします。
組織のClaude CodeでChannelsを使うには管理者の許可が必要ですか
claude.aiのTeam・Enterpriseプランでは必要です。Ownerロールの管理者がChannelsを有効化するまで、開発フラグを含むすべてのチャンネルがブロックされます。個人のPro・Maxプランではこの組織側のチェック自体が発生せず、セッションごとに--channelsか--dangerously-load-development-channelsを付けるだけで利用できます。