Il y a quelques semaines, OpenAI a introduit des applications pour ChatGPT. Comme vous pouvez le voir ci-dessous, il permet aux entreprises d'injecter leur produit directement dans le chat pour aider à satisfaire la demande de l'utilisateur. Une application peut être déclenchée soit par une mention explicite, soit lorsque le modèle décide que l’application va être utile. So, what is a ChatGPT App? Pour un client, c’est un moyen d’obtenir une expérience utilisateur et une fonctionnalité plus riches, au-delà des contraintes d’une interface textuelle. Pour une entreprise, c’est un moyen d’atteindre plus de 800 millions d’utilisateurs de ChatGPT au bon moment. Pour un développeur, c’est un serveur MCP et une application Web qui fonctionne dans un iframe <= c’est ce dont nous sommes ici pour parler! démo Dans ce post, je vais construire une application de quiz simple, montrée ci-dessous, en l'utilisant comme exemple pour démontrer les fonctionnalités disponibles. Remarque importante : Si vous souhaitez suivre, vous aurez besoin d’un abonnement payant à ChatGPT pour activer le mode Développeur. Un abonnement client standard de 20 $/mois suffira. Si vous voulez suivre, Un abonnement client standard de 20 $ / mois suffira. Important Note: you will need a paid ChatGPT subscription to enable Developer Mode Flux de haut niveau Voici comment cela fonctionne à un niveau élevé (l'ordre réel des étapes peut varier légèrement): Tout d'abord, le développeur de l'application l'enregistre dans ChatGPT par Ce qui permet de mettre en œuvre l’app Le . Être pour , et il permet aux modèles comme ChatGPT d'explorer et d'interagir avec d'autres services. « et » Il fallait créer une application de quiz ChatGPT. À cette étape, providing a link to the MCP server (1) MCP Model Context Protocol tools resources ChatGPT learns and remembers what our app does and when it can be useful. Lorsque l'application existe déjà et que l'utilisateur comme "Faire un quiz sur Sam Altman", ChatGPT vérifiera s'il existe une App qu'il peut utiliser au lieu d'une réponse texte pour fournir une meilleure expérience à l'utilisateur . (2) (3) Si une App est trouvée, ChatGPT regarde le schéma des données dont l’App a besoin Notre application doit recevoir les données dans le format JSON suivant : (4) { questions: [ { question: "Where was Sam Altman born", options: ["San Francisco", ...], correctIndex: 2, ... }, ... ] } C’est ce qu’on appelle et nous l'enverrons dans notre application . ChatGPT will generate quiz data exactly in this format toolInput (5) L’application traitera les et produira ChatGPT rendra HTML « ressource » fourni par l’application dans la fenêtre de chat, et l’initialisera avec données Et enfin, l’utilisateur verra l’application et pourra interagir avec elle. . toolInput toolOutput toolOutput (6) (7) Créer un serveur MCP Code repo pour notre application ChatGPT : . https://github.com/renal128/quizaurus-tutorial Il y a 2 projets : et Tout d’abord, nous nous concentrerons sur qui utilise JavaScript simple dans le frontend pour garder les choses simples. quizaurus-plain quizaurus-react quizaurus-plain All of the server code is in this file - just about 140 lines of code! https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-plain/src/server.ts Serveur d'installation Il existe de nombreuses options pour créer un serveur MCP à l’aide de l’un des SDK énumérés ici : https://modelcontextprotocol.io/docs/sdk Ici, nous allons utiliser le . Type de fichier MCP SDK Le code ci-dessous montre comment le configurer : // Create an MCP server const mcpServer = new McpServer({ name: 'quizaurus-server', version: '0.0.1' }); // Add the tool that receives and validates questions, and starts a quiz mcpServer.registerTool( ... ); // Add a resource that contains the frontend code for rendering the widget mcpServer.registerResource( ... ); // Create an Express app const expressApp = express(); expressApp.use(express.json()); // Set up /mcp endpoint that will be handled by the MCP server expressApp.post('/mcp', async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); res.on('close', () => { transport.close(); }); await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body); }); const port = parseInt(process.env.PORT || '8000'); // Start the Express app expressApp.listen(port, () => { console.log(`MCP Server running on http://localhost:${port}/mcp`); }).on('error', error => { console.error('Server error:', error); process.exit(1); }); Points clés : L'application Express est un serveur générique qui reçoit des communications (comme les demandes HTTP) de l'extérieur (de ChatGPT) En utilisant Express, nous ajoutons un endpoint /mcp que nous fournirons à ChatGPT comme l'adresse de notre serveur MCP (comme https://mysite.com/mcp) The handling of the endpoint is delegated to the MCP server, that runs within the Express app /mcp all of the MCP protocol that we need is handled within that endpoint by the MCP server mcpServer.registerTool(...) et mcpServer.registerResource(...) est ce que nous utiliserons pour mettre en œuvre notre application Quiz Les outils MCP Remplissons le fossé dans placeholder ci-dessus pour enregistrer le « outil ». mcpServer.registerTool(…) ChatGPT lisera la définition de l’outil lorsque nous enregistrerons l’application, puis, lorsque l’utilisateur en aura besoin, ChatGPT invoquera l’outil pour lancer un quiz : // Add the tool that receives and validates questions, and starts a quiz mcpServer.registerTool( 'render-quiz', { title: 'Render Quiz', description: ` Use this when the user requests an interactive quiz. The tool expects to receive high-quality single-answer questions that match the schema in input/structuredContent: each item needs { question, options[], correctIndex, explanation }. Use 5–10 questions unless the user requests a specific number of questions. The questions will be shown to the user by the tool as an interactive quiz. Do not print the questions or answers in chat when you use this tool. Do not provide any sensitive or personal user information to this tool.`, _meta: { "openai/outputTemplate": "ui://widget/interactive-quiz.html", // <- hook to the resource }, inputSchema: { topic: z.string().describe("Quiz topic (e.g., 'US history')."), difficulty: z.enum(["easy", "medium", "hard"]).default("medium"), questions: z.array( z.object({ question: z.string(), options: z.array(z.string()).min(4).max(4), correctIndex: z.number().int(), explanation: z.string().optional(), }) ).min(1).max(40), }, }, async (toolInput) => { const { topic, difficulty, questions } = toolInput; // Here you can run any server-side logic to process the input from ChatGPT and // prepare toolOutput that would be fed into the frontend widget code. // E.g. you can receive search filters and return matching items. return { // Optional narration beneath the component content: [{ type: "text", text: `Starting a ${difficulty} quiz on ${topic}.` }], // `structuredContent` will be available as `toolOutput` in the frontend widget code structuredContent: { topic, difficulty, questions, }, // Private to the component; not visible to the model _meta: { "openai/locale": "en" }, }; } ); La moitié supérieure du code fournit une description de l'outil - ChatGPT s'en appuyera pour comprendre quand et comment l'utiliser: Description décrit en détail ce que fait l'outil. ChatGPT l'utilisera pour décider si l'outil est applicable à l'invitation de l'utilisateur. inputSchema est un moyen de dire à ChatGPT exactement quelles données il a besoin de fournir à l'outil et comment il devrait être structuré. Comme vous pouvez le voir ci-dessus, il contient des indices et des contraintes que ChatGPT peut utiliser pour préparer une bonne charge utile (toolInput). outputSchema est omis ici, mais vous pouvez le fournir pour dire à ChatGPT quel schéma structuréContent aura. Donc, dans un sens, l’outil est ce qui définit l’application ChatGPT ici. Jetons un coup d’œil aux deux autres champs : is the identifier of the MCP resource that the ChatGPT App will use to render the widget. We will look at in the next section below. _meta[“openai/outputTemplate”] async (toolInput) => { ... est la fonction qui reçoit toolInput de ChatGPT et produit toolOutput qui sera disponible pour le widget. C'est là que nous pouvons exécuter toute logique du côté du serveur pour traiter les données. Dans notre cas, nous n'avons pas besoin de traitement car toolInput contient déjà toutes les informations dont le widget a besoin, de sorte que la fonction renvoie les mêmes données dans structuredContent qui seront disponibles comme toolOutput pour le widget. Ressources MCP Voici comment définir une ressource MCP : // Add an MCP resource that contains frontend code for rendering the widget mcpServer.registerResource( 'interactive-quiz', "ui://widget/interactive-quiz.html", // must match `openai/outputTemplate` in the tool definition above {}, async (uri) => { // copy frontend script and css const quizaurusJs = await fs.readFile("./src/dist/QuizaurusWidget.js", "utf8"); const quizaurusCss = await fs.readFile("./src/dist/QuizaurusWidget.css", "utf8"); return { contents: [ { uri: uri.href, mimeType: "text/html+skybridge", // Below is the HTML code for the widget. // It defines a root div and injects our custom script from src/dist/QuizaurusWidget.js, // which finds the root div by its ID and renders the widget components in it. text: ` <div id="quizaurus-root" class="quizaurus-root"></div> <script type="module"> ${quizaurusJs} </script> <style> ${quizaurusCss} </style>` } ] } } ); Fondamentalement, la « ressource » fournit ici la partie frontend (widget) de l’Application. ui://widget/interactive-quiz.html est l'ID de ressource, et il devrait correspondre à _meta[«openai/outputTemplate»] de la définition de l'outil de la section précédente ci-dessus. provides HTML code of the widget contents the HTML here is very simple - we just define the root div and add that will find that root div by ID, create necessary elements (buttons, etc) and define the quiz app logic. We will look at the script in the next section below. quiz-app-root the custom script Créer un widget Implémentation du widget Maintenant, jetons un coup d’œil rapide that implements the widget (the visible part of the app): Utilisation du script QuizaurusWidget.js // Find the root div defined by the MCP resource const root = document.querySelector('#quiz-app-root'); // create HTML elements inside the root div ... // try to initialize for widgetState to restore the quiz state in case the chat page gets reloaded const selectedAnswers = window.openai.widgetState?.selectedAnswers ?? {}; let currentQuestionIndex = window.openai.widgetState?.currentQuestionIndex ?? 0; function refreshUI() { // Read questions from window.openai.toolOutput - this is the output of the tool defined in server.ts const questions = window.openai.toolOutput?.questions; // Initially the widget will be rendered with empty toolOutput. // It will be populated when ChatGPT receives toolOutput from our tool. if (!questions) { console.log("Questions have not yet been provided. Try again in a few sec.") return; } // Update UI according to the current state ... }; // when an answer button is clicked, we update the state and call refreshUI() optionButtons.forEach((b) => { b.onclick = (event) => { const selectedOption = event.target.textContent selectedAnswers[currentQuestionIndex] = selectedOption; // save and expose selected answers to ChatGPT window.openai.setWidgetState({ selectedAnswers, currentQuestionIndex }); refreshUI(); }; }); ... // at the end of the quiz, the user can click this button to review the answers with ChatGPT reviewResultsButton.onclick = () => { // send a prompt to ChatGPT, it will respond in the chat window.openai.sendFollowUpMessage({ prompt: "Review my answers and explain mistakes" }); reviewResultsButton.disabled = true; }; startQuizButton.onclick = refreshUI; refreshUI(); Remarque : ce code sera déclenché par le HTML que nous avons défini dans la ressource MCP ci-dessus (le L'HTML et le script seront à l'intérieur d'un iframe sur la page de chat ChatGPT. <script type="module">… ChatGPT expose certaines données et hooks via le global object. voici ce que nous utilisons ici: window.openai window.openai.toolOutput contient les données de question retournées par l'outil MCP. Initialement, le html sera rendu avant que l'outil ne retourne outilOutput, donc window.openai.toolOutput sera vide. Ceci est un peu ennuyeux, mais nous le corrigerons plus tard avec React. window.openai.widgetState et window.openai.setWidgetState() nous permettent de mettre à jour et d'accéder à l'état du widget. Il peut être n'importe quelle donnée que nous voulons, bien que la recommandation soit de le garder sous 4000 jetons. Ici, nous l'utilisons pour nous rappeler quelles questions ont déjà été répondues par l'utilisateur, de sorte que si la page est rechargée, le widget se souviendra de l'état. window.openai.sendFollowUpMessage ({prompt: "..."}) est un moyen de donner une prompt à ChatGPT comme si l'utilisateur l'avait écrit, et ChatGPT écrira la réponse dans le chat. Vous pouvez trouver plus de fonctionnalités dans la documentation OpenAI ici: https://developers.openai.com/apps-sdk/build/custom-ux Mettre tout ça ensemble Il est temps de le tester ! Un rappel rapide, vous aurez besoin d’un abonnement payant à ChatGPT pour activer le mode développeur. Clone this repo [Download the code] https://github.com/renal128/quizaurus-tutorial There are 2 projects in this repo, a minimalistic one, described above, and a slicker-looking React one. We’ll focus on the first one for now. Open a terminal, navigate to the repo directory and run the following commands: [Starting the server] cd quizaurus-plain install NodeJS if you don’t have it https://nodejs.org/en/download/ to install dependencies defined in package.json npm install to start the Express app with MCP server - npm start keep it running [ ] Expose your local server to the web Create a free ngrok account: https://ngrok.com/ Open a (the other one with the Express app should keep running separately) new terminal Install ngrok: https://ngrok.com/docs/getting-started#1-install-the-ngrok-agent-cli on MacOS brew install ngrok Connect ngrok on your laptop to your ngrok account by configuring it with your auth token: https://ngrok.com/docs/getting-started#2-connect-your-account Start ngrok: ngrok http 8000 You should see something like this in the bottom of the output: Forwarding: https://xxxxx-xxxxxxx-xxxxxxxxx.ngrok-free ngrok created a tunnel from your laptop to a public server, so that your local server is available to everyone on the internet, including ChatGPT. Again, , don’t close the terminal keep it running - this is the part that r , $20/month, otherwise you may not see developer mode available. [Enable Developer Mode on ChatGPT] equires a paid customer subscription Go to ChatGPT website => Settings => Apps & Connectors => Advanced settings Enable the “Developer mode” toggle [Add the app] Go back to “Apps & Connectors” and click “Create” in the top-right corner Fill in the details as on the screenshot. For “MCP Server URL” use the URL that ngrok gave you in the terminal output and . add /mcp to it at the end Click on your newly added app You should see the MCP tool under Actions - now ChatGPT knows when and how to use the app. When you make changes to the code, sometimes , otherwise it can remain cached (sometimes I even delete and re-add the app due to avoid caching). you need to click Refresh to make ChatGPT pick up the changes [ ] Finally, we’re ready to test it! Test the app In the chat window you can nudge ChatGPT to use your app by selecting it under the “ ” button. In my experience, it’s not always necessary, but let’s do it anyway. Then try a prompt like “Make an interactive 3-question quiz about Sam Altman”. + You should see ChatGPT asking your approval to call the MCP tool with the displayed . I assume that it’s a feature for unapproved apps, and it won’t happen once the app is properly reviewed by OpenAI (although, as of Nov 2025 there’s no defined process to publish an app yet). So, just click “Confirm” and wait a few seconds. toolInput As I mentioned above, the widget gets rendered before is returned by our MCP server. This means that if you click “Start Quiz” too soon, it won’t do anything - try again a couple seconds later. (we will fix that with React in the next section below). When the data is ready, clicking “Start Quiz” should show the quiz! toolOutput Utiliser la réaction Ci-dessus, nous avons regardé le code qui utilise JavaScript simple. , démontre comment implémenter une application ChatGPT à l'aide de React. Quizaurus-réacteur Vous trouverez ici une documentation utile d’OpenAI : . https://developers.openai.com/apps-sdk/build/custom-ux/ Utilisation des hooks d'aide OpenAiGlobal Vous pouvez les voir ici, le code est copié de la documentation: https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-react/web/src/openAiHooks.ts Le plus utile est , qui vous permet de souscrire l'application React aux mises à jour Rappelez-vous que dans l'application simple (non-React) ci-dessus, nous avons eu le problème que le bouton "Start Quiz" ne faisait rien jusqu'à ce que les données soient prêtes? useToolOutput window.openai.toolOutput function App() { const toolOutput = useToolOutput() as QuizData | null; if (!toolOutput) { return ( <div className="quiz-container"> <p className="quiz-loading__text">Generating your quiz...</p> </div> ); } // otherwise render the quiz ... } Lorsque l’output de l’outil est populé, React renouvellera automatiquement l’application et affichera le quiz au lieu de l’état de chargement. Router réactif L'historique de navigation de l'iframe dans lequel l'application est rendue est relié à l'historique de navigation de la page, de sorte que vous pouvez utiliser des API de routage telles que React Router pour mettre en œuvre la navigation au sein de l'application. Other quirks and features Remarque: Le développement de l'application ChatGPT n'est pas très stable pour le moment, car la fonctionnalité n'est pas complètement déployée, il est donc juste d'attendre des changements non annoncés à l'API ou des erreurs mineures. https://developers.openai.com/apps-sdk Comment et quand ChatGPT décide de montrer votre application à l’utilisateur The most important part is that your app’s metadata, such as the tool description, must feel relevant to the conversation. ChatGPT’s goal here is to provide the best UX to the user, so obviously if the app’s description is irrelevant to the prompt, the app won’t be shown. I’ve also seen ChatGPT asking the user to rate if the app was helpful or not, I suppose this feedback is also taken into account. App Metadata. Official recommendations: https://developers.openai.com/apps-sdk/guides/optimize-metadata The in order to be used. How would the user know to link an app? There are 2 ways: App Discovery. app needs to be linked/connected to the user’s account Manual - go to and find the app there. Settings => Apps & Connectors Contextual Suggestion - if the app is not connected, but is highly relevant in the conversation, ChatGPT may offer to connect it. I wasn’t able to make it work with my app, but I saw it working with pre-integrated apps like Zillow or Spotify: Déclencher une application connectée. Une fois l'application connectée, ChatGPT peut l'utiliser dans une conversation lorsque cela est approprié. L'utilisateur peut la déclencher en mentionnant simplement le nom de l'application dans le texte, en tapant @AppName ou en cliquant sur le bouton + et en sélectionnant l'application dans le menu. Les plateformes prises en charge Web - puisqu'il est mis en œuvre via iframe, le web est la plate-forme la plus facile à supporter et je n'ai eu presque aucun problème là-bas. Application mobile - si vous connectez l'application sur le web, vous devriez, être en mesure de le voir sur mobile. je n'ai pas pu déclencher l'application sur mobile - il n'a pas réussi à appeler l'outil, mais quand j'ai déclenché l'application sur le web, j'ai pu interagir avec elle sur mobile. Authenticité ChatGPT Apps prend en charge OAuth 2.1: https://developers.openai.com/apps-sdk/build/auth C'est un gros sujet, laissez-moi savoir s'il serait utile d'écrire un post séparé à ce sujet! Faire des demandes de réseau Voici ce que dit le document ( ) : « Travaillez avec votre partenaire OpenAI si vous avez besoin de domaines spécifiques autorisés. » source Standard fetch requests are allowed only when they comply with the CSP Dans un autre lieu ( Il suggère de configurer Objet dans la définition de ressource pour activer vos domaines: ici _meta _meta: { ... /* Assigns a subdomain for the HTML. When set, the HTML is rendered within `chatgpt-com.web-sandbox.oaiusercontent.com` It's also used to configure the base url for external links. */ "openai/widgetDomain": 'https://chatgpt.com', /* Required to make external network requests from the HTML code. Also used to validate `openai.openExternal()` requests. */ 'openai/widgetCSP': { // Maps to `connect-src` rule in the iframe CSP connect_domains: ['https://chatgpt.com'], // Maps to style-src, style-src-elem, img-src, font-src, media-src etc. in the iframe CSP resource_domains: ['https://*.oaistatic.com'], } } Une autre chose que vous pourriez utiliser est la Votre widget d'application (frontend) peut l'utiliser pour appeler un outil sur votre serveur MCP - vous fournissez le nom de l'outil MCP et l'outil Données d'entrée et recevez retour outilOutput: window.openai.callTool await window.openai?.callTool("my_tool_name", { "param_name": "param_value" }); Autres caractéristiques du front Consultez cette documentation pour ce qui est disponible pour votre code frontend via : : window.openai https://developers.openai.com/apps-sdk/build/custom-ux Vous pouvez accéder aux champs suivants (p. ex. vous indiquera si ChatGPT est actuellement en mode clair ou sombre): window.openai.theme theme: Theme; userAgent: UserAgent; locale: string; // layout maxHeight: number; displayMode: DisplayMode; safeArea: SafeArea; // state toolInput: ToolInput; toolOutput: ToolOutput | null; toolResponseMetadata: ToolResponseMetadata | null; widgetState: WidgetState | null; De même, vous pouvez utiliser les appels de retour suivants (par exemple, essayez Pour rendre votre application plein écran : await window.openai?.requestDisplayMode({ mode: "fullscreen" }); /** Calls a tool on your MCP. Returns the full response. */ callTool: ( name: string, args: Record<string, unknown> ) => Promise<CallToolResponse>; /** Triggers a followup turn in the ChatGPT conversation */ sendFollowUpMessage: (args: { prompt: string }) => Promise<void>; /** Opens an external link, redirects web page or mobile app */ openExternal(payload: { href: string }): void; /** For transitioning an app from inline to fullscreen or pip */ requestDisplayMode: (args: { mode: DisplayMode }) => Promise<{ /** * The granted display mode. The host may reject the request. * For mobile, PiP is always coerced to fullscreen. */ mode: DisplayMode; }>; /** Update widget state */ setWidgetState: (state: WidgetState) => Promise<void>; Merci à vous ! C'est tout, merci pour la lecture et bonne chance avec tout ce que vous construisez!