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.
Big picture
Section titled “Big picture”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 clientPairing 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.
Prerequisites
Section titled “Prerequisites”- A running
ggui servethat your client can reach over HTTP/HTTPS + WebSocket. On localhost that means serving on127.0.0.1and 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 serveprints a freshggui_admin_*admin token on boot (pin it with--admin-token <t>to survive restarts); that bearer gatesPOST /admin/pair/init. With--dev-allow-allany bearer — or none at all — is accepted asbuilder— fine on localhost, unsafe anywhere else (see Hardening).
Step 1: Mint a pairing code
Section titled “Step 1: Mint a pairing code”The server never hands out a bearer token without first minting a code. The code is the proof-of-presence for the first exchange.
From the operator console
Section titled “From the operator console”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).
From the command line
Section titled “From the command line”If you prefer a headless flow (CI, scripts, tmux):
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).
Step 2: Exchange the code for a bearer
Section titled “Step 2: Exchange the code for a bearer”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:
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.
In the Guuey app (coming soon)
Section titled “In the Guuey app (coming soon)”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.
Step 3: Talk to your server
Section titled “Step 3: Talk to your server”With the bearer paired, a client can:
- List agents declared in your server’s
ggui.json#agententry — OSS default is the single supervised agent processggui serveboots alongside MCP; no marketplace. - Start chats over the live-channel WebSocket at
ws://<your-server>/ws(orwss://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.
Managing pairings
Section titled “Managing pairings”- 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 activeAuthAdapter, so the next/mcpor/wscall from that device fails with401 No valid credentialsand 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.
Reaching your server from a phone
Section titled “Reaching your server from a phone”Localhost-only by default. To pair from a phone on the same LAN:
-
Bind
ggui serveto your LAN address (or0.0.0.0), not127.0.0.1:Terminal window pnpm exec ggui serve --host 0.0.0.0 --port 6781 -
Note your LAN IP (e.g.,
192.168.1.42) and usehttp://192.168.1.42:6781as 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:
- ngrok —
ngrok http 6781and use thehttps://…ngrok-free.appURL. - Cloudflare Tunnel —
cloudflared 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://.
Hardening before leaving localhost
Section titled “Hardening before leaving localhost”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:
- 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-corefor theAuthAdaptercontract. - Terminate TLS in front. A reverse proxy (Caddy, nginx, Cloudflare Tunnel) is the expected shape. The bare
ggui serveport is plaintext HTTP. - Firewall the admin paths.
POST /admin/pair/initis 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. - Rotate pairing tokens. Revoke paired devices from the operator console when they’re no longer in use.
What this does NOT include
Section titled “What this does NOT include”- 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) andggui.json#generation.model, or paste a key at/settings. Seeggui serve→ Generation. - Push notifications, mobile background sync, agent marketplace. Not part of the self-hosted server today.
What’s next
Section titled “What’s next”- OSS Quick Start — fastest path from zero to a running local server.
- Reference deploys — Docker / Fly.io / Render manifests for putting
ggui serveon a public URL. - MCP Protocol Reference — wire format, identical across OSS and hosted.
- WebSocket Protocol — live-channel envelopes (
ActionEnvelope,StreamEnvelope).