URL: https://gofastmcp.com/apps/architecture
Title: Architecture - FastMCP

Documentation Index
Fetch the complete documentation index at:
/llms.txt
Use this file to discover all available pages before exploring further.
Skip to main content
Meet
Prefect Horizon
, the enterprise MCP gateway built by the team behind FastMCP
FastMCP
home page
v3
Prefect Horizon
PrefectHQ/fastmcp
PrefectHQ/fastmcp
Search...
Navigation
Reference
Architecture
Search the docs...
Ctrl
K
Documentation
Get Started
Welcome!
Installation
Quickstart
Servers
Overview
Core Components
Working with Tools
MCP Providers
Interactivity
Extensibility
Auth
Deployment
Apps
Overview
Quickstart
NEW
FastMCPApp
NEW
Interactive Tools
NEW
Generative UI
NEW
Custom HTML
Reference
Prefab Providers
Development
Examples
Architecture
Clients
Overview
Client-Only Package
Transports
fastmcp-remote
Operations
UPDATED
Authentication
UPDATED
Integrations
Auth
Web Frameworks
AI Assistants
AI SDKs
MCP.json
More
Settings
CLI
Upgrading
Development
What's New
FAQ
On this page
The pipeline
Tool registration
The app=True flag
FastMCPApp registration
Serialization
PrefabApp.to_json()
The _meta.fastmcp.app tag
ToolResult assembly
Tool call routing
The get_app_tool bypass
Provider delegation
The renderer
The shared resource
postMessage communication
AppBridge
The dev server
Proxy architecture
The launch flow
Reference
Architecture
Copy page
How FastMCP apps work under the hood — from Python to pixels.
Copy page
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
and need to understand the protocol directly.
​
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
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
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
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
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()
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
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
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
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
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
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 Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI.
​
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
The renderer lives in a sandboxed iframe and communicates with the host using
postMessage
. The protocol follows the
MCP Apps extension
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
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
.
​
The dev server
fastmcp dev apps
simulates the host-side behavior locally without a real MCP client.
​
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
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
The FastMCP Client
Next
Ctrl
+I
discord
github
website
x
Powered by
This documentation is built and hosted on Mintlify, a developer documentation platform
Assistant
Responses are generated using AI and may contain mistakes.
\n\n
\n
\n \n
\n
