MCP server
Vmotif is a Model Context Protocol server. Point an agent at one URL and it can do what you do on the canvas — open a project, add and wire nodes, generate images, code and video, run whole workflows and read the results back — signed in as you, spending your wallet, leaving every result on a canvas you can open.
What agents can do#
24 tools, in five groups. 6 of them can spend the GPU wallet (they start generations or cloud-browser jobs); the rest read or edit the canvas for free.
- Read
- Budget and plan, the live model catalogue with prices, every canvas and workflow the account can open, and the full graph of any of them — nodes, edges, brand assets, the DESIGN.md in force.
- Build
- Create canvases (empty or seeded with a prebuilt workflow), add any node type the toolbar offers, connect and rearrange, patch prompts and settings, rename — all as atomic edits the user sees land live.
- Generate
- Images, code, SVG, full HTML screens and video as headless jobs; free image transforms; and whole saved workflows, with per-output results and costs.
- Drive the browser nodes
- Record a page's animation into frames and CSS context, or crawl a site into a redesign starting point.
The result of a generation is never just a URL in a terminal: every completed job is placed on a canvas, and the tool responses carry the canvas link. An agent working from the command line and a person working in the browser are looking at the same graph.
Install#
One endpoint, Streamable HTTP, OAuth on first use. Add it to whichever client you work in:
https://vmotif.com/api/mcpClaude Code
claude mcp add --transport http vmotif https://vmotif.com/api/mcpThen, inside a Claude Code session, run /mcp and choose vmotif → Authenticate. A browser tab opens the sign-in and consent page; approve it and the tools appear. A step-by-step session is on the Claude Code walkthrough.
Codex
codex mcp add --url https://vmotif.com/api/mcp --oauth-resource https://vmotif.com/api/mcp vmotif
codex mcp login vmotifCodex needs the OAuth resource bound explicitly when the server is added, then a separate login.
Cursor, Windsurf and other mcpServers clients
{
"mcpServers": {
"vmotif": {
"url": "https://vmotif.com/api/mcp"
}
}
}Any client that reads an mcpServers block with a url entry speaks Streamable HTTP and will run the OAuth flow when it first connects. In Cursor this is ~/.cursor/mcp.json or a project’s .cursor/mcp.json.
VS Code
{
"servers": {
"vmotif": {
"type": "http",
"url": "https://vmotif.com/api/mcp"
}
}
}Claude.ai and Claude Desktop
Settings → Connectors → Add custom connector, paste the endpoint URL, and sign in when prompted.
vmotif, open a new session before judging whether the tools are there.Load fewer tools#
Compact mode starts with three tool schemas. The agent retrieves the schemas that its task needs. All 24 actions remain available.
https://vmotif.com/api/mcp?tools=compactKeep the OAuth resource at https://vmotif.com/api/mcp, without the query. Open a fresh client session after a URL change.
Call discover_tools with a task or action name. Each match includes its input schema and invoke_with name. Call that tool with {"tool":"action_name","arguments":{}}.
read_tool accepts read-only actions. execute_tool accepts actions that can change data or spend credits. Existing permissions and confirmation checks still apply.
Authentication#
The server is OAuth 2.1 only, with Vmotif’s own sign-in as the authorization server. The first connection opens a browser tab: you sign in (Microsoft, Google or email) and approve the client on a consent page. The client receives a token bound to this server; the server verifies it on every request and re-checks the client against the list of approved clients each time, so a client removed from that list stops working immediately rather than at token expiry.
- API keys do not work here. A
vmk_live_key is a REST credential for/api/v1; the MCP server rejects it with401. Agents act as a signed-in user, not as a key. - Standard scopes only. Tokens carry
openid,emailandprofile. Permission to generate is not a scope; it is decided per call from your plan, your workspace membership and who owns the canvas. - Two spellings, one server.
vmotif.com/api/mcpandwww.vmotif.com/api/mcpboth serve the endpoint directly and both are accepted as the token audience, so a client configured with either keeps working. Publish the apex form. - Discovery is standard: the protected-resource metadata is at
/.well-known/oauth-protected-resource/api/mcp, and an unauthenticated request returns401with aWWW-Authenticateheader pointing at it.
Budget, workspaces and limits#
Spending tools draw on the same GPU wallet as the canvas, at the same prices, and appear in the same usage history. Pass workspace_id to bill a Team workspace’s pooled wallet instead; the caller must be a member. When an agent omits it and the account has no personal plan, the server falls back to the user’s default workspace, so a Team member’s agent works without configuration.
get_usageis the pre-flight: it reports the remaining budget in micro-dollars (1,000,000 = $1), ornullfor an unlimited pool — which an agent should read as “keep going”, not “empty”.- There is no hourly cap on generations. The wallet bounds spend and your plan’s parallel slots pace load — a burst simply queues. Image transforms carry a generous abuse limit of their own; a limited call returns an error result carrying
retryAfterSeconds. - A generation the model cannot run — wrong ratio for the model, a video duration it does not offer — fails at validation, before anything is charged, with the reason in the result.
Errors#
There are no HTTP status codes at the tool level. A failed call is an ordinary tool result with isError: true, a plain-language message in content, and the same payload in structuredContent. The messages are meant to be shown:
{
"isError": true,
"content": [{ "type": "text", "text": "Workspace AI usage budget reached for the current period." }],
"structuredContent": { "error": "Workspace AI usage budget reached for the current period." }
}Ids you do not own return not found rather than forbidden — nothing about other accounts is disclosed. Unexpected failures collapse to a fixed message such as Failed to generate image. — the detail is logged server-side, not returned.
Patterns that work#
Read before you write
read_canvasfirst. It returns node ids, positions and the DESIGN.md in force;update_canvasneeds the ids, and auto-placement inadd_canvas_nodesstacks below what is there.Let the canvas carry the state
Preferadd_canvas_nodes+run_workflowover baregenerate_*calls when the result should stay useful: the prompt, model and settings are on the canvas for the next person, and re-running is one call.Poll, don't wait
Generation and run tools returnpendingimmediately. Pollget_generation/get_workflow_run; each result appears inresultsas its output settles, so progress can be reported before the run completes.Hand back the link
Every canvas tool returnsurl. Finish by giving it to the user — the canvas is where they will look at the work.
Tool reference#
Every registered tool, its parameters and what it returns. Required parameters are marked; everything else is optional. This list is checked against the server’s registrations in CI, so it cannot fall behind.
Account & catalogue
Read-only. Check the budget, list the models the picker offers.
list_workspaces
FreeThe personal account and every workspace the caller belongs to, with role and whether it can generate or create canvases there. The list an agent reads before asking the user where work should go.
No parameters.
Returns { scopes: [{ workspace_id: "personal" | uuid, kind, name, role?, can_generate, can_create_canvases, reason? }], requires_choice }
get_usage
FreeThe plan tier and remaining GPU budget, personal or for one workspace.
workspace_iduuid- Read a workspace's pooled Team budget instead of the personal one.
Returns { tier, quota } — quota.type is token_budget, unlimited, generation_count or blocked; amounts in micro-dollars.
list_models
FreeThe models the picker offers, with category, provider, capabilities and the price actually billed.
category"image" | "text" | "video"- text includes code and SVG models.
workspace_iduuid- Bill against a Team workspace's pooled wallet instead of the personal one. The caller must be a member (any role but viewer) of an active Team workspace.
Returns { data: Model[] }
Generate
Start a headless job and poll it. Each completed generation is placed on a canvas the user can open.
generate_image
Spends walletStart an image generation.
promptrequiredstring- Up to 50,000 characters.
modelstring- An id from list_models; defaults to the picker's current image default.
images{ url, mediaType? }[]- Up to 10 reference images. https:// or data: URLs. Inline data: URLs share one request of about 4.5 MB, so stage bigger files (up to 10 MB) with create_image_upload.
text_inputs{ content, label?, language? }[]- Up to 10 text or code snippets fed to the model as context.
aspect_ratio"1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3"- Requested ratio; each model supports a subset.
canvas_iduuid | canvas link- Add the finished asset, with its reference images and text inputs, to this canvas — a UUID from list_projects or a canvas link the user pasted. Later generations from the same client follow it there. With an explicit workspace_id in another scope, billing follows workspace_id and the asset still lands here. Omit to continue the client's current generation canvas.
new_canvasboolean- Start a new canvas for this generation and the ones after it. Cannot be combined with canvas_id.
workspace_id"personal" | uuid- Where the generation bills and where its canvas lives: the personal account or a workspace from list_workspaces. Omitted, it follows canvas_id's scope, the only scope that can generate, or the client's scope from the last 30 minutes; otherwise the call returns scope_required with the options so the agent can ask the user.
Returns { id, status: "pending", canvas?: { projectId, url, newCanvas }, scope: { kind, workspaceId?, name? } } — poll with get_generation.
generate_code
Spends walletStart a code, markup, SVG, JSON or full-screen HTML generation.
promptrequiredstring- Up to 50,000 characters.
modelstring- An id from list_models; defaults to the code model.
images{ url, mediaType? }[]- Up to 10 reference images. https:// or data: URLs. Inline data: URLs share one request of about 4.5 MB, so stage bigger files (up to 10 MB) with create_image_upload.
text_inputs{ content, label?, language? }[]- Up to 10 text or code snippets fed to the model as context.
target_languagestring- typescript, css, svg, glsl, json, text… Omit and the server's own heuristics choose.
screen_htmlboolean- Generate one self-contained HTML page instead of a snippet.
canvas_iduuid | canvas link- Add the finished asset, with its reference images and text inputs, to this canvas — a UUID from list_projects or a canvas link the user pasted. Later generations from the same client follow it there. With an explicit workspace_id in another scope, billing follows workspace_id and the asset still lands here. Omit to continue the client's current generation canvas.
new_canvasboolean- Start a new canvas for this generation and the ones after it. Cannot be combined with canvas_id.
workspace_id"personal" | uuid- Where the generation bills and where its canvas lives: the personal account or a workspace from list_workspaces. Omitted, it follows canvas_id's scope, the only scope that can generate, or the client's scope from the last 30 minutes; otherwise the call returns scope_required with the options so the agent can ask the user.
Returns { id, status: "pending", canvas?: { projectId, url, newCanvas }, scope: { kind, workspaceId?, name? } }
generate_video
Spends walletStart a video generation. Duration, resolution and ratio are validated against the model's contract before anything is charged.
promptrequiredstring- Up to 50,000 characters.
modelstring- An id from list_models with category video; defaults to the video default.
images{ url, mediaType? }[]- Reference or first/last-frame images, or the subject image for motion control. Up to 8; supported counts depend on the model.
videos{ url, mediaType? }[]- HTTPS reference videos: the motion source for motion control, or a clip to edit. Array order is input order. Up to 8; a model that takes no video refuses before anything is charged.
text_inputs{ content, label?, language? }[]- Up to 10 text or code snippets fed to the model as context.
aspect_ratio"1:1" … "21:9"- Nine ratios; 21:9 is Seedance only.
durationnumber- Seconds, 1–30. Most models accept only 4, 6 or 8; Seedance 2.5 up to 30.
resolutionstring- "480p", "720p" or "1080p".
sound_enabledboolean- Generate audio on models with a sound toggle.
canvas_iduuid | canvas link- Add the finished asset, with its reference images and text inputs, to this canvas — a UUID from list_projects or a canvas link the user pasted. Later generations from the same client follow it there. With an explicit workspace_id in another scope, billing follows workspace_id and the asset still lands here. Omit to continue the client's current generation canvas.
new_canvasboolean- Start a new canvas for this generation and the ones after it. Cannot be combined with canvas_id.
workspace_id"personal" | uuid- Where the generation bills and where its canvas lives: the personal account or a workspace from list_workspaces. Omitted, it follows canvas_id's scope, the only scope that can generate, or the client's scope from the last 30 minutes; otherwise the call returns scope_required with the options so the agent can ask the user.
Returns { id, status: "pending", canvas?: { projectId, url, newCanvas }, scope: { kind, workspaceId?, name? } }
get_generation
FreeStatus, result, cost or failure for a generation you started over MCP. On first completion the result and its reference images and videos are placed on the canvas the generate call named, and its link returned. Consecutive generations from one client share a canvas until a 30-minute pause.
idrequireduuid- From generate_image, generate_code or generate_video.
Returns { id, status, result, error, projectAssociation: { projectId, url, nodeIds } }
transform_image
FreeConvert, compress to a byte target, or trim the edges of an image you own. Synchronous and free; its own rate limit.
sourcerequiredstring- A storage path or URL of an image the caller owns — a generation's result URL qualifies.
oprequired"convert" | "compress" | "trim"format"png" | "jpeg" | "webp" | "avif"- Required for convert; compress defaults to webp.
qualitynumber- 0–1 for convert, default 0.9.
target_bytesinteger- Required for compress: 10 KB – 10 MB.
max_dimensioninteger- Bound the longest edge, up to 8192. Never enlarges.
Returns { url, path, width, height, bytes, format, withinTarget, trimmed }
create_image_upload
FreeStage a reference image of up to 10 MB, bigger than an MCP request can carry inline. Returns a single-use signed upload URL (two hours) and the HTTPS image_url it will serve from: PUT the raw bytes, then pass { url: image_url } in any images input. Free; its own rate limit.
content_typerequired"image/png" | "image/jpeg" | "image/webp" | "image/gif"- The file's real type, sent as the PUT's Content-Type. Storage refuses other types by header; it does not inspect the bytes.
size_bytesinteger- Optional. Over 10 MB is refused before a URL is issued.
Returns { upload_url, method: "PUT", headers, image_url, max_bytes, expires_in_seconds, next_step }
add_generation_to_project
FreeNeeds confirmationPlace a completed generation — image, code, screen or video — on another canvas the user chose, wired up like auto-placement. A generation already on a canvas is copied; one already on the target is reported, not duplicated. The one tool that insists on explicit user confirmation.
generation_idrequireduuid- A completed MCP generation.
project_idrequireduuid | canvas link- The target canvas, chosen by the user from list_projects or pasted as a link.
confirmed_by_userrequiredtrue- Literal true, only after the user has said yes.
Returns { status: "added" | "already_on_canvas", generationId, projectId, url, nodeIds? }
Canvases
Read and edit the same graph the user sees — every node type the toolbar offers, atomically.
list_projects
FreeEvery canvas the account can open — owned and workspace — with its URL, scope and whether the caller can edit it.
limitinteger- 1–100, default 50.
offsetinteger- Default 0.
favorites_onlybooleanquerystring- Case-insensitive match on the canvas name.
workspace_id"personal" | uuid- Only canvases in this scope.
Returns { data: Project[], pagination: { total, has_more, next_offset } }
get_favorite_projects
FreeFavourited canvases, most recently updated first.
limitinteger- 1–100, default 50.
offsetinteger- Default 0.
Returns Same shape as list_projects.
read_canvas
FreeThe whole graph — nodes with positions and data, edges, tagged brand assets as live URLs, and the DESIGN.md in force.
project_idrequireduuid
Returns { project, canvas: { nodes, edges, brandAssets, designContext } }
create_canvas
FreeA new empty canvas, or one seeded atomically with a prebuilt workflow. Nodes it creates carry the client's name as provenance.
namestring- Defaults to the workflow's name or Untitled Workflow.
workspace_id"personal" | uuid- Create inside a workspace the caller belongs to; omitted or "personal" is the personal account.
premade_workflow_idstring- An id from list_premade_workflows.
Returns { id, name, url, nodeCount, edgeCount, createdBy }
list_premade_workflows
FreeThe maintained prebuilt workflows, with stable ids and graph sizes.
No parameters.
Returns { data: { id, name, description, nodeCount, edgeCount }[] }
add_premade_workflow
FreeAppend a prebuilt workflow to an existing canvas without disturbing what is there.
canvas_idrequireduuid- The canvas, from list_projects or create_canvas.
premade_workflow_idrequiredstring- An id from list_premade_workflows.
Returns { canvasId, nodeCount, edgeCount, nodeIds }
add_canvas_nodes
FreeAdd up to 25 nodes of any type the toolbar offers — image, prompt (with its output node created and wired), video, capture, style_guide_input, site_scrape, code, text_input, sticky_note, note, group or social_post. Omit x/y to auto-place.
canvas_idrequireduuid- The canvas, from list_projects or create_canvas.
nodesrequiredNodeSpec[]- A discriminated union on type; each variant accepts only its own fields.
Returns { canvasId, url, nodes: { id, type, x, y }[], edges }
update_canvas
FreeOne atomic edit: patch node content and settings, move or resize, delete nodes and edges, connect nodes, rename the canvas. An unknown id fails the whole update without changing anything.
canvas_idrequireduuid- The canvas, from list_projects or create_canvas.
namestring- Rename the canvas.
updates{ node_id, data }[]- Per-node patches — prompt, model, aspect_ratio, image_url, brand_asset_role, content, color, url, html, viewport… each node type accepts its own.
moves{ node_id, x, y, width?, height? }[]- Only group and capture nodes resize.
add_edges{ source_node_id, target_node_id }[]- Outputs feed prompts; no self-connections.
remove_node_idsstring[]- Attached edges go too.
remove_edge_idsstring[]
Returns { canvasId, url, movedCount, removedNodeCount, updatedNodeCount, addedEdges }
Workflows
Run a saved graph end to end and read the per-output results.
get_workflow
FreeA saved workflow's metadata and hydrated graph — the same payload as read_canvas, addressed by workflow id.
workflow_idrequireduuid
Returns { workflow, graph: { nodes, edges, brandAssets, designContext } }
list_workflows
FreeLightweight summaries of saved workflows.
limitinteger- 1–100, default 50.
offsetinteger- Default 0.
Returns { data: Workflow[], pagination }
run_workflow
Spends walletRun a saved graph headlessly. Executes an immutable snapshot taken at enqueue; completed outputs are written back onto the canvas.
workflow_idrequireduuidworkspace_iduuid- Bill against a Team workspace's pooled wallet instead of the personal one. The caller must be a member (any role but viewer) of an active Team workspace.
Returns { id, workflowId, status: "pending", progress: { total, completed }, results: [] }
get_workflow_run
FreeProgress, per-output results with costs, and failure details for a run you started.
workflow_idrequireduuidrun_idrequireduuid- From run_workflow.
Returns { id, status, progress, results: { outputNodeId, type, status, url | code, costMicros }[], error, completedAt }
Site & motion
Drive the two cloud-browser nodes: record a page's animation, or crawl a site into a redesign.
run_capture
Spends walletRun a capture node — record its URL or element in a cloud browser and write the frames and animation context back onto the node. Synchronous, 10–30 seconds.
canvas_idrequireduuid- The canvas, from list_projects or create_canvas.
node_idrequiredstring- A capture node, from read_canvas. Set its url, selector, duration and color_scheme first.
Returns { status, frameUrls, totalFrames, animationContext, pageTitle }
run_site_scrape
Spends walletRun a site scrape node — crawl the configured site and seed the redesign graph (screenshots, brand assets, baseline DESIGN.md, direction previews, per-page screen prompts) beside it. Synchronous, metered per page.
canvas_idrequireduuid- The canvas, from list_projects or create_canvas.
node_idrequiredstring- A site scrape node, from read_canvas. Set its url, mode, max_pages and paths first.
Returns { status, siteName, pageCount, pages, seededNodeCount, previousGraphReplaced }