Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
DEPRECATED: Replaced by mcp-app-builder. Previously used to build MCP servers with tools, resources, and prompts via mcp-use.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/patterns/common-patterns.md
1# Common Patterns23Complete end-to-end examples showing server + widget implementations for common use cases.45**Examples:** Weather app, Todo list, Recipe browser, File manager67---89## Weather App1011### Server (index.ts)1213```typescript14import { MCPServer, text, widget, object, error } from "mcp-use/server";15import { z } from "zod";1617const server = new MCPServer({18name: "weather-server",19title: "Weather Server",20version: "1.0.0",21baseUrl: process.env.MCP_URL || "http://localhost:3000"22});2324// Mock weather data25const weatherData: Record<string, { temp: number; conditions: string; icon: string }> = {26"New York": { temp: 22, conditions: "Partly Cloudy", icon: "⛅" },27"London": { temp: 15, conditions: "Rainy", icon: "🌧️" },28"Tokyo": { temp: 28, conditions: "Sunny", icon: "☀️" },29"Paris": { temp: 18, conditions: "Overcast", icon: "☁️" },30"Sydney": { temp: 25, conditions: "Clear", icon: "🌤️" }31};3233// Tool: Get weather with widget34server.tool(35{36name: "get-weather",37description: "Get current weather for a city",38schema: z.object({39city: z.string().describe("City name (e.g., 'New York', 'Tokyo')")40}),41widget: {42name: "weather-display",43invoking: "Fetching weather...",44invoked: "Weather loaded"45}46},47async ({ city }) => {48const data = weatherData[city];4950if (!data) {51return error(`No weather data for ${city}. Available cities: ${Object.keys(weatherData).join(", ")}`);52}5354return widget({55props: {56city,57temp: data.temp,58conditions: data.conditions,59icon: data.icon,60timestamp: new Date().toISOString()61},62output: text(`Weather in ${city}: ${data.temp}°C, ${data.conditions}`)63});64}65);6667// Resource: Available cities68server.resource(69{70name: "available_cities",71uri: "weather://cities",72title: "Available Cities",73description: "List of cities with weather data"74},75async () => object({76cities: Object.keys(weatherData)77})78);7980server.listen();81```8283### Widget (resources/weather-display.tsx)8485```tsx86import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";87import { z } from "zod";8889export const widgetMetadata: WidgetMetadata = {90description: "Display weather information for a city",91props: z.object({92city: z.string(),93temp: z.number(),94conditions: z.string(),95icon: z.string(),96timestamp: z.string()97}),98exposeAsTool: false99};100101export default function WeatherDisplay() {102const { props, isPending } = useWidget();103const theme = useWidgetTheme();104105if (isPending) {106return (107<McpUseProvider autoSize>108<div style={{ padding: 40, textAlign: "center" }}>109<div style={{ fontSize: 48, marginBottom: 16 }}>🌍</div>110<p>Loading weather...</p>111</div>112</McpUseProvider>113);114}115116const bgColor = theme === "dark" ? "#1e1e1e" : "#ffffff";117const textColor = theme === "dark" ? "#e0e0e0" : "#1a1a1a";118const secondaryColor = theme === "dark" ? "#b0b0b0" : "#666";119120return (121<McpUseProvider autoSize>122<div style={{123padding: 24,124backgroundColor: bgColor,125color: textColor,126borderRadius: 8127}}>128<h2 style={{ margin: "0 0 8px 0", fontSize: 24 }}>{props.city}</h2>129<p style={{ margin: "0 0 20px 0", color: secondaryColor, fontSize: 12 }}>130{new Date(props.timestamp).toLocaleString()}131</p>132133<div style={{ display: "flex", alignItems: "center", gap: 16 }}>134<div style={{ fontSize: 64 }}>{props.icon}</div>135<div>136<div style={{ fontSize: 48, fontWeight: "bold" }}>{props.temp}°C</div>137<div style={{ fontSize: 18, color: secondaryColor }}>{props.conditions}</div>138</div>139</div>140</div>141</McpUseProvider>142);143}144```145146---147148## Todo List149150### Server (index.ts)151152```typescript153import { MCPServer, text, widget, object, error } from "mcp-use/server";154import { z } from "zod";155156const server = new MCPServer({157name: "todo-server",158title: "Todo Server",159version: "1.0.0"160});161162// Mock database163let todos: Array<{ id: string; title: string; completed: boolean }> = [164{ id: "1", title: "Learn MCP", completed: true },165{ id: "2", title: "Build first widget", completed: false },166{ id: "3", title: "Deploy server", completed: false }167];168169// Tool: List todos with widget170server.tool(171{172name: "list-todos",173description: "List all todos",174schema: z.object({}),175widget: {176name: "todo-list",177invoking: "Loading todos...",178invoked: "Todos loaded"179}180},181async () => {182return widget({183props: {184todos,185totalCount: todos.length,186completedCount: todos.filter(t => t.completed).length187},188output: text(`Found ${todos.length} todos (${todos.filter(t => t.completed).length} completed)`)189});190}191);192193// Tool: Create todo194server.tool(195{196name: "create-todo",197description: "Create a new todo",198schema: z.object({199title: z.string().describe("Todo title")200})201},202async ({ title }) => {203const newTodo = {204id: Date.now().toString(),205title,206completed: false207};208209todos.push(newTodo);210211return text(`Created todo: ${title}`);212}213);214215// Tool: Toggle todo216server.tool(217{218name: "toggle-todo",219description: "Toggle todo completion status",220schema: z.object({221id: z.string().describe("Todo ID"),222completed: z.boolean().describe("New completion status")223})224},225async ({ id, completed }) => {226const todo = todos.find(t => t.id === id);227228if (!todo) {229return error(`Todo not found: ${id}`);230}231232todo.completed = completed;233234return text(`Todo ${completed ? "completed" : "uncompleted"}`);235}236);237238// Tool: Delete todo239server.tool(240{241name: "delete-todo",242description: "Delete a todo",243schema: z.object({244id: z.string().describe("Todo ID")245}),246annotations: {247destructiveHint: true248}249},250async ({ id }) => {251const index = todos.findIndex(t => t.id === id);252253if (index === -1) {254return error(`Todo not found: ${id}`);255}256257const deleted = todos.splice(index, 1)[0];258259return text(`Deleted todo: ${deleted.title}`);260}261);262263server.listen();264```265266### Widget (resources/todo-list.tsx)267268```tsx269import { useState } from "react";270import { McpUseProvider, useWidget, useWidgetTheme, useCallTool, type WidgetMetadata } from "mcp-use/react";271import { z } from "zod";272273export const widgetMetadata: WidgetMetadata = {274description: "Interactive todo list",275props: z.object({276todos: z.array(z.object({277id: z.string(),278title: z.string(),279completed: z.boolean()280})),281totalCount: z.number(),282completedCount: z.number()283}),284exposeAsTool: false285};286287export default function TodoList() {288const { props, isPending } = useWidget();289const theme = useWidgetTheme();290const { callTool: createTodo, isPending: isCreating } = useCallTool("create-todo");291const { callTool: toggleTodo } = useCallTool("toggle-todo");292const { callTool: deleteTodo } = useCallTool("delete-todo");293const [newTodo, setNewTodo] = useState("");294const [deletingId, setDeletingId] = useState<string | null>(null);295296if (isPending) {297return (298<McpUseProvider autoSize>299<div style={{ padding: 20 }}>Loading todos...</div>300</McpUseProvider>301);302}303304// Theme-aware colors (see ui-guidelines.md for useColors() hook pattern)305const colors = {306bg: theme === "dark" ? "#1e1e1e" : "#ffffff",307text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",308border: theme === "dark" ? "#404040" : "#e0e0e0",309hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5"310};311312const handleCreate = (e: React.FormEvent) => {313e.preventDefault();314if (!newTodo.trim()) return;315316createTodo({ title: newTodo }, {317onSuccess: () => setNewTodo(""),318onError: () => alert("Failed to create todo"),319});320};321322const handleToggle = (id: string, completed: boolean) => {323toggleTodo({ id, completed: !completed });324};325326const handleDelete = (id: string) => {327setDeletingId(id);328deleteTodo({ id }, {329onError: () => alert("Failed to delete"),330onSettled: () => setDeletingId(null),331});332};333334return (335<McpUseProvider autoSize>336<div style={{ padding: 20, backgroundColor: colors.bg, color: colors.text }}>337<h2 style={{ margin: "0 0 8px 0" }}>338Todos ({props.completedCount}/{props.totalCount})339</h2>340341{/* Create form */}342<form onSubmit={handleCreate} style={{ marginBottom: 16, display: "flex", gap: 8 }}>343<input344type="text"345value={newTodo}346onChange={e => setNewTodo(e.target.value)}347placeholder="New todo..."348disabled={isCreating}349style={{350flex: 1,351padding: 8,352border: `1px solid ${colors.border}`,353borderRadius: 4,354backgroundColor: colors.bg,355color: colors.text356}}357/>358<button359type="submit"360disabled={isCreating}361style={{362padding: "8px 16px",363border: "none",364borderRadius: 4,365backgroundColor: "#0066cc",366color: "white",367cursor: isCreating ? "not-allowed" : "pointer"368}}369>370{isCreating ? "Adding..." : "Add"}371</button>372</form>373374{/* Todo list */}375<div>376{props.todos.map(todo => (377<div378key={todo.id}379style={{380display: "flex",381alignItems: "center",382gap: 8,383padding: 12,384borderBottom: `1px solid ${colors.border}`,385backgroundColor: colors.bg386}}387>388<input389type="checkbox"390checked={todo.completed}391onChange={() => handleToggle(todo.id, todo.completed)}392style={{ cursor: "pointer" }}393/>394<span style={{395flex: 1,396textDecoration: todo.completed ? "line-through" : "none",397opacity: todo.completed ? 0.6 : 1398}}>399{todo.title}400</span>401<button402onClick={() => handleDelete(todo.id)}403disabled={deletingId === todo.id}404style={{405padding: "4px 12px",406border: "none",407borderRadius: 4,408backgroundColor: "transparent",409color: "#dc3545",410cursor: deletingId === todo.id ? "not-allowed" : "pointer"411}}412>413{deletingId === todo.id ? "..." : "Delete"}414</button>415</div>416))}417</div>418419{props.todos.length === 0 && (420<p style={{ textAlign: "center", color: colors.border, padding: 40 }}>421No todos yet. Create one above!422</p>423)}424</div>425</McpUseProvider>426);427}428```429430---431432## Recipe Browser433434### Server (index.ts)435436```typescript437import { MCPServer, widget, text } from "mcp-use/server";438import { z } from "zod";439440const server = new MCPServer({441name: "recipe-server",442title: "Recipe Server",443version: "1.0.0"444});445446// Mock recipe data447const recipes = [448{449id: "1",450name: "Spaghetti Carbonara",451category: "Italian",452time: 20,453difficulty: "Easy",454ingredients: ["Spaghetti", "Eggs", "Bacon", "Parmesan", "Black pepper"],455instructions: "Cook pasta. Fry bacon. Mix eggs and cheese. Combine all with pasta."456},457{458id: "2",459name: "Chicken Tikka Masala",460category: "Indian",461time: 45,462difficulty: "Medium",463ingredients: ["Chicken", "Yogurt", "Tomatoes", "Cream", "Spices"],464instructions: "Marinate chicken. Cook in spiced tomato sauce. Add cream."465},466{467id: "3",468name: "Caesar Salad",469category: "Salad",470time: 15,471difficulty: "Easy",472ingredients: ["Romaine lettuce", "Croutons", "Parmesan", "Caesar dressing"],473instructions: "Toss lettuce with dressing. Top with croutons and cheese."474}475];476477// Tool: Browse recipes478server.tool(479{480name: "browse-recipes",481description: "Browse recipe collection",482schema: z.object({483category: z.string().optional().describe("Filter by category (Italian, Indian, Salad)")484}),485widget: {486name: "recipe-browser",487invoking: "Loading recipes...",488invoked: "Recipes loaded"489}490},491async ({ category }) => {492const filtered = category493? recipes.filter(r => r.category === category)494: recipes;495496return widget({497props: {498recipes: filtered,499categories: ["All", ...new Set(recipes.map(r => r.category))],500selectedCategory: category || "All"501},502output: text(`Found ${filtered.length} recipes`)503});504}505);506507server.listen();508```509510### Widget (resources/recipe-browser.tsx)511512```tsx513import { useState, useEffect } from "react";514import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";515import { z } from "zod";516517export const widgetMetadata: WidgetMetadata = {518description: "Browse and view recipes",519props: z.object({520recipes: z.array(z.object({521id: z.string(),522name: z.string(),523category: z.string(),524time: z.number(),525difficulty: z.string(),526ingredients: z.array(z.string()),527instructions: z.string()528})),529categories: z.array(z.string()),530selectedCategory: z.string()531}),532exposeAsTool: false533};534535export default function RecipeBrowser() {536const { props, isPending } = useWidget();537const theme = useWidgetTheme();538const [selectedRecipe, setSelectedRecipe] = useState<string | null>(null);539const [filter, setFilter] = useState<string>("all");540541// Sync initial filter from props once loaded542useEffect(() => {543if (!isPending && props.selectedCategory) {544setFilter(props.selectedCategory);545}546}, [isPending, props.selectedCategory]);547548if (isPending) {549return (550<McpUseProvider autoSize>551<div style={{ padding: 20 }}>Loading recipes...</div>552</McpUseProvider>553);554}555556// Theme-aware colors (see ui-guidelines.md for useColors() hook pattern)557const colors = {558bg: theme === "dark" ? "#1e1e1e" : "#ffffff",559text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",560secondary: theme === "dark" ? "#b0b0b0" : "#666",561border: theme === "dark" ? "#404040" : "#e0e0e0",562hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5"563};564565const filteredRecipes = filter === "All"566? props.recipes567: props.recipes.filter(r => r.category === filter);568569const selected = filteredRecipes.find(r => r.id === selectedRecipe);570571return (572<McpUseProvider autoSize>573<div style={{ backgroundColor: colors.bg, color: colors.text }}>574{/* Header */}575<div style={{ padding: 16, borderBottom: `1px solid ${colors.border}` }}>576<h2 style={{ margin: "0 0 12px 0" }}>Recipe Browser</h2>577578{/* Category filters */}579<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>580{props.categories.map(cat => (581<button582key={cat}583onClick={() => setFilter(cat)}584style={{585padding: "6px 12px",586border: `1px solid ${colors.border}`,587borderRadius: 4,588backgroundColor: filter === cat ? "#0066cc" : "transparent",589color: filter === cat ? "white" : colors.text,590cursor: "pointer",591fontSize: 14592}}593>594{cat}595</button>596))}597</div>598</div>599600{/* Recipe list */}601<div style={{ display: "flex" }}>602<div style={{ flex: 1, borderRight: `1px solid ${colors.border}` }}>603{filteredRecipes.map(recipe => (604<div605key={recipe.id}606onClick={() => setSelectedRecipe(recipe.id)}607style={{608padding: 16,609borderBottom: `1px solid ${colors.border}`,610cursor: "pointer",611backgroundColor: selectedRecipe === recipe.id ? colors.hover : "transparent"612}}613>614<h3 style={{ margin: "0 0 4px 0", fontSize: 16 }}>{recipe.name}</h3>615<p style={{616margin: 0,617fontSize: 12,618color: colors.secondary619}}>620{recipe.category} • {recipe.time} min • {recipe.difficulty}621</p>622</div>623))}624</div>625626{/* Recipe detail */}627<div style={{ flex: 2, padding: 16 }}>628{selected ? (629<div>630<h2 style={{ margin: "0 0 8px 0" }}>{selected.name}</h2>631<p style={{ margin: "0 0 16px 0", color: colors.secondary }}>632{selected.category} • {selected.time} minutes • {selected.difficulty}633</p>634635<h3 style={{ fontSize: 16, marginBottom: 8 }}>Ingredients</h3>636<ul style={{ marginBottom: 16, paddingLeft: 20 }}>637{selected.ingredients.map((ing, i) => (638<li key={i}>{ing}</li>639))}640</ul>641642<h3 style={{ fontSize: 16, marginBottom: 8 }}>Instructions</h3>643<p style={{ lineHeight: 1.6 }}>{selected.instructions}</p>644</div>645) : (646<p style={{ color: colors.secondary, textAlign: "center", paddingTop: 40 }}>647Select a recipe to view details648</p>649)}650</div>651</div>652</div>653</McpUseProvider>654);655}656```657658---659660## Key Patterns Demonstrated661662### 1. **Mock Data First**663All examples use mock data, making it easy to prototype and test before connecting real APIs.664665### 2. **Widget + Tool Combination**666Each example shows how to pair a tool with a widget for visual output.667668### 3. **Interactive Actions**669Todo list shows create/update/delete operations from within widgets using `useCallTool()`.670671### 4. **Theme Support**672All widgets use `useWidgetTheme()` to adapt to light/dark mode.673674### 5. **State Management**675Recipe browser demonstrates local widget state (selected recipe, filters) vs server state (recipe data).676677### 6. **Error Handling**678Weather app shows proper error responses when data not found.679680### 7. **Loading States**681All widgets check `isPending` and show loading UI.682683### 8. **Master-Detail Layout**684Recipe browser shows a master-detail pattern with list + detail view.685686---687688## Expanding These Examples689690### Add Real APIs691Replace mock data with API calls:692693```typescript694// Weather with real API695const WEATHER_API_KEY = process.env.WEATHER_API_KEY;696697server.tool(698{ name: "get-weather", schema: z.object({ city: z.string() }), widget: { name: "weather-display" } },699async ({ city }) => {700if (!WEATHER_API_KEY) {701return error("WEATHER_API_KEY not configured. Set it in environment variables.");702}703704const response = await fetch(705`https://api.weatherapi.com/v1/current.json?key=${WEATHER_API_KEY}&q=${city}`706);707708if (!response.ok) {709return error(`Weather API error: ${response.statusText}`);710}711712const data = await response.json();713714return widget({715props: {716city: data.location.name,717temp: data.current.temp_c,718conditions: data.current.condition.text,719icon: data.current.condition.icon,720timestamp: data.current.last_updated721},722output: text(`Weather in ${city}: ${data.current.temp_c}°C, ${data.current.condition.text}`)723});724}725);726```727728### Add Database729Replace in-memory data with database:730731```typescript732import { Database } from "better-sqlite3";733734const db = new Database("todos.db");735736db.exec(`737CREATE TABLE IF NOT EXISTS todos (738id TEXT PRIMARY KEY,739title TEXT NOT NULL,740completed INTEGER NOT NULL DEFAULT 0,741created_at TEXT NOT NULL742)743`);744745server.tool(746{ name: "list-todos", schema: z.object({}), widget: { name: "todo-list" } },747async () => {748const todos = db.prepare("SELECT * FROM todos ORDER BY created_at DESC").all();749750return widget({751props: {752todos: todos.map(t => ({ ...t, completed: Boolean(t.completed) })),753totalCount: todos.length,754completedCount: todos.filter(t => t.completed).length755},756output: text(`Found ${todos.length} todos`)757});758}759);760```761762---763764## Next Steps765766- **Review server concepts** → [../server/tools.md](../server/tools.md)767- **Learn widget basics** → [../widgets/basics.md](../widgets/basics.md)768- **Check best practices** → [../../SKILL.md](../../SKILL.md)769