Skip to content

Velloo documentation

Markdown for LLMs ↗

MCP: tree mutations

compose, update_props, move_node, remove_node, set_node_id, inspect, and update_viewport_presets, plus the diagnostics every tree write returns.

A screen has one tree. compose builds it from restricted JSX; the other operations on this page target nodes within it via locators: @id references or index paths. Except for compose, they also work on snippet bodies via the virtual screenId form snippet:<id> (see Editing a snippet body).

Successful writes can carry advisory propWarnings and diagnostics. For a guided workflow, see Design a screen.

compose

Append one subtree to a screen, or replace the screen’s whole tree, from restricted JSX. Tags resolve against the screen’s library, the folder’s extensions, and snippet names, so a whole feature card, or a whole page, lands in one call. No JavaScript executes.

ArgTypeRequiredDescription
screenIdstringyesA screen id. snippet:<id> is not accepted
modestringyes"append" inserts the JSX root under parentPath; "replace" makes it the screen’s entire tree
jsxstringyesRestricted JSX with a single root element
parentPathlocatornoAppend only. Parent to insert under; default [], the screen root. Must be a component node
indexnumbernoAppend only. Insertion index in the parent’s children; default: after the last child
{
  "screenId": "landing",
  "mode": "append",
  "parentPath": "@hero",
  "jsx": "<Box vellooId=\"hero-actions\" className=\"mt-6 flex gap-3\"><Button vellooId=\"hero-cta\">Get started</Button><Button variant=\"outline\">Read the docs</Button></Box>"
}

Passing parentPath or index with mode: "replace" returns BadRequest. Rebuild a scanned placeholder screen with mode: "replace".

Restricted JSX

The compiler accepts a static subset of JSX and turns it into design nodes:

  • One root. The source must contain exactly one root element. A fragment (<>…</>) is allowed only when it wraps exactly one element.
  • Tags are component ids from list_components: Button, Card, CardHeader, an extension id, or a snippet’s PascalCase tag. A dotted tag is a tag too, but only where the catalog has that id — a repository component’s compound part (<Tabs.List>) or a qualified name (<Mantine.Button>). Library ids stay flat, so shadcn’s is <CardHeader>, not <Card.Header>. An unknown tag is rejected with close-match suggestions; a tag that is both a component and a snippet is rejected as ambiguous.
  • Attribute values are quoted strings (className="p-6", single or double quotes), JSON literals in braces (size={24}, disabled={false}, items={["A", "B"]}, sx={{"p": 2}}), or an element (leftSection={<Icon name="Plus" />}), stored as a node-valued prop and rendered as the real element. A bare attribute (disabled) means true. Identifiers, calls, template strings, functions, spread attributes, and child expressions such as {title} are rejected — passing a bare component type (icon={Plus}) is rejected with a message telling you to pass an element instead.
  • No behavior. Event handlers (onClick and any other on* attribute), ref, key, and dangerouslySetInnerHTML are rejected. Duplicate attributes are rejected.
  • Text children become the component’s children prop, with whitespace collapsed: <Button>Save</Button>. Setting children both as text and as an attribute is an error. When text sits beside elements (<Button><Icon name="Plus" />Add</Button>), each text run is wrapped in a Text node automatically.
  • vellooId="hero-cta" sets the node’s stable $id, addressable afterwards as "@hero-cta". It must be a string, start with a letter, and be unique on the screen. Assign one to anything you may touch again.

Snippets in JSX

A snippet is placed by its PascalCase tag, derived from the snippet’s id or name, as list_components reports it (pricing-tier becomes <PricingTier />):

  • Each attribute is an argument and must name a declared param; an undeclared name is rejected.
  • className, unless the snippet declares a className param, becomes the instance’s extra class suffix, appended to the body root.
  • Text children, or exactly one element child for a node-typed param, fill a param named children; a snippet without a children param rejects children.
  • Every required param (no default and not optional) must be present.
  • vellooId sets the instance’s $id.
{
  "screenId": "pricing",
  "mode": "append",
  "parentPath": "@tiers",
  "jsx": "<PricingTier vellooId=\"tier-pro\" name=\"Pro\" price={19} featured className=\"md:scale-105\" />"
}

Per-instance interior overrides are not set at placement; apply them afterwards with update_snippet_instance.

Result and errors

An append returns { mode, path, root }, where path is the new node’s index path; a replace returns { mode, screenId, replacedRootRef, root }. root is { kind: "component" | "snippet", id }. Either can carry propWarnings and diagnostics.

A JSX problem returns BadRequest with issues: [{ message, offset, line, column }] pointing into the source. A host-app package that isn’t installed never blocks design; emit_code reports it later in componentsToInstall. Other errors: ScreenNotFound, InvalidPath, IdNotFound, IdConflict.

update_props

Patch props and native styling on one or more nodes in a single atomic write: one lock, one broadcast, one undo step.

ArgTypeRequiredDescription
screenIdstringyesScreen id, or snippet:<id> for a snippet body
patchesarrayyesOne entry per node: { path, propPatch?, style? }. Length 1 for a single edit. Each entry needs propPatch, style, or both
gesturestringnoOpaque id (max 64 characters) of a continuous drag; writes sharing it collapse into one undo step

Within an entry:

  • propPatch shallow-merges into the node’s props. null removes a key; className is a prop like any other.

  • style restyles through the screen’s framework-native style channel, applied after propPatch:

    • shadcn and Tailwind no-framework folders: a Tailwind className string, which replaces the node’s className (an empty string removes it)
    • MUI: an sx object
    • no-framework, inline-style channel: a plain style object, themed via var(--...) tokens

    Object channels merge shallowly and an inner null drops that key; style: null clears the channel’s prop entirely. A payload whose shape doesn’t fit the channel returns BadRequest naming the expected shape; this is how an agent discovers the active channel. All style payloads are validated before anything is written.

{
  "screenId": "landing",
  "patches": [
    { "path": "@hero-cta", "propPatch": { "children": "Start free", "size": "lg" } },
    { "path": "@hero", "style": "flex flex-col items-center gap-6 py-24" }
  ]
}

The top-level arguments are strict: a top-level path, propPatch, style, or props is rejected; wrap the edit in patches. A single entry object sent bare as patches is lifted into a one-element array.

Returns { paths }, the resolved index path of each entry in order. update_props patches component nodes only; targeting a snippet instance returns InvalidPath pointing at update_snippet_instance.

Patching inside a snippet instance

To change one node inside a single instance’s body, or an instance’s arguments or extra classes, use update_snippet_instance. To change a body node across every instance, use update_snippet with innerPatch.

OperationScope
update_propsA component node on a screen, or in a snippet body via snippet:<id>
update_snippet_instanceOne instance: its args, extra classes, or one node inside its body
update_snippet with innerPatchThe shared definition: every instance at once

move_node

Move a node to a new parent.

ArgTypeRequiredDescription
screenIdstringyesScreen id, or snippet:<id>
fromPathlocatoryesNode to move
toParentlocatoryesDestination parent
toIndexnumbernoInsertion index in the destination’s children; default: last

Returns { newPath }. Errors: InvalidMove when moving the root, or moving a node into itself or one of its descendants; InvalidPath for a missing node or parent.

remove_node

Remove the node at a locator.

ArgTypeRequiredDescription
screenIdstringyesScreen id, or snippet:<id>
pathlocatoryes

Returns { removedRef } naming what was removed. The root itself is never removed: passing the root path [] clears all of the root’s children and leaves the root in place. To rebuild a whole screen, prefer compose with mode: "replace".

set_node_id

Set, rename, or clear a node’s stable $id anchor. Once set, the node is addressable as "@<id>" in any locator.

ArgTypeRequiredDescription
screenIdstringyesScreen id, or snippet:<id>
pathlocatoryes
idstring or nullyesA leading letter, then letters, digits, _, or -; null clears

Returns { path, id }. Per-screen uniqueness is enforced; collisions return IdConflict. Targets component nodes and snippet instances; param refs can’t carry ids.

inspect

Return the server-rendered HTML, resolved className list, $ref, and resolved props for a node; use it instead of guessing rendered output.

ArgTypeRequiredDescription
screenIdstringyes
pathlocatoryes
innerPathstringnoFor a snippet instance: "@id", a dotted index path, or "" for the body root (the default). Ignored for plain components
computedbooleannoAlso render the screen in a real browser and measure the node. Default false; it costs a render
viewportobjectnoRender size for computed, { w, h } ({ width, height } is accepted). Default: the folder’s Desktop preset
modestringnoRender variant for computed: "light", "dark", or "compare"
themestringnoNamed theme for computed. Default: the theme the screen’s board pins

Returns { ref, resolvedProps, classes, bodyHtml, note? }. The HTML comes from Velloo’s bundled library; where the screen mounts the app’s own components (see component_status), the canvas and captures show those instead, so measure them with computed: true.

When path resolves to a snippet instance, the body is rendered with its args, $overrides, and $extraClassName applied; innerPath drills into one body node. Omitting it on an instance inspects the body root and returns a note on how to drill in.

With computed: true the result adds computed: { viewport, path, tag, class?, rect: { x, y, w, h }, style, children } (the node’s rendered box and resolved computed styles, plus its direct children’s boxes) and any mount diagnostics. computed is null with a computedNote when the node renders nothing measurable, or when innerPath points inside a snippet body (body nodes aren’t separately addressable in the render; measure the instance or use render_snippet).

update_viewport_presets

Replace the folder’s viewport presets: the sizes offered when adding a frame, and the source of the default screenshot viewport (the preset whose name contains “desktop”, else the first). Send the complete list in display order; names must be distinct and at least one is required. Frames store their own w/h, so this never resizes an existing frame.

ArgTypeRequiredDescription
presetsarrayyesThe complete list in display order: [{ name, w, h }], with positive integer w/h
{
  "presets": [
    { "name": "Desktop", "w": 1440, "h": 900 },
    { "name": "Mobile", "w": 390, "h": 844 }
  ]
}

Returns { presets }. At least one preset is required and names must be distinct (case-insensitive); violations return BadRequest. Frames store their own w/h, so this never resizes an existing frame.

Diagnostics on results

There is no separate audit or class-validation operation. Design checks run automatically and arrive as a diagnostics array on the result of the call that needs them, omitted entirely when there is nothing to fix. Each entry is { severity, code, path, message, suggestion? }, with path naming the affected node:

CodeSeverityMeaning
tailwind/invalid-classwarningA class does not compile under the Tailwind JIT, which knows the folder’s theme tokens and custom CSS
tailwind/undefined-varwarningA class compiles but references a CSS variable the design system never declares, so it paints a fallback
tailwind/v3warningThe host app is on Tailwind v3 and the class has v4 semantics; suggestion carries the v3 spelling when there is one
theme/raw-colorwarningColor classes that won’t theme-flip in dark mode, with token suggestions. Listed for the first eight nodes, then summarized
render/component-threwerrorThe component threw while rendering and the canvas shows a placeholder; the message usually names the parent it must sit inside
render/server-fallbackwarningOn a screenshot, compare_to_url, or computed inspect: the capture renders Velloo’s bundled components rather than the app’s own, so don’t tune the design against it

compose, update_props, update_snippet_instance, add_screen, add_snippet, update_snippet, and batch check what they wrote; screenshot and emit_code repeat the check over the complete screen, and emit_snippet over the snippet body. The class and color checks apply to Tailwind style channels; sx and inline-style folders get render diagnostics only.

Diagnostics never fail or roll back a write; treat them as a triage list, not a gate. A node that deliberately doesn’t flip (text over a photo scrim, a brand accent) is exempted from theme/raw-color with a truthy data-accent prop, such as data-accent="ok"; the exemption does not cascade to children.