# 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](https://www.trvrse.com/settings/agent) · 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](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

```bash
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:

```json
{
  "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:

```bash
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:

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

Event `type`s: `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:

```js
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).

| # | Tool | One-liner |
|---|------|-----------|
| 1 | `create_project` | Start a new film: brief text, aspect ratio, optional render style/video model. Free. |
| 2 | `get_project_status` | THE polling workhorse — all 5 stage statuses, entity statuses, `nextActions` hints. Free. |
| 3 | `get_cost_estimate` | Quote a stage in credits before spending it. |
| 4 | `generate_script` | Stage 1: LLM screenplay from a brief. Synchronous, ~30–60s. Free. |
| 5 | `rewrite_scenes` | Stage 1: targeted LLM rewrite of specific scenes. Synchronous, ~30–60s. Free. |
| 6 | `approve_script` | REVIEW → APPROVED, unlocks stage 2. |
| 7 | `set_character_face_flag` | Mark a character as a real human face before generating references (face-trust routing). |
| 8 | `generate_references` | Stage 2: character sheets + location plates (+ visual bible if missing). Async. |
| 9 | `approve_references` | Approve entities (`"all_pending"` or explicit ids), then the stage. |
| 10 | `generate_storyboards` | Stage 3: storyboard page images (+ panel plan if missing). Async. |
| 11 | `approve_storyboards` | Approve pages, then the stage — triggers materialization into video scenes. |
| 12 | `generate_videos` | Stage 4: generate a video clip per scene. Async, 720p only. |
| 13 | `regenerate_scene` | Stage 4: regenerate ONE scene (after a failure or content-safety rejection). Async. |
| 14 | `approve_videos` | Approve stage 4 — unlocks assembly. Requires every live scene COMPLETED. |
| 15 | `render_film` | Stage 5: assemble + render. Idempotent settle-first — never silently re-renders. |
| 16 | `get_film` | Assembly readout: final `videoUrl` + duration. Free. |
| 17 | `list_projects` | Connection sanity check + project discovery — your cinema projects with per-stage statuses. Use `get_project_status` for detail. Free. |
| 18 | `make_film` | Autopilot: one call, full pipeline (script → references → storyboards → videos → assembly), unattended by default. See §3 above. |
| 19 | `get_run` | Autopilot's polling workhorse — status, current stage, spend, events, `nextActions` for a run made with `make_film`. Free. |
| 20 | `cancel_run` | Stop 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

| Stage | Precondition to enter | Unlocked by |
|---|---|---|
| Script (stage 1) | none (project just created) | `generate_script` → REVIEW; `approve_script` → APPROVED |
| References (stage 2) | script APPROVED | `generate_references` → entities REVIEW; `approve_references` → APPROVED |
| Storyboards (stage 3) | references APPROVED | `generate_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 APPROVED | `render_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.
