数週間前、OpenAIはChatGPT用のアプリを導入しました. 以下の通り、企業はユーザーの要望を満たすために、その製品をチャットに直接注入することを可能にします。 An app can be triggered either by an explicit mention or when the model decides that the app is going to be useful. So, what is a ChatGPT App? 顧客にとっては、テキストインターフェイスの制約を超えてより豊かなユーザー体験と機能を提供する方法です。 ビジネスにとっては、正しいタイミングで8億人以上のChatGPTユーザーにアクセスする方法です。 , it's an a <= that’s what we’re here to talk about! For a developer MCP server and web app that runs in an iframe デモ この記事では、以下に示すシンプルなクイズアプリを作成し、それを例として使用して利用可能な機能を示します。 重要な注意:フォローしたい場合は、開発者モードを有効にするために有料のChatGPTサブスクリプションが必要です。 重要な注意:フォローしたい場合は、開発者モードを有効にするために有料のChatGPTサブスクリプションが必要です。 High-level flow これは、高レベルでどのように機能するかです(実際のステップの順序は少し異なります): まず、アプリの開発者は ChatGPT 内でアプリを登録します。 that implements the app . stands for , and it allows models like ChatGPT to explore and interact with other services. 私たちのMCPサーバは、 」「そして」 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. アプリが既に存在し、ユーザーがプロンプトを作成する場合 like “Make a quiz about Sam Altman”, ChatGPT will check if there is an App it can use instead of a text response to provide better experience to the user . (2) (3) アプリが見つかった場合、ChatGPTはアプリが必要とするデータのスケジュールを調べます。 私たちのアプリは、以下のJSON形式でデータを受け取る必要があります。 (4) { questions: [ { question: "Where was Sam Altman born", options: ["San Francisco", ...], correctIndex: 2, ... }, ... ] } これが呼ばれる , and will send it to our app. 私たちのアプリに送信します。 . ChatGPT will generate quiz data exactly in this format toolInput (5) App が処理する 生産するであろう。 ChatGPTは、チャットウィンドウでアプリが提供するHTMLの「リソース」を表示し、それを初期化します。 データ そして最後に、ユーザーはアプリを見ることができ、それと相互作用することができるようになります。 . toolInput toolOutput toolOutput (6) (7) MCPサーバーの構築 私たちのChatGPTアプリのコードレポ: . https://github.com/renal128/quizaurus-tutorial 2つのプロジェクトがあります: and まず、我々は焦点を フロントエンドで単純なJavaScriptを使用して物事をシンプルにします。 quizaurus-plain quizaurus-react quizaurus-plain サーバーのすべてのコードはこのファイルにあります - コードの約140行! https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-plain/src/server.ts サーバー設定 ここに記載されている SDK のいずれかを使用して MCP サーバを作成するには、多くのオプションがあります。 https://modelcontextprotocol.io/docs/sdk Here, we will use the . 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); }); キーポイント: エクスプレスアプリは、外部から(ChatGPTから)通信(HTTPリクエストなど)を受け取る一般的なサーバーです。 Express を使用すると、私たちは ChatGPT に、MCP サーバのアドレスとして提供する /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(...) and mcpServer.registerResource(...) is what we will use to implement our クイズ アプリ MCPツール Let’s fill in the gap in 上の「ツール」を登録する mcpServer.registerTool(…) ChatGPT は、アプリを登録したときにツールの定義を読み、ユーザーがそれを必要とするときに ChatGPT はツールを呼び出し、クイズを開始します。 // 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 がツールに提供するために必要なデータを正確に伝える方法であり、ツールインプット(toolInput)を正しく準備するために ChatGPT が使用できるヒントと制限を含む方法です。 is omitted here, but you can provide it to tell ChatGPT what schema will have. outputSchema structuredContent したがって、ある意味では、ツールはここでChatGPT Appを定義するものです。 Let’s look at the other 2 fields here: 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”] is the function that receives from ChatGPT and produces that will be available to the widget. This is where we can run any server-side logic to process the data. In our case, we don’t need any processing because already contains all the information that the widget needs, so the function returns the same data in which will be available as to the widget. async (toolInput) => { … toolInput toolOutput toolInput structuredContent toolOutput MCPリソース Below is how we define an MCP resource: // 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>` } ] } } ); Basically, “resource” here provides the frontend (widget) part of the App. is the resource ID, and it should match of the tool definition from the previous section above. 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」の構築 ウィジェット実装 では、早速、見ていきましょう。 ウィジェット(アプリの目に見える部分)を実装する: 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(); Reminder: this code will be triggered by the HTML that we defined in the MCP resource above (このコードは、上記のMCPリソースで定義したHTMLによって引き起こされます。 HTMLとスクリプトは、ChatGPTチャットページのiframeの内部にあります。 <script type="module">… ChatGPTは、いくつかのデータとハックを、 グローバルオブジェクト. Here’s what we’re using here: window.openai window.openai.toolOutput には、MCP ツールによって返される質問データが含まれています. 最初は、ツールが toolOutput を返す前に、HTML がレンダリングされますので、 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 を使う 上記では、JavaScriptを使ったコードを調べてみました. The other project in the same repo, , React を使用して ChatGPT アプリを導入する方法を示しています。 クイザウス・レイク You can find some helpful documentation from OpenAI here: . https://developers.openai.com/apps-sdk/build/custom-ux/ OpenAiGlobal Helper ホークス ここで彼らを見ることができます、コードは文書からコピーされています: https://github.com/renal128/quizaurus-tutorial/blob/main/quizaurus-react/web/src/openAiHooks.ts The most useful one is , which allows you to subscribe the React app to the updates in アプリケーションの更新にサブスクリプトする . Remember that in the plain (non-React) app above, we had the issue that the “Start Quiz” button wasn’t doing anything until the data is ready? Now we can improve the UX by showing a loading animation: 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 ... } When toolOutput gets populated, React will automatically re-render the app and will show the quiz instead of the loading state. React Router アプリが表示される iframe のナビゲーション履歴は、ページのナビゲーション履歴に接続されているため、React Router などのルーティング API を使用してアプリ内のナビゲーションを実装できます。 Other quirks and features(その他のクィークと特徴) 注: ChatGPT アプリの開発は現時点で非常に安定していないので、この機能は完全に展開されていないため、APIs の未発表の変更や小さなバグを期待することは公平です。 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: 接続されたアプリを起動する. 一度アプリが接続されると、ChatGPTは適切な場合に会話でそれを使用することができます. ユーザーは、テキストにアプリ名を言及し、 @AppName を入力し、または + ボタンをクリックし、そこにあるメニューでアプリを選択することで、それを押すことができます. サポートプラットフォーム - since it’s implemented via iframe, web is the easiest platform to support and I had almost no issues there. 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 他の場所( )を構成することを示唆する。 object in the resource definition to enable your domains: リソースの定義で、あなたのドメインを有効にする: here _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'], } } あなたが使えるもう一つのものは、 あなたのアプリウィジェット(フロントエンド)は、MCP サーバー上のツールを呼び出すために使用できます - あなたは、MCP ツールの名前とツールの入力データを提供し、バックツールの出力を受け取ります: window.openai.callTool await window.openai?.callTool("my_tool_name", { "param_name": "param_value" }); 他のフロントの特徴 このドキュメントを参照して、フロントエンドのコードに利用可能なものをご覧ください。 : : 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; 同様に、以下のコールバックを使用できます(例えば、試してみてください。 あなたのアプリをフルスクリーンにするには): 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>; Thank you! それだけ、読んでくれてありがとう、あなたが作っているものに幸運を!