---
schema: rendered-html/v1
title: "Architecture - FastMCP"
source_url: https://gofastmcp.com/apps/architecture
generated_at: 2026-06-11T19:37:38.251914+00:00
---




[FastMCP home page![light logo](images/gofastmcp__com__apps__architecture__wordmark.avif)![dark logo](images/gofastmcp__com__apps__architecture__wordmark-white.avif)](gofastmcp__com.md)


- [PrefectHQ/fastmcp](https://github.com/PrefectHQ/fastmcp "PrefectHQ/fastmcp")
- [PrefectHQ/fastmcp](https://github.com/PrefectHQ/fastmcp "PrefectHQ/fastmcp")



Reference

Architecture















  - Prefab Providers
  - [Development](gofastmcp__com__apps__development.md)
  - [Examples](gofastmcp__com__apps__examples.md)
  - [Architecture](/apps/architecture)








- [The pipeline](#the-pipeline)
- [Tool registration](#tool-registration)
  - [The app=True flag](#the-app%3Dtrue-flag)
  - [FastMCPApp registration](#fastmcpapp-registration)
- [Serialization](#serialization)
  - [PrefabApp.to\_json()](#prefabapp-to_json)
  - [The \_meta.fastmcp.app tag](#the-_meta-fastmcp-app-tag)
  - [ToolResult assembly](#toolresult-assembly)
- [Tool call routing](#tool-call-routing)
  - [The get\_app\_tool bypass](#the-get_app_tool-bypass)
  - [Provider delegation](#provider-delegation)
- [The renderer](#the-renderer)
  - [The shared resource](#the-shared-resource)
  - [postMessage communication](#postmessage-communication)
  - [AppBridge](#appbridge)
- [The dev server](#the-dev-server)
  - [Proxy architecture](#proxy-architecture)
  - [The launch flow](#the-launch-flow)

Reference

# Architecture


How FastMCP apps work under the hood — from Python to pixels.


New in version `3.2.0`
You don’t need this page to build apps. It’s for when something isn’t rendering the way you expect, when UI tool calls aren’t reaching your server, or when you’re writing [custom HTML apps](gofastmcp__com__apps__low-level.md) and need to understand the protocol directly.

## [​](#the-pipeline) The pipeline

An MCP app moves through five stages from Python to pixels:

Python components  →  JSON tree  →  structuredContent  →  Renderer iframe  →  Host UI

You write Prefab components. FastMCP serializes them to a JSON component tree and delivers it as `structuredContent` on the tool result. The host loads the Prefab renderer in a sandboxed iframe, pushes the JSON in, and the renderer paints the UI. If the UI calls server tools, it talks back through the same `postMessage` channel.
The sections below walk each stage.

## [​](#tool-registration) Tool registration

When you mark a tool with `app=True` or `@app.ui()`, FastMCP wires up the metadata and renderer resource that the protocol requires.

### [​](#the-app=true-flag) The `app=True` flag

`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP checks whether the tool’s return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If it qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool’s `meta["ui"]` dict.
This expansion also registers the shared Prefab renderer resource (below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says “render me with `ui://prefab/renderer.html`” and the host fetches that resource when it displays the result.
Type inference works the same way. If the return type is a Prefab type and you haven’t set `app` explicitly, FastMCP auto-wires the metadata as if you’d written `app=True`.

### [​](#fastmcpapp-registration) FastMCPApp registration

`FastMCPApp` uses the same mechanism but adds two things. First, it tags every tool — both `@app.ui()` entry points and `@app.tool()` backends — with `meta["fastmcp"]["app"]` set to the app’s name. That tag lets the server identify which app a tool belongs to when routing UI calls.
Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (LLM-visible). Backend tools default to `["app"]` (UI-only). Hosts use this to filter the tool list.

## [​](#serialization) Serialization

When a Prefab tool runs, its return value — a `PrefabApp` or a bare `Component` — becomes a JSON blob the renderer can interpret.

### [​](#prefabapp-to_json) `PrefabApp.to_json()`

The entry point is `PrefabApp.to_json()`. It walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function’s registered name. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` on the wire. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.

### [​](#the-_meta-fastmcp-app-tag) The `_meta.fastmcp.app` tag

After `to_json()` produces the tree, FastMCP injects `_meta.fastmcp.app` with the app’s name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms (below).

### [​](#toolresult-assembly) ToolResult assembly

The final tool result has two parts: `content` (a list of `TextContent` blocks for the LLM) and `structuredContent` (the JSON tree for the renderer). By default, Prefab tools send `"[Rendered Prefab UI]"` as the text content — just enough for the LLM to know something was rendered. If you return a `ToolResult` explicitly, you control both halves.

## [​](#tool-call-routing) Tool call routing

Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.

### [​](#the-get_app_tool-bypass) The `get_app_tool` bypass

Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer still uses the original name.
`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This walks the provider tree directly, skipping transforms. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app.
That’s why `CallTool("save_contact")` keeps working when the server is mounted under a namespace. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find it without transforms in the way.
Authorization still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool’s `auth` config before executing.

### [​](#provider-delegation) Provider delegation

`get_app_tool` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server’s `get_app_tool`. Backend tools are reachable through any depth of composition.

## [​](#the-renderer) The renderer

The Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI.

### [​](#the-shared-resource) The shared resource

FastMCP registers the renderer as a `ui://prefab/renderer.html` resource with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. All Prefab tools on a server share this single resource.
The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the CDN domains the renderer needs. Hosts use this to configure the iframe’s Content Security Policy.

### [​](#postmessage-communication) `postMessage` communication

The renderer lives in a sandboxed iframe and communicates with the host using `postMessage`. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) spec:
The host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
The response flows back the same way: server → host → iframe via `postMessage`, and the renderer updates state with the result.

### [​](#appbridge) AppBridge

The `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (safe area insets, theme preferences). The Prefab renderer uses it internally; you only touch it directly when building [custom HTML apps](gofastmcp__com__apps__low-level.md).

## [​](#the-dev-server) The dev server

`fastmcp dev apps` simulates the host-side behavior locally without a real MCP client.

### [​](#proxy-architecture) Proxy architecture

Two HTTP servers. Your MCP server runs on port 8000 with the Streamable HTTP transport. The dev UI runs on port 8080 and serves a picker page that lists your app tools.
A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This matters because the renderer iframe runs on `localhost:8080` and your MCP server runs on `localhost:8000` — without the proxy, the renderer’s `callServerTool` requests would be cross-origin and the browser would block them. The proxy keeps everything same-origin from the iframe’s perspective.

### [​](#the-launch-flow) The launch flow

When you select a tool and click launch, the dev UI calls the tool through the proxy, receives the `structuredContent` response, and opens a new tab. That tab loads the tool’s renderer resource (via the proxy), creates an AppBridge, and pushes the tool result into the renderer. From here on it matches what a real host provides: the renderer displays the UI, and any `CallTool` actions route back through the proxy to your server.
Auto-reload is on by default, so changes to your server code restart the MCP server automatically. The dev UI keeps running — relaunch the tool to see changes.

[Examples

Previous](gofastmcp__com__apps__examples.md)[The FastMCP Client

Next](gofastmcp__com__clients__client.md)






