ui field; the components it returns are ordinary return values. This is why apps compose with everything else: a tool that renders UI is still a tool, so it still validates input, still reaches the client through context, and can still be mounted, transformed, or guarded by auth like any other.
You build apps with FastMCPApp, a thin wrapper around FastMCP that adds the two registration helpers apps need. Everything you already know about FastMCP applies — the wrapper only gives you a cleaner way to declare which tools render UI and which exist purely to back it.
app.ts
Entrypoints and backend tools
An app is built from two kinds of tools, and the distinction is about who is allowed to call them. An entrypoint is the door into a screen. It is visible to both the model and the app, so the model can decide to open it and the rendered UI can re-invoke it.app.entrypoint(config, handler) registers a tool with this dual visibility and, at the same time, registers a stub resource at ui://${name} so the host has somewhere to read the UI bundle from. The handler returns a component tree, which becomes the screen.
A backend tool is the wiring behind the screen. When a button submits a form or confirms an action, that click needs to land on a tool — but it would be noise for the model to see that tool in tools/list, because the model never calls it directly. app.backendTool(config, handler) registers a tool that the app can call but the model cannot, by giving it app-only visibility. If you do want the model to reach a backend tool as well, pass visibility: ['model', 'app'] to expose it to both.
app.ts
actionRef on the button names the backend tool the click should call. It is resolved at render time so that the reference still points at the right tool even when the app is mounted under a prefix — see Components for how actions are wired and Built-in providers for prepackaged versions of this entrypoint-plus-backend pattern.
Visibility
Visibility is the single control that decides whether a tool is exposed to the model, to the app, or to both. It lives on the tool’sui config as a visibility array, and the entrypoint and backend-tool helpers are just convenient defaults over it: ['model', 'app'] for an entrypoint, ['app'] for a backend tool.
The model only ever sees tools whose visibility includes 'model'. listTools filters out everything else, so an app-only backend tool is genuinely invisible to the LLM — it cannot call it, suggest it, or reason about it. For tools that the model can see, a UI-capable client also receives a _meta.ui block carrying the resourceUri and visibility, which is how the host knows to render a screen rather than display text.
app.ts
visibility by hand once you are using FastMCPApp, because the helpers encode the right default for each role. Reach for the raw field when a plain FastMCP tool needs UI behavior, or when a backend tool also has a legitimate reason to be model-callable.
Capability negotiation
Apps are an MCP extension, keyedio.modelcontextprotocol/ui, and a connection uses UI only when both sides agree on it. Agreement is a match on a media type: the server advertises that it can serve the app profile text/html;profile=mcp-app, and a UI-capable client advertises that it can render the same profile. Where the server advertises depends on the protocol era — a legacy client reads it from the initialize handshake, a modern (2026-07-28) client reads it from the server/discover document — but the advertisement is identical, and the server only makes it when it actually has UI to render: at least one tool with a ui field, or one resource under a ui:// URI. A server with no apps looks like any other server, and adding an app is what turns the extension on.
The client’s half is a hard requirement. Per SEP-1865 a UI-capable client must declare mimeTypes: ['text/html;profile=mcp-app'] as the value of its io.modelcontextprotocol/ui capability, naming the profiles it can paint. The server checks that list on every request — a bare presence of the extension key is not enough — and treats the client as UI-capable only when the app profile appears in it. That check is what decides whether a tool response carries its _meta.ui block and whether a screen renders at all.
A client that passes gets the full experience: _meta.ui on tool responses, the UI resource, the rendered screen. A client that does not — a terminal client, a logging pipeline, a model host without UI support — should still be able to use your tools. When such a client calls a UI tool and the handler returns a structured-content response, the framework strips the UI payload and substitutes a plain text content block reading [UI not available in this client]. The call still succeeds; it simply degrades to text. This is what lets you ship one server that renders rich UI where it can and stays usable everywhere else, with no branching in your handler.
Choosing an approach
There are three ways to put UI in front of a model, and they trade authoring effort against flexibility. Pick by how stable and how varied your screens are. When your screens are stable and known ahead of time — a search panel, an order view, a settings page — author them directly as entrypoints returning components. You write the layout once, in TypeScript, and you get full control over every element. This is the happy path, and most apps live here. When a screen follows a common interaction pattern — ask the user to approve something, pick from a list, upload a file, fill in a form — reach for a provider instead of hand-building it. Providers are prepackaged mini-apps that register the entrypoint and its backing tools for you, so a confirm/deny dialog or a validated form is oneaddProvider call rather than a screen you assemble and maintain.
When the screens are exploratory and composed at runtime — where you cannot know in advance what the user will want to see — let the model build the UI itself with generative UI. The model discovers the component catalog and assembles a tree on the fly, which the server renders inside a sandbox. This is the most flexible route and the one to use sparingly: you trade the determinism of authored screens for the ability to render things you never wrote.