この記事は完全にGPT-5.6-solが作成したもので、人間は執筆していない。 調査から修正、動作確認までの記録には備忘録として残す価値があると考え、そのまま公開用の記事として残した。
ローカルで動く小さなMCPサーバーを、ChatGPTのSecure MCP Tunnel経由で使おうとしたところ、ChatGPTのコネクタ作成画面にてコネクター作成時に次のエラーが発生した。
{ "detail": { "type": "mcp_error", "developer_message": "Connection timed out.", "kind": "network" } }
ChatGPT側のHTTPステータスは 424 Failed Dependency。一見するとDNS、ファイアウォール、APIキー、MCP SDKのどれが原因でもおかしくない。しかし、各レイヤーを分けて調べた結果、ネットワーク自体ではなく、tunnel-client v0.0.11 のstdio転送経路とMCPプロトコルの組み合わせで要求が完了しないことが分かった。
最終的にはMCPサーバーをloopback限定のStreamable HTTPでも公開し、tunnel-client から http://127.0.0.1:37373/mcp へ転送することで解消した。
この記事は2026年8月15日時点の
tunnel-client v0.0.11と、このリポジトリの実測に基づく。将来のリリースでは挙動が変わる可能性がある。
結論
healthz、readyz、control-plane pollが成功していても、MCP要求の転送だけがタイムアウトすることはある。- ChatGPTの
424 Connection timed outだけでは、ネットワーク障害とMCPプロトコル障害を区別できない。 - MCP SDKを古くしても、今回のstdio経路の停止は解消しなかった。
tunnel-client dev proxyを使うと、ChatGPTを介さず転送部分だけを再現できる。- 今回はstdioを避け、loopback限定のStreamable HTTPへ切り替えることで、
server/discover、tools/list、tools/callまで成功した。
OpenAIの公式ガイドでも、Secure MCP TunnelはローカルまたはプライベートなMCPサーバーへ外向きHTTPSで接続し、MCP側はstdioまたはHTTPを利用できると説明されている。公開用の受信ポートを開ける必要はない。
構成
対象はBunで動く現在時刻取得MCPサーバー。依存関係は再現性のため完全固定した。
{ "packageManager": "bun@1.2.7", "dependencies": { "@modelcontextprotocol/server": "2.0.0", "zod": "4.4.3" }, "devDependencies": { "@types/bun": "1.3.14", "typescript": "7.0.2" } }
最終的な通信経路は次のようになる。
ChatGPT ↓ OpenAI-hosted tunnel endpoint tunnel-client ↓ http://127.0.0.1:37373/mcp Bun MCP server
HTTPサーバーは 127.0.0.1 のみにbindしているため、LANやインターネットへ直接公開されない。
tunnel-clientの接続状態
次の確認はすべて成功した。
tunnel-client health \ --port 8080 \ --require-control-plane-poll
Healthz: PASS Readyz: PASS Control-plane poll: PASS Result: OK
これは次のことを示す。
tunnel-clientのヘルスサーバーが動いている- OpenAIのcontrol planeをpollできている
- ローカルMCPの起動確認を通過している
ただし、実際の server/discover や tools/list が最後まで応答できることまでは保証しない。
決め手になったログ
デバッグログでは、ChatGPTから来た要求がローカルの tunnel-client に到達していた。
commands_polled=1 rpc_request_id=openai-mcp-discover
つまり、ChatGPTからOpenAIのトンネルサービスを経由してローカルPCまでの経路は生きていた。その後、stdio MCPから応答が返らず、約2分後にTTLへ到達していた。
MCP connection TTL reached
この時点で「外向きHTTPSが遮断されている」という仮説は弱くなり、tunnel-client とローカルMCP間の転送に調査対象を絞れた。
ChatGPTを外して再現する
tunnel-client dev proxy を使い、ChatGPTのコネクター作成を介さずローカルだけで転送を試した。
確認した内容は次の通り。
- MCP SDK v2のstdioサーバーへ
server/discoverを送る - 古いSDKで作ったstdioサーバーへ従来の
initializeを送る - 同じ要求をStreamable HTTPサーバーへ送る
結果は、SDK v2だけでなく古いSDKのstdio経路も停止した。一方、HTTP経路では要求が完了した。
このため、SDKのメジャーバージョンだけを原因とみなしてダウングレードする方法は採用しなかった。依存関係を下げると一時的に現象が変わる可能性はあるが、今回の再現テストでは転送経路の問題を解決できなかったからだ。
関連する報告として、tunnel-client v0.0.11 がMCP 2026-07-28 のHTTPサーバーを古い initialize でprobeする互換性問題がIssueになっている。ただし、このIssueの主症状と今回のstdio停止は完全に同一ではない。共通点は、v0.0.11と新旧MCPハンドシェイクの境界で互換性問題が起きている点である。
対処:Streamable HTTP経路を追加する
MCPツールの定義を server.ts に分離し、stdioとHTTPから同じ createServer() を利用する構成にした。
stdioのエントリーポイントは薄く保つ。
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { createServer } from "./server";
serveStdio(createServer);
HTTP側は createMcpHandler をBunのHTTPサーバーへ接続する。
import { createMcpHandler } from "@modelcontextprotocol/server";
import { createServer } from "./server";
const hostname = "127.0.0.1";
const port = 37373;
const handler = createMcpHandler(createServer);
Bun.serve({
hostname,
port,
async fetch(request) {
const { pathname } = new URL(request.url);
if (request.method === "GET" && pathname === "/healthz") {
return new Response("ok", { status: 200 });
}
if (pathname !== "/mcp") {
return new Response("Not Found", { status: 404 });
}
return handler.fetch(request);
},
});
tunnel-client のprofileではstdioコマンドではなく、ローカルURLを指定する。
config_version: 1 control_plane: base_url: "https://api.openai.com" tunnel_id: "tunnel_REPLACE_ME" api_key: "env:CONTROL_PLANE_API_KEY" health: listen_addr: "127.0.0.1:8080" admin_ui: open_browser: false mcp: server_urls: - channel: main url: "http://127.0.0.1:37373/mcp"
APIキーはprofileやリポジトリへ書かず、環境変数から渡す。
起動方法
このリポジトリでは run-tunnel-debug.sh がHTTP MCPとトンネルをまとめて起動する。
export CONTROL_PLANE_API_KEY='...' ./run-tunnel-debug.sh
正常なら次の表示になる。
MCP HTTP server is ready: http://127.0.0.1:37373/mcp Starting Secure MCP Tunnel. Keep this terminal open; press Ctrl-C to stop.
このターミナルは閉じない。公式ガイドにも、コネクターの作成時とツール呼び出し中は tunnel-client run を稼働させ続ける必要があると明記されている。
別ターミナルで確認する。
curl --fail http://127.0.0.1:37373/healthz tunnel-client health \ --port 8080 \ --require-control-plane-poll
その後、ChatGPTの開発者モードで新規アプリを作り、「接続」に「トンネル」を選んで対象トンネルを指定する。
修正後の検証結果
ローカルHTTP経路と実際のChatGPT経路で、次を確認した。
| 確認項目 | 結果 |
|---|---|
MCP /healthz |
HTTP 200 |
tunnel /healthz |
PASS |
tunnel /readyz |
PASS |
| control-plane poll | PASS |
server/discover |
HTTP 200 |
tools/list |
成功 |
tools/call get-current-time |
成功 |
最初の起動時には、HTTPサーバーがlistenするまでの数百ミリ秒だけ curl: (7) Failed to connect が表示されていた。これは本当の停止ではなく起動待ちのリトライだったため、スクリプト側で途中のエラー表示を抑制し、準備完了後だけメッセージを出すようにした。
調査するときの順番
同じ Connection timed out に遭遇したら、次の順番で確認すると切り分けやすい。
- ローカルMCP単体のhealthとMCP要求を確認する
tunnel-client health --require-control-plane-pollを確認するlogs/tunnel-client.logでcommands_polledを探すrpc_methodとレスポンス転送の有無を確認するtunnel-client dev proxyでChatGPTを外して再現する- stdio固有ならStreamable HTTPを試す
- 最後にChatGPTのコネクター作成を再試行する
commands_polled=1 があるのにMCP応答がない場合、DNSやOpenAI側への到達性だけを調べ続けるより、ローカルMCP転送とプロトコル互換性を確認した方がよい。
セキュリティ上の注意
- MCP HTTPサーバーは
0.0.0.0ではなく127.0.0.1にbindする - runtime API keyは環境変数または適切なsecret storeから渡す
- APIキー、Cookie、AuthorizationヘッダーをIssueやブログへ貼らない
--log.http-raw-unsafeは通常の調査では使用しない- 共有前にログからトンネルID、ユーザー情報、リクエスト本文を除去する
今回も詳細ログはJSON形式で保存したが、生のHTTP本文や認証ヘッダーは記録していない。
まとめ
今回の 424 Failed Dependency / Connection timed out は、トンネルそのものが到達不能だったわけではない。要求はローカルPCまで届いており、stdio転送経路でMCP応答が完了しないことが原因だった。
ヘルスチェックの成功だけで「MCPツール呼び出しも成功する」と判断せず、server/discover、tools/list、tools/call の各段階を見ることが重要だった。また、SDKをすぐダウングレードするのではなく、古いSDKでも同じ経路を試したことで、より狭い原因へ絞り込めた。
最終的な回避策は、MCPツール定義を共通化したまま、ローカル限定のStreamable HTTP経路を追加することだった。公開ポートを増やさず、ChatGPTからの実際のツール一覧取得まで確認できたので、現時点ではこの構成を利用する。









