All posts
cursor ffmpeg

Run FFmpeg Jobs from Cursor with MCP

Add the Xora FFmpeg MCP server to Cursor with one mcp.json entry. The agent writes FFmpeg args, runs them in the cloud, and turns working jobs into code.

To run FFmpeg from Cursor, add the Xora MCP server to .cursor/mcp.json. Cursor’s agent can then send FFmpeg jobs to the cloud as tool calls, poll them, and give you the output URL. Nothing runs on your machine.

The setup is one JSON entry. The useful part comes after: because an MCP job and a REST job are the same thing, the agent can test a job in chat and then write the code that runs it in production.

Step 1: Get an API key

Sign up and create an API key in the dashboard. The Free plan needs no card.

Step 2: Add the server to Cursor

Add Xora to mcp.json. Use ~/.cursor/mcp.json to make it available in every project, or .cursor/mcp.json in one project.

~/.cursor/mcp.json
{
"mcpServers": {
"xora": {
"url": "https://api.xora.sh/v1/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}

The file contains your key. If you use the project file, add it to .gitignore before you commit.

Open Cursor’s settings and find the MCP section. The xora server shows five tools: create_job, get_job, create_upload, list_recipes and cancel_job. If it shows an error, check the key and the Bearer prefix.

Step 3: Run a job from the agent

Open the agent and ask for a media task. Give it a URL, or a path in the repository:

Get a JPG thumbnail at 5 seconds from https://example.com/demo.mp4.

The agent calls list_recipes, picks the thumbnail recipe, and sends:

create_job
{
"mode": "recipe",
"input": { "url": "https://example.com/demo.mp4" },
"output": { "format": "jpg" },
"recipe": { "name": "thumbnail", "seekSeconds": 5 },
"stageInput": false
}

stageInput: false lets FFmpeg read only the byte ranges it needs from the source, in place of a full download first. That makes a thumbnail from a large file a small job. See input staging.

The agent then polls get_job until the state is completed and shows you output.signedUrl.

For a file in the repository, such as fixtures/clip.mov, the agent calls create_upload, sends the bytes with one PUT, and passes the uploadId to create_job in place of a URL.

Step 4: Turn the job into code

This is where Cursor and MCP fit well together. When a job does what you want, ask the agent:

Write a TypeScript function that runs this same job with the Xora REST API.

The JSON body does not change. Only the transport changes, from a tool call to fetch:

src/media/thumbnail.ts
const XORA_API = 'https://api.xora.sh/v1'
type Job = {
id: string
state: string
output?: { signedUrl?: string }
error?: { code: string; message: string }
}
export async function thumbnail(videoUrl: string, seekSeconds: number): Promise<string> {
const headers = {
Authorization: `Bearer ${process.env.XORA_API_KEY}`,
'Content-Type': 'application/json',
}
const created = await fetch(`${XORA_API}/jobs`, {
method: 'POST',
headers,
body: JSON.stringify({
mode: 'recipe',
input: { url: videoUrl },
output: { format: 'jpg' },
recipe: { name: 'thumbnail', seekSeconds },
stageInput: false,
}),
})
if (!created.ok) throw new Error(`Xora: ${await created.text()}`)
const { id } = (await created.json()) as Job
// Poll until the job reaches a terminal state. In production, pass webhookUrl instead.
for (;;) {
await new Promise((resolve) => setTimeout(resolve, 2000))
const job = (await (await fetch(`${XORA_API}/jobs/${id}`, { headers })).json()) as Job
if (job.state === 'completed' && job.output?.signedUrl) return job.output.signedUrl
if (['failed', 'rejected', 'cancelled'].includes(job.state)) {
throw new Error(`Xora job ${id} ${job.state}: ${job.error?.code ?? 'unknown error'}`)
}
}
}

For a job you run many times, save the configuration as a preset and send only the presetId and the input URL. For production, use a webhook in place of the polling loop.

Raw FFmpeg from Cursor

Recipes cover the common jobs. For anything else, the agent can use mode: "ffmpeg" with the same arguments you would type in a terminal. For example, “make a 3-second GIF preview from 00:10”:

create_job — mode ffmpeg
{
"mode": "ffmpeg",
"input_files": { "in_1": "https://example.com/demo.mp4" },
"output_files": { "out_1": "demo_preview.gif" },
"ffmpeg": {
"args": [
"-ss", "10", "-t", "3",
"-i", "{{in_1}}",
"-vf", "fps=12,scale=480:-1:flags=lanczos",
"{{out_1}}"
]
}
}

Output file names use letters, numbers and underscores, plus the extension. The custom FFmpeg guide has the full rules for placeholders and multiple inputs.

Errors the agent can act on

When a job is refused, the tool result is JSON with an error.code. VALIDATION_ERROR tells the agent which field to fix. LIMIT_EXCEEDED means the plan allowance is used up, and PLAN_REQUIRED means the feature is not on the plan. Failed and rejected jobs cost nothing, so the agent can try again after a fix.

Next steps

Frequently asked questions

How do I use FFmpeg in Cursor?

Add the Xora MCP server to .cursor/mcp.json with your API key. Cursor's agent can then send FFmpeg jobs as tool calls and get signed URLs for the outputs, with no local FFmpeg install.

Should I put the API key in the project's .cursor/mcp.json?

Only if that file stays out of version control. To keep the key out of the repository, put the server in ~/.cursor/mcp.json in your home folder. It then works in every project.

Can the agent use files in my repository as inputs?

Yes. The agent calls create_upload, sends the file with one HTTP PUT, and passes the uploadId to create_job. Files that already have a URL can go straight to create_job.

Is the MCP job the same as an API job?

Yes. An MCP tool call creates the same job as POST /v1/jobs, with the same limits and billing. That is why a job the agent tested through MCP can go into your code as a REST call with the same JSON body.

Ready to try Xora?

Replace your local FFmpeg binaries with a secure, high-performance cloud API.