Skip to main content
Glama

OP.GG MCP Server

Official
by opgginc
apps-sdk_build_custom-ux_.txt15 kB
--- url: "https://developers.openai.com/apps-sdk/build/custom-ux/" title: "Build a custom UX" --- ## Search the docs ⌘K/CtrlK Close Clear Primary navigation ChatGPT ResourcesCodexChatGPTBlog Clear - [Home](https://developers.openai.com/apps-sdk) ### Core Concepts - [MCP Server](https://developers.openai.com/apps-sdk/concepts/mcp-server) - [User interaction](https://developers.openai.com/apps-sdk/concepts/user-interaction) - [Design guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines) ### Plan - [Research use cases](https://developers.openai.com/apps-sdk/plan/use-case) - [Define tools](https://developers.openai.com/apps-sdk/plan/tools) - [Design components](https://developers.openai.com/apps-sdk/plan/components) ### Build - [Set up your server](https://developers.openai.com/apps-sdk/build/mcp-server) - [Build a custom UX](https://developers.openai.com/apps-sdk/build/custom-ux) - [Authenticate users](https://developers.openai.com/apps-sdk/build/auth) - [Persist state](https://developers.openai.com/apps-sdk/build/storage) - [Examples](https://developers.openai.com/apps-sdk/build/examples) ### Deploy - [Deploy your app](https://developers.openai.com/apps-sdk/deploy) - [Connect from ChatGPT](https://developers.openai.com/apps-sdk/deploy/connect-chatgpt) - [Test your integration](https://developers.openai.com/apps-sdk/deploy/testing) ### Guides - [Optimize Metadata](https://developers.openai.com/apps-sdk/guides/optimize-metadata) - [Security & Privacy](https://developers.openai.com/apps-sdk/guides/security-privacy) - [Troubleshooting](https://developers.openai.com/apps-sdk/deploy/troubleshooting) ### Resources - [Reference](https://developers.openai.com/apps-sdk/reference) - [App developer guidelines](https://developers.openai.com/apps-sdk/app-developer-guidelines) ## Overview UI components turn structured tool results into a human-friendly UI. Apps SDK components are typically React components that run inside an iframe, talk to the host via the `window.openai` API, and render inline with the conversation. This guide describes how to structure your component project, bundle it, and wire it up to your MCP server. You can also check out the [examples repository on GitHub](https://github.com/openai/openai-apps-sdk-examples). ## Understand the `window.openai` API `window.openai` is the bridge between your frontend and ChatGPT. Use this quick reference to first understand how to wire up data, state, and layout concerns before you dive into component scaffolding. ``` declare global { interface Window { openai: API & OpenAiGlobals; } interface WindowEventMap { [SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent; } } type OpenAiGlobals< ToolInput extends UnknownObject = UnknownObject, ToolOutput extends UnknownObject = UnknownObject, ToolResponseMetadata extends UnknownObject = UnknownObject, WidgetState extends UnknownObject = UnknownObject > = { theme: Theme; userAgent: UserAgent; locale: string; // layout maxHeight: number; displayMode: DisplayMode; safeArea: SafeArea; // state toolInput: ToolInput; toolOutput: ToolOutput | null; toolResponseMetadata: ToolResponseMetadata | null; widgetState: WidgetState | null; }; type API<WidgetState extends UnknownObject> = { /** Calls a tool on your MCP. Returns the full response. */ callTool: (name: string, args: Record<string, unknown>) => Promise<CallToolResponse>; /** Triggers a followup turn in the ChatGPT conversation */ sendFollowUpMessage: (args: { prompt: string }) => Promise<void>; /** Opens an external link, redirects web page or mobile app */ openExternal(payload: { href: string }): void; /** For transitioning an app from inline to fullscreen or pip */ requestDisplayMode: (args: { mode: DisplayMode }) => Promise<{ /** * The granted display mode. The host may reject the request. * For mobile, PiP is always coerced to fullscreen. */ mode: DisplayMode; }>; setWidgetState: (state: WidgetState) => Promise<void>; }; // Dispatched when any global changes in the host page export const SET_GLOBALS_EVENT_TYPE = "openai:set_globals"; export class SetGlobalsEvent extends CustomEvent<{ globals: Partial<OpenAiGlobals>; }> { readonly type = SET_GLOBALS_EVENT_TYPE; } export type CallTool = ( name: string, args: Record<string, unknown> ) => Promise<CallToolResponse>; export type DisplayMode = "pip" | "inline" | "fullscreen"; export type Theme = "light" | "dark"; export type SafeAreaInsets = { top: number; bottom: number; left: number; right: number; }; export type SafeArea = { insets: SafeAreaInsets; }; export type DeviceType = "mobile" | "tablet" | "desktop" | "unknown"; export type UserAgent = { device: { type: DeviceType }; capabilities: { hover: boolean; touch: boolean; }; }; ``` ### useOpenAiGlobal Many Apps SDK projects wrap `window.openai` access in small hooks so views remain testable. This example hook listens for host `openai:set_globals` events and lets React components subscribe to a single global value: ``` export function useOpenAiGlobal<K extends keyof OpenAiGlobals>( key: K ): OpenAiGlobals[K] { return useSyncExternalStore( (onChange) => { const handleSetGlobal = (event: SetGlobalsEvent) => { const value = event.detail.globals[key]; if (value === undefined) { return; } onChange(); }; window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, { passive: true, }); return () => { window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal); }; }, () => window.openai[key] ); } ``` `useOpenAiGlobal` is an important primitive to make your app reactive to changes in display mode, theme, and “props” via subsequent tool calls. For example, read the tool input, output, and metadata: ``` export function useToolInput() { return useOpenAiGlobal('toolInput') } export function useToolOutput() { return useOpenAiGlobal('toolOutput') } export function useToolResponseMetadata() { return useOpenAiGlobal('toolResponseMetadata') } ``` ### Persist component state, expose context to ChatGPT Widget state can be used for persisting data across user sessions, and exposing data to ChatGPT. Anything you pass to `setWidgetState` will be shown to the model, and hydrated into `window.openai.widgetState`. Note that currently everything passed to `setWidgetState` is shown to the model. For the best performance, it’s advisable to keep this payload small, and to not exceed more than 4k [tokens](https://platform.openai.com/tokenizer). ### Trigger server actions `window.openai.callTool` lets the component directly make MCP tool calls. Use this for direct manipulations (refresh data, fetch nearby restaurants). Design tools to be idempotent where possible and return updated structured content that the model can reason over in subsequent turns. Please note that your tool needs to be marked as [able to be initiated by the component](https://developers.openai.com/apps-sdk/build/mcp-server###allow-component-initiated-tool-access). ``` async function refreshPlaces(city: string) { await window.openai?.callTool("refresh_pizza_list", { city }); } ``` ### Send conversational follow-ups Use `window.openai.sendFollowupMessage` to insert a message into the conversation as if the user asked it. ``` await window.openai?.sendFollowupMessage({ prompt: "Draft a tasting itinerary for the pizzerias I favorited.", }); ``` ### Request alternate layouts If the UI needs more space—like maps, tables, or embedded editors—ask the host to change the container. `window.openai.requestDisplayMode` negotiates inline, PiP, or fullscreen presentations. ``` await window.openai?.requestDisplayMode({ mode: "fullscreen" }); // Note: on mobile, PiP may be coerced to fullscreen ``` ### Use host-backed navigation Skybridge (the sandbox runtime) mirrors the iframe’s history into ChatGPT’s UI. Use standard routing APIs—such as React Router—and the host will keep navigation controls in sync with your component. Router setup (React Router’s `BrowserRouter`): ``` export default function PizzaListRouter() { return ( <BrowserRouter> <Routes> <Route path="/" element={<PizzaListApp />}> <Route path="place/:placeId" element={<PizzaListApp />} /> </Route> </Routes> </BrowserRouter> ); } ``` Programmatic navigation: ``` const navigate = useNavigate(); function openDetails(placeId: string) { navigate(`place/${placeId}`, { replace: false }); } function closeDetails() { navigate("..", { replace: true }); } ``` ## Scaffold the component project Now that you understand the `window.openai` API, it’s time to scaffold your component project. As best practice, keep the component code separate from your server logic. A common layout is: ``` app/ server/ # MCP server (Python or Node) web/ # Component bundle source package.json tsconfig.json src/component.tsx dist/component.js # Build output ``` Create the project and install dependencies (Node 18+ recommended): ``` cd app/web npm init -y npm install react@^18 react-dom@^18 npm install -D typescript esbuild ``` If your component requires drag-and-drop, charts, or other libraries, add them now. Keep the dependency set lean to reduce bundle size. ## Author the React component Your entry file should mount a component into a `root` element and read initial data from `window.openai.toolOutput` or persisted state. We have provided some example apps under the [examples page](https://developers.openai.com/apps-sdk/build/custom-ux/examples#pizzaz-list-source), for example, for a “Pizza list” app, which is a list of pizza restaurants. As you can see in the source code, the pizza list React component does the following: 1. **Mount into the host shell.** The Skybridge HTML template exposes `div#pizzaz-list-root`. The component mounts with `createRoot(document.getElementById("pizzaz-list-root")).render(<PizzaListApp />)` so the entire UI stays encapsulated inside the iframe. 2. **Subscribe to host globals.** Inside `PizzaListApp`, hooks such as `useOpenAiGlobal("displayMode")` and `useOpenAiGlobal("maxHeight")` read layout preferences directly from `window.openai`. This keeps the list responsive between inline and fullscreen layouts without custom postMessage plumbing. 3. **Render from tool output.** The component treats `window.openai.toolOutput` as the authoritative source of places returned by your tool. `widgetState` seeds any user-specific state (like favorites or filters) so the UI restores after refreshes. 4. **Persist state and call host actions.** When a user toggles a favorite, the component updates React state and immediately calls `window.openai.setWidgetState` with the new favorites array. Optional buttons can trigger `window.openai.requestDisplayMode({ mode: "fullscreen" })` or `window.openai.callTool("refresh_pizza_list", { city })` when more space or fresh data is needed. ### Explore the Pizzaz component gallery We provide a number of example components in the [Apps SDK examples](https://developers.openai.com/apps-sdk/build/examples). Treat them as blueprints when shaping your own UI: - **Pizzaz List** – ranked card list with favorites and call-to-action buttons. ![Screenshot of the Pizzaz list component](https://developers.openai.com/images/apps-sdk/pizzaz-list.png) - **Pizzaz Carousel** – embla-powered horizontal scroller that demonstrates media-heavy layouts. ![Screenshot of the Pizzaz carousel component](https://developers.openai.com/images/apps-sdk/pizzaz-carousel.png) - **Pizzaz Map** – Mapbox integration with fullscreen inspector and host state sync. ![Screenshot of the Pizzaz map component](https://developers.openai.com/images/apps-sdk/pizzaz-map.png) - **Pizzaz Album** – stacked gallery view built for deep dives on a single place. ![Screenshot of the Pizzaz album component](https://developers.openai.com/images/apps-sdk/pizzaz-album.png) - **Pizzaz Video** – scripted player with overlays and fullscreen controls. Each example shows how to bundle assets, wire host APIs, and structure state for real conversations. Copy the one closest to your use case and adapt the data layer for your tool responses. ### React helper hooks Using `useOpenAiGlobal` in a `useWidgetState` hook to keep host-persisted widget state aligned with your local React state: ``` export function useWidgetState<T extends WidgetState>( defaultState: T | (() => T) ): readonly [T, (state: SetStateAction<T>) => void]; export function useWidgetState<T extends WidgetState>( defaultState?: T | (() => T | null) | null ): readonly [T | null, (state: SetStateAction<T | null>) => void]; export function useWidgetState<T extends WidgetState>( defaultState?: T | (() => T | null) | null ): readonly [T | null, (state: SetStateAction<T | null>) => void] { const widgetStateFromWindow = useWebplusGlobal("widgetState") as T; const [widgetState, _setWidgetState] = useState<T | null>(() => { if (widgetStateFromWindow != null) { return widgetStateFromWindow; } return typeof defaultState === "function" ? defaultState() : defaultState ?? null; }); useEffect(() => { _setWidgetState(widgetStateFromWindow); }, [widgetStateFromWindow]); const setWidgetState = useCallback( (state: SetStateAction<T | null>) => { _setWidgetState((prevState) => { const newState = typeof state === "function" ? state(prevState) : state; if (newState != null) { window.openai.setWidgetState(newState); } return newState; }); }, [window.openai.setWidgetState] ); return [widgetState, setWidgetState] as const; } ``` The hooks above make it easy to read the latest tool output, layout globals, or widget state directly from React components while still delegating persistence back to ChatGPT. ## Bundle for the iframe Once you are done writing your React component, you can build it into a single JavaScript module that the server can inline: ``` // package.json { "scripts": { "build": "esbuild src/component.tsx --bundle --format=esm --outfile=dist/component.js" } } ``` Run `npm run build` to produce `dist/component.js`. If esbuild complains about missing dependencies, confirm you ran `npm install` in the `web/` directory and that your imports match installed package names (e.g., `@react-dnd/html5-backend` vs `react-dnd-html5-backend`). ## Embed the component in the server response See the [Set up your server docs](https://developers.openai.com/apps-sdk/build/mcp-server#) for how to embed the component in your MCP server response. Component UI templates are the recommended path for production. During development you can rebuild the component bundle whenever your React code changes and hot-reload the server.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/opgginc/opgg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server