Skip to content
Public preview — you found hosted GGUI early · the console is open

Troubleshooting

read as .md

A symptom-first index. Skim until you find the error string you’re seeing, then jump to the linked deep-dive. For typed handling of every protocol error class in code, the Error Handling cookbook is the canonical reference — this page is the lookup table. The authoritative numeric error table lives in MCP Protocol → Error Codes.

GGUI speaks plain MCP. Connect with any MCP-compliant client. The minimum smoke-test using the official TypeScript SDK:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:6781/mcp"), {
requestInit: { headers: { Authorization: "Bearer dev" } },
});
const client = new Client({ name: "ggui-smoke", version: "0.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(
"ok",
tools.map((t) => t.name)
);

If connect() rejects, the transport never produced a successful HTTP response — diagnose with the HTTP status table below. If connect() succeeds but listTools() rejects, you have a JSON-RPC error — see JSON-RPC error codes.

The snippet assumes a local ggui serve --dev-allow-all, where any non-empty bearer (e.g. Bearer dev) works; the strict default requires a pair-minted bearer. Hosted cloud: use https://mcp.ggui.ai/apps/<appId> — no /mcp suffix — with a ggui_user_* connector key.


  • Connector keys start with ggui_user_ — verify the prefix and that no characters were lost to a copy-paste.
  • Hosted GGUI keys (cloud) are environment-scoped. A sandbox key will not work against the production project and vice versa.
  • On the hosted cloud, rotate the key in console.ggui.ai (apps → keys) if you suspect leakage.
  • Self-hosted ggui serve defaults to strict pairing-based auth — a 401 means your bearer wasn’t pair-minted; use the pairing flow or --dev-allow-all on localhost. If you wrapped it behind your own auth, double-check the header your reverse proxy expects.

→ Recovery patterns: Error Handling cookbook.

The key authenticated but is not authorised for this app or method. It surfaces as a bare 403 whose JSON-RPC body carries -32007 — see the HTTP table; -32005 never had a first-party emitter and is retired-reserved (GGUI#910).


WebSocket connection stuck on 'reconnecting'

Section titled “WebSocket connection stuck on 'reconnecting'”

The render runs in a sandboxed iframe (<AppRenderer>); the iframe-runtime owns the live-channel WebSocket and reconnects with exponential backoff (1 s → 30 s). After the retry budget exhausts it latches at 'disconnected'. To resume:

  • Remount the <AppRenderer> — a fresh boot re-runs the bootstrap and opens a new socket.
  • Inspect your network path. Many corporate proxies strip the Upgrade: websocket header silently; the symptom is upgrade requests that never return 101 Switching Protocols in the network tab.

The wsUrl the iframe connects to is server-stamped on the render’s ai.ggui/render slice (ws:// on the local ggui serve default; wss:// once TLS fronts it) — you don’t configure it host-side. Wire format: WebSocket Protocol.

Actions feel “dropped” during a flaky connection

Section titled “Actions feel “dropped” during a flaky connection”

The iframe-runtime buffers outbound actions while the socket is reconnecting and flushes them on resume — actions aren’t lost across a transient drop. A generated component can disable destructive submits while offline, but that’s component behavior inside the iframe, not host wiring.

→ The host surfaces connection trouble via the ggui:observe channel (subscribe-failed). See Error Handling → renderer-side faults.

CONTRACT_VIOLATION error frame on the live channel

Section titled “CONTRACT_VIOLATION error frame on the live channel”

An inbound action whose name is undeclared, or whose payload fails the contract’s actionSpec[name].schema, is rejected with a typed CONTRACT_VIOLATION error frame on the live channel — nothing reaches the consume buffer. ggui_render / ggui_emit validation failures instead reject the agent’s own tool call. (The earlier _ggui:contract-error channel + ContractErrorPayload shape were removed in draft-2026-06-11.) See MCP Protocol → Error Codes.


A render has been reaped (TTL elapsed, or server restart). Re-run the in-flight intent through ggui_handshake + ggui_render; ggui_consume returns whatever events were collected before the render expired (status: 'expired').

→ Lifecycle: ggui_handshake / ggui_render. Recovery pattern: Error Handling → Recover from an expired render.

Sandbox and production renders live on separate appIds. If you’re moving between environments, regenerate the appId + key pair — renders never migrate.


Module does not export a default function component

Section titled “Module does not export a default function component”

The generated bundle is malformed. Causes, in order of likelihood:

  1. A partial or failed generation was served (the server’s generation pipeline normally repairs these before delivery).
  2. A custom gadget returned a renderer that does not export default.
  3. Generation logs (visible in the Console or your operator dashboard) show an esbuild error.

A fault in generated component code is isolated to its sandboxed iframe — it can’t crash your host tree. The host’s <AppRenderer onError> fires for iframe/transport faults; structured failures (contract errors, subscribe failures) arrive on the ggui:observe channel.

<AppRenderer
toolName="ggui_render"
sandbox={sandbox}
html={html}
onError={(err) => console.warn("render error", err)}
/>

Regeneration of broken component code happens server-side in the generation pipeline; the iframe re-mounts with corrected HTML on the next resources/read. See Error Handling → renderer-side faults.

Renderer still shows old code after I redeployed

Section titled “Renderer still shows old code after I redeployed”

The browser-side module cache keys on contractHash. A redeploy that does not bump the hash will not invalidate the cached factory. Either:

  • Trigger a new generation by changing the agent prompt or the actionSpec, which advances the hash; or
  • Hard-reload the iframe (<AppRenderer> re-mount).

[gadget] export "X" from package "Y" is not loaded

Section titled “[gadget] export "X" from package "Y" is not loaded”

The generated component called a gadget export that the operator never registered on App.gadgets. Add the package (or the specific export) to ggui.json#app.gadgets — see SDK → Gadgets → Operator registration.

gadget_not_registered / gadget_package_mismatch / gadget_public_env_missing

Section titled “gadget_not_registered / gadget_package_mismatch / gadget_public_env_missing”

Render-time validation rejections, not runtime errors: gadget_not_registered means the contract references an export the operator hasn’t registered; gadget_package_mismatch means the referenced export belongs to a different registered package than the one keyed; gadget_public_env_missing means a wrapper’s requires aren’t satisfied by App.publicEnv. All three name the offending key so the operator fix is unambiguous — see SDK → Gadgets.


The MCP transport surfaces transport-level failures as HTTP status codes on the /mcp endpoint. Read the status first. A refusal may also carry a JSON-RPC error body whose code comes from the table below — every 401 carries -32007, and on hosted GGUI a 503 for a saturated generation queue carries -32014 and a 403 for a deprovisioned app carries -32003 with a data.refusal you can act on — so either signal identifies it. An unmapped server failure is a 500 whose body carries -32603.

Status Meaning
401 Missing or invalid Authorization header — see Authentication
403 Not authorised for this appId or method. A bare 403 carries -32007 and says no more by design; a deprovisioned app answers 403 with -32003 and data.refusal — see JSON-RPC error codes
404 Unknown route, or appId does not exist on this environment
429 Rate limited at the ingress — hosted GGUI’s per-IP flood backstop, plain HTTP with no JSON-RPC body; honor Retry-After (seconds). A render-rate cap never arrives this way: it is an in-band refusal — see Rate limits
5xx Server-side failure. A 503 with Retry-After (body -32014) is hosted GGUI’s saturated generation queue — wait that long; otherwise retry with jitter, then file a support ticket with the response x-request-id

A render-rate cap never surfaces as HTTP at all. On tools/call a limited ggui_render resolves as a refusal — isError: true, structuredContent.outcome: 'refused', refusal.code app_rate_limited (or issuer_rate_limited for the issuing identity’s cap), retry: 'later', the wait in message and fix, handshake intact — so wait and retry the same call with the same handshakeId; see Rate limits. The only 429 hosted GGUI sends is the ingress’s per-IP backstop, plain HTTP with Retry-After and no JSON-RPC body; no first-party server emits -32013. Honor Retry-After wherever the server sends one — hosted GGUI’s 503 for a saturated generation queue always does, and its body carries -32014 GENERATION_OVERLOADED.

Successful HTTP responses (200) may still carry a JSON-RPC error object with a numeric code. A server may add a data field with a requestId you can quote when filing support. Authoritative reference: MCP Protocol → Error Codes.

Code Name Meaning
-32700 Parse error Malformed JSON in the request body
-32600 Invalid request Required JSON-RPC fields missing (jsonrpc, method)
-32601 Method not found Unknown method or tool name
-32602 Invalid params Wrong types or missing arguments
-32603 Internal error Server-side failure
-32007 Unauthorized Missing, invalid or expired bearer, or a handler that needs a signed-in user — rides HTTP 401; the same code is in the response’s JSON-RPC body
-32002 Session not found Session expired or never existed
-32003 App not found The endpoint no longer serves this app; for a deprovisioned app the body carries data.refusal
-32006 Mount unavailable A resources/read found the render locator but can return no mount for it
-32014 Generation overloaded Platform extension: the generation queue is saturated — HTTP 503 with Retry-After governs the retry; the server, not the caller, is the limit

-32004 is retired-reserved. Generation failure never surfaces as a JSON-RPC error: ggui_render returns a tools/call result with isError: true and structuredContent.error: { code, message } — codes PRODUCTION_FAILED | VALIDATION_ERROR | NO_PLATFORM_KEY | NO_CREDENTIALS | GENERATION_QUEUE_OVERLOADED. Match on error.code, not on a numeric JSON-RPC code. A failed render carries no resourceUri and no _meta — do not attempt to mount it.

-32006 answers a resources/read of a render locator that cannot come back as a mount. Match on error.data.code for the reason — NOT_FOUND (nothing resolves it, or you may not read it), BLUEPRINT_UNRESOLVABLE (the component behind it is gone), NOT_SUPPORTED (this deployment keeps no durable record, so an evicted render can never be restored here) or NOT_MOUNTABLE (it resolved, but nothing can deliver it). NOT_FOUND arrives on -32002 rather than -32006. None of the four is worth retrying — ask the agent for a fresh render instead.

-32003 on a 403 is the endpoint-level refusal: the app the per-app endpoint serves has been deprovisioned, and every JSON-RPC request from a caller bound to that app — initialize included — is refused before any tool runs. The body carries data.refusal { code, message, fix, retry, appId }: code is app_deprovisioned, retry is never, fix is the tenant-side repair (re-provision the app), and appId is the app the refused endpoint serves — the same id as the endpoint path — so key your repair loop on it instead of parsing the message. Retry per data.refusal.retry — today never: nothing on the caller’s side changes the answer. It is the app’s state, not yours: a revoked subject on a deprovisioned app gets this answer too, before its own deny status is read, and learns only an id it already holds. An anonymous request never sees it — it is refused 401 by the auth adapter first — and a caller bound to a different app gets the bare 403 before any lookup. A 403 with -32007 and no data is a different thing — an authorization failure the server declines to explain (no issuer, a foreign issuer, a denied subject on a live app, a key bound elsewhere or not the owner’s); check the credential and the appId in the URL.

When you drive GGUI from the Claude Agent SDK, JSON-RPC errors arrive inside the SDKMessage stream rather than as thrown exceptions — tool results land inside user messages as tool_result content blocks. Walk the stream and match on is_error:

import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({ prompt, options })) {
if (message.type === "user") {
for (const block of message.message.content) {
if (block.type === "tool_result" && block.is_error) {
// block.content holds the JSON-RPC error.message string
console.error("tool failed:", block.tool_use_id, block.content);
}
}
}
}

Host SDKs (@anthropic-ai/claude-agent-sdk, @modelcontextprotocol/sdk, openai) each define their own error types — consult their docs for the thrown shape. GGUI guarantees the wire error (HTTP status + JSON-RPC code), not the host-SDK’s exception class.

Bump your transport’s timeout or shorten the agent prompt. Most timeouts are constraint-misalignment in the prompt, not raw slowness. With the @modelcontextprotocol/sdk client:

await client.callTool({ name: "ggui_handshake", arguments }, undefined, {
timeout: 60_000, // 60 s
});

If you control the operator side, check Blueprint-First Architecture — a matched blueprint short-circuits generation entirely.

First render feels slow (no error, just wait)

Section titled “First render feels slow (no error, just wait)”

This is the blueprint-cache miss path, not a bug. The two-stage flow is: a cheap LLM match against cached blueprints → full generation if no match (cold renders typically land in 10–20 s). Symptoms:

  • A cache hit surfaces as suggestion.origin === 'cache' on the handshake; the paired ggui_render then returns cache: {hit: true, llmCallsAvoided, ...}. A miss shows origin === 'agent' / 'synth' and cache.hit: false.
  • Subsequent calls with the same actionSpec should hit cache (cache.hit: true) and avoid regeneration cost.

If repeated identical prompts never warm the cache, the prompt or actionSpec is varying between calls in a way that advances contractHash. Stabilise the inputs or pre-register a blueprint against the operator endpoint. See Blueprint-First Architecture.


tsc under moduleResolution: NodeNext reports errors inside node_modules/@modelcontextprotocol/ext-apps

Section titled “tsc under moduleResolution: NodeNext reports errors inside node_modules/@modelcontextprotocol/ext-apps”

Your own code is fine; the errors sit in a dependency’s declarations. With moduleResolution: "NodeNext" (or Node16), tsc reports TS2834 and TS2339 inside node_modules/@modelcontextprotocol/ext-apps/dist/src/app.d.ts, and TS2305 on @ggui-ai/protocol/dist/types/host-context.d.ts for McpUiHostContext, McpUiHostCapabilities and McpUiDisplayMode.

Cause. @modelcontextprotocol/ext-apps 1.7.5 ships extensionless relative specifiers (./types, ./events, ./standard-schema) in its own .d.ts files — a defect in the published package, tracked upstream at modelcontextprotocol/ext-apps#704. @ggui-ai/protocol re-exports the MCP Apps spec types from that package rather than copying them (the spec is a frozen external surface, and a copy would drift the moment it moves), so the three host-context names cascade from the same defect.

Fix. Until ext-apps ships the upstream fix, set the TypeScript switch for third-party declaration defects in the consumer’s tsconfig.json:

{
"compilerOptions": {
"skipLibCheck": true
}
}

skipLibCheck skips type-checking of .d.ts files only — your own sources still typecheck fully. The @ggui-ai/protocol dependency bump follows the day the upstream fix is published; nothing in your code changes then except removing the switch if you want it gone.

  • Network tab — inspect the WebSocket frames at /ws (self-hosted: ws://127.0.0.1:6781/ws; hosted cloud: wss://mcp.ggui.ai/ws) and the JSON-RPC bodies on /mcp.
  • MCP Apps host postMessage — for non-WebSocket hosts (Claude Desktop, generic MCP clients), props updates arrive as a ui/notifications/tool-result postMessage carrying the same ai.ggui/render slice (propsJson) the WS path projects — one projection, two envelopes.
  • Console app@ggui-ai/console gives you a live session inspector with raw envelopes, generation logs, and a contract diff viewer.
  • Conformance kit — if you’re building your own client or server, run ggui conformance against your endpoint; protocol-level mismatches show up as named violations, not stack traces. See Conformance.