ipbx-mcp
ipbx-mcp
Сервер MCP для IPBX на TypeScript. Транспорт Streamable HTTP в stateless-режиме, аутентификация по статическому bearer и/или OAuth 2.1 + Google Workspace, локальное хранение в SQLite (OAuth-клиенты, refresh-токены, журнал аудита). Унаследован от каркаса base-mcp, предоставляет данные АТС (MySQL) как типизированные tools.
Публичный URL в продакшене: https://mcp.ipbx.vivavox.com.br.
Требования
Node.js >= 22 (нужен для
better-sqlite3v12)Для OAuth: OAuth-клиент в Google Cloud Console в режиме Internal
Related MCP server: utel-mcp
Установка
npm install
cp .env.example .env # depois preencha os valores reais
npm run buildКонфигурация
Загрузите .env в процесс (systemd EnvironmentFile=, docker env_file:, или node --env-file=.env при запуске).
Обязательные
Как минимум один из способов аутентификации:
Переменная | Когда использовать |
| Статический bearer — Claude Desktop, CLI, API, скрипты, cron |
| OAuth — клиенты через claude.ai (web/mobile) |
OAuth (необязательно, но необходимо для claude.ai)
Переменная | Описание |
| Канонический URL сервера (например: |
| Ключ HS256 для JWT (32 байта hex) |
| Из OAuth-клиента в Google Cloud Console |
| Из OAuth-клиента в Google Cloud Console |
| Разрешённый домен Workspace (по умолчанию: |
Когда все они присутствуют, монтируются маршруты /authorize, /oauth/google/callback, /token и /register (DCR). Без них работает только статический bearer.
Прочие
Переменная | По умолчанию | Описание |
|
| HTTP-порт |
|
| Интерфейс (используйте |
| — | Список CSV хостов, принимаемых в заголовке |
|
| Путь к файлу SQLite |
| — | Базовый URL API IPBX, который отдаёт записи (например: |
MySQL (источник данных IPBX)
Переменная | По умолчанию | Описание |
| — | Хост MySQL |
|
| |
| — | Используйте выделенного пользователя только с |
| — | |
| — | |
|
| Размер пула ( |
| пусто | Любое значение включает TLS с проверкой сертификата |
| — | Тенант, который обслуживает этот экземпляр (см. ниже) |
База данных мультитенантная — один экземпляр Asterisk на клиента, таблица ipbx — но каждый экземпляр MCP обслуживает только один тенант. Все запросы фильтруются по IPBX_ID, и ни одна tool не принимает этот id как параметр: так изоляция между клиентами не зависит от того, что модель передаёт в вызове. Один контейнер и один поддомен на тенанта.
Сгенерируйте случайные токены с помощью:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Эндпоинты
Метод | Path | Auth | Описание |
POST |
| bearer | JSON-RPC MCP через Streamable HTTP |
GET |
| bearer |
|
DELETE |
| bearer |
|
GET |
| публичный |
|
GET |
| публичный | метаданные RFC 8414 |
GET |
| публичный | метаданные RFC 9728 |
POST |
| публичный | Dynamic Client Registration (RFC 7591) |
GET |
| публичный | Перенаправляет на Google |
GET |
| публичный | Принимает редирект от Google |
POST |
| публичный |
|
401 на /mcp включает WWW-Authenticate: Bearer realm=..., resource_metadata=... — без этого claude.ai не обнаружит AS при первом контакте.
Доступные tools
Имена tools следуют схеме ipbx_<model>_<action>, где <action> из словаря list / get / search / count.
ipbx_instance_get
Данные регистрации экземпляра IPBX, который обслуживает этот сервер — имя, IP и порты SIP/AMI.
Параметры: нет. Экземпляр фиксирован, задаётся через IPBX_ID в окружении.
Возврат:
{
"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"
}Возвращает isError, если настроенный IPBX_ID не существует в таблице ipbx.
ipbx_branch_list
Список внутренних номеров (ramais) экземпляра.
Параметры:
search(string, необязательно): частичный поиск по номеру внутреннего или имениlimit(number, необязательно): 1–500, по умолчанию100
Возврат:
{
"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
}
]
}Не возвращает SIP-учётные данные. Колонки password (пароль в открытом виде) и username (идентификатор аутентификации, отличающийся от номера внутреннего) исключены намеренно — вместе они позволяют зарегистрировать софтфон и совершать звонки с аккаунта клиента. Список колонок в SELECT явный именно для того, чтобы ни одна из них не попала случайно.
ipbx_user_list
Список пользователей панели экземпляра.
Параметры:
search(string, необязательно): частичный поиск по имени или emaillimit(number, необязательно): 1–500, по умолчанию100
Возврат:
{
"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"
}
]
}Не возвращает пароль доступа. Колонка secret исключена: это пароль для входа в панель, хранящийся в открытом виде в базе (без хеша). Раскрытие этого дало бы административный доступ к АТС.
ipbx_group_list
Список групп внутренних номеров экземпляра, с количеством внутренних в каждой.
Параметры:
search(string, необязательно): частичный поиск по имени или описаниюlimit(number, необязательно): 1–500, по умолчанию100
Возврат:
{
"total": 6,
"truncated": false,
"groups": [
{
"id": 1,
"name": "Suporte",
"description": "Grupo do suporte",
"branches": 11
}
]
}Таблица groups не хранит учётные данные — в отличие от branch и users, здесь раскрываются все колонки.
ipbx_trunk_list
Список транков экземпляра.
Параметры:
search(string, необязательно): частичный поиск по имени или хостуlimit(number, необязательно): 1–500, по умолчанию100
Возврат:
{
"total": 2,
"truncated": false,
"trunks": [
{
"id": 1,
"name": "Vivavox",
"host": "sip.vivavox.com.br",
"port": "5060",
"register": true,
"record": true,
"auth": "credentials"
}
]
}Не возвращает учётные данные оператора. username и password исключены — это самые ценные учётные данные в базе, так как позволяют совершать звонки напрямую через оператора, тарифицируемые на счёт. Вместо них идёт auth, который говорит только как транк аутентифицируется: "credentials" (пользователь/пароль) или "ip" (белый список IP, без пароля).
ipbx_queue_list
Список очередей обслуживания, со стратегией распределения и количеством участников в каждой.
Параметры: search (string, необязательно), limit (1–500, по умолчанию 100)
{
"total": 5,
"queues": [
{ "id": 1, "name": "Suporte", "strategy": "ringall", "members": 8 },
{ "id": 5, "name": "Teste", "strategy": "leastrecent", "members": 1 }
]
}ipbx_queue_member_list
Список участников очередей, в порядке обзвона.
Параметры:
queue_id(number, необязательно): фильтрует одну очередь; опустите, чтобы получить всеlimit(number, необязательно): 1–500, по умолчанию200
Возврат:
{
"total": 8,
"members": [
{
"queue_id": 1,
"queue": "Suporte",
"position": 1,
"type": "branch",
"exten": "29",
"name": "Mateus Damaceno",
"ref": "branch-10"
}
]
}Колонка queue_member.member хранит ссылку в формате <тип>-<id> — branch-10 указывает на branch.id 10, который является внутренним номером 29. Это не номер внутреннего. Tool разрешает это в exten + name, когда участник — внутренний номер. Не каждый участник такой: существуют записи redirect-N, которые возвращаются с type: "redirect" и exten/name равными null.
ipbx_ivr_list
Список IVR (URA), с привязанным аудио и транскрипцией того, что говорится звонящему.
Параметры: search (string, необязательно — совпадает по имени или тексту транскрипции), limit (1–500, по умолчанию 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
}
]
}Транскрипция — самое полезное поле: позволяет найти IVR по тому, что она говорит, а не только по имени.
ipbx_ivr_option_list
Список опций IVR — какая клавиша ведёт к какому назначению.
Параметры:
ivr_id(number, необязательно): фильтрует одну IVR; опустите, чтобы получить всеlimit(number, необязательно): 1–500, по умолчанию200
Возврат:
{
"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 полиморфен: указывает на 5 разных таблиц (branch, queue, ivr, redirect, app) в формате <тип>-<id>, а также принимает литералы без id (internal). Tool разрешает имя назначения во всех случаях; литералы возвращаются с name равным null и сохранённым ref.
Поле digit не всегда является цифрой: t — это таймаут, а шаблоны вроде 7X соответствуют диапазонам внутренних номеров.
ipbx_redirect_list
Список редиректов — коротких внутренних номеров, которые перенаправляют на внешний номер через транк. Это те же redirect-<id>, которые появляются как назначения в очередях, IVR и правилах маршрутизации.
Параметры: search (string, необязательно — совпадает по внутреннему номеру, имени или номеру), limit (1–500, по умолчанию 100)
{
"total": 12,
"redirects": [
{
"id": 2,
"exten": "73",
"name": "Ricardo Landim",
"forward": "5535988023317",
"trunk": "Vivavox",
"ref": "redirect-2"
}
]
}⚠️ Персональные данные. forward — это личный номер мобильного телефона в 100% строк — это не учётные данные, но персональные данные по LGPD. Tool возвращает его, потому что это причина существования таблицы, но он не попадает в audit_log.
ipbx_routing_list
Список планов маршрутизации, с количеством правил и временных окон в каждом.
Параметры: search (string, необязательно), limit (1–500, по умолчанию 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
Список временных окон планов.
Параметры: routing_id (number, необязательно), limit (1–500, по умолчанию 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"]
}pattern хранится в формате Asterisk, один диапазон на строку; tool возвращает его как список.
ipbx_routing_rule_list
Список правил маршрутизации — диалплан. Каждое правило сопоставляет шаблон номера внутри временного окна, удаляет цифры, добавляет префикс и отправляет на назначение.
Параметры: routing_id (number, необязательно), limit (1–500, по умолчанию 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 полиморфен, как у IVR, плюс тип trunk (используется в правилах исходящих) — всего шесть возможных назначений.
Две детали схемы, обработанные здесь: колонка в базе называется supress (с одним "p"), раскрывается как suppress; а goto2/goto3 существуют, но пусты во всех строках — появляются как goto_extra, только если когда-нибудь будут заполнены.
ipbx_cdr_list
История звонков. Период обязателен и ограничен 31 днём: в cdr нет индекса, кроме PK, поэтому любой фильтр — это полное сканирование (~268k строк на сегодня).
Параметры: date_from и date_to (YYYY-MM-DD, обязательные), scope (call | leg, по умолчанию call), src и dst (частичные), branch_id, trunk_id, answered (bool), call_id, limit (1–500, по умолчанию 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
}cdr — единственная таблица АТС без ipbx_id. Связь с тенантом — это systemname из Asterisk, который ipbx-api записывает как sip<ipbx_id>, а Asterisk проставляет в uniqueid/linkedid каждой строки — фильтр uniqueid LIKE 'sip<id>-%' с дефисом (без него sip1 совпал бы и с sip10- тоже).
Звонок — это много строк: uniqueid идентифицирует канал, linkedid — вызов, и каждая попытка Dial создаёт строку — входящий из очереди доходит до 22. scope=call группирует по linkedid; ветви, чей пункт назначения — канал Local/, это звонок очереди каждому члену (становятся ring_attempts), а остальные — разговор (суммируются в talk_seconds). scope=leg возвращает сырые ветви — используйте с call_id для отладки вызова.
has_recording требует ANSWERED в дополнение к заполненному rec — то же правило, что и recAvailable в панели. Колонка rec записывается до Dial (диалплан настраивает MixMonitor в prerouting), поэтому она отмечает «запись подготовлена», а не «есть аудио»: сама по себе она дала бы запись в 99,96% вызовов.
Ни одна колонка канала не выходит сырой: channel, dstchannel и lastdata несут username конечной точки, который является половиной SIP-учётных данных, а src приносит тот же username во внутреннем вызове. Всё проходит через src/channel.ts и выходит как внутренний номер/транк/очередь. rec тоже остаётся вне — превращается в has_recording.
Фильтрация по branch_id/trunk_id выбирает вызовы через полу-соединение, а не по строке: агрегаты по-прежнему описывают весь вызов, а не только ветви этого филиала.
ipbx_recording_get
URL аудио одного вызова по call_id, который возвращает ipbx_cdr_list.
Параметры: call_id (строка, обязательный)
{
"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. …"
}Это отдельный инструмент, а не поле ipbx_cdr_list, по причине: маршрут /call/record в ipbx-api не требует аутентификации, и URL не истекает — имя файла (SHA1) и есть учётные данные. Как поле списка, каждый вызов из CDR вываливал бы 25 постоянных доступов к разговорам в контекст, почти все никогда не используемые, а аудит должен был бы записывать 25 учётных данных или не регистрировать ничего. Один инструмент на запись даёт строку аудита с личностью запросившего. has_recording из CDR — сигнал обнаружения; этот инструмент — доступ.
Без аудио ответ объясняет причину, а не просто отказывает — Chamada nao atendida (запись подготавливается до Dial) или запись отключена на внутреннем номере. call_id из другого тенанта возвращает isError: фильтр по IPBX_ID применяется в запросе, а sip<id> из URL берётся из окружения, никогда из полученного call_id.
Зависит от IPBX_RECORD_BASE_URL. Без него сервер запускается нормально, и только этот инструмент падает с явным сообщением — это как отключить инструмент на сервере.
Каждый вызов создаёт строку в audit_log с личностью вызывающего: email Google, если JWT, service:static, если статический bearer. src/dst из ipbx_cdr_list — это телефон и не идут в аудит — остаётся только number_filter: true.
Команды
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Локальный смоук-тест:
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"}'Развёртывание
Docker (рекомендуется)
Многоступенчатый Dockerfile (node:22-slim), среда выполнения как непривилегированный пользователь mcp, /data как том для SQLite, healthcheck через /health. В продакшене развёртывание автоматическое через .github/workflows/deploy.yml (push тега vX.Y.Z → сборка в GHCR → docker run на VPS). Вручную:
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Резервное копирование 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= — это нативный эквивалент systemd для .env. Используйте выделенного пользователя (mcp) вместо root.
Настройка в MCP-клиенте
Claude Desktop / CLI (статический bearer)
{
"mcpServers": {
"ipbx": {
"type": "http",
"url": "https://mcp.ipbx.vivavox.com.br/mcp",
"headers": {
"Authorization": "Bearer SEU_MCP_AUTH_TOKEN"
}
}
}
}claude.ai (OAuth)
Добавить как Custom Connector, используя https://mcp.ipbx.vivavox.com.br/mcp. Поток OAuth запускается автоматически — claude.ai обнаруживает AS через WWW-Authenticate, регистрирует клиента через DCR, перенаправляет на Google, получает code и обменивает его на access token.
Структура
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