FFmpeg Media Processing MCP Guide
Automate video compression, audio transcoding, and media metadata inspection using an FFmpeg MCP server: Architecture, async jobs, and tool configurations.
Generate & Validate Multi-Client MCP Config
One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.
FFmpeg Media Processing MCP Guide
Automating video transformations, audio transcoding, and container format packaging typically requires constructing brittle, highly-specific command-line arguments for FFmpeg. When engineering teams build content pipelines or developers process screen recordings, recalling complex filtergraphs (-filter_complex), pixel formats (-pix_fmt yuv420p), and audio bitrates introduces avoidable friction.
By bridging FFmpeg into the Model Context Protocol (MCP), developers can grant AI assistants in Claude Desktop, Cursor, or OpenAI Codex CLI high-level semantic tools for inspecting media streams, compressing large video recordings, and extracting audio tracks.
However, media processing workloads introduce unique operational constraints to the Model Context Protocol. Unlike standard database or filesystem tools that complete in milliseconds, transcoding a multi-gigabyte video can consume gigabytes of memory and run for several minutes. Operating an FFmpeg MCP server safely requires asynchronous job orchestration, command injection hardening, and strict process timeouts.
This guide provides an architectural blueprint, multi-client configurations, a complete production TypeScript MCP server, and troubleshooting procedures for media processing with MCP.
1. System Architecture: Asynchronous Job Processing
Because Model Context Protocol communication relies on synchronous request-response JSON-RPC framing over standard input and output (stdio), a tool call that blocks for five minutes while encoding video will trigger client-side timeouts in Claude Desktop or Cursor.
To solve this, a production FFmpeg MCP server employs an asynchronous job pattern:
- ▸The host invokes
transcode_media_asyncwith source paths and target encoding presets. - ▸The server spawns a detached background
ffmpegprocess, registers an in-memory job identifier, and immediately returns a job ticket. - ▸The agent polls
check_transcode_statusor inspects file metadata once transcoding completes.
Critical Operational Protections
- ▸Shell Injection Prevention: Never construct FFmpeg commands by concatenating raw user strings into
exec(). Always invokespawn()with distinct argument arrays to prevent command injection via malicious filenames (e.g.file; rm -rf /). - ▸Mandatory Non-Interactive Flags: Always supply
-nostdinand-yto prevent FFmpeg from blocking on interactive keyboard prompts. - ▸Resource Capping: Cap concurrent video transcoding tasks to prevent exhausting CPU cores and crashing the host development workstation.
2. Environment Prerequisites and Binary Matrix
Verify that your workstation or container environment meets the following dependencies:
| Component | Minimum Version | Installation Verification | Purpose |
|---|---|---|---|
| FFmpeg | >= 6.0 | ffmpeg -version | Core media processing utility |
| ffprobe | >= 6.0 | ffprobe -version | Stream metadata inspection utility |
| Node.js | >= 20.0.0 | node -v | Server runtime environment |
| @modelcontextprotocol/sdk | >= 1.2.0 | Package manifest | Official TypeScript MCP framework |
On Linux systems, install FFmpeg with standard codecs:
sudo apt-get update && sudo apt-get install -y ffmpegOn macOS via Homebrew:
brew install ffmpeg3. Multi-Host Configuration Snippets
Configure your MCP clients to launch the media server over stdio. Ensure the directory containing ffmpeg and ffprobe is included in the path environment.
Claude Desktop Configuration (claude_desktop_config.json)
{
"mcpServers": {
"ffmpeg-media": {
"command": "node",
"args": ["/opt/mcp-servers/ffmpeg/dist/index.js"],
"env": {
"TEMP_MEDIA_DIR": "/tmp/mcp-transcoding",
"MAX_CONCURRENT_JOBS": "2"
}
}
}
}Cursor IDE Configuration (.cursor/mcp.json)
{
"mcpServers": {
"ffmpeg-media": {
"command": "node",
"args": ["./dist/index.js"],
"env": {
"TEMP_MEDIA_DIR": "./.temp-media"
}
}
}
}OpenAI Codex CLI Configuration (~/.codex/config.toml)
[mcp_servers.ffmpeg_media]
command = "node"
args = ["/opt/mcp-servers/ffmpeg/dist/index.js"]
[mcp_servers.ffmpeg_media.env]
TEMP_MEDIA_DIR = "/tmp/mcp-transcoding"
MAX_CONCURRENT_JOBS = "2"4. Production TypeScript FFmpeg MCP Server
The following server implementation exposes probe_media_metadata (synchronous stream inspection), transcode_media_async (non-blocking transcoding), and check_transcode_status:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { spawn, execFile } from 'child_process';
import { promisify } from 'util';
import crypto from 'crypto';
const execFileAsync = promisify(execFile);
interface TranscodeJob {
id: string;
inputPath: string;
outputPath: string;
status: 'processing' | 'completed' | 'failed';
error?: string;
startedAt: number;
}
const jobs = new Map<string, TranscodeJob>();
const server = new Server(
{ name: 'ffmpeg-media-mcp', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'probe_media_metadata',
description: 'Inspect media file format, duration, codecs, resolution, and audio channels using ffprobe.',
inputSchema: {
type: 'object',
properties: {
file_path: { type: 'string', description: 'Absolute path to media file' }
},
required: ['file_path']
}
},
{
name: 'transcode_media_async',
description: 'Start an asynchronous video or audio transcoding job.',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string', description: 'Source media file path' },
output_path: { type: 'string', description: 'Destination media file path' },
preset: {
type: 'string',
enum: ['mp4_720p', 'mp4_1080p', 'extract_audio_mp3', 'extract_audio_wav'],
description: 'Target encoding preset'
}
},
required: ['input_path', 'output_path', 'preset']
}
},
{
name: 'check_transcode_status',
description: 'Check the status of an ongoing or completed transcode job by job ID.',
inputSchema: {
type: 'object',
properties: {
job_id: { type: 'string', description: 'Job ticket identifier' }
},
required: ['job_id']
}
}
]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'probe_media_metadata') {
const filePath = args?.file_path as string;
const { stdout } = await execFileAsync('ffprobe', [
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
filePath
]);
const parsed = JSON.parse(stdout);
return {
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }]
};
}
if (name === 'transcode_media_async') {
const inputPath = args?.input_path as string;
const outputPath = args?.output_path as string;
const preset = args?.preset as string;
const jobId = 'job_' + crypto.randomBytes(4).toString('hex');
const job: TranscodeJob = {
id: jobId,
inputPath,
outputPath,
status: 'processing',
startedAt: Date.now()
};
jobs.set(jobId, job);
let ffmpegArgs: string[] = ['-nostdin', '-y', '-i', inputPath];
if (preset === 'mp4_720p') {
ffmpegArgs.push('-vf', 'scale=-2:720', '-c:v', 'libx264', '-crf', '23', '-c:a', 'aac', outputPath);
} else if (preset === 'mp4_1080p') {
ffmpegArgs.push('-vf', 'scale=-2:1080', '-c:v', 'libx264', '-crf', '21', '-c:a', 'aac', outputPath);
} else if (preset === 'extract_audio_mp3') {
ffmpegArgs.push('-vn', '-c:a', 'libmp3lame', '-b:a', '192k', outputPath);
} else if (preset === 'extract_audio_wav') {
ffmpegArgs.push('-vn', '-c:a', 'pcm_s16le', outputPath);
}
const proc = spawn('ffmpeg', ffmpegArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
let stderrOutput = '';
proc.stderr.on('data', chunk => { stderrOutput += chunk.toString(); });
proc.on('close', code => {
if (code === 0) {
job.status = 'completed';
} else {
job.status = 'failed';
job.error = stderrOutput.slice(-300);
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: 'Transcode task queued successfully.',
job_id: jobId,
status: 'processing',
output_path: outputPath
}, null, 2)
}
]
};
}
if (name === 'check_transcode_status') {
const jobId = args?.job_id as string;
const job = jobs.get(jobId);
if (!job) {
return { isError: true, content: [{ type: 'text', text: `Job ID '${jobId}' not found.` }] };
}
return {
content: [{ type: 'text', text: JSON.stringify(job, null, 2) }]
};
}
throw new Error(`Unrecognized tool: ${name}`);
} catch (err: any) {
return {
isError: true,
content: [{ type: 'text', text: `FFmpeg Operation Failed: ${err.message}` }]
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
process.stderr.write(`Fatal server error: ${err.message}\n`);
process.exit(1);
});5. JSON-RPC 2.0 Wire Payloads
Inspecting the underlying protocol framing demonstrates clean communication flow.
Tool Invocation Request (tools/call)
{
"jsonrpc": "2.0",
"id": "media-job-01",
"method": "tools/call",
"params": {
"name": "transcode_media_async",
"arguments": {
"input_path": "/workspace/recordings/demo.mov",
"output_path": "/workspace/dist/demo_720p.mp4",
"preset": "mp4_720p"
}
}
}Result Payload
{
"jsonrpc": "2.0",
"id": "media-job-01",
"result": {
"content": [
{
"type": "text",
"text": "{\n \"message\": \"Transcode task queued successfully.\",\n \"job_id\": \"job_e4a819b2\",\n \"status\": \"processing\",\n \"output_path\": \"/workspace/dist/demo_720p.mp4\"\n}"
}
]
}
}6. Common Errors and Production Troubleshooting
When processing media across developer workstations, monitor for the following standard issues:
Error: Unknown encoder 'libx264'
- ▸Root Cause: The installed build of FFmpeg lacks H.264 video encoding libraries.
- ▸Resolution: Install a full build of FFmpeg compiled with GPL external libraries (e.g.,
apt-get install ffmpegon Ubuntu orbrew install ffmpegon macOS).
Error: Subprocess Hangs Indefinitely
- ▸Root Cause: The destination file already exists and FFmpeg prompts
File '...' already exists. Overwrite? [y/N]. Because standard input is not a terminal, the process deadlocks. - ▸Resolution: Always supply the
-yflag and-nostdinflag in the command argument array.
Error: Path Formatting on Windows
- ▸Root Cause: Windows backslash separators in media paths passed to filter strings can cause escaping errors in FFmpeg filtergraphs.
- ▸Resolution: Normalize all file paths to use forward slashes (
/) before passing them to FFmpeg.
7. External Citations and Security Guardrails
Operating command-line media binaries with language model tools requires strict security practices:
- ▸Strict Argument Arrays: Never use
child_process.exec()with concatenated strings. Pass explicit arrays tochild_process.spawn()to prevent arbitrary shell command injection. - ▸Disk Space Quotas: Transcoded intermediate video files can easily fill disk volumes. Configure an automated retention policy to delete temporary artifacts after 24 hours.
- ▸Official Standards and Repositories:
Weekly Protocol Diffs & Verified Drops
New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.
Zero marketing spam. 1-click unsubscribe anytime.
Build your full agent toolstack in the Visual Generator
Combine FFmpeg Media Processing MCP Guide with databases, search APIs, and memory graphs in a single configuration file.
Did this setup guide work with your AI host?
Help test this configuration across Claude, Cursor, and Codex CLI. Be the first to report:
FFmpeg Media Processing MCP Guide FAQ
What is the FFmpeg Media Processing MCP Guide?
Automate video compression, audio transcoding, and media metadata inspection using an FFmpeg MCP server: Architecture, async jobs, and tool configurations.
How do I configure FFmpeg Media Processing MCP Guide in Claude Desktop or Cursor?
You can copy the configuration JSON from our guide or launch the interactive MCP Codex Config Generator at https://mcp-codex.com/generator to export valid configs in 1 click.
Can I use FFmpeg Media Processing MCP Guide with the OpenAI Codex CLI?
Yes, OpenAI Codex CLI supports Model Context Protocol. You can add it directly to ~/.codex/config.toml or pass arguments to codex mcp add.
Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.
Related Guides
OpenCode Free AI Agent Guide
Guide to running OpenCode coding agent completely free without API keys using Zen free tier, local LLMs, and OAuth accounts.
Dev ToolsSelf-Correcting Verification Loops in Agents
Build robust self-correcting /loop verification harnesses for AI agents using Test-Driven Agent Development, dual-agent critics, and MCP test sandboxes.
Dev ToolsAutomated Doc Verification Loops with Claude CLI
Build automated documentation verification loops with Claude CLI and MCP. Validate code snippets, test tool schemas, and sync SDK docs in CI/CD. Verified.