Кілька тижнів тому OpenAI представила Apps для ChatGPT. Як ви можете побачити нижче, це дозволяє компаніям вводити свій продукт прямо в чат, щоб допомогти задовольнити запит користувача. Додаток може бути спровокований або явним згадуванням, або коли модель вирішує, що додаток буде корисним. So, what is a ChatGPT App? Для клієнта це спосіб отримати більш багатий користувацький досвід і функціональність, за межами обмежень текстового інтерфейсу. Для бізнесу це спосіб досягти понад 800 мільйонів користувачів ChatGPT в потрібний час. Для розробника це MCP-сервер і веб-додаток, що працює в iframe <= це те, про що ми тут говоримо! Демо In this post, I'll walk through building a simple quiz app, shown below, using it as an example to demonstrate available features. Важлива зауваження: Якщо ви хочете слідувати, вам знадобиться платна підписка ChatGPT, щоб увімкнути режим розробника. Стандартна підписка клієнта $ 20 / місяць буде достатньо. Якщо ви хочете наслідувати, . A standard $20/month customer subscription will suffice. Important Note: you will need a paid ChatGPT subscription to enable Developer Mode Високий рівень потоку Ось як це працює на високому рівні (реальний порядок кроків може трохи відрізнятися): По-перше, розробник додатка реєструє його в ChatGPT Це дозволяє застосувати app . . Стоїть за , і це дозволяє моделям, таким як ChatGPT, досліджувати та взаємодіяти з іншими сервісами. «І» Потрібно було створити програму ChatGPT. На цьому кроці, 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. Коли додаток вже існує і користувач Наприклад, «Зробіть вікторину про Сама Альтмана», ChatGPT перевірить, чи є додаток, який він може використовувати замість текстової відповіді, щоб забезпечити кращий досвід для користувача. . (2) (3) Якщо знайдено додаток, ChatGPT розглядає схему даних, які потрібні додатку Наш додаток повинен отримувати дані в наступному форматі JSON: (4) { questions: [ { question: "Where was Sam Altman born", options: ["San Francisco", ...], correctIndex: 2, ... }, ... ] } Це називається , і відправить його в наше додаток . ChatGPT will generate quiz data exactly in this format toolInput (5) Пристрій буде обробляти і буде виробляти ChatGPT покаже HTML «ресурс», наданий додатком у вікні чату, і ініціалізує його з дані І, нарешті, користувач побачить додаток і зможе взаємодіяти з ним. . toolInput toolOutput toolOutput (6) (7) Створення MCP сервера Код репо для нашого додатка ChatGPT: . https://github.com/renal128/quizaurus-tutorial Є 2 проекти: та Спочатку ми зосередимося на яка використовує простий JavaScript в передньому кінці, щоб тримати речі простими. quizaurus-plain quizaurus-react quizaurus-plain Весь код сервера знаходиться в цьому файлі - всього близько 140 рядків коду! https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-plain/src/server.ts Налаштування сервера Є багато варіантів створення MCP-сервера за допомогою будь-якого з SDK, перерахованих тут: https://modelcontextprotocol.io/docs/sdk Here, we will use the . Typescript MCP SDK Код нижче показує, як його встановити: // 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); }); Ключові пункти : Express app - це загальний сервер, який отримує комунікації (наприклад, HTTP запити) ззовні (від ChatGPT) Використовуючи Express, ми додаємо кінцеву точку /mcp, яку ми надамо ChatGPT як адресу нашого сервера MCP (як 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(...) і mcpServer.registerResource(...) це те, що ми будемо використовувати для реалізації нашої програми Quiz MCP інструмент Давайте заповнимо розрив у Використовується для реєстрації «інструменту». mcpServer.registerTool(…) ChatGPT will read the tool definition when we register the app, and then, when the user needs it, ChatGPT will invoke the tool to start a 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" }, }; } ); Верхня половина коду надає опис інструменту - ChatGPT буде покладатися на нього, щоб зрозуміти, коли і як його використовувати: опис детально описує, що робить інструмент. ChatGPT буде використовувати його, щоб вирішити, чи застосовується інструмент до прохання користувача. inputSchema - це спосіб сказати ChatGPT точно, які дані він повинен надати інструменту і як він повинен бути структурований. Як ви бачите вище, він містить підказки та обмеження, які ChatGPT може використовувати для підготовки правильного корисного навантаження (toolInput). outputSchema тут випускається, але ви можете надати його, щоб сказати ChatGPT, яку схему структурованийContent буде мати. Отже, в певному сенсі, інструмент є тим, що визначає додаток ChatGPT тут. Давайте подивимося на інші два напрямки: 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) => { ... є функцією, яка отримує toolInput від ChatGPT і виробляє toolOutput, який буде доступний для віджету.Це місце, де ми можемо запустити будь-яку логіку сервера для обробки даних.У нашому випадку, ми не потребуємо будь-якої обробки, тому що toolInput вже містить всю інформацію, необхідну для віджету, тому функція повертає ті ж дані в структурованомуContent, які будуть доступні як toolOutput для віджету. Ресурс MCP Нижче наведено, як ми визначаємо ресурс 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>` } ] } } ); В основному, «ресурс» тут надає фронт-енд (віджет) частину додатка. ui://widget/interactive-quiz.html є ідентифікатором ресурсу, і він повинен відповідати _meta[«openai/outputTemplate»] визначення інструменту з попереднього розділу вище. 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 Створення Widget Використання Widget Тепер давайте швидко подивимося на that implements the widget (the visible part of the app): Сценарій 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(); Нагадування: цей код буде викликана HTML, який ми визначили в ресурсі MCP вище ( HTML і скрипт будуть всередині iframe на сторінці чату ChatGPT. <script type="module">… ChatGPT викриває деякі дані та хаки через Глобальний об'єкт: ось що ми використовуємо тут: window.openai window.openai.toolOutput містить дані запитання, повернуті інструментом MCP. Спочатку html буде відтворено, перш ніж інструмент поверне toolOutput, тому window.openai.toolOutput буде порожнім. Це трохи дратує, але ми виправимо це пізніше з React. window.openai.widgetState і window.openai.setWidgetState() дозволяють нам оновити і отримати доступ до стану віджет. Це може бути будь-які дані, які ми хочемо, хоча рекомендація полягає в тому, щоб тримати його нижче 4000 токенів. Тут, ми використовуємо його, щоб запам'ятати, які питання вже були відповіді користувача, так що якщо сторінка буде перезавантажена, віджет буде пам'ятати стан. window.openai.sendFollowUpMessage ({prompt: “...”}) - це спосіб дати прохання ChatGPT так, ніби користувач написав його, і ChatGPT напише відповідь в чаті. Більше можливостей можна знайти в документації OpenAI тут: https://developers.openai.com/apps-sdk/build/custom-ux Покласти все разом Час це випробувати! Для швидкого нагадування вам знадобиться платна підписка на ChatGPT, щоб увімкнути режим розробника. 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 Використання React Above, we looked at the code that uses plain JavaScript. The other project in the same repo, , demonstrates how to implement a ChatGPT app using React. Quizaurus-реакція You can find some helpful documentation from OpenAI here: . https://developers.openai.com/apps-sdk/build/custom-ux/ Використання OpenAiGlobal helper hooks Їх можна побачити тут, код скопійований з документації: https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-react/web/src/openAiHooks.ts Найбільш корисним є , що дозволяє підписатися на додаток React до оновлень в Пам'ятайте, що в простому (нереактному) додатку вище, у нас була проблема, що кнопка "Start Quiz" нічого не робила, поки дані не були готові? 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 ... } Коли toolOutput завантажується, React автоматично відтворює додаток і покаже тест замість стану завантаження. Реакція роутера The navigation history of the iframe in which the app is rendered is connected to the navigation history of the page, so you can use routing APIs such as React Router to implement navigation within the app. Other quirks and features Примітка: Розробка додатків ChatGPT на даний момент не дуже стабільна, оскільки функція не повністю запущена, тому справедливо очікувати неоголошених змін до API або дрібних помилок. https://developers.openai.com/apps-sdk Як і коли ChatGPT вирішує показати вашу програму користувачеві 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: Після підключення додатка, ChatGPT може використовувати його в розмові, коли це необхідно. Користувач може підштовхнути його, просто згадуючи ім'я додатка в тексті, вводячи @AppName або натиснувши кнопку + і вибираючи додаток в меню там. Підтримка платформ Веб - оскільки він реалізується через iframe, web є найпростішою платформою для підтримки, і у мене там майже не було проблем. Мобільне додаток - якщо ви підключите додаток в Інтернеті, ви повинні, щоб бути в змозі побачити його на мобільному. я не зміг запустити додаток на мобільному - він не вдалося викликати інструмент, але коли я запустив додаток в Інтернеті, я міг взаємодіяти з ним на мобільному. Автентифікація ChatGPT Apps підтримує OAuth 2.1: https://developers.openai.com/apps-sdk/build/auth Це велика тема, дайте мені знати, якщо буде корисно написати окремий пост про це! Складання мережевих запитів Ось про що йдеться в документі ( ) « Працюйте з вашим партнером OpenAI, якщо вам потрібні конкретні домени, дозволені». Джерело Standard fetch requests are allowed only when they comply with the CSP In another place ( Це означає, що конфігурація об'єкт у визначенні ресурсу, щоб дозволити свої домени: Тут _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'], } } Ще одне, що ви можете використовувати, це Ваш віджет програми (frontend) може використовувати його, щоб викликати інструмент на вашому сервері MCP - ви надаєте ім'я інструменту MCP і інструментВведення даних і отримуєте назад інструментВихід: window.openai.callTool await window.openai?.callTool("my_tool_name", { "param_name": "param_value" }); Інші особливості фронту Перегляньте цю документацію для того, що доступно для вашого коду frontend через : window.openai https://developers.openai.com/apps-sdk/build/custom-ux Ви можете отримати доступ до наступних полів (наприклад: Це покаже вам, чи знаходиться ChatGPT в даний час в світлому або темному режимі): 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; Аналогічно, ви можете використовувати наступні зворотні зв'язки (наприклад, спробуйте to make your app full-screen): 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>; Дякуємо Вам ! Це все, спасибі за читання і удачі з тим, що ви будуєте!