MCP (Model Context Protocol) standardizes how tools and data sources integrate with AI clients and models, enabling discoverable capabilities, permissions, and secure execution. OpenAI Apps combine models with tools via MCP to build rich, secure, model-driven applications across Chat Completions and Realtime experiences. This file concatenates all guide markdown to help LLMs ingest full project documentation. # Build OpenAI Apps with React and Vite Learn how to build modern OpenAI Apps using React, Vite, and the Model Context Protocol (MCP). This developer guide explains how to bundle JavaScript and CSS securely, avoid unsafe APIs like readFile, and use React components to create interactive ChatGPT app UIs. OpenAI’s **Apps SDK** (launched Oct 2025) lets you build interactive “apps” that run inside ChatGPT conversations. These apps can surface rich UIs – e.g. inline carousels, maps, or video players – alongside ChatGPT’s responses. Under the hood, the Apps SDK uses the open **Model Context Protocol (MCP)**: your backend (an MCP server) **lists tools** (with JSON schema) and **serves HTML widgets** as needed. When ChatGPT invokes a tool, your server returns structured JSON _plus_ a pointer to an HTML template (“widget”) for the UI. In practice, you register frontend bundles as MCP resources (`mimeType="text/html+skybridge"`) and mark your tool with metadata `"openai/outputTemplate"` so ChatGPT knows which UI to render. For example: ```js // In your Node.js MCP server (using @modelcontextprotocol/sdk) const KANBAN_JS = readFileSync("web/dist/kanban.js", "utf8"); const KANBAN_CSS = readFileSync("web/dist/kanban.css", "utf8"); server.registerResource("kanban-widget", "ui://widget/kanban-board.html", {}, async () => ({ contents: [{ uri: "ui://widget/kanban-board.html", mimeType: "text/html+skybridge", text: `
${KANBAN_CSS ? `` : ""} `.trim(), }] })); server.registerTool("kanban-board", { title: "Show Kanban Board", _meta: { "openai/outputTemplate": "ui://widget/kanban-board.html" }, inputSchema: { tasks: z.string() } }, async () => { // Return structuredContent for the UI plus optional chat text const board = await loadKanbanBoard(); return { structuredContent: { columns: board.columns.map(col => ({ id: col.id, title: col.title, tasks: col.tasks })) }, content: [{ type: "text", text: "Here's your latest board." }], _meta: { tasksById: board.tasksById } }; }); ``` This example (from OpenAI’s docs) shows two key pieces: **registerResource** declares an HTML+JS bundle as a “widget”, and **registerTool** ties a tool to that widget via `_meta["openai/outputTemplate"]`. When ChatGPT calls `"kanban-board"`, it will render your `div#kanban-root` and run the bundled script inside an isolated iframe. ## Building the Frontend UI Apps SDK UIs are essentially web components (typically written in React, but any framework works) that run in a **sandboxed iframe** inside ChatGPT. Your component code uses the `window.openai` global bridge to communicate with ChatGPT. For example, the `window.openai` API provides methods like: - `window.openai.callTool(name, args)` to invoke another tool on your server and await a result. - `window.openai.sendFollowUpMessage({ prompt })` to inject a new message into the chat as if the user sent it. - `window.openai.requestDisplayMode({mode: "fullscreen"})` to ask the host to switch your app from inline to fullscreen or Picture-in-Picture. - `window.openai.setWidgetState(stateObj)` to persist a widget-local state (which the model will see as context). For example, inside a React component you might do: ```js const refresh = async () => { await window.openai?.callTool("refresh_pizza_list", { city: "Berlin" }); }; await window.openai?.sendFollowUpMessage({ prompt: "Summarize these locations in a paragraph." }); ``` These calls let your UI trigger backend actions or chat messages seamlessly. The Docs recommend using hooks to read **globals** from `window.openai`, such as theme or the tool’s current input/output. For instance, a `useToolInput()` hook could subscribe to the `openai:set_globals` event and return `window.openai.toolInput`. This keeps your component reactive to context changes (e.g. new data after a tool call). In short, your front-end acts like any single-page app: it renders data from `window.openai.toolOutput`, updates widget state, and makes calls when needed. ## Why Not Use Raw JS/CSS? You might wonder: can I just write plain HTML, CSS and vanilla JS and serve it? In theory yes, but in practice **complexity and scale require a build tool**. Considerations for OpenAI apps: - **Multiple modules and dependencies:** If you use any npm packages (e.g. React, Axios, Leaflet), you need a bundler to package them into one script. The Apps SDK sandboxed iframe won’t magically resolve your imports. In fact, the troubleshooting guide warns: _“Make sure the HTML inlines your compiled JS and that all dependencies are bundled.”_[22] This means you must ship a single JS file (with styles) for your widget. Tools like Vite or Webpack crawl your `import` statements and pack everything together. Without bundling, you’d face missing scripts or CSP blocks. - **Modern JavaScript:** Apps are typically built with ES modules, JSX/TSX, or modern JS syntax. Browsers (even with `type="module"`) don’t support JSX out of the box. A compiler/transpiler (part of a bundler toolchain) converts your JSX/TS into plain JS that the ChatGPT iframe can execute. - **Optimizations:** Bundlers minify code, tree-shake unused bits, and generate hashed filenames for caching. The official examples use hashed bundles so updates are cached busts . This is crucial for production performance and ensuring users get the latest code. Simply serving raw JS files would lead to caching headaches and larger payloads. - **Development Experience:** Without a dev server, you’d have to manually rebuild and reload. Vite (for example) provides a **hot-reload dev server**, TypeScript support, and fast startup. In large projects, building everything each change is slow. Vite addresses this by pre-bundling dependencies (using [esbuild] under the hood) up to 10–100× faster than older bundlers. This makes iterative development much smoother. In short, an Apps SDK project is just a web app (with a special runtime), so you benefit from the same frontend tooling best practices. The OpenAI docs imply this: they assume your UI is a compiled bundle (see examples and GitHub repo). For instance, OpenAI’s sample repo uses a **Vite** setup with multiple entry points – one JS/CSS per widget – built via a `build-all.mts` script. That script produces versioned `.html`, `.js`, and `.css` assets for each component, which are then either served by your server or uploaded to a CDN. The readme shows commands like `pnpm run dev` (to start Vite’s dev server) and `pnpm run build` (to produce hashed bundles). ## Setting Up Vite for an AI App To illustrate, a typical Vite workflow might be: 1. **Project structure:** Have a frontend folder (e.g. `web/`) where your React/Vue code lives, and a backend folder for the MCP server. In `web/`, write your components (JSX/TSX, CSS/SCSS, etc). 2. **Vite config:** Configure Vite with multiple HTML entry points (one per widget) or use dynamic import. You might use `vite-plugin-mpa` or manually specify `build.rollupOptions.input`. The OpenAI example uses a custom `vite.config.mts` to handle multiple widget outputs. 3. **Dev server:** Run `vite` (often via `npm run dev`). Vite will launch on a localhost port (e.g. 5173). You can then point your MCP server or ChatGPT dev connector to fetch widget HTML/JS from this origin. The example repo serves built assets with CORS enabled on port 4444 for testing. 4. **Building:** Run `vite build` (or `npm run build`). This bundles and outputs each widget’s assets into e.g. `dist/`, with hashed filenames. The example uses `build-all.mts` to run Vite for each entry. After building, your server code can `readFileSync` or otherwise load those `.js` and `.css` files (as shown above). **Example Vite config snippet:** (this is illustrative; see the [openai-apps-sdk-examples](https://github.com/openai/openai-apps-sdk-examples) repo for full config) ```js // vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { // Suppose each widget has its own HTML in web/src/widgets/ rollupOptions: { input: { "pizza-map": "src/widgets/pizza-map.html", "pizza-carousel": "src/widgets/pizza-carousel.html", // ...other widgets }, output: { entryFileNames: '[name]-[hash].js', assetFileNames: '[name]-[hash].[ext]' } } } }); ``` With this, running `vite build` generates `pizza-map-[hash].js`, `pizza-map-[hash].css`, etc. Your server then includes those in the MCP resource HTML. This automated bundling (with dependency resolution and hashing) would be extremely tedious by hand with raw JS/CSS. ## Security and Best Practices **Network & CSP:** Frontend widgets run in a **strict sandbox**. The Apps SDK enforces a Content Security Policy: you can only `fetch` to allowed domains and can’t use privileged APIs (`alert`, `prompt`, clipboard, etc). If your UI needs to fetch data, you must allow-list that domain in the resource’s `_meta.widgetCSP.resource_domains`. For example, using the OpenAI example: ```js _meta: { "openai/widgetCSP": { connect_domains: [], resource_domains: ["https://persistent.oaistatic.com"] } } ``` This lets your iframe load scripts/images from that domain. Always validate inputs server-side; don’t trust the model or user input to generate safe URLs or file paths. The docs stress **least privilege** and **input validation**: only request the scopes and permissions you need, and thoroughly validate all tool arguments to guard against injection attacks. **Avoid insecure file reads:** The Node code example above uses `readFileSync` to load your built JS/CSS. This is fine for *your* own static assets, but be cautious: never use `readFile` on paths derived from user input, as it could allow directory traversal or reveal sensitive files. In general, static bundling is safer – consider **serving assets via HTTPS or CDN** instead of reading them dynamically. (OpenAI’s sample also shows linking to a public CDN domain `persistent.oaistatic.com` for assets; in your app you’d host on your domain or a cloud bucket.) If you need the user to upload a file, use a controlled upload endpoint and parse it on the server, rather than trying to `readFile` on the client. Browser JavaScript **cannot** arbitrarily read local files due to security; you’d use an `` with user consent if needed. **Data & Logging:** By default, anything sent via `setWidgetState` is visible to the model, so keep that payload small (docs suggest <4k tokens) and free of secrets. Avoid logging raw prompts or sensitive data. Follow the guide’s advice: redact PII in logs, honor deletion requests, and require user confirmation for any write/delete tools. ## Vite Benefits: Bundling, Dependencies, Dev DX In practical terms, using a tool like Vite greatly enhances development: - **Bundling & Caching:** Vite/Rollup bundles all your JS, CSS, and assets into optimized files. It also supports code-splitting and hashing. For example, OpenAI’s examples repo shows each widget “wrapped with the CSS it needs so you can host the bundles directly”. The hashed filenames (`app-83d4f6.js`) ensure ChatGPT loads the latest version (cache-busting). Doing this manually (managing ` `.trim(), }, ], }) ); ``` This tells the MCP server: when ChatGPT needs the `pizza-map.html` template, return the HTML snippet (wrapping `
` and needed scripts). The template URI (`ui://widget/pizza-map.html`) matches what the tool metadata will reference. - **Tools:** Each tool has a **name**, a JSON schema for inputs, and a **handler** that returns a result. For instance, the “Show Pizza Map” tool: ```ts server.registerTool( "pizza-map", { title: "Show Pizza Map", inputSchema: { pizzaTopping: z.string() }, _meta: { "openai/outputTemplate": "ui://widget/pizza-map.html", "openai/toolInvocation/invoking": "Flipping a map", "openai/toolInvocation/invoked": "Here’s the pizza map!" } }, async (args) => { // In a real app, you might use args.pizzaTopping to filter data. return { content: [{ type: "text", text: "Rendered a pizza map!" }], structuredContent: {} }; } ); ``` Here, the tool name `pizza-map` and its JSON schema (a `pizzaTopping` string) are advertised. The `_meta.outputTemplate` points to our UI resource. When the tool is called, the handler returns a text message and (optionally) structured JSON. The Apps SDK will pair this with the **pizza map widget** in ChatGPT’s UI. In summary, each tool registration ties a **tool ID** (like `pizza-carousel`) to a **UI template** (`ui://widget/pizza-carousel.html`) and a handler function. ChatGPT’s assistant knows from the schema when to call which tool, and once called, the `_meta.openai/outputTemplate` metadata tells it which embedded component to show. ### Pizzaz UI Components (Map, Carousel, List) The Pizzaz example uses several custom UI components. In ChatGPT, these appear inline with the conversation when their tool is invoked. For example, the **map component** shows pizza restaurant locations on a map. When a user asks “show me pizza places in San Francisco,” ChatGPT might trigger the `pizza-map` tool and render the map widget. Below is a screenshot of the Pizzaz map UI as it appears in ChatGPT: *Screenshot: The Pizzaz map widget shows pizza spots on a Mapbox-powered map (integrated via the Apps SDK). When the “Show Pizza Map” tool is used, ChatGPT displays this interactive component in-line.* Pizzaz also includes a **carousel widget** – a horizontal scrollable gallery of images of pizza spots. Invoking the `pizza-carousel` tool renders this UI. For example: *Screenshot: The Pizzaz carousel widget displays a gallery of pizza restaurants (with navigation arrows). It’s rendered when the “Show Pizza Carousel” tool is called.* Another example is the **list widget**, which shows a ranked list of pizzerias. When the `pizza-list` tool is used, ChatGPT shows something like this: *Screenshot: The Pizzaz list widget, showing a top-7 pizza list with images, ratings, and save buttons. This component is registered by the MCP server and injected via the output template metadata.* Each of these UI components is built with standard web tech (React, HTML, CSS, etc.) but is “wrapped” by the Apps SDK so ChatGPT can render them natively. The MCP server simply points to the prebuilt HTML/JS bundle for each component (from `assets/`), and ChatGPT takes care of displaying it. ### How It All Fits Together When a user interacts with ChatGPT, the flow is: ChatGPT sees a user request, decides one of your tools matches the intent, and issues a `call_tool` via MCP. For example, user: *“Show me pizza shops near me.”* → model triggers `pizza-map`. The MCP server runs the corresponding handler (which could call external APIs or databases for real data) and returns content + metadata. Because we set `"openai/outputTemplate": "ui://widget/pizza-map.html"`, ChatGPT then renders the map widget with the (HTML+JS) we served. The chat continues with structured JSON available for the model to reference if needed. In the Pizzaz Node code, most handlers are placeholders returning static text (“Rendered a pizza carousel!”). In a real app, you’d replace those with actual logic (e.g. call Yelp or Google Maps APIs). The beauty of the Apps SDK is you can mix *tools* (logic) with *components* (UI) seamlessly. ## Deploying the App Once your MCP server is working locally, you can connect it to ChatGPT. In development, enable **Developer mode** in ChatGPT and use a tool like [ngrok](https://ngrok.com) to tunnel to your local server. For example: ```bash ngrok http 8000 ``` Ngrok will give you a public HTTPS URL (e.g. `https://abc123.ngrok.io/mcp`) that you can add as a custom connector in ChatGPT’s **Settings > Connectors**. ChatGPT will then send MCP requests to your local server through the tunnel. For production, host your MCP server behind a stable HTTPS endpoint. Suitable platforms include cloud services like Fly.io, Render, Railway, or Google Cloud Run. Make sure your server’s `/mcp` endpoint supports streaming HTTP (needed for partial results) and proper status codes. With a public URL, simply add the connector in ChatGPT and your app is live. ## Tips: Expanding the App and Use Cases After following this tutorial, you have a working ChatGPT app connector. Here are some ways to expand it or apply it: - **Add real data sources:** Replace the placeholder handlers in `pizzaz_server_node/src` with real API calls. For example, fetch live restaurant data based on `pizzaTopping` or user’s location. - **Build new tools:** Define additional tools in MCP for other features (e.g. an “order pizza” action, or a “suggest restaurants” tool). Each can have its own UI. - **Create custom widgets:** Design your own React/HTML components and add them to the `src/` gallery. The build system will bundle them to `assets/` automatically. - **Integrate Auth or Persistence:** Use Apps SDK’s support for OAuth 2.0 if you need user-specific data or login. Add state persistence (e.g. save favorites) by connecting a database. - **Potential use cases:** Think beyond pizza! The same pattern works for any domain. For example, a travel planner could use a map widget for attractions, a list for itineraries, and tools to fetch flight/hotel info. A finance assistant might use graphs or tables as widgets. - **Follow design guidelines:** Ensure your conversational flows feel natural in ChatGPT and follow the [official guidelines](https://platform.openai.com/docs/apps/guidelines) for UX and safety. Rich metadata (titles, descriptions) helps ChatGPT know when to launch your tool. The key takeaway is that the OpenAI Apps SDK (with MCP) lets you **build AI apps with OpenAI** by defining tools + components. The Pizzaz Node example is a tutorial scaffold – use it as a template. Experiment with it, read the comments in the code, and adapt the architecture to your needs. For more details, refer to OpenAI’s documentation and the Apps SDK examples. Happy building! **Sources:** The above tutorial references the official OpenAI Apps SDK and MCP documentation and examples. The Pizzaz example is from the OpenAI [Apps SDK Examples GitHub](https://github.com/openai/openai-apps-sdk-examples) repository. The embedded screenshots are from OpenAI’s Apps SDK docs. --- # Integrating Next.js with OpenAI Apps: A Comprehensive Guide Guide on integrating OpenAI Apps into a Next.js app. We cover how to set up an MCP server, define tools, and bundle React components for use as widgets. **OpenAI Apps** turn ChatGPT into an “app platform” where third-party services can run inside conversations. Built on the open **Model Context Protocol (MCP)**, these apps let you combine chatbot prompts with rich, interactive UIs (widgets) powered by your own code. MCP is *“an open specification for connecting large language model clients to external tools, data, and user interfaces”*. In practice, this means you run an MCP server (in Node, Python, etc.) that advertises *tools* the model can call. Each tool can return structured data plus HTML/JS for a React component, and ChatGPT will automatically render that UI inline. *Figure: ChatGPT conversation with an integrated Booking.com App (hotel search). OpenAI Apps blend chat and interactive UI: the model calls the Booking.com tool and displays hotel listings directly inside ChatGPT’s interface.* Below we explain the concepts of OpenAI Apps and MCP, then walk through integrating them into a Next.js app. We cover how to **set up an MCP server**, **define tools**, and **bundle React components** for use as widgets. We include sample code, architecture tips, and best practices. ## What Are OpenAI Apps and MCP? **OpenAI Apps** are interactive tools that run *inside* ChatGPT conversations. Users can invoke an app by name or let ChatGPT suggest it when relevant (e.g. using a Zillow app for real estate or a Spotify app for playlists). Each app responds to natural-language queries and can include a fully custom UI (maps, charts, etc.) that appears right in the chat. The **Apps SDK** is the framework OpenAI provides to build these apps. It’s built on MCP – an open standard that makes LLMs connect to external tools and UIs. In short, MCP is like a “USB-C port” for AI apps: it standardizes how you advertise tools (name, schema, UI) and return data so ChatGPT knows what to do. As OpenAI puts it, the MCP server is *“the foundation of every Apps SDK integration”*: it **exposes tools that the model can call** and packages the structured data plus the HTML/JS of your widget. The Apps SDK uses this to “keep the server, model, and UI in sync”. A minimal MCP integration (and thus an OpenAI App) typically does three things: - **List tools:** Your server advertises the tools it supports, including their JSON-schema inputs/outputs. This tells ChatGPT what actions your app can perform. - **Call tools:** When the model decides to use a tool, it sends a `call_tool` request with arguments. Your MCP server receives this, executes the action (e.g. fetch data), and returns the result. - **Return widgets (UI):** Along with the result data, you supply an embedded widget by registering an HTML/JS resource in your MCP server. The server returns a reference (template URI) to that UI. ChatGPT then renders the widget inline in the chat, hydrating it with your structured data. In other words, OpenAI Apps let you **“design both the logic and interface of [chat-based] applications”**. You write normal server code (Node, Express, Next.js, etc.) to handle tool calls, and you build React components (bundled to static JS/CSS) for the UI. The MCP standard takes care of routing everything through ChatGPT. ## Why Use Next.js for OpenAI Apps? Next.js is a popular React framework for building full-stack web apps. It fits OpenAI Apps well because: - **Universal Stack:** You can use Next.js for both the server (API routes) *and* the UI component. Your MCP server code can live in a Next.js API route or custom server, and your widget can be a React component in your app. - **TypeScript & SDKs:** Next.js supports TypeScript, matching the official MCP SDK (`@modelcontextprotocol/sdk`) which works great in Node/React environments. - **Build Pipeline:** Next.js has built-in bundling (with Vite or Webpack) to compile React components into static assets. You can export your widget HTML/JS from a Next.js build and serve it in the MCP server. - **Deployment:** You can deploy to Vercel or any Node host with HTTPS, which ChatGPT requires. Vercel even has an MCP adapter to simplify this integration (see [this Vercel template](https://vercel.com/templates/next.js/model-context-protocol-mcp-with-next-js)). By integrating OpenAI Apps into Next.js, you leverage familiar tools (pages, API routes, hooks) and keep everything in one project. The guidance below assumes a Next.js (Node/React) environment, but the MCP concepts apply to any stack. ## Getting Started: Prerequisites Before building your OpenAI App, ensure you have: - **Node.js (18+)** and npm/yarn/pnpm. - **Next.js** (latest version) with a new or existing project. - **TypeScript** (optional but recommended) for defining tool schemas. - **@modelcontextprotocol/sdk:** the official MCP TypeScript SDK (install via `npm install @modelcontextprotocol/sdk`). - (Optional) **React** 18 or 19 for building components, though Next.js includes React already. In your Next.js project folder, install the MCP SDK: ```bash npm install @modelcontextprotocol/sdk ``` This gives you the `McpServer` class to define your tools, and client transports to connect to MCP servers. ## Building the MCP Server Your **MCP server** is the core of the app. It handles incoming calls from ChatGPT, executes logic, and returns data + UI. In Next.js, a common approach is to create an API route (e.g. `pages/api/mcp.ts` or using the App Router). This route will accept GET/POST from ChatGPT. Here’s a simple example using the MCP SDK in a Next.js API route (TypeScript): ```ts // pages/api/mcp.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { NextApiRequest, NextApiResponse } from 'next'; // Create the MCP server instance const server = new McpServer({ name: 'my-nextjs-app', version: '0.1.0' }); // (We will register resources and tools here...) // Export a handler that calls the MCP server on each request. export default function handler(req: NextApiRequest, res: NextApiResponse) { // MCP SDK provides a handler we can use directly return server.handle(req, res); } ``` In this snippet, `McpServer` is initialized with a name and version. You then register **resources** (UI templates) and **tools** on this server. Finally, exporting `server.handle(req, res)` wires up the MCP endpoint. ChatGPT will talk to this endpoint (e.g. https://yourdomain/api/mcp) via HTTP streaming (SSE or Long-polling). > **Tip:** If you use Next’s App Router, you can similarly create a Route Handler under `app/api/mcp/route.ts`. The [Vercel template](https://vercel.com/templates/next.js/model-context-protocol-mcp-with-next-js) uses a helper to mount an MCP server on a route. But the principle is the same: route all requests to your `McpServer`. ### Installing and Choosing the Apps SDK Although everything rests on MCP, you might see references to the “Apps SDK” in docs. The Apps SDK is not a separate npm package — it’s essentially ChatGPT’s integration layer that expects MCP. For the server-side, installing `@modelcontextprotocol/sdk` is sufficient. You don’t need a client-side SDK; the ChatGPT host will render your HTML+JS automatically. OpenAI does provide a [full Apps SDK GitHub repo with examples](https://github.com/openai/openai-apps-sdk-examples). It shows widget examples and matching servers. Use these as inspiration for UI patterns. ## Defining Tools for Your App **Tools** are the endpoints or actions your app offers. Think of each tool like an API: it has a *machine name*, a *human-friendly title*, input arguments, and output schema. When ChatGPT’s model decides to use your app, it will call one of these tools. Using the MCP server, register tools like so: ```ts import { z } from 'zod'; // Example: a simple "Hello World" tool server.registerTool( 'hello-tool', // internal name of the tool { title: 'Say Hello', // human title description: 'Returns a greeting from this app', // Schema for input arguments (here none expected) inputSchema: {}, // Metadata with the UI template we'll set up _meta: { 'openai/outputTemplate': 'ui://widget/hello.html' } }, async (args) => { // Logic for the tool: args is the parsed JSON input return { // Text that ChatGPT will see and read aloud, etc. content: [{ type: 'text', text: 'Hello from my OpenAI App!' }], // Structured data to pass to the UI widget structuredContent: { greeting: 'Hi there!' } }; } ); ``` Key points: - The first argument to `registerTool` is the tool name (`hello-tool`), used internally. - The second arg is the descriptor: `title`, `description`, and `inputSchema` (use Zod or JSON schema). - In `_meta`, we set `"openai/outputTemplate"` to the URI of our UI (to be registered as a resource). - The third arg is an async function that executes the tool. It returns an object with: - `content`: optional chat text (array of text segments) shown to user by ChatGPT. - `structuredContent`: JSON data fed to the widget’s JS. - Optionally `_meta`: extra data only for the widget (not visible to model). According to OpenAI, a well-defined tool integration should *“advertise its JSON Schema… so the model knows when—and how—to call each tool”*. In practice, this means your tool definitions (and their Zod schemas) help ChatGPT plan when to invoke your app. **Example:** Let’s define a “Weather Check” tool. ```ts server.registerTool( 'check-weather', { title: 'Check Weather', description: 'Gets current weather for a city', inputSchema: z.object({ city: z.string() }).optional(), _meta: { 'openai/outputTemplate': 'ui://widget/weather.html' } }, async ({ city }) => { // (Call a real weather API here) const data = await fetchWeatherForCity(city); return { content: [{ type: 'text', text: `Weather in ${city}: ${data.summary}, ${data.temperature}°C` }], structuredContent: { forecast: data.forecast } }; } ); ``` This tool takes a city name and would, for instance, return a text summary plus structured forecast data for a UI chart. Note how `'openai/outputTemplate'` points to `ui://widget/weather.html`, which we will register next. ## Building the React UI Component Each tool can be accompanied by a custom UI (widget). In OpenAI Apps, UI components are standard web code (HTML, JS, CSS). The ChatGPT client will embed this code in an iframe and expose some APIs via `window.openai`. Your React component runs inside that iframe. **Project layout:** Keep server and web code separate. A common structure is: ``` /app /server # Next.js (or Node) MCP server code (tools, resources) /web # Frontend for widgets (React components) /src Widget.tsx /dist widget.js # built JS for widget widget.css ``` In Next.js you can use its build process (or Vite) to bundle your React component(s). The built output will include a JS file that mounts into an HTML `div`. In your widget source (e.g. `Widget.tsx`), you *mount into a div and read initial data from `window.openai.toolOutput`*. For example: ```tsx import React from 'react'; import { createRoot } from 'react-dom/client'; function WeatherWidget() { const data = (window as any).openai.toolOutput; // structuredContent from MCP return (

Forecast

{data.forecast.summary}

{/* more rendering... */}
); } // Entry point: attach to root element const container = document.getElementById('weather-widget-root'); if (container) { createRoot(container).render(); } ``` Our HTML template (to be served by the MCP server) would have something like: ```html
``` Here, `weather-widget.js` is the compiled bundle (ES module). The widget uses `window.openai.toolOutput` as the data passed from the server, as recommended in OpenAI’s docs. The MCP server will inject the `structuredContent` JSON into the iframe for you. ### Registering the Widget Resource After bundling your widget (e.g. via `npm run build`), you register its HTML/JS/CSS as a **resource** in the MCP server. This makes the widget available at a URI. For example: ```ts import fs from 'node:fs'; const HTML = fs.readFileSync('./public/weather.html', 'utf8'); const JS = fs.readFileSync('./public/weather-widget.js', 'utf8'); const CSS = fs.readFileSync('./public/weather-widget.css', 'utf8'); // Register the widget resource server.registerResource( 'weather-widget', 'ui://widget/weather.html', {}, async () => ({ contents: [ { uri: 'ui://widget/weather.html', mimeType: 'text/html+skybridge', text: `
${CSS ? `` : ''} `.trim() } ] }) ); ``` This code tells the MCP server to serve a resource at `ui://widget/weather.html`. The `text` field is the HTML content for the widget, including your JS and inline CSS. ChatGPT will fetch this when rendering the UI. In practice, you might host these files statically or embed them as above. The key is that your tool descriptor used `_meta["openai/outputTemplate"] = "ui://widget/weather.html"`. When ChatGPT processes a tool response, it sees that template URI and knows to show your widget. You can see this pattern in the official docs. ## Linking UI with MCP Tools Once your resources and tools are registered, ChatGPT can use them together. When a user triggers your app, ChatGPT calls the tool on your MCP server. Your server returns: - `content`: free-form text for ChatGPT to display. - `structuredContent`: a JSON object (the “model sees this”). - `_meta.openai/outputTemplate`: already set on the tool, which tells ChatGPT *which* UI to embed. ChatGPT then creates an iframe for your widget at the bottom of the response. It injects your structured data into `window.openai.toolOutput` inside the iframe. Your React code (mounted on a div) reads that data and renders appropriately. For example, a tool response might be: ```json { "content": [{ "type": "text", "text": "Here's the latest weather:" }], "structuredContent": { "forecast": { "summary": "Sunny", "temp": 25 } }, "_meta": { /* optional extra for widget */ } } ``` Since `_meta.openai/outputTemplate` was set to your widget URI, ChatGPT will render: ```html
``` and then your `WeatherWidget` component reads `{ forecast: { ... } }` from `window.openai.toolOutput` to display the UI. This is exactly how the *Kanban Board* example works in the OpenAI docs: the server registers a `kanban-board.html` with a `
` and script, then returns `structuredContent` with tasks to be rendered by the widget. In summary, to inject React components: 1. **Register a HTML+JS resource** in your MCP server (`registerResource`) with a unique `uri` like `ui://widget/mycomponent.html`. 2. **Reference that URI in your tool’s `_meta.openai/outputTemplate`**. 3. **Return structuredContent from the tool**; the Apps SDK will hydrate your widget with that data. 4. **Mount your React component** into a root div in the HTML. Use `window.openai.toolOutput` inside your code to get the data. ## Testing and Deployment To test locally, run your Next.js app (port 3000 by default). The MCP endpoint (e.g. `http://localhost:3000/api/mcp`) needs to be accessible via **HTTPS** to ChatGPT. During development, use a tunnel like **ngrok**: ```bash ngrok http 3000 # Suppose it gives https://xyz123.ngrok.app ``` Then in ChatGPT Developer Mode, create a new *ChatGPT integration* (Connector) pointing to your server URL (e.g. `https://xyz123.ngrok.app/api/mcp`). ChatGPT will fetch your tool list (`List tools` operation) and let you call them. You can also try the [MCP Inspector](https://modelcontextprotocol.io) as an in-browser tester: point it at your `/mcp` endpoint and invoke tools manually. For production, deploy to an HTTPS environment. Vercel (Next.js) or any cloud VM with a certificate works. Make sure your MCP endpoint is fast to avoid timeouts in ChatGPT. See OpenAI’s [Deploy your app](https://developers.openai.com/apps-sdk/deploy) docs for more. ## Sample Architecture A simple architecture for a Next.js-based OpenAI App might look like: - **Next.js server** - **API route `/api/mcp`** runs an `McpServer`. - Tools and Resources are registered here (using `@modelcontextprotocol/sdk`). - Calls to external APIs (e.g. databases, other services) happen here. - **React widget code** - Lives in `components/` or a separate `web/` directory. - Built into static files (JS/CSS) and served by the MCP server via `registerResource`. - Communicates via `window.openai` APIs inside the iframe (e.g. `toolOutput`, `callTool`, `sendFollowupTurn`). - **ChatGPT client** - When user invokes an app, it calls the MCP server for data and widget code. - Renders chat text and the iframe with your component. ```plaintext [User ChatGPT] --calls--> [OpenAI ChatGPT Model] --makes API calls--> [Your MCP Server @ Next.js] --serves UI resources--> <--structured content, HTML-- UI rendered in ChatGPT <-- widget JS/CSS -- ``` ## Use Cases and Best Practices **Use Cases:** OpenAI Apps can serve many scenarios. For example, you might build: - A **travel planner** app: integrate Booking.com or Expedia, let users search hotels/flights with interactive calendars and maps. - A **data visualization** app: plug into a database or API, show charts or dashboards inline. - A **productivity tool**: e.g. a Kanban board, to-do list, or timeline inside chat. - A **shopping assistant**: browse products from a store’s catalog with clickable images. - **Financial tools**: stock quote lookup, calculator, or personal finance manager. - **Educational aids**: language practice, math solvers with step-by-step UI. Essentially, any service that benefits from conversational UI *plus* a visual interface can be an OpenAI App. **Best Practices:** - **Design for conversation:** Your app should fit seamlessly in chat. Provide text responses (`content`) along with your UI. Don’t overwhelm with UI; let the model also summarize key info. - **Efficient structuredContent:** Only send data the UI needs. ChatGPT can see `structuredContent`, so don’t include sensitive info there. Use `_meta` for hidden data. - **Version your UI:** ChatGPT aggressively caches widget URIs. When you update your JS/CSS, bump the resource URI (e.g. add a version in the name) to avoid stale content. - **Security (CSP):** Widgets run in iframes with strict CSP. Define a content security policy (using `openai/widget` meta) so ChatGPT can inspect it. Allow only necessary sources/scripts. - **Authentication:** If your app has user-specific data, implement auth (OAuth 2.0, JWT, etc.). The Apps SDK guides have sections on adding user identity. ChatGPT handles initial “connect” prompts. - **Widget accessible:** If your widget needs to *call tools on its own* (e.g. user clicks a button to do something), set `_meta.openai.widgetAccessible: true` on those tools. - **Optimize performance:** Keep payloads small and code lean. Large widgets or slow APIs can make the chat lag. The docs recommend keeping dependency bundles small. - **Follow design guidelines:** OpenAI’s [developer guidelines](https://developers.openai.com/apps-sdk/design) outline quality and safety standards. Provide clear UX, handle errors, and follow usage policies. By following these practices and leveraging MCP, your Next.js app can become a fully interactive OpenAI App. Developers have already done similar integrations (see [OpenAI’s example repo](https://github.com/openai/openai-apps-sdk-examples) and the Vercel MCP template). ## Conclusion Integrating Next.js with OpenAI Apps involves running an MCP server in your Next.js backend and building React widgets for the front-end. The steps are: 1. **Set up an MCP server** (e.g. a Next.js API route) using `@modelcontextprotocol/sdk`. 2. **Define tools** with names, schemas, and link each to a UI template via `_meta["openai/outputTemplate"]`. 3. **Build React components** for your widgets, bundle them, and register the resulting HTML/JS in the MCP server. 4. **Return structured data** from tools so ChatGPT can hydrate the widgets. 5. **Test via Developer Mode** (using ngrok or a live URL) and then deploy to production with HTTPS. This guide has covered the core concepts of **OpenAI Apps**, **MCP**, and how to wire them into a **Next.js** project. As OpenAI notes, the Apps SDK “extends MCP so developers can design both the logic and interface of their apps”. By following the steps above, you can create rich chat applications that run inside ChatGPT, leveraging the best of conversational AI and web UIs. **References:** This guide draws on OpenAI’s official docs and community resources. See OpenAI’s [Apps SDK documentation](https://developers.openai.com/apps-sdk/), the [openai-apps-sdk-examples](https://github.com/openai/openai-apps-sdk-examples) repo, and recent announcements for more details. ---