OpenAI recently introduced ChatGPT Apps, powered by the new Apps SDK and the Model Context Protocol (MCP).
Think of these apps as plugins for ChatGPT:
You can invoke them naturally in a conversation.
They can render custom interactive UIs inside ChatGPT (maps, carousels, videos, and more).
They run on an MCP server that you control, which defines the tools, resources, and widgets the app provides.
In this step-by-step guide, you’ll build a ChatGPT App using the official Pizza App example. This app shows how ChatGPT can render UI widgets like a pizza map or carousel, powered by your local server.
What You’ll Learn
By following this tutorial, you’ll learn how to:
Set up and run a ChatGPT App with the OpenAI Apps SDK.
Understand the core building blocks: tools, resources, and widgets.
Connect your local app server to ChatGPT using Developer Mode.
Render custom UI directly inside a ChatGPT conversation.
Table of Contents
How ChatGPT Apps Work (Big Picture)
Here’s the architecture in simple terms:
ChatGPT (frontend)
|
v
MCP Server (your backend)
|
v
Widgets (HTML/JS markup displayed inside ChatGPT)
ChatGPT sends requests like: “Show me a pizza carousel.”
MCP Server responds with resources (HTML markup) and tool logic.
Widgets are rendered inline in ChatGPT.
Step 1. Clone the Examples Repo
OpenAI provides an official examples repo that includes the Pizza App. Clone it and install the dependencies using these commands:
git clone https://github.com/openai/openai-apps-sdk-examples.git
cd openai-apps-sdk-examples
pnpm install
Step 2. Run the Pizza App Server
Navigate to the Pizza App server and start it:
cd pizzaz_server_node
pnpm start
If it works, you should see:
Pizzaz MCP server listening on http://localhost:8000
SSE stream: GET http://localhost:8000/mcp
Message post endpoint: POST http://localhost:8000/mcp/messages
This means your server is running locally.
Step 3. Expose Your Local Server
To let ChatGPT communicate with your app, your local server needs a public URL. ngrok provides a quick way to expose it during development.
3.1 Get ngrok
Sign up at ngrok.com and copy your authtoken.
3.2 Install ngrok
macOS:
brew install ngrok
Windows:
Download and unzip ngrok.
Optionally, add the folder to your PATH.
3.3 Connect Your Account
ngrok config add-authtoken <your_authtoken>
3.4 Start a Tunnel
ngrok http 8000
This gives you a public HTTPS URL (like https://xyz.ngrok.app/mcp
).
Step 4. Walk Through the Pizza App Code
The full Pizza App server code is long, so let’s break it down into digestible parts.
4.1 Imports and Setup
import { createServer } from "node:http";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod";
Server
andSSEServerTransport
come from the Apps SDK.zod
validates input to ensure ChatGPT sends the right arguments.
4.2 Defining Pizza Widgets
Widgets are the heart of the app. Each one represents a piece of UI ChatGPT can display.
Here’s the Pizza Map widget:
{
id: "pizza-map",
title: "Show Pizza Map",
templateUri: "ui://widget/pizza-map.html",
html: `
<div id="pizzaz-root"></div>
<link rel="stylesheet" href=".../pizzaz-0038.css">
<script type="module" src=".../pizzaz-0038.js"></script>
`,
responseText: "Rendered a pizza map!"
}
id
→ unique name of the widget.templateUri
→ how ChatGPT fetches the UI.html
→ actual markup and assets.responseText
→ message that shows in chat.
The app defines five widgets:
Pizza Map
Pizza Carousel
Pizza Album
Pizza List
Pizza Video
4.3 Mapping Widgets to Tools and Resources
Next, widgets are converted into tools (things ChatGPT can call) and resources (UI markup ChatGPT can render).
const tools = widgets.map((widget) => ({
name: widget.id,
description: widget.title,
inputSchema: toolInputSchema,
title: widget.title,
_meta: widgetMeta(widget)
}));
const resources = widgets.map((widget) => ({
uri: widget.templateUri,
name: widget.title,
description: `${widget.title} widget markup`,
mimeType: "text/html+skybridge",
_meta: widgetMeta(widget)
}));
This makes each widget callable and displayable.
4.4 Handling Requests
The MCP server responds to ChatGPT’s requests. For example, when ChatGPT calls a widget tool:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const widget = widgetsById.get(request.params.name);
const args = toolInputParser.parse(request.params.arguments ?? {});
return {
content: [{ type: "text", text: widget.responseText }],
structuredContent: { pizzaTopping: args.pizzaTopping },
_meta: widgetMeta(widget)
};
});
This:
Finds the widget requested.
Validates the input (
pizzaTopping
).Responds with text + metadata so ChatGPT can render the widget.
4.5 Creating the Server
Finally, the server is bound to HTTP endpoints (/mcp
and /mcp/messages
) so ChatGPT can stream messages to and from it.
const httpServer = createServer(async (req, res) => {
// handle requests to /mcp and /mcp/messages
});
httpServer.listen(8000, () => {
console.log("Pizzaz MCP server running on port 8000");
});
Step 5. Enable Developer Mode in ChatGPT
5.1 Enable Developer Mode
Open ChatGPT
Go to Settings → Apps & Connectors → Advanced Settings
Toggle Developer Mode
When Developer Mode is enabled, ChatGPT should look like this:
5.2 Create App
Go back to Settings → Apps & Connectors
Click Create
Next:
Name: Enter a name for your app (for example, Pizza App)
Description: Enter any description for your app (or leave empty)
MCP Server URL: Paste the public HTTPS URL of your MCP endpoint. Make sure it points directly to
/mcp
, not just the server rootAuthentication: Choose No authentication
Check I trust this application
Click Create to finish
Once your app is connected to ChatGPT, it should look like this:
When you click on the Back icon, you should see your app and other apps that you can connect to and use with ChatGPT:
5.3 Use Your App
To use your app,
Open a new chat in ChatGPT
Click on the + icon
Scroll down to more
You would see your app
Choose Pizza App to start using your app
Here are some commands you can try out with your pizza app in ChatGPT:
Show me a pizza map with pepperoni topping
Show me a pizza carousel with mushroom topping
Show me a pizza album with veggie topping
Show me a pizza list with cheese topping
Show me a pizza video with chicken topping
Each command tells ChatGPT which widget to render, and you can swap in any topping you like.
Below are samples:
- Pepperoni topping map:
- Extra cheese carousel:
- Mushroom topping album:
Challenges (Try These Yourself)
Here are three practical ways to extend your Pizza App. Each one ties directly to the code you already have.
Challenge A: Add a “Pizza Specials” widget (text-only)
Goal: Create a widget that just shows a short message like “Today’s special: Margherita with basil.”
Where to change:
resources.widgets
→ duplicate an entry and give it a newid
/title
.tools
→ register it as a new tool.CallTool
handler → detect when it’s called (if (request.params.name === "pizza-special")
) and return your special.
Hint:
This widget doesn’t need extra CSS/JS files. Just keep its html
to something like <div>🍕 Today’s special: Margherita</div>
. The idea is to show that widgets can be as simple as plain HTML.
Challenge B: Support Multiple Toppings
Goal: Let users order a pizza with more than one topping, like ["pepperoni", "mushroom"]
.
Where to change:
toolInputSchema
→ switch fromz.string()
toz.array(z.string())
.CallTool
handler → after parsing,args.pizzaTopping
will be an array. Join it into a string before inserting into HTML/response.Widget HTML → update the display so it lists all chosen toppings.
Hint:
Console.log the parsed args
first to confirm you’re actually getting an array. Then try something like:
const toppings = args.pizzaTopping.join(", ");
return { responseText: `Pizza ordered with ${toppings}` };
Challenge C: Fetch Real Pizza Data from an External API
Goal: Instead of hard-coding content, fetch real pizza info. For example, you could call Yelp’s API to list pizza places in a location, or use a free placeholder API to simulate data.
Where to change:
Inside the
CallTool
handler for your widget.Replace the static HTML with a
fetch(...)
call that builds dynamic HTML from the response.
Hint:
Start small with a free API like JSONPlaceholder. For example:
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
const data = await res.json();
const html = `
<ul>
${data.map((p: any) => `<li>${p.title}</li>`).join("")}
</ul>
`;
return { responseText: "Fetched pizza places!", content: [{ type: "text/html", text: html }] };
Once that works, swap in a real API such as Yelp or Google Maps Places to render actual pizza places.
Conclusion
You just built your first ChatGPT App using the OpenAI Apps SDK. With a bit of JavaScript and HTML, you created a server that ChatGPT can talk to, and rendered interactive widgets right inside the chat window.
This example focused on the pizza app sample provided by OpenAI, but you could build:
A weather dashboard,
A movie finder,
A financial data viewer,
Or even a mini-game.
The SDK makes it possible to blend conversation + interactive UI in powerful new ways.
Explore the OpenAI Apps SDK documentation to go deeper and start building your own apps.