ipbx-mcp
ipbx-mcp
Servidor MCP do IPBX em TypeScript. Transporte Streamable HTTP em modo stateless, autenticação por bearer estático e/ou OAuth 2.1 + Google Workspace, persistência local em SQLite (clients OAuth, refresh tokens, audit log). Herdado do scaffold base-mcp, expõe os dados do PABX (MySQL) como tools tipadas.
URL pública em produção: https://mcp.ipbx.vivavox.com.br.
Requisitos
Node.js >= 22 (
better-sqlite3v12 precisa)Para OAuth: OAuth Client no Google Cloud Console em modo Internal
Related MCP server: utel-mcp
Instalação
npm install
cp .env.example .env # depois preencha os valores reais
npm run buildConfiguração
Carregue o .env no processo (systemd EnvironmentFile=, docker env_file:, ou node --env-file=.env na hora do start).
Obrigatórias
Pelo menos um dos caminhos de auth:
Variável | Quando usar |
| Bearer estático — Claude Desktop, CLI, API, scripts, cron |
| OAuth — clientes via claude.ai (web/mobile) |
OAuth (opcional, mas necessário pra claude.ai)
Variável | Descrição |
| URL canônica do servidor (ex: |
| Chave HS256 dos JWTs (32 bytes hex) |
| Do OAuth Client no Google Cloud Console |
| Do OAuth Client no Google Cloud Console |
| Domínio Workspace permitido (default: |
Quando todas estão presentes, as rotas /authorize, /oauth/google/callback, /token e /register (DCR) são montadas. Sem elas, só o bearer estático funciona.
Outras
Variável | Default | Descrição |
|
| Porta HTTP |
|
| Interface (use |
| — | Lista CSV de hosts aceitos no header |
|
| Caminho do arquivo SQLite |
| — | Base da API do IPBX que serve as gravações (ex: |
MySQL (fonte de dados do IPBX)
Variável | Default | Descrição |
| — | Host do MySQL |
|
| |
| — | Use um usuário dedicado com |
| — | |
| — | |
|
| Tamanho do pool ( |
| vazio | Qualquer valor liga TLS com verificação de cert |
| — | Tenant que esta instância atende (ver abaixo) |
O banco é multi-tenant — uma instância Asterisk por cliente, tabela ipbx — mas cada instância do MCP atende um tenant só. Todas as queries filtram por IPBX_ID, e nenhuma tool aceita esse id como parâmetro: assim o isolamento entre clientes não depende do que o modelo passa na chamada. Um container e um subdomínio por tenant.
Gere tokens aleatórios com:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Endpoints
Método | Path | Auth | Descrição |
POST |
| bearer | JSON-RPC do MCP via 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 | Redireciona pro Google |
GET |
| público | Recebe o redirect do Google |
POST |
| público |
|
401 no /mcp inclui WWW-Authenticate: Bearer realm=..., resource_metadata=... — sem isso claude.ai não descobre o AS no primeiro contato.
Tools disponíveis
Nome das tools segue ipbx_<model>_<action>, com <action> no vocabulário list / get / search / count.
ipbx_instance_get
Dados de cadastro da instância IPBX que este servidor atende — nome, IP e portas SIP/AMI.
Parâmetros: nenhum. A instância é fixa, definida por IPBX_ID no ambiente.
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"
}Devolve isError se o IPBX_ID configurado não existir na tabela ipbx.
ipbx_branch_list
Lista os ramais da instância.
Parâmetros:
search(string, opcional): busca parcial por número do ramal ou nomelimit(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
}
]
}Não retorna as credenciais SIP. As colunas password (senha em claro) e username (identificador de autenticação, diferente do número do ramal) ficam de fora por design — juntas permitem registrar um softphone e originar chamadas na conta do cliente. A lista de colunas no SELECT é explícita justamente para que nenhuma delas entre por descuido.
ipbx_user_list
Lista os usuários do painel da instância.
Parâmetros:
search(string, opcional): busca parcial por nome ou 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"
}
]
}Não retorna a senha de acesso. A coluna secret fica de fora: é a senha de login do painel, guardada em texto puro no banco (sem hash). Expor isso entregaria acesso administrativo ao PABX.
ipbx_group_list
Lista os grupos de ramais da instância, com quantos ramais cada um tem.
Parâmetros:
search(string, opcional): busca parcial por nome ou descriçãolimit(number, opcional): 1–500, default100
Retorno:
{
"total": 6,
"truncated": false,
"groups": [
{
"id": 1,
"name": "Suporte",
"description": "Grupo do suporte",
"branches": 11
}
]
}A tabela groups não guarda credenciais — ao contrário de branch e users, aqui todas as colunas são expostas.
ipbx_trunk_list
Lista os troncos da instância.
Parâmetros:
search(string, opcional): busca parcial por nome ou 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"
}
]
}Não retorna as credenciais da operadora. username e password ficam de fora — são a credencial mais valiosa do banco, já que permitem originar chamadas direto pela operadora, tarifadas na conta. No lugar delas vai auth, que diz apenas como o tronco autentica: "credentials" (usuário/senha) ou "ip" (allowlist de IP, sem senha).
ipbx_queue_list
Lista as filas de atendimento, com a estratégia de distribuição e quantos membros cada uma tem.
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 os membros das filas, na ordem de toque.
Parâmetros:
queue_id(number, opcional): filtra uma fila; omita para trazer 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"
}
]
}A coluna queue_member.member guarda uma referência no formato <tipo>-<id> — branch-10 aponta para o branch.id 10, que é o ramal 29. Não é o número do ramal. A tool resolve isso para exten + name quando o membro é um ramal. Nem todo membro é: existem entradas redirect-N, que voltam com type: "redirect" e exten/name nulos.
ipbx_ivr_list
Lista as URAs, com o áudio associado e a transcrição do que é falado para quem liga.
Parâmetros: search (string, opcional — casa no nome ou no texto da transcrição), 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
}
]
}A transcrição é o campo mais útil: permite achar uma URA pelo que ela diz, não só pelo nome.
ipbx_ivr_option_list
Lista as opções das URAs — qual tecla leva a qual destino.
Parâmetros:
ivr_id(number, opcional): filtra uma URA; omita para trazer 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 é polimórfico: aponta para 5 tabelas diferentes (branch, queue, ivr, redirect, app) no formato <tipo>-<id>, e ainda aceita literais sem id (internal). A tool resolve o nome do destino em todos os casos; literais voltam com name nulo e o ref preservado.
O campo digit nem sempre é um dígito: t é timeout e padrões como 7X casam faixas de ramal.
ipbx_redirect_list
Lista os redirects — ramais curtos que encaminham para um número externo saindo por um tronco. São os mesmos redirect-<id> que aparecem como destino em filas, URAs e regras de roteamento.
Parâmetros: search (string, opcional — casa ramal, nome ou número), limit (1–500, default 100)
{
"total": 12,
"redirects": [
{
"id": 2,
"exten": "73",
"name": "Ricardo Landim",
"forward": "5535988023317",
"trunk": "Vivavox",
"ref": "redirect-2"
}
]
}⚠️ Dado pessoal. forward é um número de celular pessoal em 100% das linhas — não é credencial, mas é dado pessoal sob LGPD. A tool o retorna porque é a razão de existir da tabela, mas ele não vai para o audit_log.
ipbx_routing_list
Lista os planos de roteamento, com quantas regras e janelas de horário cada um tem.
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 as janelas de horário dos planos.
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"]
}O pattern é armazenado no formato do Asterisk, uma faixa por linha; a tool devolve como lista.
ipbx_routing_rule_list
Lista as regras de roteamento — o dialplan. Cada regra casa um padrão de número dentro de uma janela de horário, suprime dígitos, adiciona prefixo e envia ao 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 é polimórfico como o da URA, mais o tipo trunk (usado nas regras de saída) — seis destinos possíveis no total.
Dois detalhes do schema tratados aqui: a coluna do banco chama-se supress (com um "p"), exposta como suppress; e goto2/goto3 existem mas estão vazias em todas as linhas — aparecem como goto_extra apenas se algum dia forem preenchidas.
ipbx_cdr_list
Histórico de chamadas. Período é obrigatório e limitado a 31 dias: a cdr não tem índice além da PK, então todo filtro é full scan (~268k linhas hoje).
Parâmetros: date_from e date_to (YYYY-MM-DD, obrigatórios), scope (call | leg, default call), src e dst (parcial), branch_id, trunk_id, answered (bool), call_id, limit (1–500, default 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
}A cdr é a única tabela do PABX sem ipbx_id. O vínculo com o tenant é o systemname do Asterisk, que o ipbx-api escreve como sip<ipbx_id> e o Asterisk carimba no uniqueid/linkedid de cada linha — o filtro é uniqueid LIKE 'sip<id>-%', com o hífen (sem ele, sip1 casaria sip10- também).
Uma chamada são muitas linhas: uniqueid identifica o canal, linkedid a chamada, e cada tentativa de Dial gera uma linha — uma entrante de fila chega a 22. scope=call agrupa por linkedid; as pernas cujo destino é um canal Local/ são o toque da fila em cada membro (viram ring_attempts) e as demais são conversa (somam talk_seconds). scope=leg devolve as pernas cruas — use com call_id pra depurar uma chamada.
has_recording exige ANSWERED além de rec preenchido — mesma regra do recAvailable do painel. A coluna rec é escrita antes do Dial (o dialplan arma MixMonitor no prerouting), então ela marca "gravação armada" e não "existe áudio": sozinha, daria gravação em 99,96% das chamadas.
Nenhuma coluna de canal sai crua: channel, dstchannel e lastdata carregam o username do endpoint, que é metade da credencial SIP, e src traz esse mesmo username em chamada interna. Tudo passa por src/channel.ts e sai como ramal/tronco/fila. rec também fica fora — vira has_recording.
Filtrar por branch_id/trunk_id seleciona as chamadas por semi-join, não por linha: os agregados continuam descrevendo a chamada inteira, não só as pernas daquele ramal.
ipbx_recording_get
URL do áudio de uma chamada, a partir do call_id que o ipbx_cdr_list devolve.
Parâmetros: call_id (string, obrigatório)
{
"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. …"
}É tool separada em vez de campo do ipbx_cdr_list por um motivo: a rota /call/record do ipbx-api não exige autenticação e a URL não expira — o nome do arquivo (SHA1) é a credencial. Como campo de listagem, cada chamada do CDR despejaria 25 acessos permanentes a conversas no contexto, quase todos nunca usados, e o audit teria que gravar 25 credenciais ou não registrar nada. Uma tool por gravação dá uma linha de auditoria com a identidade de quem pediu. O has_recording do CDR é o sinal de descoberta; esta tool é o acesso.
Sem áudio, a resposta diz o motivo em vez de só negar — Chamada nao atendida (a gravação é armada antes do Dial) ou gravação desligada no ramal. call_id de outro tenant devolve isError: o filtro por IPBX_ID é aplicado na query, e o sip<id> da URL vem do ambiente, nunca do call_id recebido.
Depende de IPBX_RECORD_BASE_URL. Sem ela o servidor sobe normalmente e só esta tool falha, com mensagem explícita — é como desligar a tool num servidor.
Toda chamada gera uma linha em audit_log com a identidade do chamador: email Google se JWT, service:static se bearer estático. src/dst do ipbx_cdr_list são telefone e não vão pro audit — fica só 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 user não-root mcp, expõe /data como volume pro SQLite, healthcheck via /health. Em produção o deploy é automático via .github/workflows/deploy.yml (push de tag vX.Y.Z → build no GHCR → docker run na 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 do 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= é o equivalente nativo do systemd para .env. Use um usuário dedicado (mcp) em vez de root.
Configurando em um 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)
Adicionar como Custom Connector usando https://mcp.ipbx.vivavox.com.br/mcp. O flow OAuth dispara automaticamente — claude.ai descobre o AS via WWW-Authenticate, registra um client via DCR, redireciona pro Google, recebe o code e troca por um access token.
Estrutura
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