fastmcp/examples/apps/contacts/contacts_server.py
Jeremiah Lowin 30308332bb
Add FastMCPApp — a Provider for composable MCP applications (#3385)
* Add FastMCPApp — a Provider for composable MCP applications

* Wire Prefab callable resolver via to_json(tool_resolver=) parameter

* Remove inspect.signature compat check, use try/except until prefab 0.10.0

* Address review: fix add_tool registry gaps, normalize auth errors, bump prefab to 0.10.0

* Register global key after _add_component succeeds

* Simplify: extract decorator dispatch, use get_fastmcp_meta, expose get_global_tool

* Remove prek from Marvin workflows

These workflows run Claude to respond to /marvin mentions — linting
the repo is unnecessary and fails without renderer deps installed.

* Return ResolvedTool from callable resolver, add contacts example

The callable resolver now returns ResolvedTool (from prefab_ui) instead of a
plain string, carrying metadata like unwrap_result that the renderer needs to
correctly handle structuredContent envelopes. The unwrap_result flag is derived
from the tool's x-fastmcp-wrap-result output schema marker.

* Bump prefab-ui requirement to >=0.11.0

* Remove stale ty ignore comments now that prefab-ui 0.11 is published
2026-03-09 13:46:14 -04:00

152 lines
4.2 KiB
Python

"""Contact manager — a FastMCPApp example with forms and callable tool references.
Demonstrates the full FastMCPApp stack:
- @app.ui() entry point that the model calls to open the app
- @app.tool() backend tools that the UI calls via CallTool
- CallTool(fn) with function references (not strings) that resolve to global keys
- Form.from_model() for auto-generated Pydantic model forms
- Manual form construction with the context-manager pattern
Usage:
uv run python contacts_server.py # HTTP (default)
uv run python contacts_server.py --stdio # stdio for MCP clients
"""
from __future__ import annotations
from typing import Literal
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge,
Button,
Column,
ForEach,
Form,
Heading,
Input,
Muted,
Row,
Separator,
Text,
)
from prefab_ui.rx import RESULT
from pydantic import BaseModel, Field
from fastmcp import FastMCP, FastMCPApp
# ---------------------------------------------------------------------------
# Data
# ---------------------------------------------------------------------------
_contacts: list[dict] = [
{
"name": "Arthur Dent",
"email": "arthur@earth.com",
"category": "Customer",
"notes": "",
},
{
"name": "Ford Prefect",
"email": "ford@betelgeuse.org",
"category": "Partner",
"notes": "Researcher",
},
]
# ---------------------------------------------------------------------------
# Pydantic model for auto-generated forms
# ---------------------------------------------------------------------------
class ContactModel(BaseModel):
name: str = Field(title="Full Name", min_length=1)
email: str = Field(title="Email")
category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
notes: str = Field(
default="",
title="Notes",
json_schema_extra={"ui": {"type": "textarea"}},
)
# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------
app = FastMCPApp("Contacts")
@app.tool()
def save_contact(data: ContactModel) -> list[dict]:
"""Save a new contact and return the updated list."""
_contacts.append(data.model_dump())
return list(_contacts)
@app.tool()
def search_contacts(query: str) -> list[dict]:
"""Filter contacts by name or email."""
q = query.lower()
return [c for c in _contacts if q in c["name"].lower() or q in c["email"].lower()]
@app.tool(model=True)
def list_contacts() -> list[dict]:
"""Return all contacts. Visible to both the model and the UI."""
return list(_contacts)
@app.ui()
def contact_manager() -> PrefabApp:
"""Open the contact manager. The model calls this to launch the app."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts") as contact:
with Row(gap=2, align="center"):
Text(contact.name, css_class="font-medium")
Muted(contact.email)
Badge(contact.category)
Separator()
Heading("Add Contact", level=3)
Form.from_model(
ContactModel,
on_submit=CallTool(
save_contact,
on_success=[
SetState("contacts", RESULT),
ShowToast("Contact saved!", variant="success"),
],
on_error=ShowToast("{{ $error }}", variant="error"),
),
)
Separator()
Heading("Search", level=3)
with Form(
on_submit=CallTool(
search_contacts,
arguments={"query": "{{ query }}"},
on_success=SetState("contacts", RESULT),
)
):
Input(name="query", placeholder="Search by name or email...")
Button("Search")
return PrefabApp(
view=view,
state={"contacts": list(_contacts)},
)
mcp = FastMCP("Contacts Server", providers=[app])
if __name__ == "__main__":
mcp.run(transport="http")