Claude Media
MCP tunnelsに接続できないときのトラブルシューティング

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. 1

    cloudflaredのログ(外向き接続)

    TUNNEL_TOKENと、トンネルエッジへの7844番ポートの通信を調べます。No ingress rules were definedもここに出ます。

  2. 2

    プロキシのログ(内側TLS)

    tls handshake failedが出たら、証明書の有効期限・SAN・登録済みCAの3点を見ます。

  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は既存の公開ホスト名に残せます。

OAuthの手順

認可サーバーだけをトンネルに載せる

  1. 1

    認可サーバー用のルートをプロキシに追加する

    routes:
      mcp: http://your-mcp-server:8080
      auth: http://your-auth-server:8080

    routesの編集後は、docker compose restart mcp-proxy(Helmならhelm upgrade)でプロキシを再起動します。

  2. 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. 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つに絞れます。

この記事を共有:XはてブLinkedIn
MCP をもっと見る →