Skip to main content
Glama
paralelum

ipbx-mcp

by paralelum

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-sqlite3 v12 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 build

Configuració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

MCP_AUTH_TOKEN

Bearer estático — Claude Desktop, CLI, API, scripts, cron

OAUTH_JWT_SECRET + OAUTH_ISSUER

OAuth — clientes vía claude.ai (web/mobile)

OAuth (opcional, pero necesario para claude.ai)

Variable

Descripción

OAUTH_ISSUER

URL canónica del servidor (ej: https://mcp.ipbx.vivavox.com.br)

OAUTH_JWT_SECRET

Clave HS256 de los JWTs (32 bytes hex)

GOOGLE_CLIENT_ID

Del OAuth Client en Google Cloud Console

GOOGLE_CLIENT_SECRET

Del OAuth Client en Google Cloud Console

ALLOWED_GOOGLE_HD

Dominio Workspace permitido (default: vivavox.com.br)

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

PORT

3000

Puerto HTTP

HOST

0.0.0.0

Interfaz (use 127.0.0.1 en dev local)

MCP_ALLOWED_HOSTS

Lista CSV de hosts aceptados en el header Host

SQLITE_PATH

./data/app.db

Ruta del archivo SQLite

IPBX_RECORD_BASE_URL

Base de la API del IPBX que sirve las grabaciones (ej: https://ipbx.vivavox.com.br/api). Sin ella, ipbx_recording_get falla

MySQL (fuente de datos del IPBX)

Variable

Default

Descripción

MYSQL_HOST

Host del MySQL

MYSQL_PORT

3306

MYSQL_USER

Use un usuario dedicado con GRANT SELECT solamente

MYSQL_PASSWORD

MYSQL_DATABASE

MYSQL_POOL_LIMIT

5

Tamaño del pool (mysql2)

MYSQL_SSL

vacío

Cualquier valor activa TLS con verificación de cert

IPBX_ID

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

/mcp

bearer

JSON-RPC del MCP vía Streamable HTTP

GET

/mcp

bearer

405

DELETE

/mcp

bearer

405

GET

/health

público

{"status":"ok"}

GET

/.well-known/oauth-authorization-server

público

RFC 8414 metadata

GET

/.well-known/oauth-protected-resource

público

RFC 9728 metadata

POST

/register

público

Dynamic Client Registration (RFC 7591)

GET

/authorize

público

Redirige a Google

GET

/oauth/google/callback

público

Recibe el redirect de Google

POST

/token

público

authorization_code / refresh_token

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 nombre

  • limit (number, opcional): 1–500, default 100

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 email

  • limit (number, opcional): 1–500, default 100

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ón

  • limit (number, opcional): 1–500, default 100

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 host

  • limit (number, opcional): 1–500, default 100

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 todas

  • limit (number, opcional): 1–500, default 200

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 todas

  • limit (number, opcional): 1–500, default 200

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 Inspector

Smoke 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-mcp

Backup 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.target

EnvironmentFile= 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 VPS

Maintenance

ActivityMaintained
ResponsivenessSyncing

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

Related MCP Servers

  • F
    license
    A
    quality
    B
    maintenance
    An 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.
    1
    1
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server for Bvoip / 1Stream that exposes call-reporting, phone-status, and CRM-extension-mapping endpoints as MCP tools.

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/paralelum/ipbx-mcp'

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