Skip to main content
Use ChatGPT Plugin Extensions to add app launchers, file viewers, incoming routes, native settings, and selected model context. You can also set an initial display preference. The TypeScript SDK converts your declarations to OpenAI extension metadata. The host controls which extensions users can access. Start with a view-bound MCP App. Each view needs one bound tool and an outputSchema. The examples below use the v2 TypeScript API.

Check host support

The OpenAI platform support table describes the following expected support. Its web column refers to the ChatGPT Work browser and excludes classic ChatGPT. Availability depends on the host release. ChatGPT supports inline and fullscreen display modes. Other MCP Apps hosts can support pip. Test each extension in the host that your users use.

Attach evidence to the next model turn

Use useModelContext() when the user selects evidence to send with the next model turn. For example, add a book summary when the user selects Add to chat. Keep cart actions separate from attachment actions. Import the hook from mcp-use/react and render it in your view. The hook activates native context delivery. It needs no viewConfig option. The SDK checks host capabilities during initialization and checks each block before delivery. Calls made during initialization wait for the host connection.

Add, remove, and clear selected evidence

This component adds a summary, lists selected attachments, and lets the user remove or clear them. It catches action errors and shows the shared delivery error.
The hook returns these fields and methods: Each method returns Promise<{ status: "synced" | "superseded" }>. "synced" confirms context delivery, not permanent selection or a visible composer chip. "superseded" means a newer action replaced the operation before acknowledgment. Catch rejected promises as shown above. Use distinct, stable application keys, such as book:summary and book:cover. Keys are shared across all hook instances in one view runtime. Exact duplicate blocks under different keys reject. Unmounting a hook consumer does not clear its selections.

Choose a content block

The hook supports four block types: text, image, resource_link, and resource. Each type accepts a friendly top-level title. The title is also available in attachments[i].block.title after host restoration. These blocks can replace the text block in the example above. For the image and thumbnail, put a cover at public/covers/book.png.
Text blocks can include a thumbnail with an MCP Icon shape. The thumbnail path resolves like the Image component’s public path and stays a URL. The SDK does not fetch thumbnail bytes. ChatGPT iOS currently ignores thumbnails. Image src uses the same public path resolver as Image, including proxy and asset-host configuration. Absolute URLs pass through the resolver. The SDK fetches the image and converts it to native base64 content before changing the selection. Source responses must contain PNG, JPEG, GIF, or WebP bytes with the matching Content-Type. Browser CORS rules apply to remote URLs. You can also supply base64 bytes directly:
Use either src or data plus mimeType in an image block. Do not combine these forms. Returned image attachments contain data and mimeType, even when you supply src. During image preparation, pending is true and the previous selection stays visible. Other ready keys can sync independently. A fetch failure, unsupported or missing Content-Type, or empty response rejects the action and preserves the previous attachment. A later add or remove for the same key, or clearAttachments, cancels preparation and returns "superseded" for that action. A single image is limited to 10 MiB of decoded bytes, whether supplied through src or base64 data. Source downloads stop when they exceed that limit, including when the response omits its size. An oversized image rejects without replacing the current selection. This is an SDK memory budget, not a host upload-size guarantee. A late fetch response cannot restore the canceled attachment. Named types include ModelContextText, ModelContextImage, ModelContextResourceLink, and ModelContextResource. All are exported from mcp-use/react. A resource link uses its native title field, which takes precedence over unrelated title metadata. Text and image titles use OpenAI metadata. Embedded-resource titles use SDK metadata; host display of these titles can vary. Raw metadata and annotations are preserved. Conflicting shorthand and wire fields reject. Audio blocks are not supported.

Respect host removals

OpenAI host context can restore surviving attachments when the view initializes. Restored keys start with @restored: and belong to that runtime. Use these returned keys to remove restored entries. Do not use the reserved prefix for new keys or infer original keys from titles, resource URIs, or list positions. Confirmed host removals update attachments where the SDK can identify the removed blocks. Do not add the same evidence again from a render effect or a cart update. Keep removable evidence out of duplicate useViewState values or ModelContext descriptions. The optional audience field accepts "user" and "assistant" and maps to MCP annotations. Audience controls presentation. All blocks still reach the model. Assistant-only content can be restored as background instead of a visible attachment. Removal affects current context; it does not erase previous conversation turns or backend data.

Combine context hooks

Render useModelContext in the initial view tree when you also use useViewState or ModelContext. Native delivery activates when the hook commits, before state and description effects; discarded renders do not change delivery. The runtime sends attachments, structured view state, and descriptions through one native writer. State and descriptions keep their model-visible JSON projection. clearAttachments() preserves those separate contributions. See Model context to choose a channel for each value. On OpenAI hosts, native host context restores model-visible state after the hook activates. Private widget state keeps its separate persistence channel. Views that do not render useModelContext keep their existing widget-state transport. Activation rejects if model-visible widget modelContent or imageIds already exists, or a widget-state write was dispatched. The SDK does not clear that persisted context for you. Use a view without the conflicting state; reopening alone can retain the conflict. A prior native write must finish, and OpenAI host readback must match the acknowledged context before activation.

Handle delivery failures

A definite host rejection retains the requested selection and sets the shared error. The next valid add, remove, or clearAttachments makes one fresh delivery attempt with the current selection. Invalid input and background updates do not trigger another attempt. The public hook has no retry method or automatic retry loop. A successful model-context response does not require an OpenAI updateId in its metadata. When present and valid, the SDK uses that ID to correlate a whole context revision. The ID is not an attachment key or a compare-and-swap token. Uncertain transport outcomes or ambiguous host ordering stop further publication and reject subsequent mutations. A request that was already sent cannot be recalled. Wait for outstanding remote writes to finish before you open a fresh view runtime. Another notification or a hook remount does not establish that writes have finished.

Verify attachment behavior

  1. Open the view in the host that your users use.
  2. Add a summary and a cover under different keys. Check that both remain selected.
  3. Remove one attachment in the view. Check that the other remains.
  4. Clear the attachments, then add one again.
  5. Where the host supports composer removal, remove an attachment there and check the view’s list.
  6. Change the cart or filter state. Check that removed evidence does not return.
A simulated host can check protocol delivery and errors. Use the native host to check composer chips, thumbnails, and host removal behavior.

Add global and thread entrypoints

Declare global to add an app launcher in navigation. Declare thread to add a content tab in a conversation. Both launchers send {} to the tool. Each conversation has its own thread app instance.
Create views/library/view.tsx for this tool. Render the initial tool result in the view. The initial result already contains the launch data. Global and thread tools must accept empty arguments. Use optional fields or defaults for additional inputs. Keep tools with required query inputs separate from launchers. The MCP SDK validates tool calls against the input schema, including custom refinements. Entrypoints open fullscreen in supporting hosts. Entrypoint calls ignore the tool visibility hint. Use a clear tool title to name the launcher.

Set entrypoint tool icons

Set icons on the tool definition, alongside name and title, as shown above. Put library.svg in public/; the SDK emits its absolute asset URL on tools/list. Absolute HTTP(S) and image data URLs also work. Follow the OpenAI icon guidelines. Use a monochrome SVG with a transparent background, currentColor, and a 20×20 viewport. Use 1.33px strokes for stroke icons. For example, save this book icon as public/library.svg:
view.entrypoints declares launch locations in _meta["openai/ui"].entrypoints. The separate top-level icons field supplies the tool’s artwork. Server constructor icons supplies server branding and remains the fallback when a tool has no icon. Hosts prefer tool icons, then the local server icon or hosted app logo, then a generic icon. Call tools/list to check the tool’s icons and fetch each emitted src. Test the SVG in light and dark host themes.

Add a file entrypoint

Declare the file extensions that your view supports. Each extension must start with a dot. The host sends file.name and file.resourceUri to the tool.
Create views/csv/view.tsx for this tool. The host also sends the file arguments to the view through ui/notifications/tool-input. Treat file.resourceUri as an opaque host handle. Use the host resource APIs to read the file. The HTML view resource URI identifies the view, not the opened file. Use a non-empty extension list. Require both string fields when file is present. Additional input fields must be optional or have defaults.

Combine entrypoint types

One tool can declare all three entrypoint types. For a combined tool, make the outer file field optional. Keep file.name and file.resourceUri required when file is present. Replace the CSV tool registration above with this registration:
The view must show an empty screen when the host sends no file.

Check entrypoint metadata

Call tools/list to inspect the tool descriptor. The CSV tool advertises these metadata fields:
These fields appear inside the tool’s _meta object. Typed view.entrypoints takes precedence over raw _meta["openai/ui"].entrypoints. If you omit the typed field, existing raw entrypoint declarations pass through. Use useDeepLink() to read a route that the host sends to a global entrypoint. The hook uses the view’s existing MCP Apps connection. It reads the initial host context and updates when another deep link arrives.
The hook returns { url: string | undefined }. An undefined URL means the host has supplied no link. Hosts without deep-link support also return an undefined URL. The hook reads hostContext["openai/deepLink"].url. It converts older path and query payloads to the same URL form. It removes its subscription when the component unmounts. Local route changes do not update the host URL. Validate incoming routes before you load data. Authorize data access on the server. Encode the plugin ID, tool name, and complete app-relative URL. The app-relative URL must start with / and must not contain a fragment.
Desktop links use codex://plugins/<plugin-id>/app/<tool-name>?path=<encoded-url>. iOS links use the chatgpt scheme. Custom marketplace links include @<marketplace> after the plugin ID. See the deep-link specification for the native formats.

Add structured settings and actions

Register one settings pair before you start the server. server.settings() creates read and update tools. The host discovers these tools through the openai/settings capability. This example keeps one shared settings object in memory. The object resets when the process restarts.
For a production app, authorize each callback through its RequestContext argument. Scope stored values to the correct account. Persist partial updates atomically before you return all effective values. The framework does not provide storage, account scoping, or concurrency control. The read callback receives ctx. The update callback receives set and ctx. The context includes ctx.request, ctx.signal, and ctx.client. OAuth servers also expose ctx.auth.

Define fields and layout

Register at least one field. Fields support boolean, string, string enum, number, and integer schemas. Use a Standard Schema library with JSON Schema conversion, such as Zod. Give each field a non-blank title. You can also supply a description. Primitive schema constraints still apply. Use primitive schemas without value transforms. Return an effective value for every field from both callbacks. This rule also applies to optional fields and fields with schema defaults. The wire schema omits default annotations. Apply application defaults in the read callback. Use kind: "property" for a field control. Use kind: "tool" for a same-server action button. Action tools must accept {} and exist before the server starts. The MCP SDK validates action arguments when the host calls the tool. The host places fields absent from layout in an Other settings group. A regular tool action returns text feedback in settings. A view-bound tool action opens an MCP App modal. See the structured settings contract for host behavior.

Check settings tools

The default tool names are settings.read and settings.update. Set readTool and updateTool to use distinct, unused names. The registration advertises the pair during modern discovery and legacy initialization. Updates preserve fields absent from set through your callback. Empty patches, unknown keys, invalid values, and incomplete callback results produce tool errors.

Set resource display modes

Export viewConfig directly from views/<name>/view.tsx to declare supported modes and an initial preference. Build and development tools read the declaration before the view starts.
displayModes must include inline and contain no duplicates. The default list is ["inline", "fullscreen", "pip"]. autoResize defaults to true. With autoResize: false, use useSendSizeChanged() to report the view size. preferredDisplayMode accepts inline or fullscreen. The preference must appear in displayModes. The preference has no default, and the host can ignore it. Entrypoint launch behavior remains separate from this preference for ordinary tool calls. The HTML resource content advertises the ChatGPT subset in _meta["openai/ui"]:
The browser advertises the full configured list through MCP Apps capabilities. This list can include pip for other hosts.

Keep the declaration static

Use object literals, array literals, local const references, and spreads. TypeScript as and satisfies wrappers also work. Imported values and runtime expressions cause an extraction error when tooling detects the named configuration. Keep the configuration and its shared object or array references immutable. Tooling rejects writes to those references and calls that could mutate them during module initialization, including calls through local helpers. Components can read the configuration through calls such as useState and useMemo after initialization.
Declare and export viewConfig locally in the view module. A value export * without a local configuration causes an extraction error because tooling cannot resolve the configuration through another module. Type-only wildcard exports are allowed, as are unrelated value wildcards alongside a local configuration.

Request a mode at runtime

Use useDisplayMode() for mode changes after initialization. Render from the reported displayMode. A completed request does not guarantee that the host changed the mode.
availableDisplayModes contains the intersection of the view modes and host modes. If the host supplies no mode list, only inline is available. Requests for other modes reject before the runtime sends them.

Verify the integration

  1. Call tools/list to check entrypoint metadata and settings tool names.
  2. Call an entrypoint tool with its expected empty or file arguments.
  3. Call settings.read, then update one field and check the complete result.
  4. Run npx mcp-use build to check static viewConfig extraction.
  5. Read the HTML view resource to check its display metadata.
  6. Open the app in a supporting host to check launchers, routes, settings actions, and presentation.
Use MCP Apps interactivity for tool calls and host interactions inside the view. Use model context to share the view state with the model.