/

MCP

Inside OpenAI’s Apps SDK: how to build interactive ChatGPT apps with MCP

Thursday, October 16, 2025

Thursday, October 16, 2025

Two years ago, the idea of spinning up a playlist, booking a trip, or ordering dinner through an AI chat interface felt like a distant vision. Even earlier this year, demos of AI using computer vision to navigate human-designed websites were clunky and inefficient. It was fair to wonder: would AI ever interact smoothly with the digital world around us?

Now, that possibility finally feels real. Starting last year, MCP introduced a standardized way for AI to connect with third-party services and interact with tools, resources, and prompts. Today, the OpenAI Apps SDK takes that a step further, combining natural language control of third-party apps with purpose-built interfaces that render directly inside ChatGPT.

OpenAI launched Apps with a small group of partners while simultaneously opening the same capabilities to all developers through the Apps SDK.

While the impact of this release has yet to be fully realized by the millions of online businesses worldwide, it could mark a watershed moment, a glimpse of what might become the future “app store” for AI.

According to OpenAI, ChatGPT already has more than 800 million weekly active users (over 10% of the global adult population!), positioning it as a potential distribution channel with immense reach.

Let’s take a closer look at the Apps SDK and see how it works under the hood.

MCP’s role in the OpenAI Apps SDK

Before we continue with this article, we want to give a big shoutout to our friend Matthew Wang at MCPJam, who came up with the idea and co-authored this article with us!

Now into the good stuff.

The first thing to know is that the Apps SDK is built on top of MCP. If you are new to the Model Context Protocol, you can get started with the standard in this article about MCP. Otherwise we assume that you have a basic knowledge of how the protocol works.

Each Apps SDK app has two main components: 

  • MCP server that exposes tools and special resources

  • Web widget that renders inside the ChatGPT client

Here’s the high level flow of how widgets are rendered for an app built with the Apps SDK: 

  1. When you ask ChatGPT to “Make me a playlist of the latest songs from my favorite artists I haven’t listened to yet,” it will call the Spotify MCP server. 

  2. The MCP tool references its corresponding MCP resource through the _meta tag. In its contents, this script contains the compiled react component that is to be rendered.

  3. ChatGPT then retrieves this resource, which contains the widget script, from the MCP server for rendering.

  4. Finally, the client loads the widget script into an iframe, displaying your app’s interface.

In practice, it looks something like this: 


In the next section, we look at the structure of a ChatGPT app to understand how MCP servers and widgets are packaged. 


Building an Apps SDK app

App structure

Building an MCP server for the Apps SDK follows the same process as any other MCP server when it comes to exposing your services and business logic in the form of tools or resources. However, there is one key difference: it comes with a specialized resource type that allows it to render UI. That’s what we’ll concentrate on here.

In practice, the LLM automatically invokes your tools when appropriate and retrieves the related MCP resource containing the HTML needed to render a widget.

The server exposes HTML with an embedded script referencing the compiled JavaScript file. This allows the UI widget to be built with any frontend framework (React, Svelte, or others) as long as the final output is a compiled JS file that can be rendered in an iframe.

OpenAI recommends separating your app into two folders, your MCP server and your web components. 

app/

	server/				# MCP Server code
  	web/

		package.json
		tsconfig.json
		src/component.tsx	# Widget React component
		dist/component.js 	# Compiled component

However, this structure is not required. Some developers choose to host the server and widgets separately. As long as the server correctly references the widget, it can still deliver the UI back to the LLM, even if the widget is hosted remotely.

MCP tool

In the tool descriptor, the MCP server tool should reference the widget resource. Link the template URI using the _meta tag under openai/outputTemplate, and ensure that the URI in this tag matches the one defined in the corresponding MCP resource.

server.registerTool(
  "show_spotify_playlist",
  {
    title: "Show Spotify Playlist",
    _meta: {
      "openai/outputTemplate": "ui://widget/spotify-playlist.html",
      "openai/toolInvocation/invoking": "Playlist loading",
      "openai/toolInvocation/invoked": "Playlist loaded"
    },
    inputSchema: { playlistId: z.number() }
  },
  async () => {
    return {
      content: [{ type: "text", text: "Displayed the kanban board!" }],
      structuredContent: { playlistId }
    };
  }
);


You can also include a structuredContent value in the return object. This data is used to hydrate your widget component. You’ll learn more about how this works in the “Hydrating the Widget” section below.

MCP Resource

The MCP resource contains the JavaScript script that’s sent for rendering. Its URI must begin with ui:// and match the URI defined in the tool’s openai/outputTemplate. If the URIs don’t match, the widget won’t be discovered or rendered correctly.

The resource should also include a mimeType and text property. Set the mimeType to text/html+skybridge, and use the text field to include the HTML containing the JavaScript script that will be sent to the client.

Here’s an example resource definition:

server.registerResource(
  "spotify-playlist-widget",
  "ui://widget/spotify-playlist.html",
  {},
  async () => ({
    contents: [
      {
        uri: "ui://widget/spotify-playlist.html",
        mimeType: "text/html+skybridge",
        text: `<script type="module">${readFileSync(`dist/playlist.js`, "utf-8")}"></script>`, #Compiled widget component
      },
    ],
  })
);


How widgets interact with the LLM via the window.openai API

We’ve covered the MCP server component; now let’s look at the web side of an Apps SDK project: widgets.

Widgets are where your Apps SDK project comes alive. While the MCP server handles logic and data, widgets are the interactive UI layer. Through the window.openai API, widgets can exchange data with the LLM, trigger tools on your MCP server, persist state, and even send follow-up messages back into the chat.

A widget is simply a React component, but when rendered in a client that supports the Apps SDK, it gains access to the window.openai API. This API connects the UI to the client and can be called directly from within the React component.

Key capabilities include:

Hydrating the widget component with data

When a tool returns structured data, that data can be passed into the widget via the structuredContent field. The widget can then access it through the window.openai.toolOutput API.

This enables dynamic UIs, for example, displaying a Kanban board populated with tasks from the server.

server.registerTool(
  "kanban-board",
  {
    title: "Show Kanban Board",
    _meta: {
      "openai/outputTemplate": "ui://widget/kanban-board.html",
      "openai/toolInvocation/invoking": "Displaying the board",
      "openai/toolInvocation/invoked": "Displayed the board"
    },
    inputSchema: { tasks: z.string() }
  },
  async () => {
    return {
      content: [{ type: "text", text: "Displayed the kanban board!" }],
      structuredContent: {
        columns: board.columns.map((column) => ({
          id: column.id,
          title: column.title,
          tasks: column.tasks.slice(0, 5)
        }))
}
    };
  }
);

In the widget component, you can then access the same data like this:

const { columns } = window.openai.toolOutput

Think of structuredContent and toolOutput as props flowing from your MCP server into your React component, keeping the UI in sync with the model’s data.

Persisting app state

Widgets can store UI state using window.openai.setWidgetState, and retrieve it later with window.openai.widgetState. This allows the app to “remember” things across interactions (for example, a UI state such as selected tab, open/collapsed), similar to a browser’s localStorage.

For example, you might persist the user’s selected tab or whether a sidebar is open. This persistence keeps the experience seamless, especially for widgets that evolve over time (like dashboards or form-based tools).

Invoking MCP server tools 

Widgets can also call MCP server tools directly via window.openai.callTool. This lets the UI trigger server actions dynamically, which is great for creating interactive experiences.

For example, a Pizza widget could let users place an order straight from the interface:

window.openai?.callTool("order_pizza", { order: "pepperoni", qty: 2 });

This capability bridges frontend interactivity with backend intelligence, turning widgets into fully functional mini-apps inside ChatGPT.

Send messages back to the LLM 

Finally, widgets can communicate directly with the LLM using window.openai.sendFollowUpMessage. This sends a text message back into the conversation, allowing the UI to “talk” to the model.

For example, after a user selects their favorite pizzerias, your widget could prompt ChatGPT to generate a tasting itinerary:

await window.openai?.sendFollowupMessage({
  prompt: "Draft a tasting itinerary for the pizzerias I favorited.",
});

This creates a continuous, conversational loop between the model and the app that combines UI, reasoning, and dialogue into one experience.

And that’s just a taste (no pun intended) of what the window.openai API can do. There’s much more to explore, but these capabilities form the foundation of how widgets bridge the gap between your MCP server, ChatGPT, and the end user. For full details, check the official window.openai documentation here


Building, Testing, deploying your Apps SDK server

Now that you understand how Apps SDK apps work, it’s time to bring your own ideas to life. You can explore OpenAI’s official Apps SDK examples or jump right in with our Apps SDK Starter template, a minimal TypeScript application that demonstrates how to build an OpenAI Apps SDK-compatible MCP server with widget rendering in ChatGPT.

The starter integrates a TypeScript Express app with MCP and includes:

  • A working MCP server exposing tools and resources callable from ChatGPT.

  • Examples with and without UI widgets rendered natively inside ChatGPT.

  • A clean, extensible foundation ready for customization and deployment on Alpic.

Developing with the Apps SDK currently requires exposing your local server via ngrok. To make this easier, our partners at MCP Jam built Apps SDK support into their open source inspector, letting you:

  • Run and test your Apps SDK server locally, no tunnelling needed.

  • Trigger tools and preview widgets directly in the inspector.

  • Simulate production-like interactions through an LLM playground.

MCP Jam’s Apps SDK support is still in beta, but it’s already a powerful way to iterate on your app locally. 

Once your Apps SDK server is up and running, hosting it on Alpic is the easiest way to make it available. Alpic provides first-class support for deploying MCP servers and serving widget assets, ensuring your app is fast, reliable, and easy to connect to! Get started on the Alpic platform today! 

Resources
Legal
Resources
Legal
Resources
Legal