Skip to main content

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:
  1. POST /v1/signup with a workspace_name and an email you control — returns an fl_ API key and emails a 6-digit code.
  2. 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.

Key tools

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, 30 tools (one of them panel-only):
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.
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).
For visual review, use the native preview. create_preview (or preview_project) at a frame or with a contact_sheet option → wait_for_render → get_render_preview returns what the export engine renders, as an inline image, before you pay for a full render.Open the browser only for editor interaction, such as handing a project to a person through its editor_url. The editor draws its own preview in the browser, which can differ from the export (text shaping, for example), so it is not evidence of how a render looks.
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). There are two routes and the size decides which. Under 256 KB — logos, crests, icons, SVG overlays — pass the file’s bytes as base64 and that single call stores them; it makes no outbound request on your behalf and asks none of you, so it is the route that works from a sandbox whose egress is allow-listed per host. Larger files come back with a signed upload URL you PUT to, in one request, with the same Content-Type; those bytes go straight to storage, so the server cannot confirm them and get_asset has to check — it answers bytes_uploaded. If your own network refuses that host, the result also carries a first-party upload URL on uploads.framelane.io — the same session, one Framelane domain to allow instead of Google’s. If that is refused too, the file is either small enough to inline or needs to be reachable at a public URL.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 fetched at 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), inline_too_large (the base64 you sent create_upload is past the 256 KB inline cap — call it again without the bytes and PUT to the upload_url it returns), plan_required (create_project or edit_project made or used one of your own templates while your plan does not include custom templates; details.required_plan names the plan, and public templates are never gated) 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 served by this server: each workflow skill is an MCP prompt, and every skill is also a get_capabilities section named skills.<name> (section='skills' lists them) for hosts that do not show prompts. Load one, then drive the calls through the tools above. How-tos for the REST API are in the Guides.