Vonage Mcp Server — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Vonage Mcp Server (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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もプロンプトも、実効的な防御にはなりません。
Claude Desktopで簡単にインストールできます:
.mcpbファイルをダブルクリック、またはClaude Desktopにドラッグ&ドロップVONAGE_APPLICATION_ID: Vonage Application IDVONAGE_PRIVATE_KEY_PATH: 秘密鍵ファイルのパス(例: /Users/your-name/vonage/private.key)VONAGE_VOICE_FROM: 音声通話用の電話番号(E.164形式、例: 81345438093)npm installprivate.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番号このサーバーは、既定ではツールを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が必要で、いずれも未設定なら起動時にエラーになります(実行して初めて失敗するより、起動時に気づけるほうが安全なため)。
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[!NOTE] ここに書いているのは法的助言ではありません。Vonage の利用規約およびキャリアの仕様として公開されている事項を、利用者が確認すべき論点として整理したものです。最新の条件は Vonage のサポート記事と契約書をご確認ください。 このプロジェクトの規制案内は日本国内での利用を対象としています。他地域の利用者はご自身で確認する責任を負います。
#### 守るべき利用条件
| 項目 | 内容 |
|---|---|
| マーケティング目的の送信 | 受信者からのオプトイン同意が必須 |
| P2P(個人間)トラフィック | 禁止 |
| 禁止コンテンツ | 政治 / 宗教 / 未承諾プロモーション / ギャンブル |
これらはサーバー側では判定できません。 本文の内容や同意取得の有無はサーバーからは分からないため、利用者の責任で遵守してください。
#### 配信の不確実性 — もっとも注意が必要な点
[!WARNING] 日本のネットワークでは、URL を含むメッセージがフィッシング SMS 対策として配信されないことがあります。拒否基準は非公開です。 問題は、API が成功を返すのに実際には届かないことです。AIエージェントは失敗を検知できないため、放っておくと「送信できました」とユーザーに報告してしまいます。
このサーバーはこう扱います。
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-nodenpm run dev:startnpm run build# 環境変数ファイル(.env)を使用して実行(推奨・Node.js v22以降)
npm start
# 環境変数ファイルを使用せずに実行(従来方式)
npm run start:legacynpm run devnpm run cleannpm testnpm run test:watchnpm run test:coverageこのMCPサーバーをClaude Desktopで利用するための設定方法を説明します。
.mcpbファイルを使用すると、Claude Desktopに簡単にインストールできます。
#### インストール手順
mcpb CLI が必要です(未インストールなら一度だけ)。
npm install -g @anthropic-ai/mcpb npm run build:mcpbこれにより vonage-mcp-server.mcpb ファイルが作成されます。
.mcpb ファイルをダブルクリックClaude Desktop のインストールダイアログで以下を入力:
/Users/your-name/vonage/private.key)81345438093)Claude Desktop を再起動すると、Vonage MCP サーバーが利用可能になります。
#### MCPBファイルの配布
作成した .mcpb ファイルは他のユーザーと共有できます:
#### 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"
}
}
}
}#### 設定ファイルの場所
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json~/.config/Claude/claude_desktop_config.json#### 設定手順
claude_desktop_config.json を開くmcpServers セクションに上記の設定を追加設定完了後、Claude Desktopで以下の機能が利用できます:
#### ツール
すべてのツールは軽量なJSONを返します(Vonage APIの生レスポンスは返しません)。詳細は ツールのレスポンス形式 を参照してください。
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){"status":"success","message_id":"...","to":"+81..."} を返却to (必須): 発信先電話番号(E.164形式または0ABJ形式)message (必須): 読み上げるメッセージ(最大1000文字)voice (オプション): 女性 または 男性(デフォルト: 女性)dry_run (オプション): true で発信せず検証のみdry_run で提示した時間を大きく超えて課金されることはないdry_run は estimated_duration_seconds(見積もり)と max_duration_seconds(実際に適用される上限)の両方を返すcall_id (必須): 取得する通話のCall ID(UUID形式)call_status(通話ステータス)、start_time、price、rate、duration_seconds を返却message_id (必須): send_sms が返した message_iddelivery_status(submitted / delivered / failed 等)を返却Status Webhook(POST /webhooks/message-status)で受信した結果を参照する
submitted のまま(note フィールドで明示される)#### ツールのレスポンス形式
| 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エージェントの無限リトライを防げます。
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 APIを使用して自動音声通話を発信し、指定されたメッセージを日本語で読み上げます。
[!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を使用して、過去の通話のステータス情報を取得します。通話の詳細(ステータス、料金、レート、通話時間)を確認できます。
| パラメータ | 型 | 説明 |
|---|---|---|
| call_id | string | 取得する通話のCall ID(UUID形式)。必須 |
// Call IDを指定して通話ステータスを取得
get_call_status({
call_id: "ca6b7710-3423-4c8d-b630-7b981ec4b2c2"
})
// 結果例:
// ステータス: completed
// 開始時刻: 2025-12-10T03:53:19.000Z
// 料金: 0.06287850
// レート: 0.13973000
// 通話時間: 27秒#### サーバーが起動しない場合
npm run build が正常に完了しているか確認npm start でエラーが出ないか確認node -v)#### Claudeデスクトップでのエラー
claude_desktop_config.jsonのargsに--env-file=.envが含まれていることを確認#### Claude Desktopで認識されない場合
claude_desktop_config.json の設定が正しいか確認#### 機能が利用できない場合
#### Voice通話機能のトラブルシューティング
VONAGE_VOICE_FROM環境変数が正しく設定されているか確認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ツールを呼び出すことができます。
npm run start:http
これにより、ポート3000(デフォルト)でHTTPサーバーが起動します。
#### 認証
**MCP エンドポイント (`/mcp`) は Bearer トークンで認証します。**
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**([手順あり](docs/chatgpt.md)) | ✅ 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](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 の `typ` | `OAUTH_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](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 が続きます)。理由は [上の標識の節](#oauth-を使うには-idp-が別途必要です)に書いたとおりです。
>
> Gemini Enterprise のコネクタは同じ仕組みですが、まだ実機で確認し~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.