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
5 changes: 0 additions & 5 deletions .github/workflows/marvin-comment-on-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ jobs:
- name: Install dependencies
run: uv sync --python 3.12

- name: Run prek
uses: j178/prek-action@v1
env:
SKIP: no-commit-to-branch

- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/marvin-comment-on-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ jobs:
- name: Install dependencies
run: uv sync --python 3.12

- name: Run prek
uses: j178/prek-action@v1
env:
SKIP: no-commit-to-branch

- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
Expand Down
152 changes: 152 additions & 0 deletions examples/apps/contacts/contacts_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""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")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ classifiers = [

[project.optional-dependencies]
anthropic = ["anthropic>=0.40.0"]
apps = ["prefab-ui>=0.6.0"]
apps = ["prefab-ui>=0.11.0"]
azure = ["azure-identity>=1.16.0"]
code-mode = ["pydantic-monty>=0.0.7"]
gemini = ["google-genai>=1.18.0"]
Expand Down
6 changes: 6 additions & 0 deletions src/fastmcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

if TYPE_CHECKING:
from fastmcp.client import Client as Client
from fastmcp.server.app import FastMCPApp as FastMCPApp

settings = Settings()
if settings.log_enabled:
Expand Down Expand Up @@ -40,6 +41,10 @@ def __getattr__(name: str) -> object:
from fastmcp.client import Client

return Client
if name == "FastMCPApp":
from fastmcp.server.app import FastMCPApp

return FastMCPApp
if name == "client":
return importlib.import_module("fastmcp.client")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Expand All @@ -49,5 +54,6 @@ def __getattr__(name: str) -> object:
"Client",
"Context",
"FastMCP",
"FastMCPApp",
"settings",
]
Loading