All posts

How to Debug ChatGPT Apps: Troubleshooting Guide for MCP App Development (July 2026)

Abe Wheeler
ChatGPT AppsChatGPT App FrameworkChatGPT App TestingMCP AppsMCP App FrameworkMCP App TestingTutorialDebugging
Debug ChatGPT Apps locally with the sunpeak inspector and browser DevTools.

Debug ChatGPT Apps locally with the sunpeak inspector and browser DevTools.

Debugging a ChatGPT App is usually debugging an agreement between five pieces: the MCP server, the tool definition, the tool result, the app resource, and the host iframe. If any one of those pieces drifts, the symptom can look the same: a blank iframe, old UI, missing data, a tool the model will not call, or a view that only breaks after you expand it.

TL;DR: Reproduce the bug in the sunpeak Inspector first. Check the tool and resource contract before rewriting React. Confirm _meta.ui.resourceUri, structuredContent, app-only _meta, the resource MIME type, CSP, display mode, and host context. Then lock the bug into a simulation and a test so it does not come back.

This guide covers the debugging path for ChatGPT Apps, which are MCP Apps running in ChatGPT. The same checks also help when the same app runs as an interactive Claude Connector or in another MCP Apps host.

Start With the Local Inspector

The fastest ChatGPT App debugging loop is local. With sunpeak, a new app project includes an MCP server, app resources, simulations, Playwright tests, and a local Inspector that replicates the host runtime.

npx sunpeak new sunpeak-app
cd sunpeak-app
pnpm dev

The local setup gives you:

  • A browser-based Inspector where you can render each tool and resource.
  • A ChatGPT-style host runtime with theme, display mode, viewport, and tool state controls.
  • Browser DevTools for the iframe that renders your app.
  • Simulation files for saved tool inputs and tool results.
  • Test fixtures for protocol checks, iframe checks, visual regression, and evals.

If your MCP server already exists, point the Inspector at it:

npx sunpeak inspect --server http://localhost:8000/mcp

That keeps your backend where it is while giving you a local host surface for the app UI. It is also a good way to separate host bugs from server bugs. If the app fails in both the Inspector and ChatGPT, debug your contract or UI. If it only fails in ChatGPT, check deployment, caching, development app refresh, and host-specific behavior.

Verify the Tool and Resource Contract First

Before you inspect component state, verify the MCP contract. A ChatGPT App renders only after the host can discover a tool, call it, find the linked resource, load the resource iframe, and deliver the result to the app.

Check these items in order:

CheckWhat to look forCommon failure
Tool registrationCorrect name, title, description, input schema, and annotationsThe model calls the wrong tool or sends invalid arguments
Resource linkTool result or tool metadata points to _meta.ui.resourceUriThe host receives data but has no UI to render
Resource responseResource URI returns HTML with an MCP App-compatible MIME type such as text/html;profile=mcp-appThe iframe never loads
Result dataModel-visible fields are in structuredContentThe UI reads fields that are missing or renamed
App-only dataPrivate or UI-only fields are in _metaSensitive data leaks into model-visible output
Compatibility fieldsOlder OpenAI-specific keys mirror the standard fields when neededChatGPT and another host read different resource URIs

For new cross-host work, make the MCP Apps fields your source of truth. OpenAI’s current Apps SDK reference still documents ChatGPT-specific compatibility behavior, but portable apps should keep standard MCP Apps metadata, content, and resource links at the center. sunpeak wraps that lower-level bridge in hooks, so most app code should not need to touch window.openai directly.

Debug Blank Iframes

A blank iframe usually means the app resource loaded and then crashed, or the resource never loaded at all.

Use this order:

  1. Open DevTools and switch to the app iframe console.
  2. Check for a render error, failed dynamic import, missing asset, or hook error.
  3. Open the Network tab and find the resource URI request.
  4. Confirm the response is HTML, not a JSON error page or auth redirect.
  5. Check CSP if scripts, fonts, images, or API calls are blocked.
  6. Reproduce the same tool call in the sunpeak Inspector.

Guard every read from tool data. Tool output can be absent on the first render, can be empty, or can differ between a saved simulation and a live tool result.

import { useToolData } from 'sunpeak';

export function OrdersResource() {
  const { output } = useToolData<{
    orders?: Array<{ id: string; total: number }>;
  }>();

  if (!output) {
    return <p>Loading orders...</p>;
  }

  const orders = output.orders ?? [];

  if (orders.length === 0) {
    return <p>No orders matched this filter.</p>;
  }

  return (
    <ul>
      {orders.map((order) => (
        <li key={order.id}>
          {order.id}: ${order.total}
        </li>
      ))}
    </ul>
  );
}

That pattern is not only for polish. It keeps one missing field from blanking the entire iframe.

Debug Tool Result Shape Bugs

Many ChatGPT App bugs are shape bugs. The server returns items, the component reads results, and the UI quietly shows an empty state. Or the server moves a field into _meta, but the resource still reads it from structuredContent.

Save the failing state as a simulation:

{
  "tool": "show-orders",
  "toolInput": {
    "arguments": {
      "status": "open"
    }
  },
  "toolResult": {
    "content": [{ "type": "text", "text": "Found 2 open orders." }],
    "structuredContent": {
      "orders": [
        { "id": "ord_101", "total": 49.95 },
        { "id": "ord_102", "total": 129.5 }
      ]
    },
    "_meta": {
      "cursor": "next_page_token",
      "debugRequestId": "req_abc123"
    }
  }
}

Then add a protocol test that checks the exact fields the resource reads:

import { test, expect } from 'sunpeak/test';

test('show-orders returns the resource data contract', async ({ mcp }) => {
  const result = await mcp.callTool('show-orders', { status: 'open' });

  expect(result.isError).toBeFalsy();
  expect(result.structuredContent).toHaveProperty('orders');
  expect(Array.isArray(result.structuredContent.orders)).toBe(true);
  expect(result.structuredContent.orders[0]).toHaveProperty('id');
  expect(result.structuredContent.orders[0]).toHaveProperty('total');
});

This catches contract drift before a user sees an empty app. It also gives you a clean place to enforce privacy boundaries: data the model can reason about belongs in structuredContent, while app-only state belongs in _meta.

Debug Stale UI and Resource Caching

If ChatGPT shows yesterday’s app after you deployed a fix, assume caching before assuming the fix failed.

Check these items:

  1. Did the resource URI change when the bundle changed?
  2. Did the development app refresh from the ChatGPT Plugins page?
  3. Did you start a new chat after changing tool names, descriptions, schemas, or annotations?
  4. Are compatibility metadata fields pointing at the same current resource URI?
  5. Is a CDN, worker, or reverse proxy serving an old HTML response?

sunpeak cache-busts resource URIs during builds. If you hand-roll the MCP server, append a content hash or build timestamp to the resource URI and make that URI the one your tool returns. Do not reuse the same URI for different bundles unless you are sure every host and CDN layer will revalidate it.

Debug CSP, CORS, and External Fetches

ChatGPT Apps render inside sandboxed iframes, so browser security rules are part of your app contract. A local app can work in the Inspector and still fail in the host if the deployed resource blocks a script, image, font, API call, or OAuth redirect.

Use the Network and Console tabs together:

  • CSP errors usually say which directive blocked the URL.
  • CORS errors usually show up on client-side fetch calls to your API.
  • Auth redirects often return HTML login pages where the app expected JSON.
  • Missing assets often point at relative URLs that worked locally but not behind the resource origin.

Prefer server-side tool handlers for authenticated data fetches. The tool handler can use the user’s auth context, return model-visible summaries in structuredContent, and put app-only state in _meta. Client-side fetches are still useful for public assets, polling a public endpoint, or app-specific browser behavior, but they need explicit CSP and CORS support.

For a deeper sandbox checklist, see MCP App iframe sandbox: origins, CSP, postMessage, and CORS.

Debug Display Mode and Host Context

Display mode bugs happen when app code treats the host surface as fixed. Current MCP Apps use host context and display mode negotiation. The app can request a mode, but the host decides what it supports and reports the actual mode back.

Test at least these states:

  • inline, for compact conversation UI.
  • fullscreen, for dashboards, tables, maps, editors, and setup flows.
  • pip, for small persistent views where the host supports picture-in-picture.
  • Light and dark themes.
  • Narrow viewport, long labels, and safe area changes.

With sunpeak tests, render the same tool in each mode:

import { test, expect } from 'sunpeak/test';

const modes = ['inline', 'fullscreen', 'pip'] as const;

for (const displayMode of modes) {
  test(`orders renders in ${displayMode}`, async ({ inspector }) => {
    const result = await inspector.renderTool('show-orders', undefined, { displayMode });
    const app = result.app();

    await expect(app.getByText('Open orders')).toBeVisible();
    await expect(app).toHaveScreenshot(`orders-${displayMode}.png`);
  });
}

Do not auto-expand on mount. Tie display mode requests to user actions, check available modes before showing controls, and render from the actual mode after the request resolves.

Debug Tool Calling Problems

If ChatGPT does not call your tool, or calls it with bad arguments, inspect the tool definition rather than the UI.

Look for:

  • A vague description that does not say when to use the tool.
  • A schema that marks required fields as optional.
  • Enum values that do not match the app’s natural language.
  • Missing annotations such as readOnlyHint for safe read tools.
  • A destructive action that lacks clear confirmation behavior.
  • Two tools with overlapping descriptions.

Add a registration test:

import { test, expect } from 'sunpeak/test';

test('show-orders is registered for safe reads', async ({ mcp }) => {
  const tools = await mcp.listTools();
  const tool = tools.find((item) => item.name === 'show-orders');

  expect(tool).toBeTruthy();
  expect(tool?.annotations?.readOnlyHint).toBe(true);
  expect(tool?.inputSchema.required).toContain('status');
});

For model behavior, add evals that ask realistic user questions and assert the selected tool plus arguments. Keep those evals focused. You want to know whether the model can pick the right tool and fill the schema, not whether every sentence in the answer is identical.

Debug in Real ChatGPT

Some bugs only show up in the real host. Use ChatGPT for live confirmation, but keep the local Inspector open because it gives you faster state control.

When debugging live:

  1. Confirm the tunnel or production MCP endpoint is reachable.
  2. Confirm the URL includes the correct MCP path, often /mcp.
  3. Refresh the app or connector definition after metadata changes.
  4. Start a new chat after schema, tool name, or annotation changes.
  5. Open DevTools and switch to the newest app iframe context.
  6. Check the Network tab for failed MCP calls and failed resource requests.
  7. Compare the live tool result with the simulation that passes locally.

If live ChatGPT fails but the Inspector passes, write down the specific difference: different display mode, missing host capability, blocked resource URL, stale resource URI, different auth state, or a host-only bridge method. That difference is the bug.

A Practical Debugging Order

Use this order when a ChatGPT App misbehaves:

  1. Reproduce locally in the sunpeak Inspector.
  2. Check iframe console and resource network requests.
  3. Verify _meta.ui.resourceUri, resource MIME type, and CSP.
  4. Inspect structuredContent and _meta in the tool result.
  5. Add or update a simulation for the failing state.
  6. Add a protocol test for the tool contract.
  7. Add an iframe or visual test for the rendered resource.
  8. Test display mode, theme, viewport, and safe-area states.
  9. Live-test in ChatGPT only after the local contract is stable.
  10. Refresh the development app from the Plugins page and start a new chat when metadata changed.

That order keeps you from guessing. It starts at the boundary where most bugs begin, then works inward to React state and outward to the real host.

Get Started

The full debugging loop is built into npx sunpeak new: local Inspector, simulations, MCP protocol tests, iframe tests, visual regression, and evals. If you already have a server, use npx sunpeak inspect --server URL to debug it locally, or npx sunpeak test init --server URL to add tests around the server you already run.

For the broader testing plan, read the complete guide to testing ChatGPT Apps. For local setup, start with How to run a ChatGPT App locally. For host-specific layout problems, keep the display mode reference open while you debug.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

How do I debug a ChatGPT App locally?

Run "pnpm dev" in a sunpeak project and open the local Inspector. The Inspector renders your MCP App in a replicated ChatGPT-style host, gives you browser DevTools access, lets you switch display modes and themes, and lets you replay saved tool states. For an existing MCP server, run "npx sunpeak inspect --server <url>" instead.

Why is my ChatGPT App showing a blank iframe?

Start with the iframe console and the Network tab. Blank iframes usually come from a JavaScript render error, a failed app resource fetch, an invalid resource MIME type, a CSP block, a resource URI that points to an old bundle, or a tool result that never links the UI resource. Reproduce the same tool call in the sunpeak Inspector, then verify the resource URI, HTML response, and structuredContent shape.

What metadata should I check when a ChatGPT App does not render?

Check that the tool result points to a resource with _meta.ui.resourceUri, that the resource is returned with an MCP App-compatible HTML media type such as text/html;profile=mcp-app, and that model-visible data is in structuredContent while app-only data is in _meta. If you also support older ChatGPT-specific compatibility keys, keep them in sync with the standard MCP Apps fields.

Why are my ChatGPT App changes not showing up after deployment?

Resource caching is the usual cause. The resource URI should change when the bundle changes, usually with a content hash or build timestamp. Refresh the development app from the ChatGPT Plugins page, then start a new chat when you changed tool names, schemas, descriptions, or annotations. sunpeak cache-busts built resource URIs automatically.

How do I debug useToolData or tool output shape bugs?

Inspect the exact tool input and tool result in the sunpeak Inspector sidebar, then save the failing state as a simulation. Add an integration test that calls the MCP tool and asserts the exact structuredContent fields your resource reads. This catches field renames, missing arrays, null values, and app-only _meta data that accidentally moved into model-visible structuredContent.

How do I debug display mode issues in ChatGPT Apps?

Treat display mode as host state. Read the actual mode from host context, check available modes before showing expand or PiP controls, and test inline, fullscreen, and picture-in-picture separately. Visual tests are useful because text wrapping, safe areas, and nested scrolling often break only in one mode.

How do I find console logs from a ChatGPT App?

In the local sunpeak Inspector, normal browser DevTools show iframe logs. In real ChatGPT, open DevTools and choose the newest app iframe from the console context dropdown instead of the top page. Log the tool result shape, host context, display mode, and any client-side fetch errors while reproducing the bug.

What should I test before shipping a ChatGPT App fix?

Run protocol tests for tool registration and result shape, iframe tests for the rendered resource, visual tests across theme and display mode, and at least one live-host smoke test when the bug only happens in ChatGPT. In this repo, run pnpm validate before shipping content changes.