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 aPOST /precall; al terminar, el webhookPOST /webhooks/nlpearltrae 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 hiloEstructura
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.exampleCó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:consoleConsola: 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 demoTests:
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 mainEn 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 enscripts/vercel-build.sh):
Un script llamado
vercel-builden elpackage.jsonraí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/apifalla conNo workspaces foundsi Vercel ejecuta el build desde un subdirectorio. El script localiza la raíz del monorepo por su cuenta y llama anest/ngconnpx, así funciona desde cualquier ubicación.Si el deploy vuelve a fallar, mirá las primeras líneas del log: el script imprime
cwd inicialyraí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_TOKENsolo 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/:idsigan 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
/precally/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 inspectorTools: 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
En
.env:MOCK=false,NLPEARL_ACCOUNT_ID,NLPEARL_API_KEYyNLPEARL_PEARL_ID(Pearl outbound de voz). Auth confirmada en docs:Authorization: Bearer {AccountId}:{SecretKey}.En NL Pearl: configurar el flujo del Pearl con nodo PreCallAPI apuntando a
https://tu-host/precall, y activar el webhook de llamada apuntando ahttps://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).
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 enNLPEARL_WEBHOOK_SECRETy el guard lo verificará; vacío = no se exige.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 enwebhook.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/syncrecorre todas las pearls de la cuenta, trae la actividad conCalls/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/activityyGET /api/nlpearl/pearlsexponen lo espejado.Canal por pearl: nombre con "Whatsapp" ⇒
whatsapp, "TEXT/SMS/Chat" ⇒sms, resto ⇒voice. Se puede forzar conNLPEARL_TEXT_PEARL_IDS(ids separados por coma ⇒ sms).Postgres (Neon): en Vercel → Storage → Create Database → Neon (Postgres) → conectar al proyecto. Vercel inyecta
DATABASE_URLsola 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:
pearlIdde la petición (elección puntual, p. ej.POST /api/calls/triggercon{contactId, pearlId}) → Pearl asignada al canal →NLPEARL_PEARL_IDcomo respaldo heredado.API:
GET /api/workers/routingyPUT /api/workers/routing({channel, pearlId};pearlId: nulllibera el canal).NLPEARL_PEARL_IDpasa a ser opcional: solo hacen faltaNLPEARL_ACCOUNT_IDyNLPEARL_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); sinDATABASE_URLcaen 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únMOCK; el Brain nunca importa clientes concretos.El mock ejercita los endpoints HTTP reales del gateway (self-HTTP a
/precally/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
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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