Overview
Framelane runs a hosted, authenticated MCP (Model Context Protocol) action server at:
Any MCP host — Claude, Claude Desktop, Cursor, VS Code, LangChain, CrewAI, etc. — can
connect and drive Framelane over MCP without writing HTTP integration code. It speaks the
Streamable HTTP transport, and every tool requires authentication (see below). It
is an action server — it renders, runs tasks, and edits projects. For documentation,
agents read the static channels instead: llms.txt, the
installable skill, or any docs page as raw markdown
(append .md to its URL).
Getting a credential first
Onboarding happens over REST, not MCP — an MCP client connects after it holds a key:
POST /v1/signup with a workspace_name and an email you control — returns an fl_
API key and emails a 6-digit code.
POST /v1/signup/verify with the code. The key works the moment this succeeds; until
then authed calls return 403 email_not_verified.
GET /v1/capabilities (no auth) returns an onboarding block describing this loop plus the
action_mcp_url in machine-readable form. See Self-provisioning for
agents. Interactive clients can instead authenticate via
OAuth 2.1 (PKCE) — Framelane is an OAuth resource server and delegates the login to
Clerk as the authorization server; MCP hosts discover it automatically from the server’s
WWW-Authenticate challenge.
Every tool requires a credential (an fl_ key or a Clerk OAuth token), and every tool that
acts on your workspace requires a verified one — get_capabilities is the exception, so
an unverified workspace can still read the onboarding block that explains how to verify.
The full set, 25 tools:
Start by calling the get_capabilities tool. Call it with no argument for a small
index naming every catalog section and its byte size, then fetch the sections you need.
There are three tiers, and an agent needs all three:
section='catalogs' — every flat catalog in one slice (effects, motions,
transitions, blend_modes, easings, word_animations, motion_scopes,
output_formats, input_formats, element_types, task_types, rate_limits) —
what exists, each flagged with whether it is supported today. Fetch one on its own
with section='effects', section='motions', and so on.
section='element_schemas' — the full JSON Schema of every field each element type
accepts (video, audio, text, image, shape), nested motion /
effects / counter / glow / trim_keyframes included. It is by far the largest
section — the index reports its exact size — so prefer one type at a time:
section='element_schemas.text'.
section='request_schema' — the composition-level body: groups,
custom_animations, motion_blur, background_gradient, watermark_url,
transitions, canvas and output settings. None of these appear in the flat catalogs.
Read per-element fields from element_schemas, not request_schema — the latter
deliberately elides them. motions[] also carries a per-preset surface matrix
(element_entrance / element_exit / character / text_block); check it before picking
a preset, since a preset used on the wrong surface is a 422.Every response is wrapped as {section, api_version, data} — the slice you asked for
is under data. An unknown section is an invalid_request error carrying
valid_sections and a suggestion, so a typo is self-correcting.The same catalog is also the framelane://capabilities resource (section='all' puts
exactly what the resource serves under data) — use whichever your client supports.
The tool exists because resource support is not universal across MCP clients, and the
catalog is step one, so prefer the tool unless you know your client reads resources.
Composition bodies are plain JSON objects. create_render.body,
create_preview.request, create_project.body.render_request and the replace_request
op all take the composition as an ordinary object; their schema comes from
get_capabilities, deliberately not inlined into the tool definitions. That keeps
every tool’s input schema under 4 KB, so size-capped MCP clients keep the whole toolset
instead of silently dropping the ones that create things.Validation is unchanged: the server validates each body against the same model the REST
API uses, unknown keys are still rejected, and a bad field comes back as
invalid_request with an RFC 6901 pointer (e.g. /elements/0/font_wight) and a
suggestion (font_weight).
To see a composition before paying for a full render: create_preview (or
preview_project) with a contact_sheet option → wait_for_render → get_render_preview,
which returns the grid as an inline image.
Media an agent can reach. list_assets serves exactly the rows
GET /v1/workspace/assets serves (one shared query, so kind and readiness cannot
drift), and its cursors are interchangeable with the REST ones.Two timing models, and mixing them up is the usual 422. For video and audio,
duration is the source file’s full length: omit an element’s out_point and the
server fills it from that, or set in_point / out_point to trim. An image has no
duration (always null, on every path) and no out_point; its element carries a
required duration of its own, in seconds on screen, which you choose.ready is derived, not stored: video and audio become ready when the duration probe
lands, an image as soon as its bytes are in the bucket. list_assets shows only ready
assets, so get_asset is what you poll after an upload. To add a local file, call
create_upload (or POST /v1/uploads over REST) and PUT the bytes to the URL it
returns, in one request, with the same Content-Type. Passing the duration you probed
locally makes a video or audio asset usable immediately instead of waiting on the
server probe. A file already at a public https URL needs none of this — pass the URL
as an element’s source URL and it is copied into the workspace on first render.
Waiting is one tool call, not a loop. wait_for_render / wait_for_task block until the
job is terminal, streaming progress as it advances. If a call returns still_running: true
(the server hold elapsed), call again. Ensure your MCP client’s per-tool timeout is ≥ 30s.
Tool errors carry a stable machine code. A domain failure is not a transport error: it
comes back in the tool result as {"error": {"code", "message", "details"}} —
branch on code. The set is invalid_request, not_found, conflict,
quota_exceeded, rate_limited, email_not_verified, unauthorized,
invalid_source (extract_brand_kit could not read the URL),
unsupported_content_type (create_upload was given a type we do not store; the
details.allowed list says what to re-encode to) and internal. Only an unexpected
exception surfaces as a real tool error.rate_limited is per workspace, not per IP, and only the submission tools are
metered — create_render, render_project and run_task. Previews
(create_preview / preview_project), reads and get_capabilities are unmetered.
Its details carry limit_per_minute and retry_after_seconds; wait that long and
retry rather than backing off blindly.
Connecting
Point your MCP host at https://mcp.framelane.io/mcp and send your key as a Bearer token.
Cursor / VS Code
Add to your MCP settings (~/.cursor/mcp.json, or the workspace .vscode/mcp.json):
Then ask: “Use the Framelane MCP to render this composition…”
Claude Desktop
Add to claude_desktop_config.json (mcp-remote bridges the hosted server over stdio):
Agent Skills
The Framelane Agent Skills are static YAML playbooks — read them
from the docs (append .md) or install the skill, then
drive the calls through the tools above.