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.Z tag 或 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。

五、典型适用场景

  1. 企业内网 MCP → ChatGPT 连接器:内部 IT 系统的 MCP server 在 VPC 里,ChatGPT 需要调用它,安全团队不批入站规则。典型场景:内部工单系统、代码搜索(Sourcegraph)、数据库只读查询网关。
  2. 本地开发机的 MCP → Codex:开发者笔记本上跑着自研 MCP 工具,Codex CLI 在同一台机器上直接调用,全程不出网。
  3. K8s 集群内的 MCP → Responses API:把集群内的 agent 工具以 Responses API 的 tool 形式暴露给模型。
  4. CI / 私有网络里的 MCP 接入 AgentKit:AgentKit 需要一个稳定的、可被托管侧调用的 MCP 端点。
  5. 合规场景的"零入站"证明:安全评审时,"我们的 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 配置这三件事。