"""Inventory tracker -- a FastMCPApp example with CRUD operations and rich UI. Demonstrates the full FastMCPApp stack: - @app.ui() entry point that the model calls to open the app - @app.tool() backend tools for add, update, delete, and search - DataTable with sortable columns and built-in search - Form.from_model() for auto-generated Pydantic model forms - Tabs, Select filtering, ForEach results, and Toast notifications - State management with PrefabApp state dict and Rx() Usage: uv run python inventory_server.py # HTTP (default) uv run python inventory_server.py --stdio # stdio for MCP clients """ from __future__ import annotations from datetime import datetime 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, Card, CardContent, Column, DataTable, DataTableColumn, ForEach, Form, Grid, Heading, Input, Muted, Row, Select, SelectOption, Separator, Tab, Tabs, Text, ) from prefab_ui.rx import ERROR, RESULT, STATE, Rx from pydantic import BaseModel, Field from fastmcp import FastMCP, FastMCPApp # --------------------------------------------------------------------------- # Data store # --------------------------------------------------------------------------- _next_id = 11 _inventory: list[dict] = [ { "id": 1, "name": "Wireless Mouse", "category": "Electronics", "quantity": 45, "price": 29.99, "last_updated": "2026-03-20", }, { "id": 2, "name": "Mechanical Keyboard", "category": "Electronics", "quantity": 32, "price": 89.99, "last_updated": "2026-03-19", }, { "id": 3, "name": "USB-C Hub", "category": "Electronics", "quantity": 18, "price": 49.99, "last_updated": "2026-03-18", }, { "id": 4, "name": "A4 Copy Paper (500 sheets)", "category": "Office Supplies", "quantity": 200, "price": 8.50, "last_updated": "2026-03-21", }, { "id": 5, "name": "Ballpoint Pens (box)", "category": "Office Supplies", "quantity": 150, "price": 12.00, "last_updated": "2026-03-20", }, { "id": 6, "name": "Sticky Notes (pack)", "category": "Office Supplies", "quantity": 85, "price": 5.99, "last_updated": "2026-03-17", }, { "id": 7, "name": "Standing Desk", "category": "Furniture", "quantity": 8, "price": 499.00, "last_updated": "2026-03-15", }, { "id": 8, "name": "Ergonomic Chair", "category": "Furniture", "quantity": 12, "price": 349.00, "last_updated": "2026-03-16", }, { "id": 9, "name": "Monitor Arm", "category": "Furniture", "quantity": 25, "price": 79.99, "last_updated": "2026-03-22", }, { "id": 10, "name": "Webcam HD", "category": "Electronics", "quantity": 60, "price": 69.99, "last_updated": "2026-03-21", }, ] CATEGORIES = ["All", "Electronics", "Office Supplies", "Furniture"] # --------------------------------------------------------------------------- # Pydantic model for add-item form # --------------------------------------------------------------------------- class NewItem(BaseModel): name: str = Field(title="Item Name", min_length=1) category: Literal["Electronics", "Office Supplies", "Furniture"] = Field( title="Category", default="Electronics", ) quantity: int = Field(title="Quantity", ge=0, default=1) price: float = Field(title="Unit Price ($)", ge=0.0, default=0.0) # --------------------------------------------------------------------------- # App and tools # --------------------------------------------------------------------------- app = FastMCPApp("Inventory") @app.tool() def add_item(data: NewItem) -> list[dict]: """Add a new item to inventory and return the full list.""" global _next_id item = { "id": _next_id, "name": data.name, "category": data.category, "quantity": data.quantity, "price": data.price, "last_updated": datetime.now().strftime("%Y-%m-%d"), } _next_id += 1 _inventory.append(item) return list(_inventory) @app.tool() def update_quantity(item_id: int, delta: int) -> list[dict]: """Adjust an item's quantity by delta (+/-) and return the full list.""" for item in _inventory: if item["id"] == item_id: new_qty = max(0, item["quantity"] + delta) item["quantity"] = new_qty item["last_updated"] = datetime.now().strftime("%Y-%m-%d") break return list(_inventory) @app.tool() def delete_item(item_id: int) -> list[dict]: """Remove an item by ID and return the remaining inventory.""" for i, item in enumerate(_inventory): if item["id"] == item_id: _inventory.pop(i) break return list(_inventory) @app.tool() def search_items(query: str) -> list[dict]: """Search items by name (case-insensitive). Returns matching items.""" q = query.lower() return [item for item in _inventory if q in item["name"].lower()] @app.tool() def filter_by_category(category: str) -> list[dict]: """Filter inventory by category. Pass 'All' to show everything.""" if category == "All": return list(_inventory) return [item for item in _inventory if item["category"] == category] # --------------------------------------------------------------------------- # UI helpers # --------------------------------------------------------------------------- def _build_inventory_table() -> None: """Render the main DataTable with all current items.""" DataTable( columns=[ DataTableColumn(key="name", header="Name", sortable=True), DataTableColumn(key="category", header="Category", sortable=True), DataTableColumn(key="quantity", header="Qty", sortable=True), DataTableColumn(key="price", header="Price ($)", sortable=True), DataTableColumn(key="last_updated", header="Updated", sortable=True), ], rows=list(_inventory), search=True, paginated=True, page_size=10, ) def _build_search_section() -> None: """Render the search form with ForEach results.""" Heading("Search Items", level=3) Muted("Search by name across all inventory items.") with Form( on_submit=CallTool( search_items, arguments={"query": STATE.query}, on_success=SetState("search_results", RESULT), ) ): Input(name="query", placeholder="Search by name...") Button("Search") with ForEach("search_results") as result: with Card(css_class="mb-2"): with CardContent(): with Row(gap=3, align="center"): Text(result.name, css_class="font-medium") Badge(result.category) Text(result.quantity) Muted("in stock") def _build_add_form() -> None: """Render the add-item form using Form.from_model().""" Heading("Add New Item", level=3) Muted("Fill out the form below to add a new item to inventory.") Form.from_model( NewItem, submit_label="Add Item", on_submit=CallTool( add_item, on_success=[ SetState("recent_additions", RESULT), ShowToast("Item added!", variant="success"), ], on_error=ShowToast(ERROR, variant="error"), ), ) def _build_actions_section() -> None: """Render category filter, quantity adjustment, and delete controls.""" # Category filter Heading("Filter by Category", level=3) Muted("Select a category to see matching items.") with Form( on_submit=CallTool( filter_by_category, arguments={"category": STATE.selected_category}, on_success=SetState("filtered_items", RESULT), ) ): with Select(name="selected_category", placeholder="Choose a category..."): for cat in CATEGORIES: SelectOption(cat, value=cat) Button("Apply Filter") with ForEach("filtered_items") as item: with Row(gap=3, align="center", css_class="py-1"): Badge(item.id, variant="outline") Text(item.name, css_class="font-medium") Badge(item.category) Muted(item.quantity) Separator() # Quantity adjustment Heading("Adjust Quantity", level=3) Muted("Enter an item ID and use the buttons to adjust stock levels.") Input(name="adjust_id", input_type="number", placeholder="Item ID (e.g. 1)") with Row(gap=2): Button( "- 1", variant="outline", on_click=CallTool( update_quantity, arguments={"item_id": STATE.adjust_id, "delta": -1}, on_success=[ SetState("filtered_items", RESULT), ShowToast("Quantity decreased", variant="default"), ], on_error=ShowToast(ERROR, variant="error"), ), ) Button( "+ 1", variant="outline", on_click=CallTool( update_quantity, arguments={"item_id": STATE.adjust_id, "delta": 1}, on_success=[ SetState("filtered_items", RESULT), ShowToast("Quantity increased", variant="default"), ], on_error=ShowToast(ERROR, variant="error"), ), ) Button( "+ 10", on_click=CallTool( update_quantity, arguments={"item_id": STATE.adjust_id, "delta": 10}, on_success=[ SetState("filtered_items", RESULT), ShowToast("Restocked +10", variant="success"), ], on_error=ShowToast(ERROR, variant="error"), ), ) Separator() # Delete Heading("Delete Item", level=3) Muted("Permanently remove an item by its ID.") with Form( on_submit=CallTool( delete_item, arguments={"item_id": STATE.delete_id}, on_success=[ SetState("filtered_items", RESULT), ShowToast("Item deleted", variant="warning"), ], on_error=ShowToast(ERROR, variant="error"), ) ): Input(name="delete_id", input_type="number", placeholder="Item ID to delete") Button("Delete", variant="destructive") # --------------------------------------------------------------------------- # Entry point UI # --------------------------------------------------------------------------- @app.ui() def inventory_manager() -> PrefabApp: """Open the inventory manager. The model calls this to launch the app.""" with Column(gap=6, css_class="p-6") as view: with Row(gap=3, align="center"): Heading("Inventory Tracker") Badge( Rx("filtered_items.length"), variant="secondary", ) Muted("items tracked") Separator() # Summary cards per category with Grid(columns=3, gap=4): for cat in ["Electronics", "Office Supplies", "Furniture"]: count = sum(1 for it in _inventory if it["category"] == cat) total_qty = sum( it["quantity"] for it in _inventory if it["category"] == cat ) with Card(): with CardContent(): Text(cat, css_class="font-medium") Muted(f"{count} items, {total_qty} units") with Tabs(): with Tab("All Items"): _build_inventory_table() with Tab("Search"): _build_search_section() with Tab("Add Item"): _build_add_form() with Tab("Actions"): _build_actions_section() return PrefabApp( view=view, state={ "search_results": [], "filtered_items": list(_inventory), "recent_additions": [], "selected_category": "All", "adjust_id": "", "delete_id": "", "query": "", }, ) # --------------------------------------------------------------------------- # Server # --------------------------------------------------------------------------- mcp = FastMCP("Inventory Server", providers=[app]) if __name__ == "__main__": mcp.run(transport="http")