Skip to main content
Glama
jorgemovitext

Voice Brain MCP Server

Voice Brain MCP — Prototipo Voz (NL Pearl v2) + Brain (MCP) + Canales

Prototipo de gateway de voz sobre NL Pearl v2 con un "Brain" de contexto unificado por contacto (independiente del canal), expuesto también como servidor MCP, y una consola Angular 22 para operar el flujo.

Corre end-to-end en modo mock sin credenciales. Los adaptadores reales de NL Pearl v2 están listos para conectar.

Qué hace

  • Voz: NL Pearl v2 como motor detrás de nuestro propio gateway (no se usa su consola ni sus canales de texto). La llamada saliente se dispara con addLead; antes de hablar, el nodo PreCallAPI pide contexto a POST /precall; al terminar, el webhook POST /webhooks/nlpearl trae el aviso y el gateway recupera transcripción/resumen/sentimiento/datos.

  • Brain: contexto unificado por contacto (identidad, timeline cross-channel, señales tipo promesa de pago). Expuesto por REST para la consola y como servidor MCP (stdio) con tools brain_*.

  • Canales propios: WhatsApp/SMS (stubs con hueco para WABA/proveedor SMS) leen y escriben el mismo contexto → el seguimiento continúa el mismo hilo.

Diagrama del flujo (demo)

 consola /demo ──POST /api/demo/run──▶ DemoService
   1. siembra contacto (promesa activa + WhatsApp previo)
   2. addLead (VoiceEnginePort → mock | NL Pearl v2)
        │
        ▼  (ciclo de llamada)
   3. POST /precall  ◀── nodo PreCallAPI      → variables (nombre, promesa, saldo, último resumen)
   4. ... conversación ...
   5. POST /webhooks/nlpearl (HMAC guard)     → getCall → Brain.recordCallContext
        │                                        · interacción voice + señal promesa
        ▼
   6. FollowupService → brain_suggest_followup → WhatsApp propio (stub)
        │                                        · interacción whatsapp outbound
        ▼
   7. consola: timeline del contacto con voz + WhatsApp en el mismo hilo

Estructura

voice-brain-mcp/
├─ apps/
│  ├─ api/        # NestJS 11 + Fastify: Brain, NL Pearl, canales, MCP, demo
│  └─ console/    # Angular 22 (signals + zoneless). Vistas:
│                 #   /home           inicio con avatar de voz
│                 #   /contacts       directorio de contactos
│                 #   /contacts/:id   chat + contexto en vivo (2 columnas)
│                 #   /conversations  módulo de conversaciones: lista de hilos
│                 #                   + chat + contexto en vivo (3 columnas)
│                 #   /demo           flujos end-to-end paso a paso
├─ scripts/run-demo.mjs
├─ data/brain.json   # respaldo de persistencia (se crea al correr)
└─ .env              # copiar de .env.example

Cómo correr

Requisitos: Node 20+ (probado con Node 24).

cp .env.example .env     # MOCK=true por defecto
npm install

npm run dev              # api (3000) + consola (4200) juntos
# o por separado:
npm run dev:api
npm run dev:console
  • Consola: http://localhost:4200 → pestaña Demo del flujo → “Correr flujo end-to-end”. Al final hay link al contexto del contacto (voz + WhatsApp en el mismo timeline).

  • Demo por CLI (con la api levantada): npm run demo

  • Tests: npm test · Build: npm run build

Desplegado

https://voice-brain-mcp.vercel.app — corre en modo mock, sin credenciales.

Desplegar en Vercel (desde GitHub)

El repo ya trae vercel.json y la función serverless en api/index.js.

git init && git add -A && git commit -m "Prototipo voz + Brain MCP"
git remote add origin git@github.com:<usuario>/<repo>.git
git push -u origin main

En Vercel: Add New → Project → Import ese repo y Deploy. vercel.json define el build, publica la consola Angular como estático y rutea /api/*, /precall y /webhooks/* a la función Nest.

Un solo proyecto, con Root Directory en la raíz del repo. El build tolera que Vercel arranque dentro de un workspace, pero la función serverless vive en api/index.js (raíz) y Vercel solo la detecta si el Root Directory es esa raíz. Con proyectos separados por workspace, la consola se despliega pero /api/* responde 404.

Por qué el build es un script y no npm run --workspace (dos trampas de Vercel con monorepos npm, ambas ya resueltas en scripts/vercel-build.sh):

  • Un script llamado vercel-build en el package.json raíz no sirve: Vercel le da trato especial y npm lo propaga a cada workspace, que no lo define → Missing script: "vercel-build".

  • npm run build --workspace apps/api falla con No workspaces found si Vercel ejecuta el build desde un subdirectorio. El script localiza la raíz del monorepo por su cuenta y llama a nest/ng con npx, así funciona desde cualquier ubicación.

Si el deploy vuelve a fallar, mirá las primeras líneas del log: el script imprime cwd inicial y raíz del monorepo, que dicen exactamente desde dónde arrancó Vercel.

Variables de entorno (Project → Settings → Environment Variables): ninguna es obligatoria — sin nada, el deploy corre en modo mock. Para conectar NL Pearl real, cargá MOCK=false, NLPEARL_ACCOUNT_ID, NLPEARL_API_KEY, NLPEARL_PEARL_ID y NLPEARL_WEBHOOK_SECRET, y apuntá el webhook del Pearl a https://<tu-deploy>.vercel.app/webhooks/nlpearl y el nodo PreCallAPI a https://<tu-deploy>.vercel.app/precall.

Qué cambia en serverless (y por qué)

Vercel congela el proceso al responder y solo /tmp es escribible, así que el código se adapta solo (detecta VERCEL):

  • Persistencia: con un Vercel Blob store conectado (Storage → Create → Blob), el Brain guarda un único JSON compartido por todas las instancias y el estado deja de perderse. Vercel inyecta BLOB_READ_WRITE_TOKEN solo y el código lo detecta. Sin store, cae al archivo en /tmp: cada lambda tiene su propia copia, así que un contacto creado en una instancia no existe en la siguiente y los mensajes que entran por webhook no aparecen en la consola.

  • Sembrado en frío: si el Brain arranca vacío se siembra el directorio de demo con IDs fijos, para que los enlaces /contacts/:id sigan valiendo entre instancias.

  • Flujo demo: se completa dentro del request (no hay timers de fondo) y los pasos viajan en la respuesta, porque el polling podría caer en otra instancia.

  • Mock: usa los servicios in-process en vez de llamarse por HTTP a sí mismo (la protección de deployments bloquearía ese self-request). En local sigue usando HTTP real contra /precall y /webhooks/nlpearl.

  • MCP: el servidor stdio no aplica en Vercel; corre local con npm run mcp.

Brain como servidor MCP

npm run mcp                                    # servidor stdio
npx @modelcontextprotocol/inspector npm run mcp  # probarlo con el inspector

Tools: brain_resolve_identity, brain_get_context, brain_upsert_contact, brain_append_interaction, brain_set_signal, brain_get_signals, brain_record_call_context, brain_suggest_followup.

Comparte persistencia (archivo JSON) con el gateway HTTP.

Conectar NL Pearl real

  1. En .env: MOCK=false, NLPEARL_ACCOUNT_ID, NLPEARL_API_KEY y NLPEARL_PEARL_ID (Pearl outbound de voz). Auth confirmada en docs: Authorization: Bearer {AccountId}:{SecretKey}.

  2. En NL Pearl: configurar el flujo del Pearl con nodo PreCallAPI apuntando a https://tu-host/precall, y activar el webhook de llamada apuntando a https://tu-host/webhooks/nlpearl.

    Dónde está el webhook (no es workspace-wide, es por Pearl): dashboard (salí de Settings con Go Back) → abrí tu Pearl → editor de flujo PearlVibe → pestaña Outbound Settings (o Inbound Settings) → grupo Campaign Settings → bajá hasta el final, sección Webhooks → activá el toggle (los campos de URL solo aparecen al habilitarlo) → Call Webhook URL. El Lead Webhook no lo usamos. Ojo: en Settings del workspace, Agent(s) es capacidad de llamadas simultáneas, no los Pearls; y Text Channels no se usa (WhatsApp/SMS son nuestros).

  3. NLPEARL_WEBHOOK_SECRET: NL Pearl no firma webhooks con HMAC — al configurar el webhook podés adjuntar un Credential (un token que creás vos) que viaja en cada entrega. Poné ese mismo valor en NLPEARL_WEBHOOK_SECRET y el guard lo verificará; vacío = no se exige.

  4. Los paths confirmados contra la doc v2 están en apps/api/src/nlpearl/nlpearl.client.ts; los no verificados quedaron marcados // TODO: confirmar con NL Pearl (igual que el shape exacto del webhook y del PreCallAPI en webhook.controller.ts / precall.controller.ts).

Espejo NL Pearl (todos los canales) + Postgres

Desde el pivote de 2026-08, la plataforma consume todos los canales de NL Pearl (voz y texto: SMS "Línea 100 AMDC TEXT", WhatsApp, etc.) y almacena la atención a detalle en DB propia.

  • Sync multi-pearl: POST /api/nlpearl/sync recorre todas las pearls de la cuenta, trae la actividad con Calls/Bulk (paginado, límite 100 del API) y guarda el raw completo + la interacción normalizada en el Brain. La consola lo dispara sola cada ~30 s (?soft=true, con rate-limit). GET /api/nlpearl/activity y GET /api/nlpearl/pearls exponen lo espejado.

  • Canal por pearl: nombre con "Whatsapp" ⇒ whatsapp, "TEXT/SMS/Chat" ⇒ sms, resto ⇒ voice. Se puede forzar con NLPEARL_TEXT_PEARL_IDS (ids separados por coma ⇒ sms).

  • Postgres (Neon): en Vercel → Storage → Create Database → Neon (Postgres) → conectar al proyecto. Vercel inyecta DATABASE_URL sola y el Brain migra a Postgres en el próximo deploy (prioridad: Postgres > Blob > archivo JSON). El esquema se crea solo al primer uso; tablas: contacts, interactions, signals, nlpearl_pearls, nlpearl_activity (raw).

Qué Pearl usa cada canal (sin variables de entorno)

NLPEARL_PEARL_ID obligaba a un redeploy para cambiar de Pearl. Ahora la asignación vive en la DB y se cambia desde la app:

  • En Obreros, cada Pearl trae el botón "Usar para <canal>". El canal sale de su agentType (voz / WhatsApp / SMS), así que cada Pearl solo se puede asignar al canal que realmente atiende.

  • El backend resuelve en este orden: pearlId de la petición (elección puntual, p. ej. POST /api/calls/trigger con {contactId, pearlId}) → Pearl asignada al canalNLPEARL_PEARL_ID como respaldo heredado.

  • API: GET /api/workers/routing y PUT /api/workers/routing ({channel, pearlId}; pearlId: null libera el canal).

  • NLPEARL_PEARL_ID pasa a ser opcional: solo hacen falta NLPEARL_ACCOUNT_ID y NLPEARL_API_KEY.

Autenticación de la consola

Toda la plataforma exige sesión: sin login, cualquier URL de la consola cae en /login y toda la API responde 401 (guard global deny-by-default; solo son públicos los webhooks de proveedores y /precall, que llevan su propia verificación).

  • Registro / login: teléfono E.164 + contraseña (scrypt) y OTP de 6 dígitos por WhatsApp como segundo factor (vía el canal Gupshup propio).

  • Hardening: OTP hasheado con vencimiento (5 min), 5 intentos y un solo uso; cooldown de reenvío (60 s); bloqueo de cuenta 15 min tras 5 contraseñas fallidas; mensajes de error genéricos (sin enumeración de usuarios); sesión JWT en cookie httpOnly + Secure + SameSite=Lax (12 h).

  • Variables: AUTH_JWT_SECRET (obligatoria en prod — ya cargada en Vercel), AUTH_SESSION_HOURS, AUTH_OTP_TTL_MIN.

  • En MOCK=true (desarrollo) el OTP se imprime en el log del server en vez de enviarse por WhatsApp.

  • Usuarios en Postgres (tabla users); sin DATABASE_URL caen a un archivo local solo apto para desarrollo.

Decisiones / notas

  • Puertos como injection tokens (VoiceEnginePort, ChannelPort, BrainRepository): el binding mock/real vive en cada módulo adaptador según MOCK; el Brain nunca importa clientes concretos.

  • El mock ejercita los endpoints HTTP reales del gateway (self-HTTP a /precall y /webhooks/nlpearl, con firma HMAC si hay secreto), no atajos internos.

  • Persistencia: memoria + respaldo JSON detrás de BrainRepository (cambiable a SQLite/Postgres vía provider).

  • No se usan los canales de texto de NL Pearl: WhatsApp/SMS son adaptadores propios (stubs con log).

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jorgemovitext/voice-brain-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server