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でMCP tunnelsを管理できるロール
- OpenSSL 1.1.1以降(macOSと大半のLinuxディストリビューションにはプリインストール済み。Windowsは別途インストールし
opensslコマンドにPATHを通す)
以降のコード例はmacOS/Linuxのシェル構文で統一します。WindowsではPowerShellの構文に読み替えてください。
MCP tunnelsはResearch Preview段階の機能で、利用には申請が必要です。以降はアクセス権がある前提で進めます。
作業マシンから外向きに開いている必要がある経路も、事前に確認しておくと手戻りが減ります。今回の手動フローはSetup componentを使わないため、api.anthropic.comへの到達性は不要です。一方でcloudflaredはどちらのフローでも常に動くコンポーネントで、Cloudflareのトンネルエッジ(ポート7844、TCPとUDP両方)への到達を必要とします。社内ネットワークの制限が厳しいプロキシ配下やVPN接続下では、このポートがブロックされているだけでdocker compose logs cloudflaredにコネクション確立のログが一切出ないままハングします。まず制限の少ないネットワークで最初の動作確認をするほうが、原因の切り分けが早く済みます。
手順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で動くためdata/tls.key: permission deniedで起動に失敗します。生成したdata/ca.crtをConsoleのトンネル詳細ページで「Add certificate」からアップロードすると、トンネルのステータスがActiveに変わります。
自己署名の1枚証明書ではなく、わざわざCAとサーバー証明書の2段構成にしているのは、Anthropicのバックエンドが検証するのは「登録済みのCAが署名したものかどうか」だからです。CAさえ登録しておけば、90日ごとの更新はサーバー証明書だけを作り直せば済み、CA自体を毎回Consoleに登録し直す必要がありません。
手順4: サンプルMCPサーバーとProxy設定を書く
動作確認用に、helloツールを1つだけ持つMCPサーバーをPythonで書きます。FastMCPのstreamable-httpトランスポートを使うため、Proxyから見るとエンドポイントは/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にサブドメインとアップストリームのマップを書きます。ここはマップ形式が必須という実装上の落とし穴があり、うっかりリスト形式で書くとcannot unmarshal !!seq into map[string]stringで起動時に落ちます。
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手順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を設定すると、Console側に実際に解決されるURL(https://echo.<自分のトンネルドメイン>/mcp)がその場で表示されるので、打ち間違いはここで気づけます。設定を終えたら、セッションに次のように話しかけます。
Use the hello tool to greet tunnel.
ツール呼び出しとその結果がそのまま返ってくれば、トンネルは端から端まで動作確認済みです。
よくあるつまずき
ここでは代表的な5つのつまずきを挙げます。原因を3層構造(cloudflaredの外向き接続・内側TLS・アップストリームへのルーティング)から順に切り分ける体系立った手順はMCP tunnelsのトラブルシューティングにまとめています。
curl https://<proxy>:8080がwrong version numberで失敗する — これは想定どおりの挙動です。リスナーは平文のWebSocketで、TLSはそのストリームの内側で完結します。動作確認はcurlではなく、Managed AgentsかMessages経由で行います。- Proxyが
no route for hostをログに出す —tunnel_domainがトンネル詳細ページに表示された値と一致していないか、config.yamlを編集した後にProxyを再起動し忘れています。docker compose restart mcp-proxyで再起動します。 - cloudflaredが
No ingress rules were definedとログに出しHTTP 500になる — cloudflaredにローカルの接続先が無い状態です。--url http://localhost:8080とnetwork_mode: "service:mcp-proxy"の両方がcloudflaredサービスの定義に入っているか確認します。 - アップストリームのIPが
is not a private addressで拒否される — Proxyは既定でRFC1918のプライベートアドレス空間にしか接続しません。マネージドKubernetesの一部はService CIDRがこの範囲外にあるため、該当する場合はupstream.allowed_ipsに自社のCIDRを追記します(このフィールドは既定値を上書きするので、他のプライベートレンジも書き漏らさないよう注意します)。 - Anthropicがプロキシの証明書を拒否し
tls handshake failedが出る — サーバー証明書の有効期限が切れていないか、SANが*.<自分のトンネルドメイン>と一致しているか、署名したCAがこのトンネルに登録済みかの3点を順に確認します。手順3でtls.extのsubjectAltNameを書き間違えると、ここで初めて表面化します。
動作確認後の後片付け
検証が終わったらdocker compose downでコンテナを止めます。トークンやTLS秘密鍵をローカルに残したまま放置したくない場合は、Console側でトンネルトークンをローテーションしておくと、data/ディレクトリに残った旧トークンが漏れても実害はありません。同じ理由で、このクイックスタート用に作ったmcp-tunnel/ディレクトリそのものを、別の検証環境へまるごとコピーして使い回すのも避けたほうが安全です。トンネルごとに証明書を毎回作り直す前提にしておけば、1つのトークン漏えいが他のトンネルにまで波及することもありません。
次のステップ
このクイックスタートで検証したのは手動の資格情報経路です。本番運用への移行先は、デプロイ先とセットアップ時の認証方法の組み合わせで決まります。Kubernetesクラスタに載せるならHelmチャート、単一ホストや簡易な検証で足りるならDocker Composeのハードニング済み構成を選びます。セットアップの認証は、KubernetesクラスタやクラウドIAM、SPIFFEのようなOIDC発行者をすでに持っているならプログラム的アクセスを、まだ持っていない場合や単純なテストの間は今回と同じ手動を選びます。
docker-compose.yamlに自分のMCPサーバーを追加してconfig/mcp-proxy.yamlにルートを足すところから始めれば、今回のサンプルサーバーを実際の業務システムへ差し替えられます。Managed Agentsのセッションだけでなく、Agent SDK側からMCP接続を組む場合も、トンネル経由のURLを他のリモートMCPサーバーと同じ形式でmcp_serversに渡せます。