SpyBara
Go Premium

channels-reference.md 2026-05-02 18:14 UTC to 2026-05-04 22:58 UTC

749 added, 0 removed.

2026
Sun 31 06:39 Sat 30 06:23 Fri 29 06:38 Thu 28 06:37 Wed 27 06:42 Tue 26 06:33 Sun 24 06:25 Sat 23 06:18 Fri 22 06:33 Thu 21 06:36 Wed 20 06:35 Tue 19 06:34 Mon 18 23:59 Sun 17 01:01 Fri 15 22:58 Thu 14 17:02 Wed 13 23:01 Tue 12 22:57 Mon 11 23:00 Sun 10 23:03 Sat 9 04:57 Fri 8 22:00 Thu 7 22:59 Tue 5 23:00 Mon 4 22:58 Sat 2 18:14 Fri 1 18:19

์ฑ„๋„ ์ฐธ์กฐ

์›นํ›…, ์•Œ๋ฆผ, ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ Claude Code ์„ธ์…˜์œผ๋กœ ํ‘ธ์‹œํ•˜๋Š” MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. ์ฑ„๋„ ๊ณ„์•ฝ ์ฐธ์กฐ: ๊ธฐ๋Šฅ ์„ ์–ธ, ์•Œ๋ฆผ ์ด๋ฒคํŠธ, ํšŒ์‹  ๋„๊ตฌ, ๋ฐœ์‹ ์ž ๊ฒŒ์ดํŒ…, ๊ถŒํ•œ ๋ฆด๋ ˆ์ด.

์ฑ„๋„์€ Claude Code ์„ธ์…˜์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ํ‘ธ์‹œํ•˜๋Š” MCP ์„œ๋ฒ„์ด๋ฏ€๋กœ Claude๋Š” ํ„ฐ๋ฏธ๋„ ์™ธ๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ผ์— ๋ฐ˜์‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ๋ฐฉํ–ฅ ๋˜๋Š” ์–‘๋ฐฉํ–ฅ ์ฑ„๋„์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ๋ฐฉํ–ฅ ์ฑ„๋„์€ Claude๊ฐ€ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ์•Œ๋ฆผ, ์›นํ›… ๋˜๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์ฑ„ํŒ… ๋ธŒ๋ฆฌ์ง€์™€ ๊ฐ™์€ ์–‘๋ฐฉํ–ฅ ์ฑ„๋„์€ Claude๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ค์‹œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ํšŒ์‹  ๋„๊ตฌ๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐœ์‹ ์ž ๊ฒฝ๋กœ๊ฐ€ ์žˆ๋Š” ์ฑ„๋„์€ ๊ถŒํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฆด๋ ˆ์ดํ•˜๋„๋ก ์„ ํƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์›๊ฒฉ์œผ๋กœ ๋„๊ตฌ ์‚ฌ์šฉ์„ ์Šน์ธํ•˜๊ฑฐ๋‚˜ ๊ฑฐ๋ถ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ:

๊ธฐ์กด ์ฑ„๋„์„ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹  ๊ตฌ์ถ•ํ•˜๋ ค๋ฉด ์ฑ„๋„์„ ์ฐธ์กฐํ•˜์„ธ์š”. Telegram, Discord, iMessage ๋ฐ fakechat์€ ์—ฐ๊ตฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์š”

์ฑ„๋„์€ Claude Code์™€ ๋™์ผํ•œ ๋จธ์‹ ์—์„œ ์‹คํ–‰๋˜๋Š” MCP ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค. Claude Code๋Š” ์ด๋ฅผ ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋กœ ์ƒ์„ฑํ•˜๊ณ  stdio๋ฅผ ํ†ตํ•ด ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ฑ„๋„ ์„œ๋ฒ„๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ Claude Code ์„ธ์…˜ ๊ฐ„์˜ ๋ธŒ๋ฆฌ์ง€์ž…๋‹ˆ๋‹ค:

  • ์ฑ„ํŒ… ํ”Œ๋žซํผ (Telegram, Discord): ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๋กœ์ปฌ์—์„œ ์‹คํ–‰๋˜๊ณ  ํ”Œ๋žซํผ์˜ API๋ฅผ ํด๋งํ•˜์—ฌ ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€ ๋ด‡์— DM์„ ๋ณด๋‚ด๋ฉด ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  Claude๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋…ธ์ถœํ•  URL์ด ์—†์Šต๋‹ˆ๋‹ค.
  • ์›นํ›… (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 ์ „์†ก์„ ํ†ตํ•ด ์—ฐ๊ฒฐ (Claude Code๊ฐ€ ์„œ๋ฒ„๋ฅผ ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋กœ ์ƒ์„ฑ)

์„œ๋ฒ„ ์˜ต์…˜ ๋ฐ ์•Œ๋ฆผ ํ˜•์‹ ์„น์…˜์—์„œ ๊ฐ๊ฐ์„ ์ž์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์ „์ฒด ์—ฐ์Šต์€ ์˜ˆ: ์›นํ›… ์ˆ˜์‹ ๊ธฐ ๊ตฌ์ถ•์„ ์ฐธ์กฐํ•˜์„ธ์š”.

์—ฐ๊ตฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘์— ์‚ฌ์šฉ์ž ์ •์˜ ์ฑ„๋„์€ ์Šน์ธ๋œ ํ—ˆ์šฉ ๋ชฉ๋ก์— ์—†์Šต๋‹ˆ๋‹ค. --dangerously-load-development-channels๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ์ปฌ์—์„œ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์—ฐ๊ตฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘ ํ…Œ์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

์˜ˆ: ์›นํ›… ์ˆ˜์‹ ๊ธฐ ๊ตฌ์ถ•

์ด ์—ฐ์Šต์€ 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๋กœ ํ‘ธ์‹œํ•ฉ๋‹ˆ๋‹ค.

#!/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')
},
})

ํŒŒ์ผ์€ ์ˆœ์„œ๋Œ€๋กœ ์„ธ ๊ฐ€์ง€๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค:

  • ์„œ๋ฒ„ ๊ตฌ์„ฑ: ๊ธฐ๋Šฅ์— claude/channel์ด ์žˆ๋Š” MCP ์„œ๋ฒ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด Claude Code์— ์ด๊ฒƒ์ด ์ฑ„๋„์ž„์„ ์•Œ๋ ค์ค๋‹ˆ๋‹ค. instructions ๋ฌธ์ž์—ด์€ Claude์˜ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค: Claude์— ์˜ˆ์ƒํ•  ์ด๋ฒคํŠธ, ํšŒ์‹  ์—ฌ๋ถ€, ํšŒ์‹ ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ๋„๊ตฌ ๋ฐ ์ „๋‹ฌํ•  ์†์„ฑ(์˜ˆ: chat_id)์„ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
  • Stdio ์—ฐ๊ฒฐ: stdin/stdout์„ ํ†ตํ•ด Claude Code์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ชจ๋“  MCP ์„œ๋ฒ„์— ํ‘œ์ค€์ž…๋‹ˆ๋‹ค: Claude Code๊ฐ€ ์ด๋ฅผ ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • HTTP ๋ฆฌ์Šค๋„ˆ: ํฌํŠธ 8788์—์„œ ๋กœ์ปฌ ์›น ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  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๊ฐ€ ์‹œ์ž‘๋˜๋ฉด MCP ๊ตฌ์„ฑ์„ ์ฝ๊ณ  webhook.ts๋ฅผ ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋กœ ์ƒ์„ฑํ•˜๋ฉฐ ๊ตฌ์„ฑํ•œ ํฌํŠธ(์ด ์˜ˆ์ œ์—์„œ๋Š” 8788)์—์„œ HTTP ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์ž๋™์œผ๋กœ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

"์กฐ์ง ์ •์ฑ…์— ์˜ํ•ด ์ฐจ๋‹จ๋จ"์ด ํ‘œ์‹œ๋˜๋ฉด ํŒ€ ๋˜๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๊ด€๋ฆฌ์ž๊ฐ€ ๋จผ์ € ์ฑ„๋„์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ณ„๋„์˜ ํ„ฐ๋ฏธ๋„์—์„œ HTTP POST๋ฅผ ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด ์›นํ›…์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ๋Š” 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๋Š” ์„ธ์…˜์—์„œ ์ž‘๋™ํ•˜์ง€๋งŒ ์›นํ›…์„ ํ†ตํ•ด ์•„๋ฌด๊ฒƒ๋„ ๋‹ค์‹œ ๋ณด๋‚ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํšŒ์‹ ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ํšŒ์‹  ๋„๊ตฌ ๋…ธ์ถœ์„ ์ฐธ์กฐํ•˜์„ธ์š”.

์ด๋ฒคํŠธ๊ฐ€ ๋„์ฐฉํ•˜์ง€ ์•Š์œผ๋ฉด ์ง„๋‹จ์€ curl์ด ๋ฐ˜ํ™˜ํ•œ ๊ฒƒ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค:

  • curl์€ ์„ฑ๊ณตํ•˜์ง€๋งŒ Claude์— ๋„๋‹ฌํ•˜์ง€ ์•Š์Œ: ์„ธ์…˜์—์„œ /mcp๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์„œ๋ฒ„์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. "์—ฐ๊ฒฐ ์‹คํŒจ"๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์„œ๋ฒ„ ํŒŒ์ผ์˜ ์ข…์†์„ฑ ๋˜๋Š” ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ~/.claude/debug/<session-id>.txt์˜ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ์—์„œ stderr ์ถ”์ ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • curl์ด "์—ฐ๊ฒฐ ๊ฑฐ๋ถ€"๋กœ ์‹คํŒจํ•จ: ํฌํŠธ๊ฐ€ ์•„์ง ๋ฐ”์ธ๋”ฉ๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์ด์ „ ์‹คํ–‰์˜ ์˜ค๋ž˜๋œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ํฌํŠธ๋ฅผ ๋ณด์œ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. lsof -i :<port>๋Š” ์ˆ˜์‹  ์ค‘์ธ ๊ฒƒ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ์˜ค๋ž˜๋œ ํ”„๋กœ์„ธ์Šค๋ฅผ killํ•ฉ๋‹ˆ๋‹ค.

fakechat ์„œ๋ฒ„๋Š” ์›น UI, ํŒŒ์ผ ์ฒจ๋ถ€ ๋ฐ ์–‘๋ฐฉํ–ฅ ์ฑ„ํŒ…์„ ์œ„ํ•œ ํšŒ์‹  ๋„๊ตฌ๋กœ ์ด ํŒจํ„ด์„ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.

์—ฐ๊ตฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘ ํ…Œ์ŠคํŠธ

์—ฐ๊ตฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘์— ๋ชจ๋“  ์ฑ„๋„์€ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•ด ์Šน์ธ๋œ ํ—ˆ์šฉ ๋ชฉ๋ก์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ํ”Œ๋ž˜๊ทธ๋Š” ํ™•์ธ ํ”„๋กฌํ”„ํŠธ ํ›„ ํŠน์ • ํ•ญ๋ชฉ์— ๋Œ€ํ•œ ํ—ˆ์šฉ ๋ชฉ๋ก์„ ์šฐํšŒํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ๋Š” ๋‘ ํ•ญ๋ชฉ ์œ ํ˜•์„ ๋ชจ๋‘ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

# ๊ฐœ๋ฐœ ์ค‘์ธ ํ”Œ๋Ÿฌ๊ทธ์ธ ํ…Œ์ŠคํŠธ
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace

# ๋ฒ ์–ด .mcp.json ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ (์•„์ง ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ž˜ํผ ์—†์Œ)
claude --dangerously-load-development-channels server:webhook

์šฐํšŒ๋Š” ํ•ญ๋ชฉ๋ณ„์ž…๋‹ˆ๋‹ค. ์ด ํ”Œ๋ž˜๊ทธ๋ฅผ --channels์™€ ๊ฒฐํ•ฉํ•˜๋ฉด ์šฐํšŒ๊ฐ€ --channels ํ•ญ๋ชฉ์œผ๋กœ ํ™•์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—ฐ๊ตฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘์— ์Šน์ธ๋œ ํ—ˆ์šฉ ๋ชฉ๋ก์€ Anthropic์—์„œ ํ๋ ˆ์ด์…˜๋˜๋ฏ€๋กœ ์ฑ„๋„์€ ๊ตฌ์ถ• ๋ฐ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋™์•ˆ ๊ฐœ๋ฐœ ํ”Œ๋ž˜๊ทธ์— ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์˜ต์…˜

์ฑ„๋„์€ 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์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค:

ํ•„๋“œ ์œ ํ˜• ์„ค๋ช…
content string ์ด๋ฒคํŠธ ๋ณธ๋ฌธ. <channel> ํƒœ๊ทธ์˜ ๋ณธ๋ฌธ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.
meta Record<string, string> ์„ ํƒ ์‚ฌํ•ญ. ๊ฐ ํ•ญ๋ชฉ์€ ์ฑ„ํŒ… ID, ๋ฐœ์‹ ์ž ์ด๋ฆ„ ๋˜๋Š” ์•Œ๋ฆผ ์‹ฌ๊ฐ๋„์™€ ๊ฐ™์€ ๋ผ์šฐํŒ… ์ปจํ…์ŠคํŠธ๋ฅผ ์œ„ํ•ด <channel> ํƒœ๊ทธ์˜ ์†์„ฑ์ด ๋ฉ๋‹ˆ๋‹ค. ํ‚ค๋Š” ์‹๋ณ„์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: ๋ฌธ์ž, ์ˆซ์ž ๋ฐ ๋ฐ‘์ค„๋งŒ. ํ•˜์ดํ”ˆ ๋˜๋Š” ๋‹ค๋ฅธ ๋ฌธ์ž๋ฅผ ํฌํ•จํ•˜๋Š” ํ‚ค๋Š” ์ž๋™์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.

์„œ๋ฒ„๋Š” Server ์ธ์Šคํ„ด์Šค์—์„œ mcp.notification()์„ ํ˜ธ์ถœํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ํ‘ธ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ๋Š” ๋‘ ๊ฐœ์˜ ๋ฉ”ํƒ€ ํ‚ค๊ฐ€ ์žˆ๋Š” 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' },
  },
})

์ด๋ฒคํŠธ๋Š” <channel> ํƒœ๊ทธ๋กœ ๋ž˜ํ•‘๋œ Claude์˜ ์ปจํ…์ŠคํŠธ์— ๋„์ฐฉํ•ฉ๋‹ˆ๋‹ค. source ์†์„ฑ์€ ์„œ๋ฒ„์˜ ๊ตฌ์„ฑ๋œ ์ด๋ฆ„์—์„œ ์ž๋™์œผ๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค:

<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>

ํšŒ์‹  ๋„๊ตฌ ๋…ธ์ถœ

์ฑ„๋„์ด ์–‘๋ฐฉํ–ฅ์ธ ๊ฒฝ์šฐ(์•Œ๋ฆผ ํฌ์›Œ๋”๊ฐ€ ์•„๋‹Œ ์ฑ„ํŒ… ๋ธŒ๋ฆฌ์ง€), Claude๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ค์‹œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ํ‘œ์ค€ MCP ๋„๊ตฌ๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋„๊ตฌ ๋“ฑ๋ก์— ๋Œ€ํ•ด ์ฑ„๋„ ํŠน์ • ์‚ฌํ•ญ์€ ์—†์Šต๋‹ˆ๋‹ค. ํšŒ์‹  ๋„๊ตฌ์—๋Š” ์„ธ ๊ฐ€์ง€ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. Server ์ƒ์„ฑ์ž ๊ธฐ๋Šฅ์˜ tools: {} ํ•ญ๋ชฉ์ด๋ฏ€๋กœ Claude Code๊ฐ€ ๋„๊ตฌ๋ฅผ ๋ฐœ๊ฒฌํ•ฉ๋‹ˆ๋‹ค
  2. ๋„๊ตฌ์˜ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๊ณ  ์ „์†ก ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๋„๊ตฌ ํ•ธ๋“ค๋Ÿฌ
  3. Claude์— ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•  ์‹œ๊ธฐ์™€ ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ฃผ๋Š” Server ์ƒ์„ฑ์ž์˜ instructions ๋ฌธ์ž์—ด

์œ„์˜ ์›นํ›… ์ˆ˜์‹ ๊ธฐ์— ์ด๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด:

1

๋„๊ตฌ ๋ฐœ๊ฒฌ ํ™œ์„ฑํ™”

webhook.ts์˜ Server ์ƒ์„ฑ์ž์—์„œ Claude Code๊ฐ€ ์„œ๋ฒ„๊ฐ€ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•จ์„ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ๋Šฅ์— tools: {}๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

capabilities: {
experimental: { 'claude/channel': {} },
tools: {},  // ๋„๊ตฌ ๋ฐœ๊ฒฌ์„ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค
},
2

ํšŒ์‹  ๋„๊ตฌ ๋“ฑ๋ก

๋‹ค์Œ์„ webhook.ts์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. import๋Š” ๋‹ค๋ฅธ ๊ฐ€์ ธ์˜ค๊ธฐ์™€ ํ•จ๊ป˜ ํŒŒ์ผ ๋งจ ์œ„๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ํ•ธ๋“ค๋Ÿฌ๋Š” 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: curl -N๊ฐ€ Claude์˜ ํšŒ์‹ ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก SSE ์ŠคํŠธ๋ฆผ
    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 ์„œ๋ฒ„ ๋™์˜ ๋Œ€ํ™”๋Š” ๋ฆด๋ ˆ์ด๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋“ค์€ ๋กœ์ปฌ ํ„ฐ๋ฏธ๋„์—๋งŒ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

๋ฆด๋ ˆ์ด ์ž‘๋™ ๋ฐฉ์‹

๊ถŒํ•œ ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์—ด๋ฆฌ๋ฉด ๋ฆด๋ ˆ์ด ๋ฃจํ”„์—๋Š” ๋„ค ๊ฐ€์ง€ ๋‹จ๊ณ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. Claude Code๋Š” ์งง์€ ์š”์ฒญ ID๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์„œ๋ฒ„์— ์•Œ๋ฆฝ๋‹ˆ๋‹ค
  2. ์„œ๋ฒ„๋Š” ํ”„๋กฌํ”„ํŠธ ๋ฐ ID๋ฅผ ์ฑ„ํŒ… ์•ฑ์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค
  3. ์›๊ฒฉ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ ๋˜๋Š” ์•„๋‹ˆ์˜ค๋กœ ํ•ด๋‹น ID๋กœ ํšŒ์‹ ํ•ฉ๋‹ˆ๋‹ค
  4. ์ธ๋ฐ”์šด๋“œ ํ•ธ๋“ค๋Ÿฌ๋Š” ํšŒ์‹ ์„ ํŒ์ •์œผ๋กœ ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜๊ณ  Claude Code๋Š” ID๊ฐ€ ์—ด๋ฆฐ ์š”์ฒญ๊ณผ ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค

๋กœ์ปฌ ํ„ฐ๋ฏธ๋„ ๋Œ€ํ™”๋Š” ์ด ๋ชจ๋“  ๊ณผ์ •์„ ํ†ตํ•ด ์—ด๋ ค ์žˆ์Šต๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„์˜ ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ์›๊ฒฉ ํŒ์ •์ด ๋„์ฐฉํ•˜๊ธฐ ์ „์— ๋‹ต๋ณ€ํ•˜๋ฉด ํ•ด๋‹น ๋‹ต๋ณ€์ด ๋Œ€์‹  ์ ์šฉ๋˜๊ณ  ๋ณด๋ฅ˜ ์ค‘์ธ ์›๊ฒฉ ์š”์ฒญ์ด ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ: Claude Code๊ฐ€ permission_request ์•Œ๋ฆผ์„ ์ฑ„๋„ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๊ณ , ์„œ๋ฒ„๊ฐ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ฑ„ํŒ… ์•ฑ์œผ๋กœ ํฌ๋งทํ•˜๊ณ  ๋ณด๋‚ด๋ฉฐ, ์ธ๊ฐ„์ด ํŒ์ •์œผ๋กœ ํšŒ์‹ ํ•˜๊ณ , ์„œ๋ฒ„๊ฐ€ ํ•ด๋‹น ํšŒ์‹ ์„ Claude Code๋กœ ๋‹ค์‹œ ๊ถŒํ•œ ์•Œ๋ฆผ์œผ๋กœ ๊ตฌ๋ฌธ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค

๊ถŒํ•œ ์š”์ฒญ ํ•„๋“œ

Claude Code์˜ ์•„์›ƒ๋ฐ”์šด๋“œ ์•Œ๋ฆผ์€ notifications/claude/channel/permission_request์ž…๋‹ˆ๋‹ค. ์ฑ„๋„ ์•Œ๋ฆผ๊ณผ ๊ฐ™์ด ์ „์†ก์€ ํ‘œ์ค€ MCP์ด์ง€๋งŒ ๋ฉ”์„œ๋“œ ๋ฐ ์Šคํ‚ค๋งˆ๋Š” Claude Code ํ™•์žฅ์ž…๋‹ˆ๋‹ค. params ๊ฐ์ฒด์—๋Š” ์„œ๋ฒ„๊ฐ€ ์•„์›ƒ๋ฐ”์šด๋“œ ํ”„๋กฌํ”„ํŠธ๋กœ ํฌ๋งทํ•˜๋Š” ๋„ค ๊ฐœ์˜ ๋ฌธ์ž์—ด ํ•„๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

ํ•„๋“œ ์„ค๋ช…
request_id a-z์—์„œ ๊ทธ๋ ค์ง„ 5๊ฐœ์˜ ์†Œ๋ฌธ์ž์ด๋ฉฐ l์„ ์ œ์™ธํ•˜๋ฏ€๋กœ ํœด๋Œ€ํฐ์— ์ž…๋ ฅํ•  ๋•Œ 1 ๋˜๋Š” I๋กœ ์ฝํžˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์•„์›ƒ๋ฐ”์šด๋“œ ํ”„๋กฌํ”„ํŠธ์— ํฌํ•จํ•˜์—ฌ ํšŒ์‹ ์—์„œ ์—์ฝ”ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. Claude Code๋Š” ๋ฐœ๊ธ‰ํ•œ ID๋ฅผ ๊ฐ€์ง„ ํŒ์ •๋งŒ ์ˆ˜๋ฝํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ ํ„ฐ๋ฏธ๋„ ๋Œ€ํ™”๋Š” ์ด ID๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์•„์›ƒ๋ฐ”์šด๋“œ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ด๋ฅผ ํ•™์Šตํ•˜๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
tool_name Claude๊ฐ€ ์‚ฌ์šฉํ•˜๋ ค๋Š” ๋„๊ตฌ์˜ ์ด๋ฆ„(์˜ˆ: Bash ๋˜๋Š” Write).
description ์ด ํŠน์ • ๋„๊ตฌ ํ˜ธ์ถœ์ด ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์˜ ์ธ๊ฐ„์ด ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ์š”์•ฝ์ด๋ฉฐ ๋กœ์ปฌ ํ„ฐ๋ฏธ๋„ ๋Œ€ํ™”๊ฐ€ ํ‘œ์‹œํ•˜๋Š” ๋™์ผํ•œ ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค. Bash ํ˜ธ์ถœ์˜ ๊ฒฝ์šฐ ์ด๋Š” Claude์˜ ๋ช…๋ น ์„ค๋ช…์ด๊ฑฐ๋‚˜ ์ฃผ์–ด์ง„ ๊ฒƒ์ด ์—†์œผ๋ฉด ๋ช…๋ น ์ž์ฒด์ž…๋‹ˆ๋‹ค.
input_preview ๋„๊ตฌ์˜ ์ธ์ˆ˜๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ 200์ž๋กœ ์ž˜๋ฆฐ ๊ฒƒ์ž…๋‹ˆ๋‹ค. Bash์˜ ๊ฒฝ์šฐ ๋ช…๋ น์ž…๋‹ˆ๋‹ค. Write์˜ ๊ฒฝ์šฐ ํŒŒ์ผ ๊ฒฝ๋กœ ๋ฐ ์ฝ˜ํ…์ธ ์˜ ์ ‘๋‘์‚ฌ์ž…๋‹ˆ๋‹ค. ํ•œ ์ค„ ๋ฉ”์‹œ์ง€๋งŒ ๊ณต๊ฐ„์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํ”„๋กฌํ”„ํŠธ์—์„œ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ํ‘œ์‹œํ•  ๋‚ด์šฉ์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

์„œ๋ฒ„๊ฐ€ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ํŒ์ •์€ notifications/claude/channel/permission์ด๋ฉฐ ๋‘ ํ•„๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค: ์œ„์˜ ID๋ฅผ ์—์ฝ”ํ•˜๋Š” request_id ๋ฐ 'allow' ๋˜๋Š” 'deny'๋กœ ์„ค์ •๋œ behavior. Allow๋Š” ๋„๊ตฌ ํ˜ธ์ถœ์„ ์ง„ํ–‰ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. Deny๋Š” ์ด๋ฅผ ๊ฑฐ๋ถ€ํ•˜๋ฉฐ ๋กœ์ปฌ ๋Œ€ํ™”์—์„œ ์•„๋‹ˆ์˜ค๋กœ ๋‹ต๋ณ€ํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ์–ด๋А ํŒ์ •๋„ ํ–ฅํ›„ ํ˜ธ์ถœ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ฑ„ํŒ… ๋ธŒ๋ฆฌ์ง€์— ๋ฆด๋ ˆ์ด ์ถ”๊ฐ€

์–‘๋ฐฉํ–ฅ ์ฑ„๋„์— ๊ถŒํ•œ ๋ฆด๋ ˆ์ด๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ์„ธ ๊ฐ€์ง€ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

  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๋Š” ๋กœ์ปฌ ํ„ฐ๋ฏธ๋„ ๋Œ€ํ™”๋„ ์—ด์–ด ๋‘๋ฏ€๋กœ ์–ด๋А ์ชฝ์ด๋“  ๋‹ต๋ณ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋จผ์ € ๋„์ฐฉํ•˜๋Š” ๋‹ต๋ณ€์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ์ƒ๋œ ํ˜•์‹๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ์›๊ฒฉ ํšŒ์‹ ์€ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹ ์ค‘ ํ•˜๋‚˜๋กœ ์‹คํŒจํ•˜๋ฉฐ ๋‘ ๊ฒฝ์šฐ ๋ชจ๋‘ ๋Œ€ํ™”๋Š” ์—ด๋ ค ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋‹ค๋ฅธ ํ˜•์‹: ์ธ๋ฐ”์šด๋“œ ํ•ธ๋“ค๋Ÿฌ์˜ ์ •๊ทœ์‹์ด ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ approve it ๋˜๋Š” ID ์—†๋Š” yes์™€ ๊ฐ™์€ ํ…์ŠคํŠธ๋Š” Claude๋กœ ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€๋กœ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค.
  • ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹, ์ž˜๋ชป๋œ ID: ์„œ๋ฒ„๊ฐ€ ํŒ์ •์„ ๋‚ด๋ณด๋‚ด์ง€๋งŒ Claude Code๋Š” ํ•ด๋‹น ID๋ฅผ ๊ฐ€์ง„ ์—ด๋ฆฐ ์š”์ฒญ์„ ์ฐพ์ง€ ๋ชปํ•˜๊ณ  ์ž๋™์œผ๋กœ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

์ „์ฒด ์˜ˆ์ œ

์•„๋ž˜์˜ ์กฐ๋ฆฝ๋œ webhook.ts๋Š” ์ด ํŽ˜์ด์ง€์˜ ์„ธ ๊ฐ€์ง€ ํ™•์žฅ์„ ๋ชจ๋‘ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค: ํšŒ์‹  ๋„๊ตฌ, ๋ฐœ์‹ ์ž ๊ฒŒ์ดํŒ… ๋ฐ ๊ถŒํ•œ ๋ฆด๋ ˆ์ด. ์—ฌ๊ธฐ์„œ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ ์ดˆ๊ธฐ ์—ฐ์Šต์—์„œ ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ .mcp.json ํ•ญ๋ชฉ๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

curl์—์„œ ์–‘์ชฝ ๋ฐฉํ–ฅ์„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด HTTP ๋ฆฌ์Šค๋„ˆ๋Š” ๋‘ ๊ฒฝ๋กœ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

  • 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.',
  },
)

// --- reply ๋„๊ตฌ: 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: curl -N์ด ํšŒ์‹  ๋ฐ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก SSE ์ŠคํŠธ๋ฆผ
    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

๋‘ ๋ฒˆ์งธ์—์„œ ์•„์›ƒ๋ฐ”์šด๋“œ ์ธก์„ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜์—ฌ Claude์˜ ํšŒ์‹  ๋ฐ ๊ถŒํ•œ ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋„์ฐฉํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

curl -N localhost:8788/events

์„ธ ๋ฒˆ์งธ์—์„œ 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๊ฐ€ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‘˜ ๋‹ค send()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ /events๋ฅผ ํ†ตํ•ด ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธํ•˜์ง€๋งŒ ์‹œ์Šคํ…œ์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ์˜ํ•ด ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.
  • 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๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€๋˜๋ ค๋ฉด ๊ณต์‹ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์— ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ฑ„๋„ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์Šน์ธ๋˜๊ธฐ ์ „์— ๋ณด์•ˆ ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์นฉ๋‹ˆ๋‹ค. ํŒ€ ๋ฐ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๊ณ„ํš์—์„œ ๊ด€๋ฆฌ์ž๋Š” ๋Œ€์‹  ์กฐ์ง์˜ ์ž์‹ ์˜ allowedChannelPlugins ๋ชฉ๋ก์— ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ๊ธฐ๋ณธ Anthropic ํ—ˆ์šฉ ๋ชฉ๋ก์„ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ  ํ•ญ๋ชฉ

  • ์ฑ„๋„์„ ์„ค์น˜ํ•˜๊ณ  Telegram, Discord, iMessage ๋˜๋Š” fakechat ๋ฐ๋ชจ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ํŒ€ ๋˜๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์กฐ์ง์— ๋Œ€ํ•ด ์ฑ„๋„์„ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค
  • ์ž‘๋™ํ•˜๋Š” ์ฑ„๋„ ๊ตฌํ˜„์€ ํŽ˜์–ด๋ง ํ๋ฆ„, ํšŒ์‹  ๋„๊ตฌ ๋ฐ ํŒŒ์ผ ์ฒจ๋ถ€๊ฐ€ ์žˆ๋Š” ์™„์ „ํ•œ ์„œ๋ฒ„ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค
  • MCP๋Š” ์ฑ„๋„ ์„œ๋ฒ„๊ฐ€ ๊ตฌํ˜„ํ•˜๋Š” ๊ธฐ๋ณธ ํ”„๋กœํ† ์ฝœ์ž…๋‹ˆ๋‹ค
  • ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฑ„๋„์„ ํŒจํ‚ค์ง•ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ /plugin install๋กœ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค