ipbx-mcp
ipbx-mcp
Servidor MCP del IPBX en TypeScript. Transporte Streamable HTTP en modo stateless, autenticación por bearer estático y/o OAuth 2.1 + Google Workspace, persistencia local en SQLite (clientes OAuth, refresh tokens, audit log). Heredado del scaffold base-mcp, expone los datos del PABX (MySQL) como tools tipadas.
URL pública en producción: https://mcp.ipbx.vivavox.com.br.
Requisitos
Node.js >= 22 (
better-sqlite3v12 lo necesita)Para OAuth: OAuth Client en Google Cloud Console en modo Internal
Related MCP server: utel-mcp
Instalación
npm install
cp .env.example .env # depois preencha os valores reais
npm run buildConfiguración
Cargue el .env en el proceso (systemd EnvironmentFile=, docker env_file:, o node --env-file=.env al iniciar).
Obligatorias
Al menos una de las vías de auth:
Variable | Cuándo usarla |
| Bearer estático — Claude Desktop, CLI, API, scripts, cron |
| OAuth — clientes vía claude.ai (web/mobile) |
OAuth (opcional, pero necesario para claude.ai)
Variable | Descripción |
| URL canónica del servidor (ej: |
| Clave HS256 de los JWTs (32 bytes hex) |
| Del OAuth Client en Google Cloud Console |
| Del OAuth Client en Google Cloud Console |
| Dominio Workspace permitido (default: |
Cuando todas están presentes, se montan las rutas /authorize, /oauth/google/callback, /token y /register (DCR). Sin ellas, solo funciona el bearer estático.
Otras
Variable | Default | Descripción |
|
| Puerto HTTP |
|
| Interfaz (use |
| — | Lista CSV de hosts aceptados en el header |
|
| Ruta del archivo SQLite |
| — | Base de la API del IPBX que sirve las grabaciones (ej: |
MySQL (fuente de datos del IPBX)
Variable | Default | Descripción |
| — | Host del MySQL |
|
| |
| — | Use un usuario dedicado con |
| — | |
| — | |
|
| Tamaño del pool ( |
| vacío | Cualquier valor activa TLS con verificación de cert |
| — | Tenant al que atiende esta instancia (ver abajo) |
La base de datos es multi-tenant — una instancia Asterisk por cliente, tabla ipbx — pero cada instancia del MCP atiende un solo tenant. Todas las queries filtran por IPBX_ID, y ninguna tool acepta ese id como parámetro: así el aislamiento entre clientes no depende de lo que el modelo pase en la llamada. Un contenedor y un subdominio por tenant.
Genere tokens aleatorios con:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Endpoints
Método | Path | Auth | Descripción |
POST |
| bearer | JSON-RPC del MCP vía Streamable HTTP |
GET |
| bearer |
|
DELETE |
| bearer |
|
GET |
| público |
|
GET |
| público | RFC 8414 metadata |
GET |
| público | RFC 9728 metadata |
POST |
| público | Dynamic Client Registration (RFC 7591) |
GET |
| público | Redirige a Google |
GET |
| público | Recibe el redirect de Google |
POST |
| público |
|
401 en /mcp incluye WWW-Authenticate: Bearer realm=..., resource_metadata=... — sin esto claude.ai no descubre el AS en el primer contacto.
Tools disponibles
El nombre de las tools sigue ipbx_<model>_<action>, con <action> en el vocabulario list / get / search / count.
ipbx_instance_get
Datos de registro de la instancia IPBX a la que atiende este servidor — nombre, IP y puertos SIP/AMI.
Parámetros: ninguno. La instancia es fija, definida por IPBX_ID en el entorno.
Retorno:
{
"id": 1,
"shortname": "vivavox",
"fullname": "Vivavox Telecom",
"ipaddr": "138.94.55.155",
"sipport": 5601,
"amiport": 6501,
"created": "2024-06-17T16:37:59.000Z",
"updated": "2024-06-17T16:37:59.000Z"
}Devuelve isError si el IPBX_ID configurado no existe en la tabla ipbx.
ipbx_branch_list
Lista los ramales de la instancia.
Parámetros:
search(string, opcional): búsqueda parcial por número de ramal o nombrelimit(number, opcional): 1–500, default100
Retorno:
{
"total": 27,
"truncated": false,
"branches": [
{
"id": 2,
"exten": "23",
"name": "Ricardo Landim",
"group": "Suporte",
"record": true,
"webrtc": false,
"dtmf": "rfc4733",
"forward_busy": "035988023317",
"forward_noanswer": "035988023317",
"forward_noanswer_wait": 5
}
]
}No devuelve las credenciales SIP. Las columnas password (contraseña en claro) y username (identificador de autenticación, distinto del número de ramal) quedan fuera por diseño — juntas permiten registrar un softphone y originar llamadas en la cuenta del cliente. La lista de columnas en el SELECT es explícita precisamente para que ninguna de ellas entre por descuido.
ipbx_user_list
Lista los usuarios del panel de la instancia.
Parámetros:
search(string, opcional): búsqueda parcial por nombre o emaillimit(number, opcional): 1–500, default100
Retorno:
{
"total": 6,
"truncated": false,
"users": [
{
"id": 11,
"name": "Suporte",
"email": "suporte@vivavox.com.br",
"created": "2024-07-10T13:56:41.000Z",
"updated": "2024-07-10T13:56:41.000Z"
}
]
}No devuelve la contraseña de acceso. La columna secret queda fuera: es la contraseña de login del panel, guardada en texto plano en la base de datos (sin hash). Exponer eso entregaría acceso administrativo al PABX.
ipbx_group_list
Lista los grupos de ramales de la instancia, con cuántos ramales tiene cada uno.
Parámetros:
search(string, opcional): búsqueda parcial por nombre o descripciónlimit(number, opcional): 1–500, default100
Retorno:
{
"total": 6,
"truncated": false,
"groups": [
{
"id": 1,
"name": "Suporte",
"description": "Grupo do suporte",
"branches": 11
}
]
}La tabla groups no guarda credenciales — a diferencia de branch y users, aquí se exponen todas las columnas.
ipbx_trunk_list
Lista los troncos de la instancia.
Parámetros:
search(string, opcional): búsqueda parcial por nombre o hostlimit(number, opcional): 1–500, default100
Retorno:
{
"total": 2,
"truncated": false,
"trunks": [
{
"id": 1,
"name": "Vivavox",
"host": "sip.vivavox.com.br",
"port": "5060",
"register": true,
"record": true,
"auth": "credentials"
}
]
}No devuelve las credenciales de la operadora. username y password quedan fuera — son la credencial más valiosa de la base de datos, ya que permiten originar llamadas directo por la operadora, tarificadas en la cuenta. En su lugar va auth, que solo dice cómo autentica el tronco: "credentials" (usuario/contraseña) o "ip" (allowlist de IP, sin contraseña).
ipbx_queue_list
Lista las colas de atención, con la estrategia de distribución y cuántos miembros tiene cada una.
Parámetros: search (string, opcional), limit (1–500, default 100)
{
"total": 5,
"queues": [
{ "id": 1, "name": "Suporte", "strategy": "ringall", "members": 8 },
{ "id": 5, "name": "Teste", "strategy": "leastrecent", "members": 1 }
]
}ipbx_queue_member_list
Lista los miembros de las colas, en el orden de timbre.
Parámetros:
queue_id(number, opcional): filtra una cola; omítalo para traer todaslimit(number, opcional): 1–500, default200
Retorno:
{
"total": 8,
"members": [
{
"queue_id": 1,
"queue": "Suporte",
"position": 1,
"type": "branch",
"exten": "29",
"name": "Mateus Damaceno",
"ref": "branch-10"
}
]
}La columna queue_member.member guarda una referencia en el formato <tipo>-<id> — branch-10 apunta al branch.id 10, que es el ramal 29. No es el número de ramal. La tool resuelve esto a exten + name cuando el miembro es un ramal. No todo miembro lo es: existen entradas redirect-N, que vuelven con type: "redirect" y exten/name nulos.
ipbx_ivr_list
Lista las URAs, con el audio asociado y la transcripción de lo que se dice a quien llama.
Parámetros: search (string, opcional — casa en el nombre o en el texto de la transcripción), limit (1–500, default 100)
{
"total": 1,
"ivrs": [
{
"id": 5,
"name": "URA Rompimento",
"audio": "URA Rompimento",
"transcription": "Olá, se você está com falta de conexão e o LED Loss do seu modem óptico...",
"options": 1
}
]
}La transcripción es el campo más útil: permite encontrar una URA por lo que dice, no solo por el nombre.
ipbx_ivr_option_list
Lista las opciones de las URAs — qué tecla lleva a qué destino.
Parámetros:
ivr_id(number, opcional): filtra una URA; omítalo para traer todaslimit(number, opcional): 1–500, default200
Retorno:
{
"total": 7,
"options": [
{
"ivr_id": 1,
"ivr": "URA Principal - Horario comercial",
"digit": "1",
"goto": { "type": "queue", "name": "Financeiro", "exten": null, "ref": "queue-3" }
},
{
"ivr_id": 1,
"ivr": "URA Principal - Horario comercial",
"digit": "7X",
"goto": { "type": "internal", "name": null, "exten": null, "ref": "internal" }
}
]
}ivr_option.goto es polimórfico: apunta a 5 tablas distintas (branch, queue, ivr, redirect, app) en el formato <tipo>-<id>, y además acepta literales sin id (internal). La tool resuelve el nombre del destino en todos los casos; los literales vuelven con name nulo y el ref preservado.
El campo digit no siempre es un dígito: t es timeout y patrones como 7X casan rangos de ramal.
ipbx_redirect_list
Lista los redirects — ramales cortos que reenvían a un número externo saliendo por un tronco. Son los mismos redirect-<id> que aparecen como destino en colas, URAs y reglas de enrutamiento.
Parámetros: search (string, opcional — casa ramal, nombre o número), limit (1–500, default 100)
{
"total": 12,
"redirects": [
{
"id": 2,
"exten": "73",
"name": "Ricardo Landim",
"forward": "5535988023317",
"trunk": "Vivavox",
"ref": "redirect-2"
}
]
}⚠️ Dato personal. forward es un número de móvil personal en el 100% de las líneas — no es una credencial, pero es dato personal bajo LGPD. La tool lo devuelve porque es la razón de existir de la tabla, pero no va al audit_log.
ipbx_routing_list
Lista los planes de enrutamiento, con cuántas reglas y ventanas de horario tiene cada uno.
Parámetros: search (string, opcional), limit (1–500, default 100)
{
"total": 2,
"routings": [
{ "id": 1, "name": "Entrada - Padrão", "rules": 6, "time_windows": 3 },
{ "id": 2, "name": "Saida - Padrão", "rules": 8, "time_windows": 1 }
]
}ipbx_routing_time_list
Lista las ventanas de horario de los planes.
Parámetros: routing_id (number, opcional), limit (1–500, default 100)
{
"id": 1,
"routing": "Entrada - Padrão",
"name": "Horario comercial",
"ranges": ["08:00-18:00,mon", "08:00-18:00,tue", "08:00-12:00,sat"]
}El pattern se almacena en el formato de Asterisk, un rango por línea; la tool lo devuelve como lista.
ipbx_routing_rule_list
Lista las reglas de enrutamiento — el dialplan. Cada regla casa un patrón de número dentro de una ventana de horario, suprime dígitos, añade prefijo y envía al destino.
Parámetros: routing_id (number, opcional), limit (1–500, default 200)
{
"id": 4,
"routing": "Saida - Padrão",
"name": "LDN",
"time_window": "Geral",
"match": "0ZZ.",
"suppress": 1,
"prefix": "55",
"goto": { "type": "trunk", "name": "Vivavox", "exten": null, "ref": "trunk-1" }
}goto1 es polimórfico como el de la URA, más el tipo trunk (usado en las reglas de salida) — seis destinos posibles en total.
Dos detalles del schema tratados aquí: la columna de la base de datos se llama supress (con una "p"), expuesta como suppress; y goto2/goto3 existen pero están vacías en todas las filas — aparecen como goto_extra solo si algún día se rellenan.
ipbx_cdr_list
Historial de llamadas. El período es obligatorio y está limitado a 31 días: la cdr no tiene índice más allá de la PK, así que todo filtro es full scan (~268k filas hoy).
Parámetros: date_from y date_to (YYYY-MM-DD, obligatorios), scope (call | leg, por defecto call), src y dst (parcial), branch_id, trunk_id, answered (bool), call_id, limit (1–500, por defecto 25)
{
"call_id": "sip1-1787578699.251937",
"started": "2026-08-24 10:38:19",
"ended": "2026-08-24 10:42:06",
"direction": "inbound",
"from": { "type": "trunk", "id": 1, "name": "Vivavox" },
"caller": "35997609940",
"dialed": null,
"context": "queue-3",
"answered": true,
"talk_seconds": 265,
"ring_attempts": 6,
"targets": [{ "type": "branch", "id": 16, "exten": "35", "name": "Ester Vilela" }],
"answered_by": [{ "type": "branch", "id": 16, "exten": "35", "name": "Ester Vilela" }],
"dispositions": ["ANSWERED", "NO ANSWER"],
"has_recording": true,
"legs": 10
}La cdr es la única tabla del PABX sin ipbx_id. El vínculo con el tenant es el systemname de Asterisk, que el ipbx-api escribe como sip<ipbx_id> y Asterisk sella en el uniqueid/linkedid de cada línea — el filtro es uniqueid LIKE 'sip<id>-%', con el guion (sin él, sip1 coincidiría también con sip10-).
Una llamada son muchas líneas: uniqueid identifica el canal, linkedid la llamada, y cada intento de Dial genera una línea — una entrante de cola llega a 22. scope=call agrupa por linkedid; las patas cuyo destino es un canal Local/ son el timbre de la cola en cada miembro (se convierten en ring_attempts) y las demás son conversación (suman talk_seconds). scope=leg devuelve las patas crudas — úsalo con call_id para depurar una llamada.
has_recording exige ANSWERED además de rec relleno — misma regla que el recAvailable del panel. La columna rec se escribe antes del Dial (el dialplan arma MixMonitor en el prerouting), así que marca "grabación armada" y no "existe audio": sola, daría grabación en el 99,96% de las llamadas.
Ninguna columna de canal sale cruda: channel, dstchannel y lastdata cargan el username del endpoint, que es la mitad de la credencial SIP, y src trae ese mismo username en llamada interna. Todo pasa por src/channel.ts y sale como ramal/tronco/cola. rec también queda fuera — se convierte en has_recording.
Filtrar por branch_id/trunk_id selecciona las llamadas por semi-join, no por línea: los agregados siguen describiendo la llamada completa, no solo las patas de ese ramal.
ipbx_recording_get
URL del audio de una llamada, a partir del call_id que devuelve ipbx_cdr_list.
Parámetros: call_id (string, obligatorio)
{
"call_id": "sip1-1787577145.251772",
"started": "2026-08-24 10:12:25",
"has_recording": true,
"url": "https://ipbx.vivavox.com.br/api/call/record/sip1-8f0e5161….wav",
"note": "URL publica e sem expiracao: o nome do arquivo e a unica credencial. …"
}Es tool separada en lugar de campo del ipbx_cdr_list por un motivo: la ruta /call/record del ipbx-api no exige autenticación y la URL no expira — el nombre del archivo (SHA1) es la credencial. Como campo de listado, cada llamada del CDR volcaría 25 accesos permanentes a conversaciones en el contexto, casi todos nunca usados, y la auditoría tendría que registrar 25 credenciales o no registrar nada. Una tool por grabación da una línea de auditoría con la identidad de quien pidió. El has_recording del CDR es la señal de descubrimiento; esta tool es el acceso.
Sin audio, la respuesta dice el motivo en lugar de solo negar — Chamada nao atendida (la grabación se arma antes del Dial) o grabación desactivada en el ramal. call_id de otro tenant devuelve isError: el filtro por IPBX_ID se aplica en la query, y el sip<id> de la URL viene del entorno, nunca del call_id recibido.
Depende de IPBX_RECORD_BASE_URL. Sin ella el servidor sube normalmente y solo falla esta tool, con mensaje explícito — es como desactivar la tool en un servidor.
Toda llamada genera una línea en audit_log con la identidad del llamador: email de Google si JWT, service:static si bearer estático. src/dst del ipbx_cdr_list son teléfono y no van a la auditoría — queda solo number_filter: true.
Comandos
npm run build # tsc
npm run check # tsc --noEmit (sem emitir)
npm run dev # tsc --watch
npm start # node dist/index.js
npm run inspect # MCP InspectorSmoke test local:
curl -s http://localhost:3000/health
curl -s http://localhost:3000/.well-known/oauth-authorization-server
curl -s -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Deploy
Docker (recomendado)
Dockerfile multi-stage (node:22-slim), runtime como usuario no-root mcp, expone /data como volumen para SQLite, healthcheck vía /health. En producción el deploy es automático vía .github/workflows/deploy.yml (push de tag vX.Y.Z → build en GHCR → docker run en la VPS). Manualmente:
docker image build . -t ipbx-mcp:1.0
docker container run -d --env-file .env -p 50020:3000 \
-v ipbx_data:/data --restart unless-stopped --name ipbx-mcp ipbx-mcp:1.0
docker stop ipbx-mcp && docker rm ipbx-mcp
docker logs -f ipbx-mcpBackup del SQLite:
docker run --rm \
-v ipbx_data:/data \
-v $PWD:/backup \
alpine tar czf /backup/sqlite-bkp.tgz -C /data .systemd
[Unit]
Description=ipbx-mcp
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/local/ipbx-mcp
ExecStart=/usr/bin/node dist/index.js
EnvironmentFile=/var/local/ipbx-mcp/.env
User=mcp
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.targetEnvironmentFile= es el equivalente nativo de systemd para .env. Usa un usuario dedicado (mcp) en lugar de root.
Configurando en un cliente MCP
Claude Desktop / CLI (bearer estático)
{
"mcpServers": {
"ipbx": {
"type": "http",
"url": "https://mcp.ipbx.vivavox.com.br/mcp",
"headers": {
"Authorization": "Bearer SEU_MCP_AUTH_TOKEN"
}
}
}
}claude.ai (OAuth)
Añadir como Custom Connector usando https://mcp.ipbx.vivavox.com.br/mcp. El flujo OAuth se dispara automáticamente — claude.ai descubre el AS vía WWW-Authenticate, registra un client vía DCR, redirige a Google, recibe el code y lo intercambia por un access token.
Estructura
src/
index.ts # bootstrap HTTP, leitura de env, registro de rotas
server.ts # createServer() registra as tools (ipbx_*)
mysql.ts # pool mysql2 + queries do IPBX (tenant fixo)
channel.ts # nome de canal do Asterisk -> ramal/tronco/fila
sqlite.ts # better-sqlite3 + apply schemas
audit.ts # logToolCall() -> audit_log
auth/
jwt.ts # sign/verify HS256 (jose)
middleware.ts # requireAuth: JWT -> fallback bearer estático
oauth/
routes.ts # registerOAuthRoutes()
store.ts # DCR clients, codes, refresh, authorize-tx
google.ts # OAuth do Google (authorize URL + token exchange)
pkce.ts # verificação S256 em tempo constante
sql/
001_oauth_schema.sql # oauth_clients, oauth_codes, oauth_refresh_tokens, audit_log
002_oauth_authorize_tx.sql # oauth_authorize_tx (state Google <-> params)
Dockerfile
.github/workflows/deploy.yml # build GHCR + deploy SSH na VPSThis server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Query, browse, and automate OmegaAI workspaces from any MCP client. Streamable HTTP with OAuth 2.0.
The official MCP Server from Mia-Platform to interact with Mia-Platform Console
Streamable HTTP MCP server exposing planner flows, tasks, and squads.
MCP server for Codat — companies, connections, invoices, bills and financial statements.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceRemote MCP server for Odoo ERP — exposes Odoo operations over Streamable HTTP with bearer token authentication.MIT
- FlicenseAqualityBmaintenanceAn MCP server that wraps the UTEL IP-telephony REST API as MCP tools, enabling LLM agents to make authenticated HTTP requests to the UTEL API via a simple tool interface.11
- FlicenseNot gradedqualityDmaintenanceA standalone MCP server that exposes API endpoints as tools for AI assistants, using SSE transport.
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/paralelum/ipbx-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server