Skip to content

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 (coming soon): 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, coming soon) 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, coming soon) 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 the session or key lacks the capability required by the call. Surfaces as JSON-RPC code -32005 on tool calls — see the error table.


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 (JSON-RPC code -32020) 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).

Built-in tools register on import. If you removed the side-effect import of @ggui-ai/react (tree-shaking, custom barrel), put it back at module top-level.

Custom tools must be registered before the first submit — see SDK → Gadgets.

Your dependsOn graph has a cycle. Tool dependencies must form a DAG. Print the registered names from your registry and walk the edges manually until you find the loop.


The MCP transport surfaces transport-level failures as plain HTTP status codes on the /mcp endpoint. Read these before parsing any JSON-RPC body — a non-2xx status means there is no JSON-RPC envelope to read.

StatusMeaning
401Missing or invalid Authorization header — see Authentication
403Capability denied; key is valid but not authorised for this appId or method
404Unknown route, or appId does not exist on this environment
429Rate limited. Inspect Retry-After (seconds) and back off — see Rate limits
5xxServer-side failure; retry with jitter, then file a support ticket with the response x-request-id

Rate-limit responses are HTTP-only — they do NOT travel as JSON-RPC errors. The protocol reserves a platform-extension code -32013 (RATE_LIMIT_EXCEEDED) for in-band signalling, but the live server emits the HTTP-429 form exclusively today. Write retry logic against the 429 path.

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.

CodeNameMeaning
-32700Parse errorMalformed JSON in the request body
-32600Invalid requestRequired JSON-RPC fields missing (jsonrpc, method)
-32601Method not foundUnknown method or tool name
-32602Invalid paramsWrong types or missing arguments
-32603Internal errorServer-side failure
-32001Auth failedInvalid or expired API key (in-band variant; the HTTP 401 form is more common)
-32002Session not foundSession expired or never existed
-32003App not foundApp ID does not exist
-32004Generation failedUI generation failed (model, compile, or contract error). Match on the code.
-32005Capability deniedThe session or key lacks the capability required by the call
-32010Generation quota exceededPlatform extension (-32010 range): the app’s generation quota is exhausted
-32011App limit exceededPlatform extension: the account has too many apps
-32012Concurrent session limitPlatform extension: too many live GguiSessions at once
-32013Rate limit exceededPlatform extension reserved for in-band rate signalling — HTTP 429 is what ships today
-32020Contract violationPlatform extension: a payload violated the declared contract

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 in ~100 ms.

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 (~100 ms when it hits) → full generation if no match (typically several seconds). 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 and be fast.

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.


  • Network tab — inspect the WebSocket frames at /ws (self-hosted: ws://127.0.0.1:6781/ws; hosted cloud, coming soon: 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.