ggui serve
read as.mdggui serve boots the self-hosted GGUI runtime: an MCP server (@ggui-ai/mcp-server) plus, by default, a supervised agent process declared in ggui.json. It’s distinct from ggui dev, the inner-loop development hub — ggui serve is the production-shaped self-host you’d put behind a tunnel or on a VM.
Quick start
Section titled “Quick start”ggui serveBinds 127.0.0.1:6781, mounts the first-run bundle, and starts the agent declared in ggui.json#agent.entry alongside MCP. The CLI auto-opens the landing page once the banner prints (skip with --no-open).
Point an agent at it — the MCP endpoint is http://127.0.0.1:6781/mcp:
GGUI_MCP_URL=http://127.0.0.1:6781/mcpGGUI_MCP_BEARER=dev # i.e. `Authorization: Bearer dev` — works with `ggui serve --dev-allow-all`With the default strict auth, swap dev for a pair-minted bearer (or one minted via ggui keys create --keys-file <path>).
For the bootstrap walkthrough, see the self-hosted quickstart.
The first-run bundle
Section titled “The first-run bundle”A default ggui serve mounts these same-origin surfaces:
| Path | What it serves |
|---|---|
/ |
Landing page — server identity, pair-code card, links into the console |
/mcp |
MCP HTTP endpoint — agents call ggui_render, ggui_consume, etc. here. |
/ws |
Live-channel WebSocket — live session plane for MCP Apps iframes and the console. |
/ggui/health |
Liveness probe — the path the reference-deploy healthchecks hit. |
/r/<shortCode> |
Signed render-viewer URL — resolves a shortCode to its session. |
/settings |
LLM provider-key page — paste a key; takes effect without restart. |
/pair, /admin/pair/init |
Pairing endpoints for paired viewer clients. |
Hosts that need a different shape (no landing page, no pairing, programmatic control) should compose createGguiServer() directly rather than invoke this CLI. createGguiServer({ mcpServices: [...] }) can also mount additional standalone MCP services at their own paths — see MCP services.
ggui serve [options]Bind & lifecycle
Section titled “Bind & lifecycle”| Flag | Default | Purpose |
|---|---|---|
--port <n> |
6781 |
Bind port. 0 = OS-assigned (the actual port prints in the boot banner). |
--host <addr> |
127.0.0.1 |
Bind host. Loopback only by default. |
--mcp-only |
off | Run just the MCP server; skip agent supervision even if ggui.json has agent.entry. Also implies --no-open. |
--no-open |
off | Skip auto-opening the operator’s browser. Auto-open is also skipped whenever stdout is not a TTY (CI, supervised, piped output). |
Auth posture (mutually exclusive)
Section titled “Auth posture (mutually exclusive)”By default, /mcp rejects any bearer that wasn’t minted by the pairing flow. These flags relax that for specific scenarios:
| Flag | Posture |
|---|---|
--dev-allow-all |
Accept any bearer — or none at all (the no-bearer probe MCP custom connectors send) — as builder. Local-dev / tunnel smoke ONLY. Never expose to the open internet — the banner prints an unmissable warning when this is set. |
--public-demo |
Same any-bearer auth as --dev-allow-all, plus a per-IP FixedWindowRateLimiter on ggui_render (default: 30 ggui_render calls per 10 min per IP) and a “PUBLIC DEMO — operator pays” banner. Use case: a single shared LLM key for an audience demo (Show HN, blog, classroom). Mutually exclusive with --dev-allow-all. |
--multi-user |
Strict-auth multi-user posture. The console /settings LLM-keys gate switches from admin-token to auth-adapter so each authenticated end-user manages their OWN provider keys (scope = userId for kind:'user', appId for kind:'app'). kind:'builder' identities are rejected. Mutually exclusive with --dev-allow-all and --public-demo. Note: the admin-token /keys pairing plane is separate from the /settings LLM-keys plane that --multi-user rebinds. |
Custom connector hosts
Section titled “Custom connector hosts”| Flag | Purpose |
|---|---|
--oauth |
Mount OAuth 2.1 + PKCE + Dynamic Client Registration routes (.well-known/oauth-* + /oauth/{authorize,token,register}). Required for hosts whose Add Connector form has no field for a pre-shared bearer (claude.ai, ChatGPT). Pure-bearer clients (Claude Desktop with bearer in config) work without it. |
--public-base-url <u> |
Override the public base URL used to compose the iframe-runtime + live-channel URLs written into each render’s ai.ggui/render slice. Set to a tunnel URL (https://<random>.trycloudflare.com) when testing against a remote MCP host so those URLs resolve from the host’s perspective. Without this, they derive from --host:--port and only work from the same machine. |
Browser clients
Section titled “Browser clients”| Flag | Purpose |
|---|---|
--browser-origin <origin> |
Repeatable. Allow pages served from this origin past MCP-wire Origin validation, and emit it in the CORS headers. Pages on localhost need no entry — loopback is always allowed. Env twin: GGUI_BROWSER_ORIGINS (comma-separated). This is NOT authentication — /mcp still requires a bearer. Non-browser clients are unaffected either way. |
A browser-based MCP client on any other origin is rejected before auth runs, so the failure looks like an opaque Origin rejection rather than a 401. GET /ggui/health reports the effective allowlist under cors.origins, and the boot banner prints how many origins are allowed.
Operator config
Section titled “Operator config”| Flag | Purpose |
|---|---|
--admin-token <token> |
Pin the admin bearer that gates the console /keys plane. Without this, the server mints a fresh ggui_admin_* per boot and prints it on the banner. Pin a stable value when you want the bearer to survive restarts. |
--keys-file <path> |
JSON file backing the pairing service. When set, paired bearers survive restart. Stored in plaintext at 0600 perms — assume operator-controlled disk (e.g. ~/.ggui/keys.json). |
--ephemeral |
Opt out of the default cross-restart persistence bundle (.ggui/persistent/). With this flag, HMAC secrets, renders, vectors, short-codes, and paired bearers all reset on every restart. Use for tests, CI loops, or incident-response nuclear-revoke. |
--seed-pool <dir> |
Repeatable. Load a read-only shared blueprint pool (a ggui export-pool artifact) for exact-contract reuse, consulted after the server’s own blueprints. |
--mcp-instructions <p> |
Server-level MCP instructions preset (the string injected into the LLM’s system prompt above the tool catalog). Presets: default, aggressive, always, minimal, off. Unset sends none. Also accepts GGUI_MCP_INSTRUCTIONS env (CLI flag wins). |
--local-cli-login |
Opt-in: generate with the Claude login already on this machine (the one claude uses) when no Anthropic API key is set. Anthropic routes only; a real key always wins; refused with --multi-user and --public-demo. See Without an API key. |
Agent runtime supervision
Section titled “Agent runtime supervision”By default, ggui serve boots your agent alongside MCP. The entry file comes from ggui.json:
{ "agent": { "entry": "./agent.ts" }}Supported extensions:
.js/.mjs/.cjs— runs asnode <entry>.ts/.tsx/.mts— runs asnode --import=tsx <entry>(tsxmust be resolvable in your project)
Failure modes:
- No
ggui.jsonor noagent.entry— falls back to MCP-only with a warning. Useful when you want to point an external agent runtime at the server. - Malformed
ggui.jsonor unsupported entry extension — hard error, exits 1 before binding. - Agent crashes after startup — logged, MCP keeps running. No auto-restart — compose that with your own supervisor (systemd, pm2, Docker restart policy).
Generation (bring your own key)
Section titled “Generation (bring your own key)”Component-code generation on a self-hosted server uses your LLM provider key (BYOK). At boot, ggui serve resolves a key in this order:
- Provider env vars —
ANTHROPIC_API_KEY,OPENAI_API_KEY,GOOGLE_API_KEY(falling back toGEMINI_API_KEY),OPENROUTER_API_KEY. The env layer always wins. ~/.ggui/credentials.json— keys pasted at/settingsland here (plaintext,0600).
The model comes from ggui.json#generation.model, in either provider:model (canonical) or provider/model (LiteLLM) form:
{ "generation": { "model": "anthropic:claude-haiku-4-5-20251001" }}GGUI_GENERATION_MODEL env overrides the manifest model — the precedence is GGUI_GENERATION_MODEL env > ggui.json#generation.model. Without a ggui.json, the provider’s default route applies. With one, a key without a model is a hard error (see the rules below). The env value accepts both provider:model (canonical) and provider/model (LiteLLM) forms; a malformed value is a hard boot error, not a silent fallback. It’s the ops escape hatch for pointing one ggui serve instance at a different model without editing the manifest. The per-provider defaults are what that error suggests as the model to pick, and what a server without a ggui.json uses (DEFAULT_ROUTE_BY_PROVIDER in the CLI: anthropic claude-haiku-4-5-20251001, openai gpt-5.6-luna, google gemini-3.5-flash-lite, openrouter anthropic/claude-haiku-4.5). Any id the protocol registry accepts works in generation.model — for Anthropic that is the current lineup (claude-sonnet-5, claude-opus-5, claude-fable-5-1 — the most capable, at roughly ten times the default’s per-render cost) plus claude-fable-5 and the still-served 4.6–4.8 generation; an id outside the registry is rejected at parse time.
Rules:
- A key without a model is a hard error — if a boot key resolves but neither
generation.modelnorGGUI_GENERATION_MODELsets a model,ggui serveexits with an actionable message that shows both accepted model-string forms and suggests the provider’s default. - Bedrock routes are rejected on the OSS path (IAM-only; supported via the hosted runtime only).
- No key at all is not fatal — the banner prints
⚠ no LLM key configured, and renders fall back to a Connect-a-key card pointing at/settings. Pasting a key there takes effect without a restart.
Without an API key: the claude login on this machine (opt-in)
Section titled “Without an API key: the claude login on this machine (opt-in)”ggui serve --local-cli-login generates with the Claude login already on this machine, the one the claude command uses, instead of an API key. It runs Claude Code as Anthropic ships it in the Agent SDK, unmodified. GGUI never asks for your Claude credentials and never stores them: Claude Code signs in with its own login store, or with a CLAUDE_CODE_OAUTH_TOKEN you set in the environment, which GGUI passes to it unchanged. Nothing is sent through a GGUI account. Sign in by running claude yourself first.
- Your Claude plan’s terms and usage limits apply.
- It is slower than an API key. Measured on the same model: ~2.2x slower at the median; ~2.3-5 s of each serial call is the local binary startup (load-dependent); the rest of the gap is unmeasured.
- A real key always wins. With
ANTHROPIC_API_KEYset, or an Anthropic key in~/.ggui/credentials.json, the flag has no effect. - Anthropic routes only. The model rules above still apply, so a
ggui.jsonstill needs ananthropic:generation.model. On any other provider the server says the flag has no effect. - For your own use. One person’s plan never serves other users, so the flag is refused with
--multi-userand--public-demo. - It can stop working if Anthropic changes how Claude Code signs in, or how it runs under the Agent SDK.
The boot banner names the source: generation: anthropic / <model> (the claude command's login on this machine — your Claude plan's terms and usage limits apply).
Persistent storage
Section titled “Persistent storage”By default, ggui serve writes a cross-restart bundle under .ggui/persistent/ (project-local when a ggui.json was resolved, else ~/.ggui/persistent/) so HMAC secrets, renders, vectors, short-codes, and paired bearers survive a restart. claude.ai chat-history revisits keep working without re-pairing.
Bundle layout:
.ggui/persistent/├── ws-token-secret.hex (HMAC, 0600)├── short-codes.sqlite (signed render-URL resolution — backs the /r/<shortCode> viewer)├── sessions.sqlite (GguiSessionStore — renders + event history)├── vectors.sqlite (RAG corpus)└── keys.json (paired bearers)Override the directory with GGUI_PERSISTENT_DIR. Pass --ephemeral to skip the bundle entirely.
Explicit ggui.json#storage declarations always win. Declare them to override paths or swap a single surface back to memory while keeping the rest persistent:
{ "storage": { "renders": { "driver": "sqlite", "path": "./ggui-sessions.sqlite" }, "vectors": { "driver": "sqlite", "path": "./ggui-vectors.sqlite" }, "threads": { "driver": "sqlite", "path": "./ggui-threads.sqlite" } }}| Store | Default | sqlite driver requires |
|---|---|---|
renders |
sqlite under .ggui/persistent/sessions.sqlite (opt out with --ephemeral or { "driver": "memory" }) |
better-sqlite3 peer dep |
vectors |
sqlite under .ggui/persistent/vectors.sqlite (opt out with --ephemeral or { "driver": "memory" }) |
better-sqlite3 peer dep |
threads |
routes unmounted unless declared (opt-in) | better-sqlite3 peer dep |
Paths in ggui.json#storage resolve relative to the ggui.json directory, regardless of where ggui serve was invoked from.
For threads specifically, driver: "memory" mounts the routes but data resets on restart (/ggui/health reports threads.durability: "ephemeral"); driver: "sqlite" is durable.
Recommended setups
Section titled “Recommended setups”Local-only smoke (no tunnel, no remote MCP host):
ggui serveclaude.ai custom connector (over a public tunnel):
# Terminal 1: tunnelcloudflared tunnel --url http://localhost:6781
# Terminal 2: serveggui serve --oauth \ --public-base-url https://<tunnel>.trycloudflare.comNote the PAIR_CODE from the boot banner. Visit the public URL, complete pairing, save the bearer. Then in claude.ai → Settings → Connectors → Add custom connector, point it at https://<tunnel>.trycloudflare.com/mcp, leave Client ID / Secret empty (the server uses Dynamic Client Registration), click Connect, and paste the bearer when prompted.
Quick local-dev without auth (NEVER over a public tunnel):
ggui serve --dev-allow-allFor the full pairing walkthrough, see Self-hosted pairing.
Production hardening
Section titled “Production hardening”The default auth shape is dev-mode pairing. For production, swap in a real AuthAdapter by composing createGguiServer() directly in your agent entrypoint instead of running the CLI:
import { createGguiServer } from "@ggui-ai/mcp-server";
const server = createGguiServer({ auth: {/* your AuthAdapter — OIDC, Cognito, custom */},});The adapter gates both /mcp and the live-channel /ws upgrade. See Reference deploys for Docker / Fly / Render manifests.
Current limits
Section titled “Current limits”- Strict-auth only —
/mcprejects any bearer not pair-minted (or relaxed via--dev-allow-all/--public-demo). - Single-user by default — every request scopes to one
builderapp ID. Use--multi-userto scope per authenticated user. - No auto-restart on agent crash — compose with your own supervisor.
See also
Section titled “See also”gguiCLI overview — the full command surface.ggui login— sign intoapi.ggui.aiforggui_user_*connector keys (separate fromggui serve’s pairing flow).- Self-hosted pairing — pair a viewer client to a
ggui serveinstance. - Reference deploys — Docker, Fly, Render manifests for
ggui serve.