0

Common Patterns

awesome-llm-appspatterns

Complete end-to-end examples showing server + widget implementations for common use cases.

Sign in to save downloads to your library and vote.

Preview

Common Patterns

Complete end-to-end examples showing server + widget implementations for common use cases.

Examples: Weather app, Todo list, Recipe browser, File manager


Weather App

Server (index.ts)

import { MCPServer, text, widget, object, error } from "mcp-use/server";
import { z } from "zod";

const server = new MCPServer({
  name: "weather-server",
  title: "Weather Server",
  version: "1.0.0",
  baseUrl: process.env.MCP_URL || "http://localhost:3000",
});

// Mock weather data
const weatherData: Record<
  string,
  { temp: number; conditions: string; icon: string }
> = {
  "New York": { temp: 22, conditions: "Partly Cloudy", icon: "⛅" },
  London: { temp: 15, conditions: "Rainy", icon: "🌧️" },
  Tokyo: { temp: 28, conditions: "Sunny", icon: "☀️" },
  Paris: { temp: 18, conditions: "Overcast", icon: "☁️" },
  Sydney: { temp: 25, conditions: "Clear", icon: "🌤️" },
};

// Tool: Get weather with widget
server.tool(
  {
    name: "get-weather",
    description: "Get current weather for a city",
    schema: z.object({
      city: z.string().describe("City name (e.g., 'New York', 'Tokyo')"),
    }),
    widget: {
      name: "weather-display",
      invoking: "Fetching weather...",
      invoked: "Weather loaded",
    },
  },
  async ({ city }) => {
    const data = weatherData[city];

    if (!data) {
      return error(
        `No weather data for ${city}. Available cities: ${Object.keys(weatherData).join(", ")}`,
      );
    }

    return widget({
      props: {
        city,
        temp: data.temp,
        conditions: data.conditions,
        icon: data.icon,
        timestamp: new Date().toISOString(),
      },
      output: text(`Weather in ${city}: ${data.temp}°C, ${data.conditions}`),
    });
  },
);

// Resource: Available cities
server.resource(
  {
    name: "available_cities",
    uri: "weather://cities",
    title: "Available Cities",
    description: "List of cities with weather data",
  },
  async () =>
    object({
      cities: Object.keys(weatherData),
    }),
);

server.listen();

Widget (resources/weather-display.tsx)

import {
  McpUseProvider,
  useWidget,
  useWidgetTheme,
  type WidgetMetadata,
} from "mcp-use/react";
import { z } from "zod";

export const widgetMetadata: WidgetMetadata = {
  description: "Display weather information for a city",
  props: z.object({
    city: z.string(),
    temp: z.number(),
    conditions: z.string(),
    icon: z.string(),
    timestamp: z.string(),
  }),
  exposeAsTool: false,
};

export default function WeatherDisplay() {
  const { props, isPending } = useWidget();
  const theme = useWidgetTheme();

  if (isPending) {
    return (
      <McpUseProvider autoSize>
        <div style={{ padding: 40, textAlign: "center" }}>
          <div style={{ fontSize: 48, marginBottom: 16 }}>🌍</div>
          <p>Loading weather...</p>
        </div>
      </McpUseProvider>
    );
  }

  const bgColor = theme === "dark" ? "#1e1e1e" : "#ffffff";
  const textColor = theme === "dark" ? "#e0e0e0" : "#1a1a1a";
  const secondaryColor = theme === "dark" ? "#b0b0b0" : "#666";

  return (
    <McpUseProvider autoSize>
      <div
        style={{
          padding: 24,
          backgroundColor: bgColor,
          color: textColor,
          borderRadius: 8,
        }}
      >
        <h2 style={{ margin: "0 0 8px 0", fontSize: 24 }}>{props.city}</h2>
        <p
          style={{ margin: "0 0 20px 0", color: secondaryColor, fontSize: 12 }}
        >
          {new Date(props.timestamp).toLocaleString()}
        </p>

        <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
          <div style={{ fontSize: 64 }}>{props.icon}</div>
          <div>
            <div style={{ fontSize: 48, fontWeight: "bold" }}>
              {props.temp}°C
            </div>
            <div style={{ fontSize: 18, color: secondaryColor }}>
              {props.conditions}
            </div>
          </div>
        </div>
      </div>
    </McpUseProvider>
  );
}

Todo List

Server (index.ts)

import { MCPServer, text, widget, object, error } from "mcp-use/server";
import { z } from "zod";

const server = new MCPServer({
  name: "todo-server",
  title: "Todo Server",
  version: "1.0.0",
});

// Mock database
let todos: Array<{ id: string; title: string; completed: boolean }> = [
  { id: "1", title: "Learn MCP", completed: true },
  { id: "2", title: "Build first widget", completed: false },
  { id: "3", title: "Deploy server", completed: false },
];

// Tool: List todos with widget
server.tool(
  {
    name: "list-todos",
    description: "List all todos",
    schema: z.object({}),
    widget: {
      name: "todo-list",
      invoking: "Loading todos...",
      invoked: "Todos loaded",
    },
  },
  async () => {
    return widget({
      props: {
        todos,
        totalCount: todos.length,
        completedCount: todos.filter((t) => t.completed).length,
      },
      output: text(
        `Found ${todos.length} todos (${todos.filter((t) => t.completed).length} completed)`,
      ),
    });
  },
);

// Tool: Create todo
server.tool(
  {
    name: "create-todo",
    description: "Create a new todo",
    schema: z.object({
      title: z.string().describe("Todo title"),
    }),
  },
  async ({ title }) => {
    const newTodo = {
      id: Date.now().toString(),
      title,
      completed: false,
    };

    todos.push(newTodo);

    return text(`Created todo: ${title}`);
  },
);

// Tool: Toggle todo
server.tool(
  {
    name: "toggle-todo",
    description: "Toggle todo completion status",
    schema: z.object({
      id: z.string().describe("Todo ID"),
      completed: z.boolean().describe("New completion status"),
    }),
  },
  async ({ id, completed }) => {
    const todo = todos.find((t) => t.id === id);

    if (!todo) {
      return error(`Todo not found: ${id}`);
    }

    todo.completed = completed;

    return text(`Todo ${completed ? "completed" : "uncompleted"}`);
  },
);

// Tool: Delete todo
server.tool(
  {
    name: "delete-todo",
    description: "Delete a todo",
    schema: z.object({
      id: z.string().describe("Todo ID"),
    }),
    annotations: {
      destructiveHint: true,
    },
  },
  async ({ id }) => {
    const index = todos.findIndex((t) => t.id === id);

    if (index === -1) {
      return error(`Todo not found: ${id}`);
    }

    const deleted = todos.splice(index, 1)[0];

    return text(`Deleted todo: ${deleted.title}`);
  },
);

server.listen();

Widget (resources/todo-list.tsx)

import { useState } from "react";
import {
  McpUseProvider,
  useWidget,
  useWidgetTheme,
  useCallTool,
  type WidgetMetadata,
} from "mcp-use/react";
import { z } from "zod";

export const widgetMetadata: WidgetMetadata = {
  description: "Interactive todo list",
  props: z.object({
    todos: z.array(
      z.object({
        id: z.string(),
        title: z.string(),
        completed: z.boolean(),
      }),
    ),
    totalCount: z.number(),
    completedCount: z.number(),
  }),
  exposeAsTool: false,
};

export default function TodoList() {
  const { props, isPending } = useWidget();
  const theme = useWidgetTheme();
  const { callTool: createTodo, isPending: isCreating } =
    useCallTool("create-todo");
  const { callTool: toggleTodo } = useCallTool("toggle-todo");
  const { callTool: deleteTodo } = useCallTool("delete-todo");
  const [newTodo, setNewTodo] = useState("");
  const [deletingId, setDeletingId] = useState<string | null>(null);

  if (isPending) {
    return (
      <McpUseProvider autoSize>
        <div style={{ padding: 20 }}>Loading todos...</div>
      </McpUseProvider>
    );
  }

  // Theme-aware colors (see ui-guidelines.md for useColors() hook pattern)
  const colors = {
    bg: theme === "dark" ? "#1e1e1e" : "#ffffff",
    text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
    border: theme === "dark" ? "#404040" : "#e0e0e0",
    hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
  };

  const handleCreate = (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTodo.trim()) return;

    createTodo(
      { title: newTodo },
      {
        onSuccess: () => setNewTodo(""),
        onError: () => alert("Failed to create todo"),
      },
    );
  };

  const handleToggle = (id: string, completed: boolean) => {
    toggleTodo({ id, completed: !completed });
  };

  const handleDelete = (id: string) => {
    setDeletingId(id);
    deleteTodo(
      { id },
      {
        onError: () => alert("Failed to delete"),
        onSettled: () => setDeletingId(null),
      },
    );
  };

  return (
    <McpUseProvider autoSize>
      <div
        style={{ padding: 20, backgroundColor: colors.bg, color: colors.text }}
      >
        <h2 style={{ margin: "0 0 8px 0" }}>
          Todos ({props.completedCount}/{props.totalCount})
        </h2>

        {/* Create form */}
        <form
          onSubmit={handleCreate}
          style={{ marginBottom: 16, display: "flex", gap: 8 }}
        >
          <input
            type="text"
            value={newTodo}
            onChange={(e) => setNewTodo(e.target.value)}
            placeholder="New todo..."
            disabled={isCreating}
            style={{
              flex: 1,
              padding: 8,
              border: `1px solid ${colors.border}`,
              borderRadius: 4,
              backgroundColor: colors.bg,
              color: colors.text,
            }}
          />
          <button
            type="submit"
            disabled={isCreating}
            style={{
              padding: "8px 16px",
              border: "none",
              borderRadius: 4,
              backgroundColor: "#0066cc",
              color: "white",
              cursor: isCreating ? "not-allowed" : "pointer",
            }}
          >
            {isCreating ? "Adding..." : "Add"}
          </button>
        </form>

        {/* Todo list */}
        <div>
          {props.todos.map((todo) => (
            <div
              key={todo.id}
              style={{
                display: "flex",
                alignItems: "center",
                gap: 8,
                padding: 12,
                borderBottom: `1px solid ${colors.border}`,
                backgroundColor: colors.bg,
              }}
            >
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => handleToggle(todo.id, todo.completed)}
                style={{ cursor: "pointer" }}
              />
              <span
                style={{
                  flex: 1,
                  textDecoration: todo.completed ? "line-through" : "none",
                  opacity: todo.completed ? 0.6 : 1,
                }}
              >
                {todo.title}
              </span>
              <button
                onClick={() => handleDelete(todo.id)}
                disabled={deletingId === todo.id}
                style={{
                  padding: "4px 12px",
                  border: "none",
                  borderRadius: 4,
                  backgroundColor: "transparent",
                  color: "#dc3545",
                  cursor: deletingId === todo.id ? "not-allowed" : "pointer",
                }}
              >
                {deletingId === todo.id ? "..." : "Delete"}
              </button>
            </div>
          ))}
        </div>

        {props.todos.length === 0 && (
          <p style={{ textAlign: "center", color: colors.border, padding: 40 }}>
            No todos yet. Create one above!
          </p>
        )}
      </div>
    </McpUseProvider>
  );
}

Recipe Browser

Server (index.ts)

import { MCPServer, widget, text } from "mcp-use/server";
import { z } from "zod";

const server = new MCPServer({
  name: "recipe-server",
  title: "Recipe Server",
  version: "1.0.0",
});

// Mock recipe data
const recipes = [
  {
    id: "1",
    name: "Spaghetti Carbonara",
    category: "Italian",
    time: 20,
    difficulty: "Easy",
    ingredients: ["Spaghetti", "Eggs", "Bacon", "Parmesan", "Black pepper"],
    instructions:
      "Cook pasta. Fry bacon. Mix eggs and cheese. Combine all with pasta.",
  },
  {
    id: "2",
    name: "Chicken Tikka Masala",
    category: "Indian",
    time: 45,
    difficulty: "Medium",
    ingredients: ["Chicken", "Yogurt", "Tomatoes", "Cream", "Spices"],
    instructions: "Marinate chicken. Cook in spiced tomato sauce. Add cream.",
  },
  {
    id: "3",
    name: "Caesar Salad",
    category: "Salad",
    time: 15,
    difficulty: "Easy",
    ingredients: ["Romaine lettuce", "Croutons", "Parmesan", "Caesar dressing"],
    instructions: "Toss lettuce with dressing. Top with croutons and cheese.",
  },
];

// Tool: Browse recipes
server.tool(
  {
    name: "browse-recipes",
    description: "Browse recipe collection",
    schema: z.object({
      category: z
        .string()
        .optional()
        .describe("Filter by category (Italian, Indian, Salad)"),
    }),
    widget: {
      name: "recipe-browser",
      invoking: "Loading recipes...",
      invoked: "Recipes loaded",
    },
  },
  async ({ category }) => {
    const filtered = category
      ? recipes.filter((r) => r.category === category)
      : recipes;

    return widget({
      props: {
        recipes: filtered,
        categories: ["All", ...new Set(recipes.map((r) => r.category))],
        selectedCategory: category || "All",
      },
      output: text(`Found ${filtered.length} recipes`),
    });
  },
);

server.listen();

Widget (resources/recipe-browser.tsx)

import { useState, useEffect } from "react";
import {
  McpUseProvider,
  useWidget,
  useWidgetTheme,
  type WidgetMetadata,
} from "mcp-use/react";
import { z } from "zod";

export const widgetMetadata: WidgetMetadata = {
  description: "Browse and view recipes",
  props: z.object({
    recipes: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
        category: z.string(),
        time: z.number(),
        difficulty: z.string(),
        ingredients: z.array(z.string()),
        instructions: z.string(),
      }),
    ),
    categories: z.array(z.string()),
    selectedCategory: z.string(),
  }),
  exposeAsTool: false,
};

export default function RecipeBrowser() {
  const { props, isPending } = useWidget();
  const theme = useWidgetTheme();
  const [selectedRecipe, setSelectedRecipe] = useState<string | null>(null);
  const [filter, setFilter] = useState<string>("all");

  // Sync initial filter from props once loaded
  useEffect(() => {
    if (!isPending && props.selectedCategory) {
      setFilter(props.selectedCategory);
    }
  }, [isPending, props.selectedCategory]);

  if (isPending) {
    return (
      <McpUseProvider autoSize>
        <div style={{ padding: 20 }}>Loading recipes...</div>
      </McpUseProvider>
    );
  }

  // Theme-aware colors (see ui-guidelines.md for useColors() hook pattern)
  const colors = {
    bg: theme === "dark" ? "#1e1e1e" : "#ffffff",
    text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
    secondary: theme === "dark" ? "#b0b0b0" : "#666",
    border: theme === "dark" ? "#404040" : "#e0e0e0",
    hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
  };

  const filteredRecipes =
    filter === "All"
      ? props.recipes
      : props.recipes.filter((r) => r.category === filter);

  const selected = filteredRecipes.find((r) => r.id === selectedRecipe);

  return (
    <McpUseProvider autoSize>
      <div style={{ backgroundColor: colors.bg, color: colors.text }}>
        {/* Header */}
        <div
          style={{ padding: 16, borderBottom: `1px solid ${colors.border}` }}
        >
          <h2 style={{ margin: "0 0 12px 0" }}>Recipe Browser</h2>

          {/* Category filters */}
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            {props.categories.map((cat) => (
              <button
                key={cat}
                onClick={() => setFilter(cat)}
                style={{
                  padding: "6px 12px",
                  border: `1px solid ${colors.border}`,
                  borderRadius: 4,
                  backgroundColor: filter === cat ? "#0066cc" : "transparent",
                  color: filter === cat ? "white" : colors.text,
                  cursor: "pointer",
                  fontSize: 14,
                }}
              >
                {cat}
              </button>
            ))}
          </div>
        </div>

        {/* Recipe list */}
        <div style={{ display: "flex" }}>
          <div style={{ flex: 1, borderRight: `1px solid ${colors.border}` }}>
            {filteredRecipes.map((recipe) => (
              <div
                key={recipe.id}
                onClick={() => setSelectedRecipe(recipe.id)}
                style={{
                  padding: 16,
                  borderBottom: `1px solid ${colors.border}`,
                  cursor: "pointer",
                  backgroundColor:
                    selectedRecipe === recipe.id ? colors.hover : "transparent",
                }}
              >
                <h3 style={{ margin: "0 0 4px 0", fontSize: 16 }}>
                  {recipe.name}
                </h3>
                <p
                  style={{
                    margin: 0,
                    fontSize: 12,
                    color: colors.secondary,
                  }}
                >
                  {recipe.category} • {recipe.time} min • {recipe.difficulty}
                </p>
              </div>
            ))}
          </div>

          {/* Recipe detail */}
          <div style={{ flex: 2, padding: 16 }}>
            {selected ? (
              <div>
                <h2 style={{ margin: "0 0 8px 0" }}>{selected.name}</h2>
                <p style={{ margin: "0 0 16px 0", color: colors.secondary }}>
                  {selected.category} • {selected.time} minutes •{" "}
                  {selected.difficulty}
                </p>

                <h3 style={{ fontSize: 16, marginBottom: 8 }}>Ingredients</h3>
                <ul style={{ marginBottom: 16, paddingLeft: 20 }}>
                  {selected.ingredients.map((ing, i) => (
                    <li key={i}>{ing}</li>
                  ))}
                </ul>

                <h3 style={{ fontSize: 16, marginBottom: 8 }}>Instructions</h3>
                <p style={{ lineHeight: 1.6 }}>{selected.instructions}</p>
              </div>
            ) : (
              <p
                style={{
                  color: colors.secondary,
                  textAlign: "center",
                  paddingTop: 40,
                }}
              >
                Select a recipe to view details
              </p>
            )}
          </div>
        </div>
      </div>
    </McpUseProvider>
  );
}

Key Patterns Demonstrated

1. Mock Data First

All examples use mock data, making it easy to prototype and test before connecting real APIs.

2. Widget + Tool Combination

Each example shows how to pair a tool with a widget for visual output.

3. Interactive Actions

Todo list shows create/update/delete operations from within widgets using useCallTool().

4. Theme Support

All widgets use useWidgetTheme() to adapt to light/dark mode.

5. State Management

Recipe browser demonstrates local widget state (selected recipe, filters) vs server state (recipe data).

6. Error Handling

Weather app shows proper error responses when data not found.

7. Loading States

All widgets check isPending and show loading UI.

8. Master-Detail Layout

Recipe browser shows a master-detail pattern with list + detail view.


Expanding These Examples

Add Real APIs

Replace mock data with API calls:

// Weather with real API
const WEATHER_API_KEY = process.env.WEATHER_API_KEY;

server.tool(
  {
    name: "get-weather",
    schema: z.object({ city: z.string() }),
    widget: { name: "weather-display" },
  },
  async ({ city }) => {
    if (!WEATHER_API_KEY) {
      return error(
        "WEATHER_API_KEY not configured. Set it in environment variables.",
      );
    }

    const response = await fetch(
      `https://api.weatherapi.com/v1/current.json?key=${WEATHER_API_KEY}&q=${city}`,
    );

    if (!response.ok) {
      return error(`Weather API error: ${response.statusText}`);
    }

    const data = await response.json();

    return widget({
      props: {
        city: data.location.name,
        temp: data.current.temp_c,
        conditions: data.current.condition.text,
        icon: data.current.condition.icon,
        timestamp: data.current.last_updated,
      },
      output: text(
        `Weather in ${city}: ${data.current.temp_c}°C, ${data.current.condition.text}`,
      ),
    });
  },
);

Add Database

Replace in-memory data with database:

import { Database } from "better-sqlite3";

const db = new Database("todos.db");

db.exec(`
  CREATE TABLE IF NOT EXISTS todos (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    completed INTEGER NOT NULL DEFAULT 0,
    created_at TEXT NOT NULL
  )
`);

server.tool(
  { name: "list-todos", schema: z.object({}), widget: { name: "todo-list" } },
  async () => {
    const todos = db
      .prepare("SELECT * FROM todos ORDER BY created_at DESC")
      .all();

    return widget({
      props: {
        todos: todos.map((t) => ({ ...t, completed: Boolean(t.completed) })),
        totalCount: todos.length,
        completedCount: todos.filter((t) => t.completed).length,
      },
      output: text(`Found ${todos.length} todos`),
    });
  },
);

Next Steps

Ingestion metadata

Source catalog
awesome-llm-apps
File path
generative_ui_agents/ai-mcp-app-builder/apps/mcp-use-server/.agent/skills/mcp-apps-builder/references/patterns/common-patterns.md
Last refreshed
10/24/2026, 3:00:14 AM (5h ago)
Refresh schedule
Daily · 03:00 UTC
Dedupe status
Unique · deduped by (source, url)