What Is an MCP App? Architecture, Hosts, and How to Build One (September 2026)

An MCP App connects an MCP tool to an interactive web UI inside an AI host. The tool still returns useful data, while a compatible host can display a dashboard, form, or workflow alongside the conversation. The UI is optional, so the tool should still answer the request when a host cannot render it.
TL;DR: An MCP App is an MCP tool plus a ui:// HTML resource. The tool definition links to the resource with _meta.ui.resourceUri. A compatible host renders it in a sandboxed iframe and passes tool data and host context through a bridge. Keep a useful text result for hosts without UI support.
MCP Apps vs MCP Servers
An MCP server exposes tools, resources, and prompts. A plain MCP tool might return this:
{
"content": [{ "type": "text", "text": "The account has 12 open invoices." }],
"structuredContent": {
"openInvoiceCount": 12,
"totalDue": 4812
}
}
That is enough when the user only needs an answer. It is not enough when the user needs to compare rows, approve a change, edit a draft, filter a chart, or step through a workflow.
An MCP App keeps the tool and adds a resource. The tool can return structured data, while a compatible host shows an interface that renders it. A billing tool can return invoice data and open an invoice dashboard. A code review tool can return findings and open a review UI. A booking tool can return available slots and open a calendar picker.
The short version:
- An MCP server gives the host callable tools and readable resources.
- An MCP App links a tool definition to an interactive resource.
- The app UI runs in an iframe, not in the host page.
- The model sees the tool output and selected app context, not arbitrary DOM internals.
For a deeper protocol primer, read MCP Concepts Explained.
The Current Architecture
An MCP App has four moving pieces.
The MCP server owns the tools. It validates input, runs business logic, returns content and optional structuredContent, and advertises UI metadata on the tool definition.
The app resource is the UI bundle. In practice this is usually React, Vue, Svelte, or plain HTML and JavaScript. The resource can render tool output, hold app state, call server tools through the host, request display mode changes, and send user-visible updates back into the conversation.
The host is the runtime that embeds the app. ChatGPT Apps, interactive Claude Connectors, IDE integrations, and other MCP-compatible hosts can decide when to call tools, how to show the iframe, which display modes are available, and which host-specific APIs exist.
The bridge is the communication layer between the iframe and host. The MCP Apps extension defines a JSON-RPC style bridge over window.postMessage, so the resource can receive tool data and context without reaching into the host page.
That separation is the point. Your server owns data and side effects. Your resource owns UI. The host owns conversation placement and permissions. The bridge is the narrow channel between them.
How a Tool Opens an App
The exact metadata differs a bit by host and SDK, but the shape is consistent:
- The server registers an HTML resource with a
ui://URI and thetext/html;profile=mcp-appMIME type. - The server registers a tool whose definition includes
_meta.ui.resourceUriwith that same URI. - During discovery, a compatible host sees the URI in
tools/listand can fetch or cache the resource. - The host calls the tool and renders or updates the resource iframe, possibly using a resource it fetched earlier.
- The host passes the tool result to the iframe, which renders the UI.
In the MCP Apps extension, _meta.ui.resourceUri belongs on the tool definition, not the tool call result. The result carries content, optional structuredContent, and optional result _meta. ChatGPT also accepts _meta["openai/outputTemplate"] as a compatibility alias for the UI URI. For new UI, OpenAI’s ChatGPT UI guide recommends the shared MCP Apps field and bridge, with ChatGPT-specific extensions only where needed.
If you are building for more than one host, treat the MCP Apps metadata as the portable base and add host-specific fields only where they are needed. That keeps the same tool and resource usable in ChatGPT, Claude, and future hosts.
How to Handle a Host Without App UI
An MCP client can support tools without supporting embedded UI. The MCP Apps extension overview treats UI as a progressive addition to an ordinary MCP tool. During initialization, a client can advertise UI support through its extension capabilities. The MCP Apps capability helper returns those settings so you can check whether the client lists the text/html;profile=mcp-app MIME type.
Use this checklist before depending on a view:
- Return a concise
contentanswer even when you also return structured data. A text-only client can still complete the task. - Check UI capability when the server needs to choose between a UI-enabled tool and a text-only fallback.
- Keep the resource URI and the tool definition in sync. A broken URI leaves a capable host without the view.
- Test the same tool in a UI host and a tool-only client. Confirm the user can understand the result in both.
For a deeper implementation guide, see MCP App capability detection and fallbacks. For the resource format, see MCP App UI resources.
What the Model Sees
This is the most common design mistake in MCP Apps: developers assume the model can read whatever is on screen. It cannot, and it should not.
The model sees the tool result and any app state you intentionally expose back to the host. The user sees the iframe. If the user clicks a filter, checks a box, or approves a draft, your app must sync that state through the host bridge if the model needs to act on it later.
That gives you three useful channels:
structuredContentfor data the model should reason over._metafor app-only data the component needs but the model should not read.- App state for user interactions that should become model context.
Keep those channels separate. Put sensitive display-only details in _meta, not structuredContent. Put model-relevant user choices in app state, not hidden React state. For a longer guide, see MCP App Tool Results.
What You Can Build
MCP Apps are best when a text answer would force the user to do too much work. Good fits include:
- Review screens for approvals, diffs, policies, and generated drafts.
- Dashboards that need sorting, filtering, grouping, or drill-down.
- Forms where the model can prefill data and the user confirms it.
- Editors for documents, code, queries, workflows, and structured records.
- Maps, timelines, dependency graphs, and other visual views.
- Multi-step tasks where the user alternates between chat and UI.
Avoid building an app when a simple tool response is enough. If the user asks for the weather, text may be fine. If they need to compare hourly forecast risk across five job sites and pick a dispatch plan, an app starts to make sense.
A Minimal sunpeak Example
With sunpeak, a resource is a React component and a tool is a TypeScript handler. The framework discovers both from the file system and wires the tool to the resource.
// src/resources/invoices/invoices.tsx
import { useToolData, SafeArea } from 'sunpeak';
import type { ResourceConfig } from 'sunpeak';
type InvoiceSummary = {
customer: string;
openInvoiceCount: number;
totalDue: number;
};
export const resource: ResourceConfig = {
description: 'Show an invoice summary',
};
export default function InvoicesResource() {
const { output } = useToolData<unknown, InvoiceSummary>(undefined, undefined);
return (
<SafeArea>
<h1>{output.customer}</h1>
<p>{output.openInvoiceCount} open invoices</p>
<strong>${output.totalDue.toLocaleString()}</strong>
</SafeArea>
);
}
The matching tool returns the data that the component renders:
// src/tools/show-invoices.ts
import { z } from 'zod';
import type { AppToolConfig } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'invoices',
title: 'Show Invoices',
description: 'Show open invoices for a customer',
annotations: { readOnlyHint: true },
};
export const schema = {
customerId: z.string().describe('Customer ID to look up'),
};
export default async function showInvoices(args: { customerId: string }) {
return {
structuredContent: {
customer: args.customerId,
openInvoiceCount: 12,
totalDue: 4812,
},
};
}
Run it locally:
npx sunpeak new
pnpm dev
The sunpeak inspector renders the resource in replicated ChatGPT and Claude runtimes on localhost. You can switch hosts, themes, display modes, and tool states without deploying to a live host.
Host Support and Portability
The safest way to talk about host support is by capability, not by brand list. A host can support MCP tools without supporting embedded app UI. A host can support embedded UI but expose different display modes, CSP rules, auth flows, file handling, or state behavior.
When you plan an MCP App, ask these questions:
- Does the host support app resources attached to tool results?
- Which display modes can it render?
- Which bridge methods can the resource call?
- How does it scope app state across messages and reloads?
- What CSP and network rules apply inside the iframe?
- Which host-specific APIs are worth using behind feature checks?
Build the core app against the shared MCP App bridge. Then add host-specific behavior in narrow layers, for example with sunpeak/chatgpt or sunpeak/claude imports. That gives you a portable base without blocking richer host behavior.
For a hands-on portability guide, see Building One MCP App for ChatGPT and Claude.
Security Boundaries
An MCP App is a web app running in someone else’s conversation UI, so the boundaries matter.
The iframe should not be able to read the host DOM. The host should not hand the iframe the whole conversation. The model should not see app-only data unless you put it in structuredContent or sync it as app state. Network access should be limited through CSP, and external API calls should go through the server unless the client truly needs direct access.
Use this rule of thumb:
- Put secrets and privileged API calls on the server.
- Put model-readable facts in
structuredContent. - Put UI-only details in
_meta. - Put user choices the model needs in app state.
- Put allowed client network domains in resource CSP.
That structure makes the app easier to audit because each data path has a purpose. For more detail, read MCP App CSP Domains and Security Testing for MCP Apps.
Testing MCP Apps
You cannot fully test an MCP App by opening a component in a browser. The component only works when a host injects tool data, display mode, theme, app state, and bridge APIs.
Test these layers:
- Tool tests: schema validation, auth, errors, and
structuredContentshape. - Resource unit tests: rendering, empty states, loading states, and app state transitions.
- Inspector E2E tests: iframe rendering through a simulated host.
- Visual tests: display modes, themes, widths, and safe areas.
- Cross-host tests: ChatGPT and Claude behavior, especially state and bridge actions.
- Live tests: final smoke checks against real hosts before launch.
sunpeak uses simulation files to pin app states. A simulation can define tool input, tool output, user message, host, theme, display mode, and server tool mocks. That makes local and CI tests deterministic, which matters because live host testing is slower and harder to reproduce.
For a complete testing plan, read The Complete Guide to Testing ChatGPT Apps and MCP Apps and MCP App Testing Strategy.
When to Use a Framework
You can build an MCP App directly from the protocol, but you will need to solve the same set of problems:
- Register resources and connect them to tool results.
- Bundle UI resources for host iframes.
- Handle the host bridge, display mode, theme, and context.
- Keep ChatGPT and Claude compatibility fields straight.
- Mock host state for local development.
- Run tests inside a realistic iframe runtime.
A framework is useful when you want those pieces handled in one project shape. sunpeak gives you the inspector, file-based tools and resources, typed React hooks, app UI components, simulation fixtures, Playwright tests, visual tests, and CI-friendly host replicas.
The tradeoff is that you accept a project convention. For most teams, that is a good trade because the hard part of MCP Apps is not rendering a React component. The hard part is making that component behave correctly across host data, state, security, and testing boundaries.
The Practical Build Path
Start small:
- Pick one tool where UI clearly beats text.
- Define the
structuredContentshape before writing the component. - Build a read-only resource first.
- Add app state only when the model needs to know what the user did.
- Add host-specific features behind capability checks.
- Write simulations for success, empty, loading, error, and cancelled states.
- Run inspector E2E tests before trying a live host.
That path keeps the first version simple and gives you a clean place to add richer behavior. Once the read-only app is stable, add state, server tool calls, display mode transitions, file handling, auth, and live host tests.
If you want the shortest path, start with the MCP App tutorial. If you are choosing architecture first, read How to Build an MCP App and How to Choose an MCP App Framework.
Build and Test with sunpeak
sunpeak is an open-source MCP App framework and testing framework. It helps you build one app for ChatGPT, Claude, and other MCP hosts, inspect it locally, and test it in CI without paid host accounts or AI credits.
Create a project:
npx sunpeak new
Inspect an existing MCP server:
npx sunpeak inspect --server http://localhost:8000/mcp
Add tests to an existing server:
npx sunpeak test init --server http://localhost:8000/mcp
The useful mental model is simple: MCP gives the model tools, MCP Apps give the user UI, and a good framework helps you prove the boundary between the two works before real users find the edge cases.
Get Started
npx sunpeak newFurther Reading
- MCP App Tutorial - build and test your first MCP App
- How to Build an MCP App - architecture for cross-host interactive UI
- MCP App Resource Metadata - CSP, permissions, and host fields
- MCP App Tool Results - content, structuredContent, and _meta
- MCP App UI Resources - URI, MIME type, and resource links
- MCP App Capability Detection and Fallbacks
- Interactive MCP Apps with useAppState - two-way state and model context
- Complete Guide to Testing ChatGPT Apps and MCP Apps
- MCP App Framework - portable app framework for ChatGPT and Claude
- MCP App Inspector - test MCP Apps locally
- sunpeak Docs - framework, inspector, and testing guides
- MCP Specification - tools, resources, and protocol model
- MCP Apps Extension - standard app UI model
- ChatGPT UI Guide - shared bridge and optional extensions
- MCP Apps UI Capability - detecting a compatible host
Frequently Asked Questions
What is an MCP App?
An MCP App is an interactive web UI attached to a Model Context Protocol tool. The tool definition names a UI resource with _meta.ui.resourceUri. When the tool runs, a compatible host reads that resource, renders it in a sandboxed iframe, and delivers the tool result to the UI.
What is the difference between an MCP App and an MCP server?
An MCP server exposes tools, resources, and prompts to a host. An MCP App uses those building blocks and links a tool definition to an interactive UI resource. The tool still returns useful content, with optional structuredContent, while a compatible host can show the user an interface.
Do MCP Apps replace ChatGPT Apps or Claude Connectors?
No. ChatGPT Apps and interactive Claude Connectors are host-specific ways to ship MCP-powered experiences. MCP Apps are the portable app pattern underneath: tools return data, resources render UI, and host bridges connect the iframe to the conversation.
How does an MCP App connect a tool to UI?
The tool definition advertises _meta.ui.resourceUri in tools/list. The host reads the matching ui:// resource, which uses the text/html;profile=mcp-app MIME type, and renders it in an iframe. The tool call result then supplies content and optional structuredContent. ChatGPT also accepts openai/outputTemplate as a compatibility alias for the resource URI.
Can one MCP App run in multiple hosts?
Yes, across hosts that support the MCP Apps UI extension. Check the host capabilities before assuming it can render the resource. Keep the tool useful as text when UI is unavailable, use the shared bridge for core interactions, and feature-detect any host-specific API.
What should I test in an MCP App?
Test the tool contract, structuredContent shape, resource metadata, iframe rendering, display modes, themes, app state, and any host-specific paths. MCP Apps fail at the boundaries between server output, host rendering, and UI state, so tests need to cover those boundaries.
Do I need a paid ChatGPT or Claude account to build MCP Apps?
No. sunpeak includes a local inspector that replicates ChatGPT and Claude runtimes. You can render resources, switch hosts and display modes, and run Playwright tests locally or in CI before using a live host.
What is the fastest way to start building an MCP App?
Start with a small tool that returns typed structuredContent and a resource that reads that data. With sunpeak, run npx sunpeak new, add a resource in src/resources, add a tool in src/tools, then run pnpm dev to inspect and test the app locally.