Skip to content
Public preview · the console is open

ggui serve

read as .md

ggui 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.

Terminal window
ggui serve

Binds 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:

Terminal window
GGUI_MCP_URL=http://127.0.0.1:6781/mcp
GGUI_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.

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]
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).

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.
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.
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.

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.

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 as node <entry>
  • .ts / .tsx / .mts — runs as node --import=tsx <entry> (tsx must be resolvable in your project)

Failure modes:

  • No ggui.json or no agent.entry — falls back to MCP-only with a warning. Useful when you want to point an external agent runtime at the server.
  • Malformed ggui.json or 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).

Component-code generation on a self-hosted server uses your LLM provider key (BYOK). At boot, ggui serve resolves a key in this order:

  1. Provider env vars — ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY (falling back to GEMINI_API_KEY), OPENROUTER_API_KEY. The env layer always wins.
  2. ~/.ggui/credentials.json — keys pasted at /settings land 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.model nor GGUI_GENERATION_MODEL sets a model, ggui serve exits 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_KEY set, 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.json still needs an anthropic: 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-user and --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).

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.

Local-only smoke (no tunnel, no remote MCP host):

Terminal window
ggui serve

claude.ai custom connector (over a public tunnel):

Terminal window
# Terminal 1: tunnel
cloudflared tunnel --url http://localhost:6781
# Terminal 2: serve
ggui serve --oauth \
--public-base-url https://<tunnel>.trycloudflare.com

Note 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):

Terminal window
ggui serve --dev-allow-all

For the full pairing walkthrough, see Self-hosted pairing.

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.

  • Strict-auth only — /mcp rejects any bearer not pair-minted (or relaxed via --dev-allow-all / --public-demo).
  • Single-user by default — every request scopes to one builder app ID. Use --multi-user to scope per authenticated user.
  • No auto-restart on agent crash — compose with your own supervisor.