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
101 changes: 101 additions & 0 deletions examples/apps/chart_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""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`

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 import UIResponse
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) -> UIResponse:
"""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 UIResponse(
view=view,
text=f"Monthly sales: ${total:,} total revenue across 2 channels",
)


@mcp.tool(app=True)
def sales_trend(curve: str = "linear") -> UIResponse:
"""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 UIResponse(
view=view,
text="Sales trend across online and retail channels",
)


if __name__ == "__main__":
mcp.run()
165 changes: 165 additions & 0 deletions examples/apps/datatable_server.py
Original file line number Diff line number Diff line change
@@ -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
- `UIResponse` with `DataTable` for rich tabular output
- 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 import UIResponse
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) -> UIResponse:
"""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

Comment on lines +119 to +131
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Data field "role" is labeled "Department" everywhere else — naming mismatch.

The parameter is department, the docstring says "department", and the DataTableColumn header on line 146 reads "Department", but the filter and data key is "role". This inconsistency in an example file could confuse users who copy this pattern. Consider renaming the dict key to "department" in EMPLOYEES (and updating the DataTableColumn key accordingly), or renaming the parameter/docstring to "role".

Proposed fix: rename the key in the data
 EMPLOYEES = [
     {
         "name": "Alice Chen",
-        "role": "Engineering",
+        "department": "Engineering",
         "level": "Senior",

(Apply to all entries, then update the filter and DataTable column key)

-        rows = [e for e in EMPLOYEES if e["role"].lower() == department.lower()]
+        rows = [e for e in EMPLOYEES if e["department"].lower() == department.lower()]
-                DataTableColumn(key="role", header="Department", sortable=True),
+                DataTableColumn(key="department", header="Department", sortable=True),

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 UIResponse(
view=view,
state={"total": len(rows), "active": active},
text=f"Team directory: {len(rows)} members ({active} active)",
)


if __name__ == "__main__":
mcp.run()
18 changes: 5 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ classifiers = [

[project.optional-dependencies]
anthropic = ["anthropic>=0.40.0"]
apps = ["prefab-ui>=0.1.0"]
openai = ["openai>=1.102.0"]
tasks = ["pydocket>=0.17.2"]

[dependency-groups]
dev = [
"dirty-equals>=0.9.0",
"fastmcp[anthropic,openai,tasks]",
"fastmcp[anthropic,apps,openai,tasks]",
# add optional dependencies for fastmcp dev
"fastapi>=0.115.12",
"opentelemetry-sdk>=1.20.0",
Expand Down Expand Up @@ -103,6 +104,9 @@ source = "uv-dynamic-versioning"
[tool.hatch.metadata]
allow-direct-references = true

[tool.uv.sources]
prefab-ui = { path = "../prefab", editable = true }
Comment on lines +107 to +108
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove hardcoded local prefab-ui source override

With [tool.uv.sources] prefab-ui = { path = "../prefab", editable = true } in the repo config, uv sync will always try to resolve prefab-ui from a sibling ../prefab directory. That path doesn’t exist for typical CI or fresh checkouts, so the required workflow in this repo (running uv sync before tests) will fail even though prefab-ui is available on PyPI. This is a regression introduced by the commit because it forces a local-only source override instead of a normal dependency.

Useful? React with 👍 / 👎.


[tool.uv-dynamic-versioning]
vcs = "git"
style = "pep440"
Expand Down Expand Up @@ -189,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"
12 changes: 10 additions & 2 deletions src/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
]
)


Expand All @@ -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
)
]
)


Expand Down
Loading
Loading