1. Why a separate lecture about local debug
In previous modules, we already reviewed how the Apps SDK stack and MCP work. Now let’s talk about why we even need a separate lecture on local debug.
Many people go like this: “Well, I’ll just open ChatGPT, write ‘use my App’, and then see what it says. If it doesn’t work — I’ll rewrite code at random.” That’s about as useful as fixing a backend by only looking at the HTML page in a browser and never opening the server logs.
With ChatGPT Apps it’s especially easy to slide into magic: there’s GPT, it decides whether to call a tool or not, and it has its own error logic. If you don’t see what’s going on under the hood, debug turns into cargo‑cult drumming.
Our goal: turn this into a real engineering process:
- you know where to look at Next/MCP logs;
- you can manually call the MCP server via the inspector;
- you understand what Dev Mode checks and how to verify that ChatGPT can even reach your server.
And most importantly: you stop debugging via a “GPT guessing game” and start by checking the lower layers of the stack — the server and the protocol — and only then the UI and model behaviour.
2. Mental model: three debug layers
To avoid drowning in chaos, let’s think about debug in terms of three layers. This is our little “layer cake”:
| Layer | What lives there | Typical symptoms | How to debug |
|---|---|---|---|
| UI (widget) | React components, state, window.openai | Empty/grey widget, broken render, buttons don’t work | Browser DevTools |
| Backend / MCP server | tools, access to DB/API | 500s, “tool crashed”, strange data | server logs, MCP Inspector |
| MCP protocol | JSON‑RPC, tools/list, tools/call, schemas | GPT says “could not call the tool”, invalid params | inspector + request logs |
At the second layer, we care about what the MCP server itself does (tools, DB, API), and at the third — the “wires” and the format of MCP messages (JSON‑RPC, schemas, etc.).
These three are the backbone of the lecture plan and the debug course.
For clarity, here’s a request flow:
sequenceDiagram
participant User as User
participant ChatGPT as ChatGPT (Dev Mode)
participant Tunnel as Tunnel (ngrok/CF)
participant Next as Next.js + MCP
User->>ChatGPT: "Pick a gift under $50"
ChatGPT->>Next: tools/call search_gifts (via Tunnel)
Next->>Next: Call MCP tool, hit DB/API
Next-->>ChatGPT: JSON-RPC result + ToolOutput
ChatGPT-->>User: Answer + widget render
It can break anywhere: tunnel, endpoint, MCP logic, JSON schema, React widget. Your task during debug is to identify which layer has the error, instead of rewriting everything at once.
3. Next.js and MCP logs: the foundation
Let’s start with the most boring and most useful thing — logs.
Where the logs live in local development
In the standard Apps SDK template on Next.js, the MCP server is typically wrapped in an API route (/api/mcp or similar). You run npm run dev, and in one terminal you have:
- the Next.js dev server;
- a handler for the MCP endpoint that accepts JSON‑RPC requests tools/list, tools/call, etc.;
- all the output via console.log/console.error.
If you split MCP into a separate process, you’ll have a second terminal, but the idea is the same: everything interesting is visible in the console.
Important to distinguish:
- build/start errors — next dev doesn’t start, TypeScript crashes, wrong import, etc.;
- runtime errors — everything booted, but a specific request to /api/mcp crashes a tool.
In dev mode, Next.js shows runtime errors with a nice overlay and writes a stack trace to the console.
What to log in the MCP server
Although MCP uses JSON‑RPC, you don’t need to print the entire JSON for debug. Structured but concise logs are far more useful.
A good practice for MCP logs is to log at least: timestamp, request_id/traceId, tool name, parameters (anonymized), status (ok/error) and execution time.
A simple logger.ts for GiftGenius could look like this:
// src/lib/logger.ts
export function logToolEvent(
phase: "start" | "end" | "error",
data: Record<string, unknown>
) {
const ts = new Date().toISOString();
console.log(JSON.stringify({ ts, phase, ...data }));
}
And in the tool handler:
// src/mcp/tools/searchGifts.ts
import { logToolEvent } from "@/lib/logger";
export async function searchGiftsTool(args: { q: string }) {
const traceId = crypto.randomUUID();
logToolEvent("start", { tool: "search_gifts", traceId, args });
try {
// ... real gift search ...
const results = []; // stub
logToolEvent("end", { tool: "search_gifts", traceId, count: results.length });
return results;
} catch (err) {
logToolEvent("error", { tool: "search_gifts", traceId, error: String(err) });
throw err;
}
}
There are two important nuances.
First, don’t store full emails, phone numbers, card numbers, tokens in logs. It’s not only bad practice but also conflicts with baseline MCP security practices.
Second, traceId is your best friend. When you look at Next.js and MCP logs together, it’s easy to correlate events by it: the specific tools/call request, the corresponding React render, and the widget’s network log.
How to tell from the logs where it failed
You’ve got a terminal with JSON lines from logToolEvent. A typical scenario:
- a phase: "start" arrives with tool: "search_gifts";
- there’s no phase: "end", but there is phase: "error" and a stack trace;
- from this you see that your tool logic was reached, but it broke inside — e.g., an external API call, parsing, DB work.
If you don’t see any logs for that tool name at all — the request didn’t even reach the tool. Then you go higher up the stack: tunnel, /mcp endpoint, tools/call JSON request.
4. MCP Inspector: debugging MCP before ChatGPT
If logs are your eyes, then the MCP Inspector (or MCPJam Inspector) is a microscope.
More about the MCP Inspector and why you need it
In the MCP module, we already connected the Inspector to verify a “Hello, MCP” server. Here, we use it as the main debugging tool: first make sure MCP is healthy on its own, and only then dive into Dev Mode and the UI.
The Inspector is a separate application (most often a web UI plus CLI) that acts as an MCP client. It connects to your server over HTTP/SSE or stdin/stdout, performs tools/list, tools/call, and shows raw JSON messages, the handshake, the list of tools, resources, etc.
The main idea: remove ChatGPT from the equation. If your tool doesn’t work, you first want to know whether the server is alive, and whether the protocol and schema are correct, before blaming GPT.
Mini workflow with the Inspector
A typical local debugging scenario looks like this:
- Run npm run dev so Next.js + the MCP endpoint start.
- Start the MCP Inspector, for example:
npx @modelcontextprotocol/inspector
(the specific command depends on the tool you use).
- In the Inspector, specify your MCP endpoint URL, for example http://localhost:3000/api/mcp (or an HTTPS tunnel if you want to test it too).
- See whether the handshake succeeded: the server should return supported capabilities, a list of tools, resources, etc.
- Call the tool you care about by hand: pick search_gifts, enter args {"q": "for a woman under 30"}, click “Call tool”, and check:
- whether a response arrived;
- whether a JSON‑RPC or MCP error was returned;
- what the server logs for this call.
If it already fails in the Inspector, you don’t even need to open ChatGPT: fix the MCP server.
If everything is great in the Inspector but ChatGPT still complains — the problem is higher: Dev Mode URL, auth, model behaviour.
Example: intentionally breaking a tool
Let’s take our search_gifts and break it on purpose:
export async function searchGiftsTool(args: { q: string }) {
if (args.q === "break") {
throw new Error("Intentional error for debug demonstration");
}
// ... normal logic ...
return [];
}
Then:
- In the Inspector, call search_gifts with the argument {"q": "break"}.
- In the logs, you’ll see phase: "error" and a stack trace.
- Confirm that the MCP server honestly returns an error.
Later, when you hook this up to ChatGPT Dev Mode and ask the model “pick a gift with the word ‘break’”, it will try to call the tool and show the user something like “I encountered an error running the tool.” You can see the error arises not because of the model, but due to your explicit exception.
This exercise trains your thinking: you cleanly separate a business error (we ourselves threw an Error) from a protocol error (broken JSON, wrong tool name, etc.).
5. Widget debug: DevTools, state, and a “debug banner”
When the MCP server is more or less clear, move to the frontend — the Apps SDK widget.
Where and how to look for widget errors
Your widget renders inside ChatGPT in an iframe sandbox. The good news: that iframe has the same browser DevTools.
Mini procedure:
- Open ChatGPT in your browser (Chrome/Edge/Firefox).
- Open DevTools (usually F12 or Ctrl+Shift+I).
- On the Console tab, choose the frame context where your widget lives (often the domain web-sandbox.oaiusercontent.com).
- Refresh the chat/send a message so GPT shows your App.
If the widget:
- didn’t appear at all;
- appears grey/empty;
- shows a red error in the console
— it’s almost certainly a React code problem: inaccessible property, wrong import, broken hook, etc.
The Network tab is also useful. There you’ll see:
- loading of your app’s JS bundle (if 404/500 — a dev‑server/tunnel problem);
- requests your widget makes via window.fetch, and 4xx/5xx responses.
The simplest debug banner
A very handy trick is to add a small “debug banner” to the widget’s root component that, in Dev Mode, shows what environment it is and which build version.
For example:
// src/components/DebugBanner.tsx
export function DebugBanner() {
if (process.env.NODE_ENV !== "development") return null;
return (
<div style={{ padding: 4, background: "#222", color: "#0f0", fontSize: 10 }}>
ENV: dev | build: local | {new Date().toLocaleTimeString()}
</div>
);
}
And in the widget’s root component:
// src/app/widget/page.tsx
import { DebugBanner } from "@/components/DebugBanner";
export default function GiftGeniusWidget() {
return (
<div>
<DebugBanner />
{/* the rest of the gift search UI */}
</div>
);
}
If you opened ChatGPT, launched the App, and don’t see the banner — your JS didn’t reach the browser at all: either a build error, an endpoint issue, or the widget simply isn’t registered in the MCP server.
Local state and error handling
Your widget should already display different states: loading, success, error. If not — now is the time to add them.
Mini pattern:
const [status, setStatus] = useState<"idle"|"loading"|"error"|"success">("idle");
async function handleSearch(query: string) {
try {
setStatus("loading");
// call the MCP tool via window.openai.callTool or an Apps SDK hook
setStatus("success");
} catch (e) {
console.error("Search failed", e);
setStatus("error");
}
}
In JSX:
{status === "error" && (
<div style={{ color: "red" }}>Something went wrong, please try again.</div>
)}
For debug, it’s critical that:
- you don’t swallow exceptions (otherwise the console is empty and the UI just “hangs”);
- you explicitly reflect the error in the UI, otherwise it looks to the user like the App died.
6. Dev Mode as part of debug: what it does and how not to blame it unfairly
Now let’s include ChatGPT Dev Mode in the picture. Until now we considered only your code. But sometimes everything works locally, everything is perfect in the Inspector, and ChatGPT still responds “Error talking to [AppName]” or doesn’t offer your App at all.
What Dev Mode does
Dev Mode is a ChatGPT feature where you can:
- create and edit your Apps;
- specify the MCP server endpoint (usually https://your-domain/mcp or /api/mcp);
- quickly update the manifest and metadata without publishing to the Store.
From a debug perspective, Dev Mode is just another configuration layer:
- if the URL is wrong there;
- if you forgot /mcp at the end;
- if the tunnel issued a new domain and you didn’t update the settings
— ChatGPT simply can’t reach your server.
Typical Dev Mode breakage scenario
A classic:
- You brought up a tunnel https://abcd.ngrok.io, specified it in Dev Mode, everything worked.
- The next day you restarted ngrok and got https://efgh.ngrok.io.
- Dev Mode still has https://abcd.ngrok.io/mcp.
- ChatGPT says “Error talking to GiftGenius”.
At the same time, the MCP Inspector, pointed at http://localhost:3000/api/mcp, shows everything is fine. That means MCP is alive, but ChatGPT is looking in the wrong place.
Solution: open Dev Mode settings, update the URL, and don’t forget /mcp at the end.
Dev Mode vs Store
In this lecture we talk only about Dev Mode — it’s your sandbox. It’s normal here to change the URL often, reconnect a tunnel, and modify tool schemas.
When you later go to the Store, the endpoint will be fixed more rigidly, and such tricks will be a bad idea. But the Store is a few modules away, so for now we can safely break and fix in Dev Mode.
7. Mini debug algorithm: what to do when “nothing works”
Now let’s put everything into a practical algorithm. Essentially, it’s the same three debug layers from the beginning of the lecture, recorded as a sequence of steps.
Suppose you opened ChatGPT, selected GiftGenius, and asked “Pick a gift under 30$ for a geeky friend”, and:
- GPT says nothing about the App;
- or it says “Error talking to GiftGenius”;
- or an empty/grey widget opens.
How not to despair?
Step 1 (MCP/server layer). Check MCP via the Inspector and logs
First, ignore GPT and the UI. We care only about the server.
- Make sure npm run dev is running and the endpoint (/api/mcp) responds.
- Connect the MCP Inspector to http://localhost:3000/api/mcp or your tunnel.
- Verify the handshake — the list of tools should appear.
- Manually call the same tool GPT is supposed to use (for example, search_gifts) with similar arguments.
If things already break here — fix MCP: schemas, business logic, network calls. Use logs and traceId to locate the exact failure.
Step 2 (protocol/Dev Mode layer). Check Dev Mode and the URL
If everything is fine in the Inspector, but ChatGPT still doesn’t see your App or reports connection problems:
- Open the Dev Mode settings for your App.
- Look at which URL is specified for MCP.
- Compare it to what your server/tunnel actually listens to (and don’t forget to check that /mcp is present at the end if your server requires it).
Often the problem is right here.
Step 3 (UI layer). Check the widget via DevTools
If ChatGPT successfully calls tools (as seen in MCP logs), but the widget itself behaves oddly:
- Open DevTools in your browser on the ChatGPT page.
- Console tab — select your widget’s iframe context.
- Look at JS errors.
- Network tab — make sure:
- the widget’s JS bundle loads without 404/500;
- additional requests (via fetch/window.openai.fetch) return meaningful responses.
In parallel, look at your DebugBanner: if it didn’t appear, React didn’t render at all.
Step 4. Use Dev Mode to reproduce a bug report
When you receive a bug report from a colleague/user, try to preserve the exact prompt where it broke. In Dev Mode, you can very quickly reproduce the scenario:
- Run npm run dev, bring up a tunnel.
- Select your App in Dev Mode.
- Paste the problematic prompt.
- In parallel:
- watch which JSON requests hit MCP in the logs;
- in the Inspector, if needed, repeat the same tools/call with the same arguments.
This turns “sometimes something doesn’t work” into a reproducible scenario.
8. Small code touches for convenient debug
To reinforce the material, let’s add a couple more helpful fragments to our GiftGenius app.
Environment configuration and log levels
Somewhere in the server config, it’s convenient to explicitly specify the MCP endpoint and the log level:
// src/config.ts
export const config = {
mcpEndpoint:
process.env.NODE_ENV === "development"
? "http://localhost:3000/api/mcp" // the tunnel fronts this
: "https://api.giftgenius.com/api/mcp",
logLevel: process.env.NODE_ENV === "development" ? "DEBUG" : "ERROR",
};
And in logToolEvent you can account for logLevel so you don’t spam in prod.
Logging structured MCP errors
When handling tools, try to catch expected errors and return clear messages instead of throwing everything with throw:
export async function searchGiftsTool(args: { q: string }) {
const traceId = crypto.randomUUID();
logToolEvent("start", { tool: "search_gifts", traceId, args });
try {
// ... normal code ...
return { content: [{ type: "text", text: "Found 3 gifts" }] };
} catch (err) {
logToolEvent("error", { tool: "search_gifts", traceId, error: String(err) });
return {
content: [{ type: "text", text: "Gift search error. Please try again later." }],
isError: true,
};
}
}
Then ChatGPT will see that the result is marked with isError and can properly communicate the issue to the user, while you’ll see what happened in the logs.
9. Common mistakes in local ChatGPT App debug
Mistake #1: debugging “through GPT” instead of through the server and inspector.
It’s tempting to just look at the model’s replies and try to guess where your bug is. But the model is the topmost layer. If the MCP server doesn’t work on its own (manually, via the Inspector), don’t expect miracles from GPT. First make MCP stable, and only then plug in ChatGPT.
Mistake #2: not looking at logs at all or logging everything.
No logs means complete blindness: you don’t know which tool was called, with which args, and how it ended. Over‑logging, on the other hand, turns the console into a “matrix” of unrelated lines. It’s better to have a compact, structured log with tool, args (anonymized), traceId, status, and execution time.
Mistake #3: storing sensitive data in logs.
Logging tokens, full emails and card numbers is bad practice both from a security standpoint and OpenAI policy. Logs should contain only information that truly helps debug; personal data should be masked or not logged at all.
Mistake #4: blaming Dev Mode for everything.
Dev Mode often becomes a scapegoat: “OpenAI must have broken something.” In reality, very often the problem is that you forgot to update the URL after restarting the tunnel or specified the wrong path (/ instead of /mcp). Before contacting support, open Dev Mode settings and compare the endpoint to the actual server address.
Mistake #5: ignoring DevTools and a widget error.
An empty or grey widget almost always means a client‑side JavaScript error. If you only look at MCP logs but don’t open DevTools in ChatGPT, you’re seeing only half the picture. The habit of hitting F12 and checking Console/Network will save you hours.
Mistake #6: trying to “fix” a bug with magic delays.
Sometimes you want to add a setTimeout or Thread.sleep-style delay “so everything has time to load.” In the MCP/Next/React world this is almost always the wrong treatment: the problem is usually in a schema, the wrong endpoint, or a code error — not that “the server didn’t have time.” It’s better to find where the break is (Inspector → Dev Mode → widget) than to plaster it over with delays.
Mistake #7: deploying to Vercel before making sure everything works locally.
The desire to “ship to prod quickly” is understandable, but moving a broken MCP to Vercel is a perfect way to get two layers of problems: local and production. In this module, we intentionally require: first MCP Jam/Inspector → everything is OK, Dev Mode → basic scenarios work, and only then deploy.
GO TO FULL VERSION