Add Prefab Apps integration for MCP tool UIs (#3316)

* 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.
This commit is contained in:
Jeremiah Lowin 2026-02-27 14:37:57 -05:00 committed by GitHub
commit 3ff1472ea9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 2143 additions and 38 deletions

View file

@ -1,7 +1,7 @@
---
title: Low-Level API
sidebarTitle: Low-Level API
description: Integrate directly with the MCP Apps extension to build interactive tool UIs.
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
---
@ -10,9 +10,9 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.0.0" />
The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) (`io.modelcontextprotocol/ui`) lets tools return interactive UIs — an HTML page rendered in a sandboxed iframe inside the host client. Instead of returning plain text or JSON, a tool can show a chart, a form, an image viewer, or anything you can build with HTML and JavaScript.
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 the low-level extension API directly. FastMCP provides typed models for app configuration, automatic `ui://` resource handling, and CSP/permission management.
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

View file

@ -10,22 +10,63 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.0.0" />
MCP Apps let your tools return interactive UIs — rendered in a sandboxed iframe right inside the host client's conversation. Instead of returning plain text or JSON, a tool can show a chart, a form, an image viewer, or anything you can build with HTML and JavaScript.
MCP Apps let your tools return interactive UIs — rendered in a sandboxed iframe right inside the host client's conversation. Instead of returning plain text, a tool can show a chart, a sortable table, a form, or anything you can build with HTML.
FastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps), so you can start building apps today. FastMCP 3.1 will introduce a full Python-native app framework that makes building rich UIs dramatically simpler — no HTML or JavaScript required.
FastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and provides two approaches:
## What's Available Today
## Prefab Apps (Recommended)
FastMCP provides typed models and helpers for working with the MCP Apps extension directly:
<VersionBadge version="3.1.0" />
- **`AppConfig`** to link tools to UI resources and control visibility
- **`ui://` resources** that automatically serve HTML with the correct MIME type
- **`ResourceCSP`** and **`ResourcePermissions`** for security and sandboxing
[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe layouts, charts, tables, forms, and interactive behaviors using a Python DSL — and the framework compiles them to a JSON protocol that a shared renderer interprets. It started as a component library inside FastMCP and grew into its own framework with [comprehensive documentation](https://prefab.prefect.io).
This is the [low-level API](/apps/low-level) — you write the HTML yourself and wire up communication with the host via the `@modelcontextprotocol/ext-apps` JavaScript SDK. It gives you full control over the UI.
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
## What's Coming in 3.1
mcp = FastMCP("Dashboard")
FastMCP 3.1 will ship a Python-native app framework that lets you build interactive UIs entirely in Python. Define layouts, handle events, and manage state without writing any HTML or JavaScript — FastMCP generates the app for you.
@mcp.tool(app=True)
def sales_chart(year: int) -> PrefabApp:
"""Show sales data as an interactive chart."""
data = get_sales_data(year)
Stay tuned. In the meantime, the [low-level API](/apps/low-level) is ready to use.
with Column(gap=4, css_class="p-6") as view:
Heading(f"{year} Sales")
BarChart(
data=data,
series=[ChartSeries(data_key="revenue", label="Revenue")],
x_axis="month",
)
return PrefabApp(view=view)
```
Install with `pip install "fastmcp[apps]"` and see [Prefab Apps](/apps/prefab) for the integration guide.
## Custom HTML Apps
The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol, and you can use it directly when you need full control. You write your own HTML/CSS/JavaScript and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK.
This is the right choice for custom rendering (maps, 3D, video), specific JavaScript frameworks, or capabilities beyond what the component library offers.
```python
from fastmcp import FastMCP
from fastmcp.server.apps import AppConfig, ResourceCSP
mcp = FastMCP("Custom App")
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
def my_tool() -> str:
return '{"values": [1, 2, 3]}'
@mcp.resource(
"ui://my-app/view.html",
app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
)
def view() -> str:
return "<html>...</html>"
```
See [Custom HTML Apps](/apps/low-level) for the full reference.

479
docs/apps/patterns.mdx Normal file
View file

@ -0,0 +1,479 @@
---
title: Patterns
sidebarTitle: Patterns
description: Charts, tables, forms, and other common tool UIs.
icon: grid-2-plus
tag: SOON
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.1.0" />
The most common use of Prefab is giving your tools a visual representation — a chart instead of raw numbers, a sortable table instead of a text dump, a status dashboard instead of a list of booleans. Each pattern below is a complete, copy-pasteable tool.
## Charts
Prefab includes [bar, line, area, pie, radar, and radial charts](https://prefab.prefect.io/docs/components/charts). They all render client-side with tooltips, legends, and responsive sizing.
### Bar Chart
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@mcp.tool(app=True)
def quarterly_revenue(year: int) -> PrefabApp:
"""Show quarterly revenue as a bar chart."""
data = [
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
]
with Column(gap=4, css_class="p-6") as view:
Heading(f"{year} Revenue vs Costs")
BarChart(
data=data,
series=[
ChartSeries(data_key="revenue", label="Revenue"),
ChartSeries(data_key="costs", label="Costs"),
],
x_axis="quarter",
show_legend=True,
)
return PrefabApp(view=view)
```
Multiple `ChartSeries` entries plot different data keys. Add `stacked=True` to stack bars, or `horizontal=True` to flip the axes.
### Area Chart
`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation (`"linear"`, `"smooth"`, `"step"`) and `show_dots` for data points:
```python
from prefab_ui.components import Column, Heading, AreaChart, ChartSeries
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@mcp.tool(app=True)
def usage_trend() -> PrefabApp:
"""Show API usage over time."""
data = [
{"date": "Feb 1", "requests": 1200},
{"date": "Feb 2", "requests": 1350},
{"date": "Feb 3", "requests": 980},
{"date": "Feb 4", "requests": 1500},
{"date": "Feb 5", "requests": 1420},
]
with Column(gap=4, css_class="p-6") as view:
Heading("API Usage")
AreaChart(
data=data,
series=[ChartSeries(data_key="requests", label="Requests")],
x_axis="date",
curve="smooth",
height=250,
)
return PrefabApp(view=view)
```
### Pie and Donut Charts
`PieChart` uses `data_key` (the numeric value) and `name_key` (the label) instead of series. Set `inner_radius` for a donut:
```python
from prefab_ui.components import Column, Heading, PieChart
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@mcp.tool(app=True)
def ticket_breakdown() -> PrefabApp:
"""Show open tickets by category."""
data = [
{"category": "Bug", "count": 23},
{"category": "Feature", "count": 15},
{"category": "Docs", "count": 8},
{"category": "Infra", "count": 12},
]
with Column(gap=4, css_class="p-6") as view:
Heading("Open Tickets")
PieChart(
data=data,
data_key="count",
name_key="category",
show_legend=True,
inner_radius=60,
)
return PrefabApp(view=view)
```
## Data Tables
[DataTable](https://prefab.prefect.io/docs/components/data-display/data-table) provides sortable columns, full-text search, and pagination — all running client-side in the browser.
```python
from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Directory")
@mcp.tool(app=True)
def employee_directory() -> PrefabApp:
"""Show a searchable, sortable employee directory."""
employees = [
{"name": "Alice Chen", "department": "Engineering", "role": "Staff Engineer", "location": "SF"},
{"name": "Bob Martinez", "department": "Design", "role": "Lead Designer", "location": "NYC"},
{"name": "Carol Johnson", "department": "Engineering", "role": "Senior Engineer", "location": "London"},
{"name": "David Kim", "department": "Product", "role": "Product Manager", "location": "SF"},
{"name": "Eva Müller", "department": "Engineering", "role": "Engineer", "location": "Berlin"},
]
with Column(gap=4, css_class="p-6") as view:
Heading("Employee Directory")
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="department", header="Department", sortable=True),
DataTableColumn(key="role", header="Role"),
DataTableColumn(key="location", header="Office", sortable=True),
],
rows=employees,
searchable=True,
paginated=True,
page_size=15,
)
return PrefabApp(view=view)
```
## Forms
A form collects input, but it needs somewhere to send that input. The [`CallTool`](https://prefab.prefect.io/docs/concepts/actions) action connects a form to a tool on your MCP server — so you need two tools: one that renders the form, and one that handles the submission.
```python
from prefab_ui.components import (
Column, Heading, Row, Muted, Badge, Input, Select,
Textarea, Button, Form, ForEach, Separator,
)
from prefab_ui.actions import ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Contacts")
contacts_db: list[dict] = [
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Partner"},
]
@mcp.tool(app=True)
def contact_form() -> PrefabApp:
"""Show a contact list with a form to add new contacts."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts"):
with Row(gap=2, align="center"):
Muted("{{ name }}")
Muted("{{ email }}")
Badge("{{ category }}")
Separator()
with Form(
on_submit=CallTool(
"save_contact",
result_key="contacts",
on_success=ShowToast("Contact saved!", variant="success"),
on_error=ShowToast("{{ $error }}", variant="error"),
)
):
Input(name="name", label="Full Name", required=True)
Input(name="email", label="Email", input_type="email", required=True)
Select(
name="category",
label="Category",
options=["Customer", "Vendor", "Partner", "Other"],
)
Textarea(name="notes", label="Notes", placeholder="Optional notes...")
Button("Save Contact")
return PrefabApp(view=view, state={"contacts": list(contacts_db)})
@mcp.tool
def save_contact(
name: str,
email: str,
category: str = "Other",
notes: str = "",
) -> list[dict]:
"""Save a new contact and return the updated list."""
contacts_db.append({"name": name, "email": email, "category": category, "notes": notes})
return list(contacts_db)
```
When the user submits the form, the renderer calls `save_contact` on the server with all named input values as arguments. Because `result_key="contacts"` is set, the returned list replaces the `contacts` state — and the `ForEach` re-renders with the new data automatically.
The `save_contact` tool is a regular MCP tool. The LLM can also call it directly in conversation. Your UI actions and your conversational tools are the same thing.
### Pydantic Model Forms
For complex forms, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, validation, and submit wiring:
```python
from typing import Literal
from pydantic import BaseModel, Field
from prefab_ui.components import Column, Heading, Form
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Bug Tracker")
class BugReport(BaseModel):
title: str = Field(title="Bug Title")
severity: Literal["low", "medium", "high", "critical"] = Field(
title="Severity", default="medium"
)
description: str = Field(title="Description")
steps_to_reproduce: str = Field(title="Steps to Reproduce")
@mcp.tool(app=True)
def report_bug() -> PrefabApp:
"""Show a bug report form."""
with Column(gap=4, css_class="p-6") as view:
Heading("Report a Bug")
Form.from_model(BugReport, on_submit=CallTool("create_bug_report"))
return PrefabApp(view=view)
@mcp.tool
def create_bug_report(data: dict) -> str:
"""Create a bug report from the form submission."""
report = BugReport(**data)
# save to database...
return f"Created bug report: {report.title}"
```
`str` fields become text inputs, `Literal` becomes a select, `bool` becomes a checkbox. The `on_submit` CallTool receives all field values under a `data` key.
## Status Displays
Cards, badges, progress bars, and grids combine naturally for dashboards. See the [Prefab layout](https://prefab.prefect.io/docs/concepts/composition) and [container](https://prefab.prefect.io/docs/components/containers) docs for the full set of layout and display components.
```python
from prefab_ui.components import (
Column, Row, Grid, Heading, Text, Muted, Badge,
Card, CardContent, Progress, Separator,
)
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Monitoring")
@mcp.tool(app=True)
def system_status() -> PrefabApp:
"""Show current system health."""
services = [
{"name": "API Gateway", "status": "healthy", "ok": True, "latency_ms": 12, "uptime_pct": 99.9},
{"name": "Database", "status": "healthy", "ok": True, "latency_ms": 3, "uptime_pct": 99.99},
{"name": "Cache", "status": "degraded", "ok": False, "latency_ms": 45, "uptime_pct": 98.2},
{"name": "Queue", "status": "healthy", "ok": True, "latency_ms": 8, "uptime_pct": 99.8},
]
all_ok = all(s["ok"] for s in services)
with Column(gap=4, css_class="p-6") as view:
with Row(gap=2, align="center"):
Heading("System Status")
Badge(
"All Healthy" if all_ok else "Degraded",
variant="success" if all_ok else "destructive",
)
Separator()
with Grid(columns=2, gap=4):
for svc in services:
with Card():
with CardContent():
with Row(gap=2, align="center"):
Text(svc["name"], css_class="font-medium")
Badge(
svc["status"],
variant="success" if svc["ok"] else "destructive",
)
Muted(f"Response: {svc['latency_ms']}ms")
Progress(value=svc["uptime_pct"])
return PrefabApp(view=view)
```
## Conditional Content
[`If`, `Elif`, and `Else`](https://prefab.prefect.io/docs/concepts/composition#conditional-rendering) show or hide content based on state. Changes are instant — no server round-trip.
```python
from prefab_ui.components import Column, Heading, Switch, Separator, Alert, If
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Flags")
@mcp.tool(app=True)
def feature_flags() -> PrefabApp:
"""Toggle feature flags with live preview."""
with Column(gap=4, css_class="p-6") as view:
Heading("Feature Flags")
Switch(name="dark_mode", label="Dark Mode")
Switch(name="beta_features", label="Beta Features")
Separator()
with If("{{ dark_mode }}"):
Alert(title="Dark mode enabled", description="UI will use dark theme.")
with If("{{ beta_features }}"):
Alert(
title="Beta features active",
description="Experimental features are now visible.",
variant="warning",
)
return PrefabApp(view=view, state={"dark_mode": False, "beta_features": False})
```
## Tabs
[Tabs](https://prefab.prefect.io/docs/components/containers/tabs) organize content into switchable views. Switching is client-side — no server round-trip.
```python
from prefab_ui.components import (
Column, Heading, Text, Muted, Badge, Row,
DataTable, DataTableColumn, Tabs, Tab, ForEach,
)
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Projects")
@mcp.tool(app=True)
def project_overview(project_id: str) -> PrefabApp:
"""Show project details organized in tabs."""
project = {
"name": "FastMCP v3",
"description": "Next generation MCP framework with Apps support.",
"status": "Active",
"created_at": "2025-01-15",
"members": [
{"name": "Alice Chen", "role": "Lead"},
{"name": "Bob Martinez", "role": "Design"},
],
"activity": [
{"timestamp": "2 hours ago", "message": "Merged PR #342"},
{"timestamp": "1 day ago", "message": "Released v3.0.1"},
],
}
with Column(gap=4, css_class="p-6") as view:
Heading(project["name"])
with Tabs():
with Tab("Overview"):
Text(project["description"])
with Row(gap=4):
Badge(project["status"])
Muted(f"Created: {project['created_at']}")
with Tab("Members"):
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Role"),
],
rows=project["members"],
)
with Tab("Activity"):
with ForEach("activity"):
with Row(gap=2):
Muted("{{ timestamp }}")
Text("{{ message }}")
return PrefabApp(view=view, state={"activity": project["activity"]})
```
## Accordion
[Accordion](https://prefab.prefect.io/docs/components/containers/accordion) collapses sections to save space. `multiple=True` lets users expand several items at once:
```python
from prefab_ui.components import (
Column, Heading, Row, Text, Badge, Progress,
Accordion, AccordionItem,
)
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("API Monitor")
@mcp.tool(app=True)
def api_health() -> PrefabApp:
"""Show health details for each API endpoint."""
endpoints = [
{"path": "/api/users", "status": 200, "healthy": True, "avg_ms": 45, "p99_ms": 120, "uptime_pct": 99.9},
{"path": "/api/orders", "status": 200, "healthy": True, "avg_ms": 82, "p99_ms": 250, "uptime_pct": 99.7},
{"path": "/api/search", "status": 200, "healthy": True, "avg_ms": 150, "p99_ms": 500, "uptime_pct": 99.5},
{"path": "/api/webhooks", "status": 503, "healthy": False, "avg_ms": 2000, "p99_ms": 5000, "uptime_pct": 95.1},
]
with Column(gap=4, css_class="p-6") as view:
Heading("API Health")
with Accordion(multiple=True):
for ep in endpoints:
with AccordionItem(ep["path"]):
with Row(gap=4):
Badge(
f"{ep['status']}",
variant="success" if ep["healthy"] else "destructive",
)
Text(f"Avg: {ep['avg_ms']}ms")
Text(f"P99: {ep['p99_ms']}ms")
Progress(value=ep["uptime_pct"])
return PrefabApp(view=view)
```
## Next Steps
- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript
- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions

191
docs/apps/prefab.mdx Normal file
View file

@ -0,0 +1,191 @@
---
title: Prefab Apps
sidebarTitle: Prefab Apps
description: Build interactive tool UIs in pure Python — no HTML or JavaScript required.
icon: palette
tag: SOON
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.1.0" />
[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe what your interface should look like — a chart, a table, a form — and return it from your tool. FastMCP takes care of everything else: registering the renderer, wiring the protocol metadata, and delivering the component tree to the host.
Prefab started as a component library inside FastMCP and grew into a full framework for building interactive applications — with its own state management, reactive expression system, and action model. The [Prefab documentation](https://prefab.prefect.io) covers all of this in depth. This page focuses on the FastMCP integration: what you return from a tool, and what FastMCP does with it.
```bash
pip install "fastmcp[apps]"
```
<Tip>
Prefab UI is in active early development and its API changes frequently. We strongly recommend pinning `prefab-ui` to a specific version in your project's dependencies. Installing `fastmcp[apps]` pulls in `prefab-ui` but won't pin it — so a routine `pip install --upgrade` could introduce breaking changes.
```toml
# pyproject.toml
dependencies = [
"fastmcp[apps]",
"prefab-ui==0.8.0", # pin to a known working version
]
```
</Tip>
Here's the simplest possible Prefab App — a tool that returns a bar chart:
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Dashboard")
@mcp.tool(app=True)
def revenue_chart(year: int) -> PrefabApp:
"""Show annual revenue as an interactive bar chart."""
data = [
{"quarter": "Q1", "revenue": 42000},
{"quarter": "Q2", "revenue": 51000},
{"quarter": "Q3", "revenue": 47000},
{"quarter": "Q4", "revenue": 63000},
]
with Column(gap=4, css_class="p-6") as view:
Heading(f"{year} Revenue")
BarChart(
data=data,
series=[ChartSeries(data_key="revenue", label="Revenue")],
x_axis="quarter",
)
return PrefabApp(view=view)
```
That's it — you declare a layout using Python's `with` statement, and return it. When the host calls this tool, the user sees an interactive bar chart instead of a JSON blob. The [Patterns](/apps/patterns) page has more examples: area charts, data tables, forms, status dashboards, and more.
## What You Return
### Components
The simplest way to get started. If you're returning a visual representation of data and don't need Prefab's more advanced features like initial state or stylesheets, just return the components directly. FastMCP wraps them in a `PrefabApp` automatically:
```python
from prefab_ui.components import Column, Heading, Badge
from fastmcp import FastMCP
mcp = FastMCP("Status")
@mcp.tool(app=True)
def status_badge() -> Column:
"""Show system status."""
with Column(gap=2) as view:
Heading("All Systems Operational")
Badge("Healthy", variant="success")
return view
```
Want a chart? Return a chart. Want a table? Return a table. FastMCP handles the wiring.
### PrefabApp
When you need more control — setting initial state values that components can read and react to, or configuring the rendering engine — return a `PrefabApp` explicitly:
```python
from prefab_ui.components import Column, Heading, Text, Button, If, Badge
from prefab_ui.actions import ToggleState
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Demo")
@mcp.tool(app=True)
def toggle_demo() -> PrefabApp:
"""Interactive toggle with state."""
with Column(gap=4, css_class="p-6") as view:
Button("Toggle", on_click=ToggleState("show"))
with If("{{ show }}"):
Badge("Visible!", variant="success")
return PrefabApp(view=view, state={"show": False})
```
The `state` dict provides the initial values. Components reference state with `{{ expression }}` templates. State mutations like `ToggleState` happen entirely in the browser — no server round-trip. The [Prefab state guide](https://prefab.prefect.io/docs/concepts/state) covers this in detail.
### ToolResult
Every tool result has two audiences: the renderer (which displays the UI) and the LLM (which reads the text content to understand what happened). By default, Prefab Apps send `"[Rendered Prefab UI]"` as the text content, which tells the LLM almost nothing.
If you want the LLM to understand the result — so it can reference the data in conversation, summarize it, or decide what to do next — wrap your return in a `ToolResult` with a meaningful `content` string:
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
from fastmcp.tools import ToolResult
mcp = FastMCP("Sales")
@mcp.tool(app=True)
def sales_overview(year: int) -> ToolResult:
"""Show sales data visually and summarize for the model."""
data = get_sales_data(year)
total = sum(row["revenue"] for row in data)
with Column(gap=4, css_class="p-6") as view:
Heading("Sales Overview")
BarChart(data=data, series=[ChartSeries(data_key="revenue")])
return ToolResult(
content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
structured_content=view,
)
```
The user sees the chart. The LLM sees `"Total revenue for 2025: $203,000 across 4 quarters"` and can reason about it.
## Type Inference
If your tool's return type annotation is a Prefab type — `PrefabApp`, `Component`, or their `Optional` variants — FastMCP detects this and enables app rendering automatically:
```python
@mcp.tool
def greet(name: str) -> PrefabApp:
return PrefabApp(view=Heading(f"Hello, {name}!"))
```
This is equivalent to `@mcp.tool(app=True)`. Explicit `app=True` is recommended for clarity, and is required when the return type doesn't reveal a Prefab type (e.g., `-> ToolResult`).
## How It Works
Behind the scenes, when a tool returns a Prefab component or `PrefabApp`, FastMCP:
1. **Registers a shared renderer** — a `ui://prefab/renderer.html` resource containing the JavaScript rendering engine, fetched once by the host and reused across all your Prefab tools.
2. **Wires the tool metadata** — so the host knows to load the renderer iframe when displaying the tool result.
3. **Serializes the component tree** — your Python components become `structuredContent` on the tool result, which the renderer interprets and displays.
None of this requires any configuration. The `app=True` flag (or type inference) is the only thing you need.
## Mixing with Custom HTML Apps
Prefab tools and [custom HTML tools](/apps/low-level) coexist in the same server. Prefab tools share a single renderer resource; custom tools point to their own. Both use the same MCP Apps protocol:
```python
from fastmcp.server.apps import AppConfig
@mcp.tool(app=True)
def team_directory() -> PrefabApp:
...
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/map.html"))
def map_view() -> str:
...
```
## Next Steps
- **[Patterns](/apps/patterns)** — Charts, tables, forms, and other common tool UIs
- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript
- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions

View file

@ -1,3 +1,7 @@
html:not([data-page-mode="wide"]) #content-area {
max-width: 44rem !important;
}
img.nav-logo {
max-width: 200px;
}

View file

@ -202,6 +202,8 @@
"group": "Apps",
"pages": [
"apps/overview",
"apps/prefab",
"apps/patterns",
"apps/low-level"
]
},

View file

@ -0,0 +1,102 @@
"""Chart MCP App — interactive data visualizations with Prefab.
Demonstrates `fastmcp[apps]` with Prefab chart components:
- `BarChart` and `LineChart` for categorical and trend data
- Multiple series, stacking, and curve styles
- Layout composition with `Column`, `Heading`, and `Muted`
- Custom text fallback via `ToolResult`
Usage:
uv run python chart_server.py # HTTP (port 8000)
uv run python chart_server.py --stdio # stdio for MCP clients
"""
from __future__ import annotations
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
BarChart,
ChartSeries,
Column,
Heading,
LineChart,
Muted,
)
from fastmcp import FastMCP
mcp = FastMCP("Sales Dashboard")
MONTHLY_SALES = [
{"month": "Jan", "online": 4200, "retail": 2400},
{"month": "Feb", "online": 3800, "retail": 2100},
{"month": "Mar", "online": 5100, "retail": 2800},
{"month": "Apr", "online": 4600, "retail": 3200},
{"month": "May", "online": 5800, "retail": 3100},
{"month": "Jun", "online": 6200, "retail": 3500},
]
@mcp.tool(app=True)
def sales_overview(stacked: bool = False) -> PrefabApp:
"""View monthly sales broken down by channel.
Args:
stacked: Stack bars to show total revenue per month.
"""
total = sum(row["online"] + row["retail"] for row in MONTHLY_SALES)
with Column(gap=6, css_class="p-6") as view:
with Column(gap=1):
Heading("Monthly Sales")
Muted(f"${total:,} total revenue")
BarChart(
data=MONTHLY_SALES,
series=[
ChartSeries(data_key="online", label="Online"),
ChartSeries(data_key="retail", label="Retail"),
],
x_axis="month",
stacked=stacked,
show_legend=True,
)
return PrefabApp(
title="Sales Dashboard",
view=view,
)
@mcp.tool(app=True)
def sales_trend(curve: str = "linear") -> PrefabApp:
"""View sales trends over time as a line chart.
Args:
curve: Line style "linear", "smooth", or "step".
"""
with Column(gap=6, css_class="p-6") as view:
with Column(gap=1):
Heading("Sales Trend")
Muted("Online vs. retail over 6 months")
LineChart(
data=MONTHLY_SALES,
series=[
ChartSeries(data_key="online", label="Online"),
ChartSeries(data_key="retail", label="Retail"),
],
x_axis="month",
curve=curve,
show_dots=True,
show_legend=True,
)
return PrefabApp(
title="Sales Trend",
view=view,
)
if __name__ == "__main__":
mcp.run()

View file

@ -0,0 +1,165 @@
"""DataTable MCP App — interactive, sortable data views with Prefab.
Demonstrates `fastmcp[apps]` with Prefab UI components:
- `app=True` for automatic renderer wiring
- `PrefabApp` with `DataTable` for rich tabular views
- Searchable, sortable, paginated tables
- Layout composition with `Column`, `Heading`, `Text`, and `Badge`
Usage:
uv run python datatable_server.py # HTTP (port 8000)
uv run python datatable_server.py --stdio # stdio for MCP clients
"""
from __future__ import annotations
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge,
Column,
DataTable,
DataTableColumn,
Heading,
Muted,
Row,
)
from fastmcp import FastMCP
mcp = FastMCP("Team Directory")
EMPLOYEES = [
{
"name": "Alice Chen",
"role": "Engineering",
"level": "Senior",
"location": "San Francisco",
"status": "active",
},
{
"name": "Bob Martinez",
"role": "Design",
"level": "Lead",
"location": "New York",
"status": "active",
},
{
"name": "Carol Johnson",
"role": "Engineering",
"level": "Staff",
"location": "London",
"status": "active",
},
{
"name": "David Kim",
"role": "Product",
"level": "Senior",
"location": "San Francisco",
"status": "away",
},
{
"name": "Eva Müller",
"role": "Engineering",
"level": "Mid",
"location": "Berlin",
"status": "active",
},
{
"name": "Frank Okafor",
"role": "Data Science",
"level": "Senior",
"location": "Lagos",
"status": "active",
},
{
"name": "Grace Liu",
"role": "Engineering",
"level": "Junior",
"location": "Singapore",
"status": "active",
},
{
"name": "Hassan Ali",
"role": "Design",
"level": "Senior",
"location": "Dubai",
"status": "away",
},
{
"name": "Iris Tanaka",
"role": "Product",
"level": "Lead",
"location": "Tokyo",
"status": "active",
},
{
"name": "James Wright",
"role": "Engineering",
"level": "Senior",
"location": "London",
"status": "inactive",
},
{
"name": "Karen Petrov",
"role": "Data Science",
"level": "Lead",
"location": "Berlin",
"status": "active",
},
{
"name": "Liam O'Brien",
"role": "Engineering",
"level": "Mid",
"location": "Dublin",
"status": "active",
},
]
@mcp.tool(app=True)
def list_team(department: str | None = None) -> PrefabApp:
"""Browse the team directory with sorting and search.
Args:
department: Filter by department (e.g. "Engineering", "Design").
Leave empty to show everyone.
"""
if department:
rows = [e for e in EMPLOYEES if e["role"].lower() == department.lower()]
else:
rows = EMPLOYEES
active = sum(1 for e in rows if e["status"] == "active")
with Column(gap=6, css_class="p-6") as view:
with Column(gap=1):
Heading("Team Directory")
with Row(gap=2):
Muted(f"{len(rows)} members")
Muted(f"{active} active", css_class="text-success")
if department:
Badge(department, variant="outline")
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Department", sortable=True),
DataTableColumn(key="level", header="Level", sortable=True),
DataTableColumn(key="location", header="Location", sortable=True),
DataTableColumn(key="status", header="Status", sortable=True),
],
rows=rows,
searchable=True,
paginated=True,
page_size=10,
)
return PrefabApp(
title="Team Directory",
view=view,
state={"total": len(rows), "active": active},
)
if __name__ == "__main__":
mcp.run()

View file

@ -0,0 +1,489 @@
"""Patterns showcase — every Prefab pattern from the docs in one server.
A runnable collection of the patterns from https://gofastmcp.com/apps/patterns.
Each tool demonstrates a different Prefab UI pattern: charts, tables, forms,
status displays, conditional content, tabs, and accordions.
Usage:
uv run python patterns_server.py # HTTP (port 8000)
uv run python patterns_server.py --stdio # stdio for MCP clients
"""
from __future__ import annotations
from prefab_ui.actions import ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Accordion,
AccordionItem,
Alert,
AreaChart,
Badge,
BarChart,
Button,
Card,
CardContent,
ChartSeries,
Column,
DataTable,
DataTableColumn,
ForEach,
Form,
Grid,
Heading,
If,
Input,
Muted,
PieChart,
Progress,
Row,
Select,
Separator,
Switch,
Tab,
Tabs,
Text,
Textarea,
)
from fastmcp import FastMCP
mcp = FastMCP("Patterns Showcase")
# ---------------------------------------------------------------------------
# Data
# ---------------------------------------------------------------------------
QUARTERLY_DATA = [
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
]
DAILY_USAGE = [
{"date": f"Feb {d}", "requests": v}
for d, v in zip(
range(1, 11),
[1200, 1350, 980, 1500, 1420, 1680, 1550, 1700, 1450, 1600],
)
]
TICKETS = [
{"category": "Bug", "count": 23},
{"category": "Feature", "count": 15},
{"category": "Docs", "count": 8},
{"category": "Infra", "count": 12},
]
EMPLOYEES = [
{
"name": "Alice Chen",
"department": "Engineering",
"role": "Staff Engineer",
"location": "San Francisco",
},
{
"name": "Bob Martinez",
"department": "Design",
"role": "Lead Designer",
"location": "New York",
},
{
"name": "Carol Johnson",
"department": "Engineering",
"role": "Senior Engineer",
"location": "London",
},
{
"name": "David Kim",
"department": "Product",
"role": "Product Manager",
"location": "San Francisco",
},
{
"name": "Eva Müller",
"department": "Engineering",
"role": "Engineer",
"location": "Berlin",
},
{
"name": "Frank Okafor",
"department": "Data Science",
"role": "Senior Analyst",
"location": "Lagos",
},
{
"name": "Grace Liu",
"department": "Engineering",
"role": "Junior Engineer",
"location": "Singapore",
},
{
"name": "Hassan Ali",
"department": "Design",
"role": "Senior Designer",
"location": "Dubai",
},
]
SERVICES = [
{
"name": "API Gateway",
"status": "healthy",
"ok": True,
"latency_ms": 12,
"uptime_pct": 99.9,
},
{
"name": "Database",
"status": "healthy",
"ok": True,
"latency_ms": 3,
"uptime_pct": 99.99,
},
{
"name": "Cache",
"status": "degraded",
"ok": False,
"latency_ms": 45,
"uptime_pct": 98.2,
},
{
"name": "Queue",
"status": "healthy",
"ok": True,
"latency_ms": 8,
"uptime_pct": 99.8,
},
]
ENDPOINTS = [
{
"path": "/api/users",
"status": 200,
"healthy": True,
"avg_ms": 45,
"p99_ms": 120,
"uptime_pct": 99.9,
},
{
"path": "/api/orders",
"status": 200,
"healthy": True,
"avg_ms": 82,
"p99_ms": 250,
"uptime_pct": 99.7,
},
{
"path": "/api/search",
"status": 200,
"healthy": True,
"avg_ms": 150,
"p99_ms": 500,
"uptime_pct": 99.5,
},
{
"path": "/api/webhooks",
"status": 503,
"healthy": False,
"avg_ms": 2000,
"p99_ms": 5000,
"uptime_pct": 95.1,
},
]
PROJECT = {
"name": "FastMCP v3",
"description": "Next generation MCP framework with Apps support.",
"status": "Active",
"created_at": "2025-01-15",
"members": [
{"name": "Alice Chen", "role": "Lead"},
{"name": "Bob Martinez", "role": "Design"},
{"name": "Carol Johnson", "role": "Backend"},
],
"activity": [
{
"timestamp": "2 hours ago",
"message": "Merged PR #342: Add Prefab UI integration",
},
{
"timestamp": "5 hours ago",
"message": "Opened issue #345: CORS convenience API",
},
{"timestamp": "1 day ago", "message": "Released v3.0.1"},
],
}
# In-memory contact store for the form demo
_contacts: list[dict] = [
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Partner"},
]
# ---------------------------------------------------------------------------
# Charts
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def quarterly_revenue(year: int = 2025) -> PrefabApp:
"""Show quarterly revenue as a bar chart."""
with Column(gap=4, css_class="p-6") as view:
Heading(f"{year} Revenue vs Costs")
BarChart(
data=QUARTERLY_DATA,
series=[
ChartSeries(data_key="revenue", label="Revenue"),
ChartSeries(data_key="costs", label="Costs"),
],
x_axis="quarter",
show_legend=True,
)
return PrefabApp(view=view)
@mcp.tool(app=True)
def usage_trend() -> PrefabApp:
"""Show API usage over time as an area chart."""
with Column(gap=4, css_class="p-6") as view:
Heading("API Usage (10 Days)")
AreaChart(
data=DAILY_USAGE,
series=[ChartSeries(data_key="requests", label="Requests")],
x_axis="date",
curve="smooth",
height=250,
)
return PrefabApp(view=view)
@mcp.tool(app=True)
def ticket_breakdown() -> PrefabApp:
"""Show open tickets by category as a donut chart."""
with Column(gap=4, css_class="p-6") as view:
Heading("Open Tickets")
PieChart(
data=TICKETS,
data_key="count",
name_key="category",
show_legend=True,
inner_radius=60,
)
return PrefabApp(view=view)
# ---------------------------------------------------------------------------
# Data Tables
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def employee_directory() -> PrefabApp:
"""Show a searchable, sortable employee directory."""
with Column(gap=4, css_class="p-6") as view:
Heading("Employee Directory")
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="department", header="Department", sortable=True),
DataTableColumn(key="role", header="Role"),
DataTableColumn(key="location", header="Office", sortable=True),
],
rows=EMPLOYEES,
searchable=True,
paginated=True,
page_size=15,
)
return PrefabApp(view=view)
# ---------------------------------------------------------------------------
# Forms
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def contact_form() -> PrefabApp:
"""Show a form to create a new contact, with a live contact list below."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts"):
with Row(gap=2, align="center"):
Text("{{ name }}", css_class="font-medium")
Muted("{{ email }}")
Badge("{{ category }}")
Separator()
Heading("Add Contact", level=3)
with Form(
on_submit=CallTool(
"save_contact",
result_key="contacts",
on_success=ShowToast("Contact saved!", variant="success"),
on_error=ShowToast("{{ $error }}", variant="error"),
)
):
Input(name="name", label="Full Name", required=True)
Input(name="email", label="Email", input_type="email", required=True)
Select(
name="category",
label="Category",
options=["Customer", "Vendor", "Partner", "Other"],
)
Textarea(name="notes", label="Notes", placeholder="Optional notes...")
Button("Save Contact")
return PrefabApp(view=view, state={"contacts": list(_contacts)})
@mcp.tool
def save_contact(
name: str,
email: str,
category: str = "Other",
notes: str = "",
) -> list[dict]:
"""Save a new contact and return the updated list."""
contact = {"name": name, "email": email, "category": category, "notes": notes}
_contacts.append(contact)
return list(_contacts)
# ---------------------------------------------------------------------------
# Status Displays
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def system_status() -> PrefabApp:
"""Show current system health."""
all_ok = all(s["ok"] for s in SERVICES)
with Column(gap=4, css_class="p-6") as view:
with Row(gap=2, align="center"):
Heading("System Status")
Badge(
"All Healthy" if all_ok else "Degraded",
variant="success" if all_ok else "destructive",
)
Separator()
with Grid(columns=2, gap=4):
for svc in SERVICES:
with Card():
with CardContent():
with Row(gap=2, align="center"):
Text(svc["name"], css_class="font-medium")
Badge(
svc["status"],
variant="success" if svc["ok"] else "destructive",
)
Muted(f"Response: {svc['latency_ms']}ms")
Progress(value=svc["uptime_pct"])
return PrefabApp(view=view)
# ---------------------------------------------------------------------------
# Conditional Content
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def feature_flags() -> PrefabApp:
"""Toggle feature flags with live preview."""
with Column(gap=4, css_class="p-6") as view:
Heading("Feature Flags")
Switch(name="dark_mode", label="Dark Mode")
Switch(name="beta_features", label="Beta Features")
Separator()
with If("{{ dark_mode }}"):
Alert(title="Dark mode enabled", description="UI will use dark theme.")
with If("{{ beta_features }}"):
Alert(
title="Beta features active",
description="Experimental features are now visible.",
variant="warning",
)
return PrefabApp(view=view, state={"dark_mode": False, "beta_features": False})
# ---------------------------------------------------------------------------
# Tabs
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def project_overview() -> PrefabApp:
"""Show project details organized in tabs."""
with Column(gap=4, css_class="p-6") as view:
Heading(PROJECT["name"])
with Tabs():
with Tab("Overview"):
Text(PROJECT["description"])
with Row(gap=4):
Badge(PROJECT["status"])
Muted(f"Created: {PROJECT['created_at']}")
with Tab("Members"):
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Role"),
],
rows=PROJECT["members"],
)
with Tab("Activity"):
with ForEach("activity"):
with Row(gap=2):
Muted("{{ timestamp }}")
Text("{{ message }}")
return PrefabApp(view=view, state={"activity": PROJECT["activity"]})
# ---------------------------------------------------------------------------
# Accordion
# ---------------------------------------------------------------------------
@mcp.tool(app=True)
def api_health() -> PrefabApp:
"""Show health details for each API endpoint."""
with Column(gap=4, css_class="p-6") as view:
Heading("API Health")
with Accordion(multiple=True):
for ep in ENDPOINTS:
with AccordionItem(ep["path"]):
with Row(gap=4):
Badge(
f"{ep['status']}",
variant="success" if ep["healthy"] else "destructive",
)
Text(f"Avg: {ep['avg_ms']}ms")
Text(f"P99: {ep['p99_ms']}ms")
Progress(value=ep["uptime_pct"])
return PrefabApp(view=view)
if __name__ == "__main__":
mcp.run()

View file

@ -52,6 +52,7 @@ classifiers = [
[project.optional-dependencies]
anthropic = ["anthropic>=0.40.0"]
apps = ["prefab-ui>=0.6.0"]
azure = ["azure-identity>=1.16.0"]
code-mode = ["pydantic-monty>=0.0.7"]
openai = ["openai>=1.102.0"]
@ -60,7 +61,7 @@ tasks = ["pydocket>=0.17.2"]
[dependency-groups]
dev = [
"dirty-equals>=0.9.0",
"fastmcp[anthropic,azure,code-mode,openai,tasks]",
"fastmcp[anthropic,apps,azure,code-mode,openai,tasks]",
# add optional dependencies for fastmcp dev
"fastapi>=0.115.12",
"opentelemetry-sdk>=1.20.0",
@ -105,6 +106,7 @@ source = "uv-dynamic-versioning"
[tool.hatch.metadata]
allow-direct-references = true
[tool.uv-dynamic-versioning]
vcs = "git"
style = "pep440"
@ -191,18 +193,6 @@ known-first-party = ["fastmcp"]
"SIM", # flake8-simplify
]
[tool.basedpyright]
pythonVersion = "3.10"
typeCheckingMode = "standard"
reportMissingTypeStubs = false
reportUnknownParameterType = false
reportUnknownArgumentType = false
reportUnknownMemberType = false
reportUnknownVariableType = false
reportPrivateUsage = false
reportUnnecessaryIsInstance = false
reportUnnecessaryComparison = false
reportConstantRedefinition = false
[tool.codespell]
ignore-words-list = "asend,shttp,te"

View file

@ -26,7 +26,11 @@ class TextResource(Resource):
async def read(self) -> ResourceResult:
"""Read the text content."""
return ResourceResult(
contents=[ResourceContent(content=self.text, mime_type=self.mime_type)]
contents=[
ResourceContent(
content=self.text, mime_type=self.mime_type, meta=self.meta
)
]
)
@ -38,7 +42,11 @@ class BinaryResource(Resource):
async def read(self) -> ResourceResult:
"""Read the binary content."""
return ResourceResult(
contents=[ResourceContent(content=self.data, mime_type=self.mime_type)]
contents=[
ResourceContent(
content=self.data, mime_type=self.mime_type, meta=self.meta
)
]
)

View file

@ -7,7 +7,7 @@ UI metadata for clients that support interactive app rendering.
from __future__ import annotations
from typing import Any
from typing import Any, Literal
from pydantic import BaseModel, Field
@ -92,7 +92,7 @@ class AppConfig(BaseModel):
alias="resourceUri",
description="URI of the UI resource (typically ui:// scheme). Tools only.",
)
visibility: list[str] | None = Field(
visibility: list[Literal["app", "model"]] | None = Field(
default=None,
description="Where this tool is visible: 'app', 'model', or both. Tools only.",
)

View file

@ -7,10 +7,21 @@ registration functionality to LocalProvider.
from __future__ import annotations
import inspect
import types
import warnings
from collections.abc import Callable
from functools import partial
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Literal,
TypeVar,
Union,
get_args,
get_origin,
overload,
)
import mcp.types
from mcp.types import AnyFunction, ToolAnnotations
@ -22,6 +33,14 @@ from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import Tool
from fastmcp.utilities.types import NotSet, NotSetT
try:
from prefab_ui.app import PrefabApp as _PrefabApp
from prefab_ui.components.base import Component as _PrefabComponent
_HAS_PREFAB = True
except ImportError:
_HAS_PREFAB = False
if TYPE_CHECKING:
from fastmcp.server.providers.local_provider import LocalProvider
from fastmcp.tools.tool import ToolResultSerializerType
@ -30,6 +49,99 @@ F = TypeVar("F", bound=Callable[..., Any])
DuplicateBehavior = Literal["error", "warn", "replace", "ignore"]
PREFAB_RENDERER_URI = "ui://prefab/renderer.html"
def _is_prefab_type(tp: Any) -> bool:
"""Check if *tp* is or contains a prefab type, recursing through unions and Annotated."""
if isinstance(tp, type) and issubclass(tp, (_PrefabApp, _PrefabComponent)):
return True
origin = get_origin(tp)
if origin is Union or origin is types.UnionType or origin is Annotated:
return any(_is_prefab_type(a) for a in get_args(tp))
return False
def _has_prefab_return_type(tool: Tool) -> bool:
"""Check if a FunctionTool's return type annotation is a prefab type."""
if not _HAS_PREFAB or not isinstance(tool, FunctionTool):
return False
rt = tool.return_type
if rt is None or rt is inspect.Parameter.empty:
return False
return _is_prefab_type(rt)
def _ensure_prefab_renderer(provider: LocalProvider) -> None:
"""Lazily register the shared prefab renderer as a ui:// resource."""
from prefab_ui.renderer import get_renderer_csp, get_renderer_html
from fastmcp.resources.types import TextResource
from fastmcp.server.apps import (
UI_MIME_TYPE,
AppConfig,
ResourceCSP,
app_config_to_meta_dict,
)
renderer_key = f"resource:{PREFAB_RENDERER_URI}@"
if renderer_key in provider._components:
return
csp = get_renderer_csp()
resource_app = AppConfig(
csp=ResourceCSP(
resource_domains=csp.get("resource_domains"),
connect_domains=csp.get("connect_domains"),
)
)
resource = TextResource(
uri=PREFAB_RENDERER_URI, # type: ignore[arg-type] # AnyUrl accepts ui:// scheme at runtime
name="Prefab Renderer",
text=get_renderer_html(),
mime_type=UI_MIME_TYPE,
meta={"ui": app_config_to_meta_dict(resource_app)},
)
provider._add_component(resource)
def _expand_prefab_ui_meta(tool: Tool) -> None:
"""Expand meta["ui"] = True into the full AppConfig dict for a prefab tool."""
from prefab_ui.renderer import get_renderer_csp
from fastmcp.server.apps import AppConfig, ResourceCSP, app_config_to_meta_dict
csp = get_renderer_csp()
app_config = AppConfig(
resource_uri=PREFAB_RENDERER_URI,
csp=ResourceCSP(
resource_domains=csp.get("resource_domains"),
connect_domains=csp.get("connect_domains"),
),
)
meta = dict(tool.meta) if tool.meta else {}
meta["ui"] = app_config_to_meta_dict(app_config)
tool.meta = meta
def _maybe_apply_prefab_ui(provider: LocalProvider, tool: Tool) -> None:
"""Auto-wire prefab UI metadata and renderer resource if needed."""
if not _HAS_PREFAB:
return
meta = tool.meta or {}
ui = meta.get("ui")
if ui is True:
# Explicit app=True: expand to full AppConfig and register renderer
_ensure_prefab_renderer(provider)
_expand_prefab_ui_meta(tool)
elif ui is None and _has_prefab_return_type(tool):
# Inference: return type is a prefab type, auto-wire
_ensure_prefab_renderer(provider)
_expand_prefab_ui_meta(tool)
# If ui is a dict, it's already manually configured — leave it alone
class ToolDecoratorMixin:
"""Mixin class providing tool decorator functionality for LocalProvider.
@ -87,6 +199,7 @@ class ToolDecoratorMixin:
self._add_component(tool)
if not enabled:
self.disable(keys={tool.key})
_maybe_apply_prefab_ui(self, tool)
return tool
@overload
@ -264,6 +377,7 @@ class ToolDecoratorMixin:
self._add_component(tool_obj)
if not enabled:
self.disable(keys={tool_obj.key})
_maybe_apply_prefab_ui(self, tool_obj)
return tool_obj
else:
from fastmcp.tools.function_tool import ToolMeta

View file

@ -3,9 +3,10 @@
from __future__ import annotations
import inspect
import types
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, get_type_hints
from typing import Annotated, Any, Generic, Union, get_args, get_origin, get_type_hints
import mcp.types
from pydantic import PydanticSchemaGenerationError
@ -27,6 +28,25 @@ from fastmcp.utilities.types import (
replace_type,
)
try:
from prefab_ui.app import PrefabApp as _PrefabApp
from prefab_ui.components.base import Component as _PrefabComponent
_PREFAB_TYPES: tuple[type, ...] = (_PrefabApp, _PrefabComponent)
except ImportError:
_PREFAB_TYPES = ()
def _contains_prefab_type(tp: Any) -> bool:
"""Check if *tp* is or contains a prefab type, recursing through unions and Annotated."""
if isinstance(tp, type) and issubclass(tp, _PREFAB_TYPES):
return True
origin = get_origin(tp)
if origin is Union or origin is types.UnionType or origin is Annotated:
return any(_contains_prefab_type(a) for a in get_args(tp))
return False
T = TypeVarExt("T", default=Any)
logger = get_logger(__name__)
@ -65,6 +85,7 @@ class ParsedFunction:
description: str | None
input_schema: dict[str, Any]
output_schema: dict[str, Any] | None
return_type: Any = None
@classmethod
def from_function(
@ -145,7 +166,18 @@ class ParsedFunction:
# If resolution fails, keep the string annotation
logger.debug("Failed to resolve type hint for return annotation: %s", e)
# Save original for return_type before any schema-related replacement
original_output_type = output_type
if output_type not in (inspect._empty, None, Any, ...):
# Prefab component subclasses (Column, Card, etc.) shouldn't
# produce output schemas — replace_type only does exact matching,
# so we handle subclass matching explicitly here. We also need
# to handle composite types like ``Column | None`` and
# ``Annotated[PrefabApp, ...]`` by recursing into their args.
if _PREFAB_TYPES and _contains_prefab_type(output_type):
output_type = _UnserializableType
# there are a variety of types that we don't want to attempt to
# serialize because they are either used by FastMCP internally,
# or are MCP content types that explicitly don't form structured
@ -164,6 +196,7 @@ class ParsedFunction:
mcp.types.AudioContent,
mcp.types.ResourceLink,
mcp.types.EmbeddedResource,
*_PREFAB_TYPES,
),
_UnserializableType,
),
@ -198,4 +231,5 @@ class ParsedFunction:
description=fn_doc,
input_schema=input_schema,
output_schema=output_schema or None,
return_type=original_output_type,
)

View file

@ -8,6 +8,7 @@ from collections.abc import Callable
from dataclasses import dataclass, field
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Literal,
Protocol,
@ -20,6 +21,7 @@ import anyio
import mcp.types
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData, Icon, ToolAnnotations, ToolExecution
from pydantic import Field
from pydantic.json_schema import SkipJsonSchema
import fastmcp
@ -84,6 +86,7 @@ class ToolMeta:
class FunctionTool(Tool):
fn: SkipJsonSchema[Callable[..., Any]]
return_type: Annotated[SkipJsonSchema[Any], Field(exclude=True)] = None
def to_mcp_tool(
self,
@ -230,6 +233,7 @@ class FunctionTool(Tool):
return cls(
fn=parsed_fn.fn,
return_type=parsed_fn.return_type,
name=metadata.name or parsed_fn.name,
version=str(metadata.version) if metadata.version is not None else None,
title=metadata.title,

View file

@ -38,6 +38,14 @@ from fastmcp.utilities.types import (
NotSetT,
)
try:
from prefab_ui.app import PrefabApp as _PrefabApp
from prefab_ui.components.base import Component as _PrefabComponent
_HAS_PREFAB = True
except ImportError:
_HAS_PREFAB = False
if TYPE_CHECKING:
from docket import Docket
from docket.execution import Execution
@ -82,6 +90,14 @@ class ToolResult(BaseModel):
converted_content: list[ContentBlock] = _convert_to_content(result=content)
if structured_content is not None:
# Convert Prefab types to their wire-format envelope before
# generic serialization, so the renderer gets the right shape.
if _HAS_PREFAB:
if isinstance(structured_content, _PrefabApp):
structured_content = structured_content.to_json()
elif isinstance(structured_content, _PrefabComponent):
structured_content = _PrefabApp(view=structured_content).to_json()
try:
structured_content = pydantic_core.to_jsonable_python(
value=structured_content
@ -248,6 +264,12 @@ class Tool(FastMCPComponent):
if isinstance(raw_value, ToolResult):
return raw_value
if _HAS_PREFAB:
if isinstance(raw_value, _PrefabApp):
return _prefab_to_tool_result(raw_value)
if isinstance(raw_value, _PrefabComponent):
return _prefab_to_tool_result(_PrefabApp(view=raw_value))
content = _convert_to_content(raw_value, serializer=self.serializer)
# Skip structured content for ContentBlock types only if no output_schema
@ -454,6 +476,17 @@ def _convert_to_single_content_block(
return TextContent(type="text", text=_serialize_with_fallback(item, serializer))
_PREFAB_TEXT_FALLBACK = "[Rendered Prefab UI]"
def _prefab_to_tool_result(app: Any) -> ToolResult:
"""Convert a PrefabApp to a FastMCP ToolResult."""
return ToolResult(
content=[TextContent(type="text", text=_PREFAB_TEXT_FALLBACK)],
structured_content=app.to_json(),
)
def _convert_to_content(
result: Any,
serializer: ToolResultSerializerType | None = None,

431
tests/test_apps_prefab.py Normal file
View file

@ -0,0 +1,431 @@
"""Tests for MCP Apps Phase 2 — Prefab integration.
Covers ``convert_result`` for PrefabApp/Component, ``app=True`` auto-wiring,
return-type inference, output-schema suppression, and end-to-end round trips.
"""
from __future__ import annotations
from typing import Annotated
from mcp.types import TextContent
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, Text
from prefab_ui.components.base import Component
from fastmcp import Client, FastMCP
from fastmcp.resources.types import TextResource
from fastmcp.server.apps import UI_MIME_TYPE, AppConfig
from fastmcp.server.providers.local_provider.decorators.tools import (
PREFAB_RENDERER_URI,
)
from fastmcp.tools.tool import Tool, ToolResult
# ---------------------------------------------------------------------------
# convert_result
# ---------------------------------------------------------------------------
class TestConvertResult:
def test_prefab_app(self):
with Column() as view:
Heading("Hello")
app = PrefabApp(view=view, state={"name": "Alice"})
tool = Tool(name="t", parameters={})
result = tool.convert_result(app)
assert isinstance(result, ToolResult)
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "[Rendered Prefab UI]"
assert result.structured_content is not None
assert result.structured_content["version"] == "0.2"
assert result.structured_content["state"] == {"name": "Alice"}
assert result.structured_content["view"]["type"] == "Column"
def test_bare_component(self):
heading = Heading("World")
tool = Tool(name="t", parameters={})
result = tool.convert_result(heading)
assert isinstance(result, ToolResult)
assert result.structured_content is not None
assert result.structured_content["version"] == "0.2"
assert result.structured_content["view"]["type"] == "Heading"
def test_tool_result_with_prefab_structured_content(self):
"""ToolResult with PrefabApp as structured_content preserves custom text."""
app = PrefabApp(view=Heading("Hello"), state={"x": 1})
tool = Tool(name="t", parameters={})
result = tool.convert_result(
ToolResult(content="Custom fallback text", structured_content=app)
)
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Custom fallback text"
assert result.structured_content is not None
assert result.structured_content["version"] == "0.2"
assert result.structured_content["view"]["type"] == "Heading"
def test_tool_result_with_component_structured_content(self):
"""ToolResult with bare Component as structured_content."""
tool = Tool(name="t", parameters={})
result = tool.convert_result(
ToolResult(content="My text", structured_content=Heading("Hi"))
)
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "My text"
assert result.structured_content is not None
assert result.structured_content["version"] == "0.2"
assert result.structured_content["view"]["type"] == "Heading"
def test_tool_result_passthrough(self):
"""ToolResult without prefab structured_content passes through unchanged."""
original = ToolResult(content="hello")
tool = Tool(name="t", parameters={})
assert tool.convert_result(original) is original
# ---------------------------------------------------------------------------
# app=True auto-wiring
# ---------------------------------------------------------------------------
class TestAppTrue:
def test_app_true_sets_meta(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def my_tool() -> str:
return "hello"
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert "ui" in tool.meta
assert tool.meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
def test_app_true_registers_renderer_resource(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def my_tool() -> str:
return "hello"
renderer_key = f"resource:{PREFAB_RENDERER_URI}@"
assert renderer_key in mcp._local_provider._components
def test_renderer_resource_has_correct_mime_type(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def my_tool() -> str:
return "hello"
renderer_key = f"resource:{PREFAB_RENDERER_URI}@"
resource = mcp._local_provider._components[renderer_key]
assert isinstance(resource, TextResource)
assert resource.mime_type == UI_MIME_TYPE
def test_renderer_resource_has_csp(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def my_tool() -> str:
return "hello"
renderer_key = f"resource:{PREFAB_RENDERER_URI}@"
resource = mcp._local_provider._components[renderer_key]
assert resource.meta is not None
assert "ui" in resource.meta
assert "csp" in resource.meta["ui"]
def test_multiple_tools_share_renderer(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def tool_a() -> str:
return "a"
@mcp.tool(app=True)
def tool_b() -> str:
return "b"
renderer_keys = [
k for k in mcp._local_provider._components if k.startswith("resource:ui://")
]
assert len(renderer_keys) == 1
def test_explicit_app_config_not_overridden(self):
mcp = FastMCP("test")
@mcp.tool(app=AppConfig(resource_uri="ui://custom/app.html"))
def my_tool() -> PrefabApp:
return PrefabApp(view=Heading("hi"))
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert tool.meta["ui"]["resourceUri"] == "ui://custom/app.html"
# ---------------------------------------------------------------------------
# Return type inference
# ---------------------------------------------------------------------------
class TestInference:
def test_prefab_app_annotation_inferred(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> PrefabApp:
return PrefabApp(view=Heading("hi"))
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert tool.meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
def test_component_annotation_inferred(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> Component:
return Heading("hi")
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert tool.meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
def test_no_annotation_no_inference(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool():
return "hello"
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is None or "ui" not in (tool.meta or {})
def test_non_prefab_annotation_no_inference(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> str:
return "hello"
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is None or "ui" not in (tool.meta or {})
def test_optional_prefab_app_inferred(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> PrefabApp | None:
return None
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert tool.meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
def test_annotated_prefab_app_inferred(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> Annotated[PrefabApp | None, "some metadata"]:
return None
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert tool.meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
def test_component_subclass_union_inferred(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> Column | None:
return None
tools = mcp._local_provider._components
tool = next(
v
for v in tools.values()
if hasattr(v, "parameters") and v.name == "my_tool"
)
assert tool.meta is not None
assert tool.meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
# ---------------------------------------------------------------------------
# Output schema suppression
# ---------------------------------------------------------------------------
class TestOutputSchema:
def test_prefab_app_return_no_output_schema(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> PrefabApp:
return PrefabApp(view=Heading("hi"))
tools = mcp._local_provider._components
tool: Tool = next(
v for v in tools.values() if isinstance(v, Tool) and v.name == "my_tool"
)
assert tool.output_schema is None
def test_component_return_no_output_schema(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> Column:
with Column() as view:
Heading("hi")
return view
tools = mcp._local_provider._components
tool: Tool = next(
v for v in tools.values() if isinstance(v, Tool) and v.name == "my_tool"
)
assert tool.output_schema is None
def test_optional_component_no_output_schema(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> Column | None:
return None
tools = mcp._local_provider._components
tool: Tool = next(
v for v in tools.values() if isinstance(v, Tool) and v.name == "my_tool"
)
assert tool.output_schema is None
def test_annotated_prefab_app_no_output_schema(self):
mcp = FastMCP("test")
@mcp.tool
def my_tool() -> Annotated[PrefabApp | None, "metadata"]:
return None
tools = mcp._local_provider._components
tool: Tool = next(
v for v in tools.values() if isinstance(v, Tool) and v.name == "my_tool"
)
assert tool.output_schema is None
# ---------------------------------------------------------------------------
# Integration — client-server round trip
# ---------------------------------------------------------------------------
class TestIntegration:
async def test_tool_call_returns_prefab_structured_content(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def greet(name: str) -> PrefabApp:
with Column() as view:
Heading("Hello")
Text(f"Welcome, {name}!")
return PrefabApp(view=view, state={"name": name})
async with Client(mcp) as client:
result = await client.call_tool("greet", {"name": "Alice"})
assert result.structured_content is not None
assert result.structured_content["version"] == "0.2"
assert result.structured_content["state"] == {"name": "Alice"}
async def test_tool_call_with_custom_text(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def greet(name: str) -> ToolResult:
app = PrefabApp(view=Heading(f"Hello {name}"))
return ToolResult(
content=f"Greeting for {name}",
structured_content=app,
)
async with Client(mcp) as client:
result = await client.call_tool("greet", {"name": "Alice"})
assert any(
"Greeting for Alice" in c.text for c in result.content if hasattr(c, "text")
)
assert result.structured_content is not None
assert result.structured_content["version"] == "0.2"
async def test_tools_list_includes_app_meta(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def my_tool() -> PrefabApp:
return PrefabApp(view=Heading("hi"))
async with Client(mcp) as client:
tools = await client.list_tools()
tool = next(t for t in tools if t.name == "my_tool")
meta = tool.meta or {}
assert "ui" in meta
assert meta["ui"]["resourceUri"] == PREFAB_RENDERER_URI
async def test_renderer_resource_readable(self):
mcp = FastMCP("test")
@mcp.tool(app=True)
def my_tool() -> str:
return "hello"
async with Client(mcp) as client:
contents = await client.read_resource(PREFAB_RENDERER_URI)
assert len(contents) > 0
text = contents[0].text if hasattr(contents[0], "text") else ""
assert "<html" in text.lower() or "<!doctype" in text.lower()

24
uv.lock generated
View file

@ -786,6 +786,9 @@ dependencies = [
anthropic = [
{ name = "anthropic" },
]
apps = [
{ name = "prefab-ui" },
]
azure = [
{ name = "azure-identity" },
]
@ -803,7 +806,7 @@ tasks = [
dev = [
{ name = "dirty-equals" },
{ name = "fastapi" },
{ name = "fastmcp", extra = ["anthropic", "azure", "code-mode", "openai", "tasks"] },
{ name = "fastmcp", extra = ["anthropic", "apps", "azure", "code-mode", "openai", "tasks"] },
{ name = "inline-snapshot", extra = ["dirty-equals"] },
{ name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@ -845,6 +848,7 @@ requires-dist = [
{ name = "opentelemetry-api", specifier = ">=1.20.0" },
{ name = "packaging", specifier = ">=24.0" },
{ name = "platformdirs", specifier = ">=4.0.0" },
{ name = "prefab-ui", marker = "extra == 'apps'", specifier = ">=0.6.0" },
{ name = "py-key-value-aio", extras = ["filetree", "keyring", "memory"], specifier = ">=0.4.4,<0.5.0" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
{ name = "pydantic-monty", marker = "extra == 'code-mode'", specifier = ">=0.0.7" },
@ -857,13 +861,13 @@ requires-dist = [
{ name = "watchfiles", specifier = ">=1.0.0" },
{ name = "websockets", specifier = ">=15.0.1" },
]
provides-extras = ["anthropic", "azure", "code-mode", "openai", "tasks"]
provides-extras = ["anthropic", "apps", "azure", "code-mode", "openai", "tasks"]
[package.metadata.requires-dev]
dev = [
{ name = "dirty-equals", specifier = ">=0.9.0" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastmcp", extras = ["anthropic", "azure", "code-mode", "openai", "tasks"] },
{ name = "fastmcp", extras = ["anthropic", "apps", "azure", "code-mode", "openai", "tasks"] },
{ name = "inline-snapshot", extras = ["dirty-equals"], specifier = ">=0.27.2" },
{ name = "ipython", specifier = ">=8.12.3" },
{ name = "loq", specifier = ">=0.1.0a3" },
@ -1709,6 +1713,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "prefab-ui"
version = "0.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cyclopts" },
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/32/e7bc3db47cd93e2683241bd2a328b3136c44c4332a4277a2ac2f3f6beca4/prefab_ui-0.8.3.tar.gz", hash = "sha256:bfe1304f0fc457da764763232f533bff732160e8cc8ce12977ba48f8e7574152", size = 2819196, upload-time = "2026-02-27T14:44:10.52Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/23/cbc8decc176428a1f03b676fee4242f8a8d0ae7e76d5fc76ff96f13f13c1/prefab_ui-0.8.3-py3-none-any.whl", hash = "sha256:b1bac064958f08b20694e67b916ad8ab82de8db883578c31ba4437b521b425e1", size = 803888, upload-time = "2026-02-27T14:44:09.31Z" },
]
[[package]]
name = "prek"
version = "0.3.3"