Layout and rendering
render-view is the tool the LLM calls to build a UI. It runs a pipeline to resolve keys, then returns a payload the widget bundle can render alongside an MCP UI resource.
Input schema
{
"keys": { "<key>": <value> }, // initial context keys
"steps": [ // optional pipeline
{ "id": "<ctxKey>", "step": "<step-id>", "optional": false }
],
"layout": <layout>, // see below
"title": "<optional string>"
}A layout cell carries widget, optional span (1–12), and optional per-instance props forwarded into the widget's WidgetProps.widgetProps. Validate accepted prop shapes on the widget via WidgetDefinition.propsSchema so the LLM constructing a layout has the contract upfront.
// layout cell
{
"widget": "analytics:kpi-grid",
"span": 6,
"props": { "processDefinitionKey": "miraveloLeasing" },
}See packages/core/src/tools/register-framework-tools.ts for the exact Zod schema and packages/core/src/framework/layout-schemas.ts for the cell definition.
Layout shapes
Three accepted forms:
Flat rows
[{ "row": [{ "widget": "articles:article-card", "span": 6 }] }]Explicit wrapper
{
"rows": [{ "row": [{ "widget": "orders:table", "span": 6 }] }],
}Tabs
{
"tabs": [
{ "label": "Article", "rows": [{ "row": [{ "widget": "articles:article-card" }] }] },
{ "label": "Orders", "rows": [{ "row": [{ "widget": "orders:table" }] }] },
],
}span is 1–12 (12-column grid). Missing → widget's declared size decides.
Schema source: packages/core/src/framework/layout-schemas.ts.
Output payload
{
content: [{ type: "text", text: "<summary>" }],
structuredContent: {
_refreshParams: { keys, steps, layout, title },
title,
context: {
keys: Record<string, unknown>,
stepIds: string[],
stepData: Record<stepId, { data, keys, _app, _dataType }>,
errors: { stepId, reason }[],
},
layout: LayoutConfig,
},
}The widget bundle reads context.keys to feed each widget's WidgetProps.keys. stepData is available through WidgetProps.context for widgets that need the raw step result.
Refresh
The framework registers a second tool (refresh-view by default — override via app.refreshToolName) that accepts the same schema. The UI stores _refreshParams from the initial render-view response and calls refresh-view to re-run the pipeline without the LLM in the loop.
Running a layout end-to-end
YAML input:
# articles-layout.yaml
rows:
- row:
- widget: articles:article-card
span: 6Tool call:
curl -sX POST http://localhost:3010/mcp \
-H 'content-type: application/json' \
-d @- <<'JSON' | jq
{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{
"name":"render-view",
"arguments":{
"keys":{"articles:articleId":"1"},
"steps":[{"id":"article","step":"articles:resolve-article"}],
"layout":{"rows":[{"row":[{"widget":"articles:article-card","span":6}]}]}
}
}
}
JSONSee examples/layouts/ for runnable shapes — articles-layout.yaml (single-widget row) and orders-dashboard.yaml (multi-widget dashboard).