Skip to content

Pair a client to a self-hosted server

read as .md

If you followed the OSS Quick Start, you already have a local ggui serve running. This page pairs a viewer client to it, so the client can browse agents, start chats, and view generated UIs while the server stays on your infrastructure. The pairing protocol below is live today and works with any client you build (the curl flow is the primary path); the first-party Guuey companion app (web / iOS / Android) is coming soon.

Your MCP agent ── MCP ──→ ggui serve (your infra)
│ POST /admin/pair/init (admin) → {code, codeExpiresAt, serverName}
│ POST /pair (public) → {pairingId, token, serverName, deviceName}
│ /ws (live channel, bearer-gated)
Paired viewer client

Pairing exchanges a one-shot code for a bearer token (no built-in expiry; lives until you revoke it server-side). The client stores the token and sends it on every subsequent call. Your server is the only backend involved — it never talks to mcp.ggui.ai or any other external service on your behalf.

  • A running ggui serve that your client can reach over HTTP/HTTPS + WebSocket. On localhost that means serving on 127.0.0.1 and opening the client on the same machine; across the network, see Reaching your server from a phone.
  • An operator session on the server — i.e., you can mint codes. By default ggui serve prints a fresh ggui_admin_* admin token on boot (pin it with --admin-token <t> to survive restarts); that bearer gates POST /admin/pair/init. With --dev-allow-all any bearer — or none at all — is accepted as builder — fine on localhost, unsafe anywhere else (see Hardening).

The server never hands out a bearer token without first minting a code. The code is the proof-of-presence for the first exchange.

ggui serve exposes a minimal operator UI at http://127.0.0.1:6781/. Click Start pairing — the console calls POST /admin/pair/init with the current session bearer and displays a 6-digit code with an expiry timer (default: 10 minutes).

If you prefer a headless flow (CI, scripts, tmux):

Terminal window
curl -sS -X POST http://127.0.0.1:6781/admin/pair/init \
-H "Authorization: Bearer $GGUI_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"serverName":"my laptop"}'

Replace $GGUI_ADMIN_TOKEN with the ggui_admin_* printed on the boot banner (or whatever you passed to --admin-token). With --dev-allow-all, any bearer — or none at all — works.

Response:

{
"code": "472301",
"codeExpiresAt": "2026-04-20T12:34:56.000Z",
"serverName": "my laptop"
}

Treat the code like a one-time password — anyone who gets it in the next 10 minutes can pair. It’s consumed on the first successful POST /pair.

If the code expires before the client finishes pairing, POST /pair returns 401 {error: {code: 'pairing_rejected', …}}. There’s no auto-refresh — re-run POST /admin/pair/init to mint a fresh code and try again. Only one outstanding code exists at a time (a second init overwrites the pending one).

Every client performs the same exchange — POST /pair (public, no auth — pairing IS the bootstrap for future auth) with the code and a device name:

Terminal window
curl -sS -X POST http://127.0.0.1:6781/pair \
-H 'Content-Type: application/json' \
-d '{"code":"472301","deviceName":"my client"}'

Response:

{
"pairingId": "…",
"token": "…",
"serverName": "my laptop",
"deviceName": "my client"
}

Store the token and send it as Authorization: Bearer <token> on the server’s MCP (/mcp) and live-channel WebSocket (/ws) endpoints.

The first-party Guuey companion app (web / iOS / Android) will wrap this exchange in a Settings → Servers → Add server form — enter the server URL (http://127.0.0.1:6781 or the reachable hostname), type the 6-digit code, tap Pair. It stores the token and shows the server with a reachability status dot. Until it ships, the curl flow above (or your own client) is the path.

With the bearer paired, a client can:

  • List agents declared in your server’s ggui.json#agent entry — OSS default is the single supervised agent process ggui serve boots alongside MCP; no marketplace.
  • Start chats over the live-channel WebSocket at ws://<your-server>/ws (or wss:// once you front it with TLS). Messages flow through the paired MCP transport; chat history lives in your server’s session store.
  • View generated UIs — when an agent calls ggui_render, the result is delivered as an MCP-Apps resource (ui://ggui/render/<sessionId>) your server serves. The client mounts it inline (same-origin cookies on your server, your CSP).

A paired client does NOT proxy any of this — your server is the only backend involved.

  • Revoke server-side: The operator console has a Paired devices list — revoking there calls POST /admin/pair/:pairingId/revoke (admin bearer required, idempotent). The token is removed from the active AuthAdapter, so the next /mcp or /ws call from that device fails with 401 No valid credentials and the client must re-pair.
  • Rotate a lost token: If you suspect a token leaked, revoke the server-side entry and re-run Step 1 / Step 2.

Localhost-only by default. To pair from a phone on the same LAN:

  1. Bind ggui serve to your LAN address (or 0.0.0.0), not 127.0.0.1:

    Terminal window
    pnpm exec ggui serve --host 0.0.0.0 --port 6781
  2. Note your LAN IP (e.g., 192.168.1.42) and use http://192.168.1.42:6781 as the server URL in your client.

If the phone is NOT on the same network (mobile data, different NATs), you need a public endpoint. ggui serve doesn’t ship a managed tunnel — use any of:

  • ngrokngrok http 6781 and use the https://…ngrok-free.app URL.
  • Cloudflare Tunnelcloudflared tunnel --url http://localhost:6781.
  • Tailscale — put both devices on your tailnet; use the MagicDNS hostname.

Any of these makes pairing work across arbitrary networks without exposing your port to the open internet. Once you have a public https:// URL, the live-channel WebSocket automatically upgrades to wss://.

ggui serve defaults to strict-auth: /mcp only accepts pair-minted bearers, and /admin/pair/init requires the per-boot ggui_admin_* token. With --dev-allow-all it relaxes to accept any bearer — or none at all — as builder — fine on 127.0.0.1, unsafe anywhere else.

Before you put the server on a public URL:

  1. Swap in a real AuthAdapter. createGguiServer({ auth }) accepts a custom adapter that gates both the MCP endpoint and the live-channel WebSocket. See @ggui-ai/mcp-server-core for the AuthAdapter contract.
  2. Terminate TLS in front. A reverse proxy (Caddy, nginx, Cloudflare Tunnel) is the expected shape. The bare ggui serve port is plaintext HTTP.
  3. Firewall the admin paths. POST /admin/pair/init is the only surface that mints codes. Block the /admin/ prefix from the public internet with one reverse-proxy rule; keep it on a local admin interface or VPN.
  4. Rotate pairing tokens. Revoke paired devices from the operator console when they’re no longer in use.
  • Workspaces, billing, usage metering, team management. Guuey-hosted SaaS surfaces. They do not exist on a self-hosted server.
  • Managed (operator-paid) generation. Generation on a self-hosted server uses YOUR provider key (BYOK) — set ANTHROPIC_API_KEY (or another provider key) and ggui.json#generation.model, or paste a key at /settings. See ggui serve → Generation.
  • Push notifications, mobile background sync, agent marketplace. Not part of the self-hosted server today.