FFmpeg MCP Server
The Xora FFmpeg MCP server gives an AI agent real FFmpeg as tool calls. The agent writes the FFmpeg arguments (or picks a recipe), Xora runs the job in the cloud, and the agent gets a signed URL for the output. Nothing needs to be installed where the agent runs.
The server is a remote MCP server at
https://api.xora.sh/v1/mcp (Streamable HTTP). It authenticates with the same
API keys as the REST API and its tools call the same job routes — plan limits,
billing, and structured errors behave identically whether a job comes from
curl or from an agent’s tool call. For an overview of every way an agent can
reach Xora, see FFmpeg for AI agents.
Connect
Section titled “Connect”Two ways to authenticate:
- OAuth — clients with OAuth support (claude.ai custom connectors, Claude Desktop connectors) discover the flow automatically: add the endpoint URL, sign in with your Xora dashboard account, and approve the consent prompt.
- API key — an
Authorization: Bearerheader carrying a Xora API key (create one in the dashboard), for headless agents, CI, and any client that can set a request header.
Settings → Connectors → Add custom connector, then paste:
https://api.xora.sh/v1/mcpThe OAuth flow starts automatically: you’ll sign in to your Xora dashboard (if you aren’t already) and approve the access prompt. No key to copy. Revoke any time from the consent screen’s dashboard link.
claude mcp add --transport http xora https://api.xora.sh/v1/mcp \ --header "Authorization: Bearer YOUR_API_KEY"Add to ~/.cursor/mcp.json (or .cursor/mcp.json in your project):
{ "mcpServers": { "xora": { "url": "https://api.xora.sh/v1/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } }}import { Client } from '@modelcontextprotocol/sdk/client/index.js';import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const transport = new StreamableHTTPClientTransport( new URL('https://api.xora.sh/v1/mcp'), { requestInit: { headers: { authorization: `Bearer ${process.env.XORA_API_KEY}` } } });const client = new Client({ name: 'my-agent', version: '1.0.0' });await client.connect(transport);
const { tools } = await client.listTools();// create_upload, create_job, get_job, list_recipes, cancel_jobconsole.log(tools.map((t) => t.name));To verify a connection without writing code:
npx @modelcontextprotocol/inspector --cli https://api.xora.sh/v1/mcp \ --transport http \ --header "Authorization: Bearer YOUR_API_KEY" \ --method tools/list| Tool | What it does |
|---|---|
create_job | Create an asynchronous job — mode: "recipe" (named operation), mode: "ffmpeg" (raw argument array with {{in_1}}/{{out_1}} placeholders), or mode: "probe" (ffprobe metadata only). Returns { id, state } immediately. |
get_job | Fetch a job by id: state, progress, signed download URLs when completed, probe metadata for probe jobs, structured error for failed or rejected jobs. |
create_upload | Get a presigned URL for a local file that has no public address. Returns { uploadId, url, expiresAt, maxBytes }; the agent PUTs the bytes, then passes uploadId to create_job. |
list_recipes | Static catalog of the nine recipes with parameters, constraints, and output formats. Costs nothing to call. |
cancel_job | Cancel a queued or running job. |
The server also publishes instructions during the MCP handshake, so most agents
discover the workflow on their own: call list_recipes once, create_job,
then poll get_job until the state is terminal
(completed | failed | rejected | cancelled).
Working with local files
Section titled “Working with local files”create_job needs an input Xora can read. When the agent is holding a file
path rather than a URL, create_upload provides the destination:
- Call
create_uploadwith{ "sizeBytes": 734003200, "filename": "interview.mov" }. The declared size is checked against the plan’s per-file limit up front. PUTthe raw file bytes to the returnedurl— a plain HTTP request the agent makes itself, with noAuthorizationheader and no form encoding. The URL is valid for one hour.- Call
create_jobwith"input": { "uploadId": "…" }in place of"input": { "url": … }.
Uploads survive for three days and can back any number of jobs in that window, so an agent producing several outputs from one source should upload once and reuse the id. See direct uploads for limits and the REST equivalent.
The agent loop
Section titled “The agent loop”A typical create_job tool call:
{ "mode": "recipe", "input": { "url": "https://example.com/interview.mov" }, "output": { "format": "mp4" }, "recipe": { "name": "compress", "crf": 23 }}…returns { "id": "01JXYZ…", "state": "queued" }. Polling get_job with that
id eventually returns the terminal record:
{ "id": "01JXYZ1234ABCDEF56789000", "state": "completed", "progress": 100, "output": { "signedUrl": "https://cdn.xora.sh/...signed-url..." }}Signed URLs expire, so agents should download promptly (or configure
BYOB delivery so outputs land in your own
bucket with nothing to collect). Pass webhookUrl on create_job to receive a
webhook on the terminal state instead of polling.
Errors agents can branch on
Section titled “Errors agents can branch on”Tool failures come back as MCP error results whose text is the API’s structured error JSON, plus the HTTP status:
{ "httpStatus": 402, "error": { "code": "LIMIT_EXCEEDED", "message": "Plan credits exhausted", "feature": "credits", "retryable": false }}error.code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | The request body is wrong — the message says which field. |
LIMIT_EXCEEDED | 402 | Plan credits or storage exhausted; new jobs are refused until reset or upgrade. |
PLAN_REQUIRED | 403 | The feature (for example BYOB) isn’t on the current plan. |
NOT_FOUND | 404 | No job with that id on this account. |
Failed and rejected jobs bill nothing — see error handling for the difference between the two.
The Xora skill
Section titled “The Xora skill”For Claude specifically, there’s also an Agent Skill
— judgment the tool schemas can’t carry: which recipe fits which ask, when to
probe first, how to handle a local file, cost-saving habits like
stageInput: false, and how to branch on error codes. It complements the MCP
server (and covers plain REST when MCP isn’t connected).
Install it for Claude Code:
mkdir -p ~/.claude/skills/xora && \ curl -fsSL https://xora.sh/skills/xora/SKILL.md -o ~/.claude/skills/xora/SKILL.mdUse .claude/skills/xora/SKILL.md inside a repo to share it with a project
instead, or upload it as a custom skill in claude.ai.
Step-by-step tutorials
Section titled “Step-by-step tutorials”- How to give Claude FFmpeg with an MCP server — Claude Code, claude.ai and Claude Desktop.
- Run FFmpeg jobs from Cursor with MCP — set up
mcp.json, then turn a tested job into REST code.
Can the agent run any FFmpeg command?
Section titled “Can the agent run any FFmpeg command?”Yes. create_job with mode: "ffmpeg" takes a raw argument array, including
-filter_complex and multiple inputs (input_files keyed in_1, in_2, …).
Output file names in output_files use letters, numbers and underscores plus
an extension, for example clip_720p.mp4. See
custom FFmpeg args.
Which clients work with the Xora MCP server?
Section titled “Which clients work with the Xora MCP server?”Any client that supports remote MCP servers over Streamable HTTP. The tabs above show Claude Code, claude.ai and Claude Desktop, Cursor, and the MCP TypeScript SDK.
Is there a local (stdio) version?
Section titled “Is there a local (stdio) version?”No local server is needed: the agent only sends JSON, and FFmpeg runs on Xora.
Clients that cannot connect to a remote server can call the
REST API directly. /llms.txt describes the full
workflow for agents.
Do MCP jobs cost more than REST jobs?
Section titled “Do MCP jobs cost more than REST jobs?”No. A tool call creates the same job as POST /v1/jobs and bills the same
credits. Failed, rejected and cancelled jobs cost nothing. See
pricing.