CodeGym /Corsi /ChatGPT Apps /Com'è fatto il template: struttura del progetto e file ch...

Com'è fatto il template: struttura del progetto e file chiave

ChatGPT Apps
Livello 2 , Lezione 1
Disponibile

1. Introduzione

Il progetto ChatGPT App HelloWorld non è «una scatola nera magica di CodeGym in cui è meglio non toccare nulla». È un normale progetto Next.js, solo che al suo interno convivono contemporaneamente:

  • il frontend, che viene renderizzato dentro ChatGPT,
  • il server MCP, che risponde alle chiamate degli strumenti (tools),
  • le impostazioni che mettono tutto questo insieme a ChatGPT.

Se non si capisce dove si trova cosa, di solito accadono tre scenari classici:

  1. Lo sviluppatore scrive per errore window in un file server, va in crash e inizia a odiare tutto lo stack.
  2. Prova ad aggiungere un pulsante nell'UI, ma modifica il page.tsx sbagliato (per esempio la root dell'app invece del widget) e non vede cambiamenti in ChatGPT.
  3. Mette per sbaglio OPENAI_API_KEY nella parte client e la chiave finisce nel browser.

Perciò l'obiettivo di oggi è «tracciare la mappa»: dove sta l'UI, dove l'MCP, dove i config, e dove andare quando volete:

  • cambiare l'aspetto del widget;
  • aggiungere un nuovo tool;
  • ritoccare qualche impostazione di piattaforma (CORS, assetPrefix ecc.).

2. Anatomia ad alto livello del progetto

Il progetto Next.js ChatGPT App HelloWorld usa l'App Router ed è organizzato intorno alla cartella app/. In essa convivono nello stesso albero di pagine:

  • l'UI del widget, che verrà renderizzato dentro ChatGPT,
  • l'endpoint MCP, che gestirà le chiamate agli strumenti (tool).

Albero tipico (semplificato; i nomi delle cartelle nel tuo template possono differire, ma il pattern è lo stesso):

my-chatgpt-app/
├─ app/
│  ├─ api/                          // REST API
│  │  └─ time/                      // GET /api/time restituisce l'ora sul server
│  │     └─ route.ts
│  ├─ hooks/                        // Raccolta di hook dell'Apps SDK ufficiale
│  │  ├─ use-call-tool.ts
│  │  ├─ use-display-mode.ts
│  │  └─ use-open-external.ts
│  ├─ mcp/                          // Server MCP: è qui che ChatGPT chiama quando invoca gli strumenti (tools)
│  │  └─ route.ts
│  ├─ globals.css                   // globals.css di root di tutta l'applicazione
│  ├─ layout.tsx                    // layout di root di tutta l'applicazione
│  └─ page.tsx                      // Pagina del widget dentro ChatGPT
├─ public/                          // Statici: icone, manifest, ecc.
├─ next.config.ts                   // Config di Next.js e impostazioni specifiche per Apps (assetPrefix ecc.)
├─ proxy.ts                         // CORS/header per funzionare dentro un iframe (ex middleware.ts)
├─ package.json                     // Dipendenze del progetto
├─ tsconfig.json                    // Configurazione TypeScript
└─ .env.local                       // Segreti: OPENAI_API_KEY ecc.

Se i widget sono diversi, di solito non si mettono in app/page.tsx, ma in app/widget/page.tsx. Ma la logica non cambia: c'è comunque una pagina‑widget e un endpoint che svolge il ruolo di server MCP.

È utile pensarla così: il tuo repository è un «Giano bifronte»:

  • un «volto» — il percorso /mcp, dove va ChatGPT quando vuole invocare uno strumento;
  • l'altro «volto» — il percorso /widget (oppure /), che viene caricato in un iframe quando il modello decide di mostrare la tua UI.

Per non fare confusione, fissiamo in mente tre gruppi di file:

  1. Strato UI — tutto ciò che è legato a pagine React/Next (app/widget, componenti, stili).
  2. Strato MCPapp/mcp/route.ts e i file che utilizza.
  3. Strato collante e configurazioninext.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.

Poco sotto passeremo in rassegna ciascuno di questi strati.

3. Dove vive il widget: cartella app/widget e/o app/page.tsx

Partiamo da ciò che toccherete più spesso — il widget, cioè l'UI che sarà visibile dentro ChatGPT.

Nella maggior parte dei progetti attuali c'è o:

  • la cartella app/widget/page.tsx — il widget vive sotto il prefisso separato /widget,
  • oppure il app/page.tsx di root — il widget coincide con la pagina root.

Segni distintivi principali del file del widget:

  • in alto trovi 'use client', perché il componente gira nel browser, dialoga con window e l'Apps SDK;
  • è un normale componente React che renderizza il markup e (un po' più avanti nel corso) dialoga con window.openai.

Un esempio semplicissimo di widget didattico (potreste già vedere qualcosa di molto simile nel vostro progetto):

// 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">
        Qui costruiremo l'UI del nostro widget.
      </p>
    </main>
  );
}

Se nel tuo template il widget si trova direttamente in app/page.tsx, il codice sarà pressappoco lo stesso, solo senza la cartella intermedia widget.

Fate attenzione a alcuni punti.

In primo luogo, la direttiva 'use client' è obbligatoria: il widget legge/scrive in window.openai, ascolta eventi ecc.; questo è possibile solo in un componente client. Se la togliete, Next proverà a rendere la pagina server-side e otterrete errori del tipo «window is not defined».

In secondo luogo, è un componente React normale, niente di magico. Potete:

  • suddividerlo in sotto‑componenti in components/,
  • usare Tailwind o qualsiasi altro sistema CSS,
  • collegare contesti, hook ecc.

In terzo luogo, più avanti sarà proprio qui che:

  • leggerete window.openai.toolInput e window.openai.toolOutput, per renderizzare i dati reali,
  • salverete lo widgetState tramite window.openai.setWidgetState,
  • invocherete openExternal, callTool e altri metodi del runtime.

Per ora è sufficiente sapere: se volete cambiare l'interfaccia visiva — molto probabilmente dovete andare in app/widget/page.tsx oppure app/page.tsx.

4. Layout di root: app/layout.tsx come «cornice» per l'intera applicazione

Il file successivo importante è app/layout.tsx. Esso:

  • definisce la struttura HTML (<html>, <body>),
  • collega gli stili globali (globals.css),
  • spesso inizializza il «bootstrap» per l'Apps SDK (un wrapper che ascolta window.openai e propaga i dati a React).

Esempio semplificato:

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

Il nome NextChatSDKBootstrap qui è indicativo; nel tuo template potrebbe essere OpenAIAppProvider o un altro componente. Di solito ha un solo compito: impostare la connessione tra l'albero React e il runtime dell'Apps SDK, sottoscriversi ai dati globali (theme, displayMode, toolInput ecc.) e distribuirli ai figli.

Conclusione pratica importante: se ti serve collegare un contesto globale, degli stili o una libreria UI (per esempio shadcn/ui), il posto giusto è quasi sempre app/layout.tsx (o un layout dentro app/widget per impostazioni e componenti specifici del widget).

Analisi di NextChatSDKBootstrap

NextChatSDKBootstrap l'ho visto nel template ufficiale di Vercel. Se non lo sapevi, sono proprio quelli che hanno creato e sviluppano Next. Sul loro sito c'è un ottimo post su ChatGPT App su Next. C'è anche uno Starter Template. Sebbene in alcuni punti sia già un po' datato, credo ci siano buone probabilità che ne mantengano l'attualità.

Evidenziamo 5 cose chiave che ci dà NextChatSDKBootstrap:

  • 1. Corregge i problemi di idratazione
    Il punto è che ChatGPT prima carica l'HTML del tuo widget sul proprio server, lo ripulisce e lo patcha. Di conseguenza, il meccanismo di idratazione si lamenta e riempie la console di avvisi. Questo può impedirti di passare la review.
  • 2. Patcher della cronologia del browser
    Il punto è che il tuo widget viene caricato in un iframe da un dominio speciale in ChatGPT. E se provi a usare il tuo dominio, romperai la sandbox. Perciò nella cronologia del browser si salva solo il percorso senza dominio.
  • 3. Sovrascrive la funzione fetch()
    Tutti i tuoi fetch() verso indirizzi relativi senza dominio non funzioneranno nel widget, perché il dominio dell'iframe è diverso. Per questo sostituiamo la funzione fetch() con una nostra che manda le richieste senza dominio all'URL corretto. Se il dominio è indicato, tutto funziona senza modifiche.
  • 4. I click sui link funzionano
    Se i link si aprono dentro l'iframe, ChatGPT non lo approverà. È stato quindi aggiunto del codice che intercetta i click sui link e li apre in una finestra esterna tramite openExternal().
  • 5. Impostazione di head base (DEPRECATO)
    Questo codice aggiungeva anche <base> nell'<head>, ma ora non funziona. La sandbox azzera qualsiasi base impostato, quindi raccomando di usare link assoluti per tutto: script, risorse, font, API ecc.

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

Passiamo ora alla seconda metà del «Giano bifronte» — il server che parla con ChatGPT via MCP.

Il file app/mcp/route.ts è un normale Route Handler dell'App Router che:

  • riceve richieste HTTP da ChatGPT (in genere POST con JSON‑payload in formato MCP),
  • le passa al server MCP (basato su @modelcontextprotocol/sdk o su un leggero wrapper),
  • restituisce indietro una risposta JSON in formato MCP.

Ci sono due strade: si può scrivere sul puro MCP SDK, oppure provare ad addolcire gli spigoli usando alcune classi di Next/Vercel.

Ecco una variante con il puro MCP SDK in TS:

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

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

// 2. Registriamo le MCP Resources
// 3. Registriamo gli MCP Tools

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

// 5. Avvio del server
await server.connect(transport);

Ma è meglio usare alcune classi pronte per lavorare in modo più piacevole:

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

Qui McpGateway è una classe wrapper attorno a McpServer, che create da qualche parte (per esempio in lib/mcp/server.ts) usando l'SDK. Nel nostro caso è tutta contenuta in app/mcp/route.ts. Vediamo integralmente che cosa c'è in questo file.

type ContentWidget

All'inizio del file definiamo il tipo ContentWidget. Contiene tutti i dati del widget e viene usato in due punti: quando si registra il widget come mcp‑resource e quando un mcp‑tool restituisce i metadata in cui indica quale widget usare per visualizzare i dati che ha restituito.

type ContentWidget = {
  id: string;            // Nome/chiave univoco
  title: string;         // Title
  description: string;   // Description
  templateUri: string;   // URI univoco del widget, può essere qualsiasi. Non influisce su nulla.
  invoking: string;      // Testo sopra il widget mentre si carica
  invoked: string;       // Testo sopra il widget quando è già stato caricato
  html: string;          // Tutto il codice HTML del widget.
  widgetDomain: string;  // «Dominio» del widget. Non influisce su nulla.
};

class McpGateway

Classe wrapper su McpServer, semplifica alcune cose. Contiene 6 metodi:

  • initialize() — qui carichiamo l'HTML del nostro widget
  • registerResources() — registriamo i widget come mcp‑resources
  • registerTools() — registriamo le funzioni come mcp‑tools
  • widgetMeta() — restituisce i metadati del widget
  • getAppsSdkCompatibleHtml() — carica il codice HTML del widget e lo patcha leggermente
  • makeImgUrlsAbsolute() — patcha l'HTML: rende assoluti i link alle immagini

Vediamoli più nel dettaglio:

public async initialize()

Questo metodo scarica da Internet il codice HTML dei widget e compila un oggetto di tipo ContentWidget.

{
  id: "hello_world",                         // Chiave univoca del widget
  templateUri: "ui://widget/hello_world.html", // URI univoco del widget. «ui:» non significa nulla.
  title: "HelloWorld Widget",               // Nome del widget
  description: "Displays the HelloWorld widget", // Spiegazione per la LLM di cosa fa il widget
  invoking: "Loading widget...",            // Testo sopra il widget durante il caricamento
  invoked: "Widget loaded",                 // Testo sopra il widget dopo il caricamento
  html: htmlWidget,                         // HTML del widget
  widgetDomain: baseURL,                    // «Dominio» del widget. Al momento non influisce su nulla.
}

public registerResources()

Registra i widget come mcp‑resources. Chiama il metodo server.registerResource(), al quale vengono passati 4 parametri:

  • id/chiave della risorsa MCP
  • URI della risorsa (serve al protocollo MCP; per il widget è di fatto un sinonimo di indirizzo univoco)
  • Metadati della risorsa MCP
  • Funzione che restituisce la risorsa MCP

Metadati del widget

{
  title: widget.title,                 // Nome della risorsa/widget
  description: widget.description,     // Descrizione della risorsa/widget
  mimeType: "text/html+skybridge",     // Importante! Solo questo tipo di HTML verrà visualizzato come widget
  _meta: {
    "openai/widgetDescription": widget.description, // Descrizione del widget
    "openai/widgetPrefersBorder": true,            // Diciamo a ChatGPT di disegnare un bordo per il widget
  },
}

Widget come risorsa MCP

{
  uri: uri.href,                        // Il nostro URI (preso dal parametro uri)
  mimeType: "text/html+skybridge",      // Importante! Solo questo tipo di HTML verrà visualizzato come widget
  text: widget.html,                    // HTML del widget
  _meta: {
    "openai/widgetDescription": widget.description, // Descrizione del widget
    "openai/widgetPrefersBorder": true,            // Diciamo a ChatGPT di disegnare un bordo per il widget
    "openai/widgetDomain": widget.widgetDomain,    // «Dominio» del widget. Al momento non influisce su nulla.
    "openai/widgetCSP": {                          // Importante! Domini consentiti al widget:
      connect_domains: [                           // Domini per le connessioni (fetch ecc.)
        baseURL,
        "https://codegym.cc",
      ],
      resource_domains: [                          // Domini per le risorse (css/font/img)
        baseURL,
        "https://codegym.cc",
        "https://cdn.tailwindcss.com",
        "https://persistent.oaistatic.com",
        "https://fonts.googleapis.com",
        "https://fonts.gstatic.com"
      ]
    }
  },
}

In futuro torneremo più volte su openai/widgetCSP, ma per ora vorrei sottolineare 2 aspetti:

  • connect_domains — elenco di domini per:
    • fetch()
    • caricamento degli script
    • openExternal()
  • resource_domains — elenco di domini per:
    • immagini
    • CSS
    • font

In teoria potete elencare 200 domini, ma riuscirete a superare la review con una lista del genere? È un'altra storia.

Ho anche visto questi parametri in app già pubblicate e ho trovato amplitude.com. Anche questa è una buona notizia. Penso che una buona analytics non faccia male a nessuno.

public registerTools()

Registra le funzioni come mcp‑tools. Chiama il metodo server.registerTool(), al quale vengono passati 3 parametri:

  • id/chiave dell'MCP‑tool
  • Metadati dell'MCP‑tool
  • Funzione che restituisce l'MCP‑tool

Metadati dello strumento

Tutti i parametri in questo elenco sono importanti. Ne parlerò in dettaglio nelle prossime lezioni.

{
  title: widget.title,                               // Nome dello strumento
  description: "Returns HelloWorld widget",          // Importante! Descrizione di cosa fa lo strumento
  inputSchema: z.object({}).describe("No inputs"),   // Schema dei parametri dello strumento. Si può usare Zod
  _meta: this.widgetMeta(widget),                    // Metadati del widget: quale widget visualizzare
  annotations: {
    destructiveHint: false,                          // Il metodo fa qualcosa di importante - serve conferma
    openWorldHint: false,                            // Il metodo cambia qualcosa in servizi di terze parti
    readOnlyHint: true                               // Il metodo non modifica nulla
  },
}

Funzione che fa qualcosa di importante

async (input, extra) => {
  // 1. Validazione dei parametri
  // 2. Facciamo qualcosa di importante
  return {
    content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Descrizione del risultato per l'IA
    structuredContent: {                                      // Importante! Questo è il JSON del risultato.
      timestamp: new Date().toISOString()                     // Può contenere qualsiasi dato.
    },
    _meta: this.widgetMeta(widget),                           // Metadati del widget che visualizza il JSON
  };                                                          // Può mancare — in tal caso non ci sarà alcun widget
}

private widgetMeta(widget: ContentWidget)

Restituisce i metadati del widget — in base a questi ChatGPT determinerà quale widget usare per visualizzare il risultato JSON.

{
  "openai/outputTemplate": widget.templateUri,            // URI del widget
  "openai/toolInvocation/invoking": widget.invoking,      // Testo sopra il widget mentre si carica
  "openai/toolInvocation/invoked": widget.invoked,        // Testo sopra il widget quando è stato caricato
  "openai/widgetAccessible": true,                        // Si può invocare l'MCP-tool dal widget
  "openai/resultCanProduceWidget": true,                  // L'MCP-tool restituirà un widget
}

Vorrei discutere separatamente una cosa semplice come "openai/outputTemplate". Nel protocollo MCP ci sono 3 entità (di cui saprete di più nel modulo 6):

  • MCP Resources
  • MCP Templates
  • MCP Tools

Ebbene, questo "openai/outputTemplate" non ha alcun rapporto con gli MCP Templates. Gli MCP Templates non sono proprio utilizzati nelle ChatGPT Apps. La parola template qui deriva da questo:

I widget sono stati pensati come un template per visualizzare un JSON. L'MCP‑tool restituisce un certo JSON, l'IA visualizza un widget, gli passa il JSON tramite il parametro ToolOutput e il widget visualizza quel JSON in modo gradevole. outputTemplate — è semplicemente un sinonimo di widget.

Direi che per ora basta. Analizzeremo queste cose nel dettaglio nel modulo 4: come descrivere gli strumenti, la JSON Schema e i gestori. Ora è sufficiente capire: se qualcosa è legato agli strumenti (tools) e alla logica — cercate vicino a app/mcp/route.ts.

6. Configurazione e «colla»: next.config.ts, middleware.ts, .env e affini

Ora analizziamo l'insieme principale di file necessari affinché il tuo progetto Next.js funzioni correttamente dentro l'iframe di ChatGPT e sia raggiungibile da ChatGPT tramite un tunnel HTTPS (ngrok, Cloudflare Tunnel ecc.; dei tunnel parleremo a parte).

next.config.ts

In questo file, oltre alle impostazioni standard di Next.js, spesso si configura:

  • assetPrefix — affinché gli asset (JS, CSS da /_next/) vengano caricati correttamente non dal dominio di ChatGPT, ma dal tuo URL di sviluppo (tunnel o Vercel);
  • qualunque impostazione specifica necessaria al template (per esempio flag sperimentali per Next 16).

In pratica appare come un normale export di nextConfig con i campi necessari. Per la lezione ci interessa una cosa: se in ChatGPT il widget non riesce a caricare CSS/JS, molto spesso il colpevole è proprio assetPrefix.

proxy.ts (ex middleware.ts)

Questo file inserisce un layer di middleware tra la richiesta proveniente da ChatGPT e le tue route. Nel template in genere:

  • imposta gli header CORS, in modo che l'iframe di ChatGPT abbia il diritto di contattare il tuo server;
  • a volte configura header aggiuntivi per le React Server Components.

Per ora non serve conoscerne tutte le finezze. È utile solo ricordare: se ChatGPT segnala problemi di CORS o vedi strani errori in DevTools relativi a divieti di accesso, dai un'occhiata a proxy.ts.

.env

Il file .env (o .env.local) è il posto per i segreti e i parametri d'ambiente:

  • OPENAI_API_KEY (se il server MCP contatta l'OpenAI API direttamente),
  • indirizzi delle tue API interne,
  • token di servizi terzi ecc.

C'è una sfumatura importante: in Next.js le variabili che iniziano con NEXT_PUBLIC_ finiscono automaticamente nel bundle JS e diventano accessibili nel browser. Non farlo mai con OPENAI_API_KEY; i segreti devono stare solo nelle variabili server‑side.

package.json e tsconfig.json

In package.json vedrai:

  • le versioni di Next.js, React, Apps SDK, MCP SDK e le altre dipendenze;
  • gli script dev, build, start e talvolta comandi ausiliari (linter, formatter ecc.).

In tsconfig.json trovi le consuete impostazioni di TypeScript:

  • percorsi degli alias (@/lib, @/components),
  • modalità strict,
  • target di compilazione.

Dal punto di vista di questo corso, la cosa principale è capire che il template usa il normale stack TypeScript e puoi estenderlo nel modo standard.

7. Navigatore rapido del progetto per lo sviluppatore

Fissiamo dove andare quando vuoi fare le cose tipiche. Senza elenchi, solo in forma di mini‑scenari.

Se vuoi cambiare testi/pulsanti nel widget, apri il file dell'UI del widget: è app/widget/page.tsx oppure app/page.tsx — a seconda del template. Lì modifichi il JSX, aggiungi nuovi componenti, colleghi un design system. Ed è proprio qui che userai il runtime dell'Apps SDK (window.openai o hook comodi) per visualizzare i dati.

Se devi aggiungere un nuovo pulsante che fa qualcosa sul server, inizi comunque dal file UI. Il pulsante nel widget, al click, chiamerà window.openai.callTool, e l'implementazione di questo strumento la aggiungerai nella configurazione del server MCP, cioè nel codice vicino a app/mcp/route.ts. Il collegamento UI ↔ logica del tool lo vedremo proprio nei moduli 4 e successivi.

Quando vuoi insegnare a ChatGPT una nuova funzionalità (per esempio, «ricerca di tour» o «selezione prodotti»), vai nello strato MCP (file importati da app/mcp/route.ts). Lì registri un nuovo tool con JSON Schema, descrizione e handler. Il widget potrà poi leggere il risultato tramite window.openai.toolOutput e visualizzarlo in modo gradevole.

Se ti si rompe la statica o il widget si comporta in modo strano solo in ChatGPT, mentre in locale è tutto ok, ricordati dello strato collante. Per prima cosa controlla next.config.ts (in particolare assetPrefix) e middleware.ts/proxy.ts (CORS). Se di recente hai cambiato tunnel, URL o fatto deploy su Vercel, la correttezza di queste impostazioni è critica.

Infine, se sospetti problemi con le chiavi o l'ambiente, la tua triade di file è — .env.local, package.json (per verificare quali dipendenze e script vengono realmente usati) e i log del dev‑server. Proprio questo insieme garantisce che l'MCP abbia accesso ai segreti e ai servizi necessari.

8. Mini‑pratica: familiarizziamoci con il file system a mano

La teoria è teoria, ma fissiamo con mano dove sta cosa. Questi passi si possono fare subito nell'editor/IDE.

Prova ad aprire nel tuo progetto la cartella app e trova quale file è responsabile del widget. Se il template usa app/page.tsx, proprio lì vedrai una scritta familiare del tipo «HelloWorld — ChatGPT App» o un testo di benvenuto. Se il widget non esiste come cartella separata, apri app/page.tsx e assicurati che ci sia 'use client' e del markup JSX.

Poi trova app/mcp/route.ts. Fai attenzione a quali moduli importa: di solito vedrai o l'uso diretto dell'MCP SDK, o la chiamata di una funzione di supporto da lib/mcp/*. Valuta quanto è «sottile» questo layer — idealmente non deve contenere quasi alcuna business logic, solo «ricevi JSON → passalo al server → restituisci JSON».

Dopo di che dai un'occhiata a next.config.ts e proxy.ts/middleware.ts. Non è necessario capire tutto ciò che c'è scritto, limita a fissare che:

  • next.config.ts è responsabile della configurazione di Next, incluse le regole di build e la distribuzione degli asset;
  • proxy.ts interviene nelle richieste HTTP (quasi certamente ci vedrai la gestione degli header).

E per finire apri .env o .env.local e assicurati che le tue chiavi stiano proprio lì e non nel codice. Se da qualche parte vedi NEXT_PUBLIC_OPENAI_API_KEY — è un'ottima occasione per correggere, finché si tratta solo di sviluppo locale.

9. Schema visivo: come ChatGPT interagisce con il vostro template

Per fissare definitivamente il quadro, è utile guardare un flusso semplice:

flowchart TD
    U[Utente in ChatGPT] -->|Scrive una richiesta| M[Modello di ChatGPT]

    M -->|Invoca uno strumento| MCP["Il vostro endpoint MCP
app/mcp/route.ts"] MCP -->|"Risposta JSON MCP (structuredContent, _meta, link UI)"| M M -->|Decide di mostrare l'UI| WIDGET_URL["URL del widget
(/widget oppure /)"] WIDGET_URL -->|iframe| W[Il vostro widget
app/page.tsx] W -->|legge window.openai.toolOutput
+ widgetState| U

Qui è importante notare che l'iniziatore è quasi sempre il modello di ChatGPT, e non il browser dell'utente, come nelle classiche web‑app. Il tuo app/mcp/route.ts e app/widget/page.tsx — sono semplicemente due «porte» diverse dello stesso progetto Next.js: una per il robot (MCP), l'altra per l'UI.

Se tieni a mente questa mappa del progetto (widget → strato MCP → config) ed eviti consapevolmente le trappole elencate, più avanti nel corso potrai concentrarti sulla logica e sull'UX della tua App, invece di cercare «quel file che rompe tutto».

10. Errori tipici nella gestione della struttura del template

Errore n. 1: Confondere il widget con una normale pagina del sito.
A volte lo sviluppatore vede nel template sia app/page.tsx sia app/widget/page.tsx, modifica «il file sbagliato» e si stupisce che le modifiche non compaiano in ChatGPT. Il widget è proprio la pagina usata come outputTemplate/iframe per lo strumento MCP. Se cambi un'altra route, ChatGPT non lo verrà nemmeno a sapere. Fai sempre riferimento al README del template e guarda quale URL è indicato come widget.

Errore n. 2: Scrivere codice client (window, document) nei file server MCP.
Il file app/mcp/route.ts e tutto ciò che importa viene eseguito sul server. Qualsiasi tentativo di usare window o le API del DOM porterà a un crash del runtime. Se vuoi fare qualcosa nell'UI, quasi certamente deve stare nei file sotto app/widget o in altri componenti client. Lo strato MCP è puro backend: richieste, database, API esterne e costruzione della risposta strutturata.

Errore n. 3: Ignorare assetPrefix e le impostazioni CORS.
In locale su localhost:3000 funziona tutto benissimo, ma appena apri l'App tramite tunnel in ChatGPT — scompaiono gli stili, JS non si carica, la console si riempie di errori CORS. Spesso il motivo è che la configurazione di next.config.ts o middleware.ts/proxy.ts non tiene conto del nuovo URL pubblico o è stata rotta per sbaglio durante un refactor. Quando modifichi questi file, ricordati sempre che il tuo codice vivrà dentro un iframe sul dominio di ChatGPT, non direttamente su localhost.

Errore n. 4: Conservare i segreti non in .env, ma direttamente nel codice o in variabili NEXT_PUBLIC_*.
Nascondere la OPENAI_API_KEY in una const apiKey = 'sk-...' da qualche parte in app/widget/page.tsx — è l'idea peggiore: la chiave finirà nel bundle JS e arriverà a qualsiasi utente. Quasi altrettanto sbagliato — definire una variabile NEXT_PUBLIC_OPENAI_API_KEY, perché il prefisso NEXT_PUBLIC_ ne garantisce l'arrivo nel browser. Metti sempre i segreti in .env senza questo prefisso e usali solo lato server (server MCP, funzioni backend).

Errore n. 5: Considerare il template «troppo intelligente» e aver paura di toccarlo.
A volte gli sviluppatori trattano lo starter ufficiale come qualcosa di sacro: «meglio non toccarlo, potrei rompere l'integrazione». Di conseguenza scrivono tutto il proprio codice altrove, complicano l'architettura e inciampano comunque nelle stesse trappole. In realtà il template — è solo un progetto Next.js ben assemblato con un paio di impostazioni per l'Apps SDK. Capire che app/ — è UI e MCP, e il resto — normali config, libera molto: inizi a lavorare con il codice come con un normale progetto React/Next, non con una scatola magica.

Errore n. 6: Cercare di risolvere tutti i problemi «a livello di widget».
A volte si vorrebbe fare tutto nell'UI: business logic, accesso ai database, richieste a API esterne. Nel contesto delle ChatGPT Apps questa è un'idea particolarmente cattiva: il widget vive in una sandbox molto rigida, non vede i tuoi segreti e dipende molto da window.openai. Se serve qualcosa di serio — il posto giusto è nello strato MCP e nei servizi backend, mentre il widget dovrebbe essere un sottile layer di presentazione che visualizza dati strutturati e, se necessario, attiva gli strumenti.

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