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.
Is the MCP server reachable?
Section titled “Is the MCP server reachable?”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.
Authentication
Section titled “Authentication”401 Unauthorized from the MCP transport
Section titled “401 Unauthorized from the MCP transport”- 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
sandboxkey 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 servedefaults to strict pairing-based auth — a401means your bearer wasn’t pair-minted; use the pairing flow or--dev-allow-allon localhost. If you wrapped it behind your own auth, double-check the header your reverse proxy expects.
→ Recovery patterns: Error Handling cookbook.
403 Forbidden (capability denied)
Section titled “403 Forbidden (capability denied)”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.
Connection
Section titled “Connection”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: websocketheader silently; the symptom is upgrade requests that never return101 Switching Protocolsin 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.
Renders
Section titled “Renders”Session not found (-32002)
Section titled “Session not found (-32002)”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.
Cross-environment render mismatch
Section titled “Cross-environment render mismatch”Sandbox and production renders live on separate appIds. If you’re moving between environments, regenerate the appId + key pair — renders never migrate.
Component rendering
Section titled “Component rendering”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:
- A partial or failed generation was served (the server’s generation pipeline normally repairs these before delivery).
- A custom
gadgetreturned a renderer that does not exportdefault. - Generation logs (visible in the Console or your operator dashboard) show an esbuild error.
Component shows the fallback border
Section titled “Component shows the fallback border”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).
Gadgets and tools
Section titled “Gadgets and tools”Tool "X" not registered
Section titled “Tool "X" not registered”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.
Circular dependency detected
Section titled “Circular dependency detected”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.
HTTP status codes
Section titled “HTTP status codes”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.
| Status | Meaning |
|---|---|
401 | Missing or invalid Authorization header — see Authentication |
403 | Capability denied; key is valid but not authorised for this appId or method |
404 | Unknown route, or appId does not exist on this environment |
429 | Rate limited. Inspect Retry-After (seconds) and back off — see Rate limits |
5xx | Server-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.
JSON-RPC error codes
Section titled “JSON-RPC error codes”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 |
-32001 | Auth failed | Invalid or expired API key (in-band variant; the HTTP 401 form is more common) |
-32002 | Session not found | Session expired or never existed |
-32003 | App not found | App ID does not exist |
-32004 | Generation failed | UI generation failed (model, compile, or contract error). Match on the code. |
-32005 | Capability denied | The session or key lacks the capability required by the call |
-32010 | Generation quota exceeded | Platform extension (-32010 range): the app’s generation quota is exhausted |
-32011 | App limit exceeded | Platform extension: the account has too many apps |
-32012 | Concurrent session limit | Platform extension: too many live GguiSessions at once |
-32013 | Rate limit exceeded | Platform extension reserved for in-band rate signalling — HTTP 429 is what ships today |
-32020 | Contract violation | Platform extension: a payload violated the declared contract |
Reading errors from Claude Agent SDK
Section titled “Reading errors from Claude Agent SDK”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.
Generation timeout
Section titled “Generation timeout”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 pairedggui_renderthen returnscache: {hit: true, llmCallsAvoided, ...}. A miss showsorigin === 'agent'/'synth'andcache.hit: false. - Subsequent calls with the same
actionSpecshould 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.
Debugging tips
Section titled “Debugging tips”- 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-resultpostMessage carrying the sameai.ggui/renderslice (propsJson) the WS path projects — one projection, two envelopes. - Console app —
@ggui-ai/consolegives 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 conformanceagainst your endpoint; protocol-level mismatches show up as named violations, not stack traces. See Conformance.