Caso de estudio
AgentBeacon
¿Qué está haciendo mi agente de IA ahora mismo, y me necesita?
- Rol
- Diseño y desarrollo (solo)
- Stack
- TypeScript · Node.js · MCP · n8n
- Estado
- Fase 1 completada, en producción en un Raspberry Pi 5 propio
El problema
Trabajo con Claude Code y OpenAI Codex en varias máquinas: un par de MacBooks y un Mac Studio. Los agentes ya son lo bastante buenos para trabajar un buen rato sin mí, y ahí está el problema. Cuando uno se detiene a pedir un permiso o una decisión, nada me avisa. Me entero cuando vuelvo al terminal, a veces una hora después.
No quería un dashboard para estar mirándolo, ni una notificación por cada archivo que lee un agente. Quería una sola señal, y solo cuando importa: un agente me está esperando, terminó o falló.
La solución
Los agentes reportan un estado normalizado a través de una sola herramienta MCP, agentbeacon_set_status. AgentBeacon valida la entrada con zod, la convierte en un evento con una forma única, elimina duplicados y la envía por POST a un solo webhook genérico. Ese webhook es un workflow de n8n que corre en mi propio servidor, y n8n se encarga de llevar el mensaje a Telegram.
AgentBeacon no sabe nada de Telegram. Añadir Discord, email o un historial es un cambio en n8n, no un release de código.
Arquitectura
El contrato del evento es la frontera de la que cuelga todo lo demás. Los agentes escriben en él, los providers lo leen, y ninguno de los dos lados sabe que el otro existe.
La regla que no negocio
src/core nunca importa de src/providers. Todo provider implementa la misma interfaz pequeña, { name, isEnabled(), accepts?(event), handle(event) }, y eso es lo único que el core llega a ver.
Modelo de estados
Cinco estados y ni uno más. Cada vez que me dieron ganas de añadir idle, paused o cancelled, no había una necesidad real detrás.
-
workingSilencioTrabajo activo: leyendo, escribiendo, corriendo tests o un build.
-
waitingSilencioEsperando algo externo que no depende de mí, como un deploy.
-
needs_attentionNotificaNo puede seguir sin mí: un permiso, una decisión, información que falta.
-
completedNotificaLa tarea terminó bien.
-
failedNotificaNo pudo terminar, con la razón cuando la hay.
Por defecto solo needs_attention, completed y failed envían notificación. Cada uno se puede activar o desactivar con variables de entorno.
Decisiones clave y sus costos
-
La identidad es máquina + agente + sesión
Cada ejecución se identifica como
MB16:codex:s1. Sin la máquina en la clave, dos máquinas que reportaban con el sessionIddefaultcaían en la misma entrada: una pisaba a la otra y la dedup silenciaba las notificaciones de la segunda. Es el tipo de bug que nunca ves probando con una sola máquina. -
El cliente nunca escoge su alias de máquina
El alias se deriva en el servidor a partir del bearer token. Si el body trae un campo
machine, se descarta. Una máquina no se puede hacer pasar por otra, y un error de tipeo en un archivo de configuración no convierte una máquina en dos. Los tokens se comparan en tiempo constante, y el modo HTTP se niega a arrancar si no hay tokens configurados. -
La dedup solo se activa después de una entrega exitosa
Si un estado se repite idéntico, sale una sola notificación, pero la entrada de dedup solo se guarda cuando una entrega funcionó. Si todos los providers fallaron, publicar el mismo estado otra vez lo reintenta en lugar de tragárselo. Perder un needs_attention es el peor fallo que puede tener este sistema.
-
Los fallos de un provider se quedan aislados
El fan-out usa
Promise.allSettled. Un provider que falla nunca tumba el proceso ni hace fallar la tarea del agente. El provider de webhook reintenta un número limitado de veces (3 por defecto, 5 como máximo) con backoff exponencial desde 250 ms y un timeout de 8 s. Solo reintenta errores de red, 429 y 5xx. Un 404 significa que la ruta de n8n está mal, y reintentar no lo va a arreglar. -
Borré el provider de Telegram
Antes había un provider directo de Telegram. Eso implicaba tener un bot token y un chat id en el código, y sacar un release cada vez que quería un canal nuevo. Lo eliminé y le pasé la entrega a n8n. Ahora AgentBeacon hace una sola cosa: emitir un evento limpio hacia un solo webhook.
-
Los secretos nunca llegan a los logs
El logger oculta credenciales Bearer, campos authorization y cualquier cosa con forma de bot token de Telegram. En modo stdio los logs van a stderr, porque stdout es el canal del protocolo MCP y una línea de log perdida ahí rompe la sesión.
Despliegue
En producción hay una sola instancia compartida a la que apuntan todas las máquinas. Usa el transporte HTTP: MCP Streamable HTTP para los agentes, un endpoint REST POST /events para scripts y un /healthz público para chequeos.
- Build de Docker multi-stage sobre
node:24-alpine, arm64, corriendo con un usuario sin privilegios de root. - Desplegado con Dokploy en un Raspberry Pi 5 en casa.
- Expuesto a través de Cloudflare Tunnel, así que no hay ningún puerto de entrada abierto en mi red.
El costo que acepté
Una sola réplica, y el estado vive en memoria, así que se reinicia con cada redeploy. Para la Fase 1 me parece bien. Sin base de datos y sin colas, a propósito, hasta que una fase de verdad los necesite.
En números
-
238
tests pasando
node:test en ~250 ms, sin red y sin credenciales. fetch se inyecta.
-
≈1.6×
más código de tests que de fuente
~2,550 líneas de tests vs ~1,570 líneas de código fuente.
-
2
dependencias en runtime
@modelcontextprotocol/sdk y zod. Lo demás es Node.
-
1
herramienta MCP
Una herramienta con un campo de estado, no una por estado.
-
5
estados
working, waiting, needs_attention, completed, failed.
Stack
- TypeScript (strict, ESM)
- Node.js
- MCP SDK
- zod
- fetch nativo
- node:test
- Docker
- Dokploy
- Cloudflare Tunnel
- n8n
- Telegram
Lo que viene
-
Fase 2
Luces Philips Hue
Un color por estado, para saber desde el otro lado del cuarto que un agente me necesita.
-
Fase 4
Pantalla física con ESP32
Un aparatito en el escritorio que muestra qué está haciendo cada sesión activa.
-
Fase 5
Dashboard e historial
La primera fase que necesita base de datos, así que primero lleva un ADR y después código.
Preguntas abiertas
- Firmar los payloads del webhook.
- Versionar el contrato del evento.
Lo que aprendí
- El contrato del evento es la verdadera frontera. Cuando esa forma se estabilizó, añadir providers y transportes se volvió fácil, y borrarlos también.
- Deja la entrega en manos de una herramienta de workflows. Meter los canales en el servicio significaba secretos en el código y un release por canal. n8n ya hace ese trabajo bien.
- Los bugs que valía la pena arreglar no eran features. Eran colisiones de identidad y notificaciones que se perdían en silencio, las formas en que el sistema podía fallar sin decir nada.
¿Necesitas algo así?
Diseño y construyo sistemas pequeños, bien probados y honestos sobre sus limitaciones. Si tu equipo tiene un problema parecido a este, hablemos.
El código fuente es privado. Con gusto te lo enseño en una llamada.