ffmpeg-mcp-video-editor
Give any MCP client a real video editor — 32 typed tools over ffmpeg, Whisper and MediaPipe, plus an optional local UI with a drag-and-drop timeline.
README
<p align="center"> <img src="docs/banner.svg" alt="ffmpeg-mcp" width="1200"> </p>
<p align="center"> <img src="https://img.shields.io/badge/Python-3.11%20%7C%203.12-3776AB?logo=python&logoColor=white" alt="Python 3.11 | 3.12"> <img src="https://img.shields.io/badge/MCP-stdio-E8630A" alt="MCP: stdio transport"> <img src="https://img.shields.io/badge/ffmpeg-6%2B-007808?logo=ffmpeg&logoColor=white" alt="ffmpeg 6+"> <img src="https://img.shields.io/badge/tools-38-1b1b1b" alt="38 MCP tools"> <img src="https://img.shields.io/badge/tests-727-22c55e" alt="727 tests"> <img src="https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux%20%C2%B7%20Windows-555" alt="Platforms"> <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3da639" alt="License: MIT"></a> </p>
<p align="center"> Give any MCP client a real video editor —<br> <b>38 typed tools</b> over ffmpeg, Whisper and MediaPipe, plus an optional local UI with a <b>drag-and-drop timeline</b>. </p>
<p align="center"> <a href="#features">Features</a> · <a href="#quick-start">Quick start</a> · <a href="#how-it-works">How it works</a> · <a href="#reframing-for-reels-and-shorts">Reframing</a> · <a href="#the-tools">Tools</a> · <a href="#the-local-ui">Local UI</a> · <a href="#security">Security</a> · <a href="#license">License</a> </p>
Features
- Typed tools, not a shell — ffmpeg's editing surface arrives as 38 schema'd MCP tools, so the calling model gets parameters and guardrails instead of hand-writing filter graphs.
- Nothing blocks — anything that encodes frames returns a job id immediately, with real progress parsed from ffmpeg's own output and a cancel that actually kills the subprocess.
- Zero setup — finds your ffmpeg, or downloads a verified static build for your OS on first run. The Whisper and face models fetch themselves too.
- Captions that survive their own text — a subtitle containing
Time: 12:30, [note]; it's 50%would corrupt a naively built filter graph. Here it doesn't, and there's a test proving it. - Sees faces — detect them, follow one with a smoothed crop to turn landscape into vertical, or blur every face except your subject.
- Hears speech — Whisper transcription, translation, and one-shot auto-captioning with word-level timestamps.
- Renders whole timelines — clips, transitions, overlays, captions and ducked audio tracks compile into a single ffmpeg pass.
- Two front doors, one backend — the optional local UI shares the same job store, so it and your MCP client see and cancel each other's work.
- Can see what it edits — extract a frame or a contact sheet, measure brightness, colour and loudness. Editing is a loop, so the server can check its own output rather than rendering blind.
- Tested where it counts — 727 tests; the unit tests run with no ffmpeg installed at all.
Quick start
uv sync --all-extras # core + Whisper + vision + UI
uv run ffmpeg-mcp-server
Register it with your MCP client. Rather than hand-editing paths, print the block with your real ones already filled in:
printf '{\n "mcpServers": {\n "ffmpeg-mcp": {\n "command": "%s",\n "args": ["run", "--directory", "%s", "ffmpeg-mcp-server"]\n }\n }\n}\n' "$(command -v uv)" "$PWD"
Paste the result into your client's MCP config, then restart it. Both paths must
be absolute: a GUI app does not inherit your shell's PATH, and it launches
the server from an unrelated working directory.
<details> <summary>It says <code>error: No such file or directory (os error 2)</code></summary>
<br>
That is uv failing before the server ever starts, and it almost always means
the --directory path does not exist — most often a placeholder that was pasted
verbatim. Run the command above to get the correct one. You can check it
directly with:
uv run --directory /your/path ffmpeg-mcp-server # should print startup logs, not exit
If uv itself is not found, use its absolute path (command -v uv) as
"command".
</details>
Then just ask for what you want:
"Take
interview.mp4, follow the speaker's face, make it a vertical Reel, and burn in captions."
track_and_crop → job 8f3a… ▸ done interview_reframed.mp4
auto_caption → job b71c… ▸ done interview_reframed_captioned.mp4 + .srt
probe_media and list_capabilities answer instantly. Everything else returns a
job id — poll job_status, then job_result. cancel_job stops it mid-render.
Give it real paths on your machine. The server runs natively, so it reads your filesystem — not files you attach to the chat, which live in the assistant's own sandbox. Say
~/Downloads/clip.mov, not/mnt/user-data/uploads/clip.mov. Paths must also sit underFFMPEG_MCP_ALLOWED_ROOTS, which defaults to your home directory.
Projects
Several sessions can share one server without mixing together. Name a project and everything that session does is filed under it:
set_project { "name": "dress-reel" } // one session
set_project { "name": "wedding-teaser" } // another, same server
Jobs are stamped with the project, unspecified outputs land in
<workspace>/projects/<name>/, and list_jobs shows only that project by
default — so a busy shared queue stays readable. project: "all" spans them,
and list_projects shows what exists with per-project counts.
Long queues page rather than dumping everything:
list_jobs { "limit": 25, "offset": 50 } // returns total and has_more
Workers still share the queue, so projects divide the bookkeeping without dividing the compute.
How it works
<p align="center"> <img src="docs/architecture.svg" alt="MCP client and local UI share one SQLite job store, worker pool, and workspace" width="1120"> </p>
Tools validate their arguments and enqueue; a worker pool picks the job up and runs it. The job store is plain SQLite inside the workspace, which is what lets the MCP server and the UI be two processes over one queue — either can enqueue, either can watch progress, and either can cancel a job the other one started, because cancellation is a cooperative flag the owning worker polls.
Every ffmpeg invocation goes through one async execution path that owns
timeouts, progress parsing and error wrapping. Arguments are always a list;
shell=True appears nowhere in the project.
Reframing for Reels and Shorts
The most-asked-for edit, as one call. resize_video takes a named preset
(19 of them), an aspect_ratio like 9:16, or explicit dimensions — and fit
decides what happens to the picture that no longer fits.
<p align="center"> <img src="docs/reframe.svg" alt="cover, contain, blur and track_and_crop compared on the same source" width="1120"> </p>
{ "input_path": "talk.mp4", "preset": "reel", "fit": "blur" } // blurred bars, nothing cropped
{ "input_path": "talk.mp4", "aspect_ratio": "1:1", "focus": "top" } // crop, keep the top
For a talking head that must stay in frame, prefer track_and_crop — it
follows the face and smooths the crop path, because a crop that snaps frame to
frame looks worse than a slightly imperfect one that glides.
The tools
| Area | Tools | |
|---|---|---|
| 🎬 | Core | probe_media · trim · concat · convert_format · transform · speed_ramp · list_capabilities |
| ⏱ | Jobs | job_status · job_result · cancel_job · list_jobs |
| 🗂 | Projects | set_project · list_projects |
| 🎨 | Colour | color_grade · apply_lut · apply_curves |
| 💬 | Text | burn_captions · text_overlay · build_srt |
| 🗣 | Speech | transcribe_audio · auto_caption · translate_transcript |
| 👤 | Vision | detect_faces · track_and_crop · blur_faces · detect_scenes |
| 🎞 | Compose | add_transition · overlay_media · render_timeline |
| 🔊 | Audio | mix_audio · normalize_audio · fade_audio |
| 📐 | Format | resize_video · list_resolution_presets |
| 🔍 | Inspect | extract_frame · extract_filmstrip · analyze_video · measure_audio |
<sub>Six tools are read-only and answer synchronously — <code>probe_media</code>, <code>list_capabilities</code>, <code>list_resolution_presets</code>, and the three job queries. The rest return a job id.</sub>
The local UI
A separate process over the same workspace and job store — run it alongside the MCP server, or entirely on its own.
uv run ffmpeg-mcp-ui # http://127.0.0.1:8756
| Panel | What it does | |
|---|---|---|
| 📊 | Jobs | Live queue pushed over a WebSocket, with progress bars and cancel. Jobs started by your MCP client appear here too. |
| ▶️ | Preview | Plays inputs and outputs in-browser, with a before/after view whose two players stay in step — grading and cropping are hard to judge from a still. |
| 🧰 | Tools | A form for every tool, generated from its JSON schema, so the list can never drift from what the tools accept. |
| ✂️ | Timeline | Drag clips to reorder, drag their edges to trim, scrub a playhead synced to the preview. Render emits exactly the structure render_timeline accepts. |
| 📥 | Drop in | Drag media from anywhere on your machine; it lands in the workspace and is immediately editable. |
<sub>Dark, glassmorphic, one orange accent. Built assets are committed, so running the UI needs no Node toolchain.</sub>
Requirements
- Python 3.11 or 3.12 (managed with
uv) - ffmpeg 6+ — or let it download a static build on first run
- macOS, Linux, or Windows
Configuration
Environment variables, all prefixed FFMPEG_MCP_:
| Variable | Default | Meaning |
|---|---|---|
WORKSPACE |
~/.ffmpeg-mcp/workspace |
Outputs, job store, cached binaries and models. |
ALLOWED_ROOTS |
$HOME |
Roots that inputs and outputs must sit under. |
MAX_INPUT_BYTES |
16 GiB | Rejects oversized inputs up front. |
MAX_JOB_SECONDS |
10800 | Wall-clock cap per job. |
RETENTION_HOURS |
24 | How long finished jobs and their files are kept. |
WORKER_CONCURRENCY |
2 | Jobs running at once, per process. |
FFMPEG_PATH / FFPROBE_PATH |
— | Use specific binaries instead of resolving one. |
AUTO_DOWNLOAD |
1 |
Allow downloading a static ffmpeg build. |
WHISPER_MODEL |
base |
tiny … large-v3, turbo. |
LOG_LEVEL |
INFO |
How ffmpeg is found
Configured path → build cached in the workspace → a system ffmpeg ≥ 6 → a downloaded static build.
Builds from BtbN (Linux, Windows) are verified against the checksums.sha256
published with the release, and a mismatch is fatal. evermeet.cx (macOS)
publishes only a GPG signature and no digest, so macOS downloads are pinned on
first use — the hash is recorded in <workspace>/bin/manifest.json and any
later download of the same URL must match. Prefer to avoid that? Install ffmpeg
yourself and it'll be used instead.
The MediaPipe face model (~230 KB) is fetched on first vision call and verified against a pinned SHA-256.
Security
- Path allowlist — every input and output is resolved through symlinks before being checked, so a symlink in the workspace can't reach
/etc. - No shell, ever — ffmpeg arguments are always a list.
shell=Trueappears nowhere. - Filter escaping, verified — filter strings are built in one module applying both levels of ffmpeg's escaping. Overlay text goes to a sidecar file referenced with
textfile=andexpansion=none, so caption text never enters the graph at all. - Auditable — every job records its resolved ffmpeg command line. File contents are never logged.
- The UI is the same trust boundary — on loopback it runs unauthenticated; bound anywhere else it generates and requires a token.
- Uploads are rebuilt, not filtered — the directory component is discarded (
../../etc/passwd→passwd), the stem reduced to[A-Za-z0-9._-], and the extension must be a media type the tools read.
Development
uv run pytest # 727 tests
uv run pytest -m "not integration" # most need no ffmpeg
uv run ruff check . && uv run ruff format --check .
uv run mypy
Unit tests cover the pure logic — filter builders and escaping, path validation, the job store, SRT and LUT parsing, tracking geometry, timeline compilation. Integration tests generate small fixture clips on first run and verify rendered output by probing it.
Rebuilding the frontend:
cd ui-src && npm install && npm run build # emits into src/ffmpeg_mcp/ui/static/
Notes
blur_faces covers up to 12 tracked faces and says so when it truncates. A
missed detection means an unblurred face — review the output before publishing
anything sensitive.
License
MIT © AbyAbyss.
ffmpeg itself is separately licensed, and the static builds this server can
download for you are GPL-configured. Those run as a separate process that
this project invokes over a command line — it links no ffmpeg code — so the MIT
terms above cover this codebase only. If you redistribute a bundle that ships an
ffmpeg binary alongside it, check that binary's own terms. Point
FFMPEG_MCP_FFMPEG_PATH at an LGPL build if you would rather avoid GPL
components entirely.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。