CodeGym /Cours /ChatGPT Apps /Comment est organisé le modèle : structure du projet et f...

Comment est organisé le modèle : structure du projet et fichiers clés

ChatGPT Apps
Niveau 2 , Leçon 1
Disponible

1. Introduction

Le projet ChatGPT App HelloWorld — ce n’est pas une « boîte noire magique de CodeGym qu’il vaut mieux ne pas toucher ». C’est un projet Next.js classique, simplement plusieurs parties y cohabitent en même temps :

  • un front-end rendu à l’intérieur de ChatGPT,
  • un serveur MCP qui répond aux appels d’outils (tools),
  • des réglages qui relient tout cela à ChatGPT.

Si l’on ne comprend pas où se trouve quoi, on rencontre généralement trois scénarios classiques :

  1. Le développeur écrit par inadvertance window dans un fichier serveur, essuie un crash et finit par détester toute la stack.
  2. Il essaie d’ajouter un bouton dans l’UI, mais modifie le mauvais page.tsx (par exemple la racine de l’application au lieu du widget) et ne voit aucun changement dans ChatGPT.
  3. Il place par erreur OPENAI_API_KEY côté client, et la clé fuit vers le navigateur.

C’est pourquoi l’objectif d’aujourd’hui est de tracer la carte : où est l’UI, où est le MCP, où sont les configs, et où aller quand vous voulez :

  • modifier l’aspect du widget ;
  • ajouter un nouveau tool ;
  • ajuster un paramètre de plateforme (CORS, assetPrefix, etc.).

2. Anatomie du projet à haut niveau

Le projet Next.js ChatGPT App HelloWorld utilise App Router et s’organise autour du dossier app/. Il y fait cohabiter dans un même arbre de pages :

  • l’UI du widget, qui sera rendu à l’intérieur de ChatGPT,
  • l’endpoint MCP, qui traitera les appels de tools.

Arborescence typique (simplifiée, les noms de dossiers peuvent varier dans votre modèle, mais le pattern reste le même) :

my-chatgpt-app/
├─ app/
│  ├─ api/                          // REST API
│  │  └─ time/                      // GET /api/time renvoie l’heure sur le serveur
│  │     └─ route.ts
│  ├─ hooks/                        // Ensemble de hooks de l’Apps SDK officiel
│  │  ├─ use-call-tool.ts
│  │  ├─ use-display-mode.ts
│  │  └─ use-open-external.ts
│  ├─ mcp/                          // Serveur MCP : ChatGPT s’y connecte quand il appelle des tools
│  │  └─ route.ts
│  ├─ globals.css                   // globals.css racine de toute l’application
│  ├─ layout.tsx                    // Layout racine de toute l’application
│  └─ page.tsx                      // Page du widget à l’intérieur de ChatGPT
├─ public/                          // Statique : icônes, manifest, etc.
├─ next.config.ts                   // Config Next.js et réglages spécifiques aux Apps (assetPrefix, etc.)
├─ proxy.ts                         // CORS/en-têtes pour fonctionner dans un iframe (ancien middleware.ts)
├─ package.json                     // Dépendances du projet
├─ tsconfig.json                    // Configuration TypeScript
└─ .env.local                       // Secrets : OPENAI_API_KEY, etc.

S’il y a plusieurs widgets, on les place généralement non pas dans app/page.tsx, mais dans app/widget/page.tsx. La logique ne change pas pour autant : il y a toujours une page‑widget et un endpoint qui joue le rôle de serveur MCP.

On peut le voir ainsi : votre dépôt est un « Janus à deux visages » :

  • un « visage » — le chemin /mcp, où va ChatGPT lorsqu’il veut appeler un outil ;
  • l’autre « visage » — le chemin /widget (ou /), chargé dans un iframe quand le modèle décide d’afficher votre UI.

Pour éviter de s’emmêler, retenons trois groupes de fichiers :

  1. Couche UI — tout ce qui concerne les pages React/Next (app/widget, composants, styles).
  2. Couche MCPapp/mcp/route.ts et les fichiers qu’il utilise.
  3. Couche « colle » et configsnext.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.

Nous allons parcourir un peu plus bas chacune de ces couches.

3. Où vit le widget : dossier app/widget et/ou app/page.tsx

Commençons par ce que vous manipulerez le plus souvent — le widget, c’est-à-dire l’UI visible à l’intérieur de ChatGPT.

Dans la plupart des projets actuels, il y a soit :

  • le fichier app/widget/page.tsx — le widget vit sous le préfixe /widget,
  • soit la racine app/page.tsx — le widget coïncide avec la page racine.

Signes distinctifs du fichier de widget :

  • tout en haut figure 'use client', parce que le composant s’exécute dans le navigateur, communique avec window et l’Apps SDK ;
  • c’est un composant React ordinaire, qui rend du markup et (un peu plus tard dans le cours) communique avec window.openai.

Exemple minimal d’un widget pédagogique (vous pouvez voir quelque chose de très similaire dans votre projet) :

// 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">
        Ici, nous construirons l’UI de notre widget.
      </p>
    </main>
  );
}

Si, dans votre modèle, le widget se trouve directement dans app/page.tsx, le code sera à peu près le même, simplement sans le dossier intermédiaire widget.

Attention à quelques points.

Premièrement, la directive 'use client' est indispensable : le widget lit/écrit dans window.openai, écoute des événements, etc., et cela n’est possible que dans un composant client. Si vous la retirez, Next tentera d’en faire une page serveur et vous obtiendrez des erreurs du type « window is not defined ».

Deuxièmement, c’est un composant React tout ce qu’il y a de plus normal. Vous pouvez :

  • le découper en sous‑composants dans components/,
  • utiliser Tailwind ou tout autre système CSS,
  • brancher des contextes, des hooks, etc.

Troisièmement, plus tard c’est précisément ici que vous allez :

  • lire window.openai.toolInput et window.openai.toolOutput, pour afficher des données réelles,
  • sauvegarder widgetState via window.openai.setWidgetState,
  • appeler openExternal, callTool et autres méthodes du runtime.

Pour l’instant, retenez ceci : si vous souhaitez modifier l’interface visuelle — c’est presque certainement dans app/widget/page.tsx ou app/page.tsx.

4. Layout racine : app/layout.tsx comme « cadre » de toute l’application

Le fichier important suivant est app/layout.tsx. Il :

  • définit la structure HTML (<html>, <body>),
  • importe les styles globaux (globals.css),
  • initialise souvent un « bootstrap » pour l’Apps SDK (un wrapper qui écoute window.openai et propage les données dans React).

Exemple simplifié :

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

Le nom NextChatSDKBootstrap est ici arbitraire, dans votre modèle cela peut être OpenAIAppProvider ou un autre composant. Sa mission est généralement la même : établir la liaison entre l’arbre React et le runtime de l’Apps SDK, s’abonner aux données globales (theme, displayMode, toolInput, etc.) et les distribuer aux enfants.

Point pratique important : si vous devez connecter un contexte global, des styles ou une bibliothèque UI (par exemple shadcn/ui), l’endroit approprié est presque toujours app/layout.tsx (ou un layout sous app/widget pour les réglages et composants spécifiques au widget).

Décryptage de NextChatSDKBootstrap

J’ai repéré NextChatSDKBootstrap dans le modèle officiel chez Vercel. Si vous ne le saviez pas, ce sont précisément eux qui ont créé et développent Next. Ils ont un bon article sur leur site à propos de l’app ChatGPT sous Next. Il y a aussi un Starter Template. Bien qu’il soit par endroits un peu daté, je pense qu’il y a de bonnes chances qu’ils en maintiennent l’actualité.

Mettons en évidence 5 points clés que fournit NextChatSDKBootstrap :

  • 1. Corrige les problèmes d’hydratation
    En bref, ChatGPT charge d’abord le HTML de votre widget sur son serveur, le nettoie et le patch. Résultat : le mécanisme d’hydratation se plaint et envoie des avertissements dans la console. Cela peut vous empêcher de passer la review.
  • 2. Patche l’historique du navigateur
    Votre widget est chargé dans un iframe depuis un domaine spécial de ChatGPT. Et si vous utilisez votre propre domaine, vous cassez le bac à sable. Par conséquent, seul le chemin sans domaine est conservé dans l’historique du navigateur.
  • 3. Réécrit la fonction fetch()
    Tous vos fetch() vers des adresses relatives sans domaine ne fonctionneront pas dans le widget, car le domaine de l’iframe est différent. Nous remplaçons donc fetch() par une version qui envoie les requêtes sans domaine vers l’URL correcte. Si un domaine est indiqué, tout fonctionne comme d’habitude.
  • 4. Les clics sur les liens fonctionnent
    Si les liens s’ouvrent dans l’iframe, ChatGPT ne l’appréciera pas. Du code a donc été ajouté pour intercepter les clics sur les liens et les ouvrir dans une fenêtre externe via openExternal().
  • 5. Ajout d’un base dans head (DEPRECATED)
    Ce code ajoutait aussi un <base> dans <head>, mais cela ne fonctionne plus. Le bac à sable réinitialise tout base défini. Je recommande donc d’utiliser des liens absolus pour tout : scripts, ressources, polices, API, etc.

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

Passons maintenant à la seconde moitié du « Janus à deux visages » — le serveur qui parle avec ChatGPT via MCP.

Le fichier app/mcp/route.ts est un Route Handler classique d’App Router qui :

  • reçoit des requêtes HTTP de ChatGPT (souvent POST avec un payload JSON au format MCP),
  • les transmet au serveur MCP (basé sur @modelcontextprotocol/sdk ou un léger wrapper),
  • renvoie en retour une réponse JSON au format MCP.

Deux options s’offrent à vous : écrire sur le SDK MCP pur, ou adoucir les angles avec quelques classes de Next/Vercel.

Voici une variante avec le SDK MCP pur en TS :

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

// 1. Créer le serveur MCP
const server = new McpServer({
  name: "simple-mcp-server",
  version: "1.0.0",
});

// 2. Enregistrer les ressources MCP
// 3. Enregistrer les tools MCP

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

// 5. Démarrer le serveur
await server.connect(transport);

Mais il est préférable d’utiliser quelques classes prêtes à l’emploi pour améliorer l’ergonomie :

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

Ici, McpGateway est une classe‑wrapper autour de McpServer, que vous créez quelque part (par exemple dans lib/mcp/server.ts) à l’aide du SDK. Dans notre cas, tout tient dans app/mcp/route.ts. Passons en revue en détail ce que contient ce fichier.

type ContentWidget

Au début du fichier, nous décrivons le type ContentWidget. Il contient toutes les données du widget et est utilisé à deux endroits : lors de l’enregistrement du widget comme ressource MCP, et quand un tool MCP renvoie des métadonnées indiquant quel widget utiliser pour afficher les données qu’il a renvoyées.

type ContentWidget = {
  id: string;            // Nom/clé unique
  title: string;         // Title
  description: string;   // Description
  templateUri: string;   // URI unique du widget, peut être arbitraire. N’influe sur rien.
  invoking: string;      // Libellé affiché pendant le chargement du widget
  invoked: string;       // Libellé affiché une fois le widget chargé
  html: string;          // Tout le code HTML du widget
  widgetDomain: string;  // « Domaine » du widget. N’influe sur rien.
};

class McpGateway

Classe‑wrapper autour de McpServer, elle simplifie certaines choses. Elle contient 6 méthodes :

  • initialize() — ici nous chargeons le HTML de notre widget
  • registerResources() — enregistre les widgets comme ressources MCP
  • registerTools() — enregistre des fonctions comme tools MCP
  • widgetMeta() — renvoie les métadonnées du widget
  • getAppsSdkCompatibleHtml() — charge le HTML du widget et le patche légèrement
  • makeImgUrlsAbsolute() — patche le HTML : rend les URLs d’images absolues

Voyons-les plus en détail :

public async initialize()

Cette méthode charge depuis Internet le code HTML des widgets et remplit un objet de type ContentWidget.

{
  id: "hello_world",                         // Clé unique du widget
  templateUri: "ui://widget/hello_world.html", // URI unique du widget. « ui: » ne signifie rien.
  title: "HelloWorld Widget",               // Nom du widget
  description: "Displays the HelloWorld widget", // Explication pour le LLM de ce que fait le widget
  invoking: "Loading widget...",            // Libellé pendant le chargement du widget
  invoked: "Widget loaded",                 // Libellé une fois le widget chargé
  html: htmlWidget,                         // HTML du widget
  widgetDomain: baseURL,                    // « Domaine » du widget. N’influe sur rien.
}

public registerResources()

Enregistre les widgets comme ressources MCP. Appelle la méthode server.registerResource(), à laquelle on passe 4 paramètres :

  • l’id/clé de la ressource MCP
  • l’URI de la ressource (spécifique au protocole MCP, pour le widget c’est essentiellement un synonyme d’adresse unique)
  • les métadonnées de la ressource MCP
  • la fonction qui renvoie la ressource MCP

Métadonnées du widget

{
  title: widget.title,                 // Nom de la ressource/du widget
  description: widget.description,     // Description de la ressource/du widget
  mimeType: "text/html+skybridge",     // Important ! Seul ce type de HTML s’affichera comme widget
  _meta: {
    "openai/widgetDescription": widget.description, // Description du widget
    "openai/widgetPrefersBorder": true,            // Demander à ChatGPT d’afficher une bordure autour du widget
  },
}

Widget en tant que ressource MCP

{
  uri: uri.href,                        // Notre URI (provenant du paramètre uri)
  mimeType: "text/html+skybridge",      // Important ! Seul ce type de HTML s’affichera comme widget
  text: widget.html,                    // HTML du widget
  _meta: {
    "openai/widgetDescription": widget.description, // Description du widget
    "openai/widgetPrefersBorder": true,            // Demander à ChatGPT d’afficher une bordure autour du widget
    "openai/widgetDomain": widget.widgetDomain,    // « Domaine » du widget. N’influe sur rien.
    "openai/widgetCSP": {                          // Important ! Domaines accessibles au widget :
      connect_domains: [                           // Domaines pour les connexions (fetch, etc.)
        baseURL,
        "https://codegym.cc",
      ],
      resource_domains: [                          // Domaines pour les ressources (css/polices/images)
        baseURL,
        "https://codegym.cc",
        "https://cdn.tailwindcss.com",
        "https://persistent.oaistatic.com",
        "https://fonts.googleapis.com",
        "https://fonts.gstatic.com"
      ]
    }
  },
}

Nous reviendrons plus d’une fois sur openai/widgetCSP, mais notons déjà deux points :

  • connect_domains — liste de domaines pour :
    • fetch()
    • le chargement de scripts
    • openExternal()
  • resource_domains — liste de domaines pour :
    • les images
    • le CSS
    • les polices

En théorie, vous pouvez écrire 200 domaines, mais pourrez‑vous passer la review avec une telle liste — c’est une autre histoire.

J’ai aussi étudié ces paramètres dans des applications déjà publiées et j’y ai trouvé amplitude.com. C’est une nouvelle plutôt positive. Une bonne analytique ne fait de mal à personne.

public registerTools()

Enregistre des fonctions comme tools MCP. Appelle la méthode server.registerTool(), à laquelle on passe 3 paramètres :

  • l’id/clé du MCP‑tool
  • les métadonnées du MCP‑tool
  • la fonction qui renvoie le MCP‑tool

Métadonnées de l’outil

Tous les paramètres de cette liste sont importants. J’en parlerai plus en détail dans les prochaines leçons.

{
  title: widget.title,                               // Nom de l’outil
  description: "Returns HelloWorld widget",          // Important ! Description de ce que fait l’outil
  inputSchema: z.object({}).describe("No inputs"),   // Schéma des paramètres de l’outil. Zod possible
  _meta: this.widgetMeta(widget),                    // Métadonnées du widget : quel widget afficher
  annotations: {
    destructiveHint: false,                          // L’action est sensible — une confirmation peut être nécessaire
    openWorldHint: false,                            // L’action modifie des services tiers
    readOnlyHint: true                               // L’action ne modifie rien
  },
}

Fonction qui fait quelque chose d’important

async (input, extra) => {
  // 1. Validation des paramètres
  // 2. Faire quelque chose d’important
  return {
    content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Description du résultat pour l’IA
    structuredContent: {                                      // Important ! C’est le JSON du résultat.
      timestamp: new Date().toISOString()                     // Peut contenir n’importe quelles données.
    },
    _meta: this.widgetMeta(widget),                           // Métadonnées du widget affichant le JSON
  };                                                          // Peut être absent — alors pas de widget
}

private widgetMeta(widget: ContentWidget)

Renvoie les métadonnées du widget — ChatGPT s’en sert pour déterminer quel widget utiliser pour afficher le résultat JSON.

{
  "openai/outputTemplate": widget.templateUri,            // URI du widget
  "openai/toolInvocation/invoking": widget.invoking,      // Libellé pendant le chargement du widget
  "openai/toolInvocation/invoked": widget.invoked,        // Libellé une fois le widget chargé
  "openai/widgetAccessible": true,                        // Le MCP‑tool peut être appelé depuis le widget
  "openai/resultCanProduceWidget": true,                  // Le MCP‑tool renverra un widget
}

J’aimerais isoler un point simple comme "openai/outputTemplate". Le protocole MCP comporte 3 entités (que vous verrez en détail dans le module 6) :

  • MCP Resources
  • MCP Templates
  • MCP Tools

Or, ce "openai/outputTemplate" n’a strictement aucun rapport avec les MCP Templates. Les MCP Templates ne sont pas utilisés dans les ChatGPT Apps. Le mot « template » vient d’ici :

Les widgets ont été conçus comme un gabarit d’affichage pour du JSON. Le MCP‑tool renvoie un certain JSON, l’IA affiche un widget, lui transmet le JSON via le paramètre ToolOutput, et le widget l’affiche joliment. outputTemplate est simplement un synonyme de widget.

Voilà pour l’instant. Nous détaillerons tout cela dans le module 4 : comment décrire les outils, JSON Schema et les handlers. Pour l’instant, il suffit de comprendre que si quelque chose concerne les outils (tools) et la logique — cherchez près de app/mcp/route.ts.

6. Configuration et « colle » : next.config.ts, middleware.ts, .env et compagnie

Voyons maintenant l’ensemble principal de fichiers nécessaires pour que votre projet Next.js fonctionne correctement à l’intérieur de l’iframe de ChatGPT et soit accessible par ChatGPT via un tunnel HTTPS (ngrok, Cloudflare Tunnel, etc. ; nous parlerons des tunnels séparément).

next.config.ts

Dans ce fichier, en plus des réglages standard de Next.js, on configure souvent :

  • assetPrefix — afin que la statique (JS, CSS depuis /_next/) se charge correctement non pas depuis le domaine de ChatGPT, mais depuis votre URL de dev (tunnel ou Vercel) ;
  • tout réglage spécifique requis par le modèle (par exemple des flags expérimentaux pour Next 16).

En pratique, cela ressemble à une simple exportation de nextConfig avec les champs nécessaires. Pour cette leçon, une chose est cruciale : si, dans ChatGPT, le widget ne parvient pas à charger le CSS/JS, très souvent le coupable est assetPrefix.

proxy.ts (ancien middleware.ts)

Ce fichier insère une couche de middleware entre la requête de ChatGPT et vos routes. Dans le modèle, il :

  • définit des en‑têtes CORS, pour que l’iframe de ChatGPT ait le droit d’accéder à votre serveur ;
  • parfois règle des en‑têtes supplémentaires pour les React Server Components.

Il n’est pas nécessaire d’en connaître toutes les subtilités tout de suite. Il est utile seulement de garder en tête : si ChatGPT se plaint du CORS ou si vous voyez des erreurs étranges dans DevTools à propos d’accès interdits, jetez un œil à proxy.ts.

.env

Le fichier .env (ou .env.local) est l’endroit pour les secrets et paramètres d’environnement :

  • OPENAI_API_KEY (si le serveur MCP appelle lui‑même l’API OpenAI),
  • les adresses de vos API internes,
  • les tokens de services tiers, etc.

Détail important : dans Next.js, les variables commençant par NEXT_PUBLIC_ sont automatiquement incluses dans le bundle JS et deviennent accessibles dans le navigateur. Ne le faites jamais avec OPENAI_API_KEY ; les secrets doivent être uniquement des variables côté serveur.

package.json et tsconfig.json

Dans package.json, vous verrez :

  • les versions de Next.js, React, Apps SDK, MCP SDK et autres dépendances ;
  • les scripts dev, build, start, et parfois des commandes auxiliaires (linter, formateur, etc.).

Dans tsconfig.json se trouvent les réglages habituels de TypeScript :

  • les alias de chemins (@/lib, @/components),
  • le mode strict,
  • les cibles de compilation.

Dans le cadre de ce cours, l’essentiel est de comprendre que le modèle utilise une stack TypeScript standard, que vous pouvez étendre de manière habituelle.

7. « Navigation rapide du projet » pour développeur

Fixons où aller lorsque vous souhaitez faire des actions typiques. Sans listes, sous forme de mini‑scénarios.

Si vous voulez changer le texte/les boutons du widget, ouvrez le fichier d’UI du widget : c’est soit app/widget/page.tsx, soit app/page.tsx — selon le modèle. Vous y modifiez le JSX, ajoutez de nouveaux composants, branchez un design system. Et c’est précisément ici que vous utiliserez le runtime de l’Apps SDK (window.openai ou des hooks pratiques) pour afficher les données.

Si vous devez ajouter un nouveau bouton qui fait quelque chose côté serveur, vous commencez quand même par le fichier d’UI. Le bouton, au clic, appellera window.openai.callTool, et vous ajouterez l’implémentation de cet outil dans la configuration du serveur MCP, c’est‑à‑dire dans le code à proximité de app/mcp/route.ts. Le chaînage UI ↔ logique de tool sera détaillé dans les modules 4 et suivants.

Quand vous voulez apprendre à ChatGPT une nouvelle fonctionnalité (par exemple « recherche de voyages » ou « sélection de produits »), allez dans la couche MCP (fichiers importés depuis app/mcp/route.ts). Vous y enregistrez un nouveau tool avec JSON Schema, description et handler. Le widget pourra ensuite lire le résultat via window.openai.toolOutput et l’afficher proprement.

Si vos assets ne se chargent plus ou si le widget s’affiche bizarrement uniquement dans ChatGPT, alors qu’en local tout va bien, pensez à la couche « colle ». Il faut d’abord vérifier next.config.ts (surtout assetPrefix) et middleware.ts/proxy.ts (CORS). Si vous avez récemment changé de tunnel, d’URL ou déployé sur Vercel, la justesse de ces réglages est cruciale.

Enfin, si vous suspectez des problèmes de clés ou d’environnement, votre trio de fichiers est : .env.local, package.json (pour vérifier quelles dépendances et scripts sont réellement utilisés) et les logs du serveur de dev. C’est ce trio qui garantit que le MCP a accès aux secrets et services nécessaires.

8. Mini‑pratique : découvrons le système de fichiers à la main

La théorie, c’est bien, mais ancrons‑la en pratiquant. Vous pouvez faire ces étapes tout de suite dans votre éditeur/IDE.

Essayez d’ouvrir dans votre projet le dossier app et de trouver quel fichier est responsable du widget. Si le modèle utilise app/page.tsx, c’est là que vous verrez un message familier du genre « HelloWorld — ChatGPT App » ou un texte d’accueil. Si le widget n’existe pas en tant que dossier séparé, ouvrez app/page.tsx et vérifiez qu’il contient 'use client' et un peu de markup JSX.

Ensuite, trouvez app/mcp/route.ts. Regardez quels modules il importe : généralement vous verrez soit une utilisation directe du MCP SDK, soit l’appel d’une fonction utilitaire depuis lib/mcp/*. Évaluez la « finesse » de cette couche — idéalement, il n’y a presque pas de logique métier, uniquement « reçu du JSON → passé au serveur → renvoyé du JSON ».

Après cela, jetez un œil à next.config.ts et proxy.ts/middleware.ts. Pas besoin de tout comprendre, contentez‑vous de noter que :

  • next.config.ts gère la configuration de Next, y compris les règles de build et de distribution des assets ;
  • proxy.ts s’intercale dans les requêtes HTTP (vous y verrez presque à coup sûr la gestion d’en‑têtes).

Pour finir, ouvrez .env ou .env.local et assurez‑vous que vos clés s’y trouvent, et non dans le code. Si vous voyez quelque part NEXT_PUBLIC_OPENAI_API_KEY — c’est une excellente raison de corriger tant que vous n’êtes qu’en développement local.

9. Schéma visuel : comment ChatGPT interagit avec votre modèle

Pour compléter le tableau, voici un flux simple :

flowchart TD
    U[Utilisateur dans ChatGPT] -->|Écrit une requête| M[Modèle ChatGPT]

    M -->|Appelle un tool| MCP["Votre endpoint MCP
app/mcp/route.ts"] MCP -->|"Réponse JSON MCP (structuredContent, _meta, lien UI)"| M M -->|Décide d’afficher l’UI| WIDGET_URL["URL du widget
(/widget ou /)"] WIDGET_URL -->|iframe| W[Votre widget
app/page.tsx] W -->|lit window.openai.toolOutput
+ widgetState| U

Il est important de noter ici que l’initiateur est presque toujours le modèle ChatGPT, et non le navigateur de l’utilisateur, comme dans une application web classique. Votre app/mcp/route.ts et app/widget/page.tsx — ce sont simplement deux « portes » vers le même projet Next.js : l’une pour le robot (MCP), l’autre pour l’UI.

Si vous gardez en tête cette carte du projet (widget → couche MCP → configs) et évitez consciemment les pièges mentionnés, la suite du cours vous permettra de vous concentrer sur la logique et l’UX de votre App, plutôt que de chercher « le fichier qui casse tout ».

10. Erreurs typiques lors du travail avec la structure du modèle

Erreur n°1 : confondre le widget avec une page classique du site.
Parfois, le développeur voit dans le modèle à la fois app/page.tsx et app/widget/page.tsx, modifie « le mauvais » fichier et s’étonne que les changements n’apparaissent pas dans ChatGPT. Le widget — c’est précisément la page utilisée comme outputTemplate/iframe pour l’outil MCP. Si vous modifiez une autre route, ChatGPT ne le saura même pas. Référez‑vous toujours au README du modèle et vérifiez quelle URL est indiquée comme widget.

Erreur n°2 : écrire du code client (window, document) dans les fichiers serveur MCP.
Le fichier app/mcp/route.ts et tout ce qu’il importe s’exécute côté serveur. Toute tentative d’y utiliser window ou l’API DOM fera s’effondrer le runtime. Si vous souhaitez faire quelque chose dans l’UI, cela doit presque certainement se trouver dans les fichiers sous app/widget ou d’autres composants client. La couche MCP — c’est du pur backend : requêtes, bases de données, APIs externes et formation de la réponse structurée.

Erreur n°3 : ignorer assetPrefix et les réglages CORS.
En local sur localhost:3000, tout fonctionne parfaitement, mais dès que vous ouvrez l’App via un tunnel dans ChatGPT — les styles disparaissent, le JS ne se charge pas, et la console se remplit d’erreurs CORS. La cause tient souvent à la configuration de next.config.ts ou de middleware.ts/proxy.ts qui n’intègre pas votre nouvelle URL publique ou a été cassée par un refactoring. En modifiant ces fichiers, gardez toujours à l’esprit que votre code vivra dans un iframe sur le domaine de ChatGPT, et non directement sur localhost.

Erreur n°4 : stocker des secrets ailleurs que dans .env, ou les mettre dans des variables NEXT_PUBLIC_*.
Cacher OPENAI_API_KEY dans const apiKey = 'sk-...' quelque part dans app/widget/page.tsx — c’est la pire idée : la clé sera incluse dans le bundle JS et ira chez n’importe quel utilisateur. Presque aussi mauvais — créer une variable NEXT_PUBLIC_OPENAI_API_KEY, car le préfixe NEXT_PUBLIC_ garantit son exposition dans le navigateur. Mettez toujours les secrets dans .env sans ce préfixe et utilisez‑les uniquement côté serveur (serveur MCP, fonctions backend).

Erreur n°5 : penser que le modèle est « trop intelligent » et avoir peur d’y toucher.
Certains traitent le starter officiel comme quelque chose de sacré : « n’y touche pas, tu risques de casser l’intégration ». Ils finissent par écrire tout leur code à côté, complexifiant l’architecture, et rencontrent tout de même les mêmes pièges. En réalité, le modèle — ce n’est qu’un projet Next.js proprement assemblé avec quelques réglages pour l’Apps SDK. Comprendre que app/ — c’est l’UI et le MCP, et que le reste — ce sont des configs ordinaires, libère l’esprit : vous travaillez avec le code comme avec un projet React/Next habituel, pas avec une boîte magique.

Erreur n°6 : essayer de résoudre tous les problèmes « au niveau du widget ».
Il est parfois tentant de tout faire dans l’UI : logique métier, accès aux bases, appels d’APIs externes. Dans le contexte des ChatGPT Apps, c’est une très mauvaise idée : le widget vit dans un bac à sable très strict, ne voit pas vos secrets et dépend fortement de window.openai. Si quelque chose est sérieux — sa place est dans la couche MCP et les services backend, tandis que le widget doit rester une fine couche de présentation, qui affiche des données structurées et, si nécessaire, déclenche des tools.

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