Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 69 additions & 64 deletions docs/development/v3-notes/v3-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,75 @@ fastmcp install stdio server.py

The command automatically detects the project directory and generates the appropriate `uv run` invocation, making it easy to integrate FastMCP servers with MCP clients.

### MCP Apps (SDK Compatibility)

Support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) — the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet — those are future phases.

**Registering tools with UI metadata:**

```python
from fastmcp import FastMCP
from fastmcp.server.apps import ToolUI, ResourceUI, ResourceCSP, ResourcePermissions

mcp = FastMCP("My Server")

# Register the HTML bundle as a ui:// resource with CSP
@mcp.resource(
"ui://my-app/view.html",
ui=ResourceUI(
csp=ResourceCSP(resource_domains=["https://unpkg.com"]),
permissions=ResourcePermissions(clipboard_write={}),
),
)
def app_html() -> str:
from pathlib import Path
return Path("./dist/index.html").read_text()

# Tool with UI — clients render an iframe alongside the result
@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html"))
async def list_users() -> list[dict]:
return [{"id": "1", "name": "Alice"}]

# App-only tool — visible to the UI but hidden from the model
@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html", visibility=["app"]))
async def delete_user(id: str) -> dict:
return {"deleted": True}
```

The `ui=` parameter accepts either a typed model (`ToolUI`, `ResourceUI`) or a raw dict for forward compatibility. It merges into `meta["ui"]` — alongside any other metadata you set.

**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly.

**Extension negotiation**: The server advertises `io.modelcontextprotocol/ui` in `capabilities.extensions`. UI metadata (`_meta.ui`) always flows through to clients — the MCP Apps spec assigns visibility enforcement to the host, not the server. Tools can check whether the connected client supports a given extension at runtime via `ctx.client_supports_extension()`:

```python
from fastmcp import Context
from fastmcp.server.apps import ToolUI, UI_EXTENSION_ID

@mcp.tool(ui=ToolUI(resource_uri="ui://dashboard"))
async def dashboard(ctx: Context) -> dict:
data = compute_dashboard()
if ctx.client_supports_extension(UI_EXTENSION_ID):
return data
return {"summary": format_text(data)}
```

**Key details:**
- `ToolUI` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional except for typical usage of `resource_uri`)
- `ResourceUI` fields: `csp`, `permissions`, `domain`, `prefers_border` — metadata for the resource itself when it's a UI bundle
- `csp` accepts a `ResourceCSP` model with structured domain lists: `connect_domains`, `resource_domains`, `frame_domains`, `base_uri_domains`
- `permissions` accepts a `ResourcePermissions` model: `camera`, `microphone`, `geolocation`, `clipboard_write` (each set to `{}` to request)
- Both models use `extra="allow"` for forward compatibility with future spec additions
- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`, `connectDomains`, `clipboardWrite`)
- Resource metadata (including CSP/permissions) is propagated to `resources/read` response content items so hosts can read it when rendering the iframe
- `ctx.client_supports_extension(id)` is a general-purpose method — works for any extension, not just MCP Apps
- `structuredContent` in tool results already works via `ToolResult` — MCP Apps clients use this to pass data into the iframe
- The server does not strip `_meta.ui` for non-UI clients; per the spec, visibility enforcement is the host's responsibility

**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.

Implementation: `src/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).

---

## 3.0.0beta1
Expand Down Expand Up @@ -658,70 +727,6 @@ STDIO transport bypasses all auth checks (no OAuth concept).

---

### MCP Apps (SDK Compatibility)

v3.0 adds Phase 1 support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) — the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Phase 1 is SDK compatibility only: extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet — those are future phases.

**Registering tools with UI metadata:**

```python
from fastmcp import FastMCP
from fastmcp.server.apps import ToolUI, ResourceUI, UI_MIME_TYPE

mcp = FastMCP("My Server")

# Register the HTML bundle as a ui:// resource
@mcp.resource("ui://my-app/view.html")
def app_html() -> str:
from pathlib import Path
return Path("./dist/index.html").read_text()

# Tool with UI — clients render an iframe alongside the result
@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html"))
async def list_users() -> list[dict]:
return [{"id": "1", "name": "Alice"}]

# App-only tool — visible to the UI but hidden from the model
@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html", visibility=["app"]))
async def delete_user(id: str) -> dict:
return {"deleted": True}
```

The `ui=` parameter accepts either a typed model (`ToolUI`, `ResourceUI`) or a raw dict for forward compatibility. It merges into `meta["ui"]` — alongside any other metadata you set.

**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly.

**Extension negotiation**: The server advertises `io.modelcontextprotocol/ui` in `capabilities.extensions`. UI metadata (`_meta.ui`) always flows through to clients — the MCP Apps spec assigns visibility enforcement to the host, not the server. Tools can check whether the connected client supports a given extension at runtime via `ctx.client_supports_extension()`:

```python
from fastmcp import Context
from fastmcp.server.apps import ToolUI, UI_EXTENSION_ID

@mcp.tool(ui=ToolUI(resource_uri="ui://dashboard"))
async def dashboard(ctx: Context) -> dict:
data = compute_dashboard()
if ctx.client_supports_extension(UI_EXTENSION_ID):
# Client will render the iframe with structured data
return data
# Fallback: text-only summary
return {"summary": format_text(data)}
```

**Key details:**
- `ToolUI` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional except for typical usage of `resource_uri`)
- `ResourceUI` fields: `csp`, `permissions`, `domain`, `prefers_border` — metadata for the resource itself when it's a UI bundle
- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`)
- `ctx.client_supports_extension(id)` is a general-purpose method — works for any extension, not just MCP Apps
- `structuredContent` in tool results already works via `ToolResult` — MCP Apps clients use this to pass data into the iframe
- Text content fallback already works — tools return both `content` and `structured_content`
- The server does not strip `_meta.ui` for non-UI clients; per the spec, visibility enforcement is the host's responsibility

**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.

Implementation: `src/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).

---

### FileSystemProvider

v3.0 introduces `FileSystemProvider`, a fundamentally different approach to organizing MCP servers. Instead of importing a server instance and decorating functions with `@server.tool`, you use standalone decorators in separate files and let the provider discover them.
Expand Down
35 changes: 35 additions & 0 deletions examples/apps/qr_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# QR Code MCP App

An MCP App server that generates QR codes with an interactive viewer UI. Ported from the [ext-apps QR server example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) to demonstrate FastMCP's MCP Apps support.

## What it demonstrates

- Linking a tool to a `ui://` resource via `ToolUI`
- Serving embedded HTML with the `@modelcontextprotocol/ext-apps` JS SDK from CDN
- Declaring CSP resource domains via `ResourceCSP`
- Returning `ImageContent` (base64 PNG) from a tool

## Setup

```bash
cd examples/apps/qr_server
uv sync
```

## Usage

```bash
uv run python qr_server.py
```

Or install it into an MCP client:

```bash
fastmcp install stdio fastmcp.json
```

## How it works

The server registers one tool (`generate_qr`) and one resource (`ui://qr-server/view.html`). The tool generates a QR code as a base64 PNG image. The resource serves an HTML page that uses the MCP Apps JS SDK to receive the tool result and display the image in a sandboxed iframe.

The HTML loads the ext-apps SDK from unpkg, so the resource declares `csp=ResourceCSP(resource_domains=["https://unpkg.com"])` to allow the host to set the appropriate Content-Security-Policy.
13 changes: 13 additions & 0 deletions examples/apps/qr_server/fastmcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "qr_server.py"
},
"environment": {
"dependencies": [
"fastmcp",
"qrcode[pil]>=8.0",
"pillow"
]
}
}
13 changes: 13 additions & 0 deletions examples/apps/qr_server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "fastmcp-app-examples"
version = "0.1.0"
description = "MCP App examples for FastMCP"
requires-python = ">=3.10"
dependencies = [
"fastmcp",
"qrcode[pil]>=8.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
170 changes: 170 additions & 0 deletions examples/apps/qr_server/qr_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""QR Code MCP App Server — generates QR codes with an interactive view UI.

Demonstrates MCP Apps with FastMCP:
- Tool linked to a ui:// resource via ToolUI
- HTML resource with CSP metadata for CDN-loaded dependencies
- Embedded HTML using the @modelcontextprotocol/ext-apps JS SDK
- ImageContent return type for binary data
- Both stdio and HTTP transport modes

Based on https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server

Setup (from examples/apps/):
uv sync

Usage:
uv run python qr_server.py # HTTP mode (port 3001)
uv run python qr_server.py --stdio # stdio mode for MCP clients
"""

from __future__ import annotations

import base64
import io

import qrcode # type: ignore[import-untyped]
from mcp import types

from fastmcp import FastMCP
from fastmcp.server.apps import ResourceCSP, ResourceUI, ToolUI
from fastmcp.tools import ToolResult

VIEW_URI: str = "ui://qr-server/view.html"

mcp: FastMCP = FastMCP("QR Code Server")

EMBEDDED_VIEW_HTML: str = """\
<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="light dark">
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: transparent;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 340px;
width: 340px;
}
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 qrDiv = document.getElementById('qr');
qrDiv.innerHTML = '';

const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
const mimeType = allowedTypes.includes(img.mimeType) ? img.mimeType : 'image/png';

const image = document.createElement('img');
image.src = `data:${mimeType};base64,${img.data}`;
image.alt = "QR Code";
qrDiv.appendChild(image);
}
};

function handleHostContextChanged(ctx) {
if (ctx.safeAreaInsets) {
document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
}
}

app.onhostcontextchanged = handleHostContextChanged;

await app.connect();
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
</script>
</body>
</html>"""


@mcp.tool(ui=ToolUI(resource_uri=VIEW_URI))
def generate_qr(
text: str = "https://gofastmcp.com",
box_size: int = 10,
border: int = 4,
error_correction: str = "M",
fill_color: str = "black",
back_color: str = "white",
) -> ToolResult:
"""Generate a QR code from text.

Args:
text: The text/URL to encode
box_size: Size of each box in pixels (default: 10)
border: Border size in boxes (default: 4)
error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%)
fill_color: Foreground color (hex like #FF0000 or name like red)
back_color: Background color (hex like #FFFFFF or name like white)
"""
error_levels = {
"L": qrcode.constants.ERROR_CORRECT_L,
"M": qrcode.constants.ERROR_CORRECT_M,
"Q": qrcode.constants.ERROR_CORRECT_Q,
"H": qrcode.constants.ERROR_CORRECT_H,
}

if box_size <= 0:
raise ValueError("box_size must be > 0")
if border < 0:
raise ValueError("border must be >= 0")

error_key = error_correction.upper()
if error_key not in error_levels:
raise ValueError(f"error_correction must be one of: {', '.join(error_levels)}")

qr = qrcode.QRCode(
version=1,
error_correction=error_levels[error_key],
box_size=box_size,
border=border,
)
qr.add_data(text)
qr.make(fit=True)

img = qr.make_image(fill_color=fill_color, back_color=back_color)
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,
ui=ResourceUI(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
)
def view() -> str:
"""Interactive QR code viewer — renders tool results as images."""
return EMBEDDED_VIEW_HTML


if __name__ == "__main__":
mcp.run()
2 changes: 2 additions & 0 deletions src/fastmcp/cli/install/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ async def process_common_args(
file = (config_path.parent / source_path).resolve()
else:
file = source_path.resolve()
# Update the source path so load_server() resolves correctly
config.source.path = str(file)
server_object = (
config.source.entrypoint if hasattr(config.source, "entrypoint") else None
)
Expand Down
Loading