SpyBara
Go Premium

channels-reference.md 2026-11-11 22:59 UTC to 2026-11-12 23:02 UTC

No changes for this document in this history range.

2026
Tue 10 21:57

Référence des canaux

Créez un serveur MCP qui envoie des webhooks, des alertes et des messages de chat dans une session Claude Code. Référence du contrat de canal : déclaration de capacité, événements de notification, outils de réponse, contrôle de l'expéditeur et relais de permission.

[!NOTE]

Les canaux sont en aperçu de recherche. Les organisations Team et Enterprise doivent les activer explicitement.

Un canal est un serveur MCP qui envoie des événements dans une session Claude Code afin que Claude puisse réagir aux choses qui se produisent en dehors du terminal.

Vous pouvez créer un canal unidirectionnel ou bidirectionnel. Les canaux unidirectionnels transmettent les alertes, webhooks ou événements de surveillance pour que Claude agisse. Les canaux bidirectionnels comme les passerelles de chat exposent également un outil de réponse afin que Claude puisse renvoyer des messages. Un canal avec un chemin d'expéditeur de confiance peut également opter pour relayer les invites de permission afin que vous puissiez approuver ou refuser l'utilisation d'outils à distance.

Cette page couvre :

Pour utiliser un canal existant au lieu d'en créer un, consultez Canaux. Telegram, Discord, iMessage et fakechat sont inclus dans l'aperçu de recherche.

Aperçu

Un canal est un serveur MCP qui s'exécute sur la même machine que Claude Code. Claude Code le lance en tant que sous-processus et communique via stdio. Votre serveur de canal est le pont entre les systèmes externes et la session Claude Code :

  • Plateformes de chat (Telegram, Discord) : votre plugin s'exécute localement et interroge l'API de la plateforme pour les nouveaux messages. Quand quelqu'un envoie un message direct à votre bot, le plugin reçoit le message et le transmet à Claude. Aucune URL à exposer.
  • Webhooks (CI, surveillance) : votre serveur écoute sur un port HTTP local. Les systèmes externes envoient des POST à ce port, et votre serveur envoie la charge utile à Claude.

Diagramme d'architecture montrant les systèmes externes se connectant à votre serveur de canal local, qui communique avec Claude Code via stdio

Ce dont vous avez besoin

La seule exigence stricte est le package @modelcontextprotocol/sdk et un runtime compatible Node.js. Bun, Node et Deno fonctionnent tous. Les plugins pré-construits dans l'aperçu de recherche utilisent Bun, mais votre canal n'a pas besoin de le faire.

Votre serveur doit :

  1. Déclarer la capacité claude/channel afin que Claude Code enregistre un écouteur de notification
  2. Émettre des événements notifications/claude/channel quand quelque chose se produit
  3. Se connecter via transport stdio

Les sections Options du serveur et Format de notification couvrent chacune de ces points en détail. Consultez Exemple : créer un récepteur de webhook pour une procédure pas à pas complète.

Pendant l'aperçu de recherche, les canaux personnalisés ne sont pas sur la liste d'approbation. Utilisez --dangerously-load-development-channels pour tester localement. Consultez Tester pendant l'aperçu de recherche pour plus de détails.

Exemple : créer un récepteur de webhook

Ce guide pas à pas crée un serveur en un seul fichier qui écoute les requêtes HTTP et les transmet à votre session Claude Code. À la fin, tout ce qui peut envoyer un HTTP POST, comme un pipeline CI, une alerte de monitoring ou une commande curl, pourra envoyer des événements à Claude.

Cet exemple utilise Bun comme runtime pour son serveur HTTP intégré et sa prise en charge de TypeScript. Vous pouvez utiliser Node ou Deno à la place ; la seule exigence est le MCP SDK.

1. Créer le projet

Les exemples de relais de permissions importent zod directement, il est donc installé en même temps que le MCP SDK. Créez un nouveau répertoire et installez les deux :

mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk zod

2. Écrire le serveur de canal

Créez un fichier nommé webhook.ts. C'est l'intégralité de votre serveur de canal : il se connecte à Claude Code via stdio et écoute les HTTP POST sur le port 8788. Lorsqu'une requête arrive, il transmet le corps à Claude sous forme d'événement de canal.

#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

// Create the MCP server and declare it as a channel
const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    // this key is what makes it a channel — Claude Code registers a listener for it
    capabilities: { experimental: { 'claude/channel': {} } },
    // Claude Code delivers this to Claude as context when the server connects, so it knows how to handle these events
    instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
  },
)

// Connect to Claude Code over stdio (Claude Code spawns this process)
await mcp.connect(new StdioServerTransport())

// Start an HTTP server that forwards every POST to Claude
Bun.serve({
  port: 8788,  // any open port works
  // localhost-only: nothing outside this machine can POST
  hostname: '127.0.0.1',
  async fetch(req) {
    const body = await req.text()
    await mcp.notification({
      method: 'notifications/claude/channel',
      params: {
        content: body,  // becomes the body of the <channel> tag
        // each key becomes a tag attribute, e.g. <channel path="/" method="POST">
        meta: { path: new URL(req.url).pathname, method: req.method },
      },
    })
    return new Response('ok')
  },
})

Le fichier configure le serveur, se connecte via stdio et démarre un listener HTTP, dans cet ordre :

  • Configuration du serveur : crée le serveur MCP avec claude/channel dans ses capacités, ce qui indique à Claude Code qu'il s'agit d'un canal. Claude Code transmet la chaîne instructions à Claude comme contexte lorsque le serveur se connecte : indiquez à Claude quels événements attendre, s'il doit répondre et, le cas échéant, comment acheminer les réponses.
  • Connexion stdio : se connecte à Claude Code via stdin/stdout. C'est la norme pour tout serveur MCP.
  • Listener HTTP : démarre un serveur web local sur le port 8788. Chaque corps de POST est transmis à Claude comme événement de canal via mcp.notification(). Le content devient le corps de l'événement, et chaque entrée meta devient un attribut de la balise <channel>. Le listener a besoin d'accéder à l'instance mcp, il s'exécute donc dans le même processus. Vous pourriez le séparer en modules distincts pour un projet plus important.

3. Enregistrer votre serveur auprès de Claude Code

Ajoutez le serveur à votre configuration MCP pour que Claude Code sache comment le démarrer. Pour un .mcp.json au niveau du projet dans le même répertoire, utilisez un chemin relatif. Pour une configuration au niveau utilisateur dans ~/.claude.json, utilisez le chemin absolu complet afin que le serveur soit trouvé depuis n'importe quel projet :

{
  "mcpServers": {
    "webhook": { "command": "bun", "args": ["./webhook.ts"] }
  }
}

Claude Code lit votre configuration MCP au démarrage et lance chaque serveur en tant que sous-processus.

4. Le tester

Pendant la research preview, les canaux personnalisés ne figurent pas sur la liste d'autorisation, démarrez donc Claude Code avec le flag de développement :

claude --dangerously-load-development-channels server:webhook

Claude Code affiche d'abord une boîte de dialogue d'avertissement en plein écran listant les canaux de développement que vous chargez. Sélectionnez I am using this for local development pour continuer, ou Exit pour quitter.

La première fois que vous démarrez une session dans ce projet, Claude Code demande également votre consentement avant d'utiliser le nouveau serveur de .mcp.json. La boîte de dialogue indique « New MCP server found in this project: webhook ». Sélectionnez Use this MCP server pour continuer.

Une fois que vous avez accepté, Claude Code lance votre webhook.ts en tant que sous-processus, et le listener HTTP démarre automatiquement sur le port que vous avez configuré, 8788 dans cet exemple. Vous n'avez pas besoin d'exécuter le serveur vous-même.

Un avis grisé sous la bannière de démarrage confirme que le canal est enregistré : Channels (experimental) messages from server:webhook inject directly in this session · restart without --dangerously-load-development-channels to stop.

Si vous voyez « blocked by org policy », l'administrateur de votre organisation doit d'abord activer les canaux.

Dans un terminal séparé, simulez un webhook en envoyant un HTTP POST contenant un message à votre serveur. Cet exemple envoie une alerte d'échec CI au port 8788 (ou à celui que vous avez configuré) :

curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"

Le payload arrive dans le contexte de Claude sous forme de balise <channel> :

<channel source="webhook" path="/" method="POST">
build failed on main: https://ci.example.com/run/1234
</channel>

Votre terminal affiche l'événement sous forme de résumé sur une ligne, ← webhook: build failed on main: https://ci.example.com/run/1234, plutôt que la balise brute. Vous verrez ensuite Claude commencer à répondre : lire des fichiers, exécuter des commandes, ou tout ce que le message demande. Il s'agit d'un canal unidirectionnel : Claude agit dans votre session mais ne renvoie rien via le webhook. Pour ajouter des réponses, consultez Exposer un outil de réponse.

Si l'événement n'arrive pas, le diagnostic dépend de ce que curl a renvoyé :

  • curl réussit mais rien n'atteint Claude : exécutez /mcp dans votre session pour vérifier l'état du serveur. Un état failed signifie généralement une erreur de dépendance ou d'import dans votre fichier serveur. Pour voir la trace stderr, redémarrez avec claude --debug --dangerously-load-development-channels server:webhook et consultez le log de débogage dans ~/.claude/debug/<session-id>.txt.
  • curl échoue avec « connection refused » : soit le port n'est pas encore lié, soit un processus résiduel d'une exécution précédente l'occupe. lsof -i :<port> indique ce qui écoute ; arrêtez le processus résiduel avec kill avant de redémarrer votre session.

Le serveur fakechat étend ce modèle avec une interface web, des pièces jointes et un outil de réponse pour un chat bidirectionnel.

Tester pendant l'aperçu de recherche

Pendant l'aperçu de recherche, chaque canal doit être sur la liste d'approbation pour s'enregistrer. Le drapeau de développement contourne la liste d'approbation pour des entrées spécifiques après une invite de confirmation. Cet exemple montre les deux types d'entrée :

# Tester un plugin que vous développez
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace

# Tester un serveur .mcp.json nu (pas encore d'enveloppe de plugin)
claude --dangerously-load-development-channels server:webhook

Exécutez le flag de développement dans une session interactive, où Claude Code peut afficher la demande de confirmation. Si vous le passez en mode non interactif avec -p ou via l'Agent SDK, Claude Code ignore le flag et le canal ne s'enregistre pas.

Le contournement est par entrée. Combiner ce drapeau avec --channels n'étend pas le contournement aux entrées --channels. Pendant l'aperçu de recherche, votre canal n'est pas sur la liste d'approbation approuvée, donc il reste sur le drapeau de développement pendant que vous le construisez et le testez.

[!NOTE]

Ce drapeau ignore uniquement la liste d'approbation. La politique d'organisation channelsEnabled s'applique toujours. Ne l'utilisez pas pour exécuter des canaux de sources non fiables.

Options du serveur

Un canal définit ces options dans le constructeur Server. Les champs instructions et capabilities.tools sont MCP standard ; capabilities.experimental['claude/channel'] et capabilities.experimental['claude/channel/permission'] sont les ajouts spécifiques au canal :

Champ Type Description
capabilities.experimental['claude/channel'] object Requis. Toujours {}. La présence enregistre l'écouteur de notification.
capabilities.experimental['claude/channel/permission'] object ou false Optionnel. Définissez-le sur {} pour déclarer que ce canal peut recevoir des demandes de relais de permission. Quand déclaré, Claude Code transmet les invites d'approbation d'outils à votre canal afin que vous puissiez les approuver ou les refuser à distance. Pour refuser, omettez la clé ou définissez-la sur false. Avant v2.1.234, Claude Code traitait false comme déclaré. Consultez Relayer les invites de permission.
capabilities.tools object Bidirectionnel uniquement. Toujours {}. Capacité d'outil MCP standard. Consultez Exposer un outil de réponse.
instructions string Recommandé. Claude Code la transmet à Claude en tant que contexte quand le serveur se connecte. Dites à Claude quels événements attendre, ce que les attributs de la balise <channel> signifient, s'il faut répondre et, le cas échéant, quel outil utiliser et quel attribut repasser (comme chat_id).

Pour créer un canal unidirectionnel, omettez capabilities.tools. Cet exemple montre une configuration bidirectionnelle avec la capacité de canal, les outils et les instructions définis :

import { Server } from '@modelcontextprotocol/sdk/server/index.js'

const mcp = new Server(
  { name: 'your-channel', version: '0.0.1' },
  {
    capabilities: {
      experimental: { 'claude/channel': {} },  // enregistre l'écouteur de canal
      tools: {},  // omettez pour les canaux unidirectionnels
    },
    // Claude Code la transmet à Claude en tant que contexte quand le serveur se connecte, afin qu'il sache comment gérer vos événements
    instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
  },
)

Format de notification

Votre serveur émet notifications/claude/channel avec deux paramètres :

Champ Type Description
content string Le corps de l'événement. Livré en tant que corps de la balise <channel>.
meta Record<string, string> Optionnel. Chaque entrée devient un attribut sur la balise <channel> pour le contexte de routage comme l'ID de chat, le nom de l'expéditeur ou la gravité de l'alerte. Les clés doivent être des identifiants : lettres, chiffres et traits de soulignement uniquement. Les clés contenant des tirets ou d'autres caractères sont silencieusement supprimées.

Votre serveur envoie des événements en appelant mcp.notification() sur l'instance Server. Cet exemple envoie une alerte d'échec CI avec deux clés 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'événement arrive dans le contexte de Claude enveloppé dans une balise <channel>. L'attribut source est défini automatiquement à partir du nom configuré de votre serveur :

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

Claude Code ne reconnaît pas les notifications. L'await sur mcp.notification() se résout quand le message est écrit au transport, pas quand Claude l'a traité. Si la session n'a pas chargé votre serveur en tant que canal, ou si la politique d'organisation le bloque, les événements sont supprimés silencieusement sans erreur retournée à votre serveur.

Si vous avez besoin d'une confirmation de livraison, suivez l'état de l'événement dans votre serveur et exposez un outil de réponse que Claude peut appeler pour signaler le statut en retour.

Les événements s'accumulent dans la session et sont traités dans l'ordre. Si plusieurs notifications arrivent pendant que Claude est occupé, elles sont livrées ensemble au prochain tour et Claude les traite comme un groupe. Pour traiter les flux d'événements indépendants en parallèle, exécutez des sessions séparées.

Exposer un outil de réponse

Si votre canal est bidirectionnel, comme une passerelle de chat plutôt qu'un transmetteur d'alerte, exposez un outil MCP standard que Claude peut appeler pour envoyer des messages en retour. Rien dans l'enregistrement de l'outil n'est spécifique au canal. Un outil de réponse a trois composants :

  1. Une entrée tools: {} dans les capacités du constructeur Server afin que Claude Code découvre l'outil
  2. Des gestionnaires d'outils qui définissent le schéma de l'outil et implémentent la logique d'envoi
  3. Une chaîne instructions dans le constructeur Server qui indique à Claude quand et comment appeler l'outil

Pour ajouter ceux-ci au récepteur de webhook ci-dessus :

1. Activer la découverte d'outils

Dans votre constructeur Server dans webhook.ts, ajoutez tools: {} aux capacités afin que Claude Code sache que votre serveur offre des outils :

capabilities: {
  experimental: { 'claude/channel': {} },
  tools: {},  // active la découverte d'outils
},

2. Enregistrer l'outil de réponse

Ajoutez ce qui suit à webhook.ts. L'import va en haut du fichier avec vos autres imports ; les deux gestionnaires vont entre le constructeur Server et mcp.connect(). Cela enregistre un outil reply que Claude peut appeler avec un chat_id et un text :

// Ajoutez cet import en haut de webhook.ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'

// Claude interroge ceci au démarrage pour découvrir quels outils votre serveur offre
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Send a message back over this channel',
    // inputSchema indique à Claude quels arguments passer
    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 appelle ceci quand il veut invoquer un outil
mcp.setRequestHandler(CallToolRequestSchema, async req => {
  if (req.params.name === 'reply') {
    const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
    // send() est votre sortie : POST à votre plateforme de chat, ou pour les tests locaux
    // la diffusion SSE montrée dans l'exemple complet ci-dessous.
    send(`Reply to ${chat_id}: ${text}`)
    return { content: [{ type: 'text', text: 'sent' }] }
  }
  throw new Error(`unknown tool: ${req.params.name}`)
})

3. Mettre à jour les instructions

Mettez à jour la chaîne instructions dans votre constructeur Server afin que Claude sache router les réponses via l'outil. Cet exemple indique à Claude de passer chat_id à partir de la balise entrante :

instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.'

Voici le webhook.ts complet avec support bidirectionnel. Les réponses sortantes se diffusent via GET /events en utilisant Server-Sent Events (SSE), donc curl -N localhost:8788/events peut les regarder en direct ; le chat entrant arrive sur 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'

// --- Sortant : écrire à n'importe quel écouteur curl -N sur /events ---
// Un vrai pont enverrait un POST à votre plateforme de chat à la place.
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,  // ne pas fermer les flux SSE inactifs
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events : flux SSE afin que curl -N puisse regarder les réponses de Claude en direct
    if (req.method === 'GET' && url.pathname === '/events') {
      const stream = new ReadableStream({
        start(ctrl) {
          ctrl.enqueue(': connected\n\n')  // afin que curl affiche quelque chose immédiatement
          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 : transmettre à Claude en tant qu'événement de canal
    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')
  },
})

Le serveur fakechat montre un exemple plus complet avec pièces jointes et édition de messages.

Contrôler les messages entrants

Un canal non contrôlé est un vecteur d'injection de requête. Quiconque peut atteindre votre point de terminaison peut mettre du texte devant Claude. Un canal écoutant une plateforme de chat ou un point de terminaison public a besoin d'une vérification d'expéditeur réelle avant d'émettre quoi que ce soit.

Vérifiez l'expéditeur par rapport à une liste d'approbation avant d'appeler mcp.notification(). Cet exemple supprime tout message d'un expéditeur qui n'est pas dans l'ensemble :

const allowed = new Set(loadAllowlist())  // à partir de votre access.json ou équivalent

// à l'intérieur de votre gestionnaire de messages, avant d'émettre :
if (!allowed.has(message.from.id)) {  // expéditeur, pas salle
  return  // supprimer silencieusement
}
await mcp.notification({ ... })

Contrôlez sur l'identité de l'expéditeur, pas l'identité du chat ou de la salle : message.from.id dans l'exemple, pas message.chat.id. Dans les chats de groupe, ceux-ci diffèrent, et contrôler sur la salle laisserait quiconque dans un groupe approuvé injecter des messages dans la session.

Les canaux Telegram et Discord contrôlent sur une liste d'approbation d'expéditeur de la même manière. Ils amorçent la liste par appairage. Consultez l'une ou l'autre implémentation pour le flux d'appairage complet. Le canal iMessage adopte une approche différente : il détecte les propres adresses de l'utilisateur à partir de la base de données Messages au démarrage et les laisse passer automatiquement, avec d'autres expéditeurs ajoutés par poignée.

Relayer les demandes de permission

Lorsque Claude appelle un outil qui nécessite une approbation, la boîte de dialogue du terminal local s'ouvre et la session attend. Un canal bidirectionnel peut choisir de recevoir la même demande en parallèle et de vous la relayer sur un autre appareil. Les deux restent actives : vous pouvez répondre dans le terminal ou sur votre téléphone, et Claude Code applique la première réponse reçue et ferme l'autre.

Le relais couvre les approbations d'utilisation d'outils comme Bash, Write et Edit. Les boîtes de dialogue de confiance accordée au projet et de consentement aux serveurs MCP ne sont pas relayées ; elles n'apparaissent que dans le terminal local.

Claude Code v2.1.234 et versions ultérieures n'envoient les demandes de permission qu'aux serveurs enregistrés comme canaux pour la session, de sorte que le relais est soumis aux mêmes contrôles d'activation par session et d'organisation que la distribution des messages. Le relais exige également que vous activiez le serveur avec --channels ou le flag de développement, et que le serveur déclare la capacité de permission.

Fonctionnement du relais

Lorsqu'une demande de permission s'ouvre, la boucle de relais comporte quatre étapes :

  1. Claude Code génère un court ID de requête et notifie votre serveur
  2. Votre serveur transmet la demande et l'ID à votre application de chat
  3. L'utilisateur distant répond par oui ou par non avec cet ID
  4. Votre gestionnaire entrant analyse la réponse pour en tirer un verdict, et Claude Code ne l'applique que si l'ID correspond à une requête ouverte

La boîte de dialogue du terminal local reste ouverte pendant tout ce processus. Si quelqu'un au terminal répond avant l'arrivée du verdict distant, c'est cette réponse qui est appliquée et la requête distante en attente est abandonnée.

Diagramme de séquence : Claude Code envoie une notification permission_request au serveur de canal, le serveur met en forme la demande et l'envoie à l'application de chat, l'humain répond avec un verdict, et le serveur analyse cette réponse pour la transformer en notification permission renvoyée à Claude Code

Champs de la demande de permission

La notification sortante de Claude Code est notifications/claude/channel/permission_request. Comme pour la notification de canal, le transport est du MCP standard, mais la méthode et le schéma sont des extensions de Claude Code. L'objet params comporte quatre champs de type chaîne que votre serveur met en forme dans la demande sortante :

Champ Description
request_id Cinq lettres minuscules tirées de a-z sans l, afin qu'il ne soit jamais confondu avec un 1 ou un I lorsqu'il est saisi sur un téléphone. Incluez-le dans votre demande sortante pour qu'il puisse être renvoyé dans la réponse. Claude Code n'accepte qu'un verdict portant un ID qu'il a émis. La boîte de dialogue du terminal local n'affiche pas cet ID ; votre gestionnaire sortant est donc le seul moyen de le connaître.
tool_name Nom de l'outil que Claude souhaite utiliser, par exemple Bash ou Write.
description Résumé lisible de ce que fait cet appel d'outil spécifique, jamais la commande elle-même. Pour un appel Bash, il s'agit de la description de la commande par Claude ; lorsque le modèle ne fournit aucune description, le champ contient la constante Run shell command et ne comporte aucun détail sur la commande. Affichez input_preview lorsque vous avez de la place.
input_preview Les arguments de l'outil sous forme de texte d'affichage au format JSON, indexé par champ de premier niveau. Pour Bash, il s'agit de la commande ; pour Write, du chemin du fichier et du contenu. Omettez-le de votre demande si vous n'avez de place que pour un message d'une ligne. C'est votre serveur qui décide de ce qu'il affiche.

Les clients sous Claude Code v2.1.211 ou version ultérieure assainissent description et input_preview avant de les relayer. Attendez-vous à trois modifications dans le texte que vous recevez :

  • Claude Code neutralise les caractères de forçage de direction, les caractères invisibles et les caractères ressemblant à des guillemets et à des chevrons.
  • Claude Code réduit chaque suite d'espaces à un seul espace.
  • Claude Code relaie le texte en entier jusqu'à 3 500 points de code. Pour une valeur plus longue, vous recevez son début et sa fin autour d'un marqueur comptabilisé ⋯ N code points elided ⋯. La fin d'une commande longue parvient donc toujours à la personne qui approuve.

Pour input_preview, Claude Code applique la limite de 3 500 à chaque champ de premier niveau des arguments séparément et conserve les guillemets structurels propres au JSON. Les clients antérieurs à v2.1.211 relaient description tel quel et coupent input_preview à 200 unités UTF-16 avec des points de suspension à la fin.

Les clients sous Claude Code v2.1.234 ou version ultérieure relaient le marqueur (value unserializable) à la place d'une valeur de champ d'input_preview qu'ils ne peuvent pas sérialiser en toute sécurité, comme une structure circulaire ou un tableau extrêmement volumineux. Vous recevez toujours la clé du champ, et les autres champs de l'aperçu restent inchangés.

Les clients sous Claude Code v2.1.234 ou version ultérieure masquent également les identifiants dans description et input_preview. Vous recevez [REDACTED] à la place d'un jeton d'identification de fournisseur reconnaissable, comme une clé API ou un jeton d'accès personnel. Attendez-vous à trois effets du masquage lorsque vous affichez les champs :

  • Claude Code masque les noms de clés dans input_preview ainsi que leurs valeurs. Un nom de clé que vous affichez peut ne pas correspondre au nom de clé dans l'entrée.
  • Claude Code ne masque jamais un segment contenant une syntaxe shell, des caractères de chemin ou des caractères d'URL. Un masque ne peut pas dissimuler la commande, le chemin de fichier ou la destination en cours d'approbation.
  • Claude Code ne masque pas un secret dépourvu de préfixe reconnaissable, ni un secret qui s'étend sur des espaces, comme un bloc de clé privée. Tous deux parviennent à votre serveur sans masquage.

Le masquage ne change pas qui reçoit les champs. Tout ce qui reste non masqué n'est envoyé qu'aux serveurs que vous avez activés avec --channels ou le flag de développement. Considérez les deux champs comme non fiables, sauf si vous contrôlez le parc de clients.

Le verdict que votre serveur renvoie est notifications/claude/channel/permission avec deux champs : request_id, qui reprend l'ID ci-dessus, et behavior, défini sur 'allow' ou 'deny'. Allow laisse l'appel d'outil se poursuivre ; deny le rejette. Aucun des deux verdicts n'affecte les appels futurs.

Ajouter le relais à un pont de chat

L'ajout du relais des permissions à un canal bidirectionnel nécessite trois composants :

  1. Une entrée claude/channel/permission: {} sous les capacités experimental dans votre constructeur Server, afin que Claude Code sache qu'il doit transmettre les demandes
  2. Un gestionnaire de notification pour notifications/claude/channel/permission_request qui met en forme la demande et l'envoie via l'API de votre plateforme
  3. Une vérification dans votre gestionnaire de messages entrants qui reconnaît yes <id> ou no <id> et émet un verdict notifications/claude/channel/permission au lieu de transmettre le texte à Claude

Ne déclarez la capacité que si votre canal authentifie l'expéditeur, car toute personne pouvant répondre via votre canal peut approuver ou refuser l'utilisation d'outils dans votre session.

Pour les ajouter à un pont de chat bidirectionnel comme celui assemblé dans Exposer un outil de réponse :

1. Déclarer la capacité de permission

Dans votre constructeur Server, ajoutez claude/channel/permission: {} à côté de claude/channel sous experimental :

capabilities: {
  experimental: {
    'claude/channel': {},
    'claude/channel/permission': {},  // opt in to permission relay
  },
  tools: {},
},

2. Gérer la requête entrante

Enregistrez un gestionnaire de notification entre votre constructeur Server et mcp.connect(). Claude Code l'appelle avec les quatre champs de la requête lorsqu'une boîte de dialogue de permission s'ouvre. Votre gestionnaire met en forme la demande pour votre plateforme et inclut des instructions pour répondre avec l'ID :

import { z } from 'zod'

// setNotificationHandler routes by z.literal on the method field,
// so this schema is both the validator and the dispatch key
const PermissionRequestSchema = z.object({
  method: z.literal('notifications/claude/channel/permission_request'),
  params: z.object({
    request_id: z.string(),     // five lowercase letters, include verbatim in your prompt
    tool_name: z.string(),      // e.g. "Bash", "Write"
    description: z.string(),    // summary of this call. Treat as untrusted.
    input_preview: z.string(),  // tool args as JSON-shaped text. Treat as untrusted.
  }),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  // send() is your outbound: POST to your chat platform, or for local
  // testing the SSE broadcast shown in the full example below.
  send(
    `Claude wants to run ${params.tool_name}: ${params.description}\n` +
    // input_preview carries the actual arguments; render it when you
    // have room: for Bash the description alone may be just
    // "Run shell command" with zero command detail
    `${params.input_preview}\n\n` +
    // the ID in the instruction is what your inbound handler parses in Step 3
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})

3. Intercepter le verdict dans votre gestionnaire entrant

Votre gestionnaire entrant est la boucle ou le callback qui reçoit les messages de votre plateforme : le même endroit où vous filtrez selon l'expéditeur et émettez notifications/claude/channel pour transmettre le chat à Claude. Ajoutez, avant l'appel de transmission du chat, une vérification qui reconnaît le format du verdict et émet la notification de permission à la place.

L'expression régulière correspond au format d'ID généré par Claude Code : cinq lettres, jamais l. Le flag /i tolère la mise en majuscule de la réponse par la correction automatique du téléphone ; convertissez l'ID capturé en minuscules avant de le renvoyer.

// matches "y abcde", "yes abcde", "n abcde", "no abcde"
// [a-km-z] is the ID alphabet Claude Code uses (lowercase, skips 'l')
// /i tolerates phone autocorrect; lowercase the capture before sending
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  // gate on sender first

  const m = PERMISSION_REPLY_RE.exec(message.text)
  if (m) {
    // m[1] is the verdict word, m[2] is the request ID
    // emit the verdict notification back to Claude Code instead of chat
    await mcp.notification({
      method: 'notifications/claude/channel/permission',
      params: {
        request_id: m[2].toLowerCase(),  // normalize in case of autocorrect caps
        behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
      },
    })
    return  // handled as verdict, don't also forward as chat
  }

  // didn't match verdict format: fall through to the normal chat path
  await mcp.notification({
    method: 'notifications/claude/channel',
    params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
  })
}

Si une réponse distante n'a aucun effet et que la boîte de dialogue du terminal local reste ouverte, vérifiez les causes suivantes :

  • Le format est incorrect : l'expression régulière de votre gestionnaire entrant ne correspond pas, de sorte qu'un texte comme approve it ou yes sans ID est transmis à Claude comme un message normal.
  • L'ID est incorrect : votre serveur émet un verdict, mais Claude Code ne trouve aucune requête ouverte avec cet ID et l'abandonne silencieusement.
  • L'ID a été utilisé par une demande précédente : votre serveur émet un verdict avec l'ID de la requête, mais une demande précédente de la session avait le même ID. Dès qu'un ID se répète, Claude Code abandonne tout verdict de canal qui le porte, afin qu'une réponse destinée à une demande ne puisse pas approuver l'autre. Un ID étant composé de cinq lettres aléatoires, une répétition est rare. Répondez plutôt au terminal.

Exemple complet

Le fichier webhook.ts assemblé ci-dessous combine les trois extensions de cette page : l'outil de réponse, le filtrage des expéditeurs et le relais des permissions. Si vous commencez ici, vous aurez également besoin de la configuration du projet et de l'entrée .mcp.json du guide initial.

Pour que les deux directions puissent être testées avec curl, l'écouteur HTTP sert deux chemins :

  • GET /events : maintient un flux SSE ouvert et envoie chaque message sortant sous forme de ligne data:, afin que curl -N puisse observer en direct l'arrivée des réponses de Claude et des demandes de permission.
  • POST / : le côté entrant, le même gestionnaire que précédemment, avec désormais la vérification du format de verdict insérée avant la branche de transmission du chat.
#!/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: write to any curl -N listeners on /events ---
// A real bridge would POST to your chat platform instead.
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)
}

// Sender allowlist. For the local walkthrough we trust the single X-Sender
// header value "dev"; a real bridge would check the platform's user ID.
const allowed = new Set(['dev'])

const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    capabilities: {
      experimental: {
        'claude/channel': {},
        'claude/channel/permission': {},  // opt in to permission relay
      },
      tools: {},
    },
    instructions:
      'Messages arrive as <channel source="webhook" chat_id="...">. ' +
      'Reply with the reply tool, passing the chat_id from the tag.',
  },
)

// --- reply tool: Claude calls this to send a message back ---
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}`)
})

// --- permission relay: Claude Code (not Claude) calls this when a dialog opens
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` +
    `${params.input_preview}\n\n` +
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})

await mcp.connect(new StdioServerTransport())

// --- HTTP on :8788: GET /events streams outbound, POST routes inbound ---
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,  // don't close idle SSE streams
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events: SSE stream so curl -N can watch replies and prompts live
    if (req.method === 'GET' && url.pathname === '/events') {
      const stream = new ReadableStream({
        start(ctrl) {
          ctrl.enqueue(': connected\n\n')  // so curl shows something immediately
          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' },
      })
    }

    // everything else is inbound: gate on sender first
    const body = await req.text()
    const sender = req.headers.get('X-Sender') ?? ''
    if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })

    // check for verdict format before treating as 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')
    }

    // normal chat: forward to Claude as a channel event
    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')
  },
})

Testez le chemin du verdict dans trois terminaux. Le premier est votre session Claude Code, démarrée avec le flag de développement afin qu'elle lance webhook.ts :

claude --dangerously-load-development-channels server:webhook

Ce guide teste la boîte de dialogue de permission elle-même ; une fois la session ouverte, appuyez donc sur Shift+Tab jusqu'à ce que la barre d'état affiche ⏸ manual mode on. En mode auto, le classifieur déciderait de l'appel reply à votre place, et aucune boîte de dialogue ne s'ouvrirait pour que le côté distant y réponde.

Dans le deuxième, diffusez le côté sortant afin de voir les réponses de Claude et les éventuelles demandes de permission au moment où elles se déclenchent :

curl -N localhost:8788/events

Dans le troisième, envoyez un message qui amènera Claude à essayer d'exécuter une commande :

curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788

Lister des fichiers est une opération en lecture seule, donc Claude l'exécute sans approbation. La boîte de dialogue de permission s'ouvre lorsque Claude appelle l'outil reply pour renvoyer sa réponse. La boîte de dialogue locale s'ouvre dans votre terminal Claude Code et, un instant plus tard, la demande pour mcp__webhook__reply apparaît dans le flux /events, avec l'ID à cinq lettres. Approuvez-la depuis le côté distant :

curl -d "yes <id>" -H "X-Sender: dev" localhost:8788

La boîte de dialogue locale se ferme, l'outil reply s'exécute et la réponse de Claude apparaît dans le flux.

Les trois éléments propres aux canaux dans ce fichier :

  • Capacités dans le constructeur Server : claude/channel enregistre l'écouteur de notifications, claude/channel/permission active le relais des permissions, tools permet à Claude de découvrir l'outil de réponse.
  • Chemins sortants : le gestionnaire de l'outil reply est ce que Claude appelle pour les réponses conversationnelles ; le gestionnaire de notification PermissionRequestSchema est ce que Claude Code appelle lorsqu'une boîte de dialogue de permission s'ouvre. Tous deux appellent send() pour diffuser via /events, mais ils sont déclenchés par des parties différentes du système.
  • Gestionnaire HTTP : GET /events maintient un flux SSE ouvert afin que curl puisse observer le sortant en direct ; POST est le côté entrant, filtré selon l'en-tête X-Sender. Un corps yes <id> ou no <id> est envoyé à Claude Code sous forme de notification de verdict et n'atteint jamais Claude ; tout le reste est transmis à Claude sous forme d'événement de canal.

Empaqueter en tant que plugin

Pour rendre votre canal installable et partageable, enveloppez-le dans un plugin et publiez-le sur un marketplace. Les utilisateurs l'installent avec /plugin install, puis l'activent par session avec --channels plugin:<name>@<marketplace>.

Un canal publié sur votre propre marketplace a toujours besoin de --dangerously-load-development-channels pour s'exécuter, car il n'est pas sur la liste d'approbation. La liste d'approbation par défaut est les plugins de canal dans claude-plugins-official. Le marketplace communautaire n'est pas sur la liste d'approbation des canaux.

Si vous travaillez avec un contact partenaire Anthropic, contactez-le pour coordonner une inscription au marketplace officiel. Sur les plans Team et Enterprise, un administrateur peut plutôt inclure votre plugin dans la liste allowedChannelPlugins de l'organisation, qui remplace la liste d'approbation Anthropic par défaut.

Voir aussi

  • Canaux pour installer et utiliser Telegram, Discord, iMessage ou la démo fakechat, et pour activer les canaux pour une organisation Team ou Enterprise
  • Implémentations de canaux fonctionnels pour le code serveur complet avec flux d'appairage, outils de réponse et pièces jointes
  • MCP pour le protocole sous-jacent que les serveurs de canal implémentent
  • Plugins pour empaqueter votre canal afin que les utilisateurs puissent l'installer avec /plugin install