使用说明
Vonage MCP Server
English | 日本語
VonageのSMS送信と音声通話を AIエージェントから安全に使うための MCP (Model Context Protocol) サーバーです。
利用者が自分の環境にコンテナを立て、自分の Vonage 資格情報を設定して使う OSS のリファレンス実装です。このプロジェクトがあなたの資格情報を預かることはありません。
このプロジェクトの前提
脅威モデル — エージェントを信頼できる主体として扱わない
攻撃者はプロンプトインジェクションによって AI エージェントを操れるものとします。
SMS 送信と音声通話は取り消せず、課金が発生し、相手にも迷惑がかかりうる操作です。「エージェントが正しく使えば安全」という前提は置けません。このサーバーのガードレールは、エージェントが敵対的に振る舞っても被害が上限内に収まることを目指しています。
そのため、既定ではツールが1つも公開されません。 使う機能を環境変数で明示的に有効にしてください。
対象範囲
| 項目 | 範囲 |
|---|---|
| 規制上の案内 | 日本国内での利用を対象とします |
| 発信先 | 海外番号も許容しますが、既定では日本(国番号 81)のみ有効です |
| 提供形態 | 利用者が自環境にデプロイします。1デプロイ = 1 Vonage アプリケーション |
| 対応クライアント | MCP 仕様準拠。stdio と Streamable HTTP の2形態 |
[!IMPORTANT] 規制に関する記載は法的助言ではありません。 このプロジェクトが案内するのは日本国内での利用を対象とした事項に限られます。他地域で利用する場合は、その地域の規制・キャリア仕様・Vonage の利用規約をご自身で確認する責任を負います。 日本国内での利用についても、日本向け SMS の利用条件に挙げた事項はサーバー側で判定できないため、遵守は利用者の責任です。
[!WARNING] 単一インスタンスで動かしてください。 レートリミット・配信ステータス・Webhook のリプレイ検出はすべてプロセス内のメモリに保持されます。複数インスタンスで動かすと、
- レートリミットが実効的にインスタンス数倍に緩みます(
RATE_LIMIT_PER_HOUR=5を3インスタンスで動かせば毎時15件)- Webhook がインスタンス A に届いて
get_sms_statusが B で処理されると、配信ステータスを取得できません- Webhook のリプレイ検出がインスタンスをまたげません
Cloud Run なら
--max-instances=1を指定してください。外部ストア対応は現時点のスコープ外です。
対応プラットフォーム
本サーバーは MCP 仕様に準拠した stdio と Streamable HTTP の2形態を実装しており、プラットフォーム固有の分岐は持ちません。以下は各基盤の公開ドキュメントに基づく対応状況です。
凡例: ✅ 実機で確認済み / 📄 ドキュメント上は対応(未検証)/ ⚠️ 制約あり
| プラットフォーム | 接続方法 | 送れる認証 | ツール実行前の承認 | 状態 |
|---|---|---|---|---|
| Claude Desktop(ローカル) | stdio / MCPB | 不要 | あり(読み取り系にも出ます) | ✅ |
| Claude Code | stdio / HTTP | --header で Bearer |
あり | 📄 |
| Streamable HTTP 全般(Cloud Run 等) | Streamable HTTP | Bearer / 上流 IAM | クライアント次第 | ✅ |
| Claude.ai / Desktop(リモート) | Streamable HTTP | OAuth、または静的ヘッダ(beta・組織管理者が設定) | あり | 📄 |
| Gemini Enterprise(コネクタ) | Streamable HTTP | OAuth 2.0 か「認証なし」のみ(別途 IdP が必要) | あり(既定で必ず出る) | 📄 |
| Gemini Enterprise(ADK で自作) | Streamable HTTP | 任意ヘッダで Bearer | あり(ADK の require_confirmation。Apps に承認ウィンドウが出ます) |
✅ |
| AWS Bedrock AgentCore Gateway | Streamable HTTP(SSE は使いません) | API キープロバイダで任意ヘッダに Bearer。IAM SigV4 は使えません(下記) | 無し | ✅ |
| Dify | Streamable HTTP(SSE は使いません) | 任意ヘッダで Bearer | 無し。 Workflow に Human Input ノードを置けば可 | ✅ |
| ChatGPT(カスタムプラグイン) | Streamable HTTP | OAuth 2.1 のみ。MCP_AUTH_TOKEN は渡せません(別途 IdP が必要) |
保証されません | ✅ |
| n8n(MCP Client Tool) | HTTP Streamable / stdio | Bearer / 任意ヘッダ / OAuth2 | AI Agent ノードで有効化すれば可 | 📄 |
[!IMPORTANT] ChatGPT と Gemini Enterprise のコネクタは、
MCP_AUTH_TOKENでは繋がりません。 認証の選択肢が「OAuth か認証なし」しかなく、任意のヘッダーを設定する欄がないためです。これらに繋ぐには、別途 IdP(認可サーバー)を用意して OAuth モードを使ってください → OAuth を使うには IdP が別途必要です
[!NOTE] 「📄」はまだ実機で確認していないという意味です。各基盤のドキュメント上は接続できるはずですが、動作報告をいただけると助かります。
Claude Desktop は 2026-08-24 に v1.34493 で確認しました。 MCPB のインストール、capability トグル、
ALLOWED_NUMBERSによるブロック、SMS の実配信と音声通話の実発信、通話ステータスの取得、実行前の承認プロンプトまで動作しています。 ただしreadOnlyHintは尊重されず、get_sms_statusやget_call_statusのような読み取り専用ツールでも承認プロンプトが出ます(安全側の挙動なので実害はありません)。Streamable HTTP(Cloud Run)も同日に確認しました。 Bearer 認証(未認証は 401)、
ALLOWED_NUMBERSによるブロック、SMS と音声通話の実行に加えて、get_sms_statusがdeliveredを返すこと(Status Webhook 経由)と、get_call_statusが通話時間・料金を返すことを確認しています。 配信ステータスは stdio では取得できません。 受け取るには HTTP で待ち受け、Vonage に Status URL を登録する必要があります。Dify Cloud(Sandbox プラン)は 2026-08-31 に確認しました。 カスタムヘッダでの Bearer 認証、
tools/listの取り込み、dry_run、SMS の実送信と音声の実発信、get_sms_statusのdelivered、get_call_statusの料金・通話時間まで動作しています。detail: ok/sip_code: 200も返っており、Event Webhook 経由の失敗理由の伝達もこの経路で動いています。 手順は docs/dify.md。 ただし Dify には実行前の承認 UI がありません。 Agent アプリはエージェントの判断だけでツールを実行し、destructiveHintは無視されます。Agent アプリで使うならALLOWED_NUMBERSが唯一の防御です(Workflow なら Human Input ノードを置けます)。AWS Bedrock AgentCore Gateway は 2026-08-31 に
ap-northeast-1で確認しました。 API キープロバイダによる Bearer 認証、ターゲット作成時の自動同期、**Strands + Bedrock のエージェントが自分でツールを選んでの SMS 実送信(delivered)と音声実発信(completed/sip_code: 200/detail: ok)**まで動作しています。手順は docs/agentcore.md。 IAM SigV4 の outbound はこのサーバーでは使えません。 Gateway は署名するだけで、ターゲット側が SigV4 を検証できる必要があり、対応するのは API Gateway / Lambda Function URLs / AgentCore Runtime です。Cloud Run は含まれません。 Gateway は API であって UI ではないため、承認は一切ありません。 ここでもALLOWED_NUMBERSが唯一の防御です。また ツール名に<ターゲット名>___が前置されるので、ツール名を名指しする指示文はそのままでは使えません。
ツール実行前の承認について
send_sms / make_voice_call には、MCP のツール注釈で destructiveHint: true を付けています。これを解釈する基盤(Gemini Enterprise など)では、実行前に確認が表示されます。get_sms_status / get_call_status は readOnlyHint: true なので確認は省かれます。
[!WARNING] 注釈は仕様上ヒントであり、強制ではありません。 無視する基盤もあり、「常に許可」を選べる基盤もあります。実測でも、Claude Desktop は
readOnlyHintを尊重せず、読み取り専用ツールにも承認プロンプトを出しました。 上の表で承認が「無い」基盤を使う場合は、ALLOWED_NUMBERSとRATE_LIMIT_PER_HOURを必ず設定してください。承認UIもプロンプトも、実効的な防御にはなりません。
インストール方法
方法1: MCPB Bundle(推奨 - ワンクリックインストール)
Claude Desktopで簡単にインストールできます:
-
MCPBファイルのダウンロード
- vonage-mcp-server.mcpb をダウンロード
-
Claude Desktopで開く
.mcpbファイルをダブルクリック、またはClaude Desktopにドラッグ&ドロップ
-
環境変数の設定
- Claude Desktopのインストールダイアログで以下を入力:
VONAGE_APPLICATION_ID: Vonage Application IDVONAGE_PRIVATE_KEY_PATH: 秘密鍵ファイルのパス(例:/Users/your-name/vonage/private.key)VONAGE_VOICE_FROM: 音声通話用の電話番号(E.164形式、例:81345438093)
- Claude Desktopのインストールダイアログで以下を入力:
-
インストール完了
- Claude Desktopを再起動すると、Vonage MCPサーバーが利用可能になります
方法2: 手動セットアップ
セットアップ
依存関係のインストール
npm install
Vonage設定
-
Vonageアカウントの作成
- 日本国内でご利用の場合は 日本語での申し込みページ から作成できます(詳細)
- 海外の場合は Vonage Developer Portal から作成します
- アプリケーションを作成し、Application IDを取得
-
秘密鍵の準備
- Vonage Developer Portalで秘密鍵(private.key)をダウンロード
- プロジェクトルートに
private.keyとして保存
-
環境変数の設定
cp .env.example .env.envファイルを編集して以下を設定:VONAGE_APPLICATION_ID=your_application_id_here VONAGE_PRIVATE_KEY_PATH=./private.key VONAGE_VOICE_FROM=14155550100 # Voice通話用のFROM番号
機能の有効化(capability トグル)
このサーバーは、既定ではツールを1つも公開しません。 使う機能だけを環境変数で明示的に有効にしてください。電話とSMSは実際に課金が発生し、相手にも迷惑がかかりうる操作なので、「気づかないうちに使える状態になっていた」を避けるための設計です。
| 環境変数 | 有効になるツール | 既定 |
|---|---|---|
ENABLE_SMS |
send_sms / get_sms_status |
OFF |
ENABLE_VOICE |
make_voice_call / get_call_status |
OFF |
# SMSの単発送信だけを使う場合
ENABLE_SMS=true
無効なツールは tools/list の結果に含まれません。エージェントが存在しないツールを呼ぼうとして迷走せず、使わないツールの定義がコンテキストを消費することもありません。
[!WARNING] v1.3.0 の破壊的変更:
generate_jwtツールを削除しました。 Vonage API を直接叩ける署名済みクレデンシャルを呼び出し側に渡すツールで、受け取った相手にはこのサーバーのガードレール(宛先制限・レートリミット・capability トグル)が一切効きません。既定 OFF にしても、一度有効化した後にプロンプトインジェクションで長寿命のトークンを生成させられる余地が残ります。 AIエージェントから Vonage を使いやすくするという本サーバーの目的に対して汎用の JWT 発行は主要な用途ではないため、迂回路を残さない判断をしました。JWT が必要な場合は Vonage 公式のサーバーSDK を直接お使いください。
[!IMPORTANT] 有効な値は
true/falseのみで、大文字小文字を区別します。ENABLE_SMS=TrueやENABLE_SMS=1は起動エラーになります。無効にしたつもりのfalseが truthy と判定されて機能が公開される事故を防ぐため、曖昧な値は推測せずに落とす方針です。
[!NOTE] capability を有効にした場合、
VONAGE_APPLICATION_IDは必須になります。ENABLE_VOICE=trueの場合はさらにVONAGE_VOICE_FROMが必要で、いずれも未設定なら起動時にエラーになります(実行して初めて失敗するより、起動時に気づけるほうが安全なため)。
安全機能(Guardrails)の環境変数
AIエージェント(Gemini Enterprise / Claude 等)から利用する際の、意図しない課金・スパム送信を防ぐための設定です。すべて任意で、未設定でも動作します。
| 環境変数 | デフォルト | 説明 |
|---|---|---|
ALLOWED_COUNTRY_CODES |
81(日本のみ) |
送信・架電を許可する国番号(カンマ区切り、+ の有無は問わない)。海外宛を使う場合は明示的に追加する。* を指定すると制限を外す(非推奨)。実在しない国番号を書くと起動エラーになる。 |
ALLOW_PREMIUM_NUMBERS |
false |
true にすると、0990(情報料代理徴収)・0570(ナビダイヤル)・0180(テレドーム)への送信・架電を許可する。 |
ALLOWED_NUMBERS |
(未設定=制限なし) | 送信・架電を許可する宛先番号のホワイトリスト(カンマ区切り)。設定すると、これ以外の番号へのリクエストはエラーになる。表記ゆれ(090-1234-5678 等)は正規化して比較される。 |
RATE_LIMIT_PER_HOUR |
5 |
1時間あたりの送信・架電件数の上限(0〜10000 の整数)。send_sms / make_voice_call の合計に適用される。0 は「すべて拒否」(緊急停止)。dry_run: true の呼び出しは消費しない。 |
SMS_RATE_LIMIT_PER_HOUR |
(未設定=RATE_LIMIT_PER_HOUR に委ねる) |
SMS だけをさらに絞りたい場合の上限。 |
SMS_SEGMENT_LIMIT_PER_HOUR |
(未設定=制限なし) | 1時間あたりのセグメント数の上限。課金と直結する唯一の設定。 |
SMS_MAX_SEGMENTS |
3 |
1通のSMSに許すセグメント数(1〜10)。 |
VOICE_RATE_LIMIT_PER_HOUR |
(未設定=RATE_LIMIT_PER_HOUR に委ねる) |
架電だけをさらに絞りたい場合の上限。 |
DISABLE_RATE_LIMIT |
false |
true にするとレートリミットを完全に無効化する。危険な設定であり、起動のたびに警告が出る。本番環境では使わないこと。 |
VONAGE_API_SIGNATURE_SECRET |
(未設定) | Status Webhook の署名検証に使う Vonage の Signature Secret。推奨。Vonage Dashboard の Settings → API settings で取得できる。 |
VOICE_INBOUND_MESSAGE |
(案内文) | 音声の着信時に読み上げる文面。このサーバーは着信を処理しないため、既定では「お受けしておりません」という案内を読み上げて切る。1000文字以内(発信の読み上げと同じ上限)。着信は誰でも掛けられるため、長い案内文はそのまま通話時間と音声合成の課金になる。超過すると起動時にエラーで止まる。 |
VONAGE_WEBHOOK_SECRET |
(未設定) | 署名検証が使えない環境向けの代替。設定すると x-webhook-secret ヘッダーの一致を要求する。VONAGE_API_SIGNATURE_SECRET が設定されている場合は使われない。Vonage は任意ヘッダーを送れないため、Vonage から直接呼ばれる Webhook には使えない(手前のゲートウェイがヘッダーを付与する構成でのみ有効)。 |
WEBHOOK_MAX_AGE_SECONDS |
300 |
署名付き Webhook の iat / exp に許す時刻のずれ(秒、1〜3600)。短いほどリプレイ可能な時間窓が縮む。 |
# 検証中は自分の番号だけに送信を許可する例
ALLOWED_NUMBERS=+819012345678,+819087654321
RATE_LIMIT_PER_HOUR=3
VONAGE_API_SIGNATURE_SECRET=your_signature_secret_here
日本向け SMS の利用条件
[!NOTE] ここに書いているのは法的助言ではありません。Vonage の利用規約およびキャリアの仕様として公開されている事項を、利用者が確認すべき論点として整理したものです。最新の条件は Vonage のサポート記事と契約書をご確認ください。 このプロジェクトの規制案内は日本国内での利用を対象としています。他地域の利用者はご自身で確認する責任を負います。
守るべき利用条件
| 項目 | 内容 |
|---|---|
| マーケティング目的の送信 | 受信者からのオプトイン同意が必須 |
| P2P(個人間)トラフィック | 禁止 |
| 禁止コンテンツ | 政治 / 宗教 / 未承諾プロモーション / ギャンブル |
これらはサーバー側では判定できません。 本文の内容や同意取得の有無はサーバーからは分からないため、利用者の責任で遵守してください。
配信の不確実性 — もっとも注意が必要な点
[!WARNING] 日本のネットワークでは、URL を含むメッセージがフィッシング SMS 対策として配信されないことがあります。拒否基準は非公開です。 問題は、API が成功を返すのに実際には届かないことです。AIエージェントは失敗を検知できないため、放っておくと「送信できました」とユーザーに報告してしまいます。
このサーバーはこう扱います。
- ブロックはしません。 正当な用途がありますし、拒否基準が非公開である以上こちらで判定しきれません
- 日本宛で URL を含む本文の場合、
dry_runと成功レスポンスにdelivery_warningを添えます send_smsの description に「成功レスポンスは配信保証ではない」と明記しています
重要な連絡に SMS を使う場合は、get_sms_status で配信結果を確認し、別の手段も併用してください。
文字数
Unicode 連結メッセージの上限は Docomo / KDDI / Softbank が 670文字、Rakuten が 660文字です。このサーバーは安全側の 660文字を絶対上限とし、実際の制限はセグメント数で掛けています。
content_id / entity_id について
Vonage Messages API には「特定国の規制要件を満たすためのパラメータ」として content_id / entity_id がありますが、これらはインドの DLT(Distributed Ledger Technology)登録制度のためのものです。entity_id が DLT に登録した Principal Entity ID、content_id が承認済みテンプレート ID にあたります。
日本宛の送信では不要なため、このサーバーでは使用していません。
送信者ID(sender ID)のルール
SMS の from には Vonage 公式ルールが適用されます。**英数字1〜11文字(A-Z a-z 0-9)**で、先頭文字と最小長の制限はありません。2FA や AB も有効です。
日本宛には、これに加えて日本の携帯キャリア固有の制限がかかります。
| 種別 | 日本宛 | 挙動 |
|---|---|---|
英数字(例: VonageMCP) |
✅ 使用可 | そのまま表示される |
電話番号(例: +819012345678) |
❌ 使用不可 | 拒否します |
汎用語(INFO / SMS / NOTICE など) |
❌ 使用不可 | 拒否します |
[!IMPORTANT] 日本宛で電話番号を送信元に指定しても、Vonage 側で別の送信者IDに上書きされます。 このサーバーは、これを「送れる」と応答せずに拒否します。通してしまうと、
dry_runが「この番号から送ります」と答えてユーザーが承認したのに、実際にはまったく別の送信者IDで届くからです。承認した内容と届く内容が食い違うのが、いちばん避けたい失敗の形です。
日本以外が宛先の場合、これら2つの制限は適用されません(自社の発信元電話番号を送信元に使えます)。
宛先のガードレール
宛先には、緩められるものと緩められないものの2種類の制限がかかります。
| 対象 | 挙動 | 緩められるか |
|---|---|---|
緊急通報番号(110 / 119 / 118) |
常にブロック | 不可 |
高額課金番号(0990 / 0570 / 0180) |
既定でブロック | ALLOW_PREMIUM_NUMBERS=true |
| 国番号 | 既定で日本(81)のみ |
ALLOWED_COUNTRY_CODES |
| 個別の番号 | 未設定なら制限なし | ALLOWED_NUMBERS |
これらは AND で効きます。ALLOWED_NUMBERS に載せた番号でも、国番号が許可されていなければブロックされます。判定はすべて dry_run: true の時点で行われるので、「dry_run は通ったのに本番で弾かれた」は起きません。
[!WARNING]
ALLOWED_COUNTRY_CODESは IRSF(国際収益分配詐欺)に対する主防御にはなりません。 国番号と国は一対一ではありません。+1は米国・カナダに加えてカリブ海の多数の国が共有しているため、「米国宛だけ許可」というポリシーはこの仕組みでは表現できません。米国宛のつもりで1を追加すると、同じ+1配下の高リスク地域も同時に開きます。 実効的な防御は以下の併用です:
ALLOWED_NUMBERSによる宛先の個別指定(もっとも確実)- Vonage アカウント側の地域制限・利用額上限・アラート(サーバーを迂回されても効く唯一の層)
RATE_LIMIT_PER_HOURによる被害額の上限
[!NOTE] 短縮番号(
110や海外の911/112など)は、E.164 の桁数要件を満たさないため一律で拒否されます。日本の緊急通報番号については、桁数検証とは独立した明示的なブロックも入れています(桁数の扱いが将来変わっても効き続けるようにするため)。
レートリミットの数え方
「ツール呼び出し回数」ではなく「送信件数」で消費されます。 どのツールを使ったかではなく、何件送ったかで枠が減ります。
ただしSMS の課金はセグメント単位です(SMS の課金単位)。件数の枠は「操作の回数」を抑えるもので、費用そのものを抑えたい場合は segments の枠を使ってください。
枠はツールごとではなく、次の2層で管理されます。
| バケット | 消費単位 | 対象 | 環境変数 |
|---|---|---|---|
global |
件数 | SMS・架電のすべて | RATE_LIMIT_PER_HOUR |
sms |
件数 | send_sms |
SMS_RATE_LIMIT_PER_HOUR |
voice |
件数 | make_voice_call |
VOICE_RATE_LIMIT_PER_HOUR |
segments |
セグメント数 | SMS のみ | SMS_SEGMENT_LIMIT_PER_HOUR |
1回の送信は該当するバケットを同時に消費します。どれか1つでも足りなければどれも消費せずエラーになるので、「送っていないのに枠だけ減る」ことはありません。エラーレスポンスの exceeded_bucket に、どのバケットで不足したかが入ります。
SMS の課金単位 — セグメント
SMS の課金は通数ではなくセグメント単位です。 1セグメントに入る文字数はエンコーディングで変わります。
| エンコーディング | 1通の場合 | 連結時(1セグメントあたり) |
|---|---|---|
| GSM-7(英数字のみ) | 160文字 | 153文字 |
| UCS-2(日本語などを含む) | 70文字 | 67文字 |
非ASCII文字が1文字でも混ざると、本文全体が UCS-2 になります。 英数字159文字+日本語1文字で、1セグメントから3セグメントに跳ねます。
[!IMPORTANT]
RATE_LIMIT_PER_HOUR=5は「1時間に5通まで」であって「5通分の課金まで」ではありません。 日本語で160文字のSMSは3セグメント=3通分の課金になるため、5通送ると実際の課金は約15通分です。 費用そのものを抑えたい場合はSMS_SEGMENT_LIMIT_PER_HOURを設定してください。これがセグメント数を直接数える唯一の設定です。 設定しない場合の最悪ケースはRATE_LIMIT_PER_HOUR × SMS_MAX_SEGMENTSセグメント(既定なら 5 × 3 = 15)です。
本文の上限はセグメント数で指定します(SMS_MAX_SEGMENTS、既定 3)。文字数で縛っても課金と対応しないためです。既定の3セグメントは、日本語なら約200文字、英数字なら約450文字に相当します。
dry_run のレスポンスに encoding と segments が含まれるので、送信前にユーザーへ提示してください。
[!IMPORTANT]
RATE_LIMIT_PER_HOUR=5は「1時間に合計5件まで」を意味します。 ツールごとに別枠ではありません。送信手段を変えて上限を超えることはできません。
[!WARNING] v1.3.0 の破壊的変更: SMS 本文の上限が「160文字」から「3セグメント」に変わりました。 英数字だけなら約450文字まで送れるようになり(従来より緩和)、日本語では約200文字までになります(従来の160文字より緩和)。ただし絵文字や記号を多用すると従来より厳しくなる場合があります。 文字数で縛っても課金と対応しないためです。同じ160文字でも、英数字なら1通分、日本語なら3通分の課金でした。
[!WARNING] v1.3.0 の破壊的変更:
RATE_LIMIT_PER_HOUR=0の意味が反転しました。 v1.2.1 以前は0が「無制限」でしたが、v1.3.0 以降は「すべて拒否」になります。緊急停止のつもりで0を設定した管理者が、逆に無制限にしてしまう事故を防ぐためです。 無制限にしたい場合はDISABLE_RATE_LIMIT=trueを明示的に設定してください。
[!IMPORTANT] 環境変数は起動時に厳格に検証されます。解釈できない値があると、サーバーは警告を出して動き続けるのではなく、エラーメッセージを表示して起動に失敗します(fail-fast)。
- 真偽値(
ENABLE_*/DISABLE_RATE_LIMIT)に指定できるのはtrue/falseのみです。大文字小文字を区別し、1/yes/on/Trueはすべてエラーになります。Falseのような値を truthy と誤判定して、無効にしたつもりの設定が有効になる事故を防ぐためです- 数値(
RATE_LIMIT_PER_HOUR/SMS_MAX_SEGMENTS)は10進整数のみです。小数・指数表記・負数・範囲外はエラーになります- 問題はまとめて報告されます。1つ直すたびに再起動する必要はありません
[!IMPORTANT]
ALLOWED_NUMBERSを設定しているのに有効な電話番号が1件も解釈できない場合(例:ALLOWED_NUMBERS=,)、「制限なし」ではなくすべて拒否として扱います。設定ミスを安全側に倒すためです。制限が不要な場合は環境変数自体を削除してください。
[!IMPORTANT]
VONAGE_API_SIGNATURE_SECRETとVONAGE_WEBHOOK_SECRETのどちらも未設定の場合、Status Webhook エンドポイントは 503 を返して無効化されます。未認証で受け付けると、誰でも任意のmessage_idの配信ステータスを偽装できてしまうためです。
[!IMPORTANT] 署名付き Webhook では、署名の一致だけでなく
payload_hash/iat/jtiをすべて検証し、いずれかが欠けていれば 401 で拒否します。 署名が正しいことは「Vonage が一度発行した」ことしか意味しません。claim が無ければ検証をスキップする実装だと、攻撃者は claim を外した JWT を作るだけで検証を無効化できます。有効な JWT が一度でもログやプロキシから漏れた場合に、無期限に、任意のボディと組み合わせて再利用されるのを防ぐための措置です。
payload_hash: このボディに対して発行された署名かiat/exp:WEBHOOK_MAX_AGE_SECONDS以内に発行されたものか(未来方向のずれも拒否)jti: 同じ JWT の使い回しでないか(受理済みのjtiは許容時間内は記憶される)
[!WARNING]
VONAGE_API_SIGNATURE_SECRETを設定した場合、共有シークレット認証にはフォールバックしません。 両方を設定していても、署名検証に失敗したリクエストはx-webhook-secretが正しくても 401 になります。フォールバックすると、攻撃者はAuthorizationヘッダーを外すか壊すだけで弱いほうの方式を選べてしまうためです(ダウングレード攻撃)。
[!NOTE] レートリミットはオンメモリ管理のため、プロセスを再起動するとカウントはリセットされます。
開発用依存関係のインストール
npm install --save-dev @types/node typescript ts-node
開発
開発サーバーの起動
npm run dev:start
TypeScriptのコンパイル
npm run build
コンパイルされたコードの実行
# 環境変数ファイル(.env)を使用して実行(推奨・Node.js v22以降)
npm start
# 環境変数ファイルを使用せずに実行(従来方式)
npm run start:legacy
ファイル監視モード(コンパイル)
npm run dev
ビルドファイルのクリーンアップ
npm run clean
テストの実行
npm test
テストの監視モード
npm run test:watch
カバレッジ付きテスト
npm run test:coverage
Claude Desktopでの利用
このMCPサーバーをClaude Desktopで利用するための設定方法を説明します。
方法1: MCPB Bundle(推奨 - ワンクリックインストール)
.mcpbファイルを使用すると、Claude Desktopに簡単にインストールできます。
インストール手順
-
MCPBファイルの作成
mcpbCLI が必要です(未インストールなら一度だけ)。npm install -g @anthropic-ai/mcpbnpm run build:mcpbこれにより
vonage-mcp-server.mcpbファイルが作成されます。 -
Claude Desktopで開く
- 作成された
.mcpbファイルをダブルクリック - または Claude Desktop にドラッグ&ドロップ
- 作成された
-
環境変数の設定 Claude Desktop のインストールダイアログで以下を入力:
- Vonage Application ID: Vonage Application ID
- Private Key Path: 秘密鍵ファイルの絶対パス(例:
/Users/your-name/vonage/private.key) - Voice Call From Number: 音声通話用の電話番号(E.164形式、例:
81345438093)
-
インストール完了 Claude Desktop を再起動すると、Vonage MCP サーバーが利用可能になります。
MCPBファイルの配布
作成した .mcpb ファイルは他のユーザーと共有できます:
- GitHub Releases で配布
- 直接ファイルを共有
方法2: 手動セットアップ
1. サーバーのビルドと起動
# プロジェクトをビルド
npm run build
# サーバーを起動(Node.js v22以降、推奨)
npm start
# または従来方式で起動(環境変数ファイルを使用しない場合)
npm run start:legacy
2. Claude Desktopの設定
Claude Desktopの設定ファイル claude_desktop_config.json に以下の設定を追加します:
{
"mcpServers": {
"vonage-mcp-server": {
"command": "node",
"args": ["--env-file=.env", "dist/index.js"],
"cwd": "/Users/your-username/path/to/vonage-mcp-server"
}
}
}
または環境変数を直接指定する方法もあります:
{
"mcpServers": {
"vonage-mcp-server": {
"command": "node",
"args": ["/Users/your-username/path/to/vonage-mcp-server/dist/index.js"],
"env": {
"VONAGE_APPLICATION_ID": "your-application-id",
"VONAGE_PRIVATE_KEY_PATH": "/Users/your-username/path/to/vonage-mcp-server/private.key"
}
}
}
}
設定ファイルの場所
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
設定手順
- 上記のパスにある
claude_desktop_config.jsonを開く mcpServersセクションに上記の設定を追加- ファイルを保存
- Claude Desktopを再起動
3. 利用可能な機能
設定完了後、Claude Desktopで以下の機能が利用できます:
ツール
すべてのツールは軽量なJSONを返します(Vonage APIの生レスポンスは返しません)。詳細は ツールのレスポンス形式 を参照してください。
-
send_sms: 単発SMS送信ツール
- 入力:
to(必須): 送信先の電話番号(E.164形式+819012345678または日本の国内形式09012345678)message(必須): 送信するメッセージ(最大3セグメント — 日本語なら約200文字、英数字なら約450文字。SMS の課金単位を参照)from(オプション): 送信元。英数字1〜11文字(A-Z a-z 0-9)。省略時は'VonageMCP'。dry_runの時点で検証される。送信者ID(sender ID)のルールを参照dry_run(オプション):trueで送信せず検証のみ(デフォルト:false)
- 機能:
- 日本の電話番号(0から始まる)は自動的にE.164形式に変換
{"status":"success","message_id":"...","to":"+81..."}を返却
- 入力:
-
make_voice_call: 音声通話ツール
- 入力:
to(必須): 発信先電話番号(E.164形式または0ABJ形式)message(必須): 読み上げるメッセージ(最大1000文字)voice(オプション):女性または男性(デフォルト: 女性)dry_run(オプション):trueで発信せず検証のみ
- 機能:
- 指定番号に発信してメッセージを音声で読み上げ
- 日本語音声対応(女性・男性)
- NCCO(Nexmo Call Control Object)を使用
- 通話時間の自動見積もり。見積もりから通話の強制切断時間(
length_timer)を決めるため、dry_runで提示した時間を大きく超えて課金されることはない dry_runはestimated_duration_seconds(見積もり)とmax_duration_seconds(実際に適用される上限)の両方を返す
- 入力:
-
get_call_status: 通話ステータス取得ツール
- 入力:
call_id(必須): 取得する通話のCall ID(UUID形式)
- 機能:
- Vonage Voice APIから通話のステータス情報を取得
call_status(通話ステータス)、start_time、price、rate、duration_secondsを返却- 環境変数から自動的にApplication IDとPrivate Keyを読み込み
- 入力:
-
get_sms_status: SMS配信ステータス取得ツール
- 入力:
message_id(必須):send_smsが返した message_id
- 機能:
delivery_status(submitted/delivered/failed等)を返却- Vonage Messages APIは配信ステータスを同期取得できないため、HTTPサーバー版の
Status Webhook(
POST /webhooks/message-status)で受信した結果を参照する - Webhook未設定時・stdio版では
submittedのまま(noteフィールドで明示される) - 記録はオンメモリで24時間保持(プロセス再起動でクリア)
- 入力:
ツールのレスポンス形式
| status | 意味 | 例 |
|---|---|---|
success |
実行成功 | {"status":"success","message_id":"abc","to":"+819012345678"} |
dry_run_success |
検証のみ成功(API呼び出しなし) | {"status":"dry_run_success","message":"Ready to send","to":"+819012345678","characters":12} |
error |
失敗 | {"status":"error","reason":"無効な電話番号形式です: 123","suggestion":"番号のフォーマットを確認してください。..."} |
エラー時は必ず reason(原因)と suggestion(AIが次に取るべき行動)が含まれます。再試行が無意味なケースでは suggestion にその旨が明記されるため、AIエージェントの無限リトライを防げます。
4. 使用例
Claude Desktopで以下のような質問ができます:
単発SMS送信
「090XXXXYYYYに「これはVonage MCPサーバーを使って送信しています。」とSMSを送ってください」
→ send_smsツールを使用してSMS送信
音声通話
「090XXXXYYYYに女性の声で『会議は明日の10時からです』と電話をかけて」
→ make_voice_callツールを使用して発信・音声読み上げ
「080XXXXYYYYに男性の声で『システム障害が発生しました。至急対応をお願いします』と電話で伝えて」
→ make_voice_callツールを使用して緊急連絡
通話ステータス取得
「Call ID ca6b7710-3423-4c8d-b630-7b981ec4b2c2 の通話ステータスを取得してください」
→ get_call_statusツールを使用して通話情報を取得
「先ほどの通話の料金と時間を教えてください」
→ get_call_statusツールで通話詳細を確認
Voice通話機能
機能概要
Voice APIを使用して自動音声通話を発信し、指定されたメッセージを日本語で読み上げます。
主な特徴
- 自動発信: 指定番号への自動発信
- 日本語音声: 女性・男性音声による自然な読み上げ
- NCCO制御: Nexmo Call Control Objectによる通話フロー制御
- 通話時間見積: メッセージ長から自動的に通話時間を算出し、通話の強制切断時間に連動させる
[!IMPORTANT] 通話時間には絶対上限(300秒)があります。 v1.2.1 以前は Vonage へ
length_timer: 7200(2時間)を送っていました。dry_runが「約N秒」と提示してユーザーが承認しても、NCCO の挙動や機械検出の結果によっては最大2時間まで課金され得る状態でした。音声は分課金なので、SMS と違って金額の跳ね方が大きい点が問題です。 現在は「見積もり + 30秒の余裕」を上限として送り、どんな場合も 300 秒を超えません。dry_runのmax_duration_secondsが実際に適用される値です。
音声オプション
| 音声タイプ | 性別 | 言語 | 特徴 |
|---|---|---|---|
| 女性 | 女性 | 日本語 | 自然で聞き取りやすい(デフォルト) |
| 男性 | 男性 | 日本語 | 落ち着いた男性音声 |
使用例
// 会議リマインダー
make_voice_call({
to: "090-1234-5678",
message: "明日の会議は10時から会議室Aで行います。資料をご準備ください。",
voice: "女性"
})
// 緊急連絡
make_voice_call({
to: "080-9876-5432",
message: "システム障害が発生しました。至急対応をお願いします。",
voice: "男性"
})
通話ステータス取得機能
機能概要
Vonage Voice APIを使用して、過去の通話のステータス情報を取得します。通話の詳細(ステータス、料金、レート、通話時間)を確認できます。
主な特徴
- 詳細情報取得: 通話のステータス、料金、レート、通話時間を一度に取得
- 自動設定読み込み: 環境変数から自動的にApplication IDとPrivate Keyを取得
- エラーハンドリング: 存在しないCall IDに対する適切なエラーメッセージ
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
| call_id | string | 取得する通話のCall ID(UUID形式)。必須 |
返却される情報
- status: 通話のステータス(completed, answered, busy, failed など)
- start_time: 通話開始時刻(ISO 8601形式)
- price: 通話料金(数値形式)
- rate: 通話レート(1分あたりの料金)
- duration: 通話時間(秒単位)
使用例
// Call IDを指定して通話ステータスを取得
get_call_status({
call_id: "ca6b7710-3423-4c8d-b630-7b981ec4b2c2"
})
// 結果例:
// ステータス: completed
// 開始時刻: 2025-12-10T03:53:19.000Z
// 料金: 0.06287850
// レート: 0.13973000
// 通話時間: 27秒
5. トラブルシューティング
サーバーが起動しない場合
npm run buildが正常に完了しているか確認npm startでエラーが出ないか確認- Node.jsバージョンが20.6.0以降であることを確認(
node -v)
Claudeデスクトップでのエラー
- JSON解析エラー「Unexpected token 'd', "[dotenv@17."... is not valid JSON」が表示される場合:
claude_desktop_config.jsonのargsに--env-file=.envが含まれていることを確認- サーバーコードがdotenvを使用していないことを確認(最新のコードではdotenvは使用していません)
- MCPサーバーを再起動
Claude Desktopで認識されない場合
claude_desktop_config.jsonの設定が正しいか確認- 作業ディレクトリ(cwd)のパスが正しいか確認
- Claude Desktopを再起動
機能が利用できない場合
- サーバーのログを確認(Claude Desktopの設定画面で確認可能)
- サーバーを再起動
Voice通話機能のトラブルシューティング
-
Voice通話が発信されない場合:
VONAGE_VOICE_FROM環境変数が正しく設定されているか確認- VonageアプリケーションでVoice機能が有効になっているか確認
- FROM番号がVonageアカウントに登録されているか確認
-
通話は繋がるが音声が再生されない場合:
- NCCOパラメータの音声設定を確認
- 音声オプション(女性/男性)が正しく指定されているか確認
プロジェクト構造
vonage-mcp-server/
├── src/ # TypeScriptソースコード
│ ├── index.ts # stdio版エントリーポイント
│ ├── http-server.ts # HTTP版エントリーポイント・Webhook受信
│ ├── tools.ts # MCPツール定義の共通レジストリ(stdio/HTTP共用)
│ ├── guardrails.ts # 電話番号検証・ホワイトリスト・レートリミット
│ ├── toolResponse.ts # 軽量JSONレスポンスの整形
│ ├── messageStatusStore.ts # SMS配信ステータスのオンメモリ保持
│ ├── webhookAuth.ts # Vonage署名付きWebhookの検証
│ ├── vonage.ts # Vonage SMS送信機能
│ ├── voiceCall.ts # Voice通話機能・NCCO生成
│ └── callStatus.ts # 通話ステータス取得機能
├── tests/ # テストファイル
│ ├── index.test.ts # メイン機能のテスト
│ ├── utils.test.ts # ユーティリティのテスト
│ ├── callStatus.test.ts # 通話ステータス取得のテスト
│ ├── tools.test.ts # ツールレジストリ・ガードレール統合のテスト
│ ├── guardrails.test.ts # ホワイトリスト・レートリミットのテスト
│ ├── messageStatusStore.test.ts # 配信ステータス保持のテスト
│ ├── http-server.test.ts # HTTPラッパーのテスト
│ └── integration.test.ts # 統合テスト
├── docs/
│ ├── deployment.md # デプロイ手順
│ ├── setup-guide.md # セットアップガイド (PDF の元原稿)
│ ├── gemini-enterprise-adk.md # Gemini Enterprise (ADK 経路) の手順
│ ├── dify.md # Dify から使う手順
│ ├── agentcore.md # AWS Bedrock AgentCore Gateway の手順
│ └── gemini_system_instruction.md # Gemini Enterprise向けSystem Instruction
### HTTPラッパー (Dify / 外部アプリ用)
HTTPラッパーを使用してサーバーを実行することで、外部アプリケーション(Difyなど)からHTTP POSTリクエスト経由でMCPツールを呼び出すことができます。
```bash
npm run start:http
これにより、ポート3000(デフォルト)でHTTPサーバーが起動します。
認証
MCP エンドポイント (/mcp) は Bearer トークンで認証します。
# 32バイトのランダムな値を生成して設定する
MCP_AUTH_TOKEN=$(openssl rand -hex 32)
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
| 環境変数 | 既定 | 説明 |
|---|---|---|
MCP_AUTH_TOKEN |
(未設定) | /mcp の Bearer トークン。16文字以上。短い値は起動エラーになる。 |
TRUST_UPSTREAM_AUTH |
false |
true にすると、このサーバー自身は認証せず上流(Cloud Run IAM / API Gateway など)に任せる。 |
BIND_HOST |
認証があれば 0.0.0.0、無ければ 127.0.0.1 |
待ち受けアドレス。 |
PORT |
3000 |
待ち受けポート。 |
ALLOWED_ORIGINS |
(未設定=すべて拒否) | CORS で許可するオリジン(カンマ区切り)。ブラウザから /mcp を呼ぶ場合のみ設定する。 |
ALLOWED_HOSTS |
ループバック運用なら localhost / 127.0.0.1 / ::1 |
許可する Host ヘッダーのホスト名(カンマ区切り)。ポートは比較に含まれない。 |
OAuth を使うには IdP が別途必要です
[!IMPORTANT] このサーバーはトークンを発行しません。検証するだけです。
OAuth で繋ぐには、このサーバーとは別に認可サーバー(IdP)を用意する必要があります。 ログイン画面を出し、アクセストークンを発行するのは IdP の仕事です。ここを用意しないと、設定をどう直しても繋がりません。
ChatGPT ──① 認可 ──▶ IdP(WorkOS / Keycloak など)※別途用意します
│ │
│◀── ② アクセストークン ──┘
│
└──③ Bearer <トークン> ──▶ このサーバー ──▶ Vonage
│
└─ ④ JWKS で署名・iss・aud・exp を検証
| 役割 | 担うもの |
|---|---|
| 認可サーバー(AS) | 別途用意する IdP |
| リソースサーバー(RS) | このサーバー |
MCP 仕様も同じ分担で、認可サーバーの実装は仕様のスコープ外と明記されています。このサーバーが RS だけを実装しているのはそのためです。
なぜ OAuth しか道が無い基盤があるのか
MCP 仕様が定める認可の標準は OAuth 2.1 です。 上の MCP_AUTH_TOKEN による静的 Bearer は仕様には存在せず、基盤が任意のヘッダを設定させてくれる場合にだけ使えます。
| 任意ヘッダを設定できる(開発者向け) | OAuth か「認証なし」しか選べない |
|---|---|
| Claude Code / ADK / Dify / AgentCore / n8n | ChatGPT のカスタムプラグイン / Gemini Enterprise のコネクタ |
課金が発生するサーバーに「認証なし」は選べません。 つまり右側の基盤に繋ぐには、OAuth 以外の道がありません。
IdP に必要な条件は2つ
どの IdP でもよいわけではありません。 次の2つを満たさないと繋がりません。
| 条件 | 満たさないとどうなるか |
|---|---|
| CIMD か DCR に対応している | クライアントが自分を登録できず、認可が始まりません。 ChatGPT は IdP と事前の関係を持たないため、手動でクライアントを登録する道がありません |
aud をこのサーバーの URI にできる |
トークンは発行されますが、このサーバーが「自分宛ではない」と判断して 401 にします |
2つめは RFC 8707(resource パラメータ)への対応か、IdP 側で audience を固定できることを意味します。
[!WARNING] 主要な IdP でも、この2つを満たさないものが多くあります。IdP 選びがこの経路の成否をほぼ決めます。
IdP CIMD / DCR audをこのサーバー向けにできるかWorkOS(手順あり) ✅ CIMD ✅ Resource Indicator を登録する Keycloak ✅ DCR ⚠️ resource非対応。audience mapper で固定する回避が要るAuth0 ✅ DCR ⚠️ 既定では resourceを見ない。Resource Parameter Compatibility Profile(Settings → Advanced)を有効にするとresourceから audience を決める。ただしaudienceも送られた場合はそちらが優先されるEntra ID ❌ ❌
実機で通した構成は docs/chatgpt.md にあります(ChatGPT + WorkOS AuthKit)。
| 環境変数 | 必須 | 説明 |
|---|---|---|
OAUTH_ISSUER |
✅ | 認可サーバーの issuer。トークンの iss と照合します。https のみ(ループバックのみ http 可)。 |
OAUTH_RESOURCE |
✅ | この MCP サーバーの正規 URI(RFC 8707 / RFC 9728 の resource)。例: https://example.com/mcp。フラグメントとクエリは指定できません。 |
OAUTH_JWKS_URI |
✅ | アクセストークンの署名鍵の取得先。IdP の /.well-known/openid-configuration にある jwks_uri を使います。 |
OAUTH_AUDIENCE |
トークンの aud に期待する値。既定は OAUTH_RESOURCE と同じ。IdP の API 識別子が URI と異なる場合だけ設定します。 |
|
OAUTH_SCOPES_SUPPORTED |
保護リソースメタデータに載せる scope(カンマ区切り)。OAUTH_REQUIRED_SCOPE を設定するなら、それをここにも含めてください(含めないと起動エラーです。クライアントはこの一覧から認可要求を組み立てるため、案内どおりに取ったトークンが必ず 403 になります)。 |
|
OAUTH_REQUIRED_SCOPE |
/mcp を呼ぶために必須の scope。設定すると、これを持たないトークンは 403 insufficient_scope になります。 |
|
OAUTH_REQUIRE_AT_JWT |
RFC 9068 の typ: at+jwt を持つトークンだけを受け付けます。既定は「OAUTH_AUDIENCE を上書きしたときだけ true」(下記)。厳しくしたい場合は明示的に true にできます。 |
OAUTH_ISSUER=https://your-tenant.example.com
OAUTH_RESOURCE=https://vonage-mcp.example.com/mcp
OAUTH_JWKS_URI=https://your-tenant.example.com/.well-known/jwks.json
設定すると次の2つが有効になります。
GET /.well-known/oauth-protected-resource(およびリソースのパスを付けた/.well-known/oauth-protected-resource/mcp)— RFC 9728 の保護リソースメタデータ。認証不要です。クライアントはトークンを持つ前にここを読み、どの認可サーバーへ行けばよいかを知ります/mcpの 401 / 403 にWWW-Authenticate: Bearer resource_metadata="..."が付きます — これが discovery の起点です
[!IMPORTANT] 必須の3つは揃っている必要があります。 1つでも欠けると起動エラーになります。部分的な設定を黙って無視すると、**「OAuth にしたつもりのサーバーが、実は静的トークンで動いていた」**という最も気づきにくい状態ができるためです。
[!IMPORTANT] 有効期限(
exp)を持たないアクセストークンは受け付けません。 無期限のトークンは、漏れた1本を失効させる手段がありません。 またOAUTH_ISSUER/OAUTH_RESOURCEは書いたとおりの文字列として使います(末尾のスラッシュも含めて識別子の一部です)。IdP の設定と1文字違わないように書いてください。OAUTH_REQUIRED_SCOPE/OAUTH_SCOPES_SUPPORTEDに空白・二重引用符・バックスラッシュ・非 ASCII を含めると起動エラーになります(RFC 6749 の scope の文字集合)。OAUTH_ISSUERにクエリやフラグメントを含めることもできません(RFC 8414)。
[!IMPORTANT]
http://localhostを使えるのは手元での確認だけです。 issuer / resource / JWKS のいずれかが http の場合、BIND_HOSTを指定しなければOAUTH_RESOURCEが指しているループバックアドレスで待ち受けます(http://[::1]:3000/mcpを配るなら::1。認証が構成済みでも0.0.0.0にはしません)。OAUTH_RESOURCEのポートとPORT、そしてアドレスの系統(IPv4 / IPv6)も揃っている必要があります(揃っていないと起動エラーです。配った URI に繋ぎに来たクライアントが接続できないため。localhostはどちらにも解決しうるので例外です)。平文でアクセストークンを受け取るサーバーを外部に出さないためです。この構成でBIND_HOSTに外部アドレスを指定すると起動時にエラーで停止します。MCP_AUTH_TOKENを併用していても同じです — 認証は「どちらか一方」で通るので、静的トークンがあってもアクセストークンは平文で流れます。
[!WARNING]
OAUTH_AUDIENCEを上書きするなら、ID トークン対策の設定が必須になります。ID トークンの
audはクライアント識別子です。既定(OAUTH_AUDIENCEを設定しない)なら、期待するaudはこのサーバーがメタデータで配っている URI なので、その URI を IdP 側でクライアント識別子として登録しない限り、ID トークンとは一致しません。 追加の設定は要りません(登録してしまった場合に何が起きるか、サーバーが何を検査するかは後述します)。しかし
OAUTH_AUDIENCEを別の値(IdP の API 識別子など)に上書きすると、その値がクライアント識別子と一致しうるようになります。同じ IdP が同じ鍵・同じ issuer で ID トークンも発行していれば、iss/aud/expでは区別できません。ログインできるだけの利用者が、API の委譲を受けないまま SMS を送れます。そのため上書きしたときだけ、「アクセストークンであることを積極的に示すもの」を1つ要求します。両方欠けていると起動時にエラーで停止します。
方法 設定 使える条件 RFC 9068 の typOAUTH_REQUIRE_AT_JWT=true(上書き時の既定)IdP がアクセストークンに typ: at+jwtを付けるAPI の scope OAUTH_REQUIRE_AT_JWT=false+OAUTH_REQUIRED_SCOPE=...IdP が typを付けない。ID トークンは API の scope を運びません
OAUTH_AUDIENCEにクライアント識別子を指定しないでください。 API / リソースの識別子にしてください。標識をどちらも設定しない構成では、
audがこのサーバーの URI 単独であることまで要求します。audを複数持つトークンは 401 で拒否します。ID トークンのaudは必ずクライアント識別子を含むため、「このサーバーの URI だけ」であれば ID トークンではありえないからです(その URI をクライアント識別子として登録していない限り)。複数 audience のアクセストークンを使う運用なら、OAUTH_REQUIRE_AT_JWTかOAUTH_REQUIRED_SCOPEを設定してください。トークンが
typ: at+jwtを持っていれば、要求していなくてもこの2つの検査は行いません。 「要求していない」ことと「実際に無い」ことは別で、標識を持っているトークンはそれ自体がアクセストークンの証明だからです(typは署名の対象です)。あわせて、トークンの
azp/client_idがこのサーバーの audience と同じなら拒否します。 その状態は「この URI がクライアント識別子として登録されている」ことそのもので、上の根拠が崩れているためです。ただしどちらの claim も必須ではないので、これで塞ぎきれるわけではありません。 確実にしたい場合はOAUTH_REQUIRE_AT_JWT=trueかOAUTH_REQUIRED_SCOPEを設定してください。
at_hash/c_hashを持つトークンは常に拒否しますが、これは当てにできません — どちらも条件付きの claim で、認可コードフローの ID トークンには通常入っていないためです。「無いこと」は根拠になりません。
[!NOTE] 当初はこの標識を無条件に要求していました。 しかし MCP 仕様に最も沿っている WorkOS ですら
typ: at+jwtを付けないため、素直に設定した利用者が全員 401 を踏む状態でした。その URI をクライアント識別子として登録しない限り衝突しない構成にまで、標識を求めていたのが誤りで、条件を実態に合わせています(登録してしまった場合の扱いは上記のとおりです)。
[!IMPORTANT] アクセストークンは audience を検証します。 同じ IdP が別のサービス向けに発行したトークンでは通りません(MCP 仕様の MUST)。IdP 側でこの MCP サーバーを1つの API / Resource として登録し、
audにその識別子が入るようにしてください。ここを省くと、「別のサービスを使う」つもりで同意しただけの利用者のトークンで SMS が送れてしまいます。
[!IMPORTANT]
MCP_AUTH_TOKENと併用すると、どちらの資格情報でも/mcpを通れます。 OAuth しか喋れない基盤と、任意ヘッダを送れる基盤を同じデプロイに繋ぐ構成は実際にあるため、片方を黙って無効化していません。この状態では起動のたびに警告が出ます。OAuth だけに絞るならMCP_AUTH_TOKENを削除してください。
[!WARNING] 繋がるかどうかは IdP のクライアント登録方式で決まります。 ChatGPT や Claude のように事前の関係が無いクライアントは、Client ID Metadata Documents か動的クライアント登録(DCR)で
client_idを得ます。IdP がどちらにも対応していない場合、その基盤からは接続できません。 基盤側でclient_id/client_secretを手入力できるなら、事前登録でも構いません。
[!NOTE]
/mcpの応答コードの意味づけ。 認証情報を送っていないリクエストには、errorを付けない401のチャレンジを返します(未認証は失敗ではなく認可フローの1歩目です)。Bearer以外の認証方式も同じ扱いです。Bearerと書いてあるのに中身が無い場合だけ400を返します。認可サーバーの署名鍵を取得できなかった場合は503で、invalid_tokenにはしません — トークンが無効とは限らず、401を返すとクライアントは正当なトークンを捨てて取り直しに行きます。
[!NOTE] 受け取ったアクセストークンは下流に流しません。 Vonage を呼ぶときに使うのは、アプリケーション ID と秘密鍵から組む別の JWT です。仕様が禁じる token passthrough には当たりません。
[!NOTE] OAuth 経路は 2026-09-11 に実機で確認しました。 ChatGPT のカスタムプラグイン + WorkOS AuthKit で、discovery → 認可 → トークン検証 → **SMS の実送信(配信ステータス受信まで)と音声の実発信(通話イベント受信まで)**が通っています。手順は docs/chatgpt.md。
そこで分かった、ドキュメントに書かれていない事実が1つあります。WorkOS はアクセストークンに
typ: at+jwtを付けません。 他の IdP でも同じことが起こりえます。v3.2.0 では必須の3つ(OAUTH_ISSUER/OAUTH_RESOURCE/OAUTH_JWKS_URI)だけで動くようにしてあります。ただし 2026-09-11 に実機で通したのは、その3つだけの構成ではありません。 当時は v3.1.1 で標識が無条件に必要だったため、
OAUTH_REQUIRE_AT_JWT=falseとOAUTH_REQUIRED_SCOPE=emailを足した5変数で確認しています。v3.2.0 の3変数構成は、まだ実機で確認していません。ただし3つで足りない場合があります。
typを付けず、かつ複数 audience のアクセストークンを出す IdP では、OAUTH_REQUIRE_AT_JWTかOAUTH_REQUIRED_SCOPEの設定が要ります(設定しないと 401 が続きます)。理由は 上の標識の節に書いたとおりです。Gemini Enterprise のコネクタは同じ仕組みですが、まだ実機で確認していません。動作報告をいただけると助かります。
[!IMPORTANT] CORS は既定で閉じています。 Bearer トークン認証があっても、ブラウザ側がトークンを持つ構成(ブラウザ拡張や Web 版の MCP クライアント)では、CORS が開いていると悪意ある Web ページが
/mcpを呼び、レスポンスまで読み取れます。ツールのレスポンスに含まれる宛先や配信状況も読み取られます。 MCP クライアントの多くはブラウザではないため、開ける必要があるのは例外的なケースだけです。必要な場合のみALLOWED_ORIGINSに列挙してください。
[!IMPORTANT] ループバック運用では
Hostヘッダーを検証します(DNS rebinding 対策)。 攻撃者が自分のドメインを127.0.0.1に解決させると、ブラウザからは同一オリジンに見えるため CORS では防げません。このときHostヘッダーには攻撃者のドメインが入るので、そこで403を返します。 外部アドレスに bind する場合、正しいHostは運用者のドメインでありサーバー側からは分かりません。推測して塞ぐと正規のリクエストまで落ちるため、ALLOWED_HOSTSが明示されるまで検証しません。インターネットに公開する場合はALLOWED_HOSTSの設定を推奨します。
[!WARNING] v1.3.0 の破壊的変更:
X-API-KEYによる認証を廃止しました。 v1.2.1 以前はX-API-KEYヘッダをVONAGE_APPLICATION_IDと比較していました。Application ID は秘密情報ではありません — Vonage に送る JWT の claim に入る公開識別子です。これを認証に使うと、Application ID を知っている者は誰でも、そのデプロイの持ち主の課金で SMS 送信や架電ができてしまいます。MCP_AUTH_TOKENに移行してください。
[!IMPORTANT] 認証を設定しない場合、HTTPサーバーは
127.0.0.1でのみ待ち受けます。 認証なしでBIND_HOSTに外部アドレスを指定すると、起動時にエラーで停止します。 リクエストごとに接続元が localhost かを判定する方式は採っていません。Cloud Run やリバースプロキシの配下では、アプリから見た接続元が127.0.0.1になり、外部からのリクエストが全部「localhost」と判定されて無認証で通るためです。bind するアドレスならプロキシの有無に左右されません。
推奨構成: 認証は手前の層に置く
もっとも堅いのは、Cloud Run IAM や API Gateway をこのサーバーの手前に置く構成です。認証の実装をこのサーバーから切り離せるうえ、鍵のローテーションや監査ログもプラットフォーム側の仕組みに乗せられます。
その場合は TRUST_UPSTREAM_AUTH=true を設定してください(起動のたびに警告が出ます)。手前で認証していない環境でこれを有効にすると完全に無防備になります。
# Cloud Run IAM で認証する例(--allow-unauthenticated は付けない)
gcloud run deploy vonage-mcp-server \
--set-env-vars TRUST_UPSTREAM_AUTH=true,ENABLE_SMS=true \
--no-allow-unauthenticated
APIエンドポイント
| 経路 | 認証 |
|---|---|
GET /health |
不要 |
POST /mcp |
Bearer トークン(/mcp 配下は全 HTTP メソッドが対象) |
POST /webhooks/* |
Vonage の署名検証 |
ALL /mcp
MCP の Streamable HTTP エンドポイントです。POST (JSON-RPC) / GET (SSE) / DELETE (セッション終了) を MCP SDK の StreamableHTTPServerTransport が処理します。手書きの JSON-RPC 実装ではないので、仕様の追加に追従できます。
仕様どおり、クライアントは POST に Accept: application/json, text/event-stream を付ける必要があります(欠けていると 406 になります)。
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
[!NOTE] セッションを持たないステートレス構成です。 リクエストごとにサーバーとトランスポートを生成し、
Mcp-Session-Idを発行しません。 セッションを持つとその状態がプロセスのメモリに載るため、Cloud Run のように複数レプリカへ分散する環境では、同じセッションが別のレプリカに届いた時点で壊れます。スティッキーセッションを前提にすると、動く基盤が減ります。このサーバーのツールはどれも1リクエストで完結し、サーバー起点の通知も送らないため、セッションを持つ理由がありません。 同じ理由で POST の応答は SSE ではなく通常の JSON で返します(仕様上どちらでも構いません)。SSE はプロキシやゲートウェイにバッファされることがあり、環境依存の不具合を持ち込みやすいためです。
エラーは2種類に分かれます。
| 種類 | 返り方 | 例 |
|---|---|---|
| スキーマ違反 | JSON-RPC エラー (-32602) |
電話番号の形式が inputSchema に合わない |
| ガードレール違反・実行時エラー | result の isError: true |
ALLOWED_NUMBERS 外の宛先、レートリミット超過、Vonage API の失敗 |
前者は MCP SDK が inputSchema で検証して弾くため、ハンドラに到達しません(エラーメッセージにはスキーマに書いた説明がそのまま入ります)。後者は原因が reason、次に取るべき行動が suggestion に入ります。
[!NOTE] 無効化されているツール(capability トグルが OFF)は登録されないため、
tools/callでは「存在しないツール」として扱われます。tools/listに出さない以上、これが MCP としての正しい表現です。どの環境変数を設定すべきかは起動ログと本 README を参照してください。
[!WARNING] v1.3.0 の破壊的変更:
POST /mcp-invokeとGET /mcp-toolsを削除しました。 MCP と等価な機能を独自のインターフェースで二重に公開していたためです。/mcpだけ認証やガードレールを直しても、こうした別経路が残っていればそこから全部迂回できます。ツールの実行経路は/mcpの1本に絞りました。 既存の呼び出しは JSON-RPC のtools/callに置き換えてください。{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"send_sms","arguments":{"to":"09012345678","message":"hello"}}}
POST /webhooks/message-status
Vonage Messages API の Status Webhook(配信結果 / DLR)の受信エンドポイントです。Vonage から呼ばれるため Bearer トークン認証の対象外ですが、別途Webhook認証が必須です。
受信した配信結果はオンメモリに24時間保持され、get_sms_status ツールから参照できます。
[!IMPORTANT] このサーバーが送信した記録のない
message_idは、配信ステータスの保持対象になりません。 同じ Vonage Application を別のシステムと共用していると、そちらが送信したメッセージの DLR もこのエンドポイントに届きます。それをそのまま保持すると、保持件数の上限(1000件)から自分のレコードが押し出され、get_sms_statusが使えなくなります。 ただし、まれに DLR が送信 API のレスポンスより先に届くことがあります。これを取りこぼさないよう、未知の ID は 5 分間だけ別の小さなバッファに保持し、対応する送信が記録された時点で取り込みます。レスポンスのpending: trueはこの状態を表します。Vonage Dashboard の Application 設定で Status URL にhttps://<host>/webhooks/message-statusを登録してください。
認証は以下の優先順位で行われます。
VONAGE_API_SIGNATURE_SECRETが設定されていれば、Authorization: Bearer <JWT>の署名(HS256)と、ボディを束縛するpayload_hashクレームを検証する(推奨)VONAGE_WEBHOOK_SECRETが設定されていれば、x-webhook-secretヘッダーと照合する- どちらも未設定なら 503 を返してエンドポイントを無効化する
| 状況 | HTTPステータス |
|---|---|
| 取り込み成功 | 200(ignored: false) |
| 再送・順序逆転で古い通知が届いた | 200(ignored: true、既存の状態を維持) |
認証情報なし・不正な署名・payload_hash 不一致 |
401 |
message_uuid / status が欠けたペイロード |
400 |
| Webhook認証が未設定 | 503 |
# 共有シークレット方式の例
curl -X POST http://localhost:3000/webhooks/message-status \
-H "Content-Type: application/json" \
-H "x-webhook-secret: $VONAGE_WEBHOOK_SECRET" \
-d '{"message_uuid":"abc-123","to":"819012345678","status":"delivered","channel":"sms"}'
POST /webhooks/inbound
受信メッセージ用のスタブ(常に 200 を返す)。Vonage側の設定必須項目を満たすために用意しています。
POST /webhooks/voice/answer
音声の着信に対して NCCO を返すエンドポイントです。このサーバーは発信専用で、着信を処理する機能を持ちません。 案内を読み上げて通話を終了します(文面は VOICE_INBOUND_MESSAGE で変更できます)。
それでも用意しているのは、Vonage の番号をアプリケーションにリンクすると、その番号への着信がアプリに向くためです。Answer URL が無いと、発信者は無言のまま切られます。
[!IMPORTANT] Vonage 側で Answer URL の HTTP メソッド(
answer_method)を POST に変更してください。 既定は GET ですが、署名付き Webhook の検証はリクエストボディのハッシュ(payload_hash)を必要とするため、ボディの無い GET は受け付けません。GET で呼ばれた場合は405と対処法を返します。
POST /webhooks/voice/event
通話イベントの受信エンドポイントです。受信した内容はオンメモリに24時間保持され、get_call_status ツールのレスポンスに detail と sip_code として重ねて返されます。
[!TIP] 通話が失敗した理由が届くのは、この Webhook だけです。 Voice API の
GET /v1/calls/{uuid}はstatusしか返さずdetailは常にnullです。設定しておくと、原因調査が推測ではなく事実になります。
detail は status ごとに意味が異なります(公式リファレンス)。まとめて「宛先が悪い」と解釈しないでください。
| status | detail | 意味 | 掛け直す意味があるか |
|---|---|---|---|
failed |
cannot_route |
宛先がこのアカウントで未対応、またはブロック。相手の状態とは無関係 | ない |
failed |
number_out_of_service |
宛先の番号自体が使われていない(番号の確認が要る) | ない |
failed |
internal_error |
Vonage 側の内部エラー。宛先については何も分からない | 時間をおけばある |
rejected |
invalid_number / restricted / declined |
番号が無効、キャリアまたは着信者が拒否 | 同じ条件では期待できない |
unanswered |
unavailable / timeout |
相手が一時的に応答できない | 時間をおけばある |
get_call_status は、接続できなかった通話にこの分類に沿った note を添えます。
[!NOTE]
detailが空でも、Webhook が未設定だとは限りません。 通知が届く前に確認した、Vonage がdetailを付けなかった、サーバーが再起動した、24時間の保持期間を過ぎた——どれも同じ「空」に見えます。このサーバーからは原因を判別できないため、noteも断定しません。
[!IMPORTANT] 理由が「まだ届いていない」のか「届きようがない」のかで、正しい対処は正反対です。 届きようがない構成は2つあり、
noteはそれぞれを名指しして再確認を勧めません。
- stdio 版 — Webhook を待ち受けるプロセスがありません。別プロセスの HTTP 版に Event URL を向けても、記録はそちらのメモリに入るだけで stdio 側の結果は変わりません(
get_sms_statusが stdio でsubmitted止まりなのと同じ理由)- Webhook の認証が未設定 —
VONAGE_API_SIGNATURE_SECRETもVONAGE_WEBHOOK_SECRETも無い場合、エンドポイントは fail-closed で 503 を返し続けます。HTTP で動いていても記録は永久に埋まりません
受信したイベントはオンメモリに24時間保持されます(最大1000件)。このうち、このサーバーが発信していない通話(着信レグや、同じ Application を共用する別システムの通話)には専用枠 200 件を設けています。 枠を分けないと、着信が多い環境で get_call_status から引ける記録のほうが押し出されるためです。
[!NOTE]
busyが返っても「相手が通話中」と断定はできません。 実測で、アプリケーション側の設定が不十分なときにrate 0/ 0秒のbusyが返り、同じ発信元から携帯宛は繋がる、という状態が起きました。detailが無いまま結論を出さないでください。
どちらのエンドポイントも認証は /webhooks/message-status と同じです(署名付きJWT を推奨、未設定なら 503 で無効化)。
[!WARNING] 古い Vonage アプリケーションでは、署名付き Webhook が既定で無効です。 その場合このサーバーは 401 を返し続けるので、Vonage Dashboard でアプリケーションの署名付き Webhook を有効化してください。
VONAGE_WEBHOOK_SECRET(共有シークレット)は、この経路の代替にはなりません。Vonage のアプリケーション設定で指定できるのは URL と HTTP メソッドだけで、x-webhook-secretヘッダーを付ける手段がないためです。 この方式が使えるのは、手前に置いたゲートウェイなどがヘッダーを付与する構成に限られます。
Vonage Dashboard の Application 設定で、次のように登録します。
| 設定項目 | URL | メソッド |
|---|---|---|
| Answer URL | https://<host>/webhooks/voice/answer |
POST |
| Event URL | https://<host>/webhooks/voice/event |
POST |
Gemini Enterprise などのAIエージェントから利用する
AIエージェントに設定すべき System Instruction(承認フロー、dry_run の使い方、エラー対処方針)を docs/gemini_system_instruction.md にまとめています。そのまま貼り付けられる形式です。
あわせて、サーバー側で ALLOWED_NUMBERS と RATE_LIMIT_PER_HOUR を設定することを強く推奨します。System Instruction はプロンプトインジェクションで破られる前提で読んでください。 実効的な防御はサーバー側の設定だけです。
⚠️ Gemini Enterprise のカスタム MCP サーバーコネクタを使う場合
公式ドキュメントによれば、このコネクタが送れる認証は 「認証なし」と「OAuth 2.0」の2つだけで、任意のヘッダを設定する欄がありません。つまり MCP_AUTH_TOKEN はこの経路では使えません。
かつコネクタは、MCP サーバーが公開インターネット上の HTTPS エンドポイントで到達可能であることを要求します。
[!CAUTION] ここで「認証なし」を選ぶと、課金を発生させられるサーバーが無認証で全世界に公開されます。 選ばないでください。
取りうる構成は次の2つです。
構成A: 上流で OAuth 2.0 を終端する
API Gateway や Identity-Aware Proxy をこのサーバーの手前に置いて OAuth 2.0 を処理し、本サーバーには TRUST_UPSTREAM_AUTH=true を設定します。認証の実装をこのサーバーから切り離せるうえ、監査ログもプラットフォーム側の仕組みに乗せられます。詳しくは推奨構成: 認証は手前の層に置くを参照してください。
構成B: ADK でエージェントを書く(実機で確認済み・推奨)
コネクタを使わず、Agent Development Kit の McpToolset から StreamableHTTPConnectionParams で接続します。任意のヘッダを送れるので MCP_AUTH_TOKEN がそのまま使えます。
ツール実行前の承認も ADK の require_confirmation で取れます。 Gemini Enterprise の Apps に承認ウィンドウが表示され、承認するまで送信されません(2026-08-25 に dry_run → 承認 → 実送信 → delivered まで確認しました)。
手順は Gemini Enterprise の Agent Apps から使う(ADK 経由) にまとめています。
import os
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
# 完全な実装では Secret Manager から解決する(下記リンク先を参照)
MCP_AUTH_TOKEN = os.environ["MCP_AUTH_TOKEN"]
def confirm_unless_dry_run(**kwargs) -> bool:
"""dry_run: true 以外の呼び出しに承認を要求する。"""
return kwargs.get("dry_run") is not True
# 課金対象のツールだけを、承認を必須にして渡す
sending = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://your-server.example.com/mcp",
headers={"Authorization": f"Bearer {MCP_AUTH_TOKEN}"},
),
tool_filter=["send_sms", "make_voice_call"],
require_confirmation=confirm_unless_dry_run,
)
[!WARNING]
require_confirmationを省くと、課金対象のツールが承認なしで実行されます。require_confirmationの callable にはツール名が渡らないため、読み取り専用のツールは別のMcpToolsetとして渡してください(承認を掛けると不要な確認が毎回出ます)。このサンプルは要点だけです。トークンをシリアライズに含めない書き方まで含めた完全な実装は ADK 経由の手順 にあります。
プロジェクト構造(続き)
├── dist/ # コンパイルされたJavaScript
├── package.json # プロジェクト設定
├── tsconfig.json # TypeScript設定
├── jest.config.js # Jest設定
├── .env.example # 環境変数設定例
├── private.key # Vonage秘密鍵(要設定・リポジトリには含めない)
├── LICENSE # Apache License 2.0
├── SECURITY.md # セキュリティポリシー
├── CONTRIBUTING.md # コントリビューションガイド
└── README.md # このファイル
依存関係
主要パッケージ
@vonage/server-sdk- Vonage SMS機能@vonage/voice- Voice通話機能専用SDK@vonage/jwt- Webhook の署名検証@modelcontextprotocol/sdk- MCP Server実装zod- スキーマ検証zod-to-json-schema- ZodスキーマからJSON Schemaを生成(HTTP版のtools/list用)express/cors- HTTPラッパー
Vonage アカウントについて
このサーバーを使うには Vonage の Application ID と秘密鍵が必要です。
日本国内でご利用の場合は、日本語での申し込みページ からのお申し込みをご検討ください。日本語でのサポートと請求に対応しています。
海外からご利用の場合は Vonage Developer Portal から直接開設してください。
このサーバーはどちらの経路で開設したアカウントでも同じように動作します。 特定の経路を強制することはありません。
提供元
このプロジェクトは 株式会社KDDIウェブコミュニケーションズ が Vonage のリセラーとして開発・公開しています。
私たちはこのサーバーをサービスとして運営していません。 OSS のリファレンス実装として提供しており、利用者の資格情報を預かることはありません。
ライセンス
Copyright 2026 KDDI Web Communications Inc.
関連ドキュメント
- セキュリティポリシー — 脆弱性の報告方法と脅威モデル
- コントリビューションガイド
- デプロイ手順
- Gemini Enterprise の Agent Apps から使う(ADK 経由) — 実機で確認済みの手順
- Gemini Enterprise 向け System Instruction