mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-16 02:29:11 +02:00
* Add prefab auto-wiring for MCP Apps (#3119) Tools that return prefab types (UIResponse, Component) automatically get wired to the shared prefab renderer resource. Works via app=True, return type inference, or both. * Prefab compatibility updates * Use published prefab-ui >=0.6.0, remove local source override * Migrate UIResponse to PrefabApp for Prefab UI integration PrefabApp is a pure data object with to_json(), html(), and csp() methods. Tools can return PrefabApp, bare Components, or ToolResult with structured_content for custom LLM fallback text. * Add Prefab UI apps documentation * Add mini apps and full apps documentation pages Mini apps covers the common single-screen patterns: charts (bar, line, area, pie), data tables with sorting/search/pagination, forms (manual and Pydantic-generated), status displays, conditional content, and layout composition with tabs and accordions. Full apps covers multi-page applications using Pages/Page components, shared state across pages, and using ToolCall with result_key for server-driven state updates. * Reframe apps docs around motivation, add generative UIs page The docs now lead with the problem — MCP tools stuff data into the LLM context window, and building HTML/JS/CSS frontends is a non-starter for Python developers — before introducing Prefab as the solution. Mini apps are framed as the primary use case: focused, single-purpose UIs that present data visually and collect structured input. New generative UIs page covers the concept of LLMs producing component JSON directly, enabling adaptive dashboards, tailored forms, and exploratory workflows. * Tag Prefab docs pages as SOON instead of NEW * Rename Low-Level API to Custom HTML Apps The page is about using the MCP Apps extension directly, not a FastMCP or Prefab internal API. Reframed to make clear this is the open MCP protocol with FastMCP providing convenience wrappers. * Tighten apps docs and widen content area Strip editorial motivation from all app doc pages — let code examples do the talking. Add content-area max-width override (44rem) to style.css. * Restructure apps docs, fix code issues Rename Prefab UI → Prefab Apps, mini-apps → patterns, remove generative-uis and full-apps pages. Rewrite prefab page to lead with what users do (declare a UI, return it) before explaining internals. Patterns page now has fully self-contained copy-pasteable examples with explicit imports and links to prefab docs. Forms show the two-tool pattern (form + handler). Add patterns_server.py example. Code fixes: move get_args to module-level import, remove dead AuthCheckCallable type alias, fix ToolCall→CallTool in all docs. * Remove unused ToolResult import from chart_server * Handle composite Prefab types in type inference and schema suppression _has_prefab_return_type and the output schema suppression logic only checked bare classes, missing unions (Column | None) and Annotated wrappers (Annotated[PrefabApp | None, ...]). Recurse through Union, types.UnionType, and Annotated to detect Prefab types in composite annotations.
303 lines
10 KiB
Text
303 lines
10 KiB
Text
---
|
|
title: Custom HTML Apps
|
|
sidebarTitle: Custom HTML
|
|
description: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.
|
|
icon: code
|
|
tag: NEW
|
|
---
|
|
|
|
import { VersionBadge } from '/snippets/version-badge.mdx'
|
|
|
|
<VersionBadge version="3.0.0" />
|
|
|
|
The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol that lets tools return interactive UIs — an HTML page rendered in a sandboxed iframe inside the host client. [Prefab UI](/apps/prefab) builds on this protocol so you never have to think about it, but when you need full control — custom rendering, a specific JavaScript framework, maps, 3D, video — you can use the MCP Apps extension directly.
|
|
|
|
This page covers how to write custom HTML apps and wire them up in FastMCP. You'll be working with the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resource and CSP management.
|
|
|
|
## How It Works
|
|
|
|
An MCP App has two parts:
|
|
|
|
1. A **tool** that does the work and returns data
|
|
2. A **`ui://` resource** containing the HTML that renders that data
|
|
|
|
The tool declares which resource to use via `AppConfig`. When the host calls the tool, it also fetches the linked resource, renders it in a sandboxed iframe, and pushes the tool result into the app via `postMessage`. The app can also call tools back, enabling interactive workflows.
|
|
|
|
```python
|
|
import json
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server.apps import AppConfig, ResourceCSP
|
|
|
|
mcp = FastMCP("My App Server")
|
|
|
|
# The tool does the work
|
|
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
|
|
def generate_chart(data: list[float]) -> str:
|
|
return json.dumps({"values": data})
|
|
|
|
# The resource provides the UI
|
|
@mcp.resource("ui://my-app/view.html")
|
|
def chart_view() -> str:
|
|
return "<html>...</html>"
|
|
```
|
|
|
|
## AppConfig
|
|
|
|
`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`:
|
|
|
|
```python
|
|
from fastmcp.server.apps import AppConfig
|
|
```
|
|
|
|
On **tools**, you'll typically set `resource_uri` to point to the UI resource:
|
|
|
|
```python
|
|
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
|
|
def my_tool() -> str:
|
|
return "result"
|
|
```
|
|
|
|
You can also pass a raw dict with camelCase keys, matching the wire format:
|
|
|
|
```python
|
|
@mcp.tool(app={"resourceUri": "ui://my-app/view.html"})
|
|
def my_tool() -> str:
|
|
return "result"
|
|
```
|
|
|
|
### Tool Visibility
|
|
|
|
The `visibility` field controls where a tool appears:
|
|
|
|
- `["model"]` — visible to the LLM (the default behavior)
|
|
- `["app"]` — only callable from within the app UI, hidden from the LLM
|
|
- `["model", "app"]` — both
|
|
|
|
This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions.
|
|
|
|
```python
|
|
@mcp.tool(
|
|
app=AppConfig(
|
|
resource_uri="ui://my-app/view.html",
|
|
visibility=["app"],
|
|
)
|
|
)
|
|
def refresh_data() -> str:
|
|
"""Only callable from the app UI, not by the LLM."""
|
|
return fetch_latest()
|
|
```
|
|
|
|
### AppConfig Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `resource_uri` | `str` | URI of the UI resource. Tools only. |
|
|
| `visibility` | `list[str]` | Where the tool appears: `"model"`, `"app"`, or both. Tools only. |
|
|
| `csp` | `ResourceCSP` | Content Security Policy for the iframe. |
|
|
| `permissions` | `ResourcePermissions` | Iframe sandbox permissions. |
|
|
| `domain` | `str` | Stable sandbox origin for the iframe. |
|
|
| `prefers_border` | `bool` | Whether the UI prefers a visible border. |
|
|
|
|
<Note>
|
|
On **resources**, `resource_uri` and `visibility` must not be set — the resource *is* the UI. Use `AppConfig` on resources only for `csp`, `permissions`, and other display settings.
|
|
</Note>
|
|
|
|
## UI Resources
|
|
|
|
Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. You don't need to set this manually.
|
|
|
|
```python
|
|
@mcp.resource("ui://my-app/view.html")
|
|
def my_view() -> str:
|
|
return "<html>...</html>"
|
|
```
|
|
|
|
The HTML can be anything — a full single-page app, a simple display, or a complex interactive tool. The host renders it in a sandboxed iframe and establishes a `postMessage` channel for communication.
|
|
|
|
### Writing the App HTML
|
|
|
|
Your HTML app communicates with the host using the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK. The simplest approach is to load it from a CDN:
|
|
|
|
```html
|
|
<script type="module">
|
|
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
|
|
|
|
const app = new App({ name: "My App", version: "1.0.0" });
|
|
|
|
// Receive tool results pushed by the host
|
|
app.ontoolresult = ({ content }) => {
|
|
const text = content?.find(c => c.type === 'text');
|
|
if (text) {
|
|
document.getElementById('output').textContent = text.text;
|
|
}
|
|
};
|
|
|
|
// Connect to the host
|
|
await app.connect();
|
|
</script>
|
|
```
|
|
|
|
The `App` object provides:
|
|
|
|
- **`app.ontoolresult`** — callback that receives tool results pushed by the host
|
|
- **`app.callServerTool({name, arguments})`** — call a tool on the server from within the app
|
|
- **`app.onhostcontextchanged`** — callback for host context changes (e.g., safe area insets)
|
|
- **`app.getHostContext()`** — get current host context
|
|
|
|
<Note>
|
|
If your HTML loads external scripts, styles, or makes API calls, you need to declare those domains in the CSP configuration. See [Security](#security) below.
|
|
</Note>
|
|
|
|
## Security
|
|
|
|
Apps run in sandboxed iframes with a deny-by-default Content Security Policy. By default, only inline scripts and styles are allowed — no external network access.
|
|
|
|
### Content Security Policy
|
|
|
|
If your app needs to load external resources (CDN scripts, API calls, embedded iframes), declare the allowed domains with `ResourceCSP`:
|
|
|
|
```python
|
|
from fastmcp.server.apps import AppConfig, ResourceCSP
|
|
|
|
@mcp.resource(
|
|
"ui://my-app/view.html",
|
|
app=AppConfig(
|
|
csp=ResourceCSP(
|
|
resource_domains=["https://unpkg.com", "https://cdn.example.com"],
|
|
connect_domains=["https://api.example.com"],
|
|
)
|
|
),
|
|
)
|
|
def my_view() -> str:
|
|
return "<html>...</html>"
|
|
```
|
|
|
|
| CSP Field | Controls |
|
|
|-----------|----------|
|
|
| `connect_domains` | `fetch`, XHR, WebSocket (`connect-src`) |
|
|
| `resource_domains` | Scripts, images, styles, fonts (`script-src`, etc.) |
|
|
| `frame_domains` | Nested iframes (`frame-src`) |
|
|
| `base_uri_domains` | Document base URI (`base-uri`) |
|
|
|
|
### Permissions
|
|
|
|
If your app needs browser capabilities like camera or clipboard access, request them via `ResourcePermissions`:
|
|
|
|
```python
|
|
from fastmcp.server.apps import AppConfig, ResourcePermissions
|
|
|
|
@mcp.resource(
|
|
"ui://my-app/view.html",
|
|
app=AppConfig(
|
|
permissions=ResourcePermissions(
|
|
camera={},
|
|
clipboard_write={},
|
|
)
|
|
),
|
|
)
|
|
def my_view() -> str:
|
|
return "<html>...</html>"
|
|
```
|
|
|
|
Hosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.
|
|
|
|
## Example: QR Code Server
|
|
|
|
This example creates a tool that generates QR codes and an app that renders them as images. It's based on the [official MCP Apps example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server). Requires the `qrcode[pil]` package.
|
|
|
|
```python expandable
|
|
import base64
|
|
import io
|
|
|
|
import qrcode
|
|
from mcp import types
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server.apps import AppConfig, ResourceCSP
|
|
from fastmcp.tools import ToolResult
|
|
|
|
mcp = FastMCP("QR Code Server")
|
|
|
|
VIEW_URI = "ui://qr-server/view.html"
|
|
|
|
|
|
@mcp.tool(app=AppConfig(resource_uri=VIEW_URI))
|
|
def generate_qr(text: str = "https://gofastmcp.com") -> ToolResult:
|
|
"""Generate a QR code from text."""
|
|
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
|
qr.add_data(text)
|
|
qr.make(fit=True)
|
|
|
|
img = qr.make_image()
|
|
buffer = io.BytesIO()
|
|
img.save(buffer, format="PNG")
|
|
b64 = base64.b64encode(buffer.getvalue()).decode()
|
|
|
|
return ToolResult(
|
|
content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
|
)
|
|
|
|
|
|
@mcp.resource(
|
|
VIEW_URI,
|
|
app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
|
|
)
|
|
def view() -> str:
|
|
"""Interactive QR code viewer."""
|
|
return """\
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="color-scheme" content="light dark">
|
|
<style>
|
|
body { display: flex; justify-content: center;
|
|
align-items: center; height: 340px; width: 340px;
|
|
margin: 0; background: transparent; }
|
|
img { width: 300px; height: 300px; border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="qr"></div>
|
|
<script type="module">
|
|
import { App } from
|
|
"https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
|
|
|
|
const app = new App({ name: "QR View", version: "1.0.0" });
|
|
|
|
app.ontoolresult = ({ content }) => {
|
|
const img = content?.find(c => c.type === 'image');
|
|
if (img) {
|
|
const el = document.createElement('img');
|
|
el.src = `data:${img.mimeType};base64,${img.data}`;
|
|
el.alt = "QR Code";
|
|
document.getElementById('qr').replaceChildren(el);
|
|
}
|
|
};
|
|
|
|
await app.connect();
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
```
|
|
|
|
The tool generates a QR code as a base64 PNG. The resource loads the MCP Apps JS SDK from unpkg (declared in the CSP), listens for tool results, and renders the image. The host wires them together — when the LLM calls `generate_qr`, the QR code appears in an interactive frame inside the conversation.
|
|
|
|
## Checking Client Support
|
|
|
|
Not all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):
|
|
|
|
```python
|
|
from fastmcp import Context
|
|
from fastmcp.server.apps import AppConfig, UI_EXTENSION_ID
|
|
|
|
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
|
|
async def my_tool(ctx: Context) -> str:
|
|
if ctx.client_supports_extension(UI_EXTENSION_ID):
|
|
# Return data optimized for UI rendering
|
|
return rich_response()
|
|
else:
|
|
# Fall back to plain text
|
|
return plain_text_response()
|
|
```
|