Skip to main content
Glama
paralelum

ipbx-mcp

by paralelum

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

  • Для 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 при запуске).

Обязательные

Как минимум один из способов аутентификации:

Переменная

Когда использовать

MCP_AUTH_TOKEN

Статический bearer — Claude Desktop, CLI, API, скрипты, cron

OAUTH_JWT_SECRET + OAUTH_ISSUER

OAuth — клиенты через claude.ai (web/mobile)

OAuth (необязательно, но необходимо для claude.ai)

Переменная

Описание

OAUTH_ISSUER

Канонический URL сервера (например: https://mcp.ipbx.vivavox.com.br)

OAUTH_JWT_SECRET

Ключ HS256 для JWT (32 байта hex)

GOOGLE_CLIENT_ID

Из OAuth-клиента в Google Cloud Console

GOOGLE_CLIENT_SECRET

Из OAuth-клиента в Google Cloud Console

ALLOWED_GOOGLE_HD

Разрешённый домен Workspace (по умолчанию: vivavox.com.br)

Когда все они присутствуют, монтируются маршруты /authorize, /oauth/google/callback, /token и /register (DCR). Без них работает только статический bearer.

Прочие

Переменная

По умолчанию

Описание

PORT

3000

HTTP-порт

HOST

0.0.0.0

Интерфейс (используйте 127.0.0.1 в локальной разработке)

MCP_ALLOWED_HOSTS

Список CSV хостов, принимаемых в заголовке Host

SQLITE_PATH

./data/app.db

Путь к файлу SQLite

IPBX_RECORD_BASE_URL

Базовый URL API IPBX, который отдаёт записи (например: https://ipbx.vivavox.com.br/api). Без него ipbx_recording_get завершится ошибкой

MySQL (источник данных IPBX)

Переменная

По умолчанию

Описание

MYSQL_HOST

Хост MySQL

MYSQL_PORT

3306

MYSQL_USER

Используйте выделенного пользователя только с GRANT SELECT

MYSQL_PASSWORD

MYSQL_DATABASE

MYSQL_POOL_LIMIT

5

Размер пула (mysql2)

MYSQL_SSL

пусто

Любое значение включает TLS с проверкой сертификата

IPBX_ID

Тенант, который обслуживает этот экземпляр (см. ниже)

База данных мультитенантная — один экземпляр Asterisk на клиента, таблица ipbx — но каждый экземпляр MCP обслуживает только один тенант. Все запросы фильтруются по IPBX_ID, и ни одна tool не принимает этот id как параметр: так изоляция между клиентами не зависит от того, что модель передаёт в вызове. Один контейнер и один поддомен на тенанта.

Сгенерируйте случайные токены с помощью:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Эндпоинты

Метод

Path

Auth

Описание

POST

/mcp

bearer

JSON-RPC MCP через Streamable HTTP

GET

/mcp

bearer

405

DELETE

/mcp

bearer

405

GET

/health

публичный

{"status":"ok"}

GET

/.well-known/oauth-authorization-server

публичный

метаданные RFC 8414

GET

/.well-known/oauth-protected-resource

публичный

метаданные RFC 9728

POST

/register

публичный

Dynamic Client Registration (RFC 7591)

GET

/authorize

публичный

Перенаправляет на Google

GET

/oauth/google/callback

публичный

Принимает редирект от Google

POST

/token

публичный

authorization_code / refresh_token

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, необязательно): частичный поиск по имени или email

  • limit (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.target

EnvironmentFile= — это нативный эквивалент 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 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