1. Introdução
O projeto ChatGPT App HelloWorld — não é uma “caixa‑preta mágica da CodeGym em que é melhor não mexer”. É um Next.js‑projeto comum, só que nele convivem ao mesmo tempo:
- o frontend, que é renderizado dentro do ChatGPT,
- um servidor MCP, que atende às chamadas de ferramentas (tools),
- as configurações que colam tudo isso ao ChatGPT.
Se você não entender onde está o quê, geralmente acontecem três cenários clássicos:
- O desenvolvedor por engano usa window em um arquivo de servidor, toma um crash e passa a odiar toda a stack.
- Tenta adicionar um botão na UI, mas edita o page.tsx errado (por exemplo, o root do app em vez do widget) e não vê mudanças no ChatGPT.
- Acidentalmente coloca OPENAI_API_KEY na parte cliente, e a chave vaza para o navegador.
Por isso, o objetivo de hoje — traçar o mapa: onde fica a UI, onde fica o MCP, onde ficam as configs, e aonde ir quando você quiser:
- mudar a aparência do widget;
- adicionar uma nova tool;
- ajustar alguma configuração de plataforma (CORS, assetPrefix etc.).
2. Anatomia de alto nível do projeto
O projeto Next.js do ChatGPT App HelloWorld usa o App Router e é organizado em torno da pasta app/. Nela, na mesma árvore de páginas, convivem:
- a UI do widget, que será renderizada dentro do ChatGPT,
- o endpoint MCP, que processará as chamadas de tool.
Árvore típica (simplificada; os nomes de pastas no seu template podem variar, mas o padrão é o mesmo):
my-chatgpt-app/
├─ app/
│ ├─ api/ // REST API
│ │ └─ time/ // GET /api/time retorna a hora no servidor
│ │ └─ route.ts
│ ├─ hooks/ // Conjunto de hooks do Apps SDK oficial
│ │ ├─ use-call-tool.ts
│ │ ├─ use-display-mode.ts
│ │ └─ use-open-external.ts
│ ├─ mcp/ // Servidor MCP: é aqui que o ChatGPT bate ao chamar tools
│ │ └─ route.ts
│ ├─ globals.css // globals.css raiz de todo o app
│ ├─ layout.tsx // layout raiz de todo o app
│ └─ page.tsx // Página do widget dentro do ChatGPT
├─ public/ // Estáticos: ícones, manifest etc.
├─ next.config.ts // Config do Next.js e configurações específicas de Apps (assetPrefix etc.)
├─ proxy.ts // CORS/cabeçalhos para funcionar dentro do iframe (antigo middleware.ts)
├─ package.json // Dependências do projeto
├─ tsconfig.json // Configuração do TypeScript
└─ .env.local // Segredos: OPENAI_API_KEY e outros
Se houver vários widgets, normalmente eles ficam não em app/page.tsx, mas em app/widget/page.tsx. Mas a lógica não muda: ainda assim há uma página‑widget e um endpoint que atua como servidor MCP.
É útil pensar assim: seu repositório — é um “Jano de duas faces”:
- uma “face” — o caminho /mcp, para onde o ChatGPT vai quando quer chamar uma ferramenta;
- a outra “face” — o caminho /widget (ou /), que é carregado no iframe quando o modelo decide exibir sua UI.
Para não se confundir, fixe na cabeça três grupos de arquivos:
- Camada de UI — tudo que está relacionado às páginas React/Next (app/widget, componentes, estilos).
- Camada MCP — app/mcp/route.ts e os arquivos que ele usa.
- Camada de “cola” e configs — next.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.
Mais adiante vamos passar por cada uma dessas camadas.
3. Onde vive o widget: pasta app/widget e/ou app/page.tsx
Comecemos pelo que você vai mexer com mais frequência — o widget, isto é, a UI que ficará visível dentro do ChatGPT.
Na maioria dos projetos atuais existe ou:
- a pasta app/widget/page.tsx — o widget vive sob o prefixo separado /widget,
- ou o app/page.tsx raiz — o widget coincide com a página raiz.
Principais sinais de que um arquivo é o do widget:
- no topo há 'use client', porque o componente roda no navegador, conversa com window e o Apps SDK;
- é um componente React comum, que renderiza a marcação e (um pouco mais adiante no curso) conversa com window.openai.
Exemplo mais simples de widget didático (você pode ver algo bem parecido no seu projeto):
// 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">
Aqui vamos construir a UI do nosso widget.
</p>
</main>
);
}
Se no seu template o widget estiver diretamente em app/page.tsx, o código será praticamente o mesmo, apenas sem a pasta intermediária widget.
Preste atenção a alguns pontos.
Em primeiro lugar, a diretiva 'use client' é obrigatória: o widget lê/escreve em window.openai, escuta eventos etc., e isso só é possível em um componente cliente. Se você removê‑la, o Next vai tentar tornar a página server‑side e você receberá erros do tipo “window is not defined”.
Em segundo lugar, é um componente React comum, nada mágico. Você pode:
- dividi‑lo em subcomponentes em components/,
- usar Tailwind ou qualquer outro sistema de CSS,
- conectar contexts, hooks etc.
Em terceiro lugar, mais tarde é aqui que você vai:
- ler window.openai.toolInput e window.openai.toolOutput para renderizar dados reais,
- salvar o widgetState via window.openai.setWidgetState,
- chamar openExternal, callTool e outros métodos do runtime.
Por agora, basta saber: se você quer mudar a interface visual — você quase certamente irá para app/widget/page.tsx ou app/page.tsx.
4. Layout raiz: app/layout.tsx como a “moldura” do aplicativo inteiro
O próximo arquivo importante é o app/layout.tsx. Ele:
- define a estrutura HTML (<html>, <body>),
- conecta estilos globais (globals.css),
- frequentemente inicializa o “bootstrap” para o Apps SDK (um wrapper que escuta window.openai e encaminha os dados para o React).
Exemplo simplificado:
// 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>
);
}
O nome NextChatSDKBootstrap aqui é ilustrativo; no seu template pode ser OpenAIAppProvider ou outro componente. A função dele costuma ser uma só: configurar a conexão entre a árvore do React e o runtime do Apps SDK, assinar dados globais (theme, displayMode, toolInput etc.) e distribuí‑los aos filhos.
Conclusão prática importante: se você precisa conectar um contexto global, estilos ou uma biblioteca de UI (por exemplo, shadcn/ui) — o lugar para isso quase sempre é o app/layout.tsx (ou um layout dentro de app/widget para configurações e componentes específicos do widget).
Dissecando NextChatSDKBootstrap
O NextChatSDKBootstrap eu vi no template oficial da Vercel. Caso você não soubesse, são justamente as pessoas que criaram e desenvolvem o Next. No site deles há um post bacana sobre ChatGPT App em Next. E também há um Starter Template. Embora em alguns pontos já esteja um pouco desatualizado, acho que há boas chances de manterem sua atualidade.
Vamos destacar 5 coisas‑chave que o NextChatSDKBootstrap nos dá:
- 1. Corrige problemas de hidratação
A questão é que o ChatGPT primeiro carrega o HTML do seu widget no próprio servidor, faz limpeza e patches. Como resultado, o mecanismo de hidratação reclama e despeja Warnings no console. Isso pode atrapalhar você a passar no review. - 2. Faz patch no histórico do navegador
Seu widget é carregado em um iframe de um domínio especial do ChatGPT. E se você usar seu próprio domínio, vai quebrar a sandbox. Por isso, no histórico do navegador é salvo apenas o path sem o domínio. - 3. Reescreve a função fetch()
Todo o seu fetch() para endereços relativos sem domínio não vai funcionar no widget, pois o domínio do iframe é outro. Então substituímos a função fetch() por uma nossa, que envia as requisições sem domínio para a URL correta. Se o domínio estiver indicado, tudo funciona sem alterações. - 4. Cliques em links funcionam
Se os links abrirem dentro do iframe, o ChatGPT não vai aprovar. Por isso foi adicionado um código que rastreia cliques em links e os abre em uma janela externa via openExternal(). - 5. Definição de head base (DEPRECATED)
Este código também adicionava <base> no <head>, mas isso não funciona mais. A sandbox reseta qualquer base configurado, então recomendo usar links absolutos para tudo: scripts, recursos, fontes, API etc.
5. Servidor MCP: app/mcp/route.ts
Agora vamos à segunda metade do “Jano de duas faces” — o servidor que conversa com o ChatGPT via MCP.
O arquivo app/mcp/route.ts — é um Route Handler comum do App Router, que:
- recebe requisições HTTP do ChatGPT (normalmente POST com payload JSON no formato MCP),
- as repassa ao servidor MCP (baseado em @modelcontextprotocol/sdk ou um wrapper leve),
- devolve de volta uma resposta JSON no formato MCP.
Há duas opções: você pode escrever usando o MCP SDK puro ou tentar suavizar as arestas e usar algumas classes do próprio Next/Vercel.
Aqui vai uma opção com o MCP SDK puro em TS:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// 1. Criamos o servidor MCP
const server = new McpServer({
name: "simple-mcp-server",
version: "1.0.0",
});
// 2. Registramos os MCP Resources
// 3. Registramos os MCP Tools
// 4. Transporte HTTP
const transport = new HttpServerTransport({
port: 3001,
path: "/mcp",
});
// 5. Inicialização do servidor
await server.connect(transport);
Mas é melhor usar algumas classes prontas para tornar o trabalho mais agradável:
// 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;
Aqui McpGateway — é uma classe‑wrapper ao redor de McpServer, que você cria em algum lugar (por exemplo, em lib/mcp/server.ts) com a ajuda do SDK. No nosso caso, ela cabe inteira em app/mcp/route.ts. Vamos dissecar completamente o que há neste arquivo.
type ContentWidget
No início do arquivo descrevemos o tipo ContentWidget. Ele contém todos os dados do widget e é usado em dois lugares: ao registrar o widget como mcp‑resource e quando o mcp‑tool retorna metadata, indicando qual widget usar para exibir os dados que ele retornou.
type ContentWidget = {
id: string; // Nome/key único
title: string; // Título
description: string; // Descrição
templateUri: string; // URI único do widget; pode ser qualquer um. Não afeta nada.
invoking: string; // Texto acima do widget enquanto ele carrega
invoked: string; // Texto acima do widget quando ele já carregou
html: string; // Todo o código HTML do widget
widgetDomain: string; // “Domínio” do widget. Não afeta nada.
};
class McpGateway
Classe‑wrapper sobre o McpServer, simplifica algumas coisas. Contém 6 métodos:
- initialize() — aqui carregamos o HTML do nosso widget
- registerResources() — registramos widgets como mcp‑resources
- registerTools() — registramos funções como mcp‑tools
- widgetMeta() — retorna os metadados do widget
- getAppsSdkCompatibleHtml() — carrega o HTML do widget e faz alguns patches
- makeImgUrlsAbsolute() — patch no HTML: transforma links de imagens em absolutos
Vamos passar por eles em mais detalhes:
public async initialize()
Este método baixa da internet o código HTML dos widgets e preenche o objeto do tipo ContentWidget.
{
id: "hello_world", // Key único do widget
templateUri: "ui://widget/hello_world.html", // URI único do widget. “ui:” não significa nada.
title: "HelloWorld Widget", // Nome do widget
description: "Displays the HelloWorld widget", // Explicação para a LLM do que o widget faz
invoking: "Loading widget...", // Texto acima do widget durante o carregamento
invoked: "Widget loaded", // Texto acima do widget após o carregamento
html: htmlWidget, // HTML do widget
widgetDomain: baseURL, // “Domínio” do widget. Atualmente não afeta nada.
}
public registerResources()
Registra widgets como mcp‑resources. Chama o método server.registerResource(), ao qual são passados 4 parâmetros:
- id/key do recurso MCP
- o URI do recurso (isso é necessário especificamente para o protocolo MCP; para o widget é praticamente sinônimo de um endereço único)
- metadados do recurso MCP
- função que retorna o recurso MCP
Metadados do widget
{
title: widget.title, // Nome do recurso/widget
description: widget.description, // Descrição do recurso/widget
mimeType: "text/html+skybridge", // Importante! Apenas esse HTML será exibido como widget
_meta: {
"openai/widgetDescription": widget.description, // Descrição do widget
"openai/widgetPrefersBorder": true, // Solicitamos ao ChatGPT desenhar uma borda no widget
},
}
Widget como recurso MCP
{
uri: uri.href, // Nosso URI (vem do parâmetro uri)
mimeType: "text/html+skybridge", // Importante! Apenas esse HTML será exibido como widget
text: widget.html, // HTML do widget
_meta: {
"openai/widgetDescription": widget.description, // Descrição do widget
"openai/widgetPrefersBorder": true, // Solicitamos ao ChatGPT desenhar uma borda no widget
"openai/widgetDomain": widget.widgetDomain, // “Domínio” do widget. Atualmente não afeta nada.
"openai/widgetCSP": { // Importante! Domínios acessíveis ao widget:
connect_domains: [ // Domínios para conexões (fetch etc.)
baseURL,
"https://codegym.cc",
],
resource_domains: [ // Domínios para recursos (css/fonts/img)
baseURL,
"https://codegym.cc",
"https://cdn.tailwindcss.com",
"https://persistent.oaistatic.com",
"https://fonts.googleapis.com",
"https://fonts.gstatic.com"
]
}
},
}
No futuro ainda voltaremos várias vezes ao openai/widgetCSP, mas por ora vale destacar 2 pontos sobre ele:
- connect_domains — lista de domínios para:
- fetch()
- carregamento de scripts
- openExternal()
- resource_domains — lista de domínios para:
- imagens
- CSS
- fontes
Em teoria você pode listar 200 domínios, mas se vai conseguir passar no review com uma lista dessas — é outra história.
Também estudei esses parâmetros em apps já publicados e encontrei amplitude.com. O que também é uma boa notícia. Acredito que uma boa analítica não faz mal a ninguém.
public registerTools()
Registra funções como mcp‑tools. Chama o método server.registerTool(), ao qual são passados 3 parâmetros:
- id/key do MCP‑tool
- metadados do MCP‑tool
- função que retorna o MCP‑tool
Metadados do instrumento
Todos os parâmetros desta lista são importantes. Vou falar mais sobre eles nas próximas aulas.
{
title: widget.title, // Nome do instrumento
description: "Returns HelloWorld widget", // Importante! Descrição do que o instrumento faz
inputSchema: z.object({}).describe("No inputs"), // Esquema dos parâmetros do instrumento. Pode usar Zod
_meta: this.widgetMeta(widget), // Metadados do widget: qual widget exibir
annotations: {
destructiveHint: false, // O método faz algo crítico — precisa de confirmação
openWorldHint: false, // O método altera algo em serviços de terceiros
readOnlyHint: true // O método não altera nada
},
}
Função que faz algo importante
async (input, extra) => {
// 1. Validação dos parâmetros
// 2. Fazer algo importante
return {
content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Descrição do resultado para a IA
structuredContent: { // Importante! Este é o JSON do resultado.
timestamp: new Date().toISOString() // Pode conter quaisquer dados.
},
_meta: this.widgetMeta(widget), // Metadados do widget que exibe o JSON
}; // Pode não estar presente — então não haverá widget
}
private widgetMeta(widget: ContentWidget)
Retorna os metadados do widget — com base neles o ChatGPT determina qual widget usar para exibir o resultado JSON.
{
"openai/outputTemplate": widget.templateUri, // URI do widget
"openai/toolInvocation/invoking": widget.invoking, // Texto acima do widget enquanto carrega
"openai/toolInvocation/invoked": widget.invoked, // Texto acima do widget quando já carregou
"openai/widgetAccessible": true, // MCP‑tool pode ser chamado a partir do widget
"openai/resultCanProduceWidget": true, // O MCP‑tool retornará um widget
}
Vale comentar à parte algo simples como "openai/outputTemplate". No protocolo MCP há 3 entidades (sobre as quais você saberá mais no módulo 6):
- MCP Resources
- MCP Templates
- MCP Tools
Pois bem, este "openai/outputTemplate" não tem qualquer relação com MCP Templates. MCP Templates não são usados em ChatGPT Apps. A palavra template aqui vem do seguinte:
Os widgets foram concebidos como um template para exibir JSON. O MCP‑tool retorna um certo JSON, a IA exibe o widget, passa o JSON para ele via o parâmetro ToolOutput, e o widget exibe esse JSON de forma bonita. outputTemplate — é simplesmente um sinônimo de widget.
Acho que por ora é isso. Vamos detalhar essas coisas no módulo 4: como descrever ferramentas, JSON Schema e handlers. Agora basta entender: se algo estiver relacionado a ferramentas (tools) e lógica — procure perto de app/mcp/route.ts.
6. Configuração e “cola”: next.config.ts, middleware.ts, .env e afins
Agora vamos ao conjunto principal de arquivos necessários para que seu projeto Next.js funcione corretamente dentro do iframe do ChatGPT e fique acessível ao ChatGPT através de um túnel HTTPS (ngrok, Cloudflare Tunnel etc.; falaremos de túneis separadamente).
next.config.ts
Neste arquivo, além das configurações padrão do Next.js, geralmente se configura:
- assetPrefix — para que os estáticos (JS, CSS de /_next/) sejam carregados corretamente não do domínio do ChatGPT, mas do seu URL de desenvolvimento (túnel ou Vercel);
- quaisquer configurações específicas necessárias ao template (por exemplo, flags experimentais do Next 16).
Na prática, isso se parece com uma exportação normal de nextConfig com os campos necessários. Para a aula, importa uma coisa: se no ChatGPT o widget não consegue carregar CSS/JS, muitas vezes o culpado é justamente o assetPrefix.
proxy.ts (antigo middleware.ts)
Este arquivo insere uma camada de middleware entre a requisição do ChatGPT e suas rotas. No template ele geralmente:
- define cabeçalhos CORS, para que o iframe do ChatGPT tenha permissão de acessar seu servidor;
- às vezes ajusta cabeçalhos adicionais para React Server Components.
Não é necessário conhecer todas as minúcias agora. É útil apenas lembrar: se o ChatGPT reclama de CORS ou você vê erros estranhos no DevTools sobre proibição de acesso, dê uma olhada no proxy.ts.
.env
O arquivo .env (ou .env.local) — é o lugar para segredos e parâmetros de ambiente:
- OPENAI_API_KEY (se o servidor MCP for acessar a OpenAI API),
- endereços das suas APIs internas,
- tokens de serviços de terceiros etc.
Há um ponto importante: no Next.js, variáveis que começam com NEXT_PUBLIC_ entram automaticamente no bundle JS e ficam acessíveis no navegador. Nunca faça isso com OPENAI_API_KEY; segredos devem ser apenas variáveis do lado do servidor.
package.json e tsconfig.json
No package.json você vai ver:
- versões do Next.js, React, Apps SDK, MCP SDK e outras dependências;
- os scripts dev, build, start e às vezes comandos auxiliares (linter, formatter etc.).
No tsconfig.json ficam as configurações habituais do TypeScript:
- caminhos de aliases (@/lib, @/components),
- modo strict,
- targets de compilação.
Do ponto de vista deste curso, o principal é entender que o template usa uma stack TypeScript comum, e você pode expandi‑la de forma padrão.
7. “Navegador rápido do projeto” para desenvolvedores
Vamos fixar aonde ir quando você quer fazer coisas típicas. Sem listas, apenas em mini‑cenários.
Se você quer mudar textos/botões no widget, abra o arquivo da UI do widget: é app/widget/page.tsx ou app/page.tsx — depende do template. Lá você ajusta o JSX, adiciona novos componentes, conecta o design system. E é exatamente ali que você vai usar o runtime do Apps SDK (window.openai ou hooks convenientes) para exibir dados.
Se precisa adicionar um novo botão que faz algo no servidor, você começa ainda assim pelo arquivo da UI. O botão no widget, ao clicar, vai chamar window.openai.callTool, e a implementação dessa ferramenta você adiciona na configuração do servidor MCP, isto é, no código perto de app/mcp/route.ts. A ligação UI ↔ lógica da tool vamos explorar nos módulos 4 em diante.
Quando você quer ensinar um novo recurso ao ChatGPT (por exemplo, “buscar pacotes de viagem” ou “selecionar produtos”), você vai à camada MCP (arquivos importados de app/mcp/route.ts). Lá registra uma nova tool com JSON Schema, descrição e handler. O widget depois pode ler o resultado via window.openai.toolOutput e exibi‑lo de forma agradável.
Se a sua estática quebrou ou o widget aparece estranho apenas no ChatGPT, mas localmente está tudo certo, lembramos da camada de cola. Primeiro verifique o next.config.ts (especialmente assetPrefix) e o middleware.ts/proxy.ts (CORS). Se você trocou recentemente o túnel, a URL ou fez deploy na Vercel, a correção dessas configurações é crítica.
Por fim, se você suspeita de problemas com chaves ou ambiente, seu trio de arquivos é — .env.local, package.json (para confirmar quais dependências e scripts realmente são usados) e os logs do servidor de desenvolvimento. É este conjunto que garante que o MCP tenha acesso aos segredos e serviços necessários.
8. Mini prática: conhecendo o sistema de arquivos na prática
Teoria é teoria, mas vamos fixar com as mãos onde fica cada coisa. Você pode fazer estes passos agora no editor/IDE.
Tente abrir no seu projeto a pasta app e encontrar qual arquivo é responsável pelo widget. Se o template usa app/page.tsx, é lá que você verá uma mensagem conhecida como “HelloWorld — ChatGPT App” ou um texto de boas‑vindas. Se não houver um widget como pasta separada, abra app/page.tsx e confira se lá existe 'use client' e alguma marcação JSX.
Depois, encontre app/mcp/route.ts. Observe quais módulos ele importa: normalmente você verá ou o uso direto do MCP SDK, ou a chamada de uma função auxiliar a partir de lib/mcp/*. Avalie o quão “fina” é essa camada — o ideal é que haja quase nenhuma lógica de negócios ali, apenas “recebeu JSON → repassou ao servidor → devolveu JSON”.
Em seguida, dê uma olhada em next.config.ts e proxy.ts/middleware.ts. Não precisa entender tudo o que está escrito, apenas fixe que:
- next.config.ts é responsável pela configuração do Next, incluindo as regras de build e de entrega de assets;
- proxy.ts interfere nas requisições HTTP (quase certamente você verá ali manipulação de cabeçalhos).
E por último abra o .env ou .env.local e verifique se suas chaves estão lá, e não no código. Se em algum lugar você enxergar NEXT_PUBLIC_OPENAI_API_KEY — é um ótimo motivo para corrigir enquanto ainda estamos apenas no desenvolvimento local.
9. Esquema visual: como o ChatGPT interage com seu template
Para fechar o quadro, vale olhar um fluxo simples:
flowchart TD
U[Usuário no ChatGPT] -->|Escreve uma solicitação| M[Modelo do ChatGPT]
M -->|Chama uma tool| MCP["Seu endpoint MCP
app/mcp/route.ts"]
MCP -->|"Resposta JSON do MCP (structuredContent, _meta, link de UI)"| M
M -->|Decide mostrar a UI| WIDGET_URL["URL do widget
(/widget ou /)"]
WIDGET_URL -->|iframe| W[Seu widget
app/page.tsx]
W -->|lê window.openai.toolOutput
+ widgetState| U
Aqui é importante notar que o iniciador quase sempre é o modelo do ChatGPT, e não o navegador do usuário, como em um aplicativo web clássico. Seu app/mcp/route.ts e app/widget/page.tsx — são apenas duas “portas” diferentes para o mesmo projeto Next.js: uma para o robô (MCP), outra para a UI.
Mantendo em mente este mapa do projeto (widget → camada MCP → configs) e evitando conscientemente as armadilhas citadas, mais adiante no curso você poderá focar na lógica e no UX do seu App, e não em procurar “aquele arquivo que quebra tudo”.
10. Erros típicos ao trabalhar com a estrutura do template
Erro nº 1: Confundir o widget com uma página comum do site.
Às vezes o desenvolvedor vê no template tanto app/page.tsx quanto app/widget/page.tsx, edita “o arquivo errado” e se surpreende por que as mudanças não aparecem no ChatGPT. O widget — é exatamente a página usada como outputTemplate/iframe para o instrumento MCP. Se você altera outra rota, o ChatGPT nem fica sabendo. Sempre confira o README do template e veja qual URL está indicado como widget.
Erro nº 2: Escrever código de cliente (window, document) em arquivos de servidor do MCP.
O arquivo app/mcp/route.ts e tudo o que ele importa é executado no servidor. Qualquer tentativa de usar window ou a DOM API ali resultará em runtime quebrado. Se você quer fazer algo na UI, quase certamente isso deve estar nos arquivos sob app/widget ou em outros componentes cliente. A camada MCP — é backend puro: requisições, bancos, APIs externas e a formação de uma resposta estruturada.
Erro nº 3: Ignorar assetPrefix e configurações de CORS.
No localhost:3000 local tudo funciona bem, mas basta abrir o App via túnel no ChatGPT — e os estilos somem, o JS não carrega, o console fica cheio de erros de CORS. Frequentemente a causa é que a configuração do next.config.ts ou do middleware.ts/proxy.ts não considera a nova URL pública ou foi quebrada por engano durante um refactor. Ao alterar esses arquivos, tenha sempre em mente que seu código vai viver dentro de um iframe no domínio do ChatGPT, e não diretamente no localhost.
Erro nº 4: Guardar segredos fora do .env, no código ou em variáveis NEXT_PUBLIC_*.
Esconder OPENAI_API_KEY em const apiKey = 'sk-...' em algum lugar de app/widget/page.tsx — é a pior das ideias: a chave vai parar no bundle JS e chegará a qualquer usuário. Quase tão ruim — é fazer a variável NEXT_PUBLIC_OPENAI_API_KEY, porque o prefixo NEXT_PUBLIC_ garante que ela irá para o navegador. Sempre coloque segredos em .env sem esse prefixo e use‑os apenas no lado do servidor (servidor MCP, funções de backend).
Erro nº 5: Considerar o template “inteligente demais” e ter medo de mexer.
Às vezes desenvolvedores tratam o starter oficial como algo sagrado: “melhor não mexer, vai que eu quebro a integração”. Como resultado, escrevem todo o código em outro lugar, complicam a arquitetura e ainda assim pisam nas mesmas armadilhas. Na verdade, o template — não passa de um projeto Next.js bem organizado com alguns ajustes para o Apps SDK. Entender que app/ — é UI e MCP, enquanto o resto são configs comuns, libera você: você passa a trabalhar com o código como em um projeto React/Next habitual, e não como em uma caixa mágica.
Erro nº 6: Tentar resolver todos os problemas “no nível do widget”.
Às vezes dá vontade de fazer tudo na UI: lógica de negócios, acesso a bancos, requisições a APIs externas. No contexto de ChatGPT Apps isso é especialmente uma má ideia: o widget vive em uma sandbox bem rígida, não vê seus segredos e depende bastante de window.openai. Se é algo sério — o lugar disso é na camada MCP e nos serviços de backend, enquanto o widget deve ser uma camada de apresentação fina, que exibe dados estruturados e, quando necessário, aciona ferramentas.
GO TO FULL VERSION