MCP Tool Reference
Everything needed to build a client against the StudioCut.Video MCP server — the endpoint, the twelve tools, the order they have to be called in, the two kinds of error, and every limit you will meet.
This is the reference for building a client. If you just want to connect Claude Desktop and get on with it, read Connect Your AI Assistant instead — it takes about five minutes. It is one of the guides in the StudioCut.Video Help Center.
The endpoint
POST https://app.studiocut.video/mcp
Authorization: Bearer <your 40-character key>
Content-Type: application/json
One endpoint, JSON-RPC 2.0 over streamable HTTP, implementing MCP revision 2025-11-25 (and accepting 2025-06-18). POST only — there is no GET, no DELETE, and no SSE session flow.
The endpoint is stateless. Every request re-authenticates; no cookie or session id is kept. There is no session to establish, resume or tear down, and the bearer token is required on every request including initialize.
Authentication
Only a key generated on the MCP and API Keys page works. A general-purpose Odoo backend key — including a wildcard key with no scope — is refused. Do not tell your users to paste one from the backend's "New API Key" button; it will always fail.
An authentication failure returns HTTP 401, a WWW-Authenticate: Bearer challenge, and an empty body — no JSON-RPC envelope. The response is byte-identical for an unknown key, an expired key, a wrong-scope key and a valid key whose account was archived. That is deliberate: telling them apart would tell an attacker which keys exist. Your client cannot distinguish them either, so surface one actionable message — "StudioCut rejected the key — generate a new one on the MCP and API Keys page."
Keys live at most 30 days, so handle expiry as a normal event, not an exception. Rotation is the supported way to replace one without downtime: the predecessor keeps authenticating for a grace period (24 hours by default) while you update your configuration.
Protocol methods
Four methods are dispatched. Anything else returns -32601 Method not found.
| Method | Notes |
|---|---|
initialize | Echoes your protocolVersion when it is a supported revision, otherwise replies with 2025-11-25 and leaves the decision to you |
ping | Returns an empty result object |
notifications/initialized | Any request without an id is a notification. It gets HTTP 202 and a zero-length body — a client that waits for a response body here will hang |
tools/list | Returns {"tools":[{"name","description","inputSchema"}, …]}. No pagination — nextCursor is never emitted, so do not write a paging loop |
tools/call | arguments is optional and defaults to {} |
Two things about tools/list that shape a client:
- Schemas are resolved per request, so enum values track the live catalogue. If a visual style is retired it disappears from the relevant enum with no deploy and no version bump. Read the schemas rather than hard-coding the vocabulary, and expect enum contents — not shapes — to drift between sessions.
listChangedisfalse. An administrator can disable a tool, but no notification is sent, so re-read the list at the start of each session rather than caching it indefinitely.
Results arrive as MCP content blocks with the tool's return value JSON-encoded inside a text block. Parse content[0].text as JSON to get the documented result object; every tool returns an object, never a bare array or scalar.
The twelve tools
tools/list returns thirteen entries — these twelve plus a server_info tool from the transport layer.
| Tool | Writes | Spends | Purpose |
|---|---|---|---|
list_design_options | — | — | The accepted design vocabulary, and the character caps on custom free-text fields |
list_productions | — | — | The account's productions, filterable |
get_production | — | — | One production's settings |
get_production_status | — | — | Where it is and what to do next — the polling tool |
get_storyboard | — | — | The Phase-1 plan, so a human can approve on evidence |
get_balance | — | — | What this credential can spend, its cap and the spend so far |
get_download_url | — | — | A link to the finished video |
validate_productions | — | — | Dry run; creates nothing |
create_productions | ✓ | — | Creates drafts, free |
estimate_cost | — | — | Price, breakdown and a confirm_token |
start_production | ✓ | ✓ | Starts the pipeline — the only tool that spends |
approve_phase1 | ✓ | — | Approves the storyboard; the point of no return |
Call get_balance before planning a batch. "Insufficient credits" is only actionable if it arrives before your agent has designed twenty videos. It also reports separately any balance that exists but cannot be spent through this channel, so a wallet that looks empty is explained rather than mysterious.
The canonical flow
list_design_options learn the vocabulary
↓
validate_productions dry run; fix errors; show the user `normalized`
↓
create_productions drafts; free; capture the returned ids
↓
estimate_cost price + confirm_token (5 min TTL)
↓
── SHOW THE USER THE PRICE, GET AGREEMENT ──
↓
start_production spends credits; needs the confirm_token
↓
get_production_status poll, ≥ 20 s apart, until next_action == "approve_phase1"
↓
── SHOW THE USER THE STORYBOARD, GET AGREEMENT ──
↓
approve_phase1 POINT OF NO RETURN; phases 2-5 run unattended
↓
get_production_status poll, ≥ 20 s apart, until next_action == "none"
↓
get_download_url give the link to the user to open
The two human checkpoints are marked, and both exist because the step after them cannot be undone. Design the integration around that gate rather than against it — it is the main thing standing between a looping agent and a month of someone's credits.
The production payload
validate_productions and create_productions take the same payload, in any of four shapes: an array of objects, a single object, a wrapper object {"productions": [...], "brand_kit": {...}}, or any of those as a JSON string — so a model that emits "here is my JSON" as text works as well as a properly decoded structure.
Only two fields are required per item: name (≤ 500 characters) and input_content (≤ 50 000 characters). Everything else is optional. Human-friendly keys are aliased — video_title, title and name are the same field, and keys are matched case-insensitively with whitespace and hyphens collapsed to underscores.
Two behaviours worth building around:
- Off-list values are redirected, not rejected. An unrecognised visual style becomes a custom style rather than failing the item. In
validate_productions,errorsinvalidate an item butwarningsnever do — the offending field is dropped and the server default applies. normalizedis exactly what would be created. Show it to the user when confirming a batch; it is the record after aliasing, brand-kit folding and coercion.
Extra languages are separate charged videos. A production with five extra languages costs six videos, not one. The ceiling is five extra languages, counted after de-duplication, and a longer list is refused outright — named, not silently truncated — in the dry run as well as at create time.
The error model
Getting this distinction right is the single most important part of the integration.
Protocol errors — fix the client
| Code | Meaning |
|---|---|
-32700 | Parse error — the body was not valid JSON |
-32600 | Invalid request — not a JSON-RPC object, or a batch array, which this revision does not support |
-32601 | Method not found |
-32602 | Invalid params, or an unknown/disabled tool name — re-read tools/list |
-32603 | Internal error — retry once with backoff, then report |
Tool errors — hand them to the model
A tool that runs and fails is a JSON-RPC success carrying isError: true. Everything after "does this tool exist" is a tool error: a bad argument, a rate limit, a spend cap, an insufficient balance, a production in the wrong state.
The message text is written to be read by a language model and acted on — it names the current state, the shortfall, or the next call to make. Pass it through verbatim. Do not replace it with a generic "tool failed", and do not treat it as a transport fault and tear down the session.
Tool errors also carry a machine-readable reason_code and a details object alongside the same sentence, so an agent branching on insufficient credits versus storage full is not pattern-matching English. Branch on the code where your agent should behave differently — retrying a rate limit makes sense, retrying an insufficient balance does not.
Transport codes: auth_failed, unknown_tool, tool_unavailable, rate_limited, cap_exceeded, schema_unavailable, invalid_arguments, user_error, internal_error.
Domain codes, with what details carries:
| Code | details |
|---|---|
insufficient_credits | What is required, what is spendable, and a top-up link |
key_credit_cap_reached | The cap, the period, the spend so far, this call's quote, and the reset time |
storage_full | Used, limit, needed, freeable, and how many productions could be purged |
concurrency_held | The limit, the count, and references of the productions holding the slots |
quote_required | No usable quote — call estimate_cost |
quote_expired | The token is past its five-minute life |
quote_stale | The token is valid but the price moved — re-quote and re-ask the user |
already_started | The reference, state and working state |
production_expired | The files were auto-purged; when, and what remains |
plan_entitlement_required | The entitlement the account lacks |
content_rejected | The production was refused by the content policy |
voice_language_incompatible | The voice and language that do not go together |
too_many_languages | What was requested against the five-language ceiling |
queued | The start was accepted but is waiting behind other work — this is success, not failure |
Treat an unrecognised reason_code as an internal error and fall back to the sentence. The list grows; the sentence is always there.
HTTP-level responses
| Status | Meaning |
|---|---|
200 | A JSON-RPC response — success, tool error, or protocol error |
202 | Notification acknowledged, empty body |
401 | Authentication failed, empty body |
413 | Request body over 1 MB |
Limits and caps
| Limit | Default | Scope |
|---|---|---|
| Rate limit | 120 calls/hour | Per credential per tool, rolling hour |
| Concurrent productions | 1 | Per credential |
| Credit cap | Set per key; unset means no per-key ceiling | Per credential, per day/week/month |
| Items per validate/create call | 20 | Per call |
| Payload characters | 2 097 152 | Per validate/create call |
| HTTP body | 1 MB | Per request |
| Extra languages | 5 | Per production |
| Quote lifetime | 300 s | Per confirm_token |
| Key lifetime | ≤ 30 days | Per key |
Notes that matter when you build against these:
- The rate limit is per tool, not global. 120 status polls do not consume your listing budget. It is a rolling hour, and the error tells you exactly how many seconds to wait — honour that number.
- The credit cap is evaluated inclusive of the call in front of it. A start whose own quote would breach the cap is refused before any money moves, rather than admitted and discovered afterwards.
get_balancereports the cap, the period and the spend so far, so an agent can pace itself. - Concurrency of 1 means one production at a time per key, and a storyboard awaiting approval holds the slot. That is intended pressure: the alternative is an agent starting a second paid production while the first is still unreviewed.
- Administrators can change the rate limit and the caps, and can disable any tool. Treat every number here as a default you discover at runtime, and treat a missing tool in
tools/listas normal rather than a fault.
Pricing through this channel
Assistant-driven production spends the account's plan credits and purchased credits. Drafting, validating, quoting, polling, reading a storyboard and fetching a download link are all free; the single charge lands when the production starts.
Two things differ from the web app, and both are in the customer's favour to know up front:
- Plan feature gates do not apply to work started through this channel, and it does not consume the account's monthly app allowance. Storage limits, content policy, voice/language compatibility and the five-language ceiling still apply on every channel.
- A 12.5% premium applies when both the quality tier is 2 or higher and the target duration is over 30 seconds. Below either threshold the price is identical to the app.
estimate_cost's breakdown reports channel, surcharge_percent and surcharge_tokens as separate fields, so you can show the user the premium as its own line rather than as an unexplained difference. Show the total before you call start_production — that is the whole point of the confirm token.
Ownership and scoping
Each key is bound at creation to either the user's personal account or one specific agency — never both, and never "whichever the user is currently looking at". The binding is immutable for the life of the key.
Everything follows from that binding: listings, reads, creates and spend all resolve against the bound context. There is no way to switch context at call time and no argument that widens scope. A user who needs both creates two keys.
For your integration this means a key is a complete, self-describing context. You never need to send an account or agency identifier, and you should not build UI implying the user can switch. If a key stops working after an agency membership changes, the correct fix is a new key.
Retention — output may be auto-purged
A customer can turn on automatic deletion of the output produced through this API, after a retention window they choose. It affects what your client can promise:
- the clock starts when a video download link is issued — nothing else starts it, and a thumbnail preview does not;
- output that was never downloaded is never auto-deleted;
- deletion removes the files, not the record —
get_productionkeeps working and reports the production as expired, with its name, cost and dates intact, whileget_download_urlreturnsproduction_expiredrather than pretending the video is still coming; - the customer can mark any production Keep to exempt it, and can change or switch off the window at any time.
Treat production_expired as final. If a user may want a video later, tell them to download it or mark it Keep.
Not available today
- OAuth 2.1. Authentication is bearer API keys only. Nothing to migrate yet, but expect the key path to be the layer OAuth issues into.
- Publishing. No tool pushes to YouTube or any other platform.
- Brand-preset creation. A
brand_kitstyles a batch; it does not save a reusable preset. - Server-initiated notifications. No resources, prompts, sampling or progress notifications. Tools only.
- Batch JSON-RPC. One request object per HTTP request.
- A REST API. MCP is the only programmatic route today.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
HTTP 0, empty body, TypeError: Failed to fetch — browser clients only | The endpoint sends no CORS headers, so a cross-origin POST is stopped at the preflight and never sent. Nothing appears in any log because nothing arrived | Not a server fault and not fixable by changing the URL or key. Proxy through your own origin, or call the endpoint server-side. The test console ships a CORS shim for this |
401, empty body | Key unknown, expired, wrong kind, or its account archived — indistinguishable by design | Generate a new key on the MCP and API Keys page, or rotate the existing one before it expires. Confirm it did not come from the Odoo backend |
-32602 Unknown or archived tool | Cached tool list; an administrator disabled the tool | Re-read tools/list each session |
-32600 on an array body | Batching is not part of this MCP revision | Send one JSON-RPC object per request |
Client hangs after notifications/initialized | Notifications get 202 with an empty body | Do not wait for a response body on id-less requests |
413 | Body over 1 MB | Split the batch; the 20-item cap usually keeps you well under |
| "A valid confirm_token … is required" | No token, expired past five minutes, or issued for another production or user | Call estimate_cost again, re-confirm with the user, then start |
| "The cost of this production changed…" | The price rose between quote and start | Re-quote, show the new figure, get fresh agreement |
| "has already been started" | Duplicate start_production | Use get_production_status; a queued result is already success |
| Production seems stuck at phase 1 | It is waiting for storyboard approval — the one human gate | next_action says approve_phase1. A human must decide |
| Rate-limit errors while polling | The poll loop is tighter than 30 seconds | Back off to 20 seconds or more and honour the retry figure in the error |
| A download URL 403s from your server | It is signed for browser access only | Give the link to the user; never fetch it server-side |
| Enum values differ between sessions | Schemas are generated live from the catalogue | Read inputSchema; do not hard-code the vocabulary |
The test console
We publish a standalone browser console that exercises every method and tool on this page against a live server: StudioCut.Video_MCP-Tester. It runs locally with no build step and ships a CORS shim, so it is the fastest way to learn the flow — and the fastest way to reproduce a wire-level problem before you go digging in your own client.
See also
- Connect Your AI Assistant — the five-minute setup path for people using an off-the-shelf client.
- AI Assistants (MCP) — what the integration is and why it is shaped this way.
- Five things people build with it — worked examples end to end.
- Multi-Language Production — what an extra language actually produces, and why each one is a separate charge.
- Security — how credentials, tenancy and spend tracking are handled.