Skip to content

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 default mac_… key is minted at signup (mint more in the dashboard’s Keys console). Operator /admin provisioning remains a secondary path for special cases.

Claude Code (CLI):

Terminal window
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" }
}
}
}
ToolPurposeHints
list_pipelinesList pipelines, each with a description + example input. Call first.read-only
get_balanceReturns { available_credits } only. Prefer get_credits when you also need the ledger history.read-only
submit_jobCURRENTLY 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_jobStatus/result of a job; result.url = the video when succeeded.read-only
list_jobsRecent jobs, { data, count, has_more } (limit param).read-only
cancel_jobCancel 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).

The server advertises this same orientation in its MCP instructions:

  1. 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_sessionsend_message → poll get_session; at awaiting_gate read pending_gate (the artifact_excerpt is usually enough) → resolve_gate → repeat until closed, then get_video.
  2. Fire-and-forget batch (no approvals) — CURRENTLY UNAVAILABLE. submit_job is disabled and returns an isError {error:"batch_unavailable"} with no credits held — no worker drains the job queue today (MACARONI_BATCH_ENABLED off by default). Use the interactive session flow above for a delivered video. (When batch is enabled — MACARONI_BATCH_ENABLED=1 + a worker — the flow is submit_job → poll get_job until terminal; on succeeded read result.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 (from tools/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).

ToolPurposeHints
open_sessionHold 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_messageAdvance the session one turn (async — poll get_session).writes; open-world
get_sessionPoll 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_messagesThe readable conversation, paginated (cursor by after).read-only
resolve_gateApprove / revise a pending gate (RESUMES the session). revise without feedback → isError invalid_request (same name as REST).writes; open-world
add_budgetRaise 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_artifactRead a named artifact a gate is asking you to approve.read-only
get_videoThe 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
interruptStop 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_sessionList videos (each entry includes format; call get_video for a specific session’s delivered dimensions) · balance+ledger · close (refund unspent hold).mixed
  • status vs a failed turn. status:"failed" means the session could not open (insufficient credits; failure_reason null). A turn failure does not change status — the session stays open (or awaiting_gate) with failure_reason set. Detect a failed turn by reading failure_reason, never status.
  • pending_gate (non-null only at awaiting_gate) is { gate_id, stage, question, options, artifact_names, artifact_excerpt }, but be defensive: question and artifact_excerpt may be null and artifact_names may be empty. Every name that is listed resolves via get_artifact (no phantoms), and artifact_excerpt (when present) is a ~2KB inline preview usually enough to decide with no extra call. Branch on the gate: a stage:"budget" gate (options:["add_budget"]) is cleared with add_budget, not resolve_gate. current_stage is an advisory, free-text label (pipeline-defined, may be null or "budget") — not a fixed enum. last_heartbeat_at is null until the first ping (fall back to updated_at); once flowing it advances while the engine works, and a running turn whose heartbeat lapses for ~180s (3 min) is reaped as failure_reason:"infra_error".
  • last_reply / last_reply_stale: last_reply is the engine’s latest conversational reply (truncated; full history via get_messages). last_reply_stale:true means 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_messages returns { data:[{ id, type, data, created_at }], next_cursor, has_more }. typeturn.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.message say 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 — see data.refunded_credits) · video.ready (done). data is 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 on get_session and inside failure messages (which also carry failure_class). infra_error (system) = a transient infrastructure failure — the turn was not billed; just resend your message to retry (a stalled-turn reap also reports infra_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 a refund-type entry in get_credits; turn.no_deliverable carries data.refunded_credits). Retry; if it persists the pipeline/brief may not be supported — only animated-explainer is certified for hosted delivery. budget_exceeded (user)add_budget. user_interrupted (user) → you stopped the turn with the interrupt tool (charged the turn’s actual model spend up to the stop, never refunded).
  • Terminal sessions. closed / failed / expired are terminal. send_message / resolve_gate / add_budget on one return session_closed / session_expired / session_failed (as an isError result over MCP) — open a new session. expired is set after 72h idle (terminal, releases the unspent hold, silent — no failure_reason, no event).
  • video_url is 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 succeeds get_session / get_video / close_session return it as an absolute URL (you don’t have to wait for status:"closed"). The link is durable and shareable; the token is the credential (revocable), and the route + robots.txt mark /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 isError result with the SAME shape REST sends as 429: { "error": "rate_limited" | "concurrency_limit", "retry_after_seconds": <n>, "hint": "…" }. open_session can return rate_limited; send_message / resolve_gate can return concurrency_limit (a new turn would exceed the cap — the gate stays pending). There is no HTTP header over MCP, so read retry_after_seconds and wait that long (or poll get_session on your running sessions until one finishes), then retry. An idempotent replay (same client_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 / feedback over ~16 000 chars are rejected as an input-validation error before any turn runs (firewall-v1; full content firewall is deferred).

The live path is the interactive session — batch submit_job is currently disabled (see below):

  1. list_pipelines → pick an id and adapt its example_input. Check its hosted-delivery marker — only animated-explainer is 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.)
  2. get_balance → ensure enough credits.
  3. open_session { pipeline, brief: "…" } → hold a budget and start the session.
  4. send_message { message: "…" }, then poll get_session until status leaves running; at awaiting_gate read pending_gate (artifact_excerpt is usually enough) → resolve_gate; repeat until status:"closed".
  5. Read video_url (resolved on read as soon as a hosted render succeeds) or call get_video; a turn that finished with no hosted render reports failure_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 /mcp return 405 — there are no sessions to resume; each POST is a complete JSON-RPC exchange. The server does not emit an mcp-session-id header (there is no server-side session to key); clients must not depend on one.
  • submit_job results (batch is disabled by default). Today submit_job returns an isError result { 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 its input key returns isError { 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"), and insufficient_credits comes back as an isError with required_credits/available_credits.
  • Autopilot:
    • submit_job (batch — currently disabled, see above; this applies only when re-enabled): keep the default true (unattended end-to-end). false is not supported: the job lands in awaiting_approval with no resume path (a dead-end that holds credits). Use open_session for any reviewable flow.
    • open_session (interactive): autopilot is reserved — accepted and recorded, but approvals are manual today (no auto-approval). You resolve gates yourself with resolve_gate.
  • BYOK — no fallback. byok:true (on submit_job / open_session) uses only the provider keys you stored via the REST POST /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-null video_url / get_video.ready=true appear iff a gateway-resolvable hosted video exists. A turn that finishes without one settles as failure_reason:"no_deliverable" (never a false “closed”) and its captured credits are automatically refunded. Only animated-explainer is certified for hosted delivery today.