Connect (Claude Code)
claude mcp add --transport http trvrse-cinema https://www.trvrse.com/api/mcp/mcp \ --header "Authorization: Bearer trv_YOUR_KEY_HERE"
Install the skill
mkdir -p ~/.claude/skills/trvrse-cinema && \ curl -fsSL https://www.trvrse.com/docs/agents/skill.md \ -o ~/.claude/skills/trvrse-cinema/SKILL.md

Mint keys at Settings → Agent access · Raw markdown · llms.txt

trvrse cinema — MCP guide for agents

You are (or are configuring) an LLM agent connected to the trvrse cinema MCP server. It exposes a five-stage film pipeline — Script → References → Storyboards → Videos → Assembly — as 20 tools: 17 stage-by-stage tools you drive yourself, plus 3 Autopilot tools (make_film, get_run, cancel_run) that hand the whole pipeline to the server for one-call film production. This guide is the complete playbook: connecting, the workflow loop, Autopilot, cost and error contracts, and a worked end-to-end transcript.

Endpoint: https://www.trvrse.com/api/mcp/mcp · Keys: minted by a human at Settings → Agent access · Raw markdown version of this page: https://www.trvrse.com/docs/agents.md

1. Quick start

Mint a key

  1. Sign in to the app and go to Settings → Agent access (https://www.trvrse.com/settings/agent).
  2. Click New key, give it a name (and, optionally, a spend cap in credits — leave blank for uncapped).
  3. The plaintext key is shown exactly once in a modal you must explicitly dismiss with Done. Copy it now — it is never shown again (only a trv_ + 8-char prefix is kept for display). If you lose it, revoke the key and mint a new one.

The key looks like trv_ followed by 43 base64url characters, e.g. trv_9F2k7q....

Endpoint

https://www.trvrse.com/api/mcp/mcp

(mcp-handler's basePath is /api/mcp; it appends the mcp transport segment itself. Streamable HTTP only — SSE is disabled.)

Claude Code

claude mcp add --transport http trvrse-cinema https://www.trvrse.com/api/mcp/mcp \
  --header "Authorization: Bearer trv_YOUR_KEY_HERE"

Generic MCP client (JSON config)

Most MCP clients (Claude Desktop, other agent runtimes) accept a config block like:

{
  "mcpServers": {
    "trvrse-cinema": {
      "url": "https://www.trvrse.com/api/mcp/mcp",
      "transport": "http",
      "headers": {
        "Authorization": "Bearer trv_YOUR_KEY_HERE"
      }
    }
  }
}

The exact key name/shape (url vs command, headers vs env) varies by client — anything that supports a streamable-HTTP MCP transport with a custom header works. There is no OAuth/dynamic-registration flow; the bearer token is the credential.

Sanity-check the connection

Once connected, ask the agent to call list_projects (or run get_project_status against an existing project id). A working connection returns real JSON; a bad key returns an isError result naming BRIDGE_AUTH_FAILED (see the error contract below).

Install this guide as a skill (optional)

Claude Code and other skills-capable agents can carry this playbook in-context instead of re-reading it each session:

SKILLS_DIR=~/.claude
mkdir -p "$SKILLS_DIR/skills/trvrse-cinema" && \
  curl -fsSL https://www.trvrse.com/docs/agents/skill.md \
  -o "$SKILLS_DIR/skills/trvrse-cinema/SKILL.md"

2. The workflow loop

Every film follows the same loop per stage:

  1. Check state — get_project_status: read the five stage statuses and nextActions (it names exactly the tool calls the pipeline admits next; trust it over guessing).
  2. Quote — get_cost_estimate for the stage you are about to generate.
  3. Generate — call the stage's generate_* tool with confirmedCost set to the quote's exact total.
  4. Poll — get_project_status every 15–30 seconds until the stage's entities leave GENERATING (typically 1–3 minutes).
  5. Review & approve — inspect results (statuses, failure reasons), regenerate anything that failed, then call the stage's approve_* tool to unlock the next stage.

Script is the exception: generate_script and rewrite_scenes run the LLM in-request and return the full screenplay synchronously (~30–60 seconds), and script work is free — no quote needed.

3. One-call films (Autopilot)

Instead of driving the five-stage loop above yourself, make_film hands the whole pipeline to the server: submit a brief and a budget, then poll get_run until the film is done.

make_film

  • briefText (required) — the production brief the whole film is generated from.
  • profile (required) — "cinematic" (16:9 default) or "short" (see below).
  • budgetCredits (required) — a hard ceiling for the run's total spend (see "Budget is a hard ceiling" below).
  • gates (optional, default [] = fully unattended) — which of "script", "references", "storyboards", "videos", "assembly" pause the run for human review (see "Gates" below).
  • webhookUrl (optional, https:// only) — receives run events (see "Webhooks" below).
  • title, aspectRatio, videoModel (optional) — same shapes as create_project; profile supplies defaults you can override.

Returns {runId, projectId, webhookSecret?}. webhookSecret is present only when webhookUrl was set, and is shown exactly once, here — save it immediately, it is never returned again and you need it to verify webhook deliveries.

Refusals: AUTOPILOT_RUN_LIMIT (this key already has 3 active runs — cancel_run one or wait for it to finish), BRIDGE_SPEND_CAP (budgetCredits exceeds the key's remaining spend cap), CINEMA_INVALID_INPUT (unknown gate name, non-https webhookUrl, or budgetCredits <= 0).

Poll with get_run

Call get_run({ runId }) every 30–60 seconds instead of get_project_status while a run is active. It returns status, currentStage, attentionReason?, spentCredits/budgetCredits, profile, gates, per-stage project detail (the same shape get_project_status returns), film (once COMPLETED), the last 20 events, and nextActions.

status meanings:

  • RUNNING — actively progressing; nothing to do but poll.
  • WAITING_GATE — paused at currentStage for human approval in the trvrse app (see "Gates" below).
  • NEEDS_ATTENTION — paused on the budget ceiling, an error, or exhausted retries; read attentionReason and events.
  • COMPLETED / FAILED / CANCELLED — terminal.

Budget is a hard ceiling

budgetCredits caps the run's entire spend, not just one stage. Before dispatching any stage's paid work, the server checks whether that stage's cost would push the run's spend over budgetCredits; if it would, the run pauses NEEDS_ATTENTION instead — it never overspends and it never silently truncates or downgrades work to fit under the ceiling. There is no tool to raise budgetCredits on an existing run: a budget-paused run resumes only if the blocking stage's cost drops (e.g. a cheaper model chosen for that stage in the app) or you cancel_run it.

Retries

Each stage gets one automatic retry wave before pausing NEEDS_ATTENTION — this includes the LLM generation steps (script, visual bible, storyboard plan), which get 2 attempts total (the original try + the one retry) the same as every other stage. After the retry wave is used, a further failure pauses the run for a human to fix (regenerate or approve in the app) or cancel_run.

Paused runs (WAITING_GATE or NEEDS_ATTENTION) are not stuck waiting on you to nudge them: the server automatically re-checks every paused run on a short interval and resumes it within roughly 2 minutes of the blocker clearing (a gate being approved, a retry-eligible fix landing) — you never need to call anything to "wake" a run yourself.

Gates

gates lists which stages pause the run WAITING_GATE for human review before continuing — approval happens in the trvrse app, not through this bridge (there is no MCP "approve this gate" tool). Once approved there, the orchestrator resumes automatically (poll get_run to see the pause and the resume). Omit gates (or pass []) for a fully unattended run.

"assembly" is the final-review gate: when it's included, the run pauses WAITING_GATE with the finished film already rendered, and completes only once a human approves the assembly stage in the app.

Webhooks

When webhookUrl (https:// only) is set, the server POSTs a JSON body to it after every run-visible event:

{ "runId": "...", "type": "stage.completed", "stage": "references", "detail": "...", "timestamp": 1732000000000 }

Event types: stage.completed, gate.pending, needs_attention, run.completed, run.failed, run.cancelled.

Every delivery carries X-Trvrse-Signature: sha256=<hex>, an HMAC-SHA256 of the raw POST body keyed with the webhookSecret make_film returned. Verify against the raw bytes you received — not a JSON.parse'd-then-JSON.stringify'd copy, which is not guaranteed to byte-match:

import crypto from "node:crypto";

function verifyTrvrseWebhook(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Delivery is retried at 0s / 30s / 5min after a non-2xx response or a network failure; if the final attempt still fails, deliveries stop for that run (a "dead letter") — the events themselves remain readable via get_run, only the push channel goes quiet.

cancel_run

Stops future spend and dispatch only (RUNNING/WAITING_GATE/ NEEDS_ATTENTION → CANCELLED). Anything already dispatched settles normally and its credits stand — nothing is refunded, and a cancelled run cannot be resumed. Refused with AUTOPILOT_NOT_ACTIVE if the run is already COMPLETED/FAILED/CANCELLED.

Short profile

profile: "short" is the vertical-clip profile: aspectRatio defaults to 9:16 (still overridable), the script targets 2–4 scenes and a 15–60 second film, and videoModel defaults to a budget model chosen for cost (pass videoModel yourself to override it). Everything else — character and location consistency across shots, face-trust routing, review gates — is identical to the "cinematic" profile; "short" only changes length, pacing, and the default model, never the consistency guarantees.

4. The tool catalog

20 tools, one call = one pipeline action. Every result is either:

  • A domain payload (including {success:false, error} refusals — these are readable, non-error results the agent should react to), or
  • An isError tool result — auth failures or malformed input (see the error contract below).
#ToolOne-liner
1create_projectStart a new film: brief text, aspect ratio, optional render style/video model. Free.
2get_project_statusTHE polling workhorse — all 5 stage statuses, entity statuses, nextActions hints. Free.
3get_cost_estimateQuote a stage in credits before spending it.
4generate_scriptStage 1: LLM screenplay from a brief. Synchronous, ~30–60s. Free.
5rewrite_scenesStage 1: targeted LLM rewrite of specific scenes. Synchronous, ~30–60s. Free.
6approve_scriptREVIEW → APPROVED, unlocks stage 2.
7set_character_face_flagMark a character as a real human face before generating references (face-trust routing).
8generate_referencesStage 2: character sheets + location plates (+ visual bible if missing). Async.
9approve_referencesApprove entities ("all_pending" or explicit ids), then the stage.
10generate_storyboardsStage 3: storyboard page images (+ panel plan if missing). Async.
11approve_storyboardsApprove pages, then the stage — triggers materialization into video scenes.
12generate_videosStage 4: generate a video clip per scene. Async, 720p only.
13regenerate_sceneStage 4: regenerate ONE scene (after a failure or content-safety rejection). Async.
14approve_videosApprove stage 4 — unlocks assembly. Requires every live scene COMPLETED.
15render_filmStage 5: assemble + render. Idempotent settle-first — never silently re-renders.
16get_filmAssembly readout: final videoUrl + duration. Free.
17list_projectsConnection sanity check + project discovery — your cinema projects with per-stage statuses. Use get_project_status for detail. Free.
18make_filmAutopilot: one call, full pipeline (script → references → storyboards → videos → assembly), unattended by default. See §3 above.
19get_runAutopilot's polling workhorse — status, current stage, spend, events, nextActions for a run made with make_film. Free.
20cancel_runStop an active autopilot run; stops future spend only.

An MCP client that lists tools sees each tool's full argument schema and description — those are authoritative for exact shapes; this catalog and the contracts below cover what the schemas cannot say.

Async / polling pattern

generate_references, generate_storyboards, generate_videos, regenerate_scene, and render_film enqueue work and return immediately. After calling one:

  1. Call get_project_status (or get_film for render) repeatedly.
  2. Stop polling once the relevant stage/entity leaves GENERATING (typically 1–3 minutes).
  3. get_project_status's nextActions field always names the tool calls the pipeline legally admits next — trust it over guessing.

generate_script and rewrite_scenes are the exception: they call an LLM in-request and return synchronously (~30–60s) with the full script content already in the result — no polling needed for those two.

Cost contract

  • Call get_cost_estimate before any paid generate_*/render_film call and pass its exact total back as that tool's confirmedCost. The server recomputes at execution time and refuses a mismatch with {success:false, error:"COST_CHANGED", newEstimate} — re-quote and retry with the fresh total.
  • stage:"references" quotes are ALL-IN: characters + locations + the visual bible's pending style frames, whenever a bible is still owed. This is the exact ceiling generate_references will spend. Caveat: a quote taken before any bible exists doesn't yet know it needs one — if generate_references itself creates the bible (no bible was approved yet), the style-frame leg is priced and spent server-side beyond your confirmedCost. Quote after a bible exists if you need an exact before-the-fact total.
  • scopeIds, model, durationSeconds, forRegen are videos-only. Passing any of them for stage:"references" or stage:"storyboards" is refused, not silently ignored — the bridge would rather tell you a scoping arg doesn't apply than let you believe it scoped something it didn't.
  • resolution is not a parameter anywhere on this bridge. All quotes and generations are 720p; there is no way to request a different resolution through MCP.
  • regenerate_scene quotes must set forRegen:true. Regeneration snaps durations server-side, so a quote taken without forRegen:true diverges from what regenerate_scene will actually charge — always quote with the same model/durationSeconds you intend to pass to the write call.

Spend-cap behavior

  • Each key can carry an optional spendCapCredits (set at mint time or edited later in Settings → Agent access). It is a safety valve, not a ledger — the bridge checks-then-reserves against the cap atomically before spending, and reservations only ever move up (or up-then-back-down when a multi-leg call ends up doing zero billable work). There is no reconciliation against actual per-entity failures.
  • When a call would push spentCredits over spendCapCredits, the bridge action returns (never throws) {success:false, error:"BRIDGE_SPEND_CAP"} — a domain refusal, not a protocol error. Raise the cap in Settings → Agent access, or use a different key, to proceed.
  • Reservations happen per-leg against each leg's own server-computed estimate (not the caller's shared confirmedCost), so a multi-leg call (e.g. references' characters + locations + frames) can't over-reserve by reserving the same total three times.

Error contract

Two, and only two, shapes an agent needs to distinguish:

  1. Domain refusal — a normal (non-error) tool result whose JSON payload is {success:false, error, ...}. Covers stage-gate violations, COST_CHANGED (+ newEstimate), BRIDGE_SPEND_CAP, materialization errors, and — notably — a mid-flight key revocation surfacing from some enqueue actions as {success:false, error:"BRIDGE_AUTH_FAILED"} (still a return, not a throw). Read the error field and react; this is not a protocol failure.
  2. Auth/malformed-input failure — an isError:true tool result. Pre-flight auth failures (malformed/unknown/revoked key, rate limit) are thrown by the Convex auth check and mapped to an McpError naming BRIDGE_AUTH_FAILED explicitly, with a pointer back to Settings → Agent access. Missing the bearer token entirely fails the same way before any Convex call happens. Other thrown errors (malformed input, ownership violations) surface as isError too, with their coded message intact.

The underlying MCP SDK converts every handler throw into an isError tool result — there is no separate JSON-RPC protocol-error channel reachable from a tool handler. So in practice: readable non-error result = domain refusal you can act on; isError result = something is wrong with the call itself (bad key, bad input) and retrying the same call won't help without fixing that.

Stage preconditions map

StagePrecondition to enterUnlocked by
Script (stage 1)none (project just created)generate_script → REVIEW; approve_script → APPROVED
References (stage 2)script APPROVEDgenerate_references → entities REVIEW; approve_references → APPROVED
Storyboards (stage 3)references APPROVEDgenerate_storyboards → pages REVIEW; approve_storyboards → APPROVED (triggers materialization)
Videos (stage 4)storyboards APPROVED and materialized (video scenes visible in get_project_status)generate_videos / regenerate_scene → scenes COMPLETED; approve_videos → APPROVED
Assembly (stage 5)videos APPROVEDrender_film → poll get_film until REVIEW + videoUrl

Calling a tool before its precondition holds is a domain refusal (readable {success:false}), not a crash — get_project_status.nextActions is the fastest way to confirm what's legal right now.

5. Worked end-to-end transcript

A representative agent session (abbreviated JSON; ids/values illustrative).

> create_project({ briefText: "A lighthouse keeper discovers the tide brings back what the sea took.", aspectRatio: "16:9" })
< { projectId: "proj_abc123" }

> generate_script({ projectId: "proj_abc123", logline: "A lighthouse keeper discovers the tide brings back what the sea took.", genre: "drama", tone: "melancholic, hopeful" })
< { saved: { scriptId: "scr_1" }, screenplay: { title: "The Tide Keeper", scenes: [ /* 6 scenes */ ], characters: [...], locations: [...] } }

# Agent reviews the screenplay JSON, decides scene 3 is too on-the-nose.
> rewrite_scenes({ projectId: "proj_abc123", targets: [{ sceneNumber: 3, revisionNotes: "Cut the dialogue in half; let the silence carry it." }] })
< { applied: { scriptId: "scr_1" }, scenes: [ /* only scene 3, rewritten */ ] }

> approve_script({ scriptId: "scr_1" })
< { approved: true }

> get_project_status({ projectId: "proj_abc123" })
< { stages: { script: { status: "APPROVED" }, refs: { status: "DRAFT" }, ... },
    refs: { bibleApproved: false, characters: [{ id: "char_1", name: "Mara" }] },
    nextActions: ["set_character_face_flag (optional)", "generate_references"] }

> set_character_face_flag({ characterId: "char_1", value: true })
< { updated: true }

> get_cost_estimate({ projectId: "proj_abc123", stage: "references" })
< { total: 40, breakdown: { characters: 10, locations: 10, styleFrames: 20 } }

> generate_references({ projectId: "proj_abc123", confirmedCost: 40 })
< { bible: { created: true, renderStyle: "photoreal", renderStyleDecision: "confirmed" },
    references: { enqueued: true } }

# poll...
> get_project_status({ projectId: "proj_abc123" })
< { refs: { characters: [{ id: "char_1", status: "REVIEW" }], locations: [{ id: "loc_1", status: "REVIEW" }] } }

> approve_references({ projectId: "proj_abc123", scope: "all_pending" })
< { approved: true, stage: { approved: true } }

> get_cost_estimate({ projectId: "proj_abc123", stage: "storyboards" })
< { total: 60, breakdown: { pages: 6 } }

> generate_storyboards({ projectId: "proj_abc123", confirmedCost: 60 })
< { plan: { pages: 6, panels: 12, unresolvedNames: [] }, generation: { enqueued: true } }

# poll until pages leave REVIEW...
> approve_storyboards({ projectId: "proj_abc123", scope: "all_pending" })
< { approved: true, stage: { approved: true } }

# poll get_project_status until video scenes appear (materialization)...
> get_cost_estimate({ projectId: "proj_abc123", stage: "videos" })
< { total: 180, breakdown: { shots: 6, perShot: 30 } }

> generate_videos({ projectId: "proj_abc123", confirmedCost: 180 })
< { enqueued: true }

# poll...
> get_project_status({ projectId: "proj_abc123" })
< { video: { scenes: [
    { pageId: "pg_4", status: "FAILED", suggestedAction: "REPHRASE_PROMPT" },
    /* the other 5 COMPLETED */
  ] } }

> get_cost_estimate({ projectId: "proj_abc123", stage: "videos", scopeIds: ["pg_4"], forRegen: true })
< { total: 30 }

> regenerate_scene({ projectId: "proj_abc123", pageId: "pg_4", confirmedCost: 30, tweakPrompt: "Remove the storm — calm sea, wide shot." })
< { enqueued: true }

# poll until pg_4 is COMPLETED...
> approve_videos({ projectId: "proj_abc123" })
< { approvedCount: 6 }

> render_film({ projectId: "proj_abc123" })
< { status: "GENERATING" }

# poll get_film...
> get_film({ projectId: "proj_abc123" })
< { status: "REVIEW", videoUrl: "https://.../the-tide-keeper.mp4", durationSeconds: 96 }

6. Gotchas

  • Approving a stage exits it. After a stage-level approve you cannot regenerate that stage's entities unless an upstream edit marks the stage STALE — in that case review and re-approve.
  • Keys are shown once. If a key is lost there is no recovery: a human must revoke it and mint a new one at Settings → Agent access.
  • regenerate_scene re-charges the scene's cost on every attempt — quote with forRegen: true first.
  • Resolution is fixed at 720p through this bridge; no parameter changes it.
  • Rate limit: 60 tool calls/min per key (burst 10). Poll at 15–30 second intervals. A rate-limited call surfaces as BRIDGE_AUTH_FAILED — back off and retry the same key; do not treat it as revocation.
  • make_film's budgetCredits cannot be raised later. A run paused NEEDS_ATTENTION for budget stays paused until the blocking stage gets cheaper (fixed in the app) or you cancel_run it — there is no "increase the budget" call.
  • Gate approvals happen in the app, not the bridge. gates pauses a run WAITING_GATE; there is no MCP tool to approve a gate — a human approves the stage in the trvrse app and the run resumes on its own within a couple of minutes.