Dev Tools·
advanced
·15 min read·Jul 9, 2026·Updated Oct 10, 2026
By Rad Tome·Lead AI Systems Architect·How we verify this

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.

ffmpegvideo-processingmediatranscodingcompressionaudiomcpdeveloper-tools
Interactive Tool
1-Click Export

Generate & Validate Multi-Client MCP Config

One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.

Open in Generator

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:

  1. ▸The host invokes transcode_media_async with source paths and target encoding presets.
  2. ▸The server spawns a detached background ffmpeg process, registers an in-memory job identifier, and immediately returns a job ticket.
  3. ▸The agent polls check_transcode_status or inspects file metadata once transcoding completes.
diagram
Local FilesystemDetached FFmpeg SubprocessFFmpeg MCP ServerAI Host (Agent / LLM)Local FilesystemDetached FFmpeg SubprocessFFmpeg MCP ServerAI Host (Agent / LLM)Agent continues reasoning or performs other taskstools/call: transcode_media_async(input, output, preset)1Spawn detached child process (ffmpeg -y ...)2JSON-RPC Result: { jobId: "job_9981", status: "processing" }3Encode frames to destination container4Process exit code 0 (SIGCHLD handler)5tools/call: check_transcode_status(jobId: "job_9981")6JSON-RPC Result: { status: "completed", outputSize: "14.2 MB" }7

Critical Operational Protections

  1. ▸Shell Injection Prevention: Never construct FFmpeg commands by concatenating raw user strings into exec(). Always invoke spawn() with distinct argument arrays to prevent command injection via malicious filenames (e.g. file; rm -rf /).
  2. ▸Mandatory Non-Interactive Flags: Always supply -nostdin and -y to prevent FFmpeg from blocking on interactive keyboard prompts.
  3. ▸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:

ComponentMinimum VersionInstallation VerificationPurpose
FFmpeg>= 6.0ffmpeg -versionCore media processing utility
ffprobe>= 6.0ffprobe -versionStream metadata inspection utility
Node.js>= 20.0.0node -vServer runtime environment
@modelcontextprotocol/sdk>= 1.2.0Package manifestOfficial TypeScript MCP framework

On Linux systems, install FFmpeg with standard codecs:

bash
sudo apt-get update && sudo apt-get install -y ffmpeg

On macOS via Homebrew:

bash
brew install ffmpeg

3. 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)

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)

json
{
  "mcpServers": {
    "ffmpeg-media": {
      "command": "node",
      "args": ["./dist/index.js"],
      "env": {
        "TEMP_MEDIA_DIR": "./.temp-media"
      }
    }
  }
}

OpenAI Codex CLI Configuration (~/.codex/config.toml)

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:

typescript
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)

json
{
  "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

json
{
  "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 ffmpeg on Ubuntu or brew install ffmpeg on 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 -y flag and -nostdin flag 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:

Technical Changelog

Weekly Protocol Diffs & Verified Drops

New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.

Zero marketing spam. 1-click unsubscribe anytime.

Ready to Deploy?

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.

Customize in Generator
Developer Verification & Feedback
0 reports

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.

RT

Written by Rad Tome

Lead AI Systems Architect & Maintainer, MCP Codex

@RadTome

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.

Editorial Integrity: Package names are checked against the npm registry and repositories against the GitHub API. Where a package could not be verified, the guide says so instead of giving you a command to run.

Related Guides