SpyBara
Go Premium

channels-reference.md 2026-09-27 23:59 UTC to 2026-09-28 22:01 UTC

This page contains 3 additions and 3 deletions.

2026
Wed 9 22:58 Sat 12 03:02 Fri 18 23:58 Fri 25 23:58 Mon 28 22:59

Referência de Channels

Construa um servidor MCP que envia webhooks, alertas e mensagens de chat para uma sessão Claude Code. Referência para o contrato de channel: declaração de capacidade, eventos de notificação, ferramentas de resposta, gating de remetente e retransmissão de permissão.

Um channel é um servidor MCP que envia eventos para uma sessão Claude Code para que Claude possa reagir a coisas que acontecem fora do terminal.

Você pode construir um channel unidirecional ou bidirecional. Channels unidirecionais encaminham alertas, webhooks ou eventos de monitoramento para Claude agir. Channels bidirecionais como pontes de chat também expõem uma ferramenta de resposta para que Claude possa enviar mensagens de volta. Um channel com um caminho de remetente confiável também pode optar por retransmitir prompts de permissão para que você possa aprovar ou negar o uso de ferramentas remotamente.

Esta página cobre:

Para usar um channel existente em vez de construir um, consulte Channels. Telegram, Discord, iMessage e fakechat estão incluídos na visualização de pesquisa.

Visão geral

Um channel é um servidor MCP que é executado na mesma máquina que Claude Code. Claude Code o spawna como um subprocesso e se comunica via stdio. Seu servidor de channel é a ponte entre sistemas externos e a sessão Claude Code:

  • Plataformas de chat (Telegram, Discord): seu plugin é executado localmente e faz polling da API da plataforma para novas mensagens. Quando alguém envia uma DM para seu bot, o plugin recebe a mensagem e a encaminha para Claude. Nenhuma URL para expor.
  • Webhooks (CI, monitoramento): seu servidor escuta em uma porta HTTP local. Sistemas externos fazem POST para essa porta, e seu servidor envia o payload para Claude.
Diagrama de arquitetura mostrando sistemas externos se conectando ao seu servidor de channel local, que se comunica com Claude Code via stdio Diagrama de arquitetura mostrando sistemas externos se conectando ao seu servidor de channel local, que se comunica com Claude Code via stdio

O que você precisa

O único requisito obrigatório é o pacote @modelcontextprotocol/sdk e um runtime compatível com Node.js. Bun, Node e Deno funcionam. Os plugins pré-construídos na visualização de pesquisa usam Bun, mas seu channel não precisa.

Seu servidor precisa:

  1. Declarar a capacidade claude/channel para que Claude Code registre um listener de notificação
  2. Emitir eventos notifications/claude/channel quando algo acontecer
  3. Conectar via transporte stdio

As seções Opções de servidor e Formato de notificação cobrem cada uma delas em detalhes. Consulte Exemplo: construir um receptor de webhook para um passo a passo completo.

Durante a visualização de pesquisa, channels personalizados não estão na lista de aprovação. Use --dangerously-load-development-channels para testar localmente. Consulte Testar durante a visualização de pesquisa para detalhes.

Exemplo: construir um receptor de webhook

Este passo a passo constrói um servidor de arquivo único que escuta solicitações HTTP e as encaminha para sua sessão Claude Code. No final, qualquer coisa que possa enviar um HTTP POST, como um pipeline de CI, um alerta de monitoramento ou um comando curl, pode enviar eventos para Claude.

Este exemplo usa Bun como o runtime para seu servidor HTTP integrado e suporte a TypeScript. Você pode usar Node ou Deno em vez disso; o único requisito é o MCP SDK.

1

Criar o projeto

Os exemplos de retransmissão de permissão importam zod diretamente, então ele é instalado junto com o MCP SDK. Crie um novo diretório e instale ambos:

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

Escrever o servidor de channel

Crie um arquivo chamado webhook.ts. Este é seu servidor de channel inteiro: ele se conecta a Claude Code via stdio e escuta POSTs HTTP na porta 8788. Quando uma solicitação chega, ele envia o corpo para Claude como um evento de channel.

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

// Criar o servidor MCP e declará-lo como um channel
const mcp = new Server(
{ name: 'webhook', version: '0.0.1' },
{
// esta chave é o que o torna um channel — Claude Code registra um listener para ela
capabilities: { experimental: { 'claude/channel': {} } },
// Claude Code entrega isso a Claude como contexto quando o servidor se conecta, para que ele saiba como lidar com esses eventos
instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
},
)

// Conectar a Claude Code via stdio (Claude Code spawna este processo)
await mcp.connect(new StdioServerTransport())

// Iniciar um servidor HTTP que encaminha cada POST para Claude
Bun.serve({
port: 8788,  // qualquer porta aberta funciona
// apenas localhost: nada fora desta máquina pode fazer POST
hostname: '127.0.0.1',
async fetch(req) {
const body = await req.text()
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: body,  // torna-se o corpo da tag <channel>
// cada chave torna-se um atributo de tag, ex: <channel path="/" method="POST">
meta: { path: new URL(req.url).pathname, method: req.method },
},
})
return new Response('ok')
},
})

O arquivo configura o servidor, conecta via stdio e inicia um listener HTTP, nessa ordem:

  • Configuração do servidor: cria o servidor MCP com claude/channel em suas capacidades, o que é o que diz a Claude Code que este é um channel. Claude Code entrega a string instructions a Claude como contexto quando o servidor se conecta: diga a Claude quais eventos esperar, se deve responder e como rotear respostas se deve.
  • Conexão stdio: conecta a Claude Code via stdin/stdout. Isto é padrão para qualquer servidor MCP.
  • Listener HTTP: inicia um servidor web local na porta 8788. Cada corpo POST é encaminhado para Claude como um evento de channel via mcp.notification(). O content torna-se o corpo do evento, e cada entrada meta torna-se um atributo na tag <channel>. O listener precisa de acesso à instância mcp, então é executado no mesmo processo. Você poderia dividi-lo em módulos separados para um projeto maior.
3

Registrar seu servidor com Claude Code

Adicione o servidor à sua configuração MCP para que Claude Code saiba como iniciá-lo. Para um .mcp.json em nível de projeto no mesmo diretório, use um caminho relativo. Para configuração em nível de usuário em ~/.claude.json, use o caminho absoluto completo para que o servidor possa ser encontrado de qualquer projeto:

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

Claude Code lê sua configuração MCP na inicialização e spawna cada servidor como um subprocesso.

4

Testá-lo

Durante a visualização de pesquisa, channels personalizados não estão na lista de aprovação, então inicie Claude Code com a flag de desenvolvimento:

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

Claude Code primeiro mostra um diálogo de aviso em tela cheia listando os channels de desenvolvimento que você está carregando. Selecione I am using this for local development para continuar, ou Exit para sair.

A primeira vez que você inicia uma sessão neste projeto, Claude Code também pede consentimento antes de usar o novo servidor de .mcp.json. O diálogo relata "New MCP server found in this project: webhook". Selecione Use this MCP server para continuar.

Depois que você aceita, Claude Code spawna seu webhook.ts como um subprocesso, e o listener HTTP inicia automaticamente na porta que você configurou, 8788 neste exemplo. Você não precisa executar o servidor você mesmo.

Um aviso atenuado abaixo do banner de inicialização confirma que o channel está registrado: Channels (experimental) messages from server:webhook inject directly in this session · restart without --dangerously-load-development-channels to stop.

Se você vir "blocked by org policy," seu administrador de organização precisa habilitar channels primeiro.

Em um terminal separado, simule um webhook enviando um HTTP POST com uma mensagem para seu servidor. Este exemplo envia um alerta de falha de CI para a porta 8788 (ou qualquer porta que você configurou):

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

O payload chega em seu contexto Claude como uma tag <channel>:

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

Seu terminal renderiza o evento como um resumo de uma linha, ← webhook: build failed on main: https://ci.example.com/run/1234, em vez da tag bruta. Você verá então Claude começar a responder: lendo arquivos, executando comandos ou o que a mensagem exigir. Este é um channel unidirecional, então Claude age em sua sessão mas não envia nada de volta através do webhook. Para adicionar respostas, consulte Expor uma ferramenta de resposta.

Se o evento não chegar, o diagnóstico depende do que curl retornou:

  • curl sucede mas nada chega a Claude: execute /mcp em sua sessão para verificar o status do servidor. Um status failed geralmente significa um erro de dependência ou importação em seu arquivo de servidor. Para ver o rastreamento stderr, reinicie com claude --debug --dangerously-load-development-channels server:webhook e verifique o log de debug em ~/.claude/debug/<session-id>.txt.
  • curl falha com "connection refused": a porta não está vinculada ainda ou um processo obsoleto de uma execução anterior a está mantendo. lsof -i :<port> mostra o que está escutando; kill o processo obsoleto antes de reiniciar sua sessão.

O servidor fakechat estende este padrão com uma UI web, anexos de arquivo e uma ferramenta de resposta para chat bidirecional.

Testar durante a visualização de pesquisa

Durante a visualização de pesquisa, cada channel deve estar na lista de aprovação para se registrar. A flag de desenvolvimento contorna a lista de aprovação para entradas específicas após um prompt de confirmação. Este exemplo mostra ambos os tipos de entrada:

# Testando um plugin que você está desenvolvendo
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace

# Testando um servidor .mcp.json simples (sem wrapper de plugin ainda)
claude --dangerously-load-development-channels server:webhook

O bypass é por entrada. Combinar esta flag com --channels não estende o bypass para as entradas --channels. Durante a visualização de pesquisa, seu channel não está na lista de aprovação, portanto permanece na flag de desenvolvimento enquanto você constrói e testa.

Opções de servidor

Um channel define essas opções no construtor Server. Os campos instructions e capabilities.tools são MCP padrão; capabilities.experimental['claude/channel'] e capabilities.experimental['claude/channel/permission'] são as adições específicas de channel:

Campo Tipo Descrição
capabilities.experimental['claude/channel'] object Obrigatório. Sempre {}. A presença registra o listener de notificação.
capabilities.experimental['claude/channel/permission'] object ou false Opcional. Defina como {} para declarar que este channel pode receber solicitações de retransmissão de permissão. Quando declarado, Claude Code encaminha prompts de aprovação de ferramentas para seu channel para que você possa aprová-los ou negá-los remotamente. Para desativar, omita a chave ou defina como false. Antes da v2.1.234, Claude Code tratava false como declarado. Consulte Retransmitir prompts de permissão.
capabilities.tools object Apenas bidirecional. Sempre {}. Capacidade de ferramenta MCP padrão. Consulte Expor uma ferramenta de resposta.
instructions string Recomendado. Claude Code o entrega a Claude como contexto quando o servidor se conecta. Diga a Claude quais eventos esperar, o que os atributos da tag <channel> significam, se deve responder e, se sim, qual ferramenta usar e qual atributo passar de volta (como chat_id).

Para criar um channel unidirecional, omita capabilities.tools. Este exemplo mostra uma configuração bidirecional com a capacidade de channel, ferramentas e instruções definidas:

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

const mcp = new Server(
  { name: 'your-channel', version: '0.0.1' },
  {
    capabilities: {
      experimental: { 'claude/channel': {} },  // registra o listener de channel
      tools: {},  // omita para channels unidirecionais
    },
    // Claude Code o entrega a Claude como contexto quando o servidor se conecta, para que ele saiba como lidar com seus eventos
    instructions: 'Messages arrive as <channel source="your-channel" ...>. Reply with the reply tool.',
  },
)

Formato de notificação

Seu servidor emite notifications/claude/channel com dois params:

Campo Tipo Descrição
content string O corpo do evento. Entregue como o corpo da tag <channel>.
meta Record<string, string> Opcional. Cada entrada torna-se um atributo na tag <channel> para contexto de roteamento como ID de chat, nome do remetente ou severidade de alerta. As chaves devem ser identificadores: apenas letras, dígitos e underscores. Chaves contendo hífens ou outros caracteres são silenciosamente descartadas.

Seu servidor envia eventos chamando mcp.notification() na instância Server. Este exemplo envia um alerta de falha de CI com duas chaves 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' },
  },
})

O evento chega no contexto de Claude envolvido em uma tag <channel>. O atributo source é definido automaticamente a partir do nome configurado do seu servidor:

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

Claude Code não reconhece notificações. O await em mcp.notification() é resolvido quando a mensagem é escrita no transporte, não quando Claude a processou. Se a sessão não carregou seu servidor como um channel, ou a política de organização o bloqueia, Claude Code descarta os eventos silenciosamente e não retorna erro ao seu servidor.

Se você precisar de confirmação de entrega, rastreie o estado do evento em seu servidor e exponha uma ferramenta de resposta que Claude possa chamar para relatar o status de volta.

Os eventos são enfileirados na sessão e processados em ordem. Se várias notificações chegarem enquanto Claude está ocupado, elas são entregues juntas no próximo turno e Claude as trata como um grupo. Para processar fluxos de eventos independentes simultaneamente, execute sessões separadas.

Expor uma ferramenta de resposta

Se seu channel é bidirecional, como uma ponte de chat em vez de um encaminhador de alerta, exponha uma ferramenta MCP padrão que Claude possa chamar para enviar mensagens de volta. Nada sobre o registro da ferramenta é específico de channel. Uma ferramenta de resposta tem três componentes:

  1. Uma entrada tools: {} em suas capacidades do construtor Server para que Claude Code descubra a ferramenta
  2. Manipuladores de ferramentas que definem o esquema da ferramenta e implementam a lógica de envio
  3. Uma string instructions em seu construtor Server que diz a Claude quando e como chamar a ferramenta

Para adicionar estes ao receptor de webhook acima:

1

Habilitar descoberta de ferramentas

Em seu construtor Server em webhook.ts, adicione tools: {} às capacidades para que Claude Code saiba que seu servidor oferece ferramentas:

capabilities: {
experimental: { 'claude/channel': {} },
tools: {},  // habilita descoberta de ferramentas
},
2

Registrar a ferramenta de resposta

Adicione o seguinte a webhook.ts. O import vai no topo do arquivo com seus outros imports; os dois manipuladores vão entre o construtor Server e mcp.connect(). Isto registra uma ferramenta reply que Claude pode chamar com um chat_id e text:

// Adicione este import no topo de webhook.ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'

// Claude consulta isto na inicialização para descobrir quais ferramentas seu servidor oferece
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'reply',
description: 'Send a message back over this channel',
// inputSchema diz a Claude quais argumentos passar
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 chama isto quando quer invocar uma ferramenta
mcp.setRequestHandler(CallToolRequestSchema, async req => {
if (req.params.name === 'reply') {
const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
// send() é sua saída: POST para sua plataforma de chat, ou para teste local
// o broadcast SSE mostrado no exemplo completo abaixo.
send(`Reply to ${chat_id}: ${text}`)
return { content: [{ type: 'text', text: 'sent' }] }
}
throw new Error(`unknown tool: ${req.params.name}`)
})
3

Atualizar as instruções

Atualize a string instructions em seu construtor Server para que Claude saiba rotear respostas de volta através da ferramenta. Este exemplo diz a Claude para passar chat_id da tag de entrada:

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

Aqui está o webhook.ts completo com suporte bidirecional. Respostas de saída fluem sobre GET /events usando Server-Sent Events (SSE), então curl -N localhost:8788/events pode observá-las ao vivo; chat de entrada chega em 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'

// --- 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)
}

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,  // don't close idle SSE streams
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events: SSE stream so curl -N can watch Claude's replies 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' },
      })
    }

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

O servidor fakechat mostra um exemplo mais completo com anexos de arquivo e edição de mensagens.

Gate de mensagens de entrada

Um channel sem gate é um vetor de injeção de prompt. Qualquer pessoa que possa alcançar seu endpoint pode colocar texto na frente de Claude. Um channel escutando uma plataforma de chat ou um endpoint público precisa de uma verificação real de remetente antes de emitir qualquer coisa.

Verifique o remetente contra uma lista de permissão antes de chamar mcp.notification(). Este exemplo descarta qualquer mensagem de um remetente não no conjunto:

const allowed = new Set(loadAllowlist())  // from your access.json or equivalent

// inside your message handler, before emitting:
if (!allowed.has(message.from.id)) {  // sender, not room
  return  // drop silently
}
await mcp.notification({ ... })

Gate na identidade do remetente, não na identidade do chat ou sala: message.from.id no exemplo, não message.chat.id. Em chats em grupo, estes diferem, e fazer gate na sala deixaria qualquer pessoa em um grupo com lista de permissão injetar mensagens na sessão.

Os channels Telegram e Discord fazem gate em uma lista de permissão de remetente da mesma forma. Eles inicializam a lista por emparelhamento. Consulte qualquer implementação para o fluxo de emparelhamento completo. O channel iMessage toma uma abordagem diferente: detecta os próprios endereços do usuário do banco de dados Messages na inicialização e os deixa passar automaticamente, com outros remetentes adicionados por handle.

Retransmitir prompts de permissão

Quando Claude chama uma ferramenta que precisa de aprovação, o diálogo do terminal local abre e a sessão aguarda. Um channel bidirecional pode optar por receber o mesmo prompt em paralelo e retransmiti-lo para você em outro dispositivo. Ambos permanecem ativos: você pode responder no terminal ou no seu telefone, e Claude Code aplica qualquer resposta que chegar primeiro e fecha a outra.

A retransmissão cobre aprovações de uso de ferramentas como Bash, Write e Edit. Diálogos de confiança de projeto e consentimento de servidor MCP não retransmitem; esses aparecem apenas no terminal local.

Claude Code v2.1.234 e posterior envia solicitações de permissão apenas para servidores que registrou como channels para a sessão, então a retransmissão fica atrás dos mesmos controles de opt-in de sessão e organização que a entrega de mensagens. A retransmissão também exige que você opte pelo servidor com --channels ou a flag de desenvolvimento, e exige que o servidor declare a capacidade de permissão.

Como a retransmissão funciona

Quando um prompt de permissão abre, o loop de retransmissão tem quatro etapas:

  1. Claude Code gera um ID de solicitação curto e notifica seu servidor
  2. Seu servidor encaminha o prompt e o ID para seu aplicativo de chat
  3. O usuário remoto responde com um sim ou não e esse ID
  4. Seu manipulador de entrada analisa a resposta em um veredicto, e Claude Code o aplica apenas se o ID corresponder a uma solicitação aberta

O diálogo do terminal local permanece aberto durante tudo isso. Se alguém no terminal responder antes do veredicto remoto chegar, essa resposta é aplicada em vez disso e a solicitação remota pendente é descartada.

Diagrama de sequência: Claude Code envia uma notificação permission_request para o servidor de channel, o servidor formata e envia o prompt para o aplicativo de chat, o humano responde com um veredicto, e o servidor analisa essa resposta em uma notificação de permissão de volta para Claude Code Diagrama de sequência: Claude Code envia uma notificação permission_request para o servidor de channel, o servidor formata e envia o prompt para o aplicativo de chat, o humano responde com um veredicto, e o servidor analisa essa resposta em uma notificação de permissão de volta para Claude Code

Campos de solicitação de permissão

A notificação de saída de Claude Code é notifications/claude/channel/permission_request. Como a notificação de channel, o transporte é MCP padrão mas o método e esquema são extensões de Claude Code. O objeto params tem quatro campos de string que seu servidor formata no prompt de saída:

Campo Descrição
request_id Cinco letras minúsculas extraídas de a-z sem l, para que nunca leia como 1 ou I quando digitado em um telefone. Inclua-o em seu prompt de saída para que possa ser ecoado na resposta. Claude Code apenas aceita um veredicto que carregue um ID que emitiu. O diálogo do terminal local não exibe este ID, então seu manipulador de saída é a única maneira de aprender.
tool_name Nome da ferramenta que Claude quer usar, por exemplo Bash ou Write.
description Resumo legível por humanos do que esta chamada de ferramenta específica faz, nunca o comando em si. Para uma chamada Bash isto é a descrição de Claude do comando; quando o modelo não fornece descrição, o campo é a constante Run shell command e não carrega detalhe de comando. Renderize input_preview quando tiver espaço.
input_preview Os argumentos da ferramenta como texto em forma de JSON, com chave por campo de nível superior. Para Bash isto é o comando; para Write, o caminho do arquivo e o conteúdo. Omita-o do seu prompt se você tiver espaço apenas para uma mensagem de uma linha. Seu servidor decide o que mostrar.

Clientes em Claude Code v2.1.211 ou posterior sanitizam description e input_preview antes de retransmiti-los. Espere três mudanças no texto que você recebe:

  • Claude Code neutraliza caracteres de override de direção, caracteres invisíveis e lookalikes de aspas e colchetes angulares.
  • Claude Code dobra cada sequência de espaço em branco em um único espaço.
  • Claude Code retransmite texto inteiro até 3.500 pontos de código. Para um valor mais longo, você recebe seu início e seu fim em torno de um marcador contado ⋯ N code points elided ⋯. O final de um comando longo ainda chega ao aprovador.

Para input_preview, Claude Code aplica o limite de 3.500 a cada campo de nível superior dos argumentos separadamente e mantém as aspas estruturais do JSON. Clientes antes de v2.1.211 retransmitem description bruto e cortam input_preview para 200 unidades UTF-16 com reticências finais.

Clientes em Claude Code v2.1.234 ou posterior retransmitem o marcador (value unserializable) no lugar de um valor de campo input_preview que não conseguem serializar com segurança, como uma estrutura circular ou um array extremamente grande. Você ainda recebe a chave do campo, e os outros campos da visualização não são alterados.

Clientes em Claude Code v2.1.234 ou posterior também mascaram credenciais em description e input_preview. Você recebe [REDACTED] no lugar de um token de credencial de provedor reconhecível, como uma chave de API ou um token de acesso pessoal. Espere três efeitos do mascaramento quando renderizar os campos:

  • Claude Code mascara nomes de chave dentro de input_preview bem como seus valores. Um nome de chave que você exibe pode não corresponder ao nome de chave na entrada.
  • Claude Code nunca mascara um intervalo que contém sintaxe de shell, caracteres de caminho ou caracteres de URL. Um mascaramento não pode ocultar o comando, caminho de arquivo ou destino sendo aprovado.
  • Claude Code não mascara um segredo que carece de um prefixo reconhecível, ou um segredo que abrange espaço em branco, como um bloco de chave privada. Ambos chegam ao seu servidor sem mascaramento.

O mascaramento não muda quem recebe os campos. O que quer que permaneça sem mascaramento vai apenas para servidores que você optou com --channels ou a flag de desenvolvimento. Trate ambos os campos como não confiáveis a menos que você controle a frota de clientes.

O veredicto que seu servidor envia de volta é notifications/claude/channel/permission com dois campos: request_id ecoando o ID acima, e behavior definido como 'allow' ou 'deny'. Allow deixa a chamada de ferramenta prosseguir; deny a rejeita. Nenhum veredicto afeta chamadas futuras.

Adicionar retransmissão a uma ponte de chat

Adicionar retransmissão de permissão a um channel bidirecional leva três componentes:

  1. Uma entrada claude/channel/permission: {} sob capacidades experimental em seu construtor Server para que Claude Code saiba encaminhar prompts
  2. Um manipulador de notificação para notifications/claude/channel/permission_request que formata o prompt e o envia através da API da sua plataforma
  3. Uma verificação em seu manipulador de mensagem de entrada que reconhece yes <id> ou no <id> e emite uma notificação de veredicto notifications/claude/channel/permission em vez de encaminhar o texto para Claude

Apenas declare a capacidade se seu channel autentica o remetente, porque qualquer pessoa que possa responder através do seu channel pode aprovar ou negar o uso de ferramentas em sua sessão.

Para adicionar estes a uma ponte de chat bidirecional como a montada em Expor uma ferramenta de resposta:

1

Declarar a capacidade de permissão

Em seu construtor Server, adicione claude/channel/permission: {} ao lado de claude/channel sob experimental:

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

Manipular a solicitação de entrada

Registre um manipulador de notificação entre seu construtor Server e mcp.connect(). Claude Code o chama com os quatro campos de solicitação quando um diálogo de permissão abre. Seu manipulador formata o prompt para sua plataforma e inclui instruções para responder com o ID:

import { z } from 'zod'

// setNotificationHandler roteia por z.literal no campo method,
// então este esquema é tanto o validador quanto a chave de dispatch
const PermissionRequestSchema = z.object({
method: z.literal('notifications/claude/channel/permission_request'),
params: z.object({
request_id: z.string(),     // cinco letras minúsculas, inclua verbatim em seu prompt
tool_name: z.string(),      // ex: "Bash", "Write"
description: z.string(),    // resumo do que esta chamada faz. Trate como não confiável.
input_preview: z.string(),  // args da ferramenta como texto em forma de JSON. Trate como não confiável.
}),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
// send() é sua saída: POST para sua plataforma de chat, ou para teste local
// o broadcast SSE mostrado no exemplo completo abaixo.
send(
`Claude wants to run ${params.tool_name}: ${params.description}\n` +
// input_preview carrega os argumentos reais; renderize quando tiver
// espaço: para Bash a descrição sozinha pode ser apenas
// "Run shell command" com zero detalhe de comando
`${params.input_preview}\n\n` +
// o ID na instrução é o que seu manipulador de entrada analisa na Etapa 3
`Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
)
})
3

Interceptar o veredicto em seu manipulador de entrada

Seu manipulador de entrada é o loop ou callback que recebe mensagens de sua plataforma: o mesmo lugar onde você faz gate no remetente e emite notifications/claude/channel para encaminhar chat para Claude. Adicione uma verificação antes da chamada de encaminhamento de chat que reconhece o formato de veredicto e emite a notificação de permissão em vez disso.

A regex corresponde ao formato de ID que Claude Code gera: cinco letras, nunca l. A flag /i tolera autocorreção de telefone capitalizando a resposta; minúscula o ID capturado antes de enviá-lo de volta.

// corresponde a "y abcde", "yes abcde", "n abcde", "no abcde"
// [a-km-z] é o alfabeto de ID que Claude Code usa (minúscula, pula 'l')
// /i tolera autocorreção de telefone; minúscula a captura antes de enviar
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] é a palavra de veredicto, m[2] é o ID de solicitação
// emita a notificação de veredicto de volta para Claude Code em vez de 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) } },
})
}

Uma resposta remota que não corresponde exatamente ao formato esperado falha de uma de duas maneiras, e em ambos os casos o diálogo do terminal local permanece aberto:

  • Formato diferente: a regex do seu manipulador de entrada falha em corresponder, então texto como approve it ou yes sem um ID cai como uma mensagem normal para Claude.
  • Formato correto, ID errado: seu servidor emite um veredicto, mas Claude Code não encontra nenhuma solicitação aberta com esse ID e o descarta silenciosamente.

Exemplo completo

O webhook.ts montado abaixo combina todas as três extensões desta página: a ferramenta de resposta, gating de remetente e retransmissão de permissão. Se você está começando aqui, você também precisará da configuração de projeto e entrada .mcp.json do passo a passo inicial.

Para tornar ambas as direções testáveis a partir de curl, o listener HTTP serve dois caminhos:

  • GET /events: mantém um stream SSE aberto e envia cada mensagem de saída como uma linha data:, então curl -N pode observar as respostas de Claude e qualquer prompt de permissão conforme eles disparam ao vivo.
  • POST /: o lado de entrada, o mesmo manipulador de antes, agora com a verificação de formato de veredicto inserida antes do ramo de encaminhamento de 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')
  },
})

Teste o caminho de veredicto em três terminais. O primeiro é sua sessão Claude Code, iniciada com a flag de desenvolvimento para que ela spawne webhook.ts:

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

Este passo a passo testa o diálogo de permissão em si, então uma vez que a sessão está aberta, pressione Shift+Tab até a barra de status mostrar ⏸ manual mode on. Em modo automático o classificador decidiria a chamada reply em vez de você, e nenhum diálogo abriria para o lado remoto responder.

No segundo, transmita o lado de saída para que você possa ver as respostas de Claude e qualquer prompt de permissão conforme eles disparam ao vivo:

curl -N localhost:8788/events

No terceiro, envie uma mensagem que fará Claude tentar executar um comando:

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

Listar arquivos é somente leitura, então Claude o executa sem aprovação. O diálogo de permissão abre quando Claude chama a ferramenta reply para enviar sua resposta de volta. O diálogo local abre em seu terminal Claude Code, e um momento depois o prompt para mcp__webhook__reply aparece no stream /events, incluindo o ID de cinco letras. Aprove-o do lado remoto:

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

O diálogo local fecha, a ferramenta reply é executada, e a resposta de Claude chega no stream.

As três peças específicas de channel neste arquivo:

  • Capacidades no construtor Server: claude/channel registra o listener de notificação, claude/channel/permission opta pela retransmissão de permissão, tools deixa Claude descobrir a ferramenta de resposta.
  • Caminhos de saída: o manipulador da ferramenta reply é o que Claude chama para respostas conversacionais; o manipulador de notificação PermissionRequestSchema é o que Claude Code chama quando um diálogo de permissão abre. Ambos chamam send() para transmitir sobre /events, mas são acionados por diferentes partes do sistema.
  • Manipulador HTTP: GET /events mantém um stream SSE aberto para que curl possa observar a saída ao vivo; POST é entrada, feita gate no cabeçalho X-Sender. Um corpo yes <id> ou no <id> vai para Claude Code como uma notificação de veredicto e nunca chega a Claude; qualquer outra coisa é encaminhada para Claude como um evento de channel.

Empacotar como um plugin

Para tornar seu channel instalável e compartilhável, envolva-o em um plugin e publique-o em um marketplace. Os usuários o instalam com /plugin install, então o habilitam por sessão com --channels plugin:<name>@<marketplace>.

Um channel publicado em seu próprio marketplace ainda precisa de --dangerously-load-development-channels para ser executado, já que não está na lista de aprovação. A lista de aprovação padrão é os plugins de channel em claude-plugins-official. Os formulários de envio no aplicativo adicionam plugins ao marketplace da comunidade, que não está na lista de aprovação de channels.

Se você está trabalhando com um contato parceiro da Anthropic, entre em contato com eles para coordenar uma listagem de marketplace oficial. Em planos Team e Enterprise, um administrador pode incluir seu plugin na lista allowedChannelPlugins da organização, que substitui a lista de aprovação padrão da Anthropic.

Veja também

  • Channels para instalar e usar Telegram, Discord, iMessage ou a demo fakechat, e para habilitar channels para uma organização Team ou Enterprise
  • Implementações de channel funcionando para código de servidor completo com fluxos de emparelhamento, ferramentas de resposta e anexos de arquivo
  • MCP para o protocolo subjacente que servidores de channel implementam
  • Plugins para empacotar seu channel para que os usuários possam instalá-lo com /plugin install