CodeGym /課程 /ChatGPT Apps /範本是怎麼運作的:專案結構與關鍵檔案

範本是怎麼運作的:專案結構與關鍵檔案

ChatGPT Apps
等級 2 , 課堂 1
開放

1. 導言

ChatGPT App HelloWorld 專案並不是「CodeGym 的魔法黑盒,最好什麼都別動」。它是一個普通的 Next.js 專案,只是裡面同時包含了:

  • 在 ChatGPT 內部渲染的前端,
  • 回應工具(tools)呼叫的 MCP 伺服器,
  • 將上述一切與 ChatGPT 串接起來的設定。

如果搞不清楚各個部分的位置,往往會出現三種典型情況:

  1. 開發者不小心在伺服器端檔案裡寫了 window,導致崩潰,從此痛恨整個技術棧。
  2. 想在 UI 裡加一個按鈕,卻改錯了 page.tsx(例如改了應用根頁而不是小工具),因此在 ChatGPT 裡看不到變更。
  3. 不小心把 OPENAI_API_KEY 放進了客戶端程式碼,結果金鑰被洩漏到瀏覽器。

所以今天的目標是——畫出這張地圖:UI 在哪、MCP 在哪、設定檔在哪,當你想要:

  • 修改小工具的外觀;
  • 新增一個 tool;
  • 微調某些平台設定(CORS、assetPrefix 等)。

2. 專案的高階剖析

ChatGPT App HelloWorld 的 Next.js 專案使用 App Router,並以 app/ 資料夾為核心。在同一個頁面樹裡並存:

  • 會在 ChatGPT 裡渲染的小工具 UI,
  • 用來處理 tool 呼叫的 MCP 端點(endpoint)。

典型的目錄樹(簡化版,你的範本可能資料夾命名略有不同,但模式一致):

my-chatgpt-app/
├─ app/
│  ├─ api/                          // REST API
│  │  └─ time/                      // GET /api/time 回傳伺服器時間
│  │     └─ route.ts
│  ├─ hooks/                        // 官方 Apps SDK 的 Hook 集合
│  │  ├─ use-call-tool.ts
│  │  ├─ use-display-mode.ts
│  │  └─ use-open-external.ts
│  ├─ mcp/                          // MCP 伺服器:ChatGPT 呼叫 tools 時會打到這裡
│  │  └─ route.ts
│  ├─ globals.css                   // 應用程式的根級 globals.css
│  ├─ layout.tsx                    // 應用程式的根級 layout
│  └─ page.tsx                      // 在 ChatGPT 內的小工具頁面
├─ public/                          // 靜態資源:圖示、manifest 等
├─ next.config.ts                   // Next.js 設定與 Apps 特定設定(assetPrefix 等)
├─ proxy.ts                         // 在 iframe 內運作需要的 CORS/標頭(原 middleware.ts)
├─ package.json                     // 專案相依套件
├─ tsconfig.json                    // TypeScript 設定
└─ .env.local                       // 機密:OPENAI_API_KEY 等

如果有多個小工具,通常不會放在 app/page.tsx,而是放在 app/widget/page.tsx。但邏輯不變:一樣會有一個小工具頁面, 以及一個扮演 MCP 伺服器角色的 endpoint。

你可以這樣理解:你的版本庫就像「雙面雅努斯」:

  • 一張「臉」是 /mcp 路徑,ChatGPT 想呼叫工具時會打到這裡;
  • 另一張「臉」是 /widget(或 /)路徑, 當模型決定顯示你的 UI 時,會把它載入到 iframe

為了不混淆,記住三組檔案:

  1. UI 層——所有與 React/Next 頁面相關的東西 (app/widget、元件、樣式)。
  2. MCP 層——app/mcp/route.ts 以及它使用的檔案。
  3. 黏合層與設定——next.config.tsproxy.ts.env.localpackage.jsontsconfig.json

接著我們會依序講解這些層。

3. 小工具在哪裡:app/widget 與/或 app/page.tsx

先從你最常動到的部分開始——小工具,也就是會在 ChatGPT 內顯示的 UI。

在大多數的當代專案中,要嘛是:

  • 資料夾 app/widget/page.tsx——小工具掛在獨立的前綴 /widget 下,
  • 或者是根層的 app/page.tsx——小工具就是根頁面。

判斷小工具檔案的要點:

  • 檔案最上方有 'use client',因為元件在瀏覽器執行, 會與 window 與 Apps SDK 溝通;
  • 它是一般的 React 元件,會渲染標記,並且(在課程稍後)與 window.openai 溝通。

最簡單的教學用小工具範例(你的專案裡可能已經有很相似的程式碼):

// app/widget/page.tsx
'use client';

import React from 'react';

export default function WidgetPage() {
  return (
    <main className="p-4">
      <h1 className="text-xl font-semibold">
        HelloWorld — ChatGPT App
      </h1>
      <p className="text-sm text-gray-500">
        我們會在這裡構建小工具的 UI。
      </p>
    </main>
  );
}

如果你的範本把小工具直接放在 app/page.tsx,程式碼也差不多, 只是沒有中間的 widget 資料夾。

請注意幾個重點。

第一,'use client' 指示是必要的: 小工具會讀寫 window.openai, 監聽事件等,而這些只能在客戶端元件完成。 如果移除它,Next 會嘗試把頁面當作伺服器端頁面,你就會遇到「window is not defined」之類的錯誤。

第二,它就是普通、完全不神奇的 React 元件。你可以:

  • 把它拆成 components/ 裡的子元件,
  • 使用 Tailwind 或任何其他 CSS 系統,
  • 掛載 context、hooks 等等。

第三,之後你會在這裡:

  • 讀取 window.openai.toolInputwindow.openai.toolOutput, 以渲染真實資料,
  • 透過 window.openai.setWidgetState 儲存 widgetState
  • 呼叫 openExternalcallTool 與其他執行期方法。

目前只要知道:如果你想改視覺介面——幾乎可以肯定要去 app/widget/page.tsxapp/page.tsx

4. 根級 layout:app/layout.tsx 作為整個應用的「框架」

下一個重要檔案是 app/layout.tsx。它會:

  • 定義 HTML 結構(<html><body>),
  • 掛上全域樣式(globals.css),
  • 經常初始化 Apps SDK 的「bootstrap」(包一層監聽 window.openai 並把資料傳給 React)。

簡化範例:

// app/layout.tsx
import './globals.css';
import type { ReactNode } from 'react';
import { OpenAIAppProvider } from '@/lib/openai-app-provider';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <NextChatSDKBootstrap baseUrl={baseURL} />
      </head>
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased h-full overflow-hidden`}>
        {children}
      </body>
    </html>
  );
}

這裡的 NextChatSDKBootstrap 是個示意名稱,你的範本可能叫 OpenAIAppProvider 或其他。它的任務通常只有一個:設定 React 樹與 Apps SDK 執行期的連結, 訂閱全域資料(themedisplayModetoolInput 等),並把它們提供給子元件。

實務上的重點:若你需要掛上全域的 context、樣式或 UI 函式庫 (例如 shadcn/ui),幾乎都應該放在 app/layout.tsx (或放在 app/widget 下的 layout,若它只針對小工具)。

解析 NextChatSDKBootstrap

我是在 Vercel 的官方範本裡看到 NextChatSDKBootstrap 的。 如果你不知道,Vercel 就是創建並持續維護 Next 的團隊。他們網站上有一篇很好的文章: 在 ChatGPT 內運行 Next 的深度解析。 也有提供 Starter Template。 雖然有些地方稍嫌過時,但我認為他們很可能會持續更新維護。

來看一下 NextChatSDKBootstrap 帶來的五個關鍵好處:

  • 1. 修正水合(hydration)問題
    問題在於 ChatGPT 會先把你的小工具 HTML 載到自己的伺服器,進行清理與修補。 結果導致水合機制抱怨並在主控台丟出許多 Warning,可能讓你過不了 review。
  • 2. 修補瀏覽器歷史紀錄
    你的小工具是以 iframe 從 ChatGPT 的特殊網域載入。 如果你使用自己的網域,會破壞沙盒環境。因此歷史紀錄只會儲存不含網域的路徑。
  • 3. 改寫 fetch() 函式
    你在小工具內對相對位址、不含網域的 fetch() 將無法運作, 因為 iframe 的網域不同。所以我們會替換 fetch() 成自有版本,讓不含網域的請求能被送往正確的 URL。如果原本就有網域,一切照常。
  • 4. 點擊連結可正確開啟
    若連結在 iframe 內開啟,ChatGPT 會不允許。 因此加上了程式碼來攔截連結點擊,並透過 openExternal() 在外部視窗開啟。
  • 5. 設定 head base(已棄用)
    這段程式以前會在 <head> 中加入 <base>, 但現在已經無效。沙盒會移除任何設定的 base, 因此建議對所有資源(腳本、資源、字型、API 等)都使用絕對連結。

5. MCP 伺服器:app/mcp/route.ts

現在來看「雙面雅努斯」的另一面——與 ChatGPT 透過 MCP 對話的伺服器

檔案 app/mcp/route.ts 是 App Router 的一般 Route Handler,會:

  • 接收來自 ChatGPT 的 HTTP 請求(通常是帶 MCP 格式 JSON 負載的 POST),
  • 把它們轉交給 MCP 伺服器(基於 @modelcontextprotocol/sdk 或薄封裝),
  • 再返回 MCP 格式的 JSON 回應。

有兩種做法:直接用 MCP SDK 撰寫,或使用 Next/Vercel 提供的一些類別讓開發更順手。

以下是純 TS MCP SDK 的版本:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// 1. 建立 MCP 伺服器
const server = new McpServer({
  name: "simple-mcp-server",
  version: "1.0.0",
});

// 2. 註冊 MCP 資源(Resources)
// 3. 註冊 MCP 工具(Tools)

// 4. HTTP 傳輸
const transport = new HttpServerTransport({
  port: 3001,
  path: "/mcp",
});

// 5. 啟動伺服器
await server.connect(transport);

但更好的方式是使用一些現成的類別,讓開發體驗更好:

// app/mcp/route.ts
import { NextRequest } from 'next/server';
import { createMcpHandler } from "mcp-handler";

const handler = createMcpHandler(async (server) => {
  const gateway = new McpGateway(server);
  await gateway.initialize();
  gateway.registerResources();
  gateway.registerTools();
});

export const GET = handler;
export const POST = handler;

這裡的 McpGateway 是包在 McpServer 外的一個類別, 用 SDK 建立(例如在 lib/mcp/server.ts),簡化一些工作。 在我們的情境中它全部都放在 app/mcp/route.ts。我們來完整解析這個檔案裡的內容。

type ContentWidget

在檔案開頭定義了 ContentWidget 型別。它包含小工具的所有資料,並用在兩個地方: 註冊小工具為 mcp-resource 時,以及當 mcp-tool 回傳 metadata、指定要用哪個小工具來呈現它回傳的資料時。

type ContentWidget = {
  id: string;            // 唯一名稱/key
  title: string;         // 標題
  description: string;   // 說明
  templateUri: string;   // 小工具的唯一 URI,可為任意值。實際上不影響行為。
  invoking: string;      // 載入中時顯示在小工具上方的文字
  invoked: string;       // 載入完成後顯示在小工具上方的文字
  html: string;          // 小工具的所有 HTML 程式碼
  widgetDomain: string;  // 小工具的「網域」。目前不影響行為。
};

class McpGateway

這是包在 McpServer 外的封裝類別,簡化一些流程。包含 6 個方法:

  • initialize()——在這裡載入小工具的 HTML
  • registerResources()——把小工具註冊為 mcp-resources
  • registerTools()——把函式註冊為 mcp-tools
  • widgetMeta()——回傳小工具的中繼資料(metadata)
  • getAppsSdkCompatibleHtml()——載入小工具 HTML 並做一些調整
  • makeImgUrlsAbsolute()——修補 HTML:將圖片連結改成絕對路徑

我們來逐一看一下:

public async initialize()

這個方法會從網路載入小工具的 HTML 程式碼,並填入 ContentWidget 物件。

{
  id: "hello_world",                         // 小工具唯一 key
  templateUri: "ui://widget/hello_world.html", // 小工具的唯一 URI。"ui:" 沒有特別意義
  title: "HelloWorld Widget",               // 小工具名稱
  description: "Displays the HelloWorld widget", // 給 LLM 的解釋:小工具做什麼
  invoking: "Loading widget...",            // 載入過程中顯示的文字
  invoked: "Widget loaded",                 // 載入完成後顯示的文字
  html: htmlWidget,                         // 小工具的 HTML
  widgetDomain: baseURL,                    // 小工具的「網域」。目前不影響行為。
}

public registerResources()

將小工具註冊成 mcp-resources。呼叫 server.registerResource(), 並傳入四個參數:

  • MCP 資源的 id/key
  • 資源的 URI(這是 MCP 協定需要的;對小工具而言就像唯一地址的同義詞)
  • MCP 資源的中繼資料
  • 回傳 MCP 資源的函式

小工具的中繼資料

{
  title: widget.title,                 // 資源/小工具名稱
  description: widget.description,     // 資源/小工具描述
  mimeType: "text/html+skybridge",     // 重要!只有這種 html 會以小工具顯示
  _meta: {
    "openai/widgetDescription": widget.description, // 小工具描述
    "openai/widgetPrefersBorder": true,            // 告訴 ChatGPT 顯示小工具邊框
  },
}

作為 MCP 資源的小工具

{
  uri: uri.href,                        // 我們的 URI(取自參數 uri)
  mimeType: "text/html+skybridge",      // 重要!只有這種 html 會以小工具顯示
  text: widget.html,                    // 小工具的 HTML
  _meta: {
    "openai/widgetDescription": widget.description, // 小工具描述
    "openai/widgetPrefersBorder": true,            // 告訴 ChatGPT 顯示小工具邊框
    "openai/widgetDomain": widget.widgetDomain,    // 小工具的「網域」。目前不影響行為。
    "openai/widgetCSP": {                          // 重要!小工具可用的網域:
      connect_domains: [                           // 連線用網域(fetch 等)
        baseURL,
        "https://codegym.cc",
      ],
      resource_domains: [                          // 資源用網域(css/fonts/img)
        baseURL,
        "https://codegym.cc",
        "https://cdn.tailwindcss.com",
        "https://persistent.oaistatic.com",
        "https://fonts.googleapis.com",
        "https://fonts.gstatic.com"
      ]
    }
  },
}

未來我們還會多次提到 openai/widgetCSP,但此刻先標註兩點:

  • connect_domains——用來:
    • fetch()
    • 載入腳本
    • openExternal()
  • resource_domains——用來:
    • 圖片
    • CSS
    • 字型

理論上你可以列 200 個網域,但能不能憑這樣的清單通過審核——就不好說了。

我也研究了已上架應用的這些參數,發現裡面有 amplitude.com。 這也是個好消息。我想良好的分析工具對大家都有幫助。

public registerTools()

將函式註冊成 mcp-tools。呼叫 server.registerTool(), 並傳入三個參數:

  • MCP tool 的 id/key
  • MCP tool 的中繼資料
  • 回傳 MCP tool 的函式

工具的中繼資料

這份清單中的每個參數都很重要。詳細會在後續課程中介紹。

{
  title: widget.title,                               // 工具名稱
  description: "Returns HelloWorld widget",          // 重要!工具在做什麼
  inputSchema: z.object({}).describe("No inputs"),   // 工具的參數結構。可用 Zod
  _meta: this.widgetMeta(widget),                    // 小工具的中繼資料:要顯示哪個小工具
  annotations: {
    destructiveHint: false,                          // 工具會做關鍵變更——需要使用者確認
    openWorldHint: false,                            // 工具會改變第三方服務的狀態
    readOnlyHint: true                               // 工具不會做變更(唯讀)
  },
}

會做重要事情的函式

async (input, extra) => {
  // 1. 參數驗證
  // 2. 執行重要邏輯
  return {
    content: [{ type: "text", text: "HelloWorld MCP-tool" }], // 給 AI 的結果描述
    structuredContent: {                                      // 重要!這就是結果的 JSON。
      timestamp: new Date().toISOString()                     // 可以包含任意資料。
    },
    _meta: this.widgetMeta(widget),                           // 顯示這個 JSON 的小工具中繼資料
  };                                                          // 也可以省略——那就不會有小工具
}

private widgetMeta(widget: ContentWidget)

回傳小工具的中繼資料——ChatGPT 會依此決定用哪個小工具來呈現 JSON 結果。

{
  "openai/outputTemplate": widget.templateUri,            // 小工具的 URI
  "openai/toolInvocation/invoking": widget.invoking,      // 載入期間顯示在小工具上方的文字
  "openai/toolInvocation/invoked": widget.invoked,        // 載入完成後顯示在小工具上方的文字
  "openai/widgetAccessible": true,                        // 可從小工具裡呼叫 MCP-tool
  "openai/resultCanProduceWidget": true,                  // MCP-tool 會回傳小工具
}

想特別說明一下 "openai/outputTemplate" 這件小事。 在 MCP 協定中有三種實體(你會在模組 6 更深入學到):

  • MCP Resources
  • MCP Templates
  • MCP Tools

需要注意的是,這個 "openai/outputTemplate" MCP Templates 沒有任何關係。 MCP Templates 在 ChatGPT Apps 裡根本不會使用。這裡的 template 一詞來自這個概念:

小工具被設計成用來呈現 JSON 的「樣板」。MCP tool 回傳某個 JSON,AI 顯示小工具並把 JSON 經由 ToolOutput 傳給它, 然後小工具把 JSON 漂亮地呈現出來。outputTemplate 就是「小工具」的同義詞。

我想這部分先到此為止。更多細節我們會在模組 4 解析:如何描述工具、JSON Schema 與處理器。 目前只要理解:凡是與工具(tools)與邏輯相關的東西——就到 app/mcp/route.ts 附近找。

6. 設定與「黏合」:next.config.tsmiddleware.ts.env 與夥伴們

接著解析讓你的 Next.js 專案能在 ChatGPT 的 iframe 內正確運作、並透過 HTTPS 通道(ngrok、Cloudflare Tunnel 等;通道之後會另外說明)被 ChatGPT 存取所需的關鍵檔案集合。

next.config.ts

除了 Next.js 的一般設定外,這個檔案常見會設定:

  • assetPrefix——讓靜態資源(/_next/ 底下的 JS、CSS) 不是從 ChatGPT 的網域載入,而是從你的開發 URL(通道或 Vercel)載入;
  • 範本需要的其他特定設定(例如 Next 16 的試驗性旗標)。

實際上就是輸出一個包含需要欄位的 nextConfig。 對本課來說,最重要的是:如果在 ChatGPT 中小工具載不到 CSS/JS,罪魁禍首經常是 assetPrefix

proxy.ts(原 middleware.ts

這個檔案會在來自 ChatGPT 的請求與你的路由之間插入 middleware。範本裡它通常會:

  • 設定 CORS 標頭,讓 ChatGPT 的 iframe 有權向你的伺服器發送請求;
  • 有時也會為 React Server Components 設定額外的標頭。

現在不必理解所有細節。只要記得:如果 ChatGPT 抱怨 CORS,或你在 DevTools 看到奇怪的存取禁止錯誤,請查看 proxy.ts

.env

.env(或 .env.local)是存放機密與環境參數的地方:

  • OPENAI_API_KEY(如果 MCP 伺服器本身會呼叫 OpenAI API),
  • 你內部 API 的位址,
  • 第三方服務的 token 等。

有個重要細節:在 Next.js 中,以 NEXT_PUBLIC_ 開頭的變數會自動被打包進 JS 並在瀏覽器可用。 千萬不要這樣處理 OPENAI_API_KEY;機密只能放在伺服器端變數。

package.jsontsconfig.json

package.json 你會看到:

  • Next.js、React、Apps SDK、MCP SDK 與其他相依套件的版本;
  • devbuildstart 指令,以及有時候的輔助指令(linter、formatter 等)。

tsconfig.json 則是你熟悉的 TypeScript 設定:

  • 別名路徑(@/lib@/components),
  • 嚴格模式,
  • 編譯目標。

就本課而言,重點是理解:範本使用一般的 TypeScript 技術棧,你可以用標準方式擴充它

7. 開發者的快速「專案導覽」

讓我們固定幾個常見情境該去哪裡找,不用清單,就以小劇本形式說明。

想改小工具中的文字/按鈕,打開小工具的 UI 檔案:可能是 app/widget/page.tsxapp/page.tsx——取決於範本。 在那裡修改 JSX、加入新元件、串接設計系統。你也會在這裡使用 Apps SDK 執行期 (window.openai 或方便的 hooks)來顯示資料。

若要新增一個按鈕,點擊後在伺服器做點事,同樣從 UI 檔案開始。 小工具裡的按鈕點擊會呼叫 window.openai.callTool, 而這個工具的實作會加在 MCP 伺服器的設定裡,也就是 app/mcp/route.ts 附近的程式碼。UI ↔ tool 邏輯的串接我們會在模組 4 之後深入解析。

想要讓 ChatGPT 學會新的功能(例如「搜尋旅遊方案」或「挑選商品」), 請到 MCP 層(由 app/mcp/route.ts 匯入的檔案)。 在那裡註冊新的 tool,包含 JSON Schema、描述與處理器。小工具之後可以透過 window.openai.toolOutput 讀取結果並精美地呈現。

如果靜態資源掛了,或小工具只在 ChatGPT 內顯示異常、但本機正常, 想想黏合層。首先檢查 next.config.ts (尤其是 assetPrefix)以及 middleware.ts/proxy.ts(CORS)。 如果你最近換了通道、URL,或部署到 Vercel,這些設定是否正確就非常關鍵。

最後,如果你懷疑是金鑰或環境變數的問題,請檢查這三個檔案—— .env.localpackage.json (確認實際使用哪些相依與指令)以及開發伺服器的日誌。 這組合負責確保 MCP 能存取所需的機密與服務。

8. 迷你實作:親手熟悉檔案系統

理論歸理論,我們動手確認一下各部分位置。你可以直接在編輯器/IDE 裡操作。

試著在你的專案打開 app 資料夾,找出哪個檔案負責小工具。 如果範本使用 app/page.tsx,你會在那裡看到像 「HelloWorld — ChatGPT App」或歡迎文字。如果沒有獨立的小工具資料夾,打開 app/page.tsx 並確認裡面有 'use client' 與一些 JSX 標記。

接著找到 app/mcp/route.ts。注意它匯入了哪些模組: 通常你會看到不是直接使用 MCP SDK,就是呼叫 lib/mcp/* 的輔助函式。評估這層薄封裝做得多「薄」——理想情況下幾乎沒有商業邏輯, 只有「收 JSON → 交給伺服器 → 回傳 JSON」。

然後看看 next.config.tsproxy.ts/middleware.ts。 不必理解所有內容,只要記住:

  • next.config.ts 負責 Next 設定,包括資產的建置與傳送規則;
  • proxy.ts 會介入 HTTP 請求(幾乎可以看到它在處理標頭)。

最後打開 .env.env.local,確認你的金鑰放在那裡而不是在程式碼中。 如果你看到 NEXT_PUBLIC_OPENAI_API_KEY—— 這是個在還只做本機開發時就應該立刻修正的好時機。

9. 視覺化示意:ChatGPT 如何與你的範本互動

為了讓全貌更清晰,看看這個簡單流程:

flowchart TD
    U[ChatGPT 使用者] -->|發送請求| M[ChatGPT 模型]

    M -->|呼叫 tool| MCP["你的 MCP endpoint
app/mcp/route.ts"] MCP -->|"MCP 的 JSON 回應(structuredContent, _meta, UI 連結)"| M M -->|決定顯示 UI| WIDGET_URL["小工具的 URL
(/widget 或 /)"] WIDGET_URL -->|iframe| W[你的小工具
app/page.tsx] W -->|讀取 window.openai.toolOutput
+ widgetState| U

這裡要注意,幾乎所有情況下的發起者都是 ChatGPT 模型,而不是使用者的瀏覽器, 這與傳統的網頁應用不同。你的 app/mcp/route.tsapp/widget/page.tsx——其實是同一個 Next.js 專案中的兩扇門: 一扇給機器(MCP),另一扇給 UI。

只要牢記這張專案地圖(小工具 → MCP 層 → 設定),並刻意避開前述那些地雷, 在接下來的課程中你就能把重心放回 App 的邏輯與 UX,而不是在那個「把一切弄壞的檔案」上打轉。

10. 操作範本結構的常見錯誤

錯誤 №1:把小工具當成網站的一般頁面。
有時開發者看到範本裡同時有 app/page.tsxapp/widget/page.tsx, 就改了「不該改」的檔案,然後納悶為什麼 ChatGPT 內沒有變化。小工具是被當作 outputTemplate/iframe 來用的那個頁面。如果你改的是其他路由,ChatGPT 根本不會知道。請務必對照範本的 README, 確認哪個 URL 被指定為小工具。

錯誤 №2:在 MCP 伺服器檔案寫入客戶端程式碼(windowdocument)。
app/mcp/route.ts 與它匯入的一切都在伺服器端執行。任何在那裡使用 window 或 DOM API 的嘗試都會讓執行期崩潰。如果要做 UI 的事情,幾乎可以肯定要放在 app/widget 下或其他客戶端元件中。MCP 層是純後端:處理請求、資料庫、外部 API, 並組裝結構化回應。

錯誤 №3:忽略 assetPrefix 與 CORS 設定。
在本機 localhost:3000 一切看似正常,但一旦透過通道在 ChatGPT 打開 App——樣式不見了、 JS 載不進來、主控台充滿 CORS 錯誤。往往是 next.config.tsmiddleware.ts/proxy.ts 的設定沒考慮到新的公開 URL,或在重構時不小心破壞了。 修改這些檔案時,務必記得你的程式碼會活在 ChatGPT 網域的 iframe 裡,而不是直接在 localhost

錯誤 №4:不把機密放在 .env,而是寫在程式或用 NEXT_PUBLIC_* 變數。
OPENAI_API_KEY 藏在某個 const apiKey = 'sk-...',例如 app/widget/page.tsx——是最糟糕的主意:金鑰會出現在 JS bundle 中並被任何使用者取得。 幾乎同樣糟的是建立 NEXT_PUBLIC_OPENAI_API_KEY,因為 NEXT_PUBLIC_ 前綴保證會把它放到瀏覽器。務必把機密放在 .env,且不要使用該前綴,並只在伺服器端(MCP 伺服器、後端函式)使用。

錯誤 №5:把範本想得「太聰明」而不敢改動。
有些開發者把官方 starter 當作神聖之物:「最好別碰,免得弄壞整合」。 結果他們把自己的程式碼都寫在邊邊角角,讓架構更複雜,最後還是踩同樣的雷。事實上範本只是整理得漂亮的 Next.js 專案, 外加少量為 Apps SDK 準備的設定。理解 app/ 就是 UI 與 MCP,而其餘是一般設定檔, 會讓你更自在:你會把它當成熟悉的 React/Next 專案來工作,而不是魔法盒子。

錯誤 №6:試圖「在小工具層級」解決所有問題。
有時會想在 UI 端做所有事:商業邏輯、連資料庫、外部 API 請求。在 ChatGPT Apps 的情境下這特別糟糕: 小工具活在非常嚴格的沙盒裡,看不到你的機密,並高度仰賴 window.openai。 如果要處理嚴肅的事——請放在 MCP 層與後端服務;小工具應該是薄薄的展示層,負責呈現結構化資料,必要時觸發工具。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION