Skip to content
MCP: lifecycle

Documentation

Velloo documentation

Markdown for LLMs ↗

MCP: lifecycle

Create, update, and remove screens, boards, frames, flow arrows, board notes, annotations, and extensions.

Lifecycle operations manage the resources a design folder is made of: screens, boards, frames, flow arrows, notes, annotations, and extensions. On the default guided surface, invoke an operation through call_velloo after checking its operation_schema; on the full surface the native names below are advertised directly. Every operation’s arguments are a strict object on either surface: an argument name the schema doesn’t declare is rejected with the valid keys rather than silently dropped.

Screens

add_screen

Create a new screen. Empty by default. Does not place the screen on a board; that’s a separate, intentional add_frame step.

Arg Type Required Description
name string yes
id string no Omit to auto-suffix a unique id derived from the name
fromScreenId string no Clone an existing screen’s tree
tree node no Supply a tree explicitly

Returns { screenId, screen }, where screen carries the metadata and a nodeCount instead of the tree; read the tree with get_screen when you need it.

A route-scan init already scaffolds one placeholder screen per detected route (id = route slug). Don’t add_screen for those; it returns ScreenIdConflict with a hint. Rebuild the existing screen instead with compose mode "replace".

add_screen is not the only way to make a screen: a compose in mode "replace" on a screenId the folder doesn’t have creates the screen and places it on a board in one call.

update_screen

Sparse metadata patch of the screen’s name and the app route it stands for; the screen id stays stable. The fields can also be sent beside the screen, { screenId, name }, and are read as the patch.

Arg Type Required Description
screenId string yes
patch object yes { name?, route? }. route is the app path, such as "/settings"; null clears it.

Returns { screen } with the screen’s metadata and a nodeCount, not its tree.

remove_screen

Delete a screen from disk. Refuses to remove the last screen (LastScreen). Any frames referencing the screen on any board are removed alongside it; the result is { removedScreenId, removedFrames: [{ boardId, frameIds[] }] }. Undoable from the canvas.

Arg Type Required Description
screenId string yes

Boards

Boards are the canvases of a design folder; one folder has many. Each board owns its own frames and persists as boards/<id>.json. A folder with zero boards is a supported state.

add_board

Create a new empty board. Returns { boardId, board }.

Arg Type Required Description
name string yes Max 80 characters
id string no Derived from name if omitted (auto-suffixed on collision); an explicit id that already exists returns BoardIdConflict
group string no Sidebar group to file the board under: an existing group’s id or name (matched case-insensitively), or a new name, which creates the group

update_board

Sparse patch on a board’s metadata. Returns { board }.

Arg Type Required Description
boardId string yes
patch object yes { name?, theme?, archived?, group? }, see below

Patch fields:

  • name: max 80 characters.
  • theme names a theme (the stem of theme/<name>.json) the board’s frames render with, the per-board look; null clears back to the folder default.
  • archived, when true, files the board away: hidden from the sidebar, from list_boards (unless includeArchived), and from a default publish, but kept and still editable. false unarchives it.
  • group moves the board to a sidebar group by existing id or name, or a new name (which creates one); null files it back under Ungrouped.
{ "boardId": "explorations", "patch": { "archived": true } }

remove_board

Delete a board and its notes. There is no last-board rule; removing the only board is allowed.

The delete cascades. Screens that no other board places are deleted with the board; screens also framed on another board are kept. Snippets that only those deleted screens reached are deleted too; a snippet that was already unreferenced before the call is left alone. The result reports all of it: { removedBoardId, removedScreenIds, removedSnippetIds }.

This is permanent. To put a board away reversibly, use update_board with patch: { archived: true } instead.

Arg Type Required Description
boardId string yes

reorder_boards

Set the left-sidebar display order of boards. Persists to config.boardOrder; the canvas drag-and-drop calls this too. Returns the persisted { order }.

Arg Type Required Description
order string[] yes Board ids in the desired order. Unknown ids are ignored; omitted boards keep their current slots

Frames

Frames are placements of screens on a chosen board. Multiple frames of the same screen always render the same tree at different sizes; that is the sync model. Different layouts per breakpoint are separate screens.

A frame’s group names a frame region on its board: an existing group’s id or name (matched case-insensitively), or a new name, which creates the group. Frames naming one new group in one write share it. Agents set or clear a frame’s group through add_frame and update_frame; an empty or over-long name is refused before anything is written. (This is unrelated to a board’s sidebar group, set via add_board / update_board.)

add_frame

Place a screen on a specific board at a chosen size and position. Returns { frame }.

Arg Type Required Description
boardId string yes
screenId string yes
x number no Used only when y is also given; otherwise the frame is placed 80px right of the rightmost existing frame, at y: 0
y number no Paired with x
w integer no Width in px; defaults to the folder’s Desktop viewport (else its first preset)
h integer no Height in px. Omitted, the frame is sized to the screen and follows it
label string no
group string no Frame region on the same board: an existing group’s id or name, or a new name, which creates it
id string no Defaults to <screenId>-<n>; an existing id returns FrameIdConflict

Frames that follow their screen

A frame added without h carries fit: "content": its height follows the screen. Velloo measures the screen shortly after it changes, whoever changed it, an agent or a person on the canvas, and resizes the frames that follow it. A burst of edits is one measurement. add_frame waits for the first measurement, so the frame it returns has the fitted height. A fitted frame is never shorter than the viewport preset of the same width, and measuring needs the screenshot browser (velloo browser install).

Setting h to a new value with update_frame ends it, because a height someone chose stays theirs. Moving a frame does not. fit: "content" in a patch starts it again, and fit: null clears it. On the canvas this is Fit height to content in the frame’s menu, shown as a badge beside the height.

screenshot and compare_to_url stay reads. They never resize a frame, and framesShorterThanContent leaves out the frames that follow their screen.

update_frame

Move, resize, relabel, regroup, or pin the color scheme of frames on one board. One entry per frame in patches, applied in one atomic write (single persist, broadcast, and undo entry); length 1 for a single frame. Returns { frames }, the board’s full frame list. An unknown frame id fails the whole call with FrameNotFound.

Arg Type Required Description
boardId string no Omit when exactly one board holds these frames
patches array yes [{ frameId, patch }], at least one entry

Each patch is { x?, y?, w?, h?, label?, group?, scheme?, fit? }. label, group, scheme, and fit accept null to clear; an omitted field is unchanged. scheme is "light" or "dark" and pins that frame’s render mode, a review affordance over the screen’s one shared tree, not a separate design variant. screenshot follows the pin when you omit mode. fit is "content": the frame’s height follows its screen.

{
  "boardId": "main",
  "patches": [
    { "frameId": "pricing-1", "patch": { "h": 2400 } },
    { "frameId": "pricing-2", "patch": { "scheme": "dark", "label": "Pricing · dark" } }
  ]
}

remove_frame

Remove a frame placement from a board. The underlying screen is untouched. Arrows that touched the frame go with it.

Arg Type Required Description
boardId string yes
frameId string yes

On the guided façade, remove_frame { frameId } without a boardId is read on the one board that has a frame of that id.

Flow arrows

Arrows between a board’s frames make the board a flow. Read one back with get_board, whose flow field groups the frames into steps.

connect_frames

Draw, edit, and erase arrows in one write: one persist, one broadcast, one undo entry.

Arg Type Required Description
boardId string no Omit when exactly one board holds these frames
edges array yes [{ from, to, label?, state?, remove? }], at least one entry

Each entry:

Field Type Description
from string Frame id the arrow leaves
to string Frame id the arrow points at
label string or null The trigger or condition in plain words (“Submit”, “Email already taken”), up to 120 characters. null clears
state string or null Makes to a state of from’s step: loading, empty, error, success, or any short name up to 32 characters, stored lower-case. Omit it for a transition to another step. null clears
remove boolean Erase the arrow between this pair

A board holds one arrow per ordered pair of frames, so from and to name it. An entry for a pair that already has an arrow edits it, and an omitted field is unchanged. Entries apply in order and the result is checked as a whole, so one call can erase one arrow and draw another.

{
  "edges": [
    { "from": "signup-1", "to": "signup-sending-1", "state": "loading", "label": "Submit" },
    { "from": "signup-sending-1", "to": "signup-taken-1", "state": "error", "label": "Email already taken" },
    { "from": "signup-sending-1", "to": "verify-1", "label": "Account created" },
    { "from": "signup-taken-1", "to": "signup-1", "label": "Try again" }
  ]
}

Returns { edges, readAsTransitions?, gaps?, unconnected? }. edges is every arrow on the board after the write. gaps and unconnected are what the drawing still leaves open, in the same shape get_board’s flow reports them.

A few guesses are read as meant:

  • A state that can only be a transition. A way back to the step’s default frame or to the state it came from, a state its target already has, or default is kept as the transition it is and listed in readAsTransitions: [{ from, to, state }].
  • An end that names a screen. from or to may be a screen id where exactly one frame shows that screen.
  • One arrow sent flat. On the guided façade, { from, to, label?, state? } with no edges around it is one arrow.

A real contradiction is refused with BadRequest and nothing is written: a second state for a frame that is already one, a loop of states, a frame pointing at itself, or remove for a pair with no arrow. An end that is not a frame on the board returns FrameNotFound.

Arrows are a spec for review and implementation. Nothing runs on the canvas, and emit_code emits nothing for them.

Canvas notes

Board-level markdown notes. A note is either free-floating in board coordinates (the same space as frame x/y) or attached to a node inside a frame, in which case the canvas shows it as a marker on that node that opens the note on hover. A free note fits its text unless someone resized it on the canvas, which records an optional height; the text then scrolls inside it. Use them for guidance that belongs next to frames: tour steps, review remarks, handoff context. list_notes is on the discovery page.

add_note

Returns { note }.

Arg Type Required Description
boardId string no Default: the first board
x number no Board coordinates; place beside frames, not on them
y number no
width number no Pixel width; default 240
body string yes Markdown-lite
attachment object no { frameId, screenId, locator }: anchor the note to a node instead of free board space

A note that names no place goes to the left of the board’s frames, below any note already standing there. A design with no board yet has nowhere to put one, and the call returns BadRequest saying so. For an attachment, frameId must be a frame on this board, that frame must show screenId (otherwise InvalidPath), and locator (@id or a path array) must resolve in that screen.

{
  "boardId": "main",
  "body": "Swap this for the real pricing table before handoff.",
  "attachment": { "frameId": "pricing-1", "screenId": "pricing", "locator": "@plans" }
}

update_note

Returns { note }; an unknown noteId returns CanvasNoteNotFound.

Arg Type Required Description
boardId string yes
noteId string yes
patch object yes { x?, y?, width?, body? }

remove_note

Returns { removedId }.

Arg Type Required Description
boardId string yes
noteId string yes

Annotations

Annotations are markdown pinned to a specific node; the canvas draws a connector to that node in every frame showing it. Authorship matters: designer-authored annotations are the designer’s channel to the agent: read them via list_annotations, act on them, never edit or delete them. Agent-authored annotations (questions for the designer, review remarks) are created, edited, and removed with the three operations below.

add_annotation

Pin agent-authored markdown to a node. Created with author: "agent" and position: "auto". Returns { annotation }. A node holds one annotation; pinning a second to the same node returns AnnotationConflict with the existingId.

Arg Type Required Description
screenId string yes
path locator yes @id or path array of the target node
body string yes Markdown
collapsed boolean no

update_annotation

Edit one of your own annotations. Pass body, collapsed, or both; neither is a BadRequest. Refuses user-authored annotations; answer those with your own annotation instead. Returns { annotation }.

Arg Type Required Description
screenId string yes
annotationId string yes
body string no Replacement markdown
collapsed boolean or null no null clears the override

remove_annotation

Remove an agent-authored annotation. Refuses user-authored ones. Returns { removedId }; an unknown id returns AnnotationNotFound.

Arg Type Required Description
screenId string yes
annotationId string yes

Extensions

Extensions declare user-owned custom components (a bespoke DataTable, a brand Hero, a real chart) that aren’t part of any library’s registry. The canvas renders them as labelled placeholders (or client-mounts the real component with render: "live"); emit_code emits a real import from importPath. Concepts: Extensions.

Each prop entry is { name, type, optional, control, defaultValue?, enumValues? } where optional is a boolean, defaultValue a string, and control one of boolean, number, string, color, enum, icon. control: "enum" requires a non-empty enumValues. A malformed entry returns InvalidExtensionProp naming the prop.

add_extension

Register a custom component so it can appear in screen trees. Returns { id, extension }, plus shadowedLibraryComponent when the id matches a library component and liveResolveWarning when a render: "live" import path can’t be resolved from the host app (the canvas shows the placeholder until it’s fixed). An id that is already registered returns ExtensionIdConflict.

Arg Type Required Description
id string yes Component id; shadows a library component with the same id
importPath string yes Emitted verbatim by codegen; use the alias your app actually uses (@/components/data-table, @acme/charts)
props array yes Prop schema (shape above)
category string no "ui" (default) or "typography"
description string no
render string no "static" (default, placeholder card) or "live": bundle the real component from the host app and client-mount it in the canvas; falls back to the placeholder on bundle/render failure
app string no Monorepos only: which host app the component lives in, a key of config.hostApps (e.g. "web", "admin"). The live island bundles from that app’s root and node_modules. Omit for the default host app; an unknown key is a BadRequest listing the known ones
fit string no Live-island sizing: "aspect-video" (default, locks a 16:9 box) or "content" (a fixed-height chart or an absolute-inset overlay drives its own height)
{
  "id": "RevenueChart",
  "importPath": "@/components/revenue-chart",
  "props": [
    { "name": "range", "type": "\"7d\" | \"30d\"", "optional": true, "control": "enum", "enumValues": ["7d", "30d"] }
  ],
  "render": "live",
  "fit": "content"
}

Don’t use extensions for compositions of existing components (use snippets) or for variations of an existing component (use props or class overrides).

update_extension

Patch an existing extension: add or remove props, change the importPath after a refactor, switch render or fit, move it to another host app. Sparse: unlisted fields keep their values. To rename, remove_extension then add_extension; renaming would break tree references. Returns { id, extension } (plus liveResolveWarning when applicable); an unknown id returns ExtensionNotFound.

Arg Type Required Description
id string yes
patch object yes { importPath?, props?, category?, description?, render?, app?, fit? }, same value rules as add_extension; props replaces the whole prop list

remove_extension

Remove an extension. Refuses with ExtensionInUse while any screen or snippet tree still references it; the error carries the offending references: [{ screenId, path }] so the agent can remove or replace them first. Returns { id }.

Arg Type Required Description
id string yes

Folder format upgrade

upgrade_design_folder

Not part of the normal catalogue. When velloo mcp (stdio) starts against a design folder whose on-disk format is older than the installed velloo, the design tools can’t load. Instead of exiting, the server serves a minimal session: its instructions explain the mismatch, and upgrade_design_folder is the only tool. It takes no arguments and runs the same in-place migration as velloo upgrade <design>, returning the applied steps and rewritten files. Reconnect the MCP server afterwards to load the design tools.

A folder written by a newer velloo can’t be migrated backwards, so that session carries only the notice: update the velloo CLI, then reconnect.