---
title: Troubleshooting
description: Symptom-first index for the common ggui failures — auth, transport reachability, session expiry, component render, tool registry, and JSON-RPC error codes.
---

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](/cookbook/error-handling/) is the canonical reference — this page is the lookup table. The authoritative numeric error table lives in [MCP Protocol → Error Codes](/api/mcp-protocol/#error-codes).

## Is the MCP server reachable?

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

```typescript
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](#http-status-codes) below. If `connect()` succeeds but `listTools()` rejects, you have a JSON-RPC error — see [JSON-RPC error codes](#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

### `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 `sandbox` key will not work against the production project and vice versa.
- On the hosted cloud, rotate the key in [console.ggui.ai](https://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](/cookbook/error-handling/).

### `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](#json-rpc-error-codes).

---

## Connection

### 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](/api/websocket-protocol/).

### 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](/cookbook/error-handling/#renderer-side-faults-stay-inside-the-iframe).

### `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](/api/mcp-protocol/#error-codes).

---

## Renders

### 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`](/api/mcp-protocol/). Recovery pattern: [Error Handling → Recover from an expired render](/cookbook/error-handling/#recover-from-an-expired-render--32002).

### Cross-environment render mismatch

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

---

## Component rendering

### `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](/clients/console/) or your operator dashboard) show an esbuild error.

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

```tsx
<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](/cookbook/error-handling/#renderer-side-faults-stay-inside-the-iframe).

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

### `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](/sdk/gadgets/).

### `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

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](#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](/api/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

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](/api/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

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

```typescript
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

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:

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

If you control the operator side, check [Blueprint-First Architecture](/architecture/ui-generator/) — a matched blueprint short-circuits generation in ~100 ms.

### 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](/architecture/ui-generator/).

---

## 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-result` postMessage carrying the same `ai.ggui/render` slice (`propsJson`) the WS path projects — one projection, two envelopes.
- **Console app** — [`@ggui-ai/console`](/clients/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](/protocol/conformance/).