Skip to main content
Glama
paralelum

ipbx-mcp

by paralelum

ipbx-mcp

IPBX 的 MCP 服务器,使用 TypeScript 编写。采用 Streamable HTTP 传输,无状态模式,认证方式为 静态 bearer 和/或 OAuth 2.1 + Google Workspace,本地持久化使用 SQLite(OAuth 客户端、刷新令牌、审计日志)。继承自 base-mcp 脚手架,将 PABX(MySQL)数据以类型化 tools 的形式暴露。

生产环境公共 URL:https://mcp.ipbx.vivavox.com.br

要求

  • Node.js >= 22better-sqlite3 v12 需要)

  • 对于 OAuth:在 Google Cloud Console 中以 Internal 模式创建 OAuth Client

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

JWT 的 HS256 密钥(32 字节十六进制)

GOOGLE_CLIENT_ID

来自 Google Cloud Console 的 OAuth Client

GOOGLE_CLIENT_SECRET

来自 Google Cloud Console 的 OAuth Client

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

Host 头中接受的 CSV 主机列表

SQLITE_PATH

./data/app.db

SQLite 文件路径

IPBX_RECORD_BASE_URL

提供录音的 IPBX API 基础地址(例如: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'))"

端点

方法

路径

认证

描述

POST

/mcp

bearer

通过 Streamable HTTP 的 MCP JSON-RPC

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

公开

动态客户端注册(RFC 7591)

GET

/authorize

公开

重定向到 Google

GET

/oauth/google/callback

公开

接收 Google 的重定向

POST

/token

公开

authorization_code / refresh_token

/mcp 上的 401 包含 WWW-Authenticate: Bearer realm=..., resource_metadata=...——没有这个,claude.ai 在首次接触时无法发现 AS。

可用工具

工具名称遵循 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"
}

如果配置的 IPBX_IDipbx 表中不存在,则返回 isError

ipbx_branch_list

列出实例的分机。

参数:

  • search(字符串,可选):按分机号码或名称进行部分搜索

  • limit(数字,可选):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(字符串,可选):按名称或电子邮件进行部分搜索

  • limit(数字,可选):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 列被排除:它是面板的登录密码,以纯文本形式存储在数据库中(无哈希)。暴露它将提供对 PABX 的管理访问权限。

ipbx_group_list

列出实例的分机组,以及每个组包含多少分机。

参数:

  • search(字符串,可选):按名称或描述进行部分搜索

  • limit(数字,可选):1–500,默认 100

返回:

{
  "total": 6,
  "truncated": false,
  "groups": [
    {
      "id": 1,
      "name": "Suporte",
      "description": "Grupo do suporte",
      "branches": 11
    }
  ]
}

groups 表不存储凭据——与 branchusers 不同,这里所有列都被暴露。

ipbx_trunk_list

列出实例的中继。

参数:

  • search(字符串,可选):按名称或主机进行部分搜索

  • limit(数字,可选):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"
    }
  ]
}

不返回运营商凭据。 usernamepassword 被排除——它们是数据库中最有价值的凭据,因为它们允许直接通过运营商发起呼叫,费用计入账户。取而代之的是 auth,它只说明中继如何认证:"credentials"(用户名/密码)或 "ip"(IP 白名单,无密码)。

ipbx_queue_list

列出呼叫队列,包括分配策略以及每个队列有多少成员。

参数: search(字符串,可选),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(数字,可选):过滤特定队列;省略则返回所有队列

  • limit(数字,可选):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它不是分机号码。 当成员是分机时,工具会将其解析为 exten + name。并非所有成员都是分机:存在 redirect-N 条目,它们以 type: "redirect" 返回,exten/name 为 null。

ipbx_ivr_list

列出 IVR,包括关联的音频以及向呼叫者播报的文本转写。

参数: search(字符串,可选——匹配名称转写文本),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 所说的内容而不是仅凭名称来查找 IVR。

ipbx_ivr_option_list

列出 IVR 的选项——哪个按键通向哪个目的地。

参数:

  • ivr_id(数字,可选):过滤特定 IVR;省略则返回所有 IVR

  • limit(数字,可选):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 个不同的表(branchqueueivrredirectapp),格式为 <类型>-<id>,并且还接受没有 id 的字面量(internal)。工具在所有情况下都解析目的地名称;字面量返回 name 为 null,并保留 ref

digit 字段并不总是数字:t 是超时,像 7X 这样的模式匹配分机范围。

ipbx_redirect_list

列出重定向——短分机,通过中继将呼叫转发到外部号码。它们与出现在队列、IVR 和路由规则中作为目的地的 redirect-<id> 相同。

参数: search(字符串,可选——匹配分机、名称或号码),limit(1–500,默认 100

{
  "total": 12,
  "redirects": [
    {
      "id": 2,
      "exten": "73",
      "name": "Ricardo Landim",
      "forward": "5535988023317",
      "trunk": "Vivavox",
      "ref": "redirect-2"
    }
  ]
}

⚠️ 个人数据。 forward 在 100% 的行中是个人手机号码——它不是凭据,但根据 LGPD 属于个人数据。工具返回它是因为它是该表存在的原因,但它不会进入 audit_log

ipbx_routing_list

列出路由计划,包括每个计划有多少规则和时间窗口。

参数: search(字符串,可选),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(数字,可选),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 格式存储,每行一个范围;工具将其作为列表返回。

ipbx_routing_rule_list

列出路由规则——拨号计划。每条规则在时间窗口内匹配号码模式,删除数字,添加前缀,并发送到目的地。

参数: routing_id(数字,可选),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 类型(用于出站规则)——总共六个可能的目的地。

这里处理了 schema 的两个细节:数据库列名为 supress(一个 "p"),暴露为 suppressgoto2/goto3 存在但在所有行中为空——仅当它们将来被填充时才作为 goto_extra 出现。

ipbx_cdr_list

呼叫历史。时间段是必需的,且限制为 31 天:cdr 除了主键外没有索引,因此每个过滤器都是全表扫描(目前约 268k 行)。

参数: date_fromdate_toYYYY-MM-DD,必填)、scopecall | leg,默认 call)、srcdst(部分匹配)、branch_idtrunk_idanswered(布尔值)、call_idlimit(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 是 PABX 中唯一没有 ipbx_id 的表。与租户的关联是 Asterisk 的 systemname,ipbx-api 将其写为 sip<ipbx_id>,Asterisk 将其盖在每一行的 uniqueid/linkedid 上——过滤条件是 uniqueid LIKE 'sip<id>-%',带连字符(没有连字符的话,sip1 也会匹配到 sip10-)。

一次通话由很多行组成:uniqueid 标识通道,linkedid 标识通话,每次 Dial 尝试都会生成一行——一条队列来电可达 22 行。scope=calllinkedid 分组;目标是 Local/ 通道的分支是队列对每个成员的振铃(变成 ring_attempts),其余的是通话(累加 talk_seconds)。scope=leg 返回原始分支——配合 call_id 使用可调试单次通话。

has_recording 要求 ANSWEREDrec 已填写——与面板的 recAvailable 规则相同。rec 列在 Dial 之前写入(拨号计划在 prerouting 中设置 MixMonitor),因此它标记的是"录音已布防"而非"存在音频":单独使用的话,99.96% 的通话都会显示有录音。

没有任何通道列是原始输出的:channeldstchannellastdata 携带端点的 username(即 SIP 凭据的一半),src 在内线通话中也携带同样的 username。所有内容都经过 src/channel.ts 处理,输出为分机/中继/队列。rec 也不在输出中——它变成了 has_recording

branch_id/trunk_id 过滤是通过半连接选择通话,而非按行过滤:聚合结果仍然描述整次通话,而不只是该分机的分支。

ipbx_recording_get

一次通话的音频 URL,基于 ipbx_cdr_list 返回的 call_id

参数: 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. …"
}

它之所以是独立的 tool 而不是 ipbx_cdr_list 的字段,原因如下:ipbx-api 的 /call/record 路由不要求认证,且 URL 永不过期——文件名(SHA1)就是凭据。如果作为列表字段,CDR 中的每次通话都会在上下文中倾倒 25 个永久访问权限,其中几乎全部永远不会被使用,而且审计要么得记录 25 个凭据,要么什么都不记录。每条录音一个 tool,可以生成一行包含请求者身份的审计记录。CDR 的 has_recording 是发现信号;这个 tool 是访问入口。

没有音频时,响应会说明原因而不是直接拒绝——Chamada nao atendida(录音在 Dial 之前布防)或分机上录音已关闭。其他租户的 call_id 返回 isErrorIPBX_ID 过滤在查询中应用,URL 中的 sip<id> 来自环境变量,绝不来自收到的 call_id

依赖 IPBX_RECORD_BASE_URL。没有它服务器正常启动,只有这个 tool 会失败,并显示明确的消息——就像在服务器上关闭了这个 tool。

每次调用都会在 audit_log 中生成一行,包含调用者身份:JWT 时为 Google 邮箱,静态 bearer 时为 service:staticipbx_cdr_listsrc/dst 是电话号码,不会进入审计——只保留 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(推荐)

多阶段 Dockerfilenode:22-slim),运行时以非 root 用户 mcp 运行,将 /data 暴露为 SQLite 的卷,通过 /health 做健康检查。生产环境通过 .github/workflows/deploy.yml 自动部署(推送 vX.Y.Z 标签 → 在 GHCR 构建 → 在 VPS 上 docker run)。手动方式:

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)

使用 https://mcp.ipbx.vivavox.com.br/mcp 添加为 Custom Connector。OAuth 流程会自动触发——claude.ai 通过 WWW-Authenticate 发现 AS,通过 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