Skip to main content
We’re going to create a simple Coffee Shop ChatGPT App where users can order and drink coffees!

Getting Started

Clone the repo and navigate to the Coffee Shop example:

Setting up the MCP Server

Creating the MCP Server

server.ts

Registering Resources and Tools

Widget resources and tools work together. The resource provides the widget HTML, and tools reference it to display your UI:
server.ts

Understanding the Widget

ChatGPT Apps can display interactive widgets inside the chat. Widget resources are registered by your app and become available when the client connects to your MCP server. Your widget can be built using vanilla JavaScript or a framework like React (optionally with TypeScript), and is bundled into a self-contained HTML file. When your tool is called, the client renders this HTML inside a sandboxed iframe and injects window.openai into it, which is how your widget communicates with the client and invokes tools on your MCP server.

The window.openai API

window.openai provides globals, methods for calling tools, sending follow-ups, managing layout, and much more. Here’s a basic React widget structure using the window.openai API:
src/CoffeeShopWidget.tsx
  • window.openai.toolOutput - The structuredContent your MCP server returned. It’s the data your tool sends to both the widget and the model for context
  • window.openai.callTool() - Lets widget buttons trigger server tools directly (requires openai/widgetAccessible: true in the tool’s metadata)
  • openai:set_globals - Event that fires when users trigger tools via chat (e.g., “order me a coffee”), keeping everything in sync
Our Coffee Shop stores state on the server, so the widget just reads toolOutput. If you need to persist state in the widget and expose it to the client, use window.openai.widgetState and window.openai.setWidgetState().
For more on the window.openai component bridge (file uploads, modals, follow-up messages, and more), see the OpenAI Apps SDK docs.

Display Modes

Widgets can request different display modes to optimize their presentation:
  • Inline (default) - Widget renders within the chat message flow
  • Picture-in-Picture (PiP) - Widget floats at the top of the screen, staying visible while scrolling
  • Fullscreen - Widget expands to fill the entire viewport for immersive experiences
Widgets start in inline mode. To request a different mode:
Users can exit PiP or fullscreen by clicking the close button, returning to inline. Our Coffee Shop uses the default inline mode, but you can test all three in MCPJam Inspector’s Playground. When your widget is in fullscreen, you can add an “Open in <server name>” button to the header chrome by calling setOpenInAppUrl. This lets users open the widget’s underlying web app in a new tab:
Only http and https URLs are accepted. The link opens in a new tab with noopener,noreferrer. This capability is optional — hosts can turn it off, in which case window.openai.setOpenInAppUrl won’t be defined. Guard for it before calling, for example window.openai.setOpenInAppUrl?.({ href: "https://app.example.com/item/42" }).

Content Security Policy (CSP)

Widgets run in a sandboxed iframe, so you need to declare which external domains your widget can interact with. Configure these permissions in your resource’s _meta using one of two formats:
  • _meta.ui.csp (preferred) — uses camelCase field names and is the standard MCP Apps format
  • _meta["openai/widgetCSP"] (legacy) — the original ChatGPT Apps format, still supported
When both are present, ui.csp values take precedence field-by-field; any field absent from ui.csp falls back to openai/widgetCSP. Both formats support the same domain categories:
Without declaring a domain in your CSP, the sandbox will block the request. Only declare the domains you actually need.

A note on authentication and monetization

Since the purpose of this guide is to get you building your first ChatGPT App, we’ve kept things simple and skipped authentication and monetization. For production apps, check out OpenAI’s authentication docs and monetization docs.

Running Your App

Build and start the server

This builds the React widget with Vite and starts the server at http://localhost:8787/mcp.

Testing with MCPJam Inspector

The easiest way to test your app:
  1. Run the inspector: npx @mcpjam/inspector@latest
  2. Enter URL: http://localhost:8787/mcp
  3. Try your app in our Playground!

Connecting to ChatGPT

To connect your app to ChatGPT:
  1. Sign in to MCPJam Inspector (ngrok tunnels require an account)
  2. Click Create ngrok tunnel with your server connected
  3. Use the tunnel URL as your connector endpoint in ChatGPT
For more information, see our ngrok tunneling feature blog.

What’s next?

Now that your Coffee Shop is running, you can:
  1. Test the flow - Call the orderCoffee tool to see your widget
  2. Try the buttons - Click “Order” and “Drink” to interact with your server
  3. Chat naturally - Say “order me 3 coffees” and watch the widget update
  4. Iterate and expand - Add more tools, improve the UI, or build something completely new!
Congratulations! You’ve built your first ChatGPT App! 🎉