1. 为什么需要一节专讲本地调试
在之前的模块中,我们已经拆解了 Apps SDK 以及 MCP 的栈结构。现在来谈谈,为什么有必要专门讲本地调试。
很多人的路径是这样:“我就打开 ChatGPT,写一句‘使用我的 App’,然后看看它说什么。如果不行——我就随便改点代码试试”。这就像只盯着浏览器里的 HTML 页面来修后端,从不打开服务器日志。
在 ChatGPT Apps 里尤其容易滑向“魔法思维”:有 GPT,它决定是否调用工具,也有自己的错误处理逻辑。如果你看不到底层发生了什么,调试就会变成“敲锣打鼓求神仙”。
我们的目标:把它变成正常的工程流程:
- 你知道去哪里看 Next/MCP 的日志;
- 你会用 Inspector 手动“拉” MCP 服务器;
- 你明白 Dev Mode 检查的是什么,并能确认 ChatGPT 的确能访问到你的服务器。
最重要的是:你不再用“GPT 猜谜”来调试,而是先检查栈的底层——服务器与协议,然后才看 UI 与模型行为。
2. 心智模型:三层调试
为了不陷入混乱,我们约定用三层来思考调试。这是我们的小“千层蛋糕”:
| 层级 | 里面是什么 | 常见症状 | 用什么调试 |
|---|---|---|---|
| UI(小部件) | React 组件、状态、window.openai | 小部件空白/灰屏、渲染异常、按钮无响应 | 浏览器 DevTools |
| 后端 / MCP 服务器 | 工具、对 DB/API 的访问 | 500 错误、“工具崩溃”、数据异常 | 服务器日志、MCP Inspector |
| MCP 协议 | JSON‑RPC、tools/list、tools/call、Schema | GPT 显示“无法调用工具”、invalid params | Inspector + 请求日志 |
第二层我们关注 MCP 服务器本身在做什么(工具、DB、API),第三层则关注“线路”和 MCP 消息格式(JSON‑RPC、Schema 等)。
这三层就是本讲与整个调试课程的主线。
更直观一点,可以看一下请求的流转:
sequenceDiagram
participant User as 用户
participant ChatGPT as ChatGPT (Dev Mode)
participant Tunnel as 隧道(ngrok/CF)
participant Next as Next.js + MCP
User->>ChatGPT: "挑选 50 美元以内的礼物"
ChatGPT->>Next: tools/call search_gifts (通过隧道)
Next->>Next: 调用 MCP 工具,访问数据库/API
Next-->>ChatGPT: JSON-RPC result + ToolOutput
ChatGPT-->>User: 回复 + 渲染小部件
任何一点都可能出问题:隧道、endpoint、MCP 逻辑、JSON Schema、React 小部件。调试的关键——先定位是哪个层出了错,而不是一上来就全改。
3. Next.js 与 MCP 日志:一切的基础
从最枯燥却最有用的东西开始——日志。
本地开发时日志在哪里
在基于 Next.js 的 Apps SDK 模板中,MCP 服务器通常包在一个 API 路由里(比如 /api/mcp)。你运行 npm run dev,一个终端里会有:
- Next.js 的开发服务器;
- MCP 的 endpoint 处理器,接收 tools/list、tools/call 等 JSON‑RPC 请求;
- console.log/console.error 打印的一切“精彩内容”。
如果你把 MCP 拆成了独立进程,会多一个终端,但思路一样:有用的信息都在控制台里。
注意区分:
- 构建/启动错误——next dev 起不来、TypeScript 崩了、import 不对等;
- 运行时错误——都起来了,但对 /api/mcp 的某个请求导致工具崩溃。
Next.js 在开发模式下会用覆盖层显示运行时错误,并把 stack trace 写进控制台。
在 MCP 服务器里该打什么日志
虽然 MCP 用的是 JSON‑RPC 协议,但调试并不需要把完整 JSON 全打出来。更有用的是结构化且简短的日志。
一条不错的 MCP 日志实践:至少记录 timestamp、 request_id/traceId、 工具名、 参数(脱敏)、 状态(ok/error) 以及耗时。
GiftGenius 的一个最简单 logger.ts 可能是这样:
// 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 }));
}
而在工具处理器中:
// 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 {
// ... 实际的礼物搜索 ...
const results = []; // 占位
logToolEvent("end", { tool: "search_gifts", traceId, count: results.length });
return results;
} catch (err) {
logToolEvent("error", { tool: "search_gifts", traceId, error: String(err) });
throw err;
}
}
有两点很重要。
其一,不要在日志中存完整的邮箱、手机号、卡号、token。这不仅不合适,还与 MCP 的基础安全实践相冲突。
其二,traceId 是你最好的朋友。把 Next.js 和 MCP 日志放一起看时,它能把事件串起来:某次 tools/call 请求、对应的 React 渲染,以及小部件的网络日志。
如何通过日志判断“哪儿崩了”
终端里不断滚着来自 logToolEvent 的 JSON。一种典型的场景:
- 收到 phase: "start" 且 tool: "search_gifts";
- 没有 phase: "end",却出现了 phase: "error" 与 stack trace;
- 这表明工具确实进入了你的业务逻辑,但在里面某处崩了——比如外部 API 请求、解析或 DB 操作。
如果你压根看不到该工具名对应的日志——说明请求根本没到工具。那就顺着栈往上查:隧道、/mcp endpoint、tools/call 的 JSON 请求。
4. MCP Inspector:在 ChatGPT 之前调试 MCP
如果日志是你的“眼睛”,那 MCP Inspector(或 MCPJam Inspector)就是“显微镜”。
MCP Inspector 是什么、为什么要用它
在 MCP 模块里我们已经接过 Inspector 来验证“Hello, MCP” 服务器了。这里把它当作核心调试工具:先确保 MCP 自己正常,再去看 Dev Mode 与 UI。
Inspector 是一个独立应用(多为 Web‑UI + CLI),扮演 MCP 客户端。它通过 HTTP/SSE 或 stdin/stdout 连接到你的服务器,执行 tools/list、tools/call,并展示原始 JSON 消息、握手、工具列表、资源等。
核心想法:把 ChatGPT 从等式里移开。如果工具不工作,你要先搞清楚服务器是否存活、协议与 Schema 是否正确,而不是先怀疑 GPT。
使用 Inspector 的迷你流程
本地调试的典型流程:
- 启动 npm run dev,让 Next.js + MCP endpoint 跑起来。
- 启动 MCP Inspector,例如:
npx @modelcontextprotocol/inspector
(具体命令取决于你使用的工具)。
- 在 Inspector 中填入 MCP endpoint 的 URL,比如 http://localhost:3000/api/mcp(若想一并验证隧道,也可填 HTTPS 隧道地址)。
- 查看握手是否成功:服务器应返回支持的能力、工具列表、资源等。
- 手动调用目标工具:选择 search_gifts,输入参数 {"q": "30 岁以下的女生"},点击 “Call tool”,然后观察:
- 是否返回了响应;
- 是否出现 JSON‑RPC 或 MCP 的错误;
- 服务器针对这次调用打印了哪些日志。
如果在 Inspector 里就已经失败,根本不用打开 ChatGPT:先修 MCP 服务器。
如果在 Inspector 一切正常,而 ChatGPT 仍旧报错——那问题更高一层:Dev Mode URL、鉴权、模型行为。
“故意把工具弄崩”示例
拿我们的 search_gifts 来故意整坏一下:
export async function searchGiftsTool(args: { q: string }) {
if (args.q === "崩溃") {
throw new Error("用于演示调试的教学错误");
}
// ... 正常逻辑 ...
return [];
}
接下来:
- 在 Inspector 中以参数 {"q": "崩溃"} 调用 search_gifts。
- 在日志里看到 phase: "error" 与 stack trace。
- 确认 MCP 服务器如实返回了错误。
之后,当你把它接到 ChatGPT Dev Mode 并让模型“挑选包含‘崩溃’一词的礼物”时,它会尝试调用工具,并向用户展示类似“I encountered an error running the tool”的消息。可以看出:错误不是模型引起的,而是你明确抛出的异常。
这种方法能训练你的思维:清楚地区分业务错误(我们自己抛了 Error)与协议错误(JSON 结构坏了、工具名不对等)。
5. 小部件调试:DevTools、状态与“调试横幅”
当 MCP 服务器基本清楚后,转到前端——Apps SDK 小部件。
在哪里、怎么查看小部件错误
你的小部件在 ChatGPT 的 iframe 沙箱里渲染。好消息是:这个 iframe 也有完整的浏览器 DevTools。
迷你步骤:
- 在浏览器(Chrome/Edge/Firefox)中打开 ChatGPT。
- 打开 DevTools(通常是 F12 或 Ctrl+Shift+I)。
- 在 Console 标签页选择你的部件所在的 frame 上下文(通常是域名 web-sandbox.oaiusercontent.com)。
- 刷新聊天/发送消息,让 GPT 显示你的 App。
如果小部件:
- 根本没出现;
- 显示为灰色/空白;
- 在控制台出现红色错误
——几乎可以断定是 React 代码问题:访问了不存在的属性、错误的 import、hook 用错等。
Network 标签页也很有用,你能看到:
- 你的应用 JS bundle 的加载(如果是 404/500——那是 dev 服务器/隧道侧的问题);
- 小部件通过 window.fetch 发起的请求以及返回的 4xx/5xx。
最简单的调试横幅
一个非常实用的小技巧——在小部件根组件加一个“调试横幅”,在 Dev Mode 显示当前环境与构建版本。
例如:
// 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>
);
}
并在小部件根组件中:
// src/app/widget/page.tsx
import { DebugBanner } from "@/components/DebugBanner";
export default function GiftGeniusWidget() {
return (
<div>
<DebugBanner />
{/* 其余的礼物搜索 UI */}
</div>
);
}
如果你打开了 ChatGPT、启动了 App,却看不到横幅——说明你的 JS 根本没到浏览器:要么构建出错、要么 endpoint 有问题、要么小部件压根没在 MCP 服务器里注册。
本地状态与错误处理
你的小部件应该能展示不同状态:加载、成功、错误。如果还没有——现在就加上。
迷你模式:
const [status, setStatus] = useState<"idle"|"loading"|"error"|"success">("idle");
async function handleSearch(query: string) {
try {
setStatus("loading");
// 通过 window.openai.callTool 或 Apps SDK 的 hook 来调用 MCP 工具
setStatus("success");
} catch (e) {
console.error("Search failed", e);
setStatus("error");
}
}
在 JSX 中:
{status === "error" && (
<div style={{ color: "red" }}>出了点问题,请重试。</div>
)}
对调试来说,务必做到:
- 不要吞掉异常(否则控制台一片静默,UI 只是“挂住”);
- 在 UI 中明确展示错误,否则用户会以为 App 死了。
6. 把 Dev Mode 纳入调试:它做什么、何时别冤枉它
现在把 ChatGPT Dev Mode 纳入全景。在此之前我们只看你的代码。但有时本地一切正常、Inspector 里也很好,ChatGPT 依然提示 “Error talking to [AppName]”,甚至根本不提供你的 App。
Dev Mode 做什么
Dev Mode 是 ChatGPT 的一个模式,你可以:
- 创建和编辑自己的 Apps;
- 指定 MCP 服务器的 endpoint(通常是 https://your-domain/mcp 或 /api/mcp);
- 无需上架 Store 就能快速更新清单和元数据。
从调试角度看,Dev Mode 只是另一层配置:
- 如果那里填了错误的 URL;
- 如果你忘了在末尾加 /mcp;
- 如果隧道换了新域名,你却没更新设置
——ChatGPT 就压根访问不到你的服务器。
Dev Mode 常见“翻车”场景
经典四步:
- 你开了一个隧道 https://abcd.ngrok.io,在 Dev Mode 里配置了它,一切正常。
- 第二天重启 ngrok,得到 https://efgh.ngrok.io。
- Dev Mode 里还是 https://abcd.ngrok.io/mcp。
- ChatGPT 显示“Error talking to GiftGenius”。
这时把 MCP Inspector 指向 http://localhost:3000/api/mcp 会发现一切正常。这意味着 MCP 是活的,但 ChatGPT 看错了地址。
解决:进入 Dev Mode 设置,更新 URL,别忘了末尾的 /mcp。
Dev Mode vs Store
本讲只讨论 Dev Mode——你的沙箱。在这里频繁改 URL、重连隧道、调整工具 Schema 都没问题。
将来进入 Store 后,endpoint 会更加固定,这些“花活”就不合适了。但离 Store 还有几个模块,现在先放心地在 Dev Mode 里改和修。
7. 调试小算法:当“一切都不工作”时怎么办
把一切收束为可执行的流程。其实就是开头那三层,只是写成步骤。
假设你打开 ChatGPT,选择 GiftGenius,发:“为极客朋友挑选 30$ 以内的礼物”,然后:
- GPT 并不提到你的 App;
- 或提示 “Error talking to GiftGenius”;
- 或小部件空白/灰屏。
如何不绝望?
步骤 1(MCP/服务器层):用 Inspector 与日志验证 MCP
先别管 GPT 与 UI。只看服务器。
- 确保 npm run dev 在跑,且 endpoint(/api/mcp)有响应。
- 把 MCP Inspector 连到 http://localhost:3000/api/mcp 或你的隧道。
- 检查握手——应能看到工具列表。
- 手动调用与 GPT 预期相同的工具(比如 search_gifts),参数尽量相似。
如果此处就挂了——修 MCP:Schema、业务逻辑、网络调用。用日志与 traceId 来定位哪儿坏了。
步骤 2(协议/Dev Mode 层):检查 Dev Mode 与 URL
如果 Inspector 一切 OK,但 ChatGPT 仍然看不到你的 App 或报连不上:
- 打开你的 App 的 Dev Mode 设置。
- 看看为 MCP 填的 URL 是什么。
- 与服务器/隧道实际监听的地址对一下(别忘了,如果你的服务器需要,末尾要有 /mcp)。
很多问题就出在这里。
步骤 3(UI 层):用 DevTools 检查小部件
如果 ChatGPT 成功调用了工具(从 MCP 日志可见),但小部件异常:
- 在 ChatGPT 页面打开浏览器 DevTools。
- Console 标签页选择你的小部件 iframe 上下文。
- 查看 JS 错误。
- Network 标签页确认:
- 小部件的 JS bundle 能正常加载,不是 404/500;
- 后续请求(通过 fetch/window.openai.fetch)有合理的响应。
同时看你的 DebugBanner:如果没出现,说明根本没渲染到 React 树。
步骤 4:用 Dev Mode 复现 bug 报告
拿到同事/用户的 bug 报告时,尽量保留触发问题的精确 prompt。用 Dev Mode 可很快复现:
- 启动 npm run dev,打开隧道。
- 在 Dev Mode 里选择你的 App。
- 粘贴有问题的 prompt。
- 同时:
- 在日志里看 MCP 收到的 JSON 请求;
- 必要时,在 Inspector 里用相同参数重复 tools/call。
这样你就把“偶尔不工作”变成了可复现场景。
8. 一些便捷调试的代码细节
为巩固内容,给我们的 GiftGenius 再加几段实用代码。
环境配置与日志级别
在服务器配置里显式写出 MCP endpoint 与日志级别很方便:
// src/config.ts
export const config = {
mcpEndpoint:
process.env.NODE_ENV === "development"
? "http://localhost:3000/api/mcp" // 隧道会覆盖这个
: "https://api.giftgenius.com/api/mcp",
logLevel: process.env.NODE_ENV === "development" ? "DEBUG" : "ERROR",
};
同时在 logToolEvent 里可参考 logLevel,避免生产环境刷屏。
记录结构化的 MCP 错误
在处理工具时,尽量捕获预期错误并返回可理解的信息,而不是一股脑 throw:
export async function searchGiftsTool(args: { q: string }) {
const traceId = crypto.randomUUID();
logToolEvent("start", { tool: "search_gifts", traceId, args });
try {
// ... 正常代码 ...
return { content: [{ type: "text", text: "找到 3 个礼物" }] };
} catch (err) {
logToolEvent("error", { tool: "search_gifts", traceId, error: String(err) });
return {
content: [{ type: "text", text: "搜索礼物出错。请稍后再试。" }],
isError: true,
};
}
}
这样 ChatGPT 会看到结果被标注为 isError,能更好地向用户说明问题,而你也能从日志里看清发生了什么。
9. 本地调试 ChatGPT App 的常见错误
错误 1:通过“GPT 猜”来调试,而不是通过服务器与 Inspector。
只盯着模型回答来猜 bug 很诱人。但模型是最上层。如果 MCP 服务器本身(通过 Inspector、手工)都不正常,就别指望 GPT 出奇迹。先把 MCP 跑稳,再接 ChatGPT。
错误 2:要么不看日志,要么什么都往里记。
没日志会让你“眼盲”:不知道哪个工具被调用、带了什么参数、结果如何。反过来,过度日志会把控制台变成无关字符串的“矩阵”。更好的做法是简洁、结构化的日志,包含 tool、args(脱敏)、traceId、status 与耗时。
错误 3:在日志里存储敏感数据。
记录 token、完整邮箱与卡号是糟糕做法,不论安全还是 OpenAI 政策。日志只应该包含对调试真正有用的信息,个人数据要么脱敏要么不写。
错误 4:把所有问题都怪在 Dev Mode 头上。
Dev Mode 常常被当作“背锅侠”:“OpenAI 又把什么搞坏了”。事实上,很多时候是你忘了在重启隧道后更新 URL,或者路径填错了(/ 写成了 /mcp)。在联系支持前,先去 Dev Mode 设置对一下 endpoint 与服务器的实际地址。
错误 5:忽视 DevTools 与小部件的错误。
小部件空白或灰屏几乎总是客户端 JavaScript 错误。如果你只看 MCP 日志,不在 ChatGPT 里打开 DevTools,你只看到了半张图。养成按 F12、查看 Console/Network 的习惯能省下大量时间。
错误 6:尝试用“魔法延迟”来“修” bug。
有时会想加个 setTimeout 或 Thread.sleep 延迟“让它都加载完”。在 MCP/Next/React 的世界里,这几乎总是错的:问题通常在 Schema、endpoint 或代码错误,不是“服务器没来得及”。与其用延迟糊住,不如找清楚哪里断了(Inspector → Dev Mode → 小部件)。
错误 7:本地未跑通就上 Vercel。
“尽快上生产”的心情可以理解,但把坏掉的 MCP 丢到 Vercel,是制造“两层问题”(本地 + 生产)的绝佳办法。本模块我们刻意要求:先 MCP Jam/Inspector → 一切 OK,Dev Mode → 基本场景跑通,最后再部署。
GO TO FULL VERSION