-
Notifications
You must be signed in to change notification settings - Fork 2k
Add prefab auto-wiring for MCP Apps #3119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() |
| 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 | ||
|
|
||
| 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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With Useful? React with 👍 / 👎. |
||
|
|
||
| [tool.uv-dynamic-versioning] | ||
| vcs = "git" | ||
| style = "pep440" | ||
|
|
@@ -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" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Data field
"role"is labeled "Department" everywhere else — naming mismatch.The parameter is
department, the docstring says "department", and theDataTableColumnheader 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"inEMPLOYEES(and updating theDataTableColumnkey 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)