Saltar al contenido principal
Los canales están en vista previa de investigación y requieren Claude Code v2.1.80 o posterior. Requieren inicio de sesión en claude.ai. La autenticación de consola y clave API no es compatible. Las organizaciones de equipo y empresa deben habilitarlos explícitamente.
Un canal es un servidor MCP que envía eventos a una sesión de Claude Code para que Claude pueda reaccionar a cosas que suceden fuera de la terminal. Puedes construir un canal unidireccional o bidireccional. Los canales unidireccionales reenvían alertas, webhooks o eventos de monitoreo para que Claude actúe sobre ellos. Los canales bidireccionales como puentes de chat también exponen una herramienta de respuesta para que Claude pueda enviar mensajes de vuelta. Un canal con una ruta de remitente confiable también puede optar por retransmitir solicitudes de permiso para que puedas aprobar o denegar el uso de herramientas de forma remota. Esta página cubre: Para usar un canal existente en lugar de construir uno, consulta Canales. Telegram, Discord, iMessage y fakechat se incluyen en la vista previa de investigación.

Descripción general

Un canal es un servidor MCP que se ejecuta en la misma máquina que Claude Code. Claude Code lo genera como un subproceso y se comunica a través de stdio. Tu servidor de canal es el puente entre sistemas externos y la sesión de Claude Code:
  • Plataformas de chat (Telegram, Discord): tu complemento se ejecuta localmente y sondea la API de la plataforma en busca de nuevos mensajes. Cuando alguien envía un mensaje directo a tu bot, el complemento recibe el mensaje y lo reenvía a Claude. Sin URL que exponer.
  • Webhooks (CI, monitoreo): tu servidor escucha en un puerto HTTP local. Los sistemas externos envían POST a ese puerto, y tu servidor envía la carga útil a Claude.
Diagrama de arquitectura que muestra sistemas externos conectándose a tu servidor de canal local, que se comunica con Claude Code a través de stdio

Lo que necesitas

El único requisito difícil es el paquete @modelcontextprotocol/sdk y un tiempo de ejecución compatible con Node.js. Bun, Node y Deno funcionan todos. Los complementos precompilados en la vista previa de investigación usan Bun, pero tu canal no tiene que hacerlo. Tu servidor necesita:
  1. Declarar la capacidad claude/channel para que Claude Code registre un oyente de notificación
  2. Emitir eventos notifications/claude/channel cuando algo suceda
  3. Conectarse a través del transporte stdio (Claude Code genera tu servidor como un subproceso)
Las secciones Opciones del servidor y Formato de notificación cubren cada una de estas en detalle. Consulta Ejemplo: construir un receptor de webhook para un tutorial completo. Durante la vista previa de investigación, los canales personalizados no están en la lista de aprobación. Usa --dangerously-load-development-channels para probar localmente. Consulta Prueba durante la vista previa de investigación para obtener detalles.

Ejemplo: construir un receptor de webhook

Este tutorial construye un servidor de un solo archivo que escucha solicitudes HTTP y las reenvía a tu sesión de Claude Code. Al final, cualquier cosa que pueda enviar un POST HTTP, como una canalización de CI, una alerta de monitoreo o un comando curl, puede enviar eventos a Claude. Este ejemplo usa Bun como tiempo de ejecución por su servidor HTTP integrado y soporte de TypeScript. Puedes usar Node o Deno en su lugar; el único requisito es el SDK de MCP.
1

Crear el proyecto

Crea un nuevo directorio e instala el SDK de MCP:
mkdir webhook-channel && cd webhook-channel
bun add @modelcontextprotocol/sdk
2

Escribir el servidor de canal

Crea un archivo llamado webhook.ts. Este es tu servidor de canal completo: se conecta a Claude Code a través de stdio y escucha POSTs HTTP en el puerto 8788. Cuando llega una solicitud, envía el cuerpo a Claude como un evento de canal.
webhook.ts
#!/usr/bin/env bun
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

// Crear el servidor MCP y declararlo como un canal
const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    // esta clave es lo que lo hace un canal — Claude Code registra un oyente para ella
    capabilities: { experimental: { 'claude/channel': {} } },
    // agregado al mensaje del sistema de Claude para que sepa cómo manejar estos eventos
    instructions: 'Los eventos del canal webhook llegan como <channel source="webhook" ...>. Son unidireccionales: léelos y actúa, no se espera respuesta.',
  },
)

// Conectar a Claude Code a través de stdio (Claude Code genera este proceso)
await mcp.connect(new StdioServerTransport())

// Iniciar un servidor HTTP que reenvíe cada POST a Claude
Bun.serve({
  port: 8788,  // cualquier puerto abierto funciona
  // solo localhost: nada fuera de esta máquina puede hacer POST
  hostname: '127.0.0.1',
  async fetch(req) {
    const body = await req.text()
    await mcp.notification({
      method: 'notifications/claude/channel',
      params: {
        content: body,  // se convierte en el cuerpo de la etiqueta <channel>
        // cada clave se convierte en un atributo de etiqueta, p. ej. <channel path="/" method="POST">
        meta: { path: new URL(req.url).pathname, method: req.method },
      },
    })
    return new Response('ok')
  },
})
El archivo hace tres cosas en orden:
  • Configuración del servidor: crea el servidor MCP con claude/channel en sus capacidades, que es lo que le dice a Claude Code que esto es un canal. La cadena instructions va al mensaje del sistema de Claude: dile a Claude qué eventos esperar, si debe responder y cómo enrutar las respuestas si debe hacerlo.
  • Conexión stdio: se conecta a Claude Code a través de stdin/stdout. Esto es estándar para cualquier servidor MCP: Claude Code lo genera como un subproceso.
  • Oyente HTTP: inicia un servidor web local en el puerto 8788. Cada cuerpo POST se reenvía a Claude como un evento de canal a través de mcp.notification(). El content se convierte en el cuerpo del evento, y cada entrada meta se convierte en un atributo en la etiqueta <channel>. El oyente necesita acceso a la instancia mcp, por lo que se ejecuta en el mismo proceso. Podrías dividirlo en módulos separados para un proyecto más grande.
3

Registrar tu servidor con Claude Code

Agrega el servidor a tu configuración de MCP para que Claude Code sepa cómo iniciarlo. Para un .mcp.json a nivel de proyecto en el mismo directorio, usa una ruta relativa. Para la configuración a nivel de usuario en ~/.claude.json, usa la ruta absoluta completa para que el servidor se pueda encontrar desde cualquier proyecto:
.mcp.json
{
  "mcpServers": {
    "webhook": { "command": "bun", "args": ["./webhook.ts"] }
  }
}
Claude Code lee tu configuración de MCP al iniciar y genera cada servidor como un subproceso.
4

Probarlo

Durante la vista previa de investigación, los canales personalizados no están en la lista de aprobación, así que inicia Claude Code con la bandera de desarrollo:
claude --dangerously-load-development-channels server:webhook
Cuando Claude Code se inicia, lee tu configuración de MCP, genera tu webhook.ts como un subproceso, y el oyente HTTP se inicia automáticamente en el puerto que configuraste (8788 en este ejemplo). No necesitas ejecutar el servidor tú mismo.Si ves “bloqueado por política de organización”, tu administrador de equipo o empresa necesita habilitar canales primero.En una terminal separada, simula un webhook enviando un POST HTTP con un mensaje a tu servidor. Este ejemplo envía una alerta de fallo de compilación al puerto 8788 (o el puerto que configuraste):
curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
La carga útil llega a tu sesión de Claude Code como una etiqueta <channel>:
<channel source="webhook" path="/" method="POST">build failed on main: https://ci.example.com/run/1234</channel>
En tu terminal de Claude Code, verás que Claude recibe el mensaje y comienza a responder: leyendo archivos, ejecutando comandos o lo que sea que el mensaje requiera. Este es un canal unidireccional, por lo que Claude actúa en tu sesión pero no envía nada de vuelta a través del webhook. Para agregar respuestas, consulta Exponer una herramienta de respuesta.Si el evento no llega, el diagnóstico depende de lo que curl devolvió:
  • curl tiene éxito pero nada llega a Claude: ejecuta /mcp en tu sesión para verificar el estado del servidor. “Falló al conectar” generalmente significa un error de dependencia o importación en tu archivo de servidor; consulta el registro de depuración en ~/.claude/debug/<session-id>.txt para el seguimiento de stderr.
  • curl falla con “conexión rechazada”: el puerto no está vinculado aún o un proceso antiguo de una ejecución anterior lo está manteniendo. lsof -i :<port> muestra qué está escuchando; kill el proceso antiguo antes de reiniciar tu sesión.
El servidor fakechat extiende este patrón con una interfaz web, archivos adjuntos y una herramienta de respuesta para chat bidireccional.

Prueba durante la vista previa de investigación

Durante la vista previa de investigación, cada canal debe estar en la lista de aprobación para registrarse. La bandera de desarrollo omite la lista de aprobación para entradas específicas después de un mensaje de confirmación. Este ejemplo muestra ambos tipos de entrada:
# Prueba de un complemento que estás desarrollando
claude --dangerously-load-development-channels plugin:yourplugin@yourmarketplace

# Prueba de un servidor .mcp.json desnudo (sin envoltura de complemento aún)
claude --dangerously-load-development-channels server:webhook
El bypass es por entrada. Combinar esta bandera con --channels no extiende el bypass a las entradas --channels. Durante la vista previa de investigación, la lista de aprobación es curada por Anthropic, por lo que tu canal permanece en la bandera de desarrollo mientras lo construyes y pruebas.
Esta bandera omite solo la lista de aprobación. La política de organización channelsEnabled aún se aplica. No la uses para ejecutar canales de fuentes no confiables.

Opciones del servidor

Un canal establece estas opciones en el constructor Server. Los campos instructions y capabilities.tools son MCP estándar; capabilities.experimental['claude/channel'] y capabilities.experimental['claude/channel/permission'] son las adiciones específicas del canal:
CampoTipoDescripción
capabilities.experimental['claude/channel']objectRequerido. Siempre {}. La presencia registra el oyente de notificación.
capabilities.experimental['claude/channel/permission']objectOpcional. Siempre {}. Declara que este canal puede recibir solicitudes de retransmisión de permisos. Cuando se declara, Claude Code reenvía solicitudes de aprobación de herramientas a tu canal para que puedas aprobar o denegar de forma remota. Consulta Retransmitir solicitudes de permiso.
capabilities.toolsobjectSolo bidireccional. Siempre {}. Capacidad de herramienta MCP estándar. Consulta Exponer una herramienta de respuesta.
instructionsstringRecomendado. Agregado al mensaje del sistema de Claude. Dile a Claude qué eventos esperar, qué significan los atributos de la etiqueta <channel>, si debe responder y, si es así, qué herramienta usar y qué atributo pasar de vuelta (como chat_id).
Para crear un canal unidireccional, omite capabilities.tools. Este ejemplo muestra una configuración bidireccional con la capacidad de canal, herramientas e instrucciones establecidas:
import { Server } from '@modelcontextprotocol/sdk/server/index.js'

const mcp = new Server(
  { name: 'your-channel', version: '0.0.1' },
  {
    capabilities: {
      experimental: { 'claude/channel': {} },  // registra el oyente de canal
      tools: {},  // omite para canales unidireccionales
    },
    // agregado al mensaje del sistema de Claude para que sepa cómo manejar tus eventos
    instructions: 'Los mensajes llegan como <channel source="your-channel" ...>. Responde con la herramienta de respuesta.',
  },
)
Para enviar un evento, llama a mcp.notification() con el método notifications/claude/channel. Los parámetros están en la siguiente sección.

Formato de notificación

Tu servidor emite notifications/claude/channel con dos parámetros:
CampoTipoDescripción
contentstringEl cuerpo del evento. Entregado como el cuerpo de la etiqueta <channel>.
metaRecord<string, string>Opcional. Cada entrada se convierte en un atributo en la etiqueta <channel> para el contexto de enrutamiento como ID de chat, nombre del remitente o severidad de alerta. Las claves deben ser identificadores: solo letras, dígitos y guiones bajos. Las claves que contienen guiones u otros caracteres se descartan silenciosamente.
Tu servidor envía eventos llamando a mcp.notification() en la instancia Server. Este ejemplo envía una alerta de fallo de compilación con dos claves 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' },
  },
})
El evento llega en el contexto de Claude envuelto en una etiqueta <channel>. El atributo source se establece automáticamente desde el nombre configurado de tu servidor:
<channel source="your-channel" severity="high" run_id="1234">
build failed on main: https://ci.example.com/run/1234
</channel>

Exponer una herramienta de respuesta

Si tu canal es bidireccional, como un puente de chat en lugar de un reenviador de alertas, expón una herramienta MCP estándar que Claude pueda llamar para enviar mensajes de vuelta. Nada sobre el registro de herramientas es específico del canal. Una herramienta de respuesta tiene tres componentes:
  1. Una entrada tools: {} en las capacidades del constructor Server para que Claude Code descubra la herramienta
  2. Manejadores de herramientas que definen el esquema de la herramienta e implementan la lógica de envío
  3. Una cadena instructions en el constructor Server que le dice a Claude cuándo y cómo llamar a la herramienta
Para agregar estos al receptor de webhook anterior:
1

Habilitar el descubrimiento de herramientas

En tu constructor Server en webhook.ts, agrega tools: {} a las capacidades para que Claude Code sepa que tu servidor ofrece herramientas:
capabilities: {
  experimental: { 'claude/channel': {} },
  tools: {},  // habilita el descubrimiento de herramientas
},
2

Registrar la herramienta de respuesta

Agrega lo siguiente a webhook.ts. El import va en la parte superior del archivo con tus otras importaciones; los dos manejadores van entre el constructor Server y mcp.connect(). Esto registra una herramienta reply que Claude puede llamar con un chat_id y text:
// Agrega este import en la parte superior de webhook.ts
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'

// Claude consulta esto al iniciar para descubrir qué herramientas ofrece tu servidor
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Enviar un mensaje de vuelta a través de este canal',
    // inputSchema le dice a Claude qué argumentos pasar
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'La conversación en la que responder' },
        text: { type: 'string', description: 'El mensaje a enviar' },
      },
      required: ['chat_id', 'text'],
    },
  }],
}))

// Claude llama a esto cuando quiere invocar una herramienta
mcp.setRequestHandler(CallToolRequestSchema, async req => {
  if (req.params.name === 'reply') {
    const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
    // send() es tu salida: POST a tu plataforma de chat, o para
    // pruebas locales la transmisión SSE mostrada en el ejemplo completo a continuación.
    send(`Reply to ${chat_id}: ${text}`)
    return { content: [{ type: 'text', text: 'sent' }] }
  }
  throw new Error(`unknown tool: ${req.params.name}`)
})
3

Actualizar las instrucciones

Actualiza la cadena instructions en tu constructor Server para que Claude sepa enrutar las respuestas de vuelta a través de la herramienta. Este ejemplo le dice a Claude que pase chat_id desde la etiqueta entrante:
instructions: 'Los mensajes llegan como <channel source="webhook" chat_id="...">. Responde con la herramienta de respuesta, pasando el chat_id de la etiqueta.'
Aquí está el webhook.ts completo con soporte bidireccional. Las respuestas salientes se transmiten a través de GET /events usando Server-Sent Events (SSE), por lo que curl -N localhost:8788/events puede verlas en vivo; el chat entrante llega en POST /:
"webhook.ts
#!/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'

// --- Salida: escribir a cualquier oyente curl -N en /events ---
// Un puente real haría POST a tu plataforma de chat en su lugar.
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: 'Los mensajes llegan como <channel source="webhook" chat_id="...">. Responde con la herramienta de respuesta, pasando el chat_id de la etiqueta.',
  },
)

mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Enviar un mensaje de vuelta a través de este canal',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'La conversación en la que responder' },
        text: { type: 'string', description: 'El mensaje a enviar' },
      },
      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,  // no cierres flujos SSE inactivos
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events: flujo SSE para que curl -N pueda ver las respuestas de Claude en vivo
    if (req.method === 'GET' && url.pathname === '/events') {
      const stream = new ReadableStream({
        start(ctrl) {
          ctrl.enqueue(': connected\n\n')  // para que curl muestre algo inmediatamente
          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: reenviar a Claude como un evento 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')
  },
})
El servidor fakechat muestra un ejemplo más completo con archivos adjuntos y edición de mensajes.

Compuerta de mensajes entrantes

Un canal sin compuerta es un vector de inyección de solicitudes. Cualquiera que pueda alcanzar tu punto final puede poner texto frente a Claude. Un canal que escucha una plataforma de chat o un punto final público necesita una comprobación de remitente real antes de emitir cualquier cosa. Comprueba el remitente contra una lista de permitidos antes de llamar a mcp.notification(). Este ejemplo descarta cualquier mensaje de un remitente que no esté en el conjunto:
const allowed = new Set(loadAllowlist())  // desde tu access.json o equivalente

// dentro de tu manejador de mensajes, antes de emitir:
if (!allowed.has(message.from.id)) {  // remitente, no sala
  return  // descartar silenciosamente
}
await mcp.notification({ ... })
Compuerta en la identidad del remitente, no en la identidad del chat o sala: message.from.id en el ejemplo, no message.chat.id. En chats grupales, estos difieren, y la compuerta en la sala permitiría que cualquiera en un grupo permitido inyecte mensajes en la sesión. Los canales Telegram y Discord se compuertan en una lista de permitidos de remitente de la misma manera. Inician la lista por emparejamiento: el usuario envía un mensaje directo al bot, el bot responde con un código de emparejamiento, el usuario lo aprueba en su sesión de Claude Code, y su ID de plataforma se agrega. Consulta cualquiera de las implementaciones para el flujo de emparejamiento completo. El canal iMessage toma un enfoque diferente: detecta las propias direcciones del usuario desde la base de datos de Mensajes al iniciar y las deja pasar automáticamente, con otros remitentes agregados por identificador.

Retransmitir solicitudes de permiso

La retransmisión de permisos requiere Claude Code v2.1.81 o posterior. Las versiones anteriores ignoran la capacidad claude/channel/permission.
Cuando Claude llama a una herramienta que necesita aprobación, se abre el diálogo del terminal local y la sesión espera. Un canal bidireccional puede optar por recibir la misma solicitud en paralelo y retransmitirla a ti en otro dispositivo. Ambos permanecen activos: puedes responder en la terminal o en tu teléfono, y Claude Code aplica cualquiera que sea la respuesta que llegue primero y cierra la otra. La retransmisión cubre aprobaciones de uso de herramientas como Bash, Write y Edit. Los diálogos de confianza del proyecto y consentimiento del servidor MCP no se retransmiten; esos solo aparecen en la terminal local.

Cómo funciona la retransmisión

Cuando se abre una solicitud de permiso, el bucle de retransmisión tiene cuatro pasos:
  1. Claude Code genera un ID de solicitud corto y notifica a tu servidor
  2. Tu servidor reenvía la solicitud y el ID a tu aplicación de chat
  3. El usuario remoto responde con un sí o no y ese ID
  4. Tu manejador entrante analiza la respuesta en un veredicto, y Claude Code lo aplica solo si el ID coincide con una solicitud abierta
El diálogo del terminal local permanece abierto durante todo esto. Si alguien en la terminal responde antes de que llegue el veredicto remoto, esa respuesta se aplica en su lugar y la solicitud remota pendiente se descarta. Diagrama de secuencia: Claude Code envía una notificación permission_request al servidor de canal, el servidor formatea y envía la solicitud a la aplicación de chat, el humano responde con un veredicto, y el servidor analiza esa respuesta en una notificación de permiso de vuelta a Claude Code

Campos de solicitud de permiso

La notificación saliente de Claude Code es notifications/claude/channel/permission_request. Como la notificación de canal, el transporte es MCP estándar pero el método y esquema son extensiones de Claude Code. El objeto params tiene cuatro campos de cadena que tu servidor formatea en la solicitud saliente:
CampoDescripción
request_idCinco letras minúsculas extraídas de a-z sin l, por lo que nunca se lee como 1 o I cuando se escribe en un teléfono. Inclúyelo en tu solicitud saliente para que pueda ser repetido en la respuesta. Claude Code solo acepta un veredicto que lleve un ID que emitió. El diálogo del terminal local no muestra este ID, por lo que tu manejador saliente es la única forma de aprenderlo.
tool_nameNombre de la herramienta que Claude quiere usar, por ejemplo Bash o Write.
descriptionResumen legible por humanos de lo que hace esta llamada de herramienta específica, el mismo texto que muestra el diálogo del terminal local. Para una llamada Bash esto es la descripción de Claude del comando, o el comando en sí si no se dio ninguno.
input_previewLos argumentos de la herramienta como una cadena JSON, truncada a 200 caracteres. Para Bash esto es el comando; para Write es la ruta del archivo y un prefijo del contenido. Omítelo de tu solicitud si solo tienes espacio para un mensaje de una línea. Tu servidor decide qué mostrar.
El veredicto que tu servidor envía de vuelta es notifications/claude/channel/permission con dos campos: request_id repitiendo el ID anterior, y behavior establecido en 'allow' o 'deny'. Permitir deja que la llamada de herramienta continúe; denegar la rechaza, lo mismo que responder No en el diálogo local. Ningún veredicto afecta llamadas futuras.

Agregar retransmisión a un puente de chat

Agregar retransmisión de permisos a un canal bidireccional requiere tres componentes:
  1. Una entrada claude/channel/permission: {} bajo capacidades experimental en tu constructor Server para que Claude Code sepa que debe reenviar solicitudes
  2. Un manejador de notificación para notifications/claude/channel/permission_request que formatea la solicitud y la envía a través de tu API de plataforma
  3. Una comprobación en tu manejador de mensajes entrantes que reconozca yes <id> o no <id> y emita una notificación de veredicto notifications/claude/channel/permission en su lugar de reenviar el texto a Claude
Solo declara la capacidad si tu canal autentica el remitente, porque cualquiera que pueda responder a través de tu canal puede aprobar o denegar el uso de herramientas en tu sesión. Para agregar estos a un puente de chat bidireccional como el ensamblado en Exponer una herramienta de respuesta:
1

Declarar la capacidad de permiso

En tu constructor Server, agrega claude/channel/permission: {} junto a claude/channel bajo experimental:
capabilities: {
  experimental: {
    'claude/channel': {},
    'claude/channel/permission': {},  // optar por retransmisión de permisos
  },
  tools: {},
},
2

Manejar la solicitud entrante

Registra un manejador de notificación entre tu constructor Server y mcp.connect(). Claude Code lo llama con los cuatro campos de solicitud cuando se abre un diálogo de permiso. Tu manejador formatea la solicitud para tu plataforma e incluye instrucciones para responder con el ID:
import { z } from 'zod'

// setNotificationHandler enruta por z.literal en el campo method,
// por lo que este esquema es tanto el validador como la clave de envío
const PermissionRequestSchema = z.object({
  method: z.literal('notifications/claude/channel/permission_request'),
  params: z.object({
    request_id: z.string(),     // cinco letras minúsculas, incluir verbatim en tu solicitud
    tool_name: z.string(),      // p. ej. "Bash", "Write"
    description: z.string(),    // resumen legible por humanos de esta llamada
    input_preview: z.string(),  // argumentos de herramienta como JSON, truncado a ~200 caracteres
  }),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  // send() es tu salida: POST a tu plataforma de chat, o para
  // pruebas locales la transmisión SSE mostrada en el ejemplo completo a continuación.
  send(
    `Claude wants to run ${params.tool_name}: ${params.description}\n\n` +
    // el ID en la instrucción es lo que tu manejador entrante analiza en el Paso 3
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})
3

Interceptar el veredicto en tu manejador entrante

Tu manejador entrante es el bucle o devolución de llamada que recibe mensajes de tu plataforma: el mismo lugar donde compuertas en remitente y emites notifications/claude/channel para reenviar chat a Claude. Agrega una comprobación antes de la llamada de reenvío de chat que reconozca el formato de veredicto y emita la notificación de permiso en su lugar.La expresión regular coincide con el formato de ID que genera Claude Code: cinco letras, nunca l. La bandera /i tolera la corrección automática del teléfono capitalizando la respuesta; minúscula el ID capturado antes de enviarlo de vuelta.
// coincide con "y abcde", "yes abcde", "n abcde", "no abcde"
// [a-km-z] es el alfabeto de ID que usa Claude Code (minúscula, omite 'l')
// /i tolera la corrección automática del teléfono; minúscula la 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  // compuerta en remitente primero

  const m = PERMISSION_REPLY_RE.exec(message.text)
  if (m) {
    // m[1] es la palabra de veredicto, m[2] es el ID de solicitud
    // emitir la notificación de veredicto de vuelta a Claude Code en lugar de chat
    await mcp.notification({
      method: 'notifications/claude/channel/permission',
      params: {
        request_id: m[2].toLowerCase(),  // normalizar en caso de autocorrect caps
        behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
      },
    })
    return  // manejado como veredicto, no también reenviar como chat
  }

  // no coincidió con formato de veredicto: caer a través de la ruta de chat normal
  await mcp.notification({
    method: 'notifications/claude/channel',
    params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
  })
}
Claude Code también mantiene abierto el diálogo del terminal local, por lo que puedes responder en cualquier lugar, y la primera respuesta que llegue se aplica. Una respuesta remota que no coincida exactamente con el formato esperado falla de una de dos maneras, y en ambos casos el diálogo permanece abierto:
  • Formato diferente: la expresión regular de tu manejador entrante no coincide, por lo que texto como approve it o yes sin un ID cae a través como un mensaje normal a Claude.
  • Formato correcto, ID incorrecto: tu servidor emite un veredicto, pero Claude Code no encuentra ninguna solicitud abierta con ese ID y lo descarta silenciosamente.

Ejemplo completo

El webhook.ts ensamblado a continuación combina las tres extensiones de esta página: la herramienta de respuesta, la compuerta de remitente y la retransmisión de permisos. Si estás comenzando aquí, también necesitarás la configuración del proyecto y entrada .mcp.json del tutorial inicial. Para hacer ambas direcciones comprobables desde curl, el oyente HTTP sirve dos rutas:
  • GET /events: mantiene abierto un flujo SSE y envía cada mensaje saliente como una línea data:, por lo que curl -N puede ver las respuestas de Claude y cualquier solicitud de permiso llegar en vivo.
  • POST /: el lado entrante, el mismo manejador que antes, ahora con la comprobación de formato de veredicto insertada antes de la rama de reenvío de chat.
"webhook.ts
#!/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'

// --- Salida: escribir a cualquier oyente curl -N en /events ---
// Un puente real haría POST a tu plataforma de chat en su lugar.
const listeners = new Set<(chunk: string) => void>()
function send(text: string) {
  const chunk = text.split('\n').map(l => `data: ${l}\n`).join('') + '\n'
  for (const emit of listeners) emit(chunk)
}

// Lista de permitidos de remitente. Para el tutorial local confiamos en el valor de encabezado único X-Sender
// "dev"; un puente real verificaría el ID de usuario de la plataforma.
const allowed = new Set(['dev'])

const mcp = new Server(
  { name: 'webhook', version: '0.0.1' },
  {
    capabilities: {
      experimental: {
        'claude/channel': {},
        'claude/channel/permission': {},  // optar por retransmisión de permisos
      },
      tools: {},
    },
    instructions:
      'Los mensajes llegan como <channel source="webhook" chat_id="...">. ' +
      'Responde con la herramienta de respuesta, pasando el chat_id de la etiqueta.',
  },
)

// --- herramienta de respuesta: Claude llama a esto para enviar un mensaje de vuelta ---
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'reply',
    description: 'Enviar un mensaje de vuelta a través de este canal',
    inputSchema: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'La conversación en la que responder' },
        text: { type: 'string', description: 'El mensaje a enviar' },
      },
      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}`)
})

// --- retransmisión de permisos: Claude Code (no Claude) llama a esto cuando se abre un diálogo
const PermissionRequestSchema = z.object({
  method: z.literal('notifications/claude/channel/permission_request'),
  params: z.object({
    request_id: z.string(),
    tool_name: z.string(),
    description: z.string(),
    input_preview: z.string(),
  }),
})

mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  send(
    `Claude wants to run ${params.tool_name}: ${params.description}\n\n` +
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})

await mcp.connect(new StdioServerTransport())

// --- HTTP en :8788: GET /events transmite salida, POST enruta entrada ---
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,  // no cierres flujos SSE inactivos
  async fetch(req) {
    const url = new URL(req.url)

    // GET /events: flujo SSE para que curl -N pueda ver respuestas y solicitudes en vivo
    if (req.method === 'GET' && url.pathname === '/events') {
      const stream = new ReadableStream({
        start(ctrl) {
          ctrl.enqueue(': connected\n\n')  // para que curl muestre algo inmediatamente
          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' },
      })
    }

    // todo lo demás es entrada: compuerta en remitente primero
    const body = await req.text()
    const sender = req.headers.get('X-Sender') ?? ''
    if (!allowed.has(sender)) return new Response('forbidden', { status: 403 })

    // comprueba el formato de veredicto antes de tratar como chat
    const m = PERMISSION_REPLY_RE.exec(body)
    if (m) {
      await mcp.notification({
        method: 'notifications/claude/channel/permission',
        params: {
          request_id: m[2].toLowerCase(),
          behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
        },
      })
      return new Response('verdict recorded')
    }

    // chat normal: reenviar a Claude como un evento de canal
    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')
  },
})
Prueba la ruta de veredicto en tres terminales. La primera es tu sesión de Claude Code, iniciada con la bandera de desarrollo para que genere webhook.ts:
claude --dangerously-load-development-channels server:webhook
En la segunda, transmite el lado saliente para que puedas ver las respuestas de Claude y cualquier solicitud de permiso llegar en vivo:
curl -N localhost:8788/events
En la tercera, envía un mensaje que hará que Claude intente ejecutar un comando:
curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788
El diálogo de permiso local se abre en tu terminal de Claude Code. Un momento después la solicitud aparece en el flujo /events, incluyendo el ID de cinco letras. Apruébalo desde el lado remoto:
curl -d "yes <id>" -H "X-Sender: dev" localhost:8788
El diálogo local se cierra y la herramienta se ejecuta. La respuesta de Claude vuelve a través de la herramienta reply y también llega al flujo. Las tres piezas específicas del canal en este archivo:
  • Capacidades en el constructor Server: claude/channel registra el oyente de notificación, claude/channel/permission opta por retransmisión de permisos, tools permite que Claude descubra la herramienta de respuesta.
  • Rutas salientes: el manejador de la herramienta reply es lo que Claude llama para respuestas conversacionales; el manejador de notificación PermissionRequestSchema es lo que Claude Code llama cuando se abre un diálogo de permiso. Ambos llaman a send() para transmitir a través de /events, pero se activan por diferentes partes del sistema.
  • Manejador HTTP: GET /events mantiene abierto un flujo SSE para que curl pueda ver la salida en vivo; POST es entrada, compuerta en el encabezado X-Sender. Un cuerpo yes <id> o no <id> va a Claude Code como una notificación de veredicto y nunca llega a Claude; cualquier otra cosa se reenvía a Claude como un evento de canal.

Empaquetar como un complemento

Para hacer tu canal instalable y compartible, envuélvelo en un complemento y publícalo en un mercado. Los usuarios lo instalan con /plugin install, luego lo habilitan por sesión con --channels plugin:<name>@<marketplace>. Un canal publicado en tu propio mercado aún necesita --dangerously-load-development-channels para ejecutarse, ya que no está en la lista de aprobación. Para que se agregue, envíalo al mercado oficial. Los complementos de canal pasan por revisión de seguridad antes de ser aprobados. En planes de equipo y empresa, un administrador puede incluir tu complemento en la lista allowedChannelPlugins de la organización, que reemplaza la lista de aprobación predeterminada de Anthropic.

Ver también

  • Canales para instalar y usar Telegram, Discord, iMessage o la demostración fakechat, y para habilitar canales para una organización de equipo o empresa
  • Implementaciones de canal de trabajo para código de servidor completo con flujos de emparejamiento, herramientas de respuesta y archivos adjuntos
  • MCP para el protocolo subyacente que implementan los servidores de canal
  • Complementos para empaquetar tu canal para que los usuarios puedan instalarlo con /plugin install