ipbx-mcp
MCP server that exposes IPBX PABX data (branches, users, groups, trunks, queues) as typed tools via Streamable HTTP, with OAuth and static bearer authentication.
README
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
Instalação
npm install
cp .env.example .env # depois preencha os valores reais
npm run build
Configuraçã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 |
|---|---|
MCP_AUTH_TOKEN |
Bearer estático — Claude Desktop, CLI, API, scripts, cron |
OAUTH_JWT_SECRET + OAUTH_ISSUER |
OAuth — clientes via claude.ai (web/mobile) |
OAuth (opcional, mas necessário pra claude.ai)
| Variável | Descrição |
|---|---|
OAUTH_ISSUER |
URL canônica do servidor (ex: https://mcp.ipbx.vivavox.com.br) |
OAUTH_JWT_SECRET |
Chave HS256 dos JWTs (32 bytes hex) |
GOOGLE_CLIENT_ID |
Do OAuth Client no Google Cloud Console |
GOOGLE_CLIENT_SECRET |
Do OAuth Client no Google Cloud Console |
ALLOWED_GOOGLE_HD |
Domínio Workspace permitido (default: vivavox.com.br) |
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 |
|---|---|---|
PORT |
3000 |
Porta HTTP |
HOST |
0.0.0.0 |
Interface (use 127.0.0.1 em dev local) |
MCP_ALLOWED_HOSTS |
— | Lista CSV de hosts aceitos no header Host |
SQLITE_PATH |
./data/app.db |
Caminho do arquivo SQLite |
MySQL (fonte de dados do IPBX)
| Variável | Default | Descrição |
|---|---|---|
MYSQL_HOST |
— | Host do MySQL |
MYSQL_PORT |
3306 |
|
MYSQL_USER |
— | Use um usuário dedicado com GRANT SELECT apenas |
MYSQL_PASSWORD |
— | |
MYSQL_DATABASE |
— | |
MYSQL_POOL_LIMIT |
5 |
Tamanho do pool (mysql2) |
MYSQL_SSL |
vazio | Qualquer valor liga TLS com verificação de cert |
IPBX_ID |
— | 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 | /mcp |
bearer | JSON-RPC do MCP via 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 | Redireciona pro Google |
| GET | /oauth/google/callback |
público | Recebe o redirect do Google |
| POST | /token |
público | authorization_code / refresh_token |
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.
Toda chamada gera uma linha em audit_log com a identidade do chamador: email Google se JWT, service:static se bearer estático.
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 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-mcp
Backup 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.target
EnvironmentFile= é 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)
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
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。