Skip to main content
Glama
ipbx-mcp

ipbx-mcp

by paralelum

ipbx-mcp

IPBX の MCP サーバー(TypeScript 製)。トランスポートは Streamable HTTP(ステートレスモード)、認証は 静的 bearer および/または OAuth 2.1 + Google Workspace、ローカル永続化は SQLite(OAuth クライアント、リフレッシュトークン、監査ログ)。base-mcp スキャフォールドから継承し、PABX(MySQL)のデータを型付きツールとして公開します。

本番公開 URL: https://mcp.ipbx.vivavox.com.br。

要件

  • Node.js >= 22(better-sqlite3 v12 が必要)

  • OAuth の場合: Google Cloud Console の OAuth クライアント(内部モード)

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)。

必須

認証パスの少なくとも 1 つ:

変数

使用時

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 バイト hex)

GOOGLE_CLIENT_ID

Google Cloud Console の OAuth クライアントから

GOOGLE_CLIENT_SECRET

Google Cloud Console の OAuth クライアントから

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 インスタンスは 1 つのテナントのみを担当します。すべてのクエリは IPBX_ID でフィルタリングされ、どのツールもこの ID をパラメータとして受け取りません。これにより、顧客間の分離はモデルが呼び出し時に渡すものに依存しません。テナントごとに 1 つのコンテナと 1 つのサブドメインを使用します。

ランダムなトークンを生成するには:

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

公開

Dynamic Client Registration (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_ID が ipbx テーブルに存在しない場合、isError を返します。

ipbx_branch_list

インスタンスの内線番号を一覧表示します。

パラメータ:

  • 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、任意): 名前またはメールの部分一致検索

  • 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 列は除外されています。これはパネルのログインパスワードで、データベースに平文で保存されています(ハッシュなし)。これを公開すると、PABX への管理アクセスを提供することになります。

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 列は <type>-<id> 形式の参照を保持します — branch-10 は branch.id 10 を指し、これは内線 29 です。内線番号ではありません。 ツールはメンバーが内線の場合、これを exten + name に解決します。すべてのメンバーが内線とは限りません: redirect-N エントリもあり、これらは type: "redirect" として返され、exten/name は null です。

ipbx_ivr_list

IVR を、関連する音声と発信者に話される内容の文字起こしとともに一覧表示します。

パラメータ: 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 が話す内容で 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 は多態的です: <type>-<id> 形式で 5 つの異なるテーブル(branch、queue、ivr、redirect、app)を指し、さらに ID なしのリテラル(internal)も受け入れます。ツールはすべての場合で宛先名を解決します。リテラルは name が null で返され、ref は保持されます。

digit フィールドは常に数字とは限りません: t はタイムアウトで、7X のようなパターンは内線番号の範囲に一致します。

ipbx_redirect_list

リダイレクト — トランク経由で外部番号に転送する短い内線番号を一覧表示します。これらはキュー、IVR、ルーティングルールの宛先として表示されるのと同じ redirect-<id> です。

パラメータ: 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 に基づく個人データです。ツールはテーブルの存在理由であるためこれを返しますが、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 形式で保存され、1 行に 1 つの範囲が含まれます。ツールはリストとして返します。

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 タイプ(発信ルールで使用)があります — 合計 6 つの可能な宛先です。

ここで処理されるスキーマの 2 つの詳細: データベースの列は supress(p が 1 つ)と呼ばれ、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 はPABXで唯一 ipbx_id を持たないテーブルです。テナントとの紐付けはAsteriskの systemname で、ipbx-apiはこれを sip<ipbx_id> として書き込み、Asteriskは各行の uniqueid/linkedid にスタンプします。フィルタは uniqueid LIKE 'sip<id>-%' で、ハイフン付きです(ハイフンがないと、sip1 が sip10- にもマッチしてしまいます)。

1回の通話は多数の行になります。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

ipbx_cdr_list が返す call_id から、1件の通話の音声URLを取得します。

パラメータ: 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 のフィールドではなく独立したツールである理由は次のとおりです。ipbx-apiの /call/record ルートは認証を要求せず、URLは期限切れになりません。ファイル名(SHA1)がそのまま資格情報だからです。リストのフィールドとして実装すると、CDRの各通話がコンテキストに25件の永続的なアクセスを放出し、そのほとんどが使われることはなく、監査は25件の資格情報を記録するか、何も記録しないかのどちらかになります。録音ごとに1つのツールを使うことで、誰が要求したかの監査証跡が1行で得られます。CDRの has_recording が発見のシグナルであり、このツールがアクセスです。

音声がない場合、応答は単に拒否するのではなく理由を示します。Chamada nao atendida(録音は Dial の前に準備されるため)または内線で録音がオフになっている場合です。別のテナントの call_id は isError を返します。IPBX_ID によるフィルタがクエリに適用され、URLの sip<id> は環境から取得され、受信した call_id から取得されることはありません。

IPBX_RECORD_BASE_URL に依存します。これがない場合、サーバーは通常どおり起動し、このツールだけが明示的なメッセージとともに失敗します。サーバー上でツールを無効化するのと同じです。

すべての通話は、呼び出し元のIDとともに audit_log に行を生成します。JWTの場合はGoogleメール、静的ベアラーの場合は 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 として実行、SQLite用に /data をボリュームとして公開、/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 のネイティブな相当物です。root ではなく専用ユーザー(mcp)を使用してください。

MCPクライアントでの設定

Claude Desktop / CLI(静的ベアラー)

{
  "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 を使用してカスタムコネクタとして追加します。OAuthフローは自動的に開始されます。claude.ai は WWW-Authenticate を介してASを発見し、DCRを介してクライアントを登録し、Googleにリダイレクトし、コードを受け取り、アクセストークンと交換します。

構造

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

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    C
    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.
    -