openai/tunnel-client · 上手攻略
- 仓库:openai/tunnel-client
- 链接:https://github.com/openai/tunnel-client
- 分类:agent / engineering(skill 形态 · Go 实现的运维型 CLI)
- 作者:spark
- 更新:2026-11-04
一、它是什么
openai/tunnel-client 是 OpenAI Secure MCP Tunnel 体系里的"客户侧客户端"——一个由你(企业/开发者)自己运行的守护进程,用 Go 写的 CLI,同时附带一个可嵌入的 Go SDK。
它的核心工作只有一句话:让你机器上跑着的私有 MCP 服务器(localhost / 内网 / K8s 集群里),能够被 OpenAI 托管的产品(ChatGPT、Codex、Responses API、AgentKit)调用,而你的 MCP 服务器本身始终不暴露在公网上。
技术形态是一个出站长轮询(outbound long-poll)代理:
ChatGPT / Codex / Responses API
│ HTTPS (公网入口在 OpenAI 侧)
▼
OpenAI 隧道服务 (tunnel-service)
│ GET /v1/tunnels/{tunnel_id}/poll ← 客户端主动拉取命令
│ POST /v1/tunnels/{tunnel_id}/response ← 客户端回传结果
▼
tunnel-client (运行在你自己机器上, 出站连接)
│ Streamable HTTP / stdio / in-memory
▼
你的私有 MCP 服务器
关键点:入口只有出站。你的服务器不需要开新的入站防火墙规则、不需要公网 IP、不需要 ngrok/frp/cloudflared 这类反向隧道产品。控制平面请求只发往 https://api.openai.com(若配置了 mTLS 客户端证书则自动切到 https://mtls.api.openai.com)。
仓库定位是"研究/早期"(卡片标 maturity=research),但版本节奏很勤:最新 release 为 v0.0.19(v0.0.x 阶段,功能已相当完整);Apache-2.0 许可;最近一次提交 2026-11-03。Go 语言。
二、它解决什么问题
问题场景:你有一个内部 MCP 服务器(比如接了 Jira、数据库、内部知识库的 MCP server),跑在笔记本或内网机器上。你想让 ChatGPT 或 Codex 用上它,但 IT/安全团队明确不批新的入站端口、不批公网暴露。
传统解法都不理想:
| 方案 | 问题 |
|---|---|
ngrok / frp / cloudflared 隧道 |
需要你在自己的基础设施里再运维一条公网通道,且把 MCP server 的攻击面直接摊到公网 |
| 自建 ngrok 类服务 | 同上,且是重复造轮子 + 安全审批更难过 |
| 公网 MCP 服务器 + OAuth | 要自己实现 OAuth / PRMD / 资源元数据发现全套,工作量大 |
| 容器端口映射到公网 | 同"暴露到公网"的问题 |
tunnel-client 的答案是:公网入口由 OpenAI 托管(它已经是被信任的一方),你的这一侧只做一条出站 HTTPS 长轮询。另外它的官方说法是 "tunnel-client is not an installer"——它是跑在本地转发流量的客户端。
顺带解决的运维问题:
- 运维可见性:内置 admin/health server(/healthz、/readyz、/metrics)+ 一个 admin UI(/ui),可以在任何 API 调用依赖它之前先确认就绪。
- 凭据分离:runtime key(CONTROL_PLANE_API_KEY,只要 Tunnels Read + Use)和 admin key(OPENAI_ADMIN_KEY,用于 tunnels CRUD)严格分离,admin key 不该喂给长期运行的守护进程。
- OAuth 支持:MCP 服务器的 OAuth Protected Resource Metadata 会被自动拉取并注册目标,authorization_servers[0] 是唯一真源。
- 内嵌 Harpoon MCP server:给内部工具用的、带标签与白名单的出站 HTTP 客户端(GET/POST/PUT),固定目标、校验标识符、不跟随重定向。
三、快速安装
macOS(官方支持的路径)
brew install openai/tools/tunnel-client
tunnel-client --version
tunnel-client help quickstart
⚠️ 重要:直接下载的 release ZIP 目前没有做 Apple 公证(notarized),会被 Gatekeeper 拦。README 明确要求:不要用 xattr -d com.apple.quarantine、spctl --master-disable 或 "Open Anyway" 绕过,请走官方 tap。Formula 会一并装上匹配的 tunnel-client、内置的 cloudflared 和 companion manifest,同时装好 bash/zsh/fish 补全。
Linux / Windows
从 release 页面拿归档或容器镜像:
- Release 语义版本 tag 形如
v0.0.19;源码归档把版本写进pkg/version/VERSION,所以go build出来的二进制也报同样的版本号。 - 官方镜像发布在
ghcr.io/openai/tunnel-client(Linux amd64 / arm64),自带匹配的 companion。生产环境请 pin 精确的vX.Y.Ztag 或 digest,不要用latest。 - Release 归档里捆绑了固定版本的
cloudflared(2026.10.2),覆盖 linux amd64/arm64、macOS amd64/arm64、windows amd64/arm64。可以用tunnel-client cloudflared version打印捆绑的 pin。
从源码构建
git clone https://github.com/openai/tunnel-client.git
cd tunnel-client
make admin-ui # 刷新内嵌 admin UI 资源;docs-only checkout 可跳过
go build -o bin/tunnel-client ./cmd/client
./bin/tunnel-client --version
需要 Go 工具链。开发校验用 go build ./... + go test ./...,并发相关用 go test -race ./...(测试里用了 t.Parallel(),每个用例要有自己的 fixture)。
四、核心用法
4.1 先把三样东西准备好
| 值 | 来源 | 用途 |
|---|---|---|
CONTROL_PLANE_TUNNEL_ID |
platform.openai.com 的 Tunnels 页面,或 tunnel-client admin tunnels create |
隧道标识,格式 tunnel_<32位小写字母数字> 或 tunnel_<4位命名空间>_<32位> |
CONTROL_PLANE_API_KEY |
Runtime API keys 页面 | 守护进程用的运行时密钥(需 Tunnels Read + Use) |
OPENAI_ADMIN_KEY |
Admin API keys 页面 | 只用于 admin tunnels list/create/update/delete,不要喂给长期运行的守护进程 |
控制台页面: - Tunnels 管理:https://platform.openai.com/settings/organization/tunnels - Runtime API keys:https://platform.openai.com/settings/organization/api-keys - Admin API keys:https://platform.openai.com/settings/organization/admin-keys - ChatGPT 连接器设置:https://chatgpt.com/#settings/Connectors
4.2 最短路径:内置演示 MCP(无需自备 MCP 服务器)
先跑通链路再接真实服务:
export CONTROL_PLANE_API_KEY="sk-..."
export CONTROL_PLANE_TUNNEL_ID="tunnel_0123456789abcdef0123456789abcdef"
tunnel-client run --embedded-stateless-mcp-stub --health.listen-addr 127.0.0.1:0
内置了 server_info、echo、uppercase 三个演示工具。--embedded-stateless-mcp-stub 会以无状态方式处理 initialize 与 notifications/initialized,不签发 MCP session ID。另一种 --embedded-mcp-stub 保留旧版有状态初始化兼容行为。一次运行只能选一种模式。
带健康检查 URL 文件的写法(推荐,便于其他工具读取实际端口):
health_url_file="$(mktemp "${TMPDIR:-/tmp}/tunnel-client-health.XXXXXX.url")"
tunnel-client run --embedded-mcp-stub \
--control-plane.tunnel-id tunnel_0123456789abcdef0123456789abcdef \
--health.listen-addr 127.0.0.1:0 \
--health.url-file "$health_url_file"
curl -fsS "$(cat "$health_url_file")/readyz"
open "$(cat "$health_url_file")/ui"
--health.listen-addr 127.0.0.1:0 让 OS 分配临时端口;也可以用 HEALTH_URL_FILE / --health.url-file 环境变量。
4.3 标准路径:stdio MCP 服务器
tunnel-client profiles samples list
tunnel-client profiles samples show sample_mcp_stdio_local
tunnel-client init --sample sample_mcp_stdio_local \
--profile local-stdio \
--tunnel-id tunnel_0123456789abcdef0123456789abcdef \
--mcp-command "python /path/to/server.py"
tunnel-client doctor --profile local-stdio --explain
tunnel-client run --profile local-stdio
doctor --explain 会在启动前告诉你缺什么。init 也可写成 --profile-file ./profiles/local-stdio.yaml 形式。
4.4 远程 HTTP MCP 服务器
tunnel-client init --sample sample_mcp_remote_no_auth \
--profile remote-http \
--tunnel-id tunnel_0123456789abcdef0123456789abcdef \
--mcp-server-url https://mcp.example.com/mcp
4.5 企业出站代理 + 私有 PKI
export HTTPS_PROXY="http://proxy.internal.example.com:8080"
export ENTERPRISE_CA_BUNDLE="/etc/ssl/certs/proxy-root.pem"
tunnel-client init --sample sample_mcp_enterprise_proxy \
--profile corp-proxy \
--tunnel-id tunnel_0123456789abcdef0123456789abcdef \
--mcp-server-url https://mcp.internal.example.com/mcp
4.6 作为 Go SDK 嵌入进程内 MCP 服务器
go get github.com/openai/tunnel-client
import (
"context"
"github.com/modelcontextprotocol/go-sdk/mcp"
tunnelclient "github.com/openai/tunnel-client"
)
ctx := context.Background()
server := mcp.NewServer(&mcp.Implementation{Name: "my-server", Version: "1.0.0"}, nil)
serverTransport, tunnelTransport := mcp.NewInMemoryTransports()
go server.Run(ctx, serverTransport)
client, err := tunnelclient.New(tunnelclient.Config{
TunnelID: "tunnel_0123456789abcdef0123456789abcdef",
APIKey: apiKey,
}, tunnelTransport)
if err != nil {
return err
}
return client.Run(ctx)
用 MCP SDK 的 in-memory transport,MCP 服务器无需绑端口、也无需 stdio。可运行示例在 examples/go-sdk-inmemory。
4.7 运维与生命周期命令
tunnel-client runtimes create|connect|list|status|stop|rm # 原生别名 + 本地运行时监管
tunnel-client admin-profiles list|set|delete # 保存的 admin-key profile
tunnel-client admin tunnels get <id> # 只读元数据查询
tunnel-client health --url http://127.0.0.1:8080 --json # 健康检查
tunnel-client codex plugin install|uninstall|export # Codex 插件
tunnel-client dev mcp-stub # 本地演示 MCP + OAuth 元数据
tunnel-client dev proxy # 本地控制平面 + 客户端,做集成测试
Codex 插件流程(原生命令树是 codex plugin,plugin codex 是兼容别名):
tunnel-client codex plugin install
tunnel-client help plugin
tunnel-client runtimes list
tunnel-client codex plugin uninstall
4.8 追踪与请求 ID 转发
追踪默认关闭,需显式开启:
# W3C trace context 通过 MCP params._meta 透传(stdio 与 Streamable HTTP)
tunnel-client run --mcp.forward-trace-context ...
# 本地 span 导出
tunnel-client run --tracing.enabled --tracing.file=/private/new-trace.jsonl
⚠️ 完整链路追踪还要求服务队列 handoff 与调用方父级传播都已就绪,否则 trace 是不完整的。
请求 ID 转发默认开启(MCP HTTP 部分),可扩展范围:
--forward-request-id=mcp,harpoon # 加上 Harpoon REST 请求
--forward-request-id=mcp,oauth # 加上符合条件的排队 OAuth 发现请求
--forward-request-id=none # 关闭
等价环境变量 FORWARD_REQUEST_ID 或 YAML 顶层 forward_request_id。
五、典型适用场景
- 企业内网 MCP → ChatGPT 连接器:内部 IT 系统的 MCP server 在 VPC 里,ChatGPT 需要调用它,安全团队不批入站规则。典型场景:内部工单系统、代码搜索(Sourcegraph)、数据库只读查询网关。
- 本地开发机的 MCP → Codex:开发者笔记本上跑着自研 MCP 工具,Codex CLI 在同一台机器上直接调用,全程不出网。
- K8s 集群内的 MCP → Responses API:把集群内的 agent 工具以 Responses API 的 tool 形式暴露给模型。
- CI / 私有网络里的 MCP 接入 AgentKit:AgentKit 需要一个稳定的、可被托管侧调用的 MCP 端点。
- 合规场景的"零入站"证明:安全评审时,"我们的 MCP server 从未接受过任何入站连接"是一个非常好交代的架构事实。
六、坑与注意
按 lessons 的三段式(现象 / 影响 / 修复)列,每条给出可操作动作。
坑 1:stdio 模式下同一 tunnel ID 只能有一个活跃实例
现象:用 --mcp.command / MCP_COMMAND 部署 stdio MCP 时,多个 tunnel-client 实例共享同一 tunnel ID。每个实例会拉起独立的 MCP 子进程,初始化和后续请求可能打到不同的子进程上。
影响:状态型 MCP server 会出现"初始化成功但请求落到另一个子进程"的诡异 bug,且官方明确不支持这种配置,重启期间的重叠也算。
修复:启动新实例前先停掉旧的,或给并行实例分配不同的 tunnel ID。
坑 2:MCP 子进程被替换后,调用方需要重新 initialize
现象:默认情况下 tunnel-client 会在 stdio 子进程意外退出、stdout EOF 或 stdin 写失败后自动重启它。进行中的调用可能失败。
影响:老的调用方不会自动重建会话,重试时会拿到 mcp_initialization_required。
修复:调用方(含 agent)负责重新 initialize。若你用外部 supervisor 接管整个客户端,请设 --mcp.stdio-failure-policy=exit(或 MCP_STDIO_FAILURE_POLICY=exit / YAML mcp.stdio_failure_policy: exit),让客户端在这些故障时以非零码退出,而不是静默换子进程。⚠️ 该 flag 不被旧版本识别,降级前必须移除。
坑 3:不要用 nohup / disown 当监管方式
现象:README 明确说不要用 nohup 或 disown 作为 tunnel-client 的监管路径。
影响:进程会被当成孤儿,崩溃后不会重启,且和 runtimes 管理的运行时抢同一个 tunnel。
修复:前台守护用 tunnel-client run ...;长期运行时用 tunnel-client runtimes connect ...,然后必须用 tunnel-client runtimes status <alias> 确认(需要机器可读字段时加 --json,看 process_running / healthy / ready),只有在 status 显示运行时在跑且健康时才报告成功。
坑 4:Homebrew 服务与 CLI 运行时不能同时开
现象:brew services start openai/tools/tunnel-client 和 tunnel-client runtimes connect 是两套独立监管,互不发现。
影响:同一 tunnel 出现两个活跃运行时,尤其对 stdio/有状态 MCP 是致命的。
修复:切换前先 tunnel-client runtimes stop <alias>(如果 profile 属于某个托管别名,先用 runtimes status <alias> --json 查 profile_path),再启动服务。切换期间保证"同一 tunnel 只有一个活跃运行时"。
坑 5:服务在登录会话内运行,不是开机即启
现象:brew services 的 launchd 服务只在你登录后运行,登出即停;Mac 睡眠会挂起工作,唤醒后运行时才恢复网络重试。
影响:指望它当系统服务长期跑,会在用户登出后静默失效。
修复:接受这个语义,或改用 Docker / K8s 部署(见 docs/deployment/overview.md)。launchd 会重启"已退出的进程",但不会因为 readiness 失败而重启。
坑 6:MCP 服务器启动慢会卡住 readiness
现象:sidecar 部署中本地 MCP listener 可能晚于 tunnel-client 绑定。可选 gate MCP_STARTUP_WAIT_TIMEOUT 会延迟首次轮询和 OAuth 发现。
影响:窗口过期后进程可能仍活着但 startup probe 失败。
修复:设 mcp.startup_wait_timeout(例如 2m);超时后等 MCP 服务器就绪再重启服务,并检查 readiness 与日志。MCP 服务器重启后,已存在的 MCP 会话可能需要重新 initialize。
坑 7:可执行文件路径必须用绝对路径
现象:Homebrew 服务读取 $(brew --prefix)/etc/tunnel-client.yaml 这个 profile。
影响:服务只把 brew 的 bin/sbin 加进 PATH,不加载你的交互式 shell 配置,所以 ~/.zshrc 里的环境变量、$HOME 展开的相对路径都可能失效。
修复:profile 里所有路径(MCP 可执行文件、工作目录、密钥文件)都写绝对路径;shell-only 的 env: 引用换成持久引用,例如 control_plane: api_key: file:/Users/YOU/.config/tunnel-client/runtime.key,并把该文件权限设为 0600。
坑 8:OAuth 信任边界需要显式配置
现象:OAuth 发现默认只信任配置的 MCP origin。
影响:如果你的 metadata 或 auth host 与 MCP origin 分离,升级后发现会失败,而且因为是失败静默(readiness 不 ready),很难查。
修复:升级前用 --mcp.oauth-trusted-origin、MCP_OAUTH_TRUSTED_ORIGINS 或 YAML mcp.oauth_trusted_origins 显式列出这些 origin,按文档的分阶段升级指引来。
坑 9:Harpoon 写入类目标有严格前置条件
现象:Harpoon 目标模板使用 YAML config_version: 2 和 template.version: 1,通过共享的 call_target MCP 工具执行,调用方不能自带 method。
影响:写入操作要求声明 operation 常量,且在结果不明确时永不自动重放;模板默认走 HTTPS,除非某个模板显式设 allow_loopback_http: true(该设置会让调用绕过已配置的 HTTP 代理,好让本地 API 凭据留在本地连接上,但不会启用其它明文目标)。GET 无 body,DELETE 不支持。
修复:先升级全部相关客户端,再开启写入或加参数元数据。升级前不要开写权限。
坑 10:控制平面轮询超时会被"学习"成更短的值
现象:如果经 HTTP 代理的轮询在响应头前就丢连接(EOF 类错误)且两个截止时间都没触发,tunnel-client 会自动为后续轮询学一个更短的超时。
影响:这个学到的超时只会变短、不低于 5 秒,且配置的 poll timeout 仍是上限。
修复:如果你依赖长轮询的长等待窗口,检查 --control-plane.poll-timeout / CONTROL_PLANE_POLL_TIMEOUT 与代理行为;首次轮询另有 --control-plane.initial-poll-timeout(默认 30s)。
坑 11:Redis 后端已被移除
现象:--engine-redis-url、非空 TUNNEL_ENGINE_REDIS_URL、或显式选择 Redis 后端,会在启动前直接返回移除错误。
影响:从旧版本升上来的 Redis 部署会起不来。
修复:删掉这些退役设置,改用 inmem(仅适合本地、临时工作),或保留旧版本直到迁移完成。--engine-queue-backend inmem 是当前支持的队列设置。
坑 12:版本号在快速演进,子命令会改名
现象:例如 tunnel-client codex plugin ... 是原生命令树,tunnel-client plugin codex ... 只是兼容别名;/v1/tunnel/... 单数别名仍被保留以做迁移。
影响:网上旧教程里的命令可能已经不存在或语义变了;某些子命令在新版本被移除。
修复:优先用 tunnel-client help <topic>(内置 quickstart / samples / doctor / oauth / plugin 五个主题帮助)和 --version 核对当前版本。⚠️ 本文引用的 v0.0.19 为 2026-11-04 抓取的最新 release,实际以你本机 --version 为准。
坑 13:stale 的隧道 ID 命名空间不要截断
现象:新版本支持带 4 字符命名空间的别名。 影响:截断 ID 会连到错误的隧道上。 修复:保留服务返回的完整 tunnel ID。
七、与同类对比
| 维度 | openai/tunnel-client | ngrok / frp / cloudflared | 自建 MCP 网关 |
|---|---|---|---|
| 公网入口位置 | OpenAI 托管 | 你自己的机器(自建隧道服务) | 你自己的机器 |
| 需要入站防火墙规则 | 否 | 否(但需你运维隧道服务端) | 否 |
| MCP server 是否暴露公网 | 否 | 是(流量经你自己的公开端点) | 视架构而定 |
| OAuth / PRMD 支持 | 内置,且有信任边界配置 | 无 MCP 语义,需自行适配 | 需自行实现 |
| 运维可见性 | /healthz /readyz /metrics + admin UI |
有,但无语义化 MCP 就绪概念 | 取决于自研程度 |
| 凭据分离 | runtime key / admin key 严格分离 | 不涉及 | 需自建 |
| 可嵌入进程 | ✅ 有 Go SDK(in-memory transport) | ❌ | ❌ |
| 厂商绑定 | 绑定 OpenAI 托管侧 | 中立 | 中立 |
| 适用 | 就是要给 OpenAI 产品用 | 通用公网暴露 | 多供应商统一网关 |
什么时候不用它:如果你要接的不是 OpenAI 系产品,tunnel-client 帮不上忙;如果你的组织不允许数据经过 OpenAI 托管的隧道服务,或者需要跨供应商统一网关,那就得自建或用通用隧道 + 自己做 MCP 网关。
八、一句话推荐
如果你要把 localhost 或内网的 MCP 服务器接到 ChatGPT / Codex / Responses API 上,并且安全团队不批新的入站端口——直接上 tunnel-client(brew install openai/tools/tunnel-client,然后 tunnel-client help quickstart),它用一条出站长轮询换掉了整个公网暴露面,是目前 OpenAI 生态里最省事的官方答案;但务必注意 stdio 单实例约束、runtime 与 Homebrew 服务不可并存、以及升级前要清理已移除的 Redis 配置这三件事。