Vai al contenuto principale
I canali sono in anteprima di ricerca e richiedono Claude Code v2.1.80 o successivo. Richiedono l’accesso a claude.ai. L’autenticazione tramite console e chiave API non è supportata. Le organizzazioni Team ed Enterprise devono abilitarli esplicitamente.
Un canale è un server MCP che invia eventi in una sessione di Claude Code in modo che Claude possa reagire a cose che accadono al di fuori del terminale. Puoi creare un canale unidirezionale o bidirezionale. I canali unidirezionali inoltrano avvisi, webhook o eventi di monitoraggio per cui Claude può agire. I canali bidirezionali come i bridge di chat espongono anche uno strumento di risposta in modo che Claude possa inviare messaggi indietro. Un canale con un percorso mittente affidabile può anche optare per inoltro delle richieste di autorizzazione in modo da poter approvare o negare l’uso dello strumento da remoto. Questa pagina copre: Per utilizzare un canale esistente invece di crearne uno, vedi Canali. Telegram, Discord, iMessage e fakechat sono inclusi nell’anteprima di ricerca.

Panoramica

Un canale è un server MCP che viene eseguito sulla stessa macchina di Claude Code. Claude Code lo genera come un sottoprocesso e comunica tramite stdio. Il tuo server di canale è il ponte tra i sistemi esterni e la sessione di Claude Code:
  • Piattaforme di chat (Telegram, Discord): il tuo plugin viene eseguito localmente e interroga l’API della piattaforma per i nuovi messaggi. Quando qualcuno invia un DM al tuo bot, il plugin riceve il messaggio e lo inoltra a Claude. Nessun URL da esporre.
  • Webhook (CI, monitoraggio): il tuo server ascolta su una porta HTTP locale. I sistemi esterni POST a quella porta e il tuo server invia il payload a Claude.
Diagramma dell'architettura che mostra i sistemi esterni che si connettono al tuo server di canale locale, che comunica con Claude Code tramite stdio

Cosa ti serve

L’unico requisito difficile è il pacchetto @modelcontextprotocol/sdk e un runtime compatibile con Node.js. Bun, Node e Deno funzionano tutti. I plugin precostruiti nell’anteprima di ricerca utilizzano Bun, ma il tuo canale non deve necessariamente. Il tuo server deve:
  1. Dichiarare la capacità claude/channel in modo che Claude Code registri un listener di notifica
  2. Emettere eventi notifications/claude/channel quando accade qualcosa
  3. Connettersi tramite trasporto stdio (Claude Code genera il tuo server come un sottoprocesso)
Le sezioni Opzioni del server e Formato di notifica coprono ciascuna di queste in dettaglio. Vedi Esempio: crea un ricevitore webhook per una procedura dettagliata completa. Durante l’anteprima di ricerca, i canali personalizzati non sono nella lista di approvazione. Usa --dangerously-load-development-channels per testare localmente. Vedi Test durante l’anteprima di ricerca per i dettagli.

Esempio: crea un ricevitore webhook

Questa procedura dettagliata crea un server a file singolo che ascolta le richieste HTTP e le inoltra nella tua sessione di Claude Code. Alla fine, qualsiasi cosa possa inviare un HTTP POST, come una pipeline CI, un avviso di monitoraggio o un comando curl, può inviare eventi a Claude. Questo esempio utilizza Bun come runtime per il suo server HTTP integrato e il supporto di TypeScript. Puoi utilizzare Node o Deno invece; l’unico requisito è l’SDK MCP.
1

Crea il progetto

Crea una nuova directory e installa l’SDK MCP:
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk
2

Scrivi il server di canale

Crea un file chiamato webhook.ts. Questo è l’intero server di canale: si connette a Claude Code tramite stdio e ascolta i POST HTTP sulla porta 8788. Quando arriva una richiesta, invia il corpo a Claude come evento di canale.
webhook.ts
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

// Crea il server MCP e dichiaralo come canale
const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    // questa chiave è ciò che lo rende un canale — Claude Code registra un listener per essa
    capabilities: { experimental: { 'claude/channel': {} } },
    // aggiunto al prompt di sistema di Claude in modo che sappia come gestire questi eventi
    instructions: 'Gli eventi dal canale webhook arrivano come <channel source="webhook" ...>. Sono unidirezionali: leggili e agisci, nessuna risposta prevista.',
  },
)

// Connettiti a Claude Code tramite stdio (Claude Code genera questo processo)
await mcp.connect(new StdioServerTransport())

// Avvia un server HTTP che inoltra ogni POST a Claude
Bun.serve({
  port: 8788,  // qualsiasi porta aperta funziona
  // solo localhost: nulla al di fuori di questa macchina può POST
  hostname: '127.0.0.1',
  async fetch(req) {
    const body = await req.text()
    await mcp.notification({
      method: 'notifications/claude/channel',
      params: {
        content: body,  // diventa il corpo del tag <channel>
        // ogni chiave diventa un attributo del tag, ad es. <channel path="/" method="POST">
        meta: { path: new URL(req.url).pathname, method: req.method },
      },
    })
    return new Response('ok')
  },
})
Il file fa tre cose in ordine:
  • Configurazione del server: crea il server MCP con claude/channel nelle sue capacità, che è ciò che dice a Claude Code che questo è un canale. La stringa instructions va nel prompt di sistema di Claude: dì a Claude quali eventi aspettarsi, se rispondere e come instradare le risposte se dovrebbe.
  • Connessione stdio: si connette a Claude Code tramite stdin/stdout. Questo è standard per qualsiasi server MCP: Claude Code lo genera come un sottoprocesso.
  • Listener HTTP: avvia un server web locale sulla porta 8788. Ogni corpo POST viene inoltrato a Claude come evento di canale tramite mcp.notification(). Il content diventa il corpo dell’evento e ogni voce meta diventa un attributo sul tag <channel>. Il listener ha bisogno dell’accesso all’istanza mcp, quindi viene eseguito nello stesso processo. Potresti dividerlo in moduli separati per un progetto più grande.
3

Registra il tuo server con Claude Code

Aggiungi il server alla tua configurazione MCP in modo che Claude Code sappia come avviarlo. Per un .mcp.json a livello di progetto nella stessa directory, usa un percorso relativo. Per la configurazione a livello di utente in ~/.claude.json, usa il percorso assoluto completo in modo che il server possa essere trovato da qualsiasi progetto:
.mcp.json
{
  "mcpServers": {
    "webhook": { "command": "bun", "args": ["./webhook.ts"] }
  }
}
Claude Code legge la tua configurazione MCP all’avvio e genera ogni server come un sottoprocesso.
4

Testalo

Durante l’anteprima di ricerca, i canali personalizzati non sono nella lista di approvazione, quindi avvia Claude Code con il flag di sviluppo:
claude --dangerously-load-development-channels server:webhook
Quando Claude Code si avvia, legge la tua configurazione MCP, genera il tuo webhook.ts come un sottoprocesso e il listener HTTP si avvia automaticamente sulla porta che hai configurato (8788 in questo esempio). Non è necessario eseguire il server da solo.Se vedi “bloccato dalla politica dell’organizzazione”, il tuo amministratore Team o Enterprise deve prima abilitare i canali.In un terminale separato, simula un webhook inviando un HTTP POST con un messaggio al tuo server. Questo esempio invia un avviso di errore CI alla porta 8788 (o qualsiasi porta tu abbia configurato):
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
Il payload arriva nella tua sessione di Claude Code come un tag <channel>:
<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel>
Nel tuo terminale di Claude Code, vedrai Claude ricevere il messaggio e iniziare a rispondere: leggendo file, eseguendo comandi o qualsiasi cosa il messaggio richieda. Questo è un canale unidirezionale, quindi Claude agisce nella tua sessione ma non invia nulla indietro tramite il webhook. Per aggiungere risposte, vedi Esponi uno strumento di risposta.Se l’evento non arriva, la diagnosi dipende da ciò che curl ha restituito:
  • curl ha successo ma nulla raggiunge Claude: esegui /mcp nella tua sessione per controllare lo stato del server. “Impossibile connettersi” di solito significa un errore di dipendenza o importazione nel tuo file server; controlla il log di debug in ~/.claude/debug/<session-id>.txt per la traccia stderr.
  • curl fallisce con “connessione rifiutata”: la porta non è ancora associata o un processo stantio da un’esecuzione precedente la sta mantenendo. lsof -i :<port> mostra cosa sta ascoltando; kill il processo stantio prima di riavviare la tua sessione.
Il server fakechat estende questo modello con un’interfaccia web, allegati di file e uno strumento di risposta per chat bidirezionale.

Test durante l’anteprima di ricerca

Durante l’anteprima di ricerca, ogni canale deve essere nella lista di approvazione approvata per registrarsi. Il flag di sviluppo bypassa la lista di approvazione per voci specifiche dopo un prompt di conferma. Questo esempio mostra entrambi i tipi di voce:
# Test di un plugin che stai sviluppando
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace

# Test di un server .mcp.json nudo (nessun wrapper di plugin ancora)
claude --dangerously-load-development-channels server:webhook
Il bypass è per voce. Combinare questo flag con --channels non estende il bypass alle voci --channels. Durante l’anteprima di ricerca, la lista di approvazione approvata è curata da Anthropic, quindi il tuo canale rimane sul flag di sviluppo mentre lo costruisci e lo testi.
Questo flag salta solo la lista di approvazione. La politica organizzativa channelsEnabled si applica comunque. Non usarla per eseguire canali da fonti non attendibili.

Opzioni del server

Un canale imposta queste opzioni nel costruttore Server. I campi instructions e capabilities.tools sono MCP standard; capabilities.experimental['claude/channel'] e capabilities.experimental['claude/channel/permission'] sono le aggiunte specifiche del canale:
CampoTipoDescrizione
capabilities.experimental['claude/channel']objectObbligatorio. Sempre {}. La presenza registra il listener di notifica.
capabilities.experimental['claude/channel/permission']objectFacoltativo. Sempre {}. Dichiara che questo canale può ricevere richieste di inoltro delle autorizzazioni. Quando dichiarato, Claude Code inoltra i prompt di approvazione dello strumento al tuo canale in modo da poter approvare o negare da remoto. Vedi Inoltro delle richieste di autorizzazione.
capabilities.toolsobjectSolo bidirezionale. Sempre {}. Capacità dello strumento MCP standard. Vedi Esponi uno strumento di risposta.
instructionsstringConsigliato. Aggiunto al prompt di sistema di Claude. Dì a Claude quali eventi aspettarsi, cosa significano gli attributi del tag <channel>, se rispondere e, se sì, quale strumento usare e quale attributo passare indietro (come chat_id).
Per creare un canale unidirezionale, ometti capabilities.tools. Questo esempio mostra una configurazione bidirezionale con la capacità del canale, gli strumenti e le istruzioni impostate:
import { Server } from '@modelcontextprotocol/sdk/server/index.js'

const mcp = new Server(
  { name: 'your-channel', version: '0.0.1' },
  {
    capabilities: {
      experimental: { 'claude/channel': {} },  // registra il listener del canale
      tools: {},  // ometti per canali unidirezionali
    },
    // aggiunto al prompt di sistema di Claude in modo che sappia come gestire i tuoi eventi
    instructions: 'I messaggi arrivano come <channel source="your-channel" ...>. Rispondi con lo strumento di risposta.',
  },
)
Per inviare un evento, chiama mcp.notification() con il metodo notifications/claude/channel. I parametri sono nella sezione successiva.

Formato di notifica

Il tuo server emette notifications/claude/channel con due parametri:
CampoTipoDescrizione
contentstringIl corpo dell’evento. Consegnato come il corpo del tag <channel>.
metaRecord<string, string>Facoltativo. Ogni voce diventa un attributo sul tag <channel> per il contesto di instradamento come ID chat, nome del mittente o gravità dell’avviso. Le chiavi devono essere identificatori: solo lettere, cifre e sottolineature. Le chiavi contenenti trattini o altri caratteri vengono silenziosamente eliminate.
Il tuo server invia gli eventi chiamando mcp.notification() sull’istanza Server. Questo esempio invia un avviso di errore CI con due chiavi meta:
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' },
  },
})
L’evento arriva nel contesto di Claude avvolto in un tag <channel>. L’attributo source viene impostato automaticamente dal nome configurato del tuo server:
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>

Esponi uno strumento di risposta

Se il tuo canale è bidirezionale, come un bridge di chat piuttosto che un inoltro di avvisi, esponi uno strumento MCP standard che Claude può chiamare per inviare messaggi indietro. Nulla della registrazione dello strumento è specifico del canale. Uno strumento di risposta ha tre componenti:
  1. Una voce tools: {} nelle capacità del tuo costruttore Server in modo che Claude Code scopra lo strumento
  2. Handler dello strumento che definiscono lo schema dello strumento e implementano la logica di invio
  3. Una stringa instructions nel tuo costruttore Server che dice a Claude quando e come chiamare lo strumento
Per aggiungere questi al ricevitore webhook sopra:
1

Abilita la scoperta dello strumento

Nel tuo costruttore Server in webhook.ts, aggiungi tools: {} alle capacità in modo che Claude Code sappia che il tuo server offre strumenti:
capabilities: {
  experimental: { 'claude/channel': {} },
  tools: {},  // abilita la scoperta dello strumento
},
2

Registra lo strumento di risposta

Aggiungi quanto segue a webhook.ts. L’import va in cima al file con i tuoi altri import; i due handler vanno tra il costruttore Server e mcp.connect(). Questo registra uno strumento reply che Claude può chiamare con un chat_id e text:
// Aggiungi questo import in cima a webhook.ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'

// Claude interroga questo all'avvio per scoprire quali strumenti offre il tuo server
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Invia un messaggio indietro su questo canale',
    // inputSchema dice a Claude quali argomenti passare
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'La conversazione in cui rispondere' },
        text: { type: 'string', description: 'Il messaggio da inviare' },
      },
      required: ['chat_id', 'text'],
    },
  }],
}))

// Claude chiama questo quando vuole invocare uno strumento
mcp.setRequestHandler(CallToolRequestSchema, async req => {
  if (req.params.name === 'reply') {
    const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
    // send() è il tuo outbound: POST alla tua piattaforma di chat, o per il test locale
    // il broadcast SSE mostrato nell'esempio completo di seguito.
    send(`Reply to ${chat_id}: ${text}`)
    return { content: [{ type: 'text', text: 'sent' }] }
  }
  throw new Error(`unknown tool: ${req.params.name}`)
})
3

Aggiorna le istruzioni

Aggiorna la stringa instructions nel tuo costruttore Server in modo che Claude sappia di instradare le risposte indietro tramite lo strumento. Questo esempio dice a Claude di passare chat_id dal tag in entrata:
instructions: 'I messaggi arrivano come <channel source="webhook" chat_id="...">. Rispondi con lo strumento di risposta, passando il chat_id dal tag.'
Ecco il webhook.ts completo con supporto bidirezionale. Le risposte in uscita vengono trasmesse su GET /events utilizzando Server-Sent Events (SSE), quindi curl -N localhost:8788/events può guardarle dal vivo; la chat in entrata arriva su POST /:
"Full
#!/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'

// --- Outbound: scrivi su qualsiasi listener curl -N su /events ---
// Un vero bridge farebbe POST alla tua piattaforma di chat invece.
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: 'I messaggi arrivano come <channel source="webhook" chat_id="...">. Rispondi con lo strumento di risposta, passando il chat_id dal tag.',
  },
)

mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Invia un messaggio indietro su questo canale',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'La conversazione in cui rispondere' },
        text: { type: 'string', description: 'Il messaggio da inviare' },
      },
      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,  // non chiudere i flussi SSE inattivi
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events: flusso SSE in modo che curl -N possa guardare le risposte di Claude dal vivo
    if (req.method === 'GET' && url.pathname === '/events') {
      const stream = new ReadableStream({
        start(ctrl) {
          ctrl.enqueue(': connected\n\n')  // in modo che curl mostri qualcosa immediatamente
          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: inoltra a Claude come evento di canale
    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')
  },
})
Il server fakechat mostra un esempio più completo con allegati di file e modifica dei messaggi.

Gating dei messaggi in entrata

Un canale senza gating è un vettore di iniezione di prompt. Chiunque possa raggiungere il tuo endpoint può mettere testo davanti a Claude. Un canale che ascolta una piattaforma di chat o un endpoint pubblico ha bisogno di un vero controllo del mittente prima di emettere qualsiasi cosa. Controlla il mittente rispetto a una lista di approvazione prima di chiamare mcp.notification(). Questo esempio elimina qualsiasi messaggio da un mittente non nel set:
const allowed = new Set(loadAllowlist())  // dal tuo access.json o equivalente

// dentro il tuo gestore di messaggi, prima di emettere:
if (!allowed.has(message.from.id)) {  // mittente, non stanza
  return  // elimina silenziosamente
}
await mcp.notification({ ... })
Gating sull’identità del mittente, non sull’identità della chat o della stanza: message.from.id nell’esempio, non message.chat.id. Nelle chat di gruppo, questi differiscono e il gating sulla stanza permetterebbe a chiunque in un gruppo approvato di iniettare messaggi nella sessione. I canali Telegram e Discord fanno il gating su una lista di approvazione del mittente allo stesso modo. Avviano la lista tramite accoppiamento: l’utente invia un DM al bot, il bot risponde con un codice di accoppiamento, l’utente lo approva nella sua sessione di Claude Code e il suo ID della piattaforma viene aggiunto. Vedi entrambe le implementazioni per il flusso di accoppiamento completo. Il canale iMessage adotta un approccio diverso: rileva gli indirizzi dell’utente dal database dei Messaggi all’avvio e li lascia passare automaticamente, con altri mittenti aggiunti per handle.

Inoltro delle richieste di autorizzazione

L’inoltro delle autorizzazioni richiede Claude Code v2.1.81 o successivo. Le versioni precedenti ignorano la capacità claude/channel/permission.
Quando Claude chiama uno strumento che ha bisogno di approvazione, la finestra di dialogo del terminale locale si apre e la sessione attende. Un canale bidirezionale può optare per ricevere lo stesso prompt in parallelo e inoltrarlo a te su un altro dispositivo. Entrambi rimangono attivi: puoi rispondere nel terminale o sul tuo telefono e Claude Code applica qualsiasi risposta arrivi per prima e chiude l’altra. L’inoltro copre le approvazioni di uso dello strumento come Bash, Write e Edit. La fiducia del progetto e i dialoghi di consenso del server MCP non vengono inoltrati; quelli appaiono solo nel terminale locale.

Come funziona l’inoltro

Quando si apre un prompt di autorizzazione, il ciclo di inoltro ha quattro passaggi:
  1. Claude Code genera un breve ID di richiesta e notifica il tuo server
  2. Il tuo server inoltra il prompt e l’ID alla tua app di chat
  3. L’utente remoto risponde con un sì o no e quell’ID
  4. Il tuo gestore in entrata analizza la risposta in un verdetto e Claude Code lo applica solo se l’ID corrisponde a una richiesta aperta
La finestra di dialogo del terminale locale rimane aperta durante tutto questo. Se qualcuno al terminale risponde prima che il verdetto remoto arrivi, quella risposta viene applicata invece e la richiesta remota in sospeso viene eliminata. Diagramma di sequenza: Claude Code invia una notifica permission_request al server del canale, il server formatta e invia il prompt all'app di chat, l'umano risponde con un verdetto e il server analizza quella risposta in una notifica di autorizzazione indietro a Claude Code

Campi della richiesta di autorizzazione

La notifica in uscita da Claude Code è notifications/claude/channel/permission_request. Come la notifica del canale, il trasporto è MCP standard ma il metodo e lo schema sono estensioni di Claude Code. L’oggetto params ha quattro campi stringa che il tuo server formatta nel prompt in uscita:
CampoDescrizione
request_idCinque lettere minuscole tratte da a-z senza l, quindi non legge mai come 1 o I quando digitato su un telefono. Includilo nel tuo prompt in uscita in modo che possa essere ripetuto nella risposta. Claude Code accetta solo un verdetto che porta un ID che ha emesso. La finestra di dialogo del terminale locale non visualizza questo ID, quindi il tuo gestore in uscita è l’unico modo per apprenderlo.
tool_nameNome dello strumento che Claude vuole usare, ad esempio Bash o Write.
descriptionRiepilogo leggibile di cosa fa questa specifica chiamata dello strumento, lo stesso testo che la finestra di dialogo del terminale locale mostra. Per una chiamata Bash questo è la descrizione di Claude del comando, o il comando stesso se nessuno è stato fornito.
input_previewGli argomenti dello strumento come stringa JSON, troncati a 200 caratteri. Per Bash questo è il comando; per Write è il percorso del file e un prefisso del contenuto. Omettilo dal tuo prompt se hai solo spazio per un messaggio di una riga. Il tuo server decide cosa mostrare.
Il verdetto che il tuo server rimanda è notifications/claude/channel/permission con due campi: request_id che ripete l’ID sopra e behavior impostato su 'allow' o 'deny'. Allow consente alla chiamata dello strumento di procedere; deny la rifiuta, lo stesso che rispondere No nella finestra di dialogo locale. Nessun verdetto influisce sulle chiamate future.

Aggiungi inoltro a un bridge di chat

L’aggiunta dell’inoltro delle autorizzazioni a un canale bidirezionale richiede tre componenti:
  1. Una voce claude/channel/permission: {} sotto le capacità experimental nel tuo costruttore Server in modo che Claude Code sappia di inoltrare i prompt
  2. Un gestore di notifica per notifications/claude/channel/permission_request che formatta il prompt e lo invia tramite l’API della tua piattaforma
  3. Un controllo nel tuo gestore di messaggi in entrata che riconosce yes <id> o no <id> e emette una notifica di verdetto notifications/claude/channel/permission invece di inoltrare il testo a Claude
Dichiara la capacità solo se il tuo canale autentica il mittente, perché chiunque possa rispondere tramite il tuo canale può approvare o negare l’uso dello strumento nella tua sessione. Per aggiungere questi a un bridge di chat bidirezionale come quello assemblato in Esponi uno strumento di risposta:
1

Dichiara la capacità di autorizzazione

Nel tuo costruttore Server, aggiungi claude/channel/permission: {} accanto a claude/channel sotto experimental:
capabilities: {
  experimental: {
    'claude/channel': {},
    'claude/channel/permission': {},  // opta per l'inoltro delle autorizzazioni
  },
  tools: {},
},
2

Gestisci la richiesta in entrata

Registra un gestore di notifica tra il tuo costruttore Server e mcp.connect(). Claude Code lo chiama con i quattro campi di richiesta quando si apre una finestra di dialogo di autorizzazione. Il tuo gestore formatta il prompt per la tua piattaforma e include istruzioni per rispondere con l’ID:
import { z } from 'zod'

// setNotificationHandler instrada per z.literal sul campo method,
// quindi questo schema è sia il validatore che la chiave di dispatch
const PermissionRequestSchema = z.object({
  method: z.literal('notifications/claude/channel/permission_request'),
  params: z.object({
    request_id: z.string(),     // cinque lettere minuscole, includi verbatim nel tuo prompt
    tool_name: z.string(),      // ad es. "Bash", "Write"
    description: z.string(),    // riepilogo leggibile di questa chiamata
    input_preview: z.string(),  // argomenti dello strumento come JSON, troncati a ~200 caratteri
  }),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  // send() è il tuo outbound: POST alla tua piattaforma di chat, o per il test locale
  // il broadcast SSE mostrato nell'esempio completo di seguito.
  send(
    `Claude vuole eseguire ${params.tool_name}: ${params.description}\n\n` +
    // l'ID nell'istruzione è ciò che il tuo gestore in entrata analizza nel Passaggio 3
    `Rispondi "yes ${params.request_id}" o "no ${params.request_id}"`,
  )
})
3

Intercetta il verdetto nel tuo gestore in entrata

Il tuo gestore in entrata è il ciclo o il callback che riceve messaggi dalla tua piattaforma: lo stesso posto dove fai il gating sul mittente e emetti notifications/claude/channel per inoltrare la chat a Claude. Aggiungi un controllo prima della chiamata di inoltro della chat che riconosce il formato del verdetto e emette la notifica di autorizzazione invece.L’espressione regolare corrisponde al formato dell’ID che Claude Code genera: cinque lettere, mai l. Il flag /i tollera l’autocorrezione del telefono che capitalizza la risposta; minuscola l’ID catturato prima di inviarlo indietro.
// corrisponde a "y abcde", "yes abcde", "n abcde", "no abcde"
// [a-km-z] è l'alfabeto dell'ID che Claude Code usa (minuscolo, salta 'l')
// /i tollera l'autocorrezione del telefono; minuscola la cattura prima di inviare
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  // fai il gating sul mittente per primo

  const m = PERMISSION_REPLY_RE.exec(message.text)
  if (m) {
    // m[1] è la parola del verdetto, m[2] è l'ID della richiesta
    // emetti la notifica del verdetto indietro a Claude Code invece della chat
    await mcp.notification({
      method: 'notifications/claude/channel/permission',
      params: {
        request_id: m[2].toLowerCase(),  // normalizza nel caso di autocorrezione maiuscola
        behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
      },
    })
    return  // gestito come verdetto, non inoltrare anche come chat
  }

  // non corrisponde al formato del verdetto: passa al percorso della chat normale
  await mcp.notification({
    method: 'notifications/claude/channel',
    params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
  })
}
Claude Code mantiene anche la finestra di dialogo del terminale locale aperta, quindi puoi rispondere in entrambi i posti e la prima risposta ad arrivare viene applicata. Una risposta remota che non corrisponde esattamente al formato previsto fallisce in uno di due modi e in entrambi i casi la finestra di dialogo rimane aperta:
  • Formato diverso: l’espressione regolare del tuo gestore in entrata non corrisponde, quindi testo come approve it o yes senza un ID passa come messaggio normale a Claude.
  • Formato corretto, ID sbagliato: il tuo server emette un verdetto, ma Claude Code non trova nessuna richiesta aperta con quell’ID e lo elimina silenziosamente.

Esempio completo

Il webhook.ts assemblato di seguito combina tutte e tre le estensioni da questa pagina: lo strumento di risposta, il gating del mittente e l’inoltro delle autorizzazioni. Se stai iniziando qui, avrai anche bisogno della configurazione del progetto e della voce .mcp.json dalla procedura dettagliata iniziale. Per rendere entrambe le direzioni testabili da curl, il listener HTTP serve due percorsi:
  • GET /events: mantiene aperto un flusso SSE e invia ogni messaggio in uscita come una riga data:, quindi curl -N può guardare le risposte di Claude e i prompt di autorizzazione arrivare dal vivo.
  • POST /: il lato in entrata, lo stesso gestore di prima, ora con il controllo del formato del verdetto inserito prima del ramo di inoltro della chat.
"Full
#!/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'

// --- Outbound: scrivi su qualsiasi listener curl -N su /events ---
// Un vero bridge farebbe POST alla tua piattaforma di chat invece.
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)
}

// Lista di approvazione del mittente. Per la procedura dettagliata locale confidiamo nel singolo valore dell'intestazione X-Sender
// "dev"; un vero bridge controllerebbe l'ID utente della piattaforma.
const allowed = new Set(['dev'])

const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    capabilities: {
      experimental: {
        'claude/channel': {},
        'claude/channel/permission': {},  // opta per l'inoltro delle autorizzazioni
      },
      tools: {},
    },
    instructions:
      'I messaggi arrivano come <channel source="webhook" chat_id="...">. ' +
      'Rispondi con lo strumento di risposta, passando il chat_id dal tag.',
  },
)

// --- strumento di risposta: Claude chiama questo per inviare un messaggio indietro ---
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Invia un messaggio indietro su questo canale',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'La conversazione in cui rispondere' },
        text: { type: 'string', description: 'Il messaggio da inviare' },
      },
      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}`)
})

// --- inoltro delle autorizzazioni: Claude Code (non Claude) chiama questo quando si apre una finestra di dialogo
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 vuole eseguire ${params.tool_name}: ${params.description}\n\n` +
    `Rispondi "yes ${params.request_id}" o "no ${params.request_id}"`,
  )
})

await mcp.connect(new StdioServerTransport())

// --- HTTP su :8788: GET /events trasmette in uscita, POST instrada in entrata ---
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,  // non chiudere i flussi SSE inattivi
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events: flusso SSE in modo che curl -N possa guardare risposte e prompt dal vivo
    if (req.method === 'GET' && url.pathname === '/events') {
      const stream = new ReadableStream({
        start(ctrl) {
          ctrl.enqueue(': connected\n\n')  // in modo che curl mostri qualcosa immediatamente
          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' },
      })
    }

    // tutto il resto è in entrata: fai il gating sul mittente per primo
    const body = await req.text()
    const sender = req.headers.get('X-Sender') ?? ''
    if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })

    // controlla il formato del verdetto prima di trattare come chat
    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')
    }

    // chat normale: inoltra a Claude come evento di canale
    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')
  },
})
Testa il percorso del verdetto in tre terminali. Il primo è la tua sessione di Claude Code, avviata con il flag di sviluppo in modo che generi webhook.ts:
claude --dangerously-load-development-channels server:webhook
Nel secondo, trasmetti il lato in uscita in modo da poter vedere le risposte di Claude e i prompt di autorizzazione mentre arrivano:
curl -N localhost:8788/events
Nel terzo, invia un messaggio che farà sì che Claude tenti di eseguire un comando:
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788
La finestra di dialogo di autorizzazione locale si apre nel tuo terminale di Claude Code. Un momento dopo il prompt appare nel flusso /events, incluso l’ID di cinque lettere. Approvalo dal lato remoto:
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788
La finestra di dialogo locale si chiude e lo strumento viene eseguito. La risposta di Claude torna tramite lo strumento reply e atterra anche nel flusso. I tre pezzi specifici del canale in questo file:
  • Capacità nel costruttore Server: claude/channel registra il listener di notifica, claude/channel/permission opta per l’inoltro delle autorizzazioni, tools consente a Claude di scoprire lo strumento di risposta.
  • Percorsi in uscita: il gestore dello strumento reply è ciò che Claude chiama per le risposte conversazionali; il gestore di notifica PermissionRequestSchema è ciò che Claude Code chiama quando si apre una finestra di dialogo di autorizzazione. Entrambi chiamano send() per trasmettere su /events, ma vengono attivati da parti diverse del sistema.
  • Gestore HTTP: GET /events mantiene aperto un flusso SSE in modo che curl possa guardare l’uscita dal vivo; POST è in entrata, gated sull’intestazione X-Sender. Un corpo yes <id> o no <id> va a Claude Code come notifica di verdetto e non raggiunge mai Claude; qualsiasi altra cosa viene inoltrata a Claude come evento di canale.

Pacchetto come plugin

Per rendere il tuo canale installabile e condivisibile, avvolgilo in un plugin e pubblicalo in un marketplace. Gli utenti lo installano con /plugin install, quindi lo abilitano per sessione con --channels plugin:<name>@<marketplace>. Un canale pubblicato nel tuo marketplace ha ancora bisogno di --dangerously-load-development-channels per essere eseguito, poiché non è nella lista di approvazione. Per aggiungerlo, invialo al marketplace ufficiale. I plugin di canale passano attraverso la revisione della sicurezza prima di essere approvati. Nei piani Team ed Enterprise, un amministratore può invece includere il tuo plugin nella lista allowedChannelPlugins dell’organizzazione, che sostituisce la lista di approvazione predefinita di Anthropic.

Vedi anche

  • Canali per installare e utilizzare Telegram, Discord, iMessage o la demo fakechat, e per abilitare i canali per un’organizzazione Team o Enterprise
  • Implementazioni di canali funzionanti per il codice del server completo con flussi di accoppiamento, strumenti di risposta e allegati di file
  • MCP per il protocollo sottostante che i server di canale implementano
  • Plugin per pacchettizzare il tuo canale in modo che gli utenti possano installarlo con /plugin install