CodeGym /Cursos /ChatGPT Apps /Cómo funciona la plantilla: estructura del proyecto y arc...

Cómo funciona la plantilla: estructura del proyecto y archivos clave

ChatGPT Apps
Nivel 2 , Lección 1
Disponible

1. Introducción

El proyecto ChatGPT App HelloWorld no es «una caja negra mágica de CodeGym en la que es mejor no tocar nada». Es un proyecto de Next.js común y corriente, solo que en él conviven a la vez:

  • el frontend, que se renderiza dentro de ChatGPT,
  • un servidor MCP, que responde a las invocaciones de herramientas (tools),
  • y los ajustes que lo conectan todo con ChatGPT.

Si no sabes dónde está cada cosa, suelen darse tres escenarios clásicos:

  1. El desarrollador escribe por accidente window en un archivo del servidor, provoca un fallo y empieza a odiar todo el stack.
  2. Intenta añadir un botón en el UI, pero edita el page.tsx equivocado (por ejemplo, la raíz de la aplicación y no el widget) y no ve cambios en ChatGPT.
  3. Mete por accidente la OPENAI_API_KEY en la parte cliente, y la clave acaba filtrándose al navegador.

Por eso, el objetivo de hoy es trazar el mapa: dónde está el UI, dónde MCP, dónde los configs y a dónde acudir cuando quieras:

  • cambiar la apariencia del widget;
  • añadir una nueva tool;
  • ajustar alguna configuración de plataforma (CORS, assetPrefix, etc.).

2. Anatomía de alto nivel del proyecto

El proyecto Next.js ChatGPT App HelloWorld usa el App Router y está organizado alrededor de la carpeta app/. En ella conviven en un mismo árbol de páginas:

  • el UI del widget, que se renderizará dentro de ChatGPT,
  • el endpoint MCP, que procesará las invocaciones de tools.

Árbol típico (simplificado; los nombres de carpetas pueden variar en tu plantilla, pero el patrón es el mismo):

my-chatgpt-app/
├─ app/
│  ├─ api/                          // REST API
│  │  └─ time/                      // GET /api/time devuelve la hora en el servidor
│  │     └─ route.ts
│  ├─ hooks/                        // Conjunto de hooks del Apps SDK oficial
│  │  ├─ use-call-tool.ts
│  │  ├─ use-display-mode.ts
│  │  └─ use-open-external.ts
│  ├─ mcp/                          // Servidor MCP: aquí llama ChatGPT cuando invoca tools
│  │  └─ route.ts
│  ├─ globals.css                   // globals.css raíz de toda la aplicación
│  ├─ layout.tsx                    // layout raíz de toda la aplicación
│  └─ page.tsx                      // Página del widget dentro de ChatGPT
├─ public/                          // Estáticos: iconos, manifiesto, etc.
├─ next.config.ts                   // Config de Next.js y ajustes específicos de Apps (assetPrefix, etc.)
├─ proxy.ts                         // CORS/encabezados para funcionar dentro de un iframe (antiguo middleware.ts)
├─ package.json                     // Dependencias del proyecto
├─ tsconfig.json                    // Configuración de TypeScript
└─ .env.local                       // Secretos: OPENAI_API_KEY y otros

Si hay varios widgets, normalmente no se colocan en app/page.tsx, sino en app/widget/page.tsx. Pero la lógica no cambia: sigue habiendo una página‑widget y un endpoint que hace de servidor MCP.

Es útil pensarlo así: tu repositorio es un «Jano bifronte»:

  • una «cara» — la ruta /mcp, adonde va ChatGPT cuando quiere invocar una herramienta;
  • la otra «cara» — la ruta /widget (o /), que se carga en un iframe cuando el modelo decide mostrar tu UI.

Para no confundirse, fijemos en la cabeza tres grupos de archivos:

  1. Capa de UI — todo lo relacionado con las páginas de React/Next (app/widget, componentes, estilos).
  2. Capa MCPapp/mcp/route.ts y los archivos que utiliza.
  3. Capa de pegamento y configsnext.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.

Un poco más abajo repasaremos cada una de estas capas.

3. Dónde vive el widget: carpeta app/widget y/o app/page.tsx

Empecemos por lo que tocarás más a menudo — el widget, es decir, el UI que será visible dentro de ChatGPT.

En la mayoría de proyectos actuales existe o bien:

  • la ruta app/widget/page.tsx — el widget vive bajo el prefijo /widget,
  • o bien la raíz app/page.tsx — el widget coincide con la página raíz.

Principales señales de que un archivo es el del widget:

  • en la primera línea aparece 'use client', porque el componente funciona en el navegador, se comunica con window y con el Apps SDK;
  • es un componente React normal que renderiza marcado y (un poco más adelante en el curso) se comunica con window.openai.

El ejemplo más simple de un widget didáctico (puede que ya veas algo muy parecido en tu proyecto):

// 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">
        Aquí construiremos la UI de nuestro widget.
      </p>
    </main>
  );
}

Si en tu plantilla el widget está directamente en app/page.tsx, el código será prácticamente el mismo, solo que sin la carpeta intermedia widget.

Atención a varios puntos.

Primero, la directiva 'use client' es obligatoria: el widget lee/escribe en window.openai, escucha eventos, etc., y eso solo es posible en un componente de cliente. Si la quitas, Next intentará hacer la página de servidor y obtendrás errores del tipo “window is not defined”.

Segundo, es un componente React de toda la vida, nada de magia. Puedes:

  • dividirlo en subcomponentes en components/,
  • usar Tailwind o cualquier otro sistema CSS,
  • conectar contextos, hooks, etc.

Tercero, más adelante aquí es donde:

  • leerás window.openai.toolInput y window.openai.toolOutput, para renderizar datos reales,
  • guardarás el widgetState mediante window.openai.setWidgetState,
  • invocarás openExternal, callTool y otros métodos del runtime.

Por ahora basta con saber: si quieres cambiar la interfaz visual — casi seguro que es en app/widget/page.tsx o app/page.tsx.

4. Layout raíz: app/layout.tsx como «marco» de toda la aplicación

El siguiente archivo importante es app/layout.tsx. Este archivo:

  • define la estructura HTML (<html>, <body>),
  • incluye estilos globales (globals.css),
  • a menudo inicializa el «bootstrap» para el Apps SDK (un envoltorio que escucha window.openai y pasa datos a React).

Ejemplo 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>
  );
}

El nombre NextChatSDKBootstrap aquí es ilustrativo; en tu plantilla puede ser OpenAIAppProvider u otro componente. Suele tener una tarea: configurar el vínculo entre el árbol de React y el runtime del Apps SDK, suscribirse a datos globales (theme, displayMode, toolInput, etc.) y repartirlos a los hijos.

Conclusión práctica importante: si necesitas conectar un contexto global, estilos o una librería de UI (por ejemplo, shadcn/ui), el sitio casi siempre es app/layout.tsx (o el layout dentro de app/widget para ajustes y componentes específicos del widget).

Análisis de NextChatSDKBootstrap

NextChatSDKBootstrap lo vi en la plantilla oficial de Vercel. Si no lo sabías, son precisamente quienes crearon y desarrollan Next. En su web tienen un buen artículo sobre ChatGPT App con Next. Y también tienen un Starter Template. Aunque en un par de puntos ya está un poco desactualizado, diría que hay muchas posibilidades de que mantengan su vigencia.

Resaltemos 5 cosas clave que nos da NextChatSDKBootstrap:

  • 1. Corrige problemas de hidratación
    El caso es que ChatGPT primero carga el HTML de tu widget en su servidor, lo limpia y lo parchea. Como resultado, el mecanismo de hidratación se queja y lanza warnings en la consola. Eso puede impedir que pases el review.
  • 2. Parchea el historial del navegador
    Tu widget se carga en un iframe desde un dominio especial de ChatGPT. Y si usas tu propio dominio, romperás la sandbox. Por eso en el historial del navegador solo se guarda la ruta sin el dominio.
  • 3. Reescribe la función fetch()
    Todas tus llamadas a fetch() a direcciones relativas sin dominio no funcionarán en el widget, ya que el dominio del iframe es distinto. Así que sustituimos fetch() por una propia que envía las solicitudes sin dominio a la URL correcta. Si el dominio está indicado, todo funciona sin cambios.
  • 4. Los clics en enlaces funcionan
    Si los enlaces se abren dentro del iframe, a ChatGPT no le gustará. Por ello se añadió código que detecta los clics en enlaces y los abre en una ventana externa mediante openExternal().
  • 5. Establecer head base (DEPRECATED)
    Este código también añadía <base> en el <head>, pero ya no funciona. La sandbox reinicia cualquier base establecido, así que recomiendo usar enlaces absolutos para todo: scripts, recursos, fuentes, API, etc.

5. Servidor MCP: app/mcp/route.ts

Pasamos ahora a la segunda mitad del «Jano bifronte» — el servidor que habla con ChatGPT vía MCP.

El archivo app/mcp/route.ts es un Route Handler normal del App Router, que:

  • recibe solicitudes HTTP de ChatGPT (normalmente POST con payload JSON en formato MCP),
  • las pasa al servidor MCP (basado en @modelcontextprotocol/sdk o en un envoltorio ligero),
  • y devuelve de vuelta una respuesta JSON en formato MCP.

Hay dos opciones: puedes escribir con el MCP SDK “a pelo”, o puedes intentar suavizar aristas usando varias clases de Next/Vercel.

He aquí una variante con el MCP SDK en TS puro:

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

// 1. Creamos el servidor MCP
const server = new McpServer({
  name: "simple-mcp-server",
  version: "1.0.0",
});

// 2. Registramos los MCP Resources
// 3. Registramos los MCP Tools

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

// 5. Arranque del servidor
await server.connect(transport);

Pero es mejor usar algunas clases ya listas para trabajar más cómodo:

// 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;

Aquí McpGateway es una clase envoltorio sobre McpServer, que creas en algún sitio (por ejemplo, en lib/mcp/server.ts) con ayuda del SDK. En nuestro caso todo cabe en app/mcp/route.ts. Vamos a desglosar por completo qué hay en este archivo.

type ContentWidget

Al principio del archivo describimos el tipo ContentWidget. Contiene todos los datos del widget y se usa en dos lugares: al registrar el widget como mcp-resource y cuando la mcp-tool devuelve metadata, donde indica qué widget usar para mostrar los datos que devolvió.

type ContentWidget = {
  id: string;            // Nombre/key único
  title: string;         // Title
  description: string;   // Description
  templateUri: string;   // URI único del widget; puede ser cualquiera. No afecta a nada.
  invoking: string;      // Texto sobre el widget mientras se está cargando
  invoked: string;       // Texto sobre el widget cuando ya ha cargado
  html: string;          // Todo el código HTML del widget.
  widgetDomain: string;  // «Dominio» del widget. No afecta a nada.
};

class McpGateway

Clase envoltorio sobre McpServer, simplifica algunas cosas. Contiene 6 métodos:

  • initialize() — aquí cargamos el HTML de nuestro widget
  • registerResources() — registramos widgets como mcp-resources
  • registerTools() — registramos funciones como mcp-tools
  • widgetMeta() — devuelve metadatos del widget
  • getAppsSdkCompatibleHtml() — carga el HTML del widget y lo parchea un poco
  • makeImgUrlsAbsolute() — parchea el HTML: convierte enlaces de imágenes a absolutos

Veámoslos con más detalle:

public async initialize()

Este método descarga de Internet el código HTML de los widgets y rellena un objeto del tipo ContentWidget.

{
  id: "hello_world",                         // Key único del widget
  templateUri: "ui://widget/hello_world.html", // URI único del widget. «ui:» no significa nada.
  title: "HelloWorld Widget",               // Nombre del widget
  description: "Displays the HelloWorld widget", // Explicación para la LLM de lo que hace el widget
  invoking: "Loading widget...",            // Texto sobre el widget mientras se está cargando
  invoked: "Widget loaded",                 // Texto sobre el widget después de cargarse
  html: htmlWidget,                         // HTML del widget
  widgetDomain: baseURL,                    // «Dominio» del widget. Actualmente no afecta a nada.
}

public registerResources()

Registra los widgets como mcp-resources. Llama al método server.registerResource(), al que se le pasan 4 parámetros:

  • id/key del recurso MCP
  • URI del recurso (lo necesita el protocolo MCP; para el widget es prácticamente un sinónimo de su dirección única)
  • Metadatos del recurso MCP
  • Función que devuelve el recurso MCP

Metadatos del widget

{
  title: widget.title,                 // Nombre del recurso/widget
  description: widget.description,     // Descripción del recurso/widget
  mimeType: "text/html+skybridge",     // ¡Importante! Solo este HTML se mostrará como widget
  _meta: {
    "openai/widgetDescription": widget.description, // Descripción del widget
    "openai/widgetPrefersBorder": true,            // Le pedimos a ChatGPT dibujar un borde al widget
  },
}

Widget como recurso MCP

{
  uri: uri.href,                        // Nuestro URI (tomado del parámetro uri)
  mimeType: "text/html+skybridge",      // ¡Importante! Solo este HTML se mostrará como widget
  text: widget.html,                    // HTML del widget
  _meta: {
    "openai/widgetDescription": widget.description, // Descripción del widget
    "openai/widgetPrefersBorder": true,            // Le pedimos a ChatGPT dibujar un borde al widget
    "openai/widgetDomain": widget.widgetDomain,    // «Dominio» del widget. Actualmente no afecta a nada.
    "openai/widgetCSP": {                          // ¡Importante! Dominios disponibles para el widget:
      connect_domains: [                           // Dominios para conexiones (fetch, etc.)
        baseURL,
        "https://codegym.cc",
      ],
      resource_domains: [                          // Dominios 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"
      ]
    }
  },
}

En el futuro volveremos a tocar openai/widgetCSP, pero ahora me gustaría resaltar 2 puntos:

  • connect_domains — lista de dominios para:
    • fetch()
    • carga de scripts
    • openExternal()
  • resource_domains — lista de dominios para:
    • imágenes
    • CSS
    • fuentes

Teóricamente puedes escribir 200 dominios, pero si con esa lista lograrás pasar el review… es otra historia.

También estudié estos parámetros en apps ya publicadas y encontré amplitude.com. Buena noticia también. Creo que una buena analítica no le viene mal a nadie.

public registerTools()

Registra funciones como mcp-tools. Llama al método server.registerTool(), al que se le pasan 3 parámetros:

  • id/key de la MCP‑tool
  • Metadatos de la MCP‑tool
  • Función que implementa la MCP‑tool

Metadatos de la herramienta

Todos los parámetros de esta lista son importantes. Hablaré de ellos con más detalle en siguientes lecciones.

{
  title: widget.title,                               // Nombre de la herramienta
  description: "Returns HelloWorld widget",          // ¡Importante! Descripción de lo que hace la herramienta
  inputSchema: z.object({}).describe("No inputs"),   // Esquema de parámetros de la herramienta. Puedes usar Zod
  _meta: this.widgetMeta(widget),                    // Metadatos del widget: qué widget mostrar
  annotations: {
    destructiveHint: false,                          // El método hace algo crítico - requiere confirmación
    openWorldHint: false,                            // El método cambia algo en servicios de terceros
    readOnlyHint: true                               // El método no modifica nada
  },
}

Función que hace algo importante

async (input, extra) => {
  // 1. Validación de parámetros
  // 2. Hacemos algo importante
  return {
    content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Descripción del resultado para la IA
    structuredContent: {                                      // ¡Importante! Este es el JSON del resultado.
      timestamp: new Date().toISOString()                     // Puede contener cualquier dato.
    },
    _meta: this.widgetMeta(widget),                           // Metadatos del widget que muestra el JSON
  };                                                          // Puede faltar; entonces no habrá widget
}

private widgetMeta(widget: ContentWidget)

Devuelve los metadatos del widget — con ellos ChatGPT determinará qué widget usar para mostrar el resultado JSON.

{
  "openai/outputTemplate": widget.templateUri,            // URI del widget
  "openai/toolInvocation/invoking": widget.invoking,      // Texto sobre el widget mientras se carga
  "openai/toolInvocation/invoked": widget.invoked,        // Texto sobre el widget cuando ya se cargó
  "openai/widgetAccessible": true,                        // La MCP-tool se puede invocar desde el widget
  "openai/resultCanProduceWidget": true,                  // La MCP-tool devolverá un widget
}

Me gustaría comentar aparte algo sencillo como "openai/outputTemplate". En el protocolo MCP hay 3 entidades (sobre las que sabrás más en el módulo 6):

  • MCP Resources
  • MCP Templates
  • MCP Tools

Pues bien, este "openai/outputTemplate" no tiene ninguna relación con MCP Templates. MCP Templates no se usan en absoluto en ChatGPT Apps. La palabra template viene de aquí:

Los widgets se concibieron como una plantilla para mostrar JSON. La MCP‑tool devuelve cierto JSON, la IA muestra un widget, le pasa el JSON a través del parámetro ToolOutput, y el widget lo muestra de forma bonita. outputTemplate es simplemente un sinónimo de widget.

Creo que con esto basta. Veremos estos temas con más detalle en el módulo 4: cómo describir herramientas, JSON Schema y manejadores. Por ahora es suficiente con entender: si algo está relacionado con herramientas (tools) y lógica — búscalo cerca de app/mcp/route.ts.

6. Configuración y «pegamento»: next.config.ts, middleware.ts, .env y compañía

Ahora veamos el conjunto principal de archivos necesarios para que tu proyecto Next.js funcione correctamente dentro de un iframe de ChatGPT y sea accesible por ChatGPT mediante un túnel HTTPS (ngrok, Cloudflare Tunnel, etc.; de los túneles hablaremos aparte).

next.config.ts

En este archivo, además de la configuración estándar de Next.js, a menudo se ajusta:

  • assetPrefix — para que los estáticos (JS, CSS de /_next/) se carguen correctamente no desde el dominio de ChatGPT, sino desde tu URL de desarrollo (túnel o Vercel);
  • cualquier ajuste específico que necesite la plantilla (por ejemplo, flags experimentales de Next 16).

En la práctica esto se ve como una exportación normal de nextConfig con los campos necesarios. Para esta lección nos importa una cosa: si en ChatGPT el widget no puede cargar CSS/JS, muy a menudo el culpable es assetPrefix.

proxy.ts (antes middleware.ts)

Este archivo inserta una capa de middleware entre la solicitud desde ChatGPT y tus rutas. En la plantilla normalmente:

  • establece encabezados CORS para que el iframe de ChatGPT tenga permiso para hablar con tu servidor;
  • a veces configura encabezados adicionales para React Server Components.

No hace falta conocer todas las sutilezas ahora. Es útil solo recordar: si ChatGPT se queja de CORS o ves errores raros en DevTools sobre accesos denegados, echa un vistazo a proxy.ts.

.env

El archivo .env (o .env.local) es el lugar para secretos y parámetros de entorno:

  • OPENAI_API_KEY (si el servidor MCP habla por sí mismo con OpenAI API),
  • direcciones de tus APIs internas,
  • tokens de servicios de terceros, etc.

Hay un matiz importante: en Next.js, las variables que empiezan por NEXT_PUBLIC_ pasan automáticamente al bundle de JS y quedan disponibles en el navegador. Nunca hagas esto con OPENAI_API_KEY; los secretos deben ser solo variables del servidor.

package.json y tsconfig.json

En package.json verás:

  • versiones de Next.js, React, Apps SDK, MCP SDK y demás dependencias;
  • scripts dev, build, start, y a veces comandos auxiliares (linter, formatter, etc.).

En tsconfig.json están los ajustes habituales de TypeScript:

  • rutas de alias (@/lib, @/components),
  • modo estricto,
  • targets de compilación.

Desde el punto de vista de este curso, lo principal es entender que la plantilla usa un stack TypeScript normal y puedes ampliarlo de la forma estándar.

7. «Navegador rápido del proyecto» para desarrolladores

Fijemos a dónde ir cuando quieras hacer cosas típicas. Sin listas, solo como mini‑escenarios.

Si quieres cambiar texto/botones en el widget, abres el archivo del UI del widget: será app/widget/page.tsx o app/page.tsx, según la plantilla. Ahí editas JSX, añades nuevos componentes y conectas un sistema de diseño. Y precisamente aquí usarás el runtime del Apps SDK (window.openai o hooks cómodos) para mostrar datos.

Si necesitas añadir un nuevo botón que haga algo en el servidor, de todas formas empiezas por el archivo de UI. El botón en el widget, al hacer clic, invocará window.openai.callTool, y la implementación de esa herramienta la añadirás en la configuración del servidor MCP, es decir, en el código junto a app/mcp/route.ts. La conexión UI ↔ lógica de tool la veremos en los módulos 4 y siguientes.

Cuando quieras enseñar a ChatGPT una nueva funcionalidad (por ejemplo, «buscar viajes» o «sugerir productos»), vas a la capa MCP (archivos importados desde app/mcp/route.ts). Allí registras una nueva tool con JSON Schema, descripción y manejador. El widget luego podrá leer el resultado mediante window.openai.toolOutput y mostrarlo con buena apariencia.

Si se ha roto la estática o el widget se muestra raro solo en ChatGPT, pero en local todo va bien, recuerda la capa de pegamento. Revisa primero next.config.ts (especialmente assetPrefix) y middleware.ts/proxy.ts (CORS). Si cambiaste recientemente de túnel, URL o desplegaste en Vercel, la corrección de esos ajustes es crítica.

Por último, si sospechas problemas con claves o entorno, tu trío de archivos es — .env.local, package.json (para asegurarte de qué dependencias y scripts se usan realmente) y los logs del servidor de desarrollo. Esta combinación es la responsable de que MCP tenga acceso a los secretos y servicios necesarios.

8. Mini práctica: conoce el sistema de archivos con tus manos

La teoría está bien, pero fijemos con las manos dónde está cada cosa. Puedes hacer estos pasos ahora mismo en tu editor/IDE.

Intenta abrir en tu proyecto la carpeta app y encontrar qué archivo responde por el widget. Si la plantilla usa app/page.tsx, ahí verás algún texto conocido como «HelloWorld — ChatGPT App» o un texto de bienvenida. Si no existe el widget como carpeta aparte, abre app/page.tsx y asegúrate de que contiene 'use client' y algo de marcado JSX.

Luego busca app/mcp/route.ts. Fíjate en qué módulos importa: normalmente verás o el uso directo del MCP SDK, o la llamada a una función auxiliar desde lib/mcp/*. Valora cuán «fina» es esa capa — idealmente casi no habrá lógica de negocio, solo «recibir JSON → pasar al servidor → devolver JSON».

Después mira next.config.ts y proxy.ts/middleware.ts. No hace falta entender todo lo que está escrito; solo fija que:

  • next.config.ts responde por la configuración de Next, incluida la compilación y la entrega de assets;
  • proxy.ts interviene en las solicitudes HTTP (casi seguro verás trabajo con encabezados).

Y por último abre .env o .env.local y verifica que tus claves estén ahí y no en el código. Si ves en algún sitio NEXT_PUBLIC_OPENAI_API_KEY — es un buen motivo para corregirlo mientras aún estás en desarrollo local.

9. Esquema visual: cómo interactúa ChatGPT con tu plantilla

Para completar la imagen, es útil mirar un flujo sencillo:

flowchart TD
    U[Usuario en ChatGPT] -->|Escribe una petición| M[Modelo de ChatGPT]

    M -->|Invoca una herramienta| MCP["Tu endpoint MCP
app/mcp/route.ts"] MCP -->|"Respuesta JSON de MCP (structuredContent, _meta, enlace al UI)"| M M -->|Decide mostrar el UI| WIDGET_URL["URL del widget
(/widget o /)"] WIDGET_URL -->|iframe| W[Tu widget
app/page.tsx] W -->|lee window.openai.toolOutput
+ widgetState| U

Es importante notar que el iniciador casi siempre es el modelo de ChatGPT, y no el navegador del usuario, como en una aplicación web clásica. Tu app/mcp/route.ts y app/widget/page.tsx — son simplemente dos «puertas» distintas al mismo proyecto Next.js: una para el robot (MCP), otra para el UI.

Si mantienes en mente este mapa del proyecto (widget → capa MCP → configs) y evitas conscientemente las trampas mencionadas, más adelante en el curso podrás centrarte en la lógica y el UX de tu App, y no en buscar «ese archivo que lo rompe todo».

10. Errores típicos al trabajar con la estructura de la plantilla

Error n.º 1: confundir el widget con una página normal del sitio.
A veces el desarrollador ve en la plantilla tanto app/page.tsx como app/widget/page.tsx, edita «el archivo que no es» y se sorprende de que los cambios no aparezcan en ChatGPT. El widget es precisamente la página que se usa como outputTemplate/iframe para la herramienta MCP. Si cambias otra ruta, ChatGPT ni se enterará. Revisa siempre el README de la plantilla y mira qué URL está indicada como widget.

Error n.º 2: escribir código de cliente (window, document) en archivos de servidor MCP.
El archivo app/mcp/route.ts y todo lo que importa se ejecuta en el servidor. Cualquier intento de usar ahí window o el DOM‑API provocará que el runtime se caiga. Si quieres hacer algo en el UI, casi seguro que debe estar en archivos bajo app/widget u otros componentes de cliente. La capa MCP es backend puro: solicitudes, bases, APIs externas y formación de la respuesta estructurada.

Error n.º 3: ignorar assetPrefix y la configuración de CORS.
En localhost:3000 todo funciona de maravilla, pero abres la App a través de un túnel en ChatGPT — y desaparecen los estilos, JS no carga y la consola se llena de errores de CORS. A menudo la causa es que la configuración de next.config.ts o middleware.ts/proxy.ts no tiene en cuenta la nueva URL pública o se rompió sin querer al refactorizar. Al cambiar estos archivos, recuerda que tu código vivirá dentro de un iframe en el dominio de ChatGPT, no directamente en localhost.

Error n.º 4: guardar secretos fuera de .env, ya sea en el código o en variables NEXT_PUBLIC_*.
Esconder OPENAI_API_KEY en const apiKey = 'sk-...' en algún app/widget/page.tsx es la peor de las ideas: la clave acabará en el bundle de JS y llegará a cualquier usuario. Casi igual de malo es crear la variable NEXT_PUBLIC_OPENAI_API_KEY, porque el prefijo NEXT_PUBLIC_ garantiza que llegue al navegador. Pon siempre los secretos en .env sin ese prefijo y úsalos solo en el lado servidor (servidor MCP, funciones de backend).

Error n.º 5: considerar la plantilla «demasiado lista» y temer tocarla.
A veces los desarrolladores tratan al starter oficial como algo sagrado: «mejor no tocar, no sea que rompa la integración». El resultado es que escriben todo su código aparte, complican la arquitectura y aun así pisan las mismas trampas. En realidad la plantilla es solo un proyecto Next.js bien ordenado con un par de ajustes para el Apps SDK. Entender que app/ es UI y MCP, y que el resto son configs normales, libera mucho: comienzas a trabajar el código como en cualquier proyecto React/Next, no como con una caja mágica.

Error n.º 6: intentar resolver todos los problemas «a nivel de widget».
A veces apetece hacerlo todo en el UI: la lógica de negocio, el acceso a bases y las solicitudes a APIs externas. En el contexto de ChatGPT Apps es una mala idea, especialmente: el widget vive en una sandbox muy estricta, no ve tus secretos y depende mucho de window.openai. Si necesitas algo serio — su lugar está en la capa MCP y en servicios de backend; el widget debe ser una capa de presentación fina que muestre datos estructurados y, cuando haga falta, dispare herramientas.

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