1. Вступ
Проєкт ChatGPT App HelloWorld — це не «магічна чорна скринька від CodeGym, у якій краще нічого не чіпати». Це звичайний Next.js‑проєкт. У ньому просто одночасно співіснують:
- фронтенд, який відображається всередині ChatGPT,
- MCP‑сервер, що відповідає на виклики інструментів (tools),
- налаштування, які поєднують усе це з ChatGPT.
Якщо не розуміти, де і що розташовано, зазвичай трапляються три класичні сценарії:
- Розробник випадково пише window у серверному файлі, отримує збій — і починає ненавидіти весь набір технологій.
- Намагається додати кнопку в UI, але редагує не той page.tsx (наприклад, корінь застосунку, а не віджет) і не бачить змін у ChatGPT.
- Випадково кладе OPENAI_API_KEY у клієнтську частину — і ключ «витікає» в браузер.
Тож сьогоднішня мета — побудувати мапу: де UI, де MCP, де конфіги та куди звертатися, коли ви хочете:
- змінити зовнішній вигляд віджета;
- додати новий tool;
- налаштувати якесь платформне обмеження (CORS, assetPrefix тощо).
2. Високорівнева анатомія проєкту
Next.js‑проєкт ChatGPT App HelloWorld використовує App Router і організований навколо теки app/. Усередині цього дерева сторінок співіснують:
- UI віджета, який відображається всередині ChatGPT,
- MCP‑endpoint, що обробляє виклики інструментів.
Типове дерево (спрощене; назви тек у вашому шаблоні можуть відрізнятися, але підхід той самий):
my-chatgpt-app/
├─ app/
│ ├─ api/ // REST API
│ │ └─ time/ // GET /api/time повертає час на сервері
│ │ └─ route.ts
│ ├─ hooks/ // Набір хуків з офіційного Apps SDK
│ │ ├─ 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/ // Статика: іконки, маніфест тощо
├─ next.config.ts // Конфіг Next.js і Apps-специфічні налаштування (assetPrefix тощо)
├─ proxy.ts // CORS/заголовки для роботи всередині iframe (колишній middleware.ts)
├─ package.json // Залежності проєкту
├─ tsconfig.json // Конфігурація TypeScript
└─ .env.local // Секрети: OPENAI_API_KEY тощо
Якщо віджетів кілька, то зазвичай їх кладуть не в app/page.tsx, а в app/widget/page.tsx. Але логіка від цього не змінюється: усе одно є одна сторінка‑віджет і один endpoint, який виконує роль MCP‑сервера.
Зручно мислити так: ваш репозиторій — це «дволикий Янус»:
- одне «обличчя» — шлях /mcp, куди звертається ChatGPT, коли хоче викликати інструмент;
- інше «обличчя» — шлях /widget (або /), який завантажується в iframe, коли модель вирішує показати ваш UI.
Щоб не плутатися, зафіксуймо три групи файлів:
- UI‑шар — усе, що повʼязано з React/Next‑сторінками (app/widget, компоненти, стилі).
- MCP‑шар — app/mcp/route.ts і файли, які він використовує.
- «Клейовий» шар і конфіги — next.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.
Трохи нижче ми пройдемося по кожному з цих шарів.
3. Де «живе» віджет: тека app/widget та/або app/page.tsx
Почнімо з того, що ви змінюватимете найчастіше, — віджета, тобто UI, який видно всередині ChatGPT.
У більшості сучасних проєктів є або:
- файл 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.js спробує зробити сторінку серверною — і ви отримаєте помилки на кшталт «window is not defined».
По‑друге, це звичайний, зовсім не магічний React‑компонент. Ви можете:
- розбивати його на підкомпоненти в components/,
- використовувати Tailwind або будь‑яку іншу CSS‑систему,
- підʼєднувати контексти, хуки тощо.
По‑третє, пізніше саме тут ви будете:
- читати window.openai.toolInput і window.openai.toolOutput, щоб намалювати реальні дані,
- зберігати widgetState через window.openai.setWidgetState,
- викликати openExternal, callTool та інші методи рантайму.
Наразі достатньо знати одне: якщо ви хочете змінити вигляд інтерфейсу, вам майже напевно потрібно відкрити app/widget/page.tsx або app/page.tsx.
4. Кореневий layout: app/layout.tsx як «рамка» для всього застосунку
Наступний важливий файл — app/layout.tsx. Він:
- визначає HTML‑структуру (<html>, <body>),
- підʼєднує глобальні стилі (globals.css),
- часто ініціалізує «bootstrap» для Apps SDK (обгортку, яка слухає 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, підписатися на глобальні дані (theme, displayMode, toolInput тощо) і передати їх дочірнім компонентам.
Важливий практичний висновок: якщо вам потрібно підʼєднати глобальний контекст, стилі або UI‑бібліотеку (наприклад, shadcn/ui), місце для цього майже завжди app/layout.tsx (або layout усередині app/widget — для налаштувань і компонентів, специфічних саме для віджета).
Розбір NextChatSDKBootstrap
NextChatSDKBootstrap я бачив в офіційному шаблоні від Vercel. Це команда, яка створила Next і активно його розвиває. На їхньому сайті є гарний матеріал про ChatGPT App на Next. Також є Starter Template. Хоча в кількох місцях він уже трохи застарів, є всі шанси, що його й надалі підтримуватимуть актуальним.
Виділимо 5 ключових речей, які нам дає NextChatSDKBootstrap:
- 1. Прибирає проблеми з гідратацією
Річ у тім, що ChatGPT спочатку завантажує HTML вашого віджета на свій сервер, очищує й «патчить» його. У результаті механізм гідратації свариться і сипле попередження (warnings) у консоль. Це може завадити вам пройти ревʼю. - 2. «Патчить» історію браузера
Річ у тім, що ваш віджет завантажується в iframe зі спеціального домену ChatGPT. Якщо ви використовуватимете власний домен, ви порушите пісочницю. Тому в історії браузера зберігається лише шлях — без домену. - 3. Переписує функцію fetch()
У віджеті не працюватимуть запити fetch() на відносні адреси без домену, адже домен у iframe інший. Тому ми підміняємо функцію fetch() на свою — вона надсилає запити без домену на правильний URL. Якщо домен указано, усе працює без змін. - 4. Переходи за посиланнями працюють коректно
Якщо посилання відкриватимуться всередині iframe, ChatGPT цього не схвалить. Тому додано код, який відстежує натискання на посилання і відкриває їх у зовнішньому вікні через openExternal(). - 5. Додавання head base (DEPRECATED)
Також цей код додавав <base> у <head>, але це вже не працює. Пісочниця скидає будь‑який встановлений base, тож раджу використовувати абсолютні посилання для всього, що є: скрипти, ресурси, шрифти, API тощо.
5. MCP‑сервер: app/mcp/route.ts
Тепер переходимо до другої половини «дволикого Януса» — сервера, який спілкується із ChatGPT через MCP.
Файл app/mcp/route.ts — це звичайний Route Handler для App Router, який:
- приймає HTTP‑запити від ChatGPT (зазвичай POST із JSON‑payload у форматі MCP),
- передає їх у MCP‑сервер (на базі @modelcontextprotocol/sdk або тонкої обгортки),
- повертає назад JSON‑відповідь у форматі MCP.
Є два варіанти: можна писати на «чистому» 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, який ви десь створюєте (наприклад, у lib/mcp/server.ts) за допомогою SDK. У нашому випадку він повністю міститься в app/mcp/route.ts. Розберімо докладно, що саме є в цьому файлі.
type ContentWidget
На початку файлу в нас описаний тип ContentWidget. Він описує всі дані віджета і використовується у двох місцях: під час реєстрації віджета як mcp‑resource і коли mcp‑tool повертає metadata, де вказує, який віджет використати для відображення даних, які він повернув.
type ContentWidget = {
id: string; // Унікальне ім’я/key
title: string; // Title
description: string; // Description
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() — повертає метадані віджета;
- 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(), у який передаються 4 параметри:
- id (key) MCP‑ресурсу;
- 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(), у який передаються 3 параметри:
- id (key) MCP‑tool;
- метадані 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, // Метод робить щось важливе — потрібен confirm
openWorldHint: false, // Метод змінює щось у third party services
readOnlyHint: true // Метод нічого не змінює
},
}
Функція, яка робить щось важливе
async (input, extra) => {
// 1. Валідація параметрів
// 2. Робимо щось важливе
return {
content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Опис результату для ІІ
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‑протоколі є 3 сутності (про які ви докладніше дізнаєтеся в модулі 6):
- MCP Resources
- MCP Templates
- MCP Tools
Отже, цей "openai/outputTemplate" не має жодного стосунку до MCP Templates. MCP Templates узагалі ніяк не використовуються в ChatGPT Apps. Слово template тут узялося звідси:
Віджети задумувалися як шаблон для відображення JSON. MCP‑tool повернув певний JSON, ШІ показав віджет, передав йому JSON через параметр ToolOutput — і віджет гарно відобразив ці дані. outputTemplate — це просто синонім віджета.
Думаю, на цьому все. Докладніше ми розберемо ці речі в модулі 4: як саме описувати інструменти, JSON Schema і обробники. Наразі достатньо розуміти: якщо щось повʼязано з інструментами (tools) і логікою, шукайте поруч із app/mcp/route.ts.
6. Конфігурація і «клей»: next.config.ts, middleware.ts, .env та інші
Тепер розберемо основний набір файлів, потрібних для того, щоб ваш Next.js‑проєкт коректно працював усередині iframe ChatGPT і був доступний ChatGPT через HTTPS‑тунель (ngrok, Cloudflare Tunnel тощо; про тунелі ми ще поговоримо окремо).
next.config.ts
У цьому файлі, окрім стандартних налаштувань Next.js, часто задають:
- assetPrefix — щоб статика (JS, CSS із /_next/) коректно завантажувалася не з домену ChatGPT, а з вашого dev‑URL (тунелю або Vercel);
- будь‑які специфічні налаштування, потрібні шаблону (наприклад, експериментальні прапорці під Next 16).
На практиці це виглядає як звичайний експорт nextConfig з потрібними полями. Для лекції нам важливо одне: якщо в ChatGPT віджет не може завантажити CSS/JS, дуже часто винен саме assetPrefix.
proxy.ts (колишній middleware.ts)
Цей файл вставляє шар middleware між запитом із ChatGPT і вашими роутами. У шаблоні він зазвичай:
- встановлює CORS‑заголовки, щоб iframe ChatGPT узагалі мав право звертатися до вашого сервера;
- іноді налаштовує додаткові заголовки для React Server Components.
Знати всі тонкощі зараз не обовʼязково. Корисно лише памʼятати: якщо ChatGPT скаржиться на CORS або ви бачите в DevTools дивні помилки про заборону доступу, зазирніть у proxy.ts.
.env
Файл .env (або .env.local) — місце для секретів і змінних середовища:
- OPENAI_API_KEY (якщо MCP‑сервер сам ходить в OpenAI API),
- адреси ваших внутрішніх API,
- токени сторонніх сервісів тощо.
Є важливий нюанс: у Next.js змінні, що починаються з NEXT_PUBLIC_, автоматично потрапляють у JS‑бандл і стають доступними в браузері. Ніколи не робіть так з OPENAI_API_KEY: секрети мають бути лише серверними змінними.
package.json і tsconfig.json
У package.json ви побачите:
- версії Next.js, React, Apps SDK, MCP SDK та інших залежностей;
- скрипти dev, build, start, а іноді й допоміжні команди (лінтер, форматер тощо).
У tsconfig.json лежать звичні для вас налаштування TypeScript:
- шляхи аліасів (@/lib, @/components),
- строгий режим,
- таргети компіляції.
З погляду цього курсу головне — розуміти, що шаблон використовує звичайний TypeScript‑стек, і ви можете розширювати його стандартним чином.
7. Швидкий «навігатор по проєкту» для розробника
Зафіксуймо, куди йти, коли ви хочете зробити типові речі. Без списків — просто у вигляді міні‑сценаріїв.
Якщо вам хочеться змінити текст або кнопки у віджеті, відкрийте файл UI віджета: це або app/widget/page.tsx, або app/page.tsx — залежно від шаблону. Там ви редагуєте JSX, додаєте нові компоненти, підʼєднуєте дизайн‑систему. І саме тут ви використовуватимете рантайм Apps SDK (window.openai або зручні хуки) для відображення даних.
Якщо потрібно додати нову кнопку, яка щось робить на сервері, ви все одно починаєте з 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.local, package.json (щоб упевнитися, які залежності та скрипти реально використовуються) і логи dev‑сервера. Саме ця звʼязка відповідає за те, щоб 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.ts і proxy.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 -->|"JSON‑відповідь MCP (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.ts і app/widget/page.tsx — це просто двоє різних «дверей» в один і той самий Next.js‑проєкт: одна для «робота» (MCP), інша — для UI.
Якщо тримати в голові цю мапу проєкту (віджет → MCP‑шар → конфіги) і свідомо уникати перелічених «граблів», далі за курсом ви зможете зосередитися на логіці та UX свого App, а не на пошуках «того самого файлу, який усе ламає».
10. Типові помилки під час роботи зі структурою шаблону
Помилка № 1: Плутати віджет зі звичайною сторінкою сайту.
Іноді розробник бачить у шаблоні і app/page.tsx, і app/widget/page.tsx, редагує «не той» файл і дивується, чому зміни не зʼявляються в ChatGPT. Віджет — це саме та сторінка, яка використовується як outputTemplate/iframe для MCP‑інструмента. Якщо ви змінюєте інший роут, ChatGPT про це навіть не дізнається. Завжди звіряйтеся з README шаблону і дивіться, який URL указано як віджет.
Помилка № 2: Писати клієнтський код (window, document) у серверних файлах MCP.
Файл app/mcp/route.ts і все, що він імпортує, виконується на сервері. Будь‑яка спроба використати там window або DOM‑API призведе до падіння рантайму. Якщо хочеться щось зробити в UI, це майже напевно має бути у файлах під app/widget або в інших клієнтських компонентах. MCP‑шар — це чистий backend: запити, бази, зовнішні API і формування структурованої відповіді.
Помилка № 3: Ігнорувати assetPrefix і CORS‑налаштування.
На локальному localhost:3000 усе працює чудово, але варто відкрити App через тунель у ChatGPT — і стилі зникають, JS не завантажується, у консолі купа CORS‑помилок. Часто причина в тому, що конфігурація next.config.ts або middleware.ts/proxy.ts не враховує новий публічний URL або була випадково зламана під час рефакторингу. Змінюючи ці файли, завжди тримайте в голові, що ваш код житиме всередині iframe на домені ChatGPT, а не напряму на localhost.
Помилка № 4: Зберігати секрети не в .env, а прямо в коді або в NEXT_PUBLIC_*‑змінних.
Ховати OPENAI_API_KEY у const apiKey = 'sk-...' десь у app/widget/page.tsx — це найгірша ідея: ключ опиниться в JS‑бандлі й стане доступним будь‑якому користувачу. Майже так само погано — зробити змінну NEXT_PUBLIC_OPENAI_API_KEY, адже префікс NEXT_PUBLIC_ гарантує потрапляння в браузер. Завжди кладіть секрети в .env без цього префікса і використовуйте їх лише на серверному боці (MCP‑сервер, backend‑функції).
Помилка № 5: Вважати шаблон «занадто розумним» і боятися його змінювати.
Іноді розробники ставляться до офіційного стартера як до чогось «священного»: «краще туди не лізти, раптом зламаю інтеграцію». У результаті вони пишуть увесь свій код десь збоку, ускладнюють архітектуру і все одно наступають на ті самі граблі. Насправді шаблон — це лише акуратно зібраний Next.js‑проєкт із парою налаштувань під Apps SDK. Розуміння того, що app/ — це UI і MCP, а все інше — звичайні конфіги, дуже «звільняє»: ви починаєте працювати з кодом як зі звичним React/Next‑проєктом, а не як із магічною коробкою.
Помилка № 6: Намагатися вирішити всі проблеми «на рівні віджета».
Іноді хочеться робити все в UI: і бізнес‑логіку, і доступ до баз, і запити до зовнішніх API. У контексті ChatGPT Apps це особливо погана ідея: віджет живе в дуже жорсткій пісочниці, не бачить ваших секретів і сильно залежить від window.openai. Якщо потрібно щось серйозне — місце цьому в MCP‑шарі й backend‑сервісах. А віджет має бути тонким презентаційним шаром: він відображає структуровані дані і, за потреби, запускає інструменти.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ