MCP tunnelsクイックスタート — 最初のトンネルを立てる
Docker ComposeとopensslだけでMCP tunnelsの動作確認環境を組む手順を、証明書生成からClaudeからの呼び出しまで実行単位で確認します。
この手順で何ができるようになるか
Docker Composeだけで、プライベートネットワーク内のMCPサーバーにClaudeから到達できる最小構成を組みます。手動の資格情報登録を使う最短経路なので、Workload Identity Federationの事前設定は不要です。ゴールは、サンプルのMCPサーバーがhttps://echo.<自分のトンネルドメイン>/mcpとしてClaudeから呼べる状態を作ることです。手順は全部で6つで、証明書の生成からClaudeへの実際の呼び出しまでを一気に通します。本番ホストの構築やトークン・証明書の更新運用まで踏み込む場合は、サーバーごとに許可範囲をどう決めるかをまとめたMCPセキュリティガイドもあわせて参照してください。
始める前に必要なもの
- アウトバウンドのインターネット接続があるマシンで動く、DockerとDocker Compose
- Claude Consoleで「Manage tunnels」権限を持つロール(組織の管理者とオーナーは既定で持っています。権限のないロールは一覧と詳細を読めるだけです)
- OpenSSL 1.1.1以降(macOSと大半のLinuxディストリビューションにはプリインストール済み。Windowsは別途インストールし
opensslコマンドにPATHを通す)
以降のコード例はmacOS/Linuxのシェル構文で統一します。WindowsではPowerShellの構文に読み替えてください。
MCP tunnelsはResearch Preview段階の機能で、利用には申請が必要です。以降はアクセス権がある前提で進めます。
トンネルは外向きの接続だけで成り立ち、受け付け側のポートは開けません。cloudflaredはCloudflareのトンネルエッジ(198.41.192.0/19と2606:4700:a0::/44、ポート7844のTCPとUDP)へ自分からつなぎにいきます。Anthropicからのリクエストは、その確立済みの接続を逆向きに通って届きます。Cloudflareが見られるのは接続元のIPアドレス、cloudflaredのホスト指紋、接続の時間とバイト量、割り当てられたサブドメインです。リクエストとレスポンスの中身は読めません。
今回の手動フローはSetup componentを使わないため、api.anthropic.com(443番ポート)への到達性は不要です。cloudflaredだけはどちらのフローでも常に動きます。ファイアウォールが7844を塞いでいるとトンネルは接続できないので、起動後はまずdocker compose logs cloudflaredを見ます。
最初に頭に入れておく数値
外向きポート
7844
TCPとUDPの両方。cloudflaredからトンネルエッジへ
有効なトンネル
10本
組織あたりの上限
有効なCA証明書
2枚
1トンネルあたり。入れ替えを止めずに行うための枠
サーバー証明書
90日
手順3の`-days 90`。更新は自分で行う
手順1: Consoleでトンネルを作成する
Claude Consoleのサイドバーで「Manage > MCP tunnels」を開き、「New tunnel」をクリックして名前を付けます。この手順では「Set up programmatic access」はオフのままにします(手動の資格情報を使うため)。作成後にトンネルを開き、「Connection」セクションからDomain(abcd1234.tunnel.anthropic.comのような形式)と「Token」(目のアイコンをクリックしてコピー)の2つの値を控えます。トンネルは画面で選択中のワークスペースに属します。別のワークスペースで使うなら、作成前に切り替えておきます。
このトークンはcloudflaredのアウトバウンド接続を認証する資格情報そのものです。この段階ではローカルの環境変数に入れるだけで構いませんが、シェル履歴やCIログにそのまま残さないよう、以降の手順でも取り扱いには注意します。
手順2: 作業ディレクトリと環境変数を用意する
mkdir -p mcp-tunnel/{config,data}
cd mcp-tunnel
export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE # 手順1で控えた値
export TUNNEL_TOKEN='eyJ...' # 手順1で控えた値手順3: CAとサーバー証明書をopensslで生成する
Proxyは、自分が管理するCAで署名したサーバー証明書を使って内部TLSを終端します。Proxy・cloudflared・Setup componentの役割分担はMCP tunnelsのアーキテクチャで解説しています。CA証明書と、TUNNEL_DOMAINのワイルドカードをSANに含むサーバー証明書の両方をopensslで作ります。
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout data/ca.key -out data/ca.crt \
-days 3650 -subj "/CN=mcp-tunnel-ca" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash"
cat > data/tls.ext <<EOF
subjectAltName = DNS:${TUNNEL_DOMAIN},DNS:*.${TUNNEL_DOMAIN}
authorityKeyIdentifier = keyid,issuer
extendedKeyUsage = serverAuth
EOF
openssl req -newkey rsa:2048 -nodes \
-keyout data/tls.key -out /tmp/server.csr \
-subj "/CN=${TUNNEL_DOMAIN}"
openssl x509 -req -in /tmp/server.csr \
-CA data/ca.crt -CAkey data/ca.key -CAcreateserial \
-out data/tls.crt -days 90 -extfile data/tls.ext
chmod 644 data/tls.key最後のchmod 644を飛ばすと、Proxyコンテナは非rootで動くためopen /data/tls.key: permission deniedで起動に失敗します。生成したdata/ca.crtをConsoleのトンネル詳細ページで「Add certificate」からアップロードすると、トンネルのステータスがActiveに変わります。
自己署名の1枚証明書ではなく、CAとサーバー証明書の2段構成にする理由は、Anthropicが内側のTLSを、トンネルに登録したCA証明書に照らして検証するからです。サーバー証明書は登録済みのCAが直接署名したもので、中間CAは挟めません。CAを一度登録しておけば、90日ごとの更新でConsoleに触る必要はありません。
手順3の各行は、リファレンスの証明書要件にそのまま対応しています。
| 公式の要件 | 手順3での指定 |
|---|---|
CA: CA:TRUEをcriticalで持つ | 手順3での指定basicConstraints=critical,CA:TRUE |
CA: SubjectKeyIdentifierがある | 手順3での指定subjectKeyIdentifier=hash |
CA: KeyUsageにkeyCertSignを含む | 手順3での指定keyUsage=critical,keyCertSign,cRLSign |
| サーバー: 登録済みCAが直接署名する | 手順3での指定-CA data/ca.crt -CAkey data/ca.key |
サーバー: AuthorityKeyIdentifierがCAのSKIと一致する | 手順3での指定authorityKeyIdentifier = keyid,issuer |
サーバー: SANに<route>.<tunnel-domain>を含む | 手順3での指定DNS:*.${TUNNEL_DOMAIN}(ワイルドカードで全ルートをカバー) |
サーバー: ExtendedKeyUsageがあればserverAuthを含む | 手順3での指定extendedKeyUsage = serverAuth |
Consoleへアップロードする前に、手元で署名関係とSANを確かめられます。次のコマンドはOKを返し、AuthorityKeyIdentifierの値はCA側のSubjectKeyIdentifierと同じ16進列になります。-extオプションはOpenSSL 3.0以降のもので、それ以前のバージョンでは-text -nooutで同じ項目を確かめます。
openssl verify -CAfile data/ca.crt data/tls.crt
openssl x509 -in data/tls.crt -noout -ext subjectAltName,authorityKeyIdentifier
openssl x509 -in data/ca.crt -noout -ext subjectKeyIdentifierクイックスタートのコマンドはCAを3650日(約10年)、鍵をRSA 2048で作ります。リファレンスでは、setupコンポーネントが作るのは5年有効のECDSA P-256のCAと、RSA 4096のサーバー証明書だとされています。どちらも要件(RSA 2048以上、またはECDSA P-256以上)の範囲内です。
手順4: サンプルMCPサーバーとProxy設定を書く
動作確認用に、helloツールを1つだけ持つMCPサーバーをPythonで書きます。FastMCPのstreamable-httpトランスポートを使うため、Proxyから見るとエンドポイントは/mcpパスで待ち受けます。このサンプルサーバーには認証を一切実装していません。トンネルが運ぶのは暗号化された通信だけで、アップストリームのMCPサーバーへの認証は行いません。実際の業務システムをつなぐときは、他のリモートMCPサーバーと同じようにOAuthやベアラートークンをサーバー側に用意します。
cat > hello_server.py <<'EOF'
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-server", host="0.0.0.0", port=9000)
@mcp.tool()
def hello(name: str = "world") -> str:
"""Say hello to someone."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
EOFProxyの設定ファイルはroutesにサブドメインとアップストリームのマップを書きます。routesは文字列のマップで、リスト形式で書くとcannot unmarshal !!seq into map[string]stringで起動時に落ちます。値はscheme://host:portの形が必須で、ポートは省略できません。パスを含めると設定の読み込み時にinvalid upstream (must be scheme://host:port)で弾かれます。
cat > config/mcp-proxy.yaml <<EOF
listen_addr: ":8080"
tunnel_domain: ${TUNNEL_DOMAIN}
tls:
cert_file: /data/tls.crt
key_file: /data/tls.key
routes:
echo: http://hello-mcp:9000
EOF/mcpというパスがここに現れないのは、プロキシがホスト名だけで振り分け、リクエストのパスとクエリをそのままアップストリームへ渡すためです。パスはConsole側の「Path」欄で指定します。
手順5: Composeファイルを書いて起動する
Proxy・cloudflared・サンプルMCPサーバーの3コンテナをComposeでまとめて定義します。
cat > docker-compose.yaml <<'EOF'
services:
mcp-proxy:
image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013
volumes:
- ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro
- ./data:/data:ro
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0
command: tunnel --no-autoupdate run --url http://localhost:8080
environment:
- TUNNEL_TOKEN
network_mode: "service:mcp-proxy"
restart: unless-stopped
hello-mcp:
image: python:3.13-slim
working_dir: /app
volumes:
- ./hello_server.py:/app/hello_server.py:ro
command: sh -c "pip install --quiet mcp && python hello_server.py"
restart: unless-stopped
EOF
docker compose up -d
docker compose logs mcp-proxy | grep "route configured"
docker compose logs cloudflared | grep "Registered tunnel connection"echo用のroute configuredが1行、Registered tunnel connectionが4行出れば起動成功です。コンテナの起動には数秒かかるため、空振りしたらログ確認コマンドをもう一度実行してみます。cloudflaredがUDPの受信バッファサイズについて警告を出すことがありますが、これはQUICのチューニングに関する注意であってエラーではありません。
手順6: Claudeから呼び出して確認する
ConsoleのManaged Agentsにある「Sessions」でセッションを作成し、エージェントピッカーで「Create new agent」を選びます。「+ MCP Server」のドロップダウンでは、同じワークスペースのトンネルが公開コネクタのカタログより上に並びます。作成したトンネルを選び、Subdomainにecho、「Path」にmcpを入れてください。「Resolves to」の行にhttps://echo.<自分のトンネルドメイン>/mcpと表示されるので、打ち間違いはここで分かります。設定を終えたら、セッションに次のように話しかけます。
Use the hello tool to greet tunnel.
ツール呼び出しとその結果が返れば、トンネルは端から端まで通っています。Consoleで作ったトンネルをclaude.aiのコネクタとして使うことはできません。使えるのはManaged AgentsのセッションとMessages APIです。
よくあるつまずき
リクエストが途中で落ちる場所は3つの層に分かれ、外側から順に調べます。公式のトラブルシューティングも、外向き接続、内側のTLS、ルーティングの順に診断する構成です。症状ごとの詳しい切り分けはMCP tunnelsのトラブルシューティングにあります。
症状を3つの層に分けて見る
1. cloudflaredの外向き接続
TUNNEL_TOKENの欠落・期限切れ・コピー間違いか、7844のTCP/UDPがファイアウォールで塞がれているかを疑います。呼び出し側がHTTP 500を受け、cloudflaredがNo ingress rules were definedを出すなら、接続先がありません。cloudflaredのサービスに--url http://localhost:8080とnetwork_mode: "service:mcp-proxy"の両方があるか確かめます。2. 内側のTLS
プロキシが
tls handshake failedを出したら、サーバー証明書の期限、SANが*.<自分のトンネルドメイン>と一致しているか、署名したCAがこのトンネルに登録済みかを順に見ます。open /data/tls.key: permission deniedで起動に失敗するのは、鍵が0600のままだからです。chmod 644で直ります。3. ルーティングとIP検証
no route for hostは、tunnel_domainがトンネル詳細ページの値と違うか、mcp-proxy.yamlの編集後に再起動していません。docker compose restart mcp-proxyで再起動します。is not a private addressは、アップストリームがRFC1918の範囲外に解決されたときに出ます。
この3層に当てはまらない症状が2つあります。1つ目は、curl https://<proxy>:8080がwrong version numberで失敗する場合です。リスナーは平文のWebSocketで、TLSはそのストリームの内側で完結するため、想定どおりの挙動です。動作確認はcurlではなく、Managed AgentsかMessages APIで行います。
2つ目は、エージェントの「+ MCP Server」にトンネルが出てこない場合です。ピッカーに載るのは、有効な証明書が1枚以上あり、セッションと同じワークスペースにあるトンネルだけです。トンネル一覧に「Needs certificate」と出ていれば、CA証明書が未登録です。
アップストリームのIPが拒否されるときは、マネージドKubernetesの一部でService CIDRがRFC1918の外にあります。該当する場合はupstream.allowed_ipsに、実際のアドレスを含む最小のCIDRを書きます。このフィールドは既定値を上書きするため、他のアップストリームが使うプライベートレンジも書き漏らさないようにします。ループバックの127.0.0.0/8を足してよいのはローカルテストだけで、0.0.0.0/0はSSRF保護を丸ごと外します。
動作確認後の後片付けと証明書の入れ替え
検証が終わったらdocker compose downでコンテナを止めます。トンネル自体をもう使わないなら、Consoleの一覧で行のメニューから「Archive」を選びます。アーカイブすると接続の受け付けが即座に止まり、元には戻せません。
data/に旧トークンや鍵を残したままにしたくない場合は、トンネル詳細の「Rotate token」で現在のトークンを無効にして新しいものを発行します。トークンの表示とローテーションは、そのつど組織のCompliance APIの活動ログに記録されます。ローテーションしても確立済みのcloudflared接続は切れないので、新しい値で再デプロイし、古い接続が自然に終わるのを待てます。
トークンとTLS秘密鍵の両方が攻撃者の手に渡ると、プロキシのなりすましでリクエストの中身が読まれるおそれがあります。このクイックスタート用のmcp-tunnel/ディレクトリを、別の検証環境へそのままコピーして使い回すのは避けます。
90日ごとのサーバー証明書の更新は利用者の責任で、CAを使い回す限りConsoleでの再登録は要りません。CA自体を入れ替えるときは、1トンネルに有効なCA証明書を2枚まで置けるので、次の順で止めずに進めます。
CA証明書をダウンタイムなしで入れ替える
- 1
新しいCAを古いCAと並べて登録する
トンネル詳細の「Certificates」で「Add certificate」を押し、新しい
ca.crtを追加します。PEM形式で8 kB以下のファイルだけが受け付けられ、秘密鍵のデータは弾かれます。 - 2
新しいCAで署名した鍵ペアでプロキシを再デプロイする
手順3の
openssl x509 -reqを新しいCAで署名し直し、data/tls.crtとdata/tls.keyを差し替えてプロキシを再デプロイします。 - 3
疎通を確かめる
Managed Agentsのセッションで
helloツールをもう一度呼び、結果が返ることを見ます。 - 4
古いCAを「Revoke」する
古い証明書の行の「Revoke」を押します。失効した証明書は「Revoked」のバッジ付きで一覧に残ります。
次のステップ
このクイックスタートで検証したのは手動の資格情報経路です。本番運用への移行先は、デプロイ先とセットアップ時の認証方法の組み合わせで決まります。Kubernetesクラスタに載せるならHelmチャート、単一ホストや簡易な検証で足りるならDocker Composeのハードニング済み構成を選びます。
証明書とトークンの扱いはどう変わるか
手動(今回)
トークンはConsoleからコピーし、証明書はopensslで自分で作って、CAだけをConsoleに登録します。Workload Identity Federationの設定は要りません。更新も自分で行います。
プログラム的アクセス
KubernetesクラスタやクラウドIAM、SPIFFEのようなOIDC発行者があるときの経路です。Setup componentがトークンの取得、CAとサーバー証明書の生成、CAの登録まで行います。setup renew-certはAPIを呼ばず、--renew-before=720hを付けると残り30日を超える間は何もしないので、定期実行に向きます。
自分のMCPサーバーはdocker-compose.yamlに追加し、config/mcp-proxy.yamlにルートを足します。docker compose restart mcp-proxyで再起動すれば、サンプルを業務システムに差し替えられます。
Managed Agentsのセッションのほかに、トンネル先のMCPサーバーはMessages APIのMCPコネクタからも呼べます。mcp_serversのurlにhttps://echo.<自分のトンネルドメイン>/mcpを渡し、ツール側でmcp_toolsetを指定します。リクエストはトンネルを作ったワークスペースのAPIキーで送ります。Agent SDKでのMCP接続の組み方はAgent SDK側からMCP接続を組むにあります。