MCP Server
Macaroni exposes its video-generation capability to AI agents over the Model Context Protocol — the same jobs/billing engine as the REST API, as callable tools. An agent (Claude, or any MCP client) can discover pipelines, check credits, submit a job, and poll it to completion on its own.
- Endpoint:
POST https://api.macaroni.video/mcp - Transport: stateless Streamable HTTP (one request/response; no session) — the recommended transport for remote, multi-client MCP servers.
- Auth:
Authorization: Bearer <api-key>(same key as the REST API). Every call is scoped to that account; unauthenticated calls get JSON-RPC-32001. - Get a key: sign up self-serve at https://app.macaroni.video (email magic link) — your
account’s
defaultmac_…key is minted at signup (mint more in the dashboard’s Keys console). Operator/adminprovisioning remains a secondary path for special cases.
Connecting
Section titled “Connecting”Claude Code (CLI):
claude mcp add --transport http macaroni \ https://api.macaroni.video/mcp \ --header "Authorization: Bearer $MAC_KEY"Config file (Claude Desktop / other clients):
{ "mcpServers": { "macaroni": { "type": "http", "url": "https://api.macaroni.video/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } }}| Tool | Purpose | Hints |
|---|---|---|
list_pipelines | List pipelines, each with a description + example input. Call first. | read-only |
get_balance | Returns { available_credits } only. Prefer get_credits when you also need the ledger history. | read-only |
submit_job | CURRENTLY UNAVAILABLE — batch is disabled; returns an isError {error:"batch_unavailable"} (no credits held). Use open_session for a delivered video today. | writes; open-world |
get_job | Status/result of a job; result.url = the video when succeeded. | read-only |
list_jobs | Recent jobs, { data, count, has_more } (limit param). | read-only |
cancel_job | Cancel a queued/awaiting job; releases the hold. | destructive; idempotent |
Every tool carries MCP annotations (readOnlyHint/destructiveHint/
idempotentHint/openWorldHint) so clients can reason about safety, and errors are
returned as isError results with a hint and suggested next step (not protocol
errors).
Two ways to make a video — pick one
Section titled “Two ways to make a video — pick one”The server advertises this same orientation in its MCP instructions:
- Interactive session (recommended when acting for a human). A video produced through a
multi-turn conversation with approval gates — review the script/scenes, then approve or
revise.
open_session→send_message→ pollget_session; atawaiting_gatereadpending_gate(theartifact_excerptis usually enough) →resolve_gate→ repeat untilclosed, thenget_video. - Fire-and-forget batch (no approvals) — CURRENTLY UNAVAILABLE.
submit_jobis disabled and returns anisError{error:"batch_unavailable"}with no credits held — no worker drains the job queue today (MACARONI_BATCH_ENABLEDoff by default). Use the interactive session flow above for a delivered video. (When batch is enabled —MACARONI_BATCH_ENABLED=1+ a worker — the flow issubmit_job→ pollget_jobuntil terminal; onsucceededreadresult.url.)
The interactive session is the only currently-working path to a delivered video. It costs
credits (get_credits / get_balance first); costs are always quoted in credits, never dollars.
Argument shapes: each tool’s
inputSchema(fromtools/list) is the authoritative, self-describing argument contract; this page documents semantics. Field names mirror the REST bodies 1:1 (e.g.additional_credits,budget_credits,feedback).
v2 interactive sessions (conversational flow)
Section titled “v2 interactive sessions (conversational flow)”REST ⇄ MCP are 1:1 (the same shapes back both).
| Tool | Purpose | Hints |
|---|---|---|
open_session | Hold a credit budget + start an interactive session. Optional format fixes the output aspect ratio ("16:9" default | "9:16" vertical — TikTok/Reels/Shorts; the only two certified today), immutable for the session. | writes; open-world |
send_message | Advance the session one turn (async — poll get_session). | writes; open-world |
get_session | Poll target. status, current_stage, credits, pending_gate, last_reply, last_reply_stale, failure_reason, last_heartbeat_at, video_url, format (the ordered aspect ratio, or null). | read-only |
get_messages | The readable conversation, paginated (cursor by after). | read-only |
resolve_gate | Approve / revise a pending gate (RESUMES the session). revise without feedback → isError invalid_request (same name as REST). | writes; open-world |
add_budget | Raise the session budget (clears a budget gate). Args: session_id, additional_credits (int), client_token. Resume is NOT automatic — follow with send_message (e.g. “continue”). Requires client_token (idempotency key) — reuse it to retry a top-up once; the tool schema itself requires it (a keyless call fails argument validation). | writes; idempotent |
get_artifact | Read a named artifact a gate is asking you to approve. | read-only |
get_video | The finished share link for a session, plus the delivered format + actual width/height/aspect_ratio (derived, e.g. "9:16") so you can route the file to the right platform without probing it. | read-only |
interrupt | Stop button. Stop the running turn (terminates its sandbox); the turn settles as failure_reason:"user_interrupted" and the session returns to open. Because you chose to stop, it is billed its actual model spend up to the stop (standard markup, capped at the remaining budget) — a user-caused stop, never refunded; the rest of the hold is untouched. Safe + idempotent (no running turn = no-op). Returns the poll shape + interrupted. | destructive; idempotent |
list_videos / get_credits / close_session | List videos (each entry includes format; call get_video for a specific session’s delivered dimensions) · balance+ledger · close (refund unspent hold). | mixed |
statusvs a failed turn.status:"failed"means the session could not open (insufficient credits;failure_reasonnull). A turn failure does not changestatus— the session staysopen(orawaiting_gate) withfailure_reasonset. Detect a failed turn by readingfailure_reason, neverstatus.pending_gate(non-null only atawaiting_gate) is{ gate_id, stage, question, options, artifact_names, artifact_excerpt }, but be defensive:questionandartifact_excerptmay be null andartifact_namesmay be empty. Every name that is listed resolves viaget_artifact(no phantoms), andartifact_excerpt(when present) is a ~2KB inline preview usually enough to decide with no extra call. Branch on the gate: astage:"budget"gate (options:["add_budget"]) is cleared withadd_budget, notresolve_gate.current_stageis an advisory, free-text label (pipeline-defined, may be null or"budget") — not a fixed enum.last_heartbeat_atis null until the first ping (fall back toupdated_at); once flowing it advances while the engine works, and a running turn whose heartbeat lapses for ~180s (3 min) is reaped asfailure_reason:"infra_error".last_reply/last_reply_stale:last_replyis the engine’s latest conversational reply (truncated; full history viaget_messages).last_reply_stale:truemeans it is not from the latest turn (e.g. the latest turn ended in a gate or failure) — don’t treat it as this turn’s answer; it is nulled on a failed turn so a failure and a stale success reply are never shown together.get_messagesreturns{ data:[{ id, type, data, created_at }], next_cursor, has_more }.type∈turn.start(your message) ·turn.reply(engine reply) ·gate(decision needed) ·budget.exceeded/turn.error/turn.reaped/turn.interrupted/turn.no_deliverable(typed failure —data.failure_reason+data.failure_class("user"|"system") +data.messagesay WHY;turn.interrupted= you stopped the turn (failure_reason:"user_interrupted", charged the turn’s actual model spend up to the stop);turn.no_deliverable= finished but produced no hosted video, captured credits auto-refunded — seedata.refunded_credits) ·video.ready(done).datais a decoded JSON object.failure_reason(typed, latest turn). Every reason has a class that decides billing: a system-caused failure auto-refunds the turn’s captured credits; a user-caused failure is billed for real and never refunded. Emitted today:infra_error·engine_error·no_deliverable(all system) ·budget_exceeded·user_interrupted(both user). Present onget_sessionand inside failure messages (which also carryfailure_class).infra_error(system) = a transient infrastructure failure — the turn was not billed; just resend your message to retry (a stalled-turn reap also reportsinfra_error).engine_error(system) → the engine erred mid-turn; its captured credits are automatically refunded.no_deliverable(system) → the turn finished but produced no hosted, deliverable video, so its captured credits are automatically refunded (you are not charged for a video you never received — the refund shows as arefund-type entry inget_credits;turn.no_deliverablecarriesdata.refunded_credits). Retry; if it persists the pipeline/brief may not be supported — onlyanimated-explaineris certified for hosted delivery.budget_exceeded(user) →add_budget.user_interrupted(user) → you stopped the turn with theinterrupttool (charged the turn’s actual model spend up to the stop, never refunded).- Terminal sessions.
closed/failed/expiredare terminal.send_message/resolve_gate/add_budgeton one returnsession_closed/session_expired/session_failed(as anisErrorresult over MCP) — open a new session.expiredis set after 72h idle (terminal, releases the unspent hold, silent — nofailure_reason, no event). video_urlis a stable, unguessable, no-index gateway link (/v1/videos/share/<token>) that streams the MP4 bytes through the gateway (range requests supported for seeking) — the underlying storage is never exposed. It is resolved on read: as soon as a render succeedsget_session/get_video/close_sessionreturn it as an absolute URL (you don’t have to wait forstatus:"closed"). The link is durable and shareable; the token is the credential (revocable), and the route +robots.txtmark/v1/videos/noindex, nofollow.- Rate & concurrency limits (back off on the typed error). Per-account ceilings bound volume so
a key can’t flood the backend (cost/DoS gate): a concurrent-turn cap (default 3 turns/
sandboxes at once) and a session-creation rate limit (default 10/min, 100/hour). When
a limit is hit the tool returns an
isErrorresult with the SAME shape REST sends as429:{ "error": "rate_limited" | "concurrency_limit", "retry_after_seconds": <n>, "hint": "…" }.open_sessioncan returnrate_limited;send_message/resolve_gatecan returnconcurrency_limit(a new turn would exceed the cap — the gate stays pending). There is no HTTP header over MCP, so readretry_after_secondsand wait that long (or pollget_sessionon your running sessions until one finishes), then retry. An idempotent replay (sameclient_token, or a send while a turn already runs) is never limited. Defaults are generous and env/per-account tunable (see API.md). Length cap:brief/message/feedbackover ~16 000 chars are rejected as an input-validation error before any turn runs (firewall-v1; full content firewall is deferred).
How an agent uses it (typical flow)
Section titled “How an agent uses it (typical flow)”The live path is the interactive session — batch submit_job is currently disabled (see below):
list_pipelines→ pick anidand adapt itsexample_input. Check its hosted-delivery marker — onlyanimated-explaineris certified to return a hosted video today. (A session takes only free text, so for a source-required pipeline put the source URL inside the brief.)get_balance→ ensure enough credits.open_session{ pipeline, brief: "…" }→ hold a budget and start the session.send_message{ message: "…" }, then pollget_sessionuntilstatusleavesrunning; atawaiting_gatereadpending_gate(artifact_excerptis usually enough) →resolve_gate; repeat untilstatus:"closed".- Read
video_url(resolved on read as soon as a hosted render succeeds) or callget_video; a turn that finished with no hosted render reportsfailure_reason:"no_deliverable"and its credits are auto-refunded (you are not charged for a non-delivery).
Batch (submit_job) is CURRENTLY UNAVAILABLE. It returns an isError result
{ error:"batch_unavailable", hint:… } with no credits held — no worker drains the job queue
today (MACARONI_BATCH_ENABLED is off by default; re-enabling needs the flag and a worker). Do
not build an agent around submit_job / get_job — use the session flow above. (When enabled,
the batch flow is submit_job → poll get_job → on succeeded read result.url; a source-required
pipeline submitted without its input key is rejected up-front with {error:"missing_required_input"}
and no credit hold.)
The submit_job tool description now leads with the batch_unavailable notice (the async +
credits model that follows applies only when batch is re-enabled), so an agent is steered to the
session flow.
- Stateless:
GET/DELETE /mcpreturn405— there are no sessions to resume; eachPOSTis a complete JSON-RPC exchange. The server does not emit anmcp-session-idheader (there is no server-side session to key); clients must not depend on one. submit_jobresults (batch is disabled by default). Todaysubmit_jobreturns anisErrorresult{ error:"batch_unavailable", hint:… }and holds no credits — this is checked first, before anything else. When batch is enabled (MACARONI_BATCH_ENABLED=1+ a worker): a source-required pipeline submitted without itsinputkey returnsisError{ error:"missing_required_input", missing:[…], hint:… }(rejected up-front, no hold); otherwise the submit places a credit hold and returns the job view (status:"queued"), andinsufficient_creditscomes back as anisErrorwithrequired_credits/available_credits.- Autopilot:
submit_job(batch — currently disabled, see above; this applies only when re-enabled): keep the defaulttrue(unattended end-to-end).falseis not supported: the job lands inawaiting_approvalwith no resume path (a dead-end that holds credits). Useopen_sessionfor any reviewable flow.open_session(interactive):autopilotis reserved — accepted and recorded, but approvals are manual today (no auto-approval). You resolve gates yourself withresolve_gate.
- BYOK — no fallback.
byok:true(onsubmit_job/open_session) uses only the provider keys you stored via the RESTPOST /v1/providers/keys— there is no fallback to platform keys, so store one for every provider the pipeline needs or the run fails. Exception (the brain): a BYOK run that stores no Anthropic key still runs the reasoning model on the platform’s metered key, so the LLM line is still charged. There is no MCP tool to manage keys — storing them is a REST-only operation. - Hosted delivery is truthful.
video.ready/ a non-nullvideo_url/get_video.ready=trueappear iff a gateway-resolvable hosted video exists. A turn that finishes without one settles asfailure_reason:"no_deliverable"(never a false “closed”) and its captured credits are automatically refunded. Onlyanimated-explaineris certified for hosted delivery today.