Kilka tygodni temu OpenAI wprowadziło aplikacje dla ChatGPT. Jak widać poniżej, pozwala firmom wstrzykiwać swój produkt bezpośrednio do czatu, aby pomóc zadowolić prośbę użytkownika. Aplikacja może być wyzwalana albo przez wyraźne wspomnienie, albo gdy model zdecyduje, że aplikacja będzie przydatna. So, what is a ChatGPT App? Dla klienta jest to sposób na bogatsze doświadczenie użytkownika i funkcjonalność, poza ograniczeniami interfejsu tekstowego. Dla firmy jest to sposób na dotarcie do ponad 800 milionów użytkowników ChatGPT w odpowiednim czasie. Dla dewelopera jest to serwer MCP i aplikacja internetowa, która działa w iframe <= o tym mówimy! Demo W tym poście przejdę przez budowę prostej aplikacji quizowej, pokazanej poniżej, używając jej jako przykładu, aby zademonstrować dostępne funkcje. Ważna uwaga: Jeśli chcesz kontynuować, będziesz potrzebował płatnej subskrypcji ChatGPT, aby włączyć tryb dewelopera. Jeśli chcesz podążać za nami, Wystarczy standardowe 20 USD/miesiąc subskrypcji klienta. Important Note: you will need a paid ChatGPT subscription to enable Developer Mode Wysoki poziom przepływu Oto, jak działa na wysokim poziomie (prawdziwa kolejność kroków może się nieznacznie różnić): Po pierwsze, deweloper aplikacji rejestruje go w ChatGPT przez Wdraża to aplikację ... stoją za , i pozwala modelom takim jak ChatGPT na eksplorację i interakcję z innymi usługami. „I” ” potrzeba utworzenia aplikacji quizowej ChatGPT. Na tym etapie, 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. Gdy aplikacja już istnieje, a użytkownik jak „Zrób quiz o Sam Altman”, ChatGPT sprawdzi, czy istnieje aplikacja, którą może użyć zamiast odpowiedzi tekstowej, aby zapewnić lepsze doświadczenie użytkownikowi . (2) (3) Jeśli zostanie znaleziona aplikacja, ChatGPT sprawdza schemat danych, których potrzebuje aplikacja Nasza aplikacja musi otrzymywać dane w następującym formacie JSON: (4) { questions: [ { question: "Where was Sam Altman born", options: ["San Francisco", ...], correctIndex: 2, ... }, ... ] } To się nazywa , a następnie wysłać go do naszej aplikacji . ChatGPT will generate quiz data exactly in this format toolInput (5) Aplikacja będzie przetwarzać i będzie produkować ChatGPT wyświetli „zasób” HTML dostarczony przez aplikację w oknie czatu i inicjalizuje go za pomocą Dane I wreszcie użytkownik zobaczy aplikację i będzie mógł z nią współpracować. . toolInput toolOutput toolOutput (6) (7) Tworzenie serwera MCP Kod repo dla naszej aplikacji ChatGPT: . https://github.com/renal128/quizaurus-tutorial Istnieją dwa projekty: i Po pierwsze, skupimy się na który używa prostego JavaScript w frontendzie, aby sprawy były proste. quizaurus-plain quizaurus-react quizaurus-plain Cały kod serwera znajduje się w tym pliku - tylko około 140 linii kodu! https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-plain/src/server.ts Serwer ustawienia Istnieje wiele opcji tworzenia serwera MCP przy użyciu dowolnego z SDK wymienionych tutaj: https://modelcontextprotocol.io/docs/sdk Tutaj będziemy korzystać z . Typowanie MCP SDK The code below shows how to set it up: // 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); }); Kluczowe punkty : Aplikacja Express jest ogólnym serwerem, który odbiera komunikację (taką jak żądania HTTP) z zewnątrz (z ChatGPT) Używając Expressu, dodajemy punkt końcowy /mcp, który dostarczymy do ChatGPT jako adres naszego serwera MCP (jak 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(...) i mcpServer.registerResource(...) to to, co będziemy używać do wdrożenia naszej aplikacji Quiz Narzędzie MCP Wypełnijmy tę lukę w Miejscowość na górze, aby zarejestrować „narzędzie”. mcpServer.registerTool(…) ChatGPT przeczyta definicję narzędzia, gdy zarejestrujemy aplikację, a następnie, gdy użytkownik tego potrzebuje, ChatGPT wezwie narzędzie, aby rozpocząć 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" }, }; } ); Górna połowa kodu zawiera opis narzędzia - ChatGPT polega na tym, aby zrozumieć, kiedy i jak go używać: opis opisuje szczegółowo, co narzędzie robi. ChatGPT użyje go, aby zdecydować, czy narzędzie jest stosowane do polecenia użytkownika. inputSchema jest sposobem, aby powiedzieć ChatGPT dokładnie, jakie dane musi dostarczyć do narzędzia i jak powinno być ustrukturyzowane. Jak widać powyżej, zawiera wskazówki i ograniczenia, które ChatGPT może wykorzystać do przygotowania prawidłowego obciążenia (toolInput). outputSchema jest tutaj pominięty, ale możesz go podać, aby powiedzieć ChatGPT, jaki schemat będzie miał strukturowanyContent. Tak więc, w pewnym sensie, to narzędzie definiuje tutaj aplikację ChatGPT. Przyjrzyjmy się pozostałym dwóm obszarom: _meta[„openai/outputTemplate”] to identyfikator zasobu MCP, który aplikacja ChatGPT użyje do renderowania widżetu. async (toolInput) => { ... jest funkcją, która otrzymuje toolInput od ChatGPT i wytwarza toolOutput, który będzie dostępny dla widżetu. To jest miejsce, w którym możemy uruchomić dowolną logikę serwera do przetwarzania danych. Zasoby MCP Poniżej przedstawiono, jak zdefiniować zasób 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>` } ] } } ); Zasadniczo „zasób” tutaj zapewnia frontend (widget) część Aplikacji. ui://widget/interactive-quiz.html jest identyfikatorem zasobu i powinien odpowiadać _meta[“openai/outputTemplate”] definicji narzędzia z poprzedniej sekcji powyżej. 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 Tworzenie widgetów Wdrożenie widgetów Teraz przyjrzyjmy się szybko który realizuje widżet (widoczna część aplikacji): Scenariusz 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(); Przypominamy: ten kod zostanie wywołany przez HTML, który zdefiniowaliśmy w zasobie MCP powyżej (tzw. thing). The HTML and the script will be inside of an iframe on the ChatGPT chat page. <script type="module">… ChatGPT exposes some data and hooks via the globalny obiekt. Oto, co używamy tutaj: window.openai window.openai.toolOutput zawiera dane zapytania zwracane przez narzędzie MCP. Początkowo HTML zostanie wyświetlony przed narzędziem zwraca narzędzieOutput, więc window.openai.toolOutput będzie pusty. window.openai.widgetState i window.openai.setWidgetState() pozwalają nam aktualizować i uzyskiwać dostęp do stanu widżetu. Może to być dowolne dane, które chcemy, chociaż zaleceniem jest zachowanie go poniżej 4000 tokenów. window.openai.sendFollowUpMessage ({prompt: “...”}) jest sposobem, aby zwrócić się do ChatGPT tak, jakby użytkownik napisał go, a ChatGPT napisze odpowiedź w czacie. Więcej możliwości można znaleźć w dokumentacji OpenAI tutaj: https://developers.openai.com/apps-sdk/build/custom-ux Umieścić to wszystko razem Czas to przetestować! Szybkie przypomnienie, będziesz potrzebował płatnej subskrypcji ChatGPT, aby włączyć tryb dewelopera. 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 Korzystanie z React Powyżej przyjrzeliśmy się kodowi, który używa prostego JavaScript. , pokazuje, jak wdrożyć aplikację ChatGPT za pomocą React. Quizaurus-reaktywny Tutaj znajdziesz przydatną dokumentację z OpenAI: . https://developers.openai.com/apps-sdk/build/custom-ux/ UżyjOpenAiGlobal helper hooks Można je zobaczyć tutaj, kod jest skopiowany z dokumentacji: https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-react/web/src/openAiHooks.ts Jednym z najbardziej użytecznych jest , który pozwala na subskrypcję aplikacji React do aktualizacji w Pamiętaj, że w zwykłej (nie reaguj) aplikacji powyżej mieliśmy problem, że przycisk „Start Quiz” nic nie robił, dopóki dane nie są gotowe? 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 ... } Gdy narzędzie Output zostanie wypełnione, React automatycznie ponownie wyświetli aplikację i pokaże quiz zamiast stanu ładowania. Reaktywny router Historia nawigacji w iframe, w którym jest wyświetlana aplikacja, jest połączona z historią nawigacji na stronie, dzięki czemu można użyć API routingu, takich jak React Router, do wdrożenia nawigacji w aplikacji. Inne quirki i cechy Uwaga: Rozwój aplikacji ChatGPT nie jest obecnie bardzo stabilny, ponieważ funkcja nie jest w pełni uruchomiona, więc należy spodziewać się niezapowiedzianych zmian API lub drobnych błędów. https://developers.openai.com/apps-sdk How & when ChatGPT decides to show your app to the user 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: Po podłączeniu aplikacji, ChatGPT może użyć go w konwersacji, gdy jest to właściwe.Użytkownik może go wywołać, po prostu wymieniając nazwę aplikacji w tekście, wpisując @AppName lub klikając przycisk + i wybierając aplikację w menu. Wspierane platformy - since it’s implemented via iframe, web is the easiest platform to support and I had almost no issues there. Web Aplikacja mobilna - jeśli podłączysz aplikację w Internecie, powinieneś, być w stanie zobaczyć ją na telefonie komórkowym. nie mogłem uruchomić aplikacji na telefonie komórkowym - nie zadzwonił do narzędzia, ale kiedy uruchomiłem aplikację w Internecie, mogłem z nią komunikować się na telefonie komórkowym. Autentyczność Aplikacje ChatGPT obsługują OAuth 2.1: https://developers.openai.com/apps-sdk/build/auth Jest to duży temat, dajcie znać, czy byłoby pomocne napisać o tym osobny post! Tworzenie żądań sieciowych Oto, co mówi dokument ( ) » » Współpracuj ze swoim partnerem OpenAI, jeśli potrzebujesz określonych domen dozwolonych.” Źródło Standard fetch requests are allowed only when they comply with the CSP W innym miejscu ( Sugeruje to konfigurację Obiekt w definicji zasobu, aby umożliwić domeny: tutaj _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'], } } Kolejną rzeczą, z której możesz skorzystać, jest Twój widget aplikacji (frontend) może go użyć do wywołania narzędzia na serwerze MCP - podajesz nazwę narzędzia MCP i narzędzieDane wejściowe i otrzymasz zwrot narzędziaOutput: window.openai.callTool await window.openai?.callTool("my_tool_name", { "param_name": "param_value" }); Inne cechy frontu Zobacz tę dokumentację na temat tego, co jest dostępne dla Twojego kodu frontend za pośrednictwem : window.openai https://developers.openai.com/apps-sdk/build/custom-ux Możesz uzyskać dostęp do następujących pól (np. informuje, czy ChatGPT jest obecnie w trybie jasnym lub ciemnym): 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; Podobnie można użyć następujących zwrotów (np. aby Twoja aplikacja była w pełnym ekranie): 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>; Dziękuję Ci ! To wszystko, dzięki za czytanie i powodzenia z tym, co budujesz!