MCP tunnelsに接続できないときのトラブルシューティング
MCP tunnelsの接続失敗を3層に分けて切り分ける手順。OAuth・証明書・IP検証・cloudflared接続それぞれの原因と対処法。
MCP tunnelsの接続失敗は3層構造で切り分ける
MCP tunnelsは、プライベートネットワーク内のMCPサーバーへインバウンドポートを開けずに到達させるトンネルです。障害が起きうる層は3つあります。cloudflaredからトンネルエッジへの外向き接続、AnthropicからプロキシへのTLS(内側TLS)、プロキシからアップストリームへのルーティングとIP検証です。この順に調べるのが最短です。外向き接続が張れていないだけなのに、証明書やIP検証から疑うと遠回りになります。
MCP tunnelsはResearch Preview段階の機能で、利用には申請が必要です。稼働率・サポート・継続性のコミットは付かず、基盤の通信を担うCloudflareも可用性を約束していません。Anthropicはいつでも仕様を変えたり提供を終えたりできるとしているので、挙動が本記事と食い違うときは提供状態の変化も疑ってください。
どのログから開くか
- 1
cloudflaredのログ(外向き接続)
TUNNEL_TOKENと、トンネルエッジへの7844番ポートの通信を調べます。No ingress rules were definedもここに出ます。 - 2
プロキシのログ(内側TLS)
tls handshake failedが出たら、証明書の有効期限・SAN・登録済みCAの3点を見ます。 - 3
プロキシのログ(ルーティングとIP検証)
no route for hostとIP validation failedは、ルート定義とアップストリームのアドレスの問題です。
クイックリファレンス — 症状と対処
| 症状 | 原因 | 対処 |
|---|---|---|
| エージェントの「+ MCP Server」候補にトンネルが出てこない | 原因表示条件を満たしていない | 対処ワークスペースとCA証明書を確かめる(後述) |
呼び出し元にHTTP 500、cloudflaredのログにNo ingress rules were defined | 原因cloudflaredに転送先がない | 対処起動オプションとネットワーク設定を足す(後述) |
プロキシログにno route for host | 原因tunnel_domainの不一致、または再起動漏れ | 対処設定ファイルの節を参照 |
プロキシログにIP validation failed: <ip> is not a private address | 原因アップストリームのMCPサーバーがRFC1918の範囲外に解決されている | 対処後述の「証明書エラーとIP検証エラーを切り分ける」を参照 |
プロキシがcannot unmarshal !!seq into map[string]stringで終了する | 原因routesをYAMLのリストで書いている | 対処連想配列形式に直す(後述) |
プロキシがopen /data/tls.key: permission deniedで終了する | 原因鍵のパーミッションが0600のまま | 対処設定ファイルの節を参照 |
curl https://<proxy>:8080がwrong version numberで失敗する | 原因想定どおりの挙動 | 対処故障ではない(後述) |
設定ファイル起因のエラーを深掘りする
クイックリファレンスの症状のうち、設定ファイルの書き方が原因のものは、エラーメッセージから原因を絞れます。
no route for hostの原因は2つです。tunnel_domainの値がトンネル詳細ページのドメインと食い違っているか、config.yamlを編集した後にプロキシを再起動していないかです。値を直したらdocker compose restart mcp-proxyを挟みます。
cannot unmarshal !!seq into map[string]stringでプロキシが起動に失敗するなら、routesをリスト記法(- name: url)で書いています。routesはキーと値のペアからなる連想配列で、routes: { docs: http://internal-wiki:8080 }のように書きます。
リストでなくても、routesの値はscheme://host:portの形でなければなりません。ポートは省略できず、http://internal-wiki:8080/mcpのようにパスを含めると、設定の読み込み時にinvalid upstream (must be scheme://host:port)で弾かれます。パスはルートに書かず、呼び出し側のURLに含めます。プロキシはパスとクエリ文字列を加工せずアップストリームへ渡します。
tunnel_domainを空にすると、routesのキーにwikiのような短いサブドメインは使えません。完全なホスト名を書く必要があります。アップストリームがhttps://のときは、upstream.tls.ca_fileかupstream.tls.include_system_casのどちらかを設定しておかないと、プロキシはアップストリームの証明書を検証する足場を持てません。
open /data/tls.key: permission deniedは、鍵ファイルのパーミッションが0600のままで、非rootのプロキシコンテナが読めていない状態です。chmod 644 data/tls.keyで解消します。
No ingress rules were definedがcloudflaredのログに出て呼び出し元にHTTP 500が返るなら、cloudflaredにローカルの転送先がありません。起動オプションに--url http://localhost:8080を渡し、network_mode: "service:mcp-proxy"でプロキシと同じネットワーク名前空間に置きます。
トンネルの前提を疑う前にワークスペースと証明書を確認する
エラーメッセージが出る前の段階でつまずくこともあります。「+ MCP Server」ピッカーにトンネルが出てこないときは、一覧に載る条件を満たしていないだけで、設定ミスとは限りません。表示されるのは、セッションと同じワークスペースにあり、有効なCA証明書を少なくとも1枚持つトンネルだけです。証明書をまだ登録していないか、トンネルを作ったのとは別のワークスペースでセッションを開いていないかを先に見ます。
Console経由で作ったトンネルは、claude.aiのコネクタ一覧には出ません。使えるのはManaged AgentsとMessages APIの2つで、claude.aiから呼べないこと自体は不具合ではありません。逆に、claude.ai(Enterprise)側で作ったトンネルはAPIからは使えず、この記事の内容はConsole側のトンネルが対象です。
トンネルはMCPサーバーへの暗号化された経路を運ぶだけで、サーバーへの認証は代行しません。OAuthやBearerトークンが必要なサーバーなら、トンネルとは別にそれを渡します。
curl https://<proxy>:8080でwrong version numberが返るのは故障ではありません。リスナーは平文のWebSocketで待ち受け、TLSはそのストリームの内側で行われる設計だからです。疎通確認はManaged AgentセッションかMessages APIで行います。
OAuthがソースIP許可リストの背後で失敗するときの対処
自組織の認可サーバーが送信元IPで許可リストを組んでいると、/tokenや/register、ディスカバリーエンドポイントへAnthropicのバックエンドが到達できず、OAuthが失敗します。Managed Agentsのvaultでリフレッシュが失敗する場合は、mcp_oauth_validateで原因を切り分ける手順が別にあります。Anthropicの送信元IP範囲を許可リストに入れたくないときは、バックエンド間のOAuth呼び出しだけをトンネル経由に切り出し、ブラウザ向けの/authorizeは既存の公開ホスト名に残せます。
認可サーバーだけをトンネルに載せる
- 1
認可サーバー用のルートをプロキシに追加する
routes: mcp: http://your-mcp-server:8080 auth: http://your-auth-server:8080routesの編集後は、docker compose restart mcp-proxy(Helmならhelm upgrade)でプロキシを再起動します。 - 2
ディスカバリーメタデータをエンドポイント別に分ける
認可サーバーの
/.well-known/oauth-authorization-serverで、authorization_endpointだけを許可済みの既存ホストへ向け、それ以外をトンネルへ向けます。{ "issuer": "https://auth.<tunnel-domain>", "authorization_endpoint": "https://<your-allowlisted-host>/authorize", "token_endpoint": "https://auth.<tunnel-domain>/token", "registration_endpoint": "https://auth.<tunnel-domain>/register", "code_challenge_methods_supported": ["S256"] } - 3
MCPサーバー側もトンネルのissuerを参照させる
MCPサーバーの
/.well-known/oauth-protected-resourceで、トンネルのホスト名を認可サーバーとして指します。{ "resource": "https://mcp.<tunnel-domain>", "authorization_servers": ["https://auth.<tunnel-domain>"] }
この構成なら、ユーザーのブラウザは許可済みの既存ホストで/authorizeを叩き、Anthropicのバックエンドは/token・/register・ディスカバリー文書をトンネル越しに取得できます。
setupコンポーネントの認証エラーを切り分ける
setupコンポーネントはOIDC JWTをフェデレーションルール経由で交換し、Tunnels APIに認証します。交換が失敗したときの疑い先は、subject(トークンの主体がルールの想定と一致するか)、audience(宛先がAPI側の期待値と一致するか)、issuer(発行元のOIDCプロバイダーが登録済みか)、JWKS(署名検証用の公開鍵セットを取得できているか)、有効期限の5点です。これは一般的なWorkload Identity Federationの失敗パターンと同じ切り分けです。MCP tunnels固有の原因は次の2つです。
- Helmチャートの既定オーディエンスは
api.anthropic.com(スキームなし)です。フェデレーションルール側がhttps://api.anthropic.comなら、api.wif.audienceをそちらに合わせます。 - 交換自体は成功したのに
403が返るなら、ルールのスコープにworkspace:manage_tunnelsが含まれていないか、ルールのサービスアカウントがそのトンネルのワークスペースに参加していません。スコープを付与するか、ワークスペースへ追加します。
Tunnels APIの全エンドポイントが要求するのは、Workload Identity Federationで得たworkspace:manage_tunnelsスコープ付きのBearerトークンで、Admin APIキーは受け付けません。旧Admin API(/v1/organizations/tunnels、スコープorg:manage_tunnels)は移行期間中は動きますが、新しいパスは/v1/tunnels、ベータヘッダーはmcp-tunnels-2026-06-22です。新しい/v1/tunnelsへ移すときは、パス・ベータヘッダー・スコープの3点を一緒に替えます。
setupがOrganization already has the maximum of 10 non-archived Tunnelsで失敗するなら、組織あたりの未アーカイブのトンネルが上限の10本に達しています。Helmでtunnel.idが空のまま、またはDocker Composeで--tunnel-idなしのままsetupを実行するたびに、新しいトンネルが作られます。
Helm環境では、setupコンポーネントはpre-installフックのJobとして動きます。失敗するとJobは調査用に残るため、kubectl logs job/mcp-tunnel-setup -n mcp-tunnelでログを読み、再試行の前に手動で削除します。
helm uninstall mcp-tunnel -n mcp-tunnel
kubectl -n mcp-tunnel delete job mcp-tunnel-setup証明書エラーとIP検証エラーを切り分ける
内側TLSでAnthropicがプロキシの証明書を拒否すると、プロキシはtls handshake failedをログに出します。見るのは、サーバー証明書の有効期限切れ、SANが*.<tunnel-domain>と一致していないこと、署名したCAがそのトンネルに登録されていないことの3点です。
期限切れなら、setupコンポーネントのsetup renew-certで、保管済みのCAが署名した新しいサーバー証明書を発行できます。APIは呼びません。有効期間の既定は2160h(90日)で、--renew-before=720hを付けると残りが30日を超える間は何もしないので、定期実行に載せても安全です。
証明書を自前のPKIで発行している場合は、次の条件にも当たります。
- サーバー証明書は、登録済みのCAが直接署名します。中間CAは挟めません。
AuthorityKeyIdentifierが、CAのSubjectKeyIdentifierと一致している必要があります。- SANには
<route>.<tunnel-domain>に合うDNS名が要ります。ワイルドカードの*.<tunnel-domain>なら全ルートを覆えます。 ExtendedKeyUsageを付けるなら、serverAuthを含めます。- 鍵はRSA 2048ビット以上かECDSA P-256以上で、署名はSHA-256以上です。
CA証明書の側にも条件があります。PEM形式で1枚のみ、8kBまで、BasicConstraintsがCA:TRUEでcritical、KeyUsageにkeyCertSignを含めます。1つのトンネルが同時に持てる有効なCAは2枚までで、この2枚枠が、止めずにCAを入れ替える余地になります。
IP検証は既定でRFC1918のプライベート範囲(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)だけを許可し、プロキシからアップストリームへの接続はIPv4限定です。マネージドKubernetesの中にはService CIDRをRFC1918の外に割り当てるディストリビューションもあるため、まず自分のクラスタのアドレスを見ます。
kubectl get svc kubernetes -n default -o jsonpath='{.spec.clusterIP}'このアドレスがプライベート範囲の外にあり、かつ正当なものなら、最も狭いCIDRをupstream.allowed_ipsに追加します。allowed_ipsはRFC1918の既定値を拡張するのではなく置き換えるため、他のアップストリームが使う範囲も一緒に書きます。
upstream:
allowed_ips:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8 # ローカルテスト用のループバック検証を丸ごと切るupstream.disable_ip_validationもありますが、allowed_ipsとは同時に指定できません。
トンネル自体が繋がらないときに見る場所
cloudflaredのログをまず見ます。よくある原因は、TUNNEL_TOKENが未設定・期限切れ・コピーミスのいずれかであることと、トンネルエッジ向けの7844番ポートのTCP/UDPをファイアウォールが塞いでいることです。トンネルエッジに届いていれば、cloudflaredのログにRegistered tunnel connectionが4行出ます。
トークンをローテーションした後は、すべてのホストでcloudflaredを新しい値で再起動します。ローテーションしても確立済みの接続は切れないため、新しい値で再デプロイして古い接続が抜けるのを待てます。
宛先は198.41.192.0/19と2606:4700:a0::/44で、7844番はTCPとUDPの両方が要ります。セットアップ時とトークンのローテーション時には、setupコンポーネントがapi.anthropic.comの443番にも出ていきます。プロキシからアップストリームへの通信は、これらとは別のホップです。許可リストを作るときは、ホップごとに宛先を分けて書きます。
cloudflaredがUDP受信バッファサイズについて警告を出すことがあります。これはQUICのチューニングに関するヒントで、エラーではありません。
一般的なMCP接続トラブルとの層の違い
トンネル特有の障害は、一般的なMCPサーバーの接続トラブルとは層が違います。疑う対象が設定ファイルや認証トークンだけでなく、ネットワークとTLSの経路そのものに広がります。
まとめ
ログに出た文字列がどの層のものかを先に決めれば、見るべき設定は1つに絞れます。