-
"control plane API key is required"
- Set
CONTROL_PLANE_API_KEY, setOPENAI_API_KEYas a fallback, or use--control-plane.api-key=env:.../file:....
- Set
-
"tunnel ID is required"
- Set
CONTROL_PLANE_TUNNEL_IDor--control-plane.tunnel-id=....
- Set
-
"invalid tunnel ID ... must match tunnel_<32 lowercase hexadecimal characters>"
- Use a tunnel ID shaped like
tunnel_0123456789abcdef0123456789abcdef.
- Use a tunnel ID shaped like
-
"MCP server URL is required"
- Set
MCP_SERVER_URLor--mcp.server-url=....
- Set
Directly downloaded macOS release ZIPs are not currently notarized and can be
blocked by Gatekeeper before tunnel-client --version or
tunnel-client help quickstart runs. Do not use Open Anyway, xattr,
or spctl to bypass the check.
Use the supported Homebrew installation path instead:
brew install openai/tools/tunnel-client
tunnel-client --version
tunnel-client help quickstart- Ensure
CONTROL_PLANE_BASE_URLis the host root (for examplehttps://api.openai.com) and not a pre-prefixed path.
A Platform page error and a tunnel-client runtime 401/403 are different
failures. tunnel-client doctor is a local preflight: it validates config, key
presence, MCP/OAuth reachability, and the local health listener. It does not
query the Platform UI or prove that a key can poll the target tunnel.
- Platform Tunnels page says "Tunnels access required", "not authorized", or
that Tunnels Read access is required
- This is Platform UI authorization, not a tunnel-client runtime or MCP failure. Tunnel permissions are organization-level, not project-level.
- Select the intended organization. Ask an organization owner or RBAC administrator to add you to a role or group with Tunnels Read to view tunnels, or Read + Manage to create, edit, or delete them. If no matching role exists, they can create one, assign it to a group, and add you to that group. Allow up to 30 minutes for a new role assignment to propagate, then reload the page.
tunnel-client doctor --explainfails- Fix the named local preflight check before starting the daemon.
doctorpasses, buttunnel-client admin tunnels get <tunnel_id>returns401or403- First identify the credential this probe used.
admin tunnels getuses--admin-keyorOPENAI_ADMIN_KEYwhen either is configured; only without an admin key does it fall back toCONTROL_PLANE_API_KEYorOPENAI_API_KEY. To test the runtime key, leave--admin-keyandOPENAI_ADMIN_KEYunset and setCONTROL_PLANE_API_KEYto the same key used bytunnel-client run. - If that same runtime key gets
401/403, confirm the tunnel ID and organization/workspace association.
- First identify the credential this probe used.
- The same runtime key can read tunnel metadata, but
tunnel-client runor/ui#logsshows a polling401/403- Metadata read works, but the runtime path likely lacks Tunnels Use. Grant the runtime-key principal Tunnels Read + Use, then restart.
- Platform lists the tunnel, but ChatGPT cannot select it
- Check the tunnel workspace ID, connector operator's Tunnels Read +
Use, and
/readyz; this is not proof of a daemon failure.
- Check the tunnel workspace ID, connector operator's Tunnels Read +
Use, and
See permissions.md for role and group setup.
For component evidence, read the local
/health?details=true or /health/mcp snapshot:
curl -fsS 'http://127.0.0.1:8080/health?details=true'
curl -fsS 'http://127.0.0.1:8080/health/mcp'- Live and ready, MCP not observed: stdio startup skips discovery. Only ordinary forwarded initialize and tools/list traffic establishes same-child evidence. Repeated health reads cannot make discovery happen.
- Poll failures: inspect
control-planefor completed failures and backoff. A current long poll or paused polling under queue pressure is not an outage. - Results missing despite successful polling: inspect
response-delivery. Polls and response uploads can fail independently; a benign 404 completion does not mean a new upload was accepted. - Backlog:
queuedepth counts local waiting commands. A full queue means backpressure and does not change readiness by itself. - Empty queue but work in progress: inspect
dispatcheractive operations and oldest active age. This counts real work, not idle worker goroutines; age by itself does not establish that an operation is stuck. - Old or partial evidence: check observation timestamps, child generation,
and
complete/partial/limitedfields. Missing names from a partial catalog are not proof that the server lacks those tools.
Each component endpoint returns its value at /health/{component}. These
snapshots return 200 even for degraded components; use /readyz for an HTTP
readiness gate. Older runtimes may return 404 for the new diagnostics.
If you are debugging why /readyz is failing or why the client never becomes
"healthy/ready", start here:
tunnel-client health --url-file "$health_url_file"is the fastest structured probe when you already have a health URL file fromtunnel-client run; create that path withmktempinstead of a fixed shared/tmpfilename./healthzis liveness only. A200 liveresponse means the process is up./readyzincludes startup gating:503 oauth discovery pendingwhile OAuth discovery is still in flight, including timeout-only startup cycles that are waiting to retry.503 oauth discovery failed: ...when required OAuth discovery reaches a non-timeout failure.503 mcp probe failed: ...when the MCP startup probe fails.200 ready (mcp initialize requires auth: ...)when the MCP endpoint is reachable but requires auth duringinitialize.200 ready (mcp startup probe timed out: ...)when the probe times out but startup should continue.
- For a Kubernetes sidecar or other local listener that can bind after
tunnel-clientstarts, setMCP_STARTUP_WAIT_TIMEOUTto a positive duration. During that window, pre-connectconnection refusedand missing Unix-socket failures keep readiness pending and delay the first control-plane poll; if the wait expires, polling resumes with the legacy behavior while/readyzremains non-ready for the startup failure. - If the process is live but
/readyzstays non-200, check logs for:- OAuth discovery failures
- control-plane connectivity errors
- MCP server connectivity errors
tunnel-client health --port 8080is the quickest loopback check when the daemon is bound to the default port.
- The admin UI logs panel can download a redacted support archive from
/api/logs/export?minutes=30. - To save the same archive into the current working directory without using a browser:
curl -fsSJO "http://127.0.0.1:8080/api/logs/export?minutes=30"- To capture one archive every five minutes until stopped:
while :; do
curl -fsSJO "http://127.0.0.1:8080/api/logs/export?minutes=30"
sleep 300
done- The archive contains
manifest.json,README.txt,tunnel-client.logs.ndjson,tunnel-client.metrics.prom,admin/status.json,admin/system.json, andadmin/oauth.json. tunnel-client.metrics.promis a point-in-time Prometheus text snapshot captured from/metricsat export time.- The
admin/*.jsonfiles are point-in-time copies of/api/status,/api/system, and/api/oauthat export time, so support can review the configuredtunnel_id, process-scopedclient_instance_id, route state, probe status, and OAuth discovery state alongside the log stream. - The archive is redacted before it is returned.
-
ChatGPT connector setup cannot discover tools
- Keep
tunnel-client run ...running while creating or testing the connector. The remote tunnel object can exist even when no local runtime is polling it. - Confirm the connector selected the same
CONTROL_PLANE_TUNNEL_IDthat the daemon is using. - Check
/readyz, not only/healthz; liveness does not prove MCP probing or OAuth discovery finished.
- Keep
-
Connector URL returns 404 or does not stream on GET
- Connector MCP traffic is POST-based JSON-RPC. GET requests to
/v1/mcp/...are not a diagnostic SSE stream. - If client logs show doubled paths, set
CONTROL_PLANE_BASE_URLto the host root, for examplehttps://api.openai.com, not a/v1/tunnels/...URL.
- Connector MCP traffic is POST-based JSON-RPC. GET requests to
-
unsupported_channelfrom the connector path- The incoming command named a channel that is not configured. Add a
channel-qualified
--mcp.server-url/--mcp.commandentry, or update the product configuration to sendmain. - For
harpoon, register at least one--harpoon.target/HARPOON_TARGETSentry. Harpoon intentionally stays unroutable with an empty target registry.
- The incoming command named a channel that is not configured. Add a
channel-qualified
-
OAuth-protected connector succeeds locally but fails in product
- The MCP server can stay private, but the authorization server itself is not automatically tunneled. It must be reachable wherever the OAuth browser flow and metadata fetches require it.
- Issuer mismatch diagnostics are allowed for external enterprise IdPs; focus
first on wrong URLs, unreachable metadata endpoints, or missing
Authorizationforwarding.
See connectors.md for the full connector request lifecycle,
channel routing model, and environment-variable checklist.
- Verify
MCP_SERVER_URLis reachable from wheretunnel-clientruns. - If your MCP server uses a private CA, ensure the OS/container trust store includes it.
- Consider temporarily enabling
--log.http-raw-unsafeand--log.level=debugin a controlled environment to debug handshake issues.
harpooncommands returnunsupported_channelwhen there are no registered Harpoon targets.- Confirm you have configured at least one
--harpoon.targetorHARPOON_TARGETSentry and that it passes validation.
- Increase
--mcp.max-concurrent-requests/MCP_MAX_CONCURRENT_REQUESTSto raise active MCP execution concurrency, but only if the MCP server can safely handle the additional parallelism. When every worker is busy, the dispatcher removes one command from the local queue and waits for a worker slot. It does not drain another command until a slot is free. - Increase
--control-plane.max-inflight/CONTROL_PLANE_MAX_INFLIGHT_REQUESTSonly to increase the local prefetch backlog. It does not increase MCP execution concurrency. A full buffer pauses polling until a queue slot is free; each poll requests at most25commands. - Account for both independent limits when sizing the process. With the
defaults, tunnel-client can hold up to
10active MCP requests,20commands in the local queue, and one dispatcher-held command waiting for a worker slot.