ipbx-mcp
ipbx-mcp
MCP-Server des IPBX in TypeScript. Streamable-HTTP-Transport im zustandslosen Modus, Authentifizierung per statischem Bearer und/oder OAuth 2.1 + Google Workspace, lokale Persistenz in SQLite (OAuth-Clients, Refresh-Tokens, Audit-Log). Vom Scaffold base-mcp übernommen, stellt die PABX-Daten (MySQL) als typisierte Tools bereit.
Öffentliche URL in Produktion: https://mcp.ipbx.vivavox.com.br.
Anforderungen
Node.js >= 22 (
better-sqlite3v12 benötigt das)Für OAuth: OAuth-Client in der Google Cloud Console im Modus Intern
Related MCP server: utel-mcp
Installation
npm install
cp .env.example .env # depois preencha os valores reais
npm run buildKonfiguration
Laden Sie die .env in den Prozess (systemd EnvironmentFile=, docker env_file:, oder node --env-file=.env beim Start).
Pflichtfelder
Mindestens einer der Auth-Pfade:
Variable | Wann verwenden |
| Statischer Bearer — Claude Desktop, CLI, API, Skripte, Cron |
| OAuth — Clients über claude.ai (Web/Mobil) |
OAuth (optional, aber für claude.ai erforderlich)
Variable | Beschreibung |
| Kanonische URL des Servers (z. B. |
| HS256-Schlüssel der JWTs (32 Bytes hex) |
| Vom OAuth-Client in der Google Cloud Console |
| Vom OAuth-Client in der Google Cloud Console |
| Erlaubte Workspace-Domain (Standard: |
Wenn alle vorhanden sind, werden die Routen /authorize, /oauth/google/callback, /token und /register (DCR) gemountet. Ohne sie funktioniert nur der statische Bearer.
Weitere
Variable | Standard | Beschreibung |
|
| HTTP-Port |
|
| Schnittstelle (lokal in der Entwicklung |
| — | CSV-Liste der im |
|
| Pfad der SQLite-Datei |
| — | Basis der IPBX-API, die die Aufzeichnungen ausliefert (z. B. |
MySQL (Datenquelle des IPBX)
Variable | Standard | Beschreibung |
| — | MySQL-Host |
|
| |
| — | Dedizierten Benutzer mit ausschließlich |
| — | |
| — | |
|
| Pool-Größe ( |
| leer | Jeder Wert aktiviert TLS mit Zertifikatsprüfung |
| — | Tenant, den diese Instanz bedient (siehe unten) |
Die Datenbank ist mandantenfähig — eine Asterisk-Instanz pro Kunde, Tabelle ipbx — aber jede MCP-Instanz bedient genau einen Tenant. Alle Abfragen filtern nach IPBX_ID, und keine Tool akzeptiert diese ID als Parameter: So hängt die Isolierung zwischen Kunden nicht davon ab, was das Modell im Aufruf übergibt. Ein Container und eine Subdomain pro Tenant.
Generieren Sie zufällige Tokens mit:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Endpunkte
Methode | Pfad | Auth | Beschreibung |
POST |
| bearer | JSON-RPC des MCP über Streamable HTTP |
GET |
| bearer |
|
DELETE |
| bearer |
|
GET |
| öffentlich |
|
GET |
| öffentlich | RFC-8414-Metadaten |
GET |
| öffentlich | RFC-9728-Metadaten |
POST |
| öffentlich | Dynamic Client Registration (RFC 7591) |
GET |
| öffentlich | Leitet zu Google weiter |
GET |
| öffentlich | Empfängt den Redirect von Google |
POST |
| öffentlich |
|
401 bei /mcp enthält WWW-Authenticate: Bearer realm=..., resource_metadata=... — ohne das entdeckt claude.ai den AS beim ersten Kontakt nicht.
Verfügbare Tools
Die Tool-Namen folgen dem Schema ipbx_<model>_<action>, wobei <action> aus dem Vokabular list / get / search / count stammt.
ipbx_instance_get
Stammdaten der IPBX-Instanz, die dieser Server bedient — Name, IP und SIP/AMI-Ports.
Parameter: keine. Die Instanz ist fest, definiert durch IPBX_ID in der Umgebung.
Rückgabe:
{
"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"
}Gibt isError zurück, wenn die konfigurierte IPBX_ID in der Tabelle ipbx nicht existiert.
ipbx_branch_list
Listet die Nebenstellen der Instanz.
Parameter:
search(string, optional): Teilsuche nach Nebenstelle-Nummer oder Namelimit(number, optional): 1–500, Standard100
Rückgabe:
{
"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
}
]
}Gibt die SIP-Zugangsdaten nicht zurück. Die Spalten password (Passwort im Klartext) und username (Authentifizierungskennung, anders als die Nebenstelle-Nummer) bleiben bewusst außen vor — zusammen erlauben sie, ein Softphone zu registrieren und Anrufe auf dem Kundenkonto zu tätigen. Die Spaltenliste im SELECT ist genau deshalb explizit, damit keine davon versehentlich hineingerät.
ipbx_user_list
Listet die Panel-Benutzer der Instanz.
Parameter:
search(string, optional): Teilsuche nach Name oder E-Maillimit(number, optional): 1–500, Standard100
Rückgabe:
{
"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"
}
]
}Gibt das Zugangspasswort nicht zurück. Die Spalte secret bleibt außen vor: Es ist das Panel-Login-Passwort, im Klartext in der Datenbank gespeichert (ohne Hash). Das offenzulegen würde administrativen Zugriff auf den PABX gewähren.
ipbx_group_list
Listet die Nebenstelle-Gruppen der Instanz, jeweils mit der Anzahl der Nebenstellen.
Parameter:
search(string, optional): Teilsuche nach Name oder Beschreibunglimit(number, optional): 1–500, Standard100
Rückgabe:
{
"total": 6,
"truncated": false,
"groups": [
{
"id": 1,
"name": "Suporte",
"description": "Grupo do suporte",
"branches": 11
}
]
}Die Tabelle groups speichert keine Zugangsdaten — anders als bei branch und users werden hier alle Spalten offengelegt.
ipbx_trunk_list
Listet die Trunks der Instanz.
Parameter:
search(string, optional): Teilsuche nach Name oder Hostlimit(number, optional): 1–500, Standard100
Rückgabe:
{
"total": 2,
"truncated": false,
"trunks": [
{
"id": 1,
"name": "Vivavox",
"host": "sip.vivavox.com.br",
"port": "5060",
"register": true,
"record": true,
"auth": "credentials"
}
]
}Gibt die Zugangsdaten des Anbieters nicht zurück. username und password bleiben außen vor — sie sind die wertvollste Zugangsdaten der Datenbank, da sie es erlauben, Anrufe direkt über den Anbieter zu tätigen, abgerechnet auf dem Konto. Stattdessen kommt auth, das nur angibt, wie der Trunk authentifiziert: "credentials" (Benutzer/Passwort) oder "ip" (IP-Allowlist, ohne Passwort).
ipbx_queue_list
Listet die Warteschlangen mit der Verteilungsstrategie und der Anzahl der Mitglieder.
Parameter: search (string, optional), limit (1–500, Standard 100)
{
"total": 5,
"queues": [
{ "id": 1, "name": "Suporte", "strategy": "ringall", "members": 8 },
{ "id": 5, "name": "Teste", "strategy": "leastrecent", "members": 1 }
]
}ipbx_queue_member_list
Listet die Mitglieder der Warteschlangen in der Rufreihenfolge.
Parameter:
queue_id(number, optional): filtert eine Warteschlange; weglassen, um alle zu holenlimit(number, optional): 1–500, Standard200
Rückgabe:
{
"total": 8,
"members": [
{
"queue_id": 1,
"queue": "Suporte",
"position": 1,
"type": "branch",
"exten": "29",
"name": "Mateus Damaceno",
"ref": "branch-10"
}
]
}Die Spalte queue_member.member speichert eine Referenz im Format <Typ>-<ID> — branch-10 zeigt auf branch.id 10, also Nebenstelle 29. Es ist nicht die Nebenstelle-Nummer. Die Tool löst das zu exten + name auf, wenn das Mitglied eine Nebenstelle ist. Nicht jedes Mitglied ist eine: Es gibt Einträge redirect-N, die mit type: "redirect" und exten/name null zurückkommen.
ipbx_ivr_list
Listet die IVRs mit der zugehörigen Audiodatei und der Transkription dessen, was dem Anrufer angesagt wird.
Parameter: search (string, optional — matcht auf Name oder Transkriptionstext), limit (1–500, Standard 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
}
]
}Die Transkription ist das nützlichste Feld: Sie erlaubt es, einen IVR über das zu finden, was er ansagt, nicht nur über den Namen.
ipbx_ivr_option_list
Listet die Optionen der IVRs — welche Taste zu welchem Ziel führt.
Parameter:
ivr_id(number, optional): filtert einen IVR; weglassen, um alle zu holenlimit(number, optional): 1–500, Standard200
Rückgabe:
{
"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 ist polymorph: Es zeigt auf 5 verschiedene Tabellen (branch, queue, ivr, redirect, app) im Format <Typ>-<ID> und akzeptiert außerdem Literale ohne ID (internal). Die Tool löst den Zielnamen in allen Fällen auf; Literale kommen mit name null und erhaltenem ref zurück.
Das Feld digit ist nicht immer eine Ziffer: t ist Timeout, und Muster wie 7X matchen Nebenstelle-Bereiche.
ipbx_redirect_list
Listet die Redirects — kurze Nebenstellen, die zu einer externen Nummer über einen Trunk weiterleiten. Es sind dieselben redirect-<ID>, die als Ziel in Warteschlangen, IVRs und Routing-Regeln erscheinen.
Parameter: search (string, optional — matcht Nebenstelle, Name oder Nummer), limit (1–500, Standard 100)
{
"total": 12,
"redirects": [
{
"id": 2,
"exten": "73",
"name": "Ricardo Landim",
"forward": "5535988023317",
"trunk": "Vivavox",
"ref": "redirect-2"
}
]
}⚠️ Personenbezogene Daten. forward ist in 100 % der Zeilen eine private Mobilnummer — keine Zugangsdaten, aber personenbezogene Daten im Sinne der LGPD. Die Tool gibt sie zurück, weil das der Existenzgrund der Tabelle ist, aber sie geht nicht in das audit_log.
ipbx_routing_list
Listet die Routing-Pläne, jeweils mit der Anzahl der Regeln und Zeitfenster.
Parameter: search (string, optional), limit (1–500, Standard 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
Listet die Zeitfenster der Pläne.
Parameter: routing_id (number, optional), limit (1–500, Standard 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"]
}Das pattern wird im Asterisk-Format gespeichert, ein Bereich pro Zeile; die Tool gibt es als Liste zurück.
ipbx_routing_rule_list
Listet die Routing-Regeln — den Dialplan. Jede Regel matcht ein Nummern-Muster innerhalb eines Zeitfensters, unterdrückt Ziffern, fügt ein Präfix hinzu und sendet an das Ziel.
Parameter: routing_id (number, optional), limit (1–500, Standard 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 ist polymorph wie bei der IVR, zusätzlich mit dem Typ trunk (bei ausgehenden Regeln verwendet) — insgesamt sechs mögliche Ziele.
Zwei Schema-Details werden hier behandelt: Die Datenbankspalte heißt supress (mit einem „p"), exponiert als suppress; und goto2/goto3 existieren, sind aber in allen Zeilen leer — sie erscheinen als goto_extra nur, falls sie jemals befüllt werden.
ipbx_cdr_list
Anrufverlauf. Der Zeitraum ist Pflicht und auf 31 Tage begrenzt: Die cdr hat außer der PK keinen Index, daher ist jeder Filter ein Full Scan (~268k Zeilen heute).
Parameter: date_from und date_to (YYYY-MM-DD, Pflichtfelder), scope (call | leg, Standard call), src und dst (teilweise), branch_id, trunk_id, answered (bool), call_id, limit (1–500, Standard 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
}Die cdr ist die einzige Tabelle der PABX ohne ipbx_id. Die Verbindung zum Tenant ist der systemname von Asterisk, den die ipbx-api als sip<ipbx_id> schreibt und Asterisk in uniqueid/linkedid jeder Zeile stempelt — der Filter ist uniqueid LIKE 'sip<id>-%', mit Bindestrich (ohne ihn würde sip1 auch sip10- matchen).
Ein Anruf besteht aus vielen Zeilen: uniqueid identifiziert den Kanal, linkedid den Anruf, und jeder Dial-Versuch erzeugt eine Zeile — ein eingehender Queue-Anruf kommt auf 22. scope=call gruppiert nach linkedid; die Beine, deren Ziel ein Local/-Kanal ist, sind das Klingeln der Queue bei jedem Mitglied (werden zu ring_attempts), und die übrigen sind Gespräch (summieren talk_seconds). scope=leg liefert die rohen Beine — verwende es mit call_id, um einen Anruf zu debuggen.
has_recording erfordert ANSWERED zusätzlich zu gefülltem rec — dieselbe Regel wie recAvailable im Panel. Die Spalte rec wird vor dem Dial geschrieben (das Dialplan richtet MixMonitor im prerouting ein), also markiert sie „Aufnahme scharfgeschaltet" und nicht „Audio vorhanden": allein würde sie bei 99,96 % der Anrufe eine Aufnahme ergeben.
Keine Kanal-Spalte kommt roh heraus: channel, dstchannel und lastdata tragen den username des Endpoints, der die Hälfte der SIP-Anmeldedaten ist, und src bringt denselben Username bei internen Anrufen. Alles läuft durch src/channel.ts und kommt als Nebenstelle/Trunk/Queue heraus. rec bleibt ebenfalls außen vor — es wird zu has_recording.
Das Filtern nach branch_id/trunk_id wählt die Anrufe per Semi-Join aus, nicht per Zeile: Die Aggregate beschreiben weiterhin den gesamten Anruf, nicht nur die Beine dieser Nebenstelle.
ipbx_recording_get
URL des Audios eines Anrufs, ausgehend von der call_id, die ipbx_cdr_list zurückgibt.
Parameter: call_id (string, Pflichtfeld)
{
"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. …"
}Es ist aus einem Grund ein separates Tool statt eines Felds von ipbx_cdr_list: Die Route /call/record der ipbx-api erfordert keine Authentifizierung und die URL läuft nicht ab — der Dateiname (SHA1) ist die Anmeldedaten. Als Listungsfeld würde jeder CDR-Anruf 25 dauerhafte Zugriffe auf Gespräche im Kontext ausgeben, fast alle nie genutzt, und das Audit müsste 25 Anmeldedaten protokollieren oder nichts registrieren. Ein Tool pro Aufnahme ergibt eine Audit-Zeile mit der Identität des Anforderers. Das has_recording des CDR ist das Entdeckungssignal; dieses Tool ist der Zugriff.
Ohne Audio nennt die Antwort den Grund, statt nur abzulehnen — Chamada nao atendida (die Aufnahme wird vor dem Dial scharfgeschaltet) oder Aufnahme an der Nebenstelle deaktiviert. Eine call_id eines anderen Tenants liefert isError: Der Filter nach IPBX_ID wird in der Query angewendet, und das sip<id> der URL stammt aus der Umgebung, nie aus der empfangenen call_id.
Hängt von IPBX_RECORD_BASE_URL ab. Ohne sie startet der Server normal und nur dieses Tool schlägt fehl, mit expliziter Meldung — so, als würde man das Tool auf einem Server deaktivieren.
Jeder Anruf erzeugt eine Zeile in audit_log mit der Identität des Aufrufers: Google-E-Mail bei JWT, service:static bei statischem Bearer. src/dst von ipbx_cdr_list sind Telefonnummern und gehen nicht ins Audit — es bleibt nur number_filter: true.
Befehle
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 InspectorLokaler Smoke-Test:
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 (empfohlen)
Multi-Stage-Dockerfile (node:22-slim), Runtime als Nicht-Root-Benutzer mcp, exponiert /data als Volume für SQLite, Healthcheck über /health. In Produktion erfolgt das Deploy automatisch über .github/workflows/deploy.yml (Push des Tags vX.Y.Z → Build in GHCR → docker run auf der VPS). Manuell:
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-Backup:
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= ist das native systemd-Äquivalent zu .env. Verwende einen dedizierten Benutzer (mcp) statt root.
Konfiguration in einem MCP-Client
Claude Desktop / CLI (statischer Bearer)
{
"mcpServers": {
"ipbx": {
"type": "http",
"url": "https://mcp.ipbx.vivavox.com.br/mcp",
"headers": {
"Authorization": "Bearer SEU_MCP_AUTH_TOKEN"
}
}
}
}claude.ai (OAuth)
Als Custom Connector über https://mcp.ipbx.vivavox.com.br/mcp hinzufügen. Der OAuth-Flow startet automatisch — claude.ai entdeckt den AS über WWW-Authenticate, registriert einen Client per DCR, leitet zu Google weiter, empfängt den Code und tauscht ihn gegen ein Access Token.
Struktur
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