Openei has recently introduced ChatGPT apps, powered by new Apps SDK and Model Context Protocol (MCP).
Think of these apps as plugins for ChatGPT:
You can ask them naturally in conversation.
They can offer custom interactive UIs within ChatGPT (maps, carousels, videos, and more).
They run on an MCP server that you control, which defines the tools, resources, and widgets provided to the app.
In this step-by-step guide, you will create a ChatGPT app using official An example of a pizza app. This app shows how ChatGPT can render UI widgets like pizza maps or carousels powered by your local server.
will you learn
By following this tutorial, you will learn how to:
Compile and run the ChatGPT app with the OpenAApps SDK.
Understand the basic building blocks: tools, resources and widgets.
Connect your local app server to ChatGPT using developer mode.
Present a custom UI directly within a Chat GPT conversation.
Table of Contents
How Chat GPT Apps Work (Big Picture)
Here is the architecture in simple terms:
ChatGPT (frontend)
|
v
MCP Server (your backend)
|
v
Widgets (HTML/JS markup displayed inside ChatGPT)
Chat GPT Sends requests such as: “Show me a pizza carousel.”
MCP server Responds with resources (HTML markup) and tool logic.
Widget Chat is offered online in GPT.
Step 1. Clone the examples repo
Openeye 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
Go 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
In order for ChatGPT to communicate with your app, your local server needs a public URL. NGROK provides a quick way to expose this during development.
3.1 Get ngrok
Sign up ngrok.com And make your own copy 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
3.4 Start a tunnel
ngrok http 8000
This gives you a public HTTPS URL (eg https://xyz.ngrok.app/mcp)
Step 4. Run through the Pizza App code
The complete 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";
4.2 Description of the Pizza Widget
Widgets are the heart of the app. Each UI represents a piece of chat that can display.
Here is the Pizza Map widget:
{
id: "pizza-map",
title: "Show Pizza Map",
templateUri: "ui://widget/pizza-map.html",
html: ` `,
responseText: "Rendered a pizza map!"
}
-
idviget The unique name of the widget. -
templateUri→ How Chat GPT takes UI. -
html→ Original markup and assets. -
responseText→ The message that appears in the chat.
Five widgets are defined in the app:
-
Pizza map
-
Pizza carousel
-
The Pizza Album
-
Pizza menu
-
Pizza video
4.3 Mapping Widgets to Tools and Resources
Next, let's switch to widgets tools (things can call chatgpt) and Resources (UI markup chat can render GPT).
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 black and editable.
4.4 Handling Requests
The MCP server responds to chat GPT requests. For example, when ChatGPT calls the WidgetTool:
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:
-
The widget has been requested.
-
Validates input (
pizzaTopping) -
Responds with text + metadata so that the chat can render the GPT widget.
4.5 Creating a Server
Finally, the server is bound to HTTP endpoints (/mcp And /mcp/messages) so ChatGPT can forward messages to and from it.
const httpServer = createServer(async (req, res) => {
});
httpServer.listen(8000, () => {
console.log("Pizzaz MCP server running on port 8000");
});
Step 5. Enable Developer Mode in ChatGPT
5.1 Enable Developer Mode

when Developer mode Enabled, ChatGPT should look like this:

5.2 Create the App

Once your app is connected to ChatGPT, it should look like this:

When you click back icon, you should see your app and other apps you can connect and use with ChatGPT:

5.3 Use Your App
To use your app,

Here are some commands you can try 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 toppings
-
Show me a list of pizzas with cheese toppings
-
Show me a pizza video with chicken topping
Each command tells chatgpt which widget to render, and you can swap in any toppings you like.

Below are samples:



Challenges (try them yourself)
Here are three practical ways to grow your pizza app. Each is directly related to the code you have.
Challenge A: Add a "Pizza Special" widget (text only).
Purpose: Create a widget that displays only a short message "Today's Special: Margarita with Basil."
Where to change:
-
resources.widgetsan Copy an entry and give it a new oneid/title. -
toolsa Register it as a new tool. -
CallToolhandler → detect when it is called (if (request.params.name === "pizza-special")) and return your special
Hint:
This widget does not require additional CSS/JS files. Just keep at it html Like something
🍕 Today’s special: Margherita
. The idea is to show that widgets can be as simple as plain HTML.
Challenge B: Support multiple toppings
Purpose: Have customers order a pizza with multiple toppings, e.g ("pepperoni", "mushroom").
Where to change:
-
toolInputSchemaSwitch fromz.string()toz.array(z.string()). -
CallToolAfter handler → parsing,args.pizzaToppingThere will be an array. Add it to the string before inserting it into the HTML/Response. -
Update Widget HTML → Display so it lists all the selected toppings.
Hint:
The console. Parsed args First to verify that you are indeed getting an array. Then try something like this:
const toppings = args.pizzaTopping.join(", ");
return { responseText: `Pizza ordered with ${toppings}` };
Challenge C: Fetch real pizza data from an external API
Purpose: Instead of hard-coding content, bring real pizzazz. For example, you can call Yelp's API for a list of pizza places in a location, or use the free placeholder API to simulate data.
Where to change:
Hint:
Start small like a free API JSONPLAY holder. For example:
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
const data = await res.json();
const html = `
${data.map((p: any) => `- ${p.title}
`).join("")}
`;
return { responseText: "Fetched pizza places!", content: ({ type: "text/html", text: html }) };
Once that works, real APIs like YELP or Google Maps are converted to serve up actual pizza places.
The result
You have just created your first chat using the GPT app Openai Apps SDK. With a bit of JavaScript and HTML, you've created a server that ChatGPT can talk to, and renders interactive widgets right inside the chat window.
This example focused on the sample pizza app provided by Openi, but you can build:
-
A weather dashboard,
-
A movie finder,
-
A financial data viewer,
-
Or even a mini-game.
SDK makes mixing possible Conversation + Interactive UI In powerful new ways.
Explore OpenAI Apps SDK Documentation To dive in and start building your apps.