ãã£ãã«ãªãã¡ã¬ã³ã¹
webhookãã¢ã©ãŒãããã£ããã¡ãã»ãŒãžã Claude Code ã»ãã·ã§ã³ã«ããã·ã¥ãã MCP ãµãŒããŒãæ§ç¯ããŸãããã£ãã«ã³ã³ãã©ã¯ãã®ãªãã¡ã¬ã³ã¹ïŒæ©èœå®£èšãéç¥ã€ãã³ããè¿ä¿¡ããŒã«ãéä¿¡è ã²ãŒãã£ã³ã°ãæš©éãªã¬ãŒã
ãã£ãã«ã¯ãªãµãŒããã¬ãã¥ãŒæ®µéã«ãããŸããTeam ããã³ Enterprise çµç¹ã¯æç€ºçã«æå¹åããå¿ èŠããããŸãã
ãã£ãã«ã¯ã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 ãã©ã³ã¹ããŒãçµç±ã§æ¥ç¶ããïŒClaude Code ã¯ãµãŒããŒããµãããã»ã¹ãšããŠçæïŒ
ãµãŒããŒãªãã·ã§ã³ãšéç¥ãã©ãŒãããã»ã¯ã·ã§ã³ã§ã¯ããããã®ããããã«ã€ããŠè©³ãã説æããŠããŸããå®å šãªãŠã©ãŒã¯ã¹ã«ãŒã«ã€ããŠã¯ãäŸïŒwebhook ã¬ã·ãŒããŒãæ§ç¯ãåç §ããŠãã ããã
ãªãµãŒããã¬ãã¥ãŒäžãã«ã¹ã¿ã ãã£ãã«ã¯æ¿èªèš±å¯ãªã¹ãã«ãããŸãããããŒã«ã«ã§ãã¹ãããã«ã¯ --dangerously-load-development-channels ã䜿çšããŠãã ããã詳现ã«ã€ããŠã¯ããªãµãŒããã¬ãã¥ãŒäžã®ãã¹ããåç
§ããŠãã ããã
äŸïŒwebhook ã¬ã·ãŒããŒãæ§ç¯
ãã®ãŠã©ãŒã¯ã¹ã«ãŒã§ã¯ãHTTP ãªã¯ãšã¹ãããªãã¹ã³ã㊠Claude Code ã»ãã·ã§ã³ã«è»¢éããåäžãã¡ã€ã«ãµãŒããŒãæ§ç¯ããŸããçµäºæã«ã¯ãCI ãã€ãã©ã€ã³ãç£èŠã¢ã©ãŒãããŸã㯠curl ã³ãã³ããªã©ãHTTP POST ãéä¿¡ã§ãããã®ã¯ãã¹ãŠãClaude ã«ã€ãã³ããããã·ã¥ã§ããŸãã
ãã®äŸã§ã¯ãçµã¿èŸŒã¿ HTTP ãµãŒããŒãš TypeScript ãµããŒãã®ããã« Bun ãã©ã³ã¿ã€ã ãšããŠäœ¿çšããŠããŸãã代ããã« Node ãŸã㯠Deno ã䜿çšã§ããŸããå¯äžã®èŠä»¶ã¯ MCP SDK ã§ãã
ãããžã§ã¯ããäœæ
æ°ãããã£ã¬ã¯ããªãäœæã㊠MCP SDK ãã€ã³ã¹ããŒã«ããŸãïŒ
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk
ãã£ãã«ãµãŒããŒãèšè¿°
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 ã®ã·ã¹ãã ããã³ããã«è¿œå ãããããããããã®ã€ãã³ãã®åŠçæ¹æ³ãç¥ã£ãŠãã
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 ã«æåŸ ããã€ãã³ããè¿ä¿¡ãããã©ãããè¿ä¿¡ããå Žåã¯ã©ã®ããã«è¿ä¿¡ãåŠçããããäŒããŸãã - Stdio æ¥ç¶ïŒstdin/stdout çµç±ã§ Claude Code ã«æ¥ç¶ããŸããããã¯ä»»æã® MCP ãµãŒããŒ ã®æšæºã§ãïŒClaude Code ã¯ããããµãããã»ã¹ãšããŠçæããŸãã
- 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 㯠.mcp.json ããæ°ãããµãŒããŒã䜿çšããåã«åæãæ±ããŸãããã€ã¢ãã°ã¯'ãã®ãããžã§ã¯ãã§èŠã€ãã£ãæ°ãã MCP ãµãŒããŒïŒwebhook'ãšå ±åããŸãããã®MCPãµãŒããŒãäœ¿çš ãéžæããŠç¶è¡ããŸãã
Claude Code ãèµ·åãããšãMCP èšå®ãèªã¿èŸŒã¿ã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 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 ã³ã³ã¹ãã©ã¯ã¿ã§ãããã®ãªãã·ã§ã³ãèšå®ããŸããinstructions ãš capabilities.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.tools |
object |
åæ¹åã®ã¿ãåžžã« {}ãæšæº MCP ããŒã«æ©èœãè¿ä¿¡ããŒã«ãå
¬éãåç
§ããŠãã ããã |
instructions |
string |
æšå¥šã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/channel ã§ mcp.notification() ãåŒã³åºããŸãããã©ã¡ãŒã¿ã¯æ¬¡ã®ã»ã¯ã·ã§ã³ã«ãããŸãã
éç¥ãã©ãŒããã
ãµãŒããŒã¯ 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>
éç¥ã¯ç¢ºèªãããŸãããmcp.notification() ã® await ã¯ãã¡ãã»ãŒãžããã©ã³ã¹ããŒãã«æžã蟌ãŸãããšãã«è§£æ±ºãããClaude ãåŠçãããšãã§ã¯ãããŸãããã»ãã·ã§ã³ããã£ãã«ãšããŠãµãŒããŒãèªã¿èŸŒãã§ããªãå ŽåããŸãã¯çµç¹ããªã·ãŒãããããããã¯ããŠããå Žåãã€ãã³ãã¯ãµãŒããŒã«ãšã©ãŒãè¿ãããããšãªããµã€ã¬ã³ãã«ãããããããŸãã
é 信確èªãå¿ èŠãªå Žåã¯ããµãŒããŒã§ã€ãã³ãç¶æ ã远跡ãã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 ãã£ãã«ã¯åãæ¹æ³ã§éä¿¡è èš±å¯ãªã¹ãã§ã²ãŒãããŸãããã¢ãªã³ã°ã§ãªã¹ããããŒãã¹ãã©ããããŸãïŒãŠãŒã¶ãŒããããã« DM ãéä¿¡ãããããã¯ãã¢ãªã³ã°ã³ãŒãã§è¿ä¿¡ãããŠãŒã¶ãŒã Claude Code ã»ãã·ã§ã³ã§æ¿èªãããã©ãããã©ãŒã ID ã远å ãããŸããå®å šãªãã¢ãªã³ã°ãããŒã«ã€ããŠã¯ãããããã®å®è£ ãåç §ããŠãã ãããiMessage ãã£ãã«ã¯ç°ãªãã¢ãããŒããåããŸãïŒèµ·åæã«ã¡ãã»ãŒãžããŒã¿ããŒã¹ãããŠãŒã¶ãŒèªèº«ã®ã¢ãã¬ã¹ãæ€åºããããããèªåçã«éããŸããä»ã®éä¿¡è ã¯ãã³ãã«ã§è¿œå ãããŸãã
æš©éããã³ããããªã¬ãŒ
Claude ãæ¿èªãå¿ èŠãªããŒã«ãåŒã³åºããšãããŒã«ã«ã¿ãŒããã«ãã€ã¢ãã°ãéããã»ãã·ã§ã³ãåŸ æ©ããŸããåæ¹åãã£ãã«ã¯ãåãããã³ããã䞊è¡ããŠåãåããå¥ã®ããã€ã¹ã§ããããªã¬ãŒããããšãéžæã§ããŸããäž¡æ¹ãã©ã€ãã®ãŸãŸã§ãïŒã¿ãŒããã«ãŸãã¯é»è©±ã§çããããšãã§ããClaude Code ã¯æåã«å°çããçããé©çšããããäžæ¹ãéããŸãã
ãªã¬ãŒã¯ BashãWriteãEdit ãªã©ã®ããŒã«äœ¿çšæ¿èªãã«ããŒããŸãããããžã§ã¯ãä¿¡é Œãš MCP ãµãŒããŒåæãã€ã¢ãã°ã¯ãªã¬ãŒãããŸããããããã¯ããŒã«ã«ã¿ãŒããã«ã«ã®ã¿è¡šç€ºãããŸãã
ãªã¬ãŒã®ä»çµã¿
æš©éããã³ãããéããšããªã¬ãŒã«ãŒãã«ã¯ 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 ã®ã³ãã³ãã®èª¬æããŸãã¯äœãäžããããŠããªãå Žåã¯ã³ãã³ãèªäœã§ãã |
input_preview |
ããŒã«ã®åŒæ°ã JSON æååãšããŠã200 æåã«åãè©°ãããã®ãBash ã®å Žåã¯ã³ãã³ããWrite ã®å Žåã¯ãã¡ã€ã«ãã¹ãšã³ã³ãã³ãã®ãã¬ãã£ãã¯ã¹ã1 è¡ã®ã¡ãã»ãŒãžã®äœå°ãããªãå Žåã¯ããã³ããããçç¥ããŸãããµãŒããŒã¯äœã衚瀺ããããæ±ºå®ããŸãã |
ãµãŒããŒãè¿éããå€å®ã¯ 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 ãšããŠãçŽ 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}"`,
)
})
ã€ã³ããŠã³ããã³ãã©ãŒã§å€å®ãã€ã³ã¿ãŒã»ãã
ã€ã³ããŠã³ããã³ãã©ãŒã¯ããã©ãããã©ãŒã ããã¡ãã»ãŒãžãåãåãã«ãŒããŸãã¯ã³ãŒã«ããã¯ïŒéä¿¡è
ã§ã²ãŒããã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 /ïŒã€ã³ããŠã³ãåŽã以åãšåããã³ãã©ãŒããã£ãã転éãã©ã³ãã®åã«å€å®ãã©ãŒããããã§ãã¯ãæ¿å ¥ãããŸããã
#!/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 ã¯æ¿èªãªãã§ãããå®è¡ããŸããæš©éãã€ã¢ãã°ã¯ 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 èš±å¯ãªã¹ãã眮ãæããŸãã
é¢é£é ç®
- ãã£ãã«ïŒTelegramãDiscordãiMessageããŸã㯠fakechat ãã¢ãã€ã³ã¹ããŒã«ããŠäœ¿çšããTeam ãŸã㯠Enterprise çµç¹ã®ãã£ãã«ãæå¹å
- ãã£ãã«å®è£ ã®åäœïŒãã¢ãªã³ã°ãããŒãè¿ä¿¡ããŒã«ããã¡ã€ã«æ·»ä»ãå«ãå®å šãªãµãŒããŒã³ãŒã
- MCPïŒãã£ãã«ãµãŒããŒãå®è£ ããåºç€ãšãªããããã³ã«
- ãã©ã°ã€ã³ïŒãã£ãã«ãããã±ãŒãžåããŠããŠãŒã¶ãŒã
/plugin installã§ã€ã³ã¹ããŒã«ã§ããããã«ãã