433 433
434Les canaux [Telegram](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram) et [Discord](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord) contrôlent sur une liste d'approbation d'expéditeur de la même manière. Ils amorçent la liste par [appairage](https://code.claude.com/docs/fr/channels.md#security). Consultez l'une ou l'autre implémentation pour le flux d'appairage complet. Le canal [iMessage](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/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.434Les canaux [Telegram](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram) et [Discord](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord) contrôlent sur une liste d'approbation d'expéditeur de la même manière. Ils amorçent la liste par [appairage](https://code.claude.com/docs/fr/channels.md#security). Consultez l'une ou l'autre implémentation pour le flux d'appairage complet. Le canal [iMessage](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/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.
435 435
436## Relayer les invites de permission436## Relayer les demandes de permission
437 437
438Quand Claude appelle un outil qui a besoin d'approbation, la boîte de dialogue du terminal local s'ouvre et la session attend. Un canal bidirectionnel peut opter pour recevoir la même invite en parallèle et la relayer vers vous sur un autre appareil. Les deux restent actifs : vous pouvez répondre dans le terminal ou sur votre téléphone, et Claude Code applique la réponse qui arrive en premier et ferme l'autre.438Lorsque 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.
439 439
440Le relais couvre les approbations d'utilisation d'outils comme `Bash`, `Write` et `Edit`. La confiance du projet et les boîtes de dialogue de consentement du serveur MCP ne relaient pas ; celles-ci n'apparaissent que dans le terminal local.440Le 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.
441 441
442Claude Code v2.1.234 et versions ultérieures envoient les demandes de permission uniquement aux serveurs qu'il a enregistrés en tant que canaux pour la session, donc le relais se situe derrière les mêmes [contrôles de consentement de session et d'organisation](https://code.claude.com/docs/fr/channels.md#security) que la livraison des messages. Le relais nécessite également que vous optiez pour le serveur avec `--channels` ou le drapeau de développement, et nécessite que le serveur déclare la capacité de permission.442Claude 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](https://code.claude.com/docs/fr/channels.md#security) 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.
443 443
444### Comment fonctionne le relais444### Fonctionnement du relais
445 445
446Quand une invite de permission s'ouvre, la boucle de relais a quatre étapes :446Lorsqu'une demande de permission s'ouvre, la boucle de relais comporte quatre étapes :
447 447
4481. Claude Code génère un court ID de demande et notifie votre serveur4481. Claude Code génère un court ID de requête et notifie votre serveur
4492. Votre serveur transmet l'invite et l'ID à votre application de chat4492. Votre serveur transmet la demande et l'ID à votre application de chat
4503. L'utilisateur distant répond par oui ou non et cet ID4503. L'utilisateur distant répond par oui ou par non avec cet ID
4514. Votre gestionnaire entrant analyse la réponse en un verdict, et Claude Code l'applique uniquement si l'ID correspond à une demande ouverte4514. 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
452 452
453La boîte de dialogue du terminal local reste ouverte pendant tout cela. Si quelqu'un au terminal répond avant l'arrivée du verdict distant, cette réponse est appliquée à la place et la demande distante en attente est supprimée.453La 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.
454 454
455455
456 456
457### Champs de demande de permission457### Champs de la demande de permission
458 458
459La notification sortante de Claude Code est `notifications/claude/channel/permission_request`. Comme la [notification de canal](#notification-format), le transport est MCP standard mais la méthode et le schéma sont des extensions Claude Code. L'objet `params` a quatre champs de chaîne que votre serveur formate dans l'invite sortante :459La notification sortante de Claude Code est `notifications/claude/channel/permission_request`. Comme pour la [notification de canal](#notification-format), 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 :
460 460
461| Champ | Description |461| Champ | Description |
462| - | - |462| - | - |
463| `request_id` | Cinq lettres minuscules tirées de `a`-`z` sans `l`, donc cela ne se lit jamais comme un `1` ou un `I` quand tapé sur un téléphone. Incluez-le dans votre invite sortante afin qu'il puisse être répété dans la réponse. Claude Code n'accepte un verdict que s'il porte un ID qu'il a émis. La boîte de dialogue du terminal local n'affiche pas cet ID, donc votre gestionnaire sortant est le seul moyen de l'apprendre. |463| `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. |
464| `tool_name` | Nom de l'outil que Claude veut utiliser, par exemple `Bash` ou `Write`. |464| `tool_name` | Nom de l'outil que Claude souhaite utiliser, par exemple `Bash` ou `Write`. |
465| `description` | Résumé lisible par l'homme de ce que cet appel d'outil spécifique fait, jamais la commande elle-même. Pour un appel Bash c'est la description de Claude de la commande ; quand le modèle ne donne aucune description, le champ est la constante `Run shell command` et ne porte aucun détail de commande. Rendez `input_preview` quand vous avez de la place. |465| `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. |
466| `input_preview` | Les arguments de l'outil sous forme de texte en forme JSON, tronqués par champ de niveau supérieur. Pour Bash c'est la commande ; pour Write, le chemin du fichier et le contenu. Omettez-le de votre invite si vous n'avez de la place que pour un message d'une ligne. Votre serveur décide ce qu'il faut afficher. |466| `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. |
467 467
468Les clients sur Claude Code v2.1.211 ou versions ultérieures assainissent `description` et `input_preview` avant de les relayer. Attendez-vous à trois changements dans le texte que vous recevez :468Les 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 :
469 469
470* Claude Code neutralise les caractères de remplacement de direction, les caractères invisibles, et les guillemets et crochets qui ressemblent à des lookalikes.470* 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.
471* Claude Code replie chaque suite d'espaces en un seul espace.471* Claude Code réduit chaque suite d'espaces à un seul espace.
472* Claude Code relaye 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 `⋯ N code points elided ⋯` compté. La fin d'une longue commande atteint toujours l'approbateur.472* 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.
473 473
474Pour `input_preview`, Claude Code applique la limite de 3 500 à chaque champ de niveau supérieur des arguments séparément et conserve les guillemets structurels du JSON. Les clients antérieurs à v2.1.211 relaient `description` brut et coupent `input_preview` à 200 unités UTF-16 avec des points de suspension à la fin.474Pour `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.
475 475
476Les clients sur Claude Code v2.1.234 ou versions ultérieures relaient le marqueur `(value unserializable)` à la place d'une valeur de champ `input_preview` qu'ils ne peuvent pas sérialiser en toute sécurité, comme une structure circulaire ou un tableau extrêmement grand. Vous recevez toujours la clé du champ, et les autres champs de l'aperçu sont inchangés.476Les 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.
477 477
478Les clients sur Claude Code v2.1.234 ou versions ultérieures masquent également les identifiants dans `description` et `input_preview`. Vous recevez `[REDACTED]` à la place d'un jeton d'identifiant de fournisseur reconnaissable, comme une clé API ou un jeton d'accès personnel. Attendez-vous à trois effets du masquage quand vous rendez les champs :478Les 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 :
479 479
480* Claude Code masque les noms de clés à l'intérieur de `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.480* 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.
481* Claude Code ne masque jamais une plage qui contient la syntaxe du shell, les caractères de chemin ou les caractères d'URL. Un masque ne peut pas cacher la commande, le chemin du fichier ou la destination en cours d'approbation.481* 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.
482* Claude Code ne masque pas un secret qui manque d'un préfixe reconnaissable, ou un secret qui s'étend sur plusieurs espaces, comme un bloc de clé privée. Les deux atteignent votre serveur sans masque.482* 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.
483 483
484Le masquage ne change pas qui reçoit les champs. Tout ce qui reste sans masque va uniquement aux serveurs pour lesquels vous avez opté avec `--channels` ou le drapeau de développement. Traitez les deux champs comme non fiables sauf si vous contrôlez la flotte de clients.484Le 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.
485 485
486Le verdict que votre serveur renvoie est `notifications/claude/channel/permission` avec deux champs : `request_id` répétant l'ID ci-dessus, et `behavior` défini sur `'allow'` ou `'deny'`. Allow laisse l'appel d'outil procéder ; deny le rejette. Aucun verdict n'affecte les appels futurs.486Le 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.
487 487
488### Ajouter le relais à une passerelle de chat488### Ajouter le relais à un pont de chat
489 489
490Ajouter le relais de permission à un canal bidirectionnel prend trois composants :490L'ajout du relais des permissions à un canal bidirectionnel nécessite trois composants :
491 491
4921. Une entrée `claude/channel/permission: {}` sous les capacités `experimental` dans votre constructeur `Server` afin que Claude Code sache transmettre les invites4921. 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
4932. Un gestionnaire de notification pour `notifications/claude/channel/permission_request` qui formate l'invite et l'envoie via votre API de plateforme4932. 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
4943. Une vérification dans votre gestionnaire de message entrant qui reconnaît `yes <id>` ou `no <id>` et émet une notification de verdict `notifications/claude/channel/permission` au lieu de transmettre le texte à Claude4943. 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
495 495
496Déclarez uniquement la capacité si votre canal [authentifie l'expéditeur](#gate-inbound-messages), car quiconque peut répondre via votre canal peut approuver ou refuser l'utilisation d'outils dans votre session.496Ne déclarez la capacité que si votre canal [authentifie l'expéditeur](#gate-inbound-messages), car toute personne pouvant répondre via votre canal peut approuver ou refuser l'utilisation d'outils dans votre session.
497 497
498Pour ajouter ceux-ci à une passerelle de chat bidirectionnelle comme celle assemblée dans [Exposer un outil de réponse](#expose-a-reply-tool) :498Pour les ajouter à un pont de chat bidirectionnel comme celui assemblé dans [Exposer un outil de réponse](#expose-a-reply-tool) :
499 499
500**1. Déclarer la capacité de permission**500**1. Déclarer la capacité de permission**
501 501
505capabilities: {505capabilities: {
506 experimental: {506 experimental: {
507 'claude/channel': {},507 'claude/channel': {},
508 'claude/channel/permission': {}, // opter pour le relais de permission508 'claude/channel/permission': {}, // opt in to permission relay
509 },509 },
510 tools: {},510 tools: {},
511},511},
512```512```
513 513
514**2. Gérer la demande entrante**514**2. Gérer la requête entrante**
515 515
516Enregistrez un gestionnaire de notification entre votre constructeur `Server` et `mcp.connect()`. Claude Code l'appelle avec les [quatre champs de demande](#permission-request-fields) quand une boîte de dialogue de permission s'ouvre. Votre gestionnaire formate l'invite pour votre plateforme et inclut des instructions pour répondre avec l'ID :516Enregistrez un gestionnaire de notification entre votre constructeur `Server` et `mcp.connect()`. Claude Code l'appelle avec les [quatre champs de la requête](#permission-request-fields) 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 :
517 517
518```ts theme={null}518```ts theme={null}
519import { z } from 'zod'519import { z } from 'zod'
520 520
521// setNotificationHandler route par z.literal sur le champ method,521// setNotificationHandler routes by z.literal on the method field,
522// donc ce schéma est à la fois le validateur et la clé de dispatch522// so this schema is both the validator and the dispatch key
523const PermissionRequestSchema = z.object({523const PermissionRequestSchema = z.object({
524 method: z.literal('notifications/claude/channel/permission_request'),524 method: z.literal('notifications/claude/channel/permission_request'),
525 params: z.object({525 params: z.object({
526 request_id: z.string(), // cinq lettres minuscules, inclure verbatim dans votre invite526 request_id: z.string(), // five lowercase letters, include verbatim in your prompt
527 tool_name: z.string(), // par ex. "Bash", "Write"527 tool_name: z.string(), // e.g. "Bash", "Write"
528 description: z.string(), // résumé de cet appel. Traiter comme non fiable.528 description: z.string(), // summary of this call. Treat as untrusted.
529 input_preview: z.string(), // arguments d'outil en JSON. Traiter comme non fiable.529 input_preview: z.string(), // tool args as JSON-shaped text. Treat as untrusted.
530 }),530 }),
531})531})
532 532
533mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {533mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
534 // send() est votre sortie : POST à votre plateforme de chat, ou pour les tests locaux534 // send() is your outbound: POST to your chat platform, or for local
535 // la diffusion SSE montrée dans l'exemple complet ci-dessous.535 // testing the SSE broadcast shown in the full example below.
536 send(536 send(
537 `Claude wants to run ${params.tool_name}: ${params.description}\n` +537 `Claude wants to run ${params.tool_name}: ${params.description}\n` +
538 // input_preview porte les arguments réels ; rendez-le quand vous538 // input_preview carries the actual arguments; render it when you
539 // avez de la place : pour Bash la description seule peut être juste539 // have room: for Bash the description alone may be just
540 // "Run shell command" sans aucun détail de commande540 // "Run shell command" with zero command detail
541 `${params.input_preview}\n\n` +541 `${params.input_preview}\n\n` +
542 // l'ID dans l'instruction est ce que votre gestionnaire entrant analyse à l'étape 3542 // the ID in the instruction is what your inbound handler parses in Step 3
543 `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,543 `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
544 )544 )
545})545})
547 547
548**3. Intercepter le verdict dans votre gestionnaire entrant**548**3. Intercepter le verdict dans votre gestionnaire entrant**
549 549
550Votre gestionnaire entrant est la boucle ou le rappel qui reçoit les messages de votre plateforme : le même endroit où vous [contrôlez sur l'expéditeur](#gate-inbound-messages) et émettez `notifications/claude/channel` pour transmettre le chat à Claude. Ajoutez une vérification avant l'appel de transmission de chat qui reconnaît le format du verdict et émet la notification de permission à la place.550Votre 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](#gate-inbound-messages) 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.
551 551
552La regex correspond au format d'ID que Claude Code génère : cinq lettres, jamais `l`. Le drapeau `/i` tolère la correction automatique du téléphone en mettant en majuscules la réponse ; mettez en minuscules l'ID capturé avant de le renvoyer.552L'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.
553 553
554```ts theme={null}554```ts theme={null}
555// correspond à "y abcde", "yes abcde", "n abcde", "no abcde"555// matches "y abcde", "yes abcde", "n abcde", "no abcde"
556// [a-km-z] est l'alphabet d'ID que Claude Code utilise (minuscules, ignore 'l')556// [a-km-z] is the ID alphabet Claude Code uses (lowercase, skips 'l')
557// /i tolère la correction automatique du téléphone ; mettez en minuscules la capture avant d'envoyer557// /i tolerates phone autocorrect; lowercase the capture before sending
558const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i558const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
559 559
560async function onInbound(message: PlatformMessage) {560async function onInbound(message: PlatformMessage) {
561 if (!allowed.has(message.from.id)) return // contrôler sur l'expéditeur d'abord561 if (!allowed.has(message.from.id)) return // gate on sender first
562 562
563 const m = PERMISSION_REPLY_RE.exec(message.text)563 const m = PERMISSION_REPLY_RE.exec(message.text)
564 if (m) {564 if (m) {
565 // m[1] est le mot du verdict, m[2] est l'ID de demande565 // m[1] is the verdict word, m[2] is the request ID
566 // émettre la notification du verdict vers Claude Code au lieu du chat566 // emit the verdict notification back to Claude Code instead of chat
567 await mcp.notification({567 await mcp.notification({
568 method: 'notifications/claude/channel/permission',568 method: 'notifications/claude/channel/permission',
569 params: {569 params: {
570 request_id: m[2].toLowerCase(), // normaliser en cas de majuscules de correction automatique570 request_id: m[2].toLowerCase(), // normalize in case of autocorrect caps
571 behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',571 behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
572 },572 },
573 })573 })
574 return // géré comme verdict, ne pas aussi transmettre comme chat574 return // handled as verdict, don't also forward as chat
575 }575 }
576 576
577 // ne correspondait pas au format du verdict : passer au chemin de chat normal577 // didn't match verdict format: fall through to the normal chat path
578 await mcp.notification({578 await mcp.notification({
579 method: 'notifications/claude/channel',579 method: 'notifications/claude/channel',
580 params: { content: message.text, meta: { chat_id: String(message.chat.id) } },580 params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
582}582}
583```583```
584 584
585Une réponse distante qui ne correspond pas exactement au format attendu échoue de l'une de deux façons, et dans les deux cas la boîte de dialogue du terminal local reste ouverte :585Si 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 :
586 586
587* **Format différent** : la regex de votre gestionnaire entrant ne correspond pas, donc du texte comme `approve it` ou `yes` sans ID tombe comme un message normal à Claude.587* **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.
588* **Format correct, ID incorrect** : votre serveur émet un verdict, mais Claude Code ne trouve aucune demande ouverte avec cet ID et le supprime silencieusement.588* **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.
589* **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.
589 590
590### Exemple complet591### Exemple complet
591 592
592Le `webhook.ts` assemblé ci-dessous combine les trois extensions de cette page : l'outil de réponse, le contrôle de l'expéditeur et le relais de permission. Si vous commencez ici, vous aurez également besoin de la [configuration du projet et de l'entrée `.mcp.json`](#example-build-a-webhook-receiver) de la procédure pas à pas initiale.593Le 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`](#example-build-a-webhook-receiver) du guide initial.
593 594
594Pour rendre les deux directions testables à partir de curl, l'écouteur HTTP sert deux chemins :595Pour que les deux directions puissent être testées avec curl, l'écouteur HTTP sert deux chemins :
595 596
596* **`GET /events`** : maintient un flux SSE ouvert et envoie chaque message sortant en tant que ligne `data:`, donc `curl -N` peut regarder les réponses et les invites de permission de Claude arriver en direct.597* **`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.
597* **`POST /`** : le côté entrant, le même gestionnaire qu'avant, maintenant avec la vérification du format du verdict insérée avant la branche de transmission de chat.598* **`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.
598 599
599```ts title="webhook.ts complet avec relais de permission" expandable theme={null}600```ts title="Full webhook.ts with permission relay" expandable theme={null}
600#!/usr/bin/env bun601#!/usr/bin/env bun
601import { Server } from '@modelcontextprotocol/sdk/server/index.js'602import { Server } from '@modelcontextprotocol/sdk/server/index.js'
602import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'603import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
603import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'604import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
604import { z } from 'zod'605import { z } from 'zod'
605 606
606// --- Sortant : écrire à n'importe quel écouteur curl -N sur /events ---607// --- Outbound: write to any curl -N listeners on /events ---
607// Un vrai pont enverrait un POST à votre plateforme de chat à la place.608// A real bridge would POST to your chat platform instead.
608const listeners = new Set<(chunk: string) => void>()609const listeners = new Set<(chunk: string) => void>()
609function send(text: string) {610function send(text: string) {
610 const chunk = text.split('\n').map(l => `data: ${l}\n`).join('') + '\n'611 const chunk = text.split('\n').map(l => `data: ${l}\n`).join('') + '\n'
611 for (const emit of listeners) emit(chunk)612 for (const emit of listeners) emit(chunk)
612}613}
613 614
614// Liste d'approbation de l'expéditeur. Pour la procédure pas à pas locale nous faisons confiance à la valeur d'en-tête X-Sender unique615// Sender allowlist. For the local walkthrough we trust the single X-Sender
615// "dev" ; un vrai pont vérifierait l'ID utilisateur de la plateforme.616// header value "dev"; a real bridge would check the platform's user ID.
616const allowed = new Set(['dev'])617const allowed = new Set(['dev'])
617 618
618const mcp = new Server(619const mcp = new Server(
621 capabilities: {622 capabilities: {
622 experimental: {623 experimental: {
623 'claude/channel': {},624 'claude/channel': {},
624 'claude/channel/permission': {}, // opter pour le relais de permission625 'claude/channel/permission': {}, // opt in to permission relay
625 },626 },
626 tools: {},627 tools: {},
627 },628 },
628 instructions:629 instructions:
629 'Les messages arrivent sous la forme <channel source="webhook" chat_id="...">. ' +630 'Messages arrive as <channel source="webhook" chat_id="...">. ' +
630 'Répondez avec l\'outil de réponse, en passant le chat_id de la balise.',631 'Reply with the reply tool, passing the chat_id from the tag.',
631 },632 },
632)633)
633 634
634// --- outil de réponse : Claude appelle ceci pour envoyer un message en retour ---635// --- reply tool: Claude calls this to send a message back ---
635mcp.setRequestHandler(ListToolsRequestSchema, async () => ({636mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
636 tools: [{637 tools: [{
637 name: 'reply',638 name: 'reply',
638 description: 'Envoyer un message en retour sur ce canal',639 description: 'Send a message back over this channel',
639 inputSchema: {640 inputSchema: {
640 type: 'object',641 type: 'object',
641 properties: {642 properties: {
642 chat_id: { type: 'string', description: 'La conversation dans laquelle répondre' },643 chat_id: { type: 'string', description: 'The conversation to reply in' },
643 text: { type: 'string', description: 'Le message à envoyer' },644 text: { type: 'string', description: 'The message to send' },
644 },645 },
645 required: ['chat_id', 'text'],646 required: ['chat_id', 'text'],
646 },647 },
656 throw new Error(`unknown tool: ${req.params.name}`)657 throw new Error(`unknown tool: ${req.params.name}`)
657})658})
658 659
659// --- relais de permission : Claude Code (pas Claude) appelle ceci quand une boîte de dialogue s'ouvre660// --- permission relay: Claude Code (not Claude) calls this when a dialog opens
660const PermissionRequestSchema = z.object({661const PermissionRequestSchema = z.object({
661 method: z.literal('notifications/claude/channel/permission_request'),662 method: z.literal('notifications/claude/channel/permission_request'),
662 params: z.object({663 params: z.object({
677 678
678await mcp.connect(new StdioServerTransport())679await mcp.connect(new StdioServerTransport())
679 680
680// --- HTTP sur :8788 : GET /events diffuse la sortie, POST route l'entrée ---681// --- HTTP on :8788: GET /events streams outbound, POST routes inbound ---
681const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i682const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
682let nextId = 1683let nextId = 1
683 684
684Bun.serve({685Bun.serve({
685 port: 8788,686 port: 8788,
686 hostname: '127.0.0.1',687 hostname: '127.0.0.1',
687 idleTimeout: 0, // ne pas fermer les flux SSE inactifs688 idleTimeout: 0, // don't close idle SSE streams
688 async fetch(req) {689 async fetch(req) {
689 const url = new URL(req.url)690 const url = new URL(req.url)
690 691
691 // GET /events : flux SSE afin que curl -N puisse regarder les réponses et les invites en direct692 // GET /events: SSE stream so curl -N can watch replies and prompts live
692 if (req.method === 'GET' && url.pathname === '/events') {693 if (req.method === 'GET' && url.pathname === '/events') {
693 const stream = new ReadableStream({694 const stream = new ReadableStream({
694 start(ctrl) {695 start(ctrl) {
695 ctrl.enqueue(': connected\n\n') // afin que curl affiche quelque chose immédiatement696 ctrl.enqueue(': connected\n\n') // so curl shows something immediately
696 const emit = (chunk: string) => ctrl.enqueue(chunk)697 const emit = (chunk: string) => ctrl.enqueue(chunk)
697 listeners.add(emit)698 listeners.add(emit)
698 req.signal.addEventListener('abort', () => listeners.delete(emit))699 req.signal.addEventListener('abort', () => listeners.delete(emit))
703 })704 })
704 }705 }
705 706
706 // tout le reste est entrant : contrôler sur l'expéditeur d'abord707 // everything else is inbound: gate on sender first
707 const body = await req.text()708 const body = await req.text()
708 const sender = req.headers.get('X-Sender') ?? ''709 const sender = req.headers.get('X-Sender') ?? ''
709 if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })710 if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })
710 711
711 // vérifier le format du verdict avant de traiter comme chat712 // check for verdict format before treating as chat
712 const m = PERMISSION_REPLY_RE.exec(body)713 const m = PERMISSION_REPLY_RE.exec(body)
713 if (m) {714 if (m) {
714 await mcp.notification({715 await mcp.notification({
721 return new Response('verdict recorded')722 return new Response('verdict recorded')
722 }723 }
723 724
724 // chat normal : transmettre à Claude en tant qu'événement de canal725 // normal chat: forward to Claude as a channel event
725 const chat_id = String(nextId++)726 const chat_id = String(nextId++)
726 await mcp.notification({727 await mcp.notification({
727 method: 'notifications/claude/channel',728 method: 'notifications/claude/channel',
732})733})
733```734```
734 735
735Testez le chemin du verdict dans trois terminaux. Le premier est votre session Claude Code, démarrée avec le [drapeau de développement](#test-during-the-research-preview) afin qu'elle lance `webhook.ts` :736Testez le chemin du verdict dans trois terminaux. Le premier est votre session Claude Code, démarrée avec le [flag de développement](#test-during-the-research-preview) afin qu'elle lance `webhook.ts` :
736 737
737```bash theme={null}738```bash theme={null}
738claude --dangerously-load-development-channels server:webhook739claude --dangerously-load-development-channels server:webhook
739```740```
740 741
741Cette procédure pas à pas teste la boîte de dialogue de permission elle-même, donc une fois la session ouverte, appuyez sur `Shift+Tab` jusqu'à ce que la barre d'état affiche `⏸ manual mode on`. En mode auto le classificateur déciderait l'appel `reply` à votre place, et aucune boîte de dialogue ne s'ouvrirait pour que le côté distant réponde.742Ce 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.
742 743
743Dans le second, diffusez le côté sortant afin que vous puissiez voir les réponses de Claude et toutes les invites de permission au fur et à mesure qu'elles se déclenchent :744Dans 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 :
744 745
745```bash theme={null}746```bash theme={null}
746curl -N localhost:8788/events747curl -N localhost:8788/events
747```748```
748 749
749Dans le troisième, envoyez un message qui fera que Claude essaiera d'exécuter une commande :750Dans le troisième, envoyez un message qui amènera Claude à essayer d'exécuter une commande :
750 751
751```bash theme={null}752```bash theme={null}
752curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788753curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788
753```754```
754 755
755L'énumération de fichiers est en lecture seule, donc Claude l'exécute sans approbation. La boîte de dialogue de permission s'ouvre quand Claude appelle l'outil `reply` pour envoyer sa réponse en retour. La boîte de dialogue locale s'ouvre dans votre terminal Claude Code, et un moment plus tard l'invite pour `mcp__webhook__reply` apparaît dans le flux `/events`, y compris l'ID à cinq lettres. Approuvez-le du côté distant :756Lister 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 :
756 757
757```bash theme={null}758```bash theme={null}
758curl -d "yes <id>" -H "X-Sender: dev" localhost:8788759curl -d "yes <id>" -H "X-Sender: dev" localhost:8788
759```760```
760 761
761La boîte de dialogue locale se ferme, l'outil `reply` s'exécute, et la réponse de Claude atterrit dans le flux.762La boîte de dialogue locale se ferme, l'outil `reply` s'exécute et la réponse de Claude apparaît dans le flux.
762 763
763Les trois pièces spécifiques au canal dans ce fichier :764Les trois éléments propres aux canaux dans ce fichier :
764 765
765* **Capacités** dans le constructeur `Server` : `claude/channel` enregistre l'écouteur de notification, `claude/channel/permission` opte pour le relais de permission, `tools` laisse Claude découvrir l'outil de réponse.766* **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.
766* **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 quand une boîte de dialogue de permission s'ouvre. Les deux appellent `send()` pour diffuser sur `/events`, mais ils sont déclenchés par différentes parties du système.767* **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.
767* **Gestionnaire HTTP** : `GET /events` maintient un flux SSE ouvert afin que curl puisse regarder la sortie en direct ; `POST` est entrant, contrôlé sur l'en-tête `X-Sender`. Un corps `yes <id>` ou `no <id>` va à Claude Code en tant que notification de verdict et n'atteint jamais Claude ; tout le reste est transmis à Claude en tant qu'événement de canal.768* **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.
768 769
769## Empaqueter en tant que plugin770## Empaqueter en tant que plugin
770 771