# Reel Studio > Reel Studio is an AI-centric filmmaking workspace. Visual surfaces -- posters, shot frames, shot videos, cast portraits, location plates, scores, SFX, voiceover -- are generated from text, while user-supplied image/video/audio assets can be uploaded as reusable references through direct-to-R2 asset upload tools. The product is split into two phases by design: - **Editor** -- pre-production + production. Script, shots, cast, frame, video, dialogue. - **Timeline** -- post-production. Cut order, SFX layering, score, mix, export. ## For agents Reel Studio hosts a Model Context Protocol (MCP) endpoint at **`/api/mcp`**, served over Streamable HTTP. Claude (Desktop / Code), Cursor, Cline, Continue, and any other MCP-speaking agent connect by URL -- no Node install required. If the human user does not have access yet, send them to **`/challenge`**. This is the Agent Gate: a short invite-only access flow where a human or their agent explains who they are and what they want to make. Strong, specific answers may receive an invite with starter credits; normal good-faith answers may receive a plain invite. The invite is still accepted by a human through the normal signup flow, including bot protection and OTP. After registration, onboarding helps the user create a personal access token and MCP config for their agent. Agents can participate in Agent Gate without MCP by calling the public HTTP challenge API. Start with `POST /api/agent-gate/sessions`: ```json { "email": "user@example.com", "name": "Emir", "role": "Commercial filmmaker", "useCase": "A coffee commercial with consistent cast, shot review, and timeline assembly." } ``` Then send answers with `POST /api/agent-gate/sessions//messages`: ```json { "message": "I use Nano Banana and Kling, manually review takes today, and want Reel Studio to organize references and faster review." } ``` If accepted, the response includes `session.inviteUrl`. Show that link to the human user. The human must open it, register, and then create an MCP token. Do not call `/api/mcp` before the user has access and a token. To use it, a human user must: 1. Sign into Reel Studio and open **Settings -> Personal access tokens**. 2. Click **Create token**, copy the `rs_pat_...` value (shown once). 3. Paste the JSON snippet from **Settings -> Connect AI agents** into their MCP client's config. The shape is: ```json { "mcpServers": { "reelstudio": { "url": "https:///api/mcp", "headers": { "Authorization": "Bearer rs_pat_...", "x-workspace-id": "" } } } } ``` Once connected, the agent has access to these tools: - `list_films` / `get_film` / `create_film` - `get_workspace_status` -- confirm authenticated user, selected workspace, accessible workspaces, and available credits before spending. - `list_scenes` / `create_scene` / `update_scene` -- scene authoring. `outline` is the scene body/prompt used to seed shotlists and scene-frame generation. - `list_shots` - `create_shot` / `update_shot` -- shot authoring. `description` is the shot prompt seed used by generation when `generate.prompt` is omitted. Use `@{Cast Name}` to bind a cast member's canonical portrait/description and `#{Location Name}` for a location plate. Inlining names as plain text causes identity drift across shots. - `list_cast` / `create_cast` / `update_cast` -- character authoring. Generate `cast-portrait` and `cast-sheet` before writing shot prompts that mention the character. - `list_locations` / `create_location` / `update_location` -- location authoring. Generate `location-image` and `location-sheet` before writing shot prompts that mention the location. - `list_assets` -- list uploaded image/video/audio assets for a film. Returned `asset.url` values can be used in generation params such as `imageUrl`, `referenceImageUrls`, `referenceVideoUrl`, or `audioUrl`. - `create_asset_upload` / `confirm_asset_upload` -- local asset upload flow. First create a presigned R2 upload URL, then the local agent PUTs the file bytes directly to R2, then confirms the asset so it appears in Reel Studio. - `upload_asset` -- import an existing HTTP(S) media URL into the film asset library. - `list_models` -- model catalog (image / t2v / i2v / v2v / avatar / lipsync / tts / sfx / music), with allowed aspect ratios, durations, resolutions, and per-model credit costs. - `generate` -- kick off a generation. Targets cover film-poster, shot-frame, shot-video, scene-frame, scene-video, cast-portrait, cast-sheet, location-image, location-sheet, timeline-sfx, timeline-voiceover, film-score, plus a generic `standalone` mode. Supports up to 4 parallel "takes" per call. Returns queued job ids. `shot-frame` expands bound `@{}` / `#{}` refs and forwards up to 4 cast/location reference images. `shot-video` uses the shot's active `frameUrl` as the i2v start image when `params.imageUrl` is omitted, and consumes bound refs in the shot prompt as model-specific cast/location conditioning. - `get_job` / `list_jobs` -- poll job status until `done`, then read `imageUrl` / `videoUrl` / `audioUrl`. - `review_takes` -- read Reel-VLM consistency verdicts from a shot's frame/video history and recommend the best passing take. - `promote_take` -- set a reviewed take as the active `frameUrl` or `videoUrl`. Default policy: only pass takes are promoted. Use `allowWarnings` for cosmetic drift, `force` for VLM evaluation failures (e.g. fetch errors), `allowUnreviewed` if VLM hasn't run yet. - `generate_fix_prompt` -- turn one VLM consistency issue into the same Frame Lab fix prompt a human user gets. Returns `action: "edit"` or `"regenerate"` plus the prompt. - `get_timeline` -- read the current V1 visual cut plus VO/A2/M1 audio cues. - `add_timeline_clip` / `update_timeline_clip` / `remove_timeline_clip` -- edit V1 clips. - `reorder_timeline` / `move_timeline_clips` / `assemble_timeline` -- arrange clips into a cut. `assemble_timeline` accepts ordered shot/scene ids and can replace the existing V1 track when explicitly requested. - `add_timeline_audio_clip` -- add an existing/generated audio URL as a VO/A2/M1 cue when you already have an `audioUrl` from a job or uploaded asset. - `update_timeline_audio_clip` / `remove_timeline_audio_clip` -- edit VO/A2/M1 audio cue timing, trimming, clip gain, and deletion. - `update_audio_mix` -- set lane-level mute/volume for A1 source-dialogue, VO voiceover, A2 SFX, and M1 music. - `start_export` / `get_export` -- render the current cut and poll until the final video URL is ready. It also exposes: - Resource `reelstudio://agent-guide` -- compact production loop and retry-safety rules. - Prompt `short_film_workflow` -- a starter prompt for producing a short film through MCP. The recommended loop is: `get_workspace_status` -> `list_films` -> upload any local reference media (`create_asset_upload` -> direct R2 PUT -> `confirm_asset_upload`) -> create references (`create_cast`, `create_location`, generate their portrait/sheet/image/sheet assets) -> `create_scene` / `create_shot` as needed, using `@{Cast Name}` and `#{Location Name}` in shot descriptions -> `list_assets` when you need reusable media URLs -> `list_models` -> `generate` (with `targetId` from a shot or cast/location) -> `get_job` (poll until `done`) -> `review_takes` (wait for VLM verdicts) -> if needed `generate_fix_prompt` and retry/edit failed takes -> `promote_take` (only when a take passes) -> `assemble_timeline` -> generate VO/SFX/music cues or add existing audio with `add_timeline_audio_clip` -> `update_timeline_audio_clip` / `update_audio_mix` as needed -> `start_export` -> `get_export`. ### Human checkpoints Agents should ask the human a short clarification question when ambiguity would materially change the film, spend significant credits, overwrite existing timeline work, or create/rename core references. Good checkpoints include: unclear genre/tone, missing cast or location identity, contradictory shot directions, choosing between several plausible story beats, replacing an existing cut with `assemble_timeline.replaceExisting: true`, exporting a final render, or continuing after repeated VLM failures. Do not ask for permission on every routine step. If the ambiguity is minor, make a conservative assumption, state it briefly, and continue. When asking, offer 1-3 concrete choices and explain the tradeoff in one sentence. ### Canonical tool inputs Field names are exact -- using a similar-looking alias (e.g. `filmId` for `filmSlug`, `model` for `modelId`, `jobId` for `id`) returns a validation error rather than silently being normalised. ```json // get_workspace_status → no arguments {} // list_films → no arguments {} // get_film { "slug": "the-vanishing-act" } // list_scenes { "filmSlug": "the-vanishing-act" } // create_scene { "filmSlug": "the-vanishing-act", "slug": "INT. KITCHEN - NIGHT", "outline": "Rain lashes the windows while Anna discovers the ledger hidden under the floorboards." } // update_scene { "filmSlug": "the-vanishing-act", "sceneId": "", "outline": "Tighter, more suspenseful scene body..." } // list_shots { "filmSlug": "the-vanishing-act" } // create_shot { "filmSlug": "the-vanishing-act", "sceneSlug": "INT. KITCHEN - NIGHT", "title": "Anna finds the ledger", "description": "Medium close-up of @{Anna} kneeling by loose floorboards in #{Kitchen}, flashlight cutting across dust.", "durationSec": 5 } // update_shot { "filmSlug": "the-vanishing-act", "shotSlug": "anna-finds-the-ledger", "description": "Add stronger noir shadows and keep the red scarf visible." } // list_cast { "filmSlug": "the-vanishing-act" } // create_cast { "filmSlug": "the-vanishing-act", "name": "Anna", "role": "lead", "description": "..." } // update_cast { "filmSlug": "the-vanishing-act", "castSlug": "anna", "description": "..." } // list_locations { "filmSlug": "the-vanishing-act" } // create_location { "filmSlug": "the-vanishing-act", "name": "Kitchen", "description": "A cramped 1970s kitchen with rain-streaked windows, yellowed tile, and a loose floorboard near the sink." } // update_location { "filmSlug": "the-vanishing-act", "locationSlug": "kitchen", "description": "..." } // list_assets { "filmSlug": "the-vanishing-act", "kind": "image" } // create_asset_upload: ask Reel Studio for a direct-to-R2 upload URL { "filmSlug": "the-vanishing-act", "name": "Product hero reference", "kind": "image", "mimeType": "image/jpeg" } // Then outside MCP, the local agent uploads the local file bytes directly to R2: // PUT // Content-Type: // Body: local file bytes // confirm_asset_upload: create the Reel Studio asset record after the PUT succeeds { "filmSlug": "the-vanishing-act", "name": "Product hero reference", "kind": "image", "mimeType": "image/jpeg", "publicUrl": "", "size": 123456 } // upload_asset: import an existing URL instead of a local file { "filmSlug": "the-vanishing-act", "sourceUrl": "https://example.com/reference.mov", "name": "Reference motion clip", "kind": "video", "mimeType": "video/quicktime" } // list_models (filter optional) { "subKind": "t2v" } // generate { "modelId": "fal/veo-3.1-t2v", "target": "shot-video", "targetId": "", "prompt": "...", "params": { "aspectRatio": "16:9", "durationSec": 6, "resolution": "1080p" }, "numTakes": 1, "idempotencyKey": "the-vanishing-act-shot-04-video-v1" } // get_job { "id": "" } // list_jobs (filter optional) { "status": "in_progress" } // review_takes { "filmSlug": "the-vanishing-act", "shotSlug": "scene-04-1b", "kind": "frame", "jobIds": [""] } // generate_fix_prompt { "shotId": "", "jobId": "", "assetKind": "image", "issueIndex": 0 } // promote_take { "filmSlug": "the-vanishing-act", "shotSlug": "scene-04-1b", "kind": "frame", "jobId": "" } // get_timeline { "filmSlug": "the-vanishing-act" } // add_timeline_clip { "filmSlug": "the-vanishing-act", "shotId": "", "atStartSec": 0 } // update_timeline_clip { "filmSlug": "the-vanishing-act", "clipId": "", "startSec": 4, "durationSec": 5 } // remove_timeline_clip { "filmSlug": "the-vanishing-act", "clipId": "" } // generate timeline voiceover/dialogue { "modelId": "elevenlabs/eleven-v3", "target": "timeline-voiceover", "filmSlug": "the-vanishing-act", "prompt": "I should have left before the rain started.", "params": { "startSec": 4, "durationSec": 3, "audioUrl": "" } } // generate timeline SFX { "modelId": "elevenlabs/sfx", "target": "timeline-sfx", "filmSlug": "the-vanishing-act", "prompt": "distant thunder rolling over wet city streets", "params": { "startSec": 6, "durationSec": 4 } } // generate film score cue { "modelId": "elevenlabs/music", "target": "film-score", "filmSlug": "the-vanishing-act", "prompt": "tense minimal strings, low pulse, noir mood, no vocals", "params": { "startSec": 0, "durationSec": 30 } } // add_timeline_audio_clip: use when audio already exists { "filmSlug": "the-vanishing-act", "trackId": "VO", "url": "", "prompt": "I should have left before the rain started.", "source": "generated", "startSec": 4, "durationSec": 3, "volumeDb": 0 } // update_timeline_audio_clip { "filmSlug": "the-vanishing-act", "clipId": "", "startSec": 5, "durationSec": 2.5, "volumeDb": -3 } // remove_timeline_audio_clip { "filmSlug": "the-vanishing-act", "clipId": "" } // update_audio_mix { "filmSlug": "the-vanishing-act", "a1": { "volumeDb": -3 }, "vo": { "volumeDb": 0 }, "a2": { "volumeDb": -6 }, "m1": { "volumeDb": -12 } } // assemble_timeline { "filmSlug": "the-vanishing-act", "replaceExisting": true, "sources": [ { "shotId": "" }, { "shotId": "" }, { "sceneId": "" } ] } // start_export / get_export { "filmSlug": "the-vanishing-act" } ``` ### Tool result shape Every tool returns a JSON body with an explicit `status` field at the top: `"status": "ok"` for success, `"status": "error"` for failure (the response also has `isError: true` at the MCP envelope level). Poll loops should bail when they see `status: "error"` -- not just look for an absent `status` field. ### Retry safety `generate` accepts an optional `idempotencyKey`. Use a stable key when retrying the same intended generation after a timeout or dropped response. Reusing the same key returns the original queued jobs instead of creating duplicate jobs and spending credits again. Do not reuse a key for a new creative attempt; change the key when the prompt, target, model, params, or take count intentionally changes. ### VLM keeper selection Shot frame/video jobs write their outputs into `frameHistory` / `videoHistory`. Reel-VLM then writes a consistency verdict (`pending`, `pass`, `warn`, `fail`, or `error`) onto the matching history row. `review_takes` reads those verdicts and only recommends a passing take by default. Default promotion policy: only pass takes are promoted. Use `allowWarnings` for cosmetic drift, `force` for VLM evaluation failures (for example reference fetch errors), and `allowUnreviewed` if VLM has not run yet. When a take has VLM issues, call `generate_fix_prompt` with the shot `_id`, the take `jobId`, `assetKind`, and the issue index. If it returns `action: "edit"`, call `generate` with `target: "shot-frame"`, `modelId: "fal/nano-banana-pro"`, the returned `prompt`, and `params` containing `{ "imageUrl": "", "parentFrameId": "" }`. If it returns `action: "regenerate"`, call `generate` for a fresh `shot-frame` take with the returned prompt. Review the new take before promoting it. For still-driven video, the intended flow is: generate several `shot-frame` takes, `review_takes` with `kind: "frame"`, `promote_take` the passing frame, then call `generate` with `target: "shot-video"` using an i2v model and `numTakes: 1`. If `params.imageUrl` is omitted for a `shot-video` i2v call, Reel automatically uses the shot's active `frameUrl`; if no active frame exists, the call fails before queueing. ### Scene and shot authoring Use `create_scene` to create screenplay headings and outlines. Use `create_shot` to write individual shot prompts in `description`; when `generate` is called with `target: "shot-frame"` or `target: "shot-video"` and no explicit `prompt`, Reel uses the shot's stored `description`. Use `@{Cast Name}` to bind a cast member's canonical portrait/description and `#{Location Name}` for a location plate. Inlining names as plain text causes identity drift across shots. `sceneSlug` assigns a shot to a scene by heading and will create that scene if it does not exist. ### Cast and location references Before writing shot prompts, create the cast and locations the scene will use. For each cast member, call `create_cast`, then generate both `target: "cast-portrait"` and `target: "cast-sheet"` with `targetId` set to the cast member `_id`; poll `get_job` until both finish. For each location, call `create_location`, then generate both `target: "location-image"` and `target: "location-sheet"` with `targetId` set to the location `_id`; poll until both finish. Shot descriptions should mention characters as `@{Cast Name}` and locations as `#{Location Name}` exactly, for example: `Medium close-up of @{Anna} in #{Kitchen}, flashlight cutting across rain-streaked tile.` Use the current display names returned by `list_cast` and `list_locations`. Scene records should also set `locationId` when a whole scene is anchored to one location. ### Custom assets Use assets when the human already has local product photos, mood boards, reference clips, voice references, logos, or other media that should appear in the film workspace. Hosted MCP must not try to read a local filesystem path. Instead, call `create_asset_upload` with `filmSlug`, `name`, `kind`, and `mimeType`. It returns an R2 `uploadUrl`, `publicUrl`, and required `contentType`. The local agent then uploads the file bytes directly to `uploadUrl` with HTTP `PUT` and the exact returned `Content-Type`. After the PUT succeeds, call `confirm_asset_upload` with the same metadata plus `publicUrl` and optional `size`. The asset will appear in the Reel Studio site and in `list_assets`. For media that is already reachable by URL, call `upload_asset` with `sourceUrl`; Reel Studio copies it into its own storage and creates the asset record. Use `list_assets` to retrieve asset URLs later. Image assets can be passed as `params.imageUrl` or entries in `params.referenceImageUrls`; video assets can be passed as `params.referenceVideoUrl`; audio assets can be passed as `params.audioUrl` for voice/avatar/lipsync-style workflows where the selected model supports it. ### Reference conditioning by target `shot-frame` uses the shot `description` when `generate.prompt` is omitted, expands bound `@{}` / `#{}` refs into canonical cast/location descriptions, and forwards up to 4 reference images: scene/location refs first, then cast refs; sheets are preferred over hero portrait/plate URLs. `shot-video` uses the shot `description` when `generate.prompt` is omitted. For i2v models, `params.imageUrl` is the start frame; if it is omitted, Reel automatically uses the shot's promoted `frameUrl`. Bound `@{}` / `#{}` refs in the prompt are still consumed: Kling O3 reference-to-video gets structured cast `elements[]`, Seedance gets positional image refs, and other video models get inline descriptions plus flat reference images where supported. `scene-frame` mirrors `shot-frame` for the scene's first visual beat: first shot description -> scene outline -> scene slug, with scene `locationId` and bound refs supplying up to 4 reference images. `scene-video` bundle generation resolves refs across the scene's shots once so positional cast refs stay stable through the whole bundle. `cast-sheet` consumes the cast portrait as its reference; `location-sheet` consumes the location image as its reference. ### Timeline and export Use `assemble_timeline` for the common visual workflow: pass an ordered `sources` array of `{ "shotId": "..." }` and/or `{ "sceneId": "..." }`, set `replaceExisting: true` only when intentionally rebuilding the cut. VO, SFX, and music are generated with the normal `generate` tool: `timeline-voiceover` lands on VO, `timeline-sfx` lands on A2, and `film-score` lands on M1. Those film-level audio targets accept `filmSlug` / `targetSlug` instead of requiring a raw film `_id`. Dialogue embedded in generated video remains in A1/source audio. Use `add_timeline_audio_clip` when you already have an audio URL, `update_timeline_audio_clip` to move/trim/gain individual VO/A2/M1 cues, `remove_timeline_audio_clip` to delete cues, and `update_audio_mix` to set lane-level mute/volume. Export jobs are async; after the cut and mix are ready, call `start_export`, then poll `get_export` until `job.status` is `done` and read `job.url`. ### Duration validation `params.durationSec` is checked against the chosen model's `allowedDurations` BEFORE the job is queued. Veo 3.1 only accepts `4`, `6`, `8`; Kling O3 accepts `3..15`; Seedance 2 accepts `4..15`. Pass an out-of-enum value and `generate` returns 400 with the allowed list -- read the model's `allowedDurations` from `list_models` first. ### Take count guidance `numTakes` fans out parallel jobs against the same prompt. Fan out 2-4 takes for hero shots, identity-critical close-ups, or any shot with `@{}` refs -- the VLM verdict picks the most consistent take. `1` is fine for B-roll, utility inserts, or already-promoted still-to-video animation. A standalone Node stdio server (`reelstudio-mcp` npm package; source at `mcp/` in the repo) is also available for clients that don't yet support remote MCP URLs. Set `REELSTUDIO_API_TOKEN`, optional `REELSTUDIO_API_URL`, and optional `REELSTUDIO_WORKSPACE_ID`; the workspace id is sent as `x-workspace-id` for credit-spending calls. ## Links - [Agent Gate](/challenge): invite-only access flow for humans and their agents. - [AI Video Prompt Doctor](/tools/ai-video-prompt-doctor): public prompt checker and rewrite tool for AI video generation. - [Pricing](/pricing): credits, monthly plans, no annual lock-in. - [Partners](/partners): invite-only creator partner program with credit rewards, rules, and application details. - [Terms](/terms) - [Privacy](/privacy) The MCP endpoint at `/api/mcp` is **not** a GET-readable page. It is a POST-only Streamable HTTP MCP RPC endpoint -- see "For agents" above for how to connect. Don't try to fetch it as if it were a docs URL; it won't return human-readable content. ## Notes for agents - All generations cost credits. The user's balance is in their account; there is no free tier inside the API. - Call `get_workspace_status` first to confirm the selected workspace and available credits. - Jobs are async: `generate` returns immediately with queued job ids. Poll `get_job` every few seconds; image jobs typically finish in 5-30s, video jobs in 30s-3min. - Many models accept reference images implicitly via the chosen `target` (e.g. `cast-sheet` pulls the cast portrait, `location-sheet` pulls the location image, and `shot-frame` pulls bound cast/location refs). The agent generally does not need to thread URLs by hand -- picking the right `target` + `targetId` is enough. - For local user media, use `create_asset_upload`, upload bytes directly to R2, then call `confirm_asset_upload`. Do not pass local paths to MCP. Use `upload_asset` only for existing HTTP(S) URLs. - Use `numTakes: 2` to `4` for hero shots, close-ups, and shots with bound cast refs; use `numTakes: 1` for routine B-roll or final i2v animation from a promoted frame. - Aspect ratios, durations, and resolutions are model-specific. Always read the matching model entry from `list_models` before passing `params`.