Pick the exposure path before touching OAuth.
Both paths connect ChatGPT to Labby. Only one is the simple all-in-one answer when Labby itself is the browser-facing OAuth authorization server.
Tailscale Funnel
Public HTTPS URL → Funnel → Labby on 127.0.0.1:8765. The same URL handles /mcp, Labby OAuth metadata, the browser authorization flow, and Google's callback.
OpenAI Secure MCP Tunnel
Outbound-only tunnel-client → OpenAI → private Labby. Great for keeping MCP private, but the browser-facing OAuth authorization server is not automatically tunneled.
Run the known-good Labby, not whatever “latest” happens to be.
This guide is deliberately pinned to Labby 1.20.1. The npm package is just a thin launcher for the matching native Labby release, so nobody needs a Rust checkout or a source build.
Confirm Node.js 18 or newer.
Labby's npm launcher declares Node 18+.
node --version npm --version
If node is missing or the major version is below 18, install a current Node LTS before continuing.
Verify the pinned no-install launcher.
This is the lowest-friction entrypoint and the one used throughout this runbook.
npx -y @dinglebear/labby@1.20.1 --version
labby 1.20.1. If the version differs, stop here rather than silently continuing with a different binary.Optional: install the same pinned package globally.
If this machine will run Labby continuously, a global install makes later commands shorter. It is still pinned to exactly the same release.
npm install --global @dinglebear/labby@1.20.1 labby --version
If you skip this, simply keep using npx -y @dinglebear/labby@1.20.1 … everywhere below.
@dinglebear/labby@1.20.1 npm package ↗
Rule keep the version pinned; do not substitute latest.
Let Codex consume Labby from a remote machine without exposing MCP publicly.
Modern Codex is an MCP client. Configure Codex to launch ssh as its stdio transport; SSH starts the pinned labby mcp process on the remote Labby host and carries MCP over stdin/stdout.
labby mcp. Current Codex does not expose a codex mcp-server command, so this guide does not pretend Labby can launch Codex as an MCP server.Prepare SSH and verify the remote Labby host in one pass.
Run this on the machine where Codex runs. Use a dedicated key so Codex can start the stdio transport non-interactively.
REMOTE_USER="YOUR_LABBY_USER" REMOTE_HOST="YOUR_LABBY_HOST_OR_TAILSCALE_IP" KEY="$HOME/.ssh/codex-labby" KNOWN_HOSTS="$HOME/.ssh/known_hosts.codex-labby" mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" test -f "$KEY" || ssh-keygen -t ed25519 -N "" -f "$KEY" -C "codex-labby" chmod 600 "$KEY" && chmod 644 "$KEY.pub" # Install the key. If ssh-copy-id is unavailable, use your normal authorized_keys workflow. ssh-copy-id -i "$KEY.pub" "$REMOTE_USER@$REMOTE_HOST" # Pin and inspect the host key. Compare this fingerprint with a trusted fingerprint for the Labby host. ssh-keyscan -t ed25519 -H "$REMOTE_HOST" > "$KNOWN_HOSTS" chmod 600 "$KNOWN_HOSTS" ssh-keygen -lf "$KNOWN_HOSTS" # One fail-closed preflight: identity + Node + exact Labby package. ssh -T -i "$KEY" -o IdentitiesOnly=yes -o UserKnownHostsFile="$KNOWN_HOSTS" -o StrictHostKeyChecking=yes -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_USER@$REMOTE_HOST" 'hostname; whoami; node --version; npx -y @dinglebear/labby@1.20.1 --version'
ssh-keyscan discovers a key but does not authenticate it. Compare the displayed fingerprint with one obtained through a trusted channel before proceeding.Add the SSH-backed Labby server to Codex.
This uses Codex's real codex mcp add NAME -- COMMAND… interface. The hardened SSH flags prevent prompts, connection sharing surprises, and silent host-key acceptance.
codex mcp remove labby 2>/dev/null || true codex mcp add labby -- ssh -T -i "$KEY" -o IdentitiesOnly=yes -o UserKnownHostsFile="$KNOWN_HOSTS" -o StrictHostKeyChecking=yes -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ControlMaster=no -S none "$REMOTE_USER@$REMOTE_HOST" npx -y @dinglebear/labby@1.20.1 mcp
The equivalent configuration is stored by Codex under ~/.codex/config.toml. No Labby or OAuth secrets need to be embedded in the project.
Prove configuration and a real MCP round trip together.
codex mcp get labby --json codex mcp list --json codex exec 'Use the Labby MCP server to list the available gateway tools. Read only; do not mutate anything.'
labby MCP server and the read-only smoke test reaches Labby successfully. If the config exists but the smoke test fails, rerun the Step 2.1 SSH preflight before debugging Codex.Expose Labby with Tailscale Funnel.
Funnel terminates public HTTPS for you and forwards it to Labby on loopback. No router port-forwarding, public IP, or hand-managed TLS certificate is required.
127.0.0.1:8765
Install Tailscale if this server does not already have it.
On mainstream Linux, Tailscale's official installer is:
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up
On macOS, Tailscale recommends the Standalone app. Install its CLI integration from Tailscale → Settings → CLI integration. Then verify the CLI is available.
tailscale version tailscale status
Enable Funnel for Labby's local port.
You can configure Funnel before Labby is running. The first run may open a Tailscale approval page to enable HTTPS/Funnel for your tailnet.
tailscale funnel --bg 8765
If your platform requires elevated privileges, rerun that exact command with sudo. The --bg mode persists across Tailscale restarts and machine reboots.
Copy the public HTTPS URL.
tailscale funnel status
Look for a line shaped like https://your-machine.your-tailnet.ts.net. Save the root URL with no trailing /mcp. In the rest of this guide it is called PUBLIC_URL.
PUBLIC_URL=https://labby-gateway.example-tailnet.ts.netChatGPT's MCP endpoint will later be
PUBLIC_URL/mcp.Know how to inspect or remove the exposure.
tailscale funnel status --json
tailscale funnel reset
Give Labby a browser login that ChatGPT can complete.
Labby acts as the OAuth authorization server that ChatGPT talks to, while Google is the human identity provider behind it. The public URL from Funnel becomes Labby's issuer and the base for Google's callback.
Set your public URL and print the exact Google callback.
Replace the example with the exact URL shown by tailscale funnel status. Do not add /mcp here.
PUBLIC_URL="https://YOUR-MACHINE.YOUR-TAILNET.ts.net" printf 'Google redirect URI: %s/auth/google/callback\n' "$PUBLIC_URL"
Keep this shell open through the rest of the setup, or rerun this block later.
Create or select a Google Cloud project.
- Open Google Cloud Console ↗.
- Select an existing project dedicated to Labby, or create a new one such as Labby Gateway.
- Open Google Auth Platform. In the current console this contains Branding, Audience, Data Access, and Clients.
Configure the consent screen.
- Under Branding, enter an app name such as Labby, your support email, and developer contact email.
- Under Audience, choose Internal only if every allowed user belongs to the same eligible Google Workspace organization. Otherwise choose External.
- If the app is External and still in Testing, add the Google account you will use for Labby as a test user.
- Under Data Access, request only the basic identity scopes Labby needs:
openid,email, andprofile.
openid,email,profile, so you do not need broad Google API permissions for ordinary login.Create the OAuth client.
- Open Google Auth Platform → Clients.
- Choose Create client.
- Application type: Web application.
- Name it something obvious, for example Labby Gateway.
- Under Authorized redirect URIs, add exactly the URL printed in Step 4.1:
https://…/auth/google/callback. - You can leave Authorized JavaScript origins empty for this server-side flow.
- Create the client and immediately save both the Client ID and Client secret.
/auth/google/callback exactly as Labby will send it.Generate Labby's token-encryption key.
This is separate from the Google client secret. Labby requires a 32-byte key to encrypt reusable OAuth credentials at rest.
openssl rand -hex 32
Copy the 64-hex-character output somewhere temporary and private. You will paste it into LABBY_TOKEN_ENCRYPTION_KEY in the next step.
Create Labby's protected environment file.
Labby defaults to ~/.labby. If an old environment file exists, back it up before editing.
mkdir -p "$HOME/.labby" chmod 700 "$HOME/.labby" if [ -f "$HOME/.labby/.env" ]; then cp -p "$HOME/.labby/.env" "$HOME/.labby/.env.before-google-oauth" fi chmod 700 "$HOME/.labby"
Now open the file in an editor:
nano "$HOME/.labby/.env"
Paste this template and replace every value beginning with YOUR_:
LABBY_MCP_HTTP_HOST=127.0.0.1 LABBY_MCP_HTTP_PORT=8765 LABBY_AUTH_MODE=oauth LABBY_AUTH_PROVIDER=google LABBY_PUBLIC_URL=https://YOUR-MACHINE.YOUR-TAILNET.ts.net LABBY_GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID LABBY_GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET LABBY_AUTH_ADMIN_EMAIL=YOUR_GOOGLE_ACCOUNT_EMAIL LABBY_TOKEN_ENCRYPTION_KEY=YOUR_64_HEX_CHARACTER_KEY
Then lock the file down:
chmod 600 "$HOME/.labby/.env"
LABBY_AUTH_ALLOWED_REDIRECT_URIS. Labby's defaults already include the current ChatGPT and Claude callback patterns. Setting that variable replaces the defaults rather than extending them.Run Labby's auth doctor before starting the server.
npx -y @dinglebear/labby@1.20.1 doctor auth
Fix any reported missing variable, malformed public URL, key-length problem, or unsafe file permission before proceeding.
Start Labby once, then run one checkpoint for the whole public path.
Instead of six tiny checks, use one local process and one consolidated verifier that covers loopback health, Funnel health, OAuth discovery, the protected MCP boundary, and Codex's saved MCP configuration.
Start the pinned Labby server.
npx -y @dinglebear/labby@1.20.1 serve --host 127.0.0.1 --port 8765
Keep this visible during first setup. Labby reads the OAuth settings from ~/.labby/.env at startup.
Run the combined readiness checkpoint.
PUBLIC_URL="https://YOUR-MACHINE.YOUR-TAILNET.ts.net"
set -e
printf '1/6 local Labby... '
curl -fsS http://127.0.0.1:8765/ready >/dev/null && echo OK
printf '2/6 public Labby... '
curl -fsS "$PUBLIC_URL/ready" >/dev/null && echo OK
printf '3/6 authorization metadata... '
curl -fsS "$PUBLIC_URL/.well-known/oauth-authorization-server" >/dev/null && echo OK
printf '4/6 protected-resource metadata... '
curl -fsS "$PUBLIC_URL/.well-known/oauth-protected-resource" >/dev/null && echo OK
printf '5/6 MCP auth boundary... '
status="$(curl --max-time 10 -sS -o /dev/null -w '%{http_code}' "$PUBLIC_URL/mcp")"
case "$status" in 401|403) echo "OK ($status protected)" ;; *) echo "CHECK ($status)"; exit 1 ;; esac
printf '6/6 Codex MCP config... '
codex mcp get labby --json >/dev/null && echo OK
tailscale funnel statusFinish with one real Codex-to-Labby smoke test.
codex exec 'Use the Labby MCP server to list the available gateway tools. Read only; do not mutate anything.'
This verifies the SSH-backed Codex path independently from the public ChatGPT path. Both should work before you turn the foreground Labby process into a long-running service.
Enable Developer Mode and add Labby as a custom app.
OpenAI's current UI calls custom MCP connectors Apps. The labels can vary slightly by workspace role, but the flow is Developer Mode → Create app → enter the MCP endpoint → scan tools → finish OAuth.
Turn on Developer Mode.
Individual / Pro
Open Settings → Apps → Advanced Settings, then enable Developer mode if the toggle is available for your account.
Business / Enterprise / Edu
Your workspace may require an admin to allow Developer Mode first. Business admins/owners manage Apps in Workspace settings. Enterprise/Edu can gate it under workspace permissions for Connected Data.
Open the Create App screen.
For an individual-enabled account, go to Settings → Apps → Create. In managed workspaces, admins/owners may instead begin from Workspace settings → Apps → Create.
/mcp.Enter the Labby app details.
| Field | Value |
|---|---|
| Name | Labby |
| Description | Your MCP gateway / homelab control plane |
| Connection | Remote MCP / URL |
| MCP server URL | https://YOUR-MACHINE.YOUR-TAILNET.ts.net/mcp |
| Authentication | OAuth |
https://YOUR-MACHINE.YOUR-TAILNET.ts.net/mcp
Do not paste only the Funnel root. ChatGPT needs Labby's actual MCP transport endpoint ending in /mcp.
Scan tools and complete OAuth.
- Choose Scan tools or the equivalent validation action.
- ChatGPT should discover Labby's OAuth metadata and open the authorization flow.
- Sign in with the Google account that exactly matches
LABBY_AUTH_ADMIN_EMAIL, or another identity already admitted by your Labby policy. - Review the permissions Labby is asking ChatGPT to receive.
- Complete the authorization and return to ChatGPT.
- Review the discovered tools, then choose Create.
Verify the app is enabled.
Open Settings → Apps. Your Labby entry should appear among enabled/developer apps, usually with a Dev/developer indicator. Managed workspaces may also show a Drafts area to administrators.
Use Labby in a chat.
Start a new web chat, add/select Labby from the Apps control for the message, or @mention the app where supported. Ask for a harmless read-only operation first, such as listing or summarizing available gateway tools.
Refresh after changing Labby's tool surface.
If you add/remove an upstream or change exposed tools later, return to the app configuration and refresh/rescan the MCP definition so ChatGPT does not operate from stale tool metadata.
Yes, ChatGPT can use OpenAI's MCP tunnel. It is not API-only.
Secure MCP Tunnel is an outbound-only transport from your machine to OpenAI. It can connect a private or localhost MCP server to ChatGPT developer-mode apps without publishing the MCP transport itself to the internet.
/mcp URL for the tunneled MCP transport.LABBY_PUBLIC_URL still need to be reachable by the user's browser. Therefore Tunnel is a complete replacement for Funnel only when you are not relying on a browser-facing private Labby OAuth server. For the exact Google OAuth setup in this guide, Funnel remains the simplest single-path solution.Use Tunnel alone when…
Your private MCP endpoint does not require a browser-reachable private authorization server, or your auth server is already reachable at a separate public HTTPS origin.
Use Funnel when…
You want one public HTTPS origin to serve Labby MCP, Labby OAuth metadata/authorization, and the Google callback with the least moving parts.
Create the tunnel in OpenAI Platform.
- Open the OpenAI Platform Tunnels page.
- Create a new tunnel, name it something recognizable such as labby-home, and save the returned
tunnel_…ID. - Associate the tunnel with the Platform organization and the target ChatGPT workspace/account that will use it.
Make sure the operator has the correct tunnel permissions.
OpenAI separates tunnel administration from tunnel use:
| Job | Permission |
|---|---|
| Create/edit/delete tunnels | Tunnels Read + Manage |
| Run tunnel-client or select the tunnel in ChatGPT | Tunnels Read + Use |
Create a runtime API key.
Create a normal project/runtime API key that the local tunnel process will use. Do not use an organization admin key for the long-running client.
Export it only in the shell/session that runs the tunnel client:
export CONTROL_PLANE_API_KEY="YOUR_OPENAI_RUNTIME_API_KEY" export CONTROL_PLANE_TUNNEL_ID="tunnel_YOUR_TUNNEL_ID"
Install the official tunnel-client.
On macOS, OpenAI documents a Homebrew install:
brew install openai/tools/tunnel-client tunnel-client --version
On Linux or Windows, use the current official release for your platform from OpenAI's repository and follow its binary installation instructions.
Inspect the built-in DCR/OAuth profile before using it.
tunnel-client help quickstart tunnel-client profiles samples list tunnel-client profiles samples show sample_mcp_with_dcr
sample_mcp_with_dcr is OpenAI's general-purpose HTTP MCP profile for servers such as Labby that publish OAuth / protected-resource metadata.
Create a Labby tunnel profile.
Labby remains private on loopback. The tunnel client talks directly to its local MCP URL.
tunnel-client init --sample sample_mcp_with_dcr --profile labby --tunnel-id "$CONTROL_PLANE_TUNNEL_ID" --mcp-server-url http://127.0.0.1:8765/mcp
The generated profile should reference env:CONTROL_PLANE_API_KEY rather than embedding the key.
Run the tunnel preflight.
tunnel-client doctor --profile labby --explain
Resolve tunnel permission, runtime key, local MCP reachability, and OAuth-discovery errors before starting the long-running process.
Start the tunnel.
tunnel-client run --profile labby
By default the generated first-use profile exposes its local health/admin listener on loopback around port 8080. In another terminal, inspect it:
curl -fsS http://127.0.0.1:8080/healthz curl -fsS http://127.0.0.1:8080/readyz
If enabled by the profile, the local operator UI is available at http://127.0.0.1:8080/ui.
Select the tunnel in ChatGPT.
- In ChatGPT web, enable Developer Mode as described in Step 6.
- Open Settings → Apps → Create.
- For Connection, choose Tunnel.
- Select the tunnel you created, or paste its
tunnel_…ID if the UI requests it. - Configure authentication. If you choose Labby's Google OAuth, remember that Labby's browser authorization URL must still be publicly reachable.
- Scan tools, complete auth if applicable, review the tool list, and create the app.
Know which route you actually built.
Tell us what MCP client you use. Get its actual config.
The ecosystem never agreed on one file shape, so this section follows the same pattern as install-mcp: describe Labby once, then render the native install method for the selected client.
npx -y @dinglebear/labby@1.20.1 mcp. Remote installs point the client at your HTTPS /mcp endpoint. No generated example uses latest.Codex is the primary path; other clients stay available.
This guide defaults to Codex and its native ~/.codex/config.toml / codex mcp model. The picker still renders the correct dialect for other clients when you need one, instead of pretending every editor consumes the same mcp.json.
Generic starter JSON is still available.
For clients that understand the conventional mcpServers stdio shape, this remains a useful portable starting point.
{
"mcpServers": {
"labby": {
"command": "npx",
"args": ["-y", "@dinglebear/labby@1.20.1", "mcp"]
}
}
}Server knobs belong in ~/.labby/.env.
LABBY_MCP_HTTP_HOST=127.0.0.1 LABBY_MCP_HTTP_PORT=8765 LABBY_AUTH_MODE=oauth LABBY_AUTH_PROVIDER=google LABBY_PUBLIC_URL=https://YOUR-MACHINE.YOUR-TAILNET.ts.net
The client installer changes how the client reaches Labby. It does not move secrets or server configuration into editor config files.
~/.labby/.env, your shell environment, or a secret manager.Do not debug Google when the socket is dead.
Start at Labby and move outward. Each check below tells you which boundary is actually broken.
“Address already in use” on port 8765.
Find the process that already owns the port.
lsof -nP -iTCP:8765 -sTCP:LISTEN
ss -ltnp | grep ':8765'
Either stop the stale Labby process or deliberately choose another port. If you change the port, update LABBY_MCP_HTTP_PORT, Funnel/Tunnel, and LABBY_SERVER_URL together.
Local Labby works, but the Funnel URL does not.
curl -fsS http://127.0.0.1:8765/ready tailscale status tailscale funnel status
If local /ready works, do not touch Labby config yet. Check whether Funnel points to port 8765, whether the node is online, and whether Funnel was approved for the tailnet.
ChatGPT says it cannot reach or scan the MCP server.
PUBLIC_URL="https://YOUR-MACHINE.YOUR-TAILNET.ts.net" curl -fsS "$PUBLIC_URL/ready" curl -fsS "$PUBLIC_URL/.well-known/oauth-protected-resource" | python3 -m json.tool curl --max-time 10 -sS -D - -o /dev/null "$PUBLIC_URL/mcp"
Use $PUBLIC_URL/mcp as the ChatGPT MCP endpoint. Use only $PUBLIC_URL for LABBY_PUBLIC_URL.
Google shows redirect_uri_mismatch.
PUBLIC_URL="https://YOUR-MACHINE.YOUR-TAILNET.ts.net" printf '%s/auth/google/callback\n' "$PUBLIC_URL"
Compare that output character-for-character with the OAuth client's Authorized redirect URIs in Google Auth Platform. Check HTTPS, hostname, capitalization, callback path, and trailing slash.
Google login succeeds, but Labby rejects the user.
Make sure LABBY_AUTH_ADMIN_EMAIL is the same verified Google email you signed in with, then rerun:
npx -y @dinglebear/labby@1.20.1 doctor auth
If using an External Google app still in Testing, also confirm the account is listed as a Google OAuth test user.
Codex shows Labby configured, but calls fail.
Separate the SSH transport from Codex itself with the same fail-closed preflight used in Step 2:
ssh -T -i "$KEY" -o IdentitiesOnly=yes -o UserKnownHostsFile="$KNOWN_HOSTS" -o StrictHostKeyChecking=yes -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" 'hostname; whoami; node --version; npx -y @dinglebear/labby@1.20.1 --version'
If SSH passes, inspect what Codex actually saved and then rerun one read-only round trip:
codex mcp get labby --json codex exec 'Use the Labby MCP server to list the available gateway tools. Read only; do not mutate anything.'
If the SSH preflight passes but the Codex smoke test does not, the failure is in the saved MCP command/config or Codex runtime, not Labby's public OAuth path.
OpenAI Tunnel is connected, but OAuth still fails.
This is the tunnel's biggest conceptual trap. Check whether the authorization URL advertised by Labby is reachable from an ordinary browser.
curl -fsS http://127.0.0.1:8765/.well-known/oauth-authorization-server | python3 -m json.tool
If the advertised issuer/authorization endpoint is localhost, a private hostname, or otherwise unreachable from the browser, the MCP tunnel cannot repair that. Publish the authorization server separately over HTTPS, or use the Funnel path for Labby + Google OAuth.
The tunnel does not appear in ChatGPT.
tunnel-client doctor --profile labby --explain
Then verify the tunnel is associated with the same Platform organization and target ChatGPT workspace/account, and that your identity has Tunnels Read + Use.
Need to remove public exposure immediately.
tailscale funnel reset
This removes Funnel configuration. It does not delete Labby configuration or Google OAuth credentials. Stop the Labby process separately if you also want the local gateway offline.
@dinglebear/labby@1.20.1 Release ↗Diagnostic order 127.0.0.1 → public HTTPS → OAuth metadata → auth challenge → ChatGPT. The first failed hop owns the investigation.