Skip to content

Recipes

Picture a chef in a well-equipped kitchen. They have the ingredients (MCP data) and the utensils (UI widgets). But without a recipe, they’d have to improvise every dish. Recipes tell them: “with these ingredients, use these utensils, in this order, to get this result.” That’s exactly what recipes do in webmcp-auto-ui.

A recipe is a composition guide that tells the agent how to transform data into an interface. There are two complementary types:

WebMCP Recipe (UI)MCP Recipe (server)
Who provides itThe agent package (.md files)The remote MCP server
What it guidesHow to display data (which widgets, what layout)How to get data (which tools, in what order)
AnalogyThe plating recipeThe cooking recipe

Without recipes, the agent would have to:

  1. Guess which MCP tools to call and in what order
  2. Guess which widget fits which type of data
  3. Improvise the layout for every request

With recipes, the agent follows a guided path: it finds a relevant recipe, reads it, and executes it. The result is more reliable, faster (fewer reasoning tokens), and more consistent.

sequenceDiagram
participant U as User
participant A as Agent LLM
participant SR as search_recipes
participant GR as get_recipe
participant MCP as MCP Server
participant WD as widget_display
U->>A: "Build me a deputy dashboard"
Note over A: STEP 1 -- Recipe search
A->>SR: search_recipes("deputy")
SR-->>A: [{name: "parlementaire-profile"}, {name: "dashboard-kpi"}]
Note over A: STEP 2 -- Read the recipe
A->>GR: get_recipe("parlementaire-profile")
GR-->>A: {schema: {...}, recipe: "1. Call search_deputes...\n2. Use profile + hemicycle..."}
Note over A: STEP 3 -- Execute DATA tools
A->>MCP: search_deputes({groupe: "ECO"})
MCP-->>A: [{nom: "Dupont", ...}, ...]
Note over A: STEP 4 -- UI display
A->>WD: widget_display({name: "profile", params: {name: "Dupont", ...}})
WD-->>A: {widget: "profile", id: "w_1"}
A->>WD: widget_display({name: "hemicycle", params: {groups: [...]}})
WD-->>A: {widget: "hemicycle", id: "w_2"}
A-->>U: Dashboard with profile + hemicycle

Recipe details are never injected into the initial prompt. Only names and descriptions are present. The LLM requests the full body only when needed:

Initial prompt (~500 tokens for 10 recipes):
"Available recipes:
- parlementaire-profile: deputy profile [profile, hemicycle, timeline]
- dashboard-kpi: numeric metrics [stat-card, chart, table]"
-> The LLM calls get_recipe("parlementaire-profile")
-> It receives the full body (~200 tokens) only when it needs it

Comparison: injecting 10 complete recipes into the prompt would cost ~2000 extra tokens. Lazy loading reduces this to ~500 fixed tokens + ~200 tokens per recipe actually used.

WebMCP recipes guide the LLM on widget selection and arrangement. They are Markdown files with YAML frontmatter, distributed with the @webmcp-auto-ui/agent package.

---
id: compose-kpi-dashboard
name: Compose a KPI dashboard
components_used: [stat-card, chart, table, kv]
when: data contains numeric metrics
servers: []
layout:
type: grid
columns: 3
arrangement: stat-cards in a row, chart + table below
---
## When to use
MCP results contain numeric metrics that need a synthetic
presentation: totals, percentages, time series.
## How
1. Identify the 3-5 main KPIs
2. Display each KPI as a stat-card with proper formatting
3. Add a chart if time series data exists
4. Complete with a data-table for details
## Common mistakes
- Too many stat-cards: beyond 5, switch to kv or table
- Unformatted numbers: "45230" instead of "45,230"
FieldTypeDescription
idstringUnique recipe identifier
namestringHuman-readable name for agent and user
components_usedstring[]Widgets recommended by this recipe
whenstringTrigger condition (free text)
serversstring[]Target MCP servers. Empty = universal
layoutobjectLayout suggestion (type, columns, arrangement)

The body (everything after the frontmatter) contains detailed instructions in Markdown.

interface Recipe {
id: string;
name: string;
description?: string;
components_used?: string[];
layout?: { type: string; columns?: number; arrangement?: string };
when: string;
servers?: string[];
body: string;
}
IDWhenRecommended widgets
composer-tableau-de-bord-kpiNumeric metrics, KPIsstat-card, chart, table, kv
afficher-oeuvres-art-collection-museeImage or art collectionsgallery, cards, carousel
analyser-actualites-hacker-newsNews articles, feedscards, table, stat-card
cartographier-observations-biodiversiteGeographic data, observationsmap, stat-card, table
explorer-dossiers-legislatifsLegislative records, billstimeline, kv, table
gallery-imagesMultiple image collectionsgallery, carousel
parlementaire-profileDeputy or senator profileprofile, hemicycle, timeline
rechercher-textes-juridiquesLegal texts, statutory articleslist, kv, code
weather-vizWeather datastat-card, chart
cross-serverMulti-server datatable, chart, kv

Recipes with a non-empty servers field only apply to the listed servers. Universal recipes (servers: []) are always available.

import { filterRecipesByServer, WEBMCP_RECIPES } from '@webmcp-auto-ui/agent';
const recipes = filterRecipesByServer(WEBMCP_RECIPES, ['tricoteuses']);
// -> universal recipes + those targeting "tricoteuses"

To add a recipe to your project:

import { parseRecipe, registerRecipes } from '@webmcp-auto-ui/agent';
const myRecipe = parseRecipe(`---
id: custom-weather-dashboard
name: Custom weather dashboard
components_used: [stat-card, chart-rich, map]
when: data contains temperature, humidity, wind
servers: []
---
## When to use
Weather data with geographic coordinates.
## How
1. Display temperature, humidity, wind as stat-cards
2. Line chart for 7-day forecast
3. Map with markers for each station
`);
registerRecipes([myRecipe]);
import {
WEBMCP_RECIPES, // 10 built-in recipes, auto-registered
parseRecipe, // parse a .md file -> Recipe
parseRecipes, // parse a batch of .md files -> Recipe[]
recipeRegistry, // singleton registry (read-only)
registerRecipes, // add recipes to the registry
filterRecipesByServer, // filter by connected server
formatRecipesForPrompt, // format for prompt injection
formatMcpRecipesForPrompt, // format MCP server recipes
} from '@webmcp-auto-ui/agent';

MCP recipes come from the remote server and describe how to use its tools. They answer the question “how to get the data”:

MCP Recipe "profil-depute":
1. Call search_deputes(nom: "Dupont")
2. Take the first result, extract the ID
3. Call get_votes(depute_id: ID)
4. Call get_mandats(depute_id: ID)
5. Combine the results
interface McpRecipe {
name: string;
description?: string;
}
// 1. The client collects recipes on connection
const recipesResult = await client.callTool('list_recipes', {});
const mcpRecipes: McpRecipe[] = JSON.parse(recipesResult.content[0].text);
// -> [{ name: 'profil-depute', description: 'Full profile with votes and mandates' }]
// 2. They are added to the MCP layer
const mcpLayer: McpLayer = {
protocol: 'mcp',
serverUrl: 'https://mcp.code4code.eu/mcp',
serverName: 'Tricoteuses',
tools: await client.listTools(),
recipes: mcpRecipes, // <- here
};

The LLM sees summaries in the prompt. When it needs full instructions, it calls get_recipe (an MCP tool exposed by the server):

sequenceDiagram
participant LLM as Agent LLM
participant MCP as MCP Server
Note over LLM: The prompt contains:<br/>"profil-depute: Full profile with votes and mandates"
LLM->>MCP: get_recipe({name: "profil-depute"})
MCP-->>LLM: {body: "1. Call search_deputes(...)\n2. Call get_votes(...)"}
Note over LLM: The agent now follows<br/>the instructions step by step

In addition to WebMCP recipes (.md files) and MCP recipes (server), there are widget recipes defined inline in the autoui server. These are the most granular recipes: each native widget has its own recipe documenting its schema and usage.

search_recipes("stat")
-> { name: "stat", description: "Key statistic (KPI, counter, total)" }
get_recipe("stat")
-> { schema: { type: "object", required: ["label", "value"], ... },
recipe: "## When to use\nTo display a single key figure..." }

The LLM discovers available widgets via search_recipes and gets the exact schema via get_recipe.

WebMCP Recipe (UI)MCP Recipe (server)Widget Recipe (autoui)
Source.md files in the agent packageRemote MCP server (list_recipes)autoui server (inline)
ScopeMulti-widget compositionMulti-tool workflowSingle widget
Guides whatPresentation (View)Data retrieval (Model)Widget schema
TypeScript typeRecipeMcpRecipeWidgetEntry
Carried byWebMcpLayerMcpLayer.recipesWebMcpLayer (autoui)
Lazy loadingget_recipe("id")get_recipe(name) (MCP)get_recipe("widget-name")
Body in promptNo (summaries)No (summaries)No (summaries)
graph TD
subgraph "Level 1: MCP Recipe"
MR["profil-depute<br/>1. search_deputes()<br/>2. get_votes()<br/>3. get_mandats()"]
end
subgraph "Level 2: WebMCP Recipe"
WR["parlementaire-profile<br/>Use profile + hemicycle + timeline"]
end
subgraph "Level 3: Widget Recipes"
R1["profile<br/>schema: {name, fields, stats}"]
R2["hemicycle<br/>schema: {groups, totalSeats}"]
R3["timeline<br/>schema: {events}"]
end
MR -->|"what to request"| DATA["Raw data"]
DATA -->|"how to display"| WR
WR -->|"which widget"| R1
WR --> R2
WR --> R3
  • ToolLayers: WebMCP recipes are carried by the WebMcpLayer, MCP recipes by McpLayer instances
  • Widgets: WebMCP recipes reference widgets by name (stat-card, profile…)
  • widget_display: the tool the LLM calls after reading the recipe
  • MCP: MCP recipes guide usage of the server’s DATA tools

The cross-server recipe is designed for agents connected to multiple MCP servers:

---
id: cross-server
name: Cross-server correlation
when: user compares data from different servers
servers: []
---
1. Identify relevant servers
2. Query each server separately
3. Cross-reference results by common key (region, date, etc.)
4. Present as comparative table or chart overlay

The recipe-browser widget lets users explore available recipes through a visual interface. Clicking a recipe loads its detail and the agent can then execute it.

The recipe registry (recipeRegistry) is a read-only singleton. To add recipes at runtime:

import { registerRecipes, parseRecipes, recipeRegistry } from '@webmcp-auto-ui/agent';
// Load recipes from a file
const newRecipes = parseRecipes([myMarkdown1, myMarkdown2]);
registerRecipes(newRecipes);
// The registry now contains built-in + new recipes
console.log(recipeRegistry.size); // 12 (10 built-in + 2 new)
graph LR
Q["User question"] --> S["search_recipes()<br/>Find the right recipe"]
S --> G["get_recipe()<br/>Read the instructions"]
G --> E["MCP tools<br/>Get the data"]
E --> W["widget_display()<br/>Display with widgets"]
W --> D["Final dashboard"]
style S fill:#e3f2fd
style G fill:#e8f5e9
style E fill:#fff3e0
style W fill:#fce4ec