ipbx-mcp
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 >= 22(
better-sqlite3v12 需要)对于 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)。
必填项
至少使用以下认证方式之一:
变量 | 使用场景 |
| 静态 Bearer — Claude Desktop、CLI、API、脚本、cron |
| OAuth — 通过 claude.ai(web/mobile)的客户端 |
OAuth(可选,但对 claude.ai 是必需的)
变量 | 描述 |
| 服务器的规范 URL(例如: |
| JWT 的 HS256 密钥(32 字节十六进制) |
| 来自 Google Cloud Console 的 OAuth Client |
| 来自 Google Cloud Console 的 OAuth Client |
| 允许的 Workspace 域名(默认: |
当所有这些变量都存在时,会挂载 /authorize、/oauth/google/callback、/token 和 /register(DCR)路由。否则,只有静态 bearer 可用。
其他
变量 | 默认值 | 描述 |
|
| HTTP 端口 |
|
| 监听接口(本地开发使用 |
| — |
|
|
| SQLite 文件路径 |
| — | 提供录音的 IPBX API 基础地址(例如: |
MySQL(IPBX 数据源)
变量 | 默认值 | 描述 |
| — | MySQL 主机 |
|
| |
| — | 使用仅具有 |
| — | |
| — | |
|
| 连接池大小( |
| 空 | 任何值都会启用带证书验证的 TLS |
| — | 此实例服务的租户(见下文) |
数据库是多租户的——每个客户一个 Asterisk 实例,表 ipbx——但每个 MCP 实例只服务一个租户。所有查询都按 IPBX_ID 过滤,并且没有 tool 接受该 id 作为参数:这样客户之间的隔离不依赖于模型在调用中传递的内容。每个租户一个容器和一个子域名。
生成随机令牌:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"端点
方法 | 路径 | 认证 | 描述 |
POST |
| bearer | 通过 Streamable HTTP 的 MCP JSON-RPC |
GET |
| bearer |
|
DELETE |
| bearer |
|
GET |
| 公开 |
|
GET |
| 公开 | RFC 8414 元数据 |
GET |
| 公开 | RFC 9728 元数据 |
POST |
| 公开 | 动态客户端注册(RFC 7591) |
GET |
| 公开 | 重定向到 Google |
GET |
| 公开 | 接收 Google 的重定向 |
POST |
| 公开 |
|
/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_ID 在 ipbx 表中不存在,则返回 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 表不存储凭据——与 branch 和 users 不同,这里所有列都被暴露。
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"
}
]
}不返回运营商凭据。 username 和 password 被排除——它们是数据库中最有价值的凭据,因为它们允许直接通过运营商发起呼叫,费用计入账户。取而代之的是 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;省略则返回所有 IVRlimit(数字,可选):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)。工具在所有情况下都解析目的地名称;字面量返回 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"),暴露为 suppress;goto2/goto3 存在但在所有行中为空——仅当它们将来被填充时才作为 goto_extra 出现。
ipbx_cdr_list
呼叫历史。时间段是必需的,且限制为 31 天:cdr 除了主键外没有索引,因此每个过滤器都是全表扫描(目前约 268k 行)。
参数: date_from 和 date_to(YYYY-MM-DD,必填)、scope(call | leg,默认 call)、src 和 dst(部分匹配)、branch_id、trunk_id、answered(布尔值)、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 是 PABX 中唯一没有 ipbx_id 的表。与租户的关联是 Asterisk 的 systemname,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 之前写入(拨号计划在 prerouting 中设置 MixMonitor),因此它标记的是"录音已布防"而非"存在音频":单独使用的话,99.96% 的通话都会显示有录音。
没有任何通道列是原始输出的:channel、dstchannel 和 lastdata 携带端点的 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 返回 isError:IPBX_ID 过滤在查询中应用,URL 中的 sip<id> 来自环境变量,绝不来自收到的 call_id。
依赖 IPBX_RECORD_BASE_URL。没有它服务器正常启动,只有这个 tool 会失败,并显示明确的消息——就像在服务器上关闭了这个 tool。
每次调用都会在 audit_log 中生成一行,包含调用者身份:JWT 时为 Google 邮箱,静态 bearer 时为 service:static。ipbx_cdr_list 的 src/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(推荐)
多阶段 Dockerfile(node: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-mcpSQLite 备份:
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)
使用 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 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