Skip to content
MCP: tree mutations

Documentation

Velloo documentation

Markdown for LLMs ↗

MCP: tree mutations

compose (the JSX an app holds, read as data), 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 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

Write JSX onto a screen: append one or more subtrees, replace the screen’s whole tree, or replace one node. Tags resolve against the screen’s library, the folder’s extensions, snippet names, and the app’s own components, so a whole feature card, or a whole page, lands in one call. The JSX is read as data. No JavaScript executes.

Arg Type Required Description
screenId string yes A screen id. snippet:<id> is not accepted
mode string yes "append" inserts JSX roots as siblings under parentPath; "replace" makes it the screen’s entire tree, or with path replaces one node
jsx string one of the two The JSX itself
file string one of the two An app file to read the JSX from: a .jsx, .tsx, .js, or .ts file inside the app, relative to the app root
parentPath locator no Append only. Parent to insert under; default [], the screen root. Must be a component node
index number no Append only. Insertion index in the parent’s children; default: after the last child
path locator no Replace only. The node to replace; default: the whole tree
{
  "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>"
}

Pass exactly one of jsx and file. Passing parentPath or index with mode: "replace", or path with mode: "append", returns BadRequest. Rebuild a scanned placeholder screen with mode: "replace".

Replacing the whole tree of a screen that doesn’t exist creates it. The screen is added and placed on the default board (else the first, else a new “Screens” board) in a frame that follows its height, and the result reports it as created: { screen, board, frame, note }. A screenId one edit away from an existing screen’s is refused as the typo it probably is, with ScreenNotFound and the suggestion; call add_screen to make it on purpose.

The JSX an app holds

Send the JSX you would write for the app. compose parses it and evaluates it as data, then writes the result out as plain elements:

{
  "screenId": "pricing",
  "mode": "replace",
  "jsx": "const plans = [{ name: 'Starter', price: 0 }, { name: 'Pro', price: 19, featured: true }];\n\nfunction Plan({ name, price, featured }) {\n  return (\n    <Card className={cn('p-6', featured && 'border-primary')}>\n      <Heading level={3}>{name}</Heading>\n      <Text>${price} / month</Text>\n      {featured && <Badge>Most popular</Badge>}\n    </Card>\n  );\n}\n\nexport default function Pricing() {\n  return <Box className=\"grid grid-cols-2 gap-6\">{plans.map((plan) => <Plan key={plan.name} {...plan} />)}</Box>;\n}"
}

It reads:

  • Data above the markup. const declarations, arrays and objects built from literals, template strings, and number and date formatters.
  • Lists and conditions. .map, .filter, .slice and the other pure array, string, and number methods a render uses; &&, ||, ??, and ternaries; spreads.
  • Class joiners. cn(...) and its relatives are joined into one class string.
  • Components defined in the source. A local component is written out where it is used. A design holds elements, not functions; to keep one reusable, make it a snippet.
  • A whole page module. Imports, directives, and types are stepped over, an imported component is a tag, and the default export is what gets composed.
  • A tag is the component its import names. Stack imported from "@mantine/core" is Mantine’s Stack, not Velloo’s primitive of the same name, when the app’s catalog has that module and export. A tag the source does not import, or imports from somewhere the catalog does not know, follows the bare-name rule below.
  • The app’s own data files. Data and helpers imported from files inside the app (import { reviews } from "@/lib/reviews", relative or through a tsconfig alias) are read from those files and evaluated the same way. A package’s code is never read, and nothing outside the app root.
  • Hooks with an initial value. useState(0) reads as 0: the design shows the screen’s first state. Mantine’s useDisclosure, useToggle, useInputState, and useCounter are read the same way, at the value their call holds.

What a static design can’t hold is dropped or refused, and never silently:

  • Event handlers and other function props are dropped, and key and ref are ignored.
  • A value nothing holds is refused by name, at its line and column: a fetch, new Date(), an unknown hook, a package’s data.
  • A component type as a prop is kept as one. <AppShell.Section component={ScrollArea}> or <Button component={Link}> stores component as { "$type": { "$ref": "ScrollArea" } }, resolved like a tag (the module the source imports it from first). The canvas passes the component itself, and emit_code writes component={ScrollArea} with its import. A name nothing defines or imports is refused, and the message names the prop it was given to.

sourceNotes in the result says what was done: which components were written out, which props were dropped, which hooks were read at their initial value.

Composing from a file

file reads a page straight from the app, whether the agent wrote it a moment ago or the app already had it:

{ "screenId": "reviews", "mode": "replace", "file": "app/reviews/page.tsx" }

A Next app-router page is composed inside the layout files above it, with the document shell removed, because the page file alone is the page without its header. sourceNotes names the layouts it was composed inside, or one that could not be read. A file that is outside the app, or not one of the four extensions, returns BadRequest.

Tags, attributes, and text

  • Roots. Append accepts a fragment (<>…</>) or bare sibling elements and inserts them in source order. Replace takes one root; a page whose markup is several root elements is placed in a plain <div>, and sourceNotes says so.
  • 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>. A lowercase tag is always the HTML element, whatever a variable of that name holds, and lowers to Box as="…", or Html as="…" on the HTML provider. An unknown component 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), values in braces (size={24}, disabled={false}, items={["A", "B"]}, sx={{"p": 2}}, or any expression the rules above can evaluate), 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.
  • Styling. On Tailwind channels, a style="…" string is treated as className, including on repository components that declare className styling. Object style channels retain their native object shape.
  • 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. Text that sits beside elements (<Button><Icon name="Plus" />Add</Button>) is stored as a text node and renders as bare text, with no wrapper element.
  • 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.

Read, edit, send back

get_screen with mode: "jsx" prints a screen, or the subtree at path, in this same form. Edit the text and return it with mode: "replace" and the same path:

{ "screenId": "home", "mode": "replace", "path": "@pricing", "jsx": "<Box vellooId=\"pricing\" className=\"grid grid-cols-3 gap-6\">…</Box>" }

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

A single-root append returns { mode, path, root }, where path is the new node’s index path; a whole-tree replace returns { mode, screenId, replacedRootRef, root }, and a replace at path returns { mode, screenId, path, root }. root is { kind: "component" | "snippet", id }. Multiple-root append returns added mutation results and roots descriptors. Append can also report into, a description of the parent it landed in. Multiple roots are written in sequence: if a later insertion fails, earlier insertions remain and the error lists them.

Depending on the call, the result also carries:

Field When
created The screen did not exist and was created and placed
framesFitted A whole-tree replace resized the frames that follow this screen: [{ frame, h }]
sourceNotes The source held something worth reporting: components written out, props dropped, hooks read, layouts composed inside
appComponents A bare name was written as the app’s own component (see below)
propWarnings, diagnostics There is something to fix

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, and IdConflict.

A replace is written from what the agent last read. If the screen changed since that agent’s last get_screen (a person edited it on the canvas, the file changed on disk, or another session wrote it), a replace is refused with ScreenChangedSinceRead and nothing is written. The error lists the nodes that differ in changed and names the get_screen call that brings the agent up to date. A replace at path is refused only when the change is inside that node, and an agent’s own writes never count against it. A session that has not read the screen yet has its first replace let through.

A bare name, one the source does not import from a module in the app’s catalog, means Velloo’s own component. When one carries props only the app’s same-named component takes (<Text fw={700}> in a Mantine app) and exactly one app component of that name takes them, compose writes that component and lists what it read under appComponents.read. When more than one could be meant, the write is refused with ShadowedComponent, which names the qualified tag to write (<Mantine.Text>), and nothing is written.

update_props

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

Arg Type Required Description
screenId string yes Screen id, or snippet:<id> for a snippet body
patches array yes One entry per node: { path, propPatch?, style? }. Length 1 for a single edit. Each entry needs propPatch, style, or both
gesture string no Opaque 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 native operation requires patches; a single entry object sent bare as patches is lifted into a one-element array. The guided façade also accepts a single edit such as { "screenId": "landing", "path": "@hero-cta", "props": { "children": "Start free" } }, normalizing props or propPatch and optional style into one patch. Use the patches form on the full surface and for bulk edits.

Returns { paths }, the resolved index path of each entry in order. The result warns when a propPatch replaces a whole object prop (such as style) and names the keys it dropped, and when a propPatch is empty. A slot prop given a markup string (rightSection="<Badge…>") warns that it renders as literal text; pass an element instead. 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.

Operation Scope
update_props A component node on a screen, or in a snippet body via snippet:<id>
update_snippet_instance One instance: its args, extra classes, or one node inside its body
update_snippet with innerPatch The shared definition: every instance at once

move_node

Move a node to a new parent.

Arg Type Required Description
screenId string yes Screen id, or snippet:<id>
fromPath locator yes Node to move
toParent locator yes Destination parent
toIndex number no Insertion 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.

Arg Type Required Description
screenId string yes Screen id, or snippet:<id>
path locator yes

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.

Arg Type Required Description
screenId string yes Screen id, or snippet:<id>
path locator yes
id string or null yes A 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.

Arg Type Required Description
screenId string yes
path locator yes
innerPath string no For a snippet instance: "@id", a dotted index path, or "" for the body root (the default). Ignored for plain components
computed boolean no Also render the screen in a real browser and measure the node. Default false; it costs a render
viewport object no Render size for computed, { w, h } ({ width, height } is accepted). Default: the folder’s Desktop preset
mode string no Render variant for computed: "light", "dark", or "compare"
theme string no Named 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.

Arg Type Required Description
presets array yes The 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:

Code Severity Meaning
tailwind/invalid-class warning A class does not compile under the Tailwind JIT, which knows the folder’s theme tokens and custom CSS
tailwind/undefined-var warning A class compiles but references a CSS variable the design system never declares, so it paints a fallback
tailwind/v3 warning The host app is on Tailwind v3 and the class has v4 semantics; suggestion carries the v3 spelling when there is one
theme/raw-color warning Color classes that won’t theme-flip in dark mode, with token suggestions. Listed for the first eight nodes, then summarized
theme/text-tone warning A Text inside a filled Button or Badge overrides its label color; use plain text or an inline span
screen/opaque warning The whole screen is one app component or extension, so its interior cannot be selected, edited, or varied; compose its constituent parts
render/component-threw error The component threw while rendering and the canvas shows a placeholder; the message usually names the parent it must sit inside
render/server-fallback warning On 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
render/stand-ins warning On compare_to_url: components in a mounted screen drawn by a stand-in rather than the real component
repo/shadowed-by-velloo info On compare_to_url: bare names that render Velloo’s component where the app has its own. A caveat about the score, not its cause

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 and recognize classes and variables from preview-entry stylesheets. Render and opaque-screen checks also apply to other channels.

A screen’s diagnostics are sent once, then only when they change: an unchanged set for the same screen comes back as diagnosticsUnchanged with its count instead of the full list.

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.