media-engine
MCP server exposing headless ffmpeg operations for video, audio, and image processing via auto-generated typed tools, enabling agentic media file manipulation.
README
<p align="center"> <img src="assets/banner.svg" alt="media·engine — headless ffmpeg operations. one engine, three faces, zero dependencies." width="100%"> </p>
<p align="center"> <a href="LICENSE"><img alt="License: GPL-3.0" src="https://img.shields.io/badge/license-GPL--3.0-a78bfa?style=flat-square"></a> <img alt="Python 3.9+" src="https://img.shields.io/badge/python-3.9%2B-5ff0d8?style=flat-square"> <img alt="Runtime dependencies: zero" src="https://img.shields.io/badge/dependencies-zero-5ff0d8?style=flat-square"> <img alt="ffmpeg 4.4+" src="https://img.shields.io/badge/ffmpeg-4.4%2B-2f3540?style=flat-square"> </p>
A standalone, headless media-processing engine: 19 ffmpeg recipes for video, audio, and still images, defined once in a single operations registry and driven through three interchangeable faces — a CLI, an MCP server, and a drag-and-drop web UI. The engine and two of the three faces are pure Python stdlib; ffmpeg is the only external binary. Everything runs locally — your files never leave your machine.
<p align="center"> <img src="assets/screenshot.png" alt="media-engine web UI running the video_to_gif operation, default palette" width="49%"> <img src="assets/screenshot-hue.png" alt="the same page with the hue knob rotated to a warm palette" width="49%"> </p> <p align="center"><sub>The web UI running <code>video_to_gif</code> — drag a file, tune the auto-generated form, watch ffmpeg run live. <em>Same page, hue knob rotated:</em> every accent is one CSS variable and shader/canvas uniform, so the whole palette and the animated background recolor instantly (and the motion can be paused to save resources).</sub></p>
<p align="center"><sub><em>Dogfood note:</em> these screenshots were downscaled for the repo with media-engine itself —<br> <code>python3 cli.py run resize_image -i shot.png -o assets/screenshot.png --imageResizePreset manual --imageCustomWidth 1200 --imageCustomHeight 550</code></sub></p>
┌──────────────────────────────┐
cli.py ────────────►│ │
server.py (MCP) ───►│ OPERATIONS registry (19) ├──► ffmpeg
webserver.py ──────►│ validate → probe → build │
└──────────────────────────────┘
Add an operation once and it appears in all three faces automatically — as a CLI subcommand, as a typed MCP tool, and as an auto-generated web form. Zero per-face code.
Contents
Highlights
- One registry, three faces. Every operation is a declarative
Operationentry (typed params, an ffmpeg command builder, an output-extension rule). The CLI, the MCP server, and the web UI are thin projections of that one registry. - Zero dependencies. The engine, the CLI, and the web UI need nothing but Python and
ffmpeg — no pip install, no venv, no build step. Only the MCP face pulls in one package
(
fastmcp). - Runs on old and new ffmpeg. A version probe picks
-fps_mode(ffmpeg ≥ 5) or the legacy-vsyncautomatically, so the same code runs on a stock Ubuntu ffmpeg 4.4 and on current builds. All 19 operations are verified end-to-end on ffmpeg 4.4.2. - Honest progress. Progress comes from ffmpeg's machine-readable
-progress pipe:1stream, not from scraping locale-dependent stderr — it feeds the CLI progress line and the web UI's live SSE updates alike. - Local and private. No network calls, no telemetry, no cloud. The web UI binds
127.0.0.1by default and processes uploads in throwaway temp workdirs.
Quick start
git clone https://github.com/constant-itis/media-engine.git
cd media-engine
ffmpeg -version # any reasonably recent build; 4.4+ verified
# CLI — no install at all
python3 cli.py list
python3 cli.py run video_to_gif -i clip.mp4 -o clip.gif
# Web UI — no install either
python3 webserver.py # then open http://127.0.0.1:8765
# MCP server — the one face with a dependency
pip install -r requirements.txt
python3 server.py
If ffmpeg/ffprobe aren't on PATH, point at them with the FFMPEG_BIN / FFPROBE_BIN
environment variables.
The three faces
CLI
python3 cli.py list # all operations, one line each
python3 cli.py info <op> # params, types, ranges, defaults
python3 cli.py run <op> -i IN -o OUT [--param value ...]
Real examples:
# two-pass palette GIF at 15 fps, 480p
python3 cli.py run video_to_gif -i clip.mp4 -o clip.gif --gifFpsPreset 15 --gifResolutionPreset 480p
# 50% faster, video and audio kept in sync
python3 cli.py run video_speed -i clip.mp4 -o fast.mp4 --speed 50
# transcode to opus with EBU R128 loudness normalization
python3 cli.py run convert_audio -i song.wav -o song.opus --audioFormat opus --normalizeLoudness true
# watermark, bottom-right, 18% of frame width
python3 cli.py run add_watermark -i clip.mp4 -o marked.mp4 --coverImage logo.png --watermarkPosition bottom-right
# print the exact ffmpeg command(s) without running them
python3 cli.py run video_to_gif -i clip.mp4 -o clip.gif --dry-run
Every param takes a value (booleans included: --gifExportWebp true). run also accepts
--threads N, --quiet, and --dry-run — dry-run prints the fully-assembled ffmpeg
invocations, which doubles as a recipe reference.
MCP server
server.py exposes the registry over the Model Context Protocol as a stdio server —
one typed tool per operation, auto-generated from the same OPERATIONS registry.
Choice params become enum-constrained schema fields, numeric ranges and defaults carry
over, output_path is optional (derived next to the input as {stem}_{op}.{ext}), and
every tool accepts dry_run. A list_operations tool covers discovery.
pip install -r requirements.txt # fastmcp — the engine itself stays stdlib
python3 server.py # stdio entrypoint your MCP client launches
Register it with any MCP-compatible client:
{
"mcpServers": {
"media-engine": {
"command": "python3",
"args": ["/absolute/path/to/media-engine/server.py"]
}
}
}
Tool results return the output path, the ffmpeg command(s) run, and pass counts;
failures come back as {"ok": false, "error": ...} with the ffmpeg stderr tail.
Web UI
webserver.py serves a single-page drag-and-drop UI over stdlib http.server — no
frameworks, no build step, no dependencies. The whole frontend is one self-contained
web/index.html (vanilla JS, inline styles, embedded font and icons).
python3 webserver.py # http://127.0.0.1:8765
python3 webserver.py --host 0.0.0.0 --port 9000
Drop in a file, pick an operation (deep-linkable as /#video_to_gif), tweak the
auto-generated parameter form, run. The op executes in a background thread inside a
temp workdir, progress streams live over Server-Sent Events, and the result previews
in-browser (image/video) with a download button. Abandoned jobs are reaped after 30
minutes.
The HTTP API underneath is plain and scriptable:
| Route | Method | Purpose |
|---|---|---|
/ |
GET | the UI (web/index.html) |
/api/operations |
GET | full registry as JSON (params, types, ranges, defaults) |
/api/run/<op> |
POST | multipart upload (file field __input__ + param fields) → {job_id} |
/api/progress/<job_id> |
GET | live progress as an SSE stream |
/api/result/<job_id> |
GET | download the output (one-shot; workdir cleaned after) |
/health |
GET | liveness check |
Security note: the server runs ffmpeg on uploaded input and binds localhost by default. Don't expose it to an untrusted network.
Operations
19 operations — 10 video, 5 audio, 4 image. Full parameter details for any op:
python3 cli.py info <op> (or /api/operations, or the MCP tool schemas — same data).
Video
| Operation | Description | Params |
|---|---|---|
video_to_gif |
Video to animated GIF (two-pass palette) or WebP | gifFpsPreset gifResolutionPreset gifExportWebp |
video_speed |
Speed up / slow down video + audio in sync (atempo-chained) | speed (−100…100 %) |
add_watermark |
Overlay a scaled, alpha-blended image watermark | coverImage watermarkPosition watermarkSize watermarkOpacity watermarkX watermarkY |
convert_video |
Change container format, codec, and bitrate | videoFormat videoCodec videoBitrate |
resize_video |
Fit into a preset resolution/orientation frame with letterbox padding | resolutionPreset resizeVideoFormat |
trim_video |
Cut a segment starting at startTime for duration (re-encodes) |
startTime duration |
extract_audio |
Extract the audio track (optionally transcoding), or strip audio entirely | extractAudioAction extractAudioFormat extractAudioBitrate |
rotate_video |
Rotate 90° CW/CCW, 180°, or mirror horizontally | rotation |
grayscale_video |
Named color-grade preset plus manual brightness/contrast/saturation trim | styleMode styleIntensity brightnessAdjust contrastAdjust saturationAdjust |
burn_subtitles |
Hardcode SRT/VTT/ASS subtitles into the video frame (irreversible) | subtitles |
Audio
| Operation | Description | Params |
|---|---|---|
convert_audio |
Transcode to a target format with optional loudness normalize / volume boost | audioFormat normalizeLoudness volumeBoostPercent outputSampleRate bitrate |
trim_audio |
Cut a segment from an audio file (start + duration) | startTime duration |
audio_settings |
Channel-layout change, full reverse, and/or fade in/out in one pass | audioChannelMode reverseAudioEnabled fadeInDuration fadeOutDuration |
add_metadata |
Write/remove ID3-style text metadata and embed/strip cover art, per container support | coverImage metadataArtist metadataTitle metadataReleaseDate metadataGenre removeCover removeAllMetadata |
audio_speed |
Change audio tempo (atempo-chained; same percent scale as video_speed) |
speed (−100…100 %) |
Image
| Operation | Description | Params |
|---|---|---|
resize_image |
Resize/reformat a still image to a target aspect ratio and size | imageAspectRatio imageResizePreset imageCustomWidth imageCustomHeight imageOutputFormat |
rotate_image |
Rotate 90°/180° and/or flip horizontally/vertically | rotation flipHorizontal flipVertical |
grayscale_image |
Black-and-white via intensity-blended color mixer plus preset contrast/brightness | blackWhiteMode blackWhiteIntensity blackWhiteContrast blackWhiteBrightness |
sharpness_image |
Box blur and/or unsharp-mask sharpening | sharpenAmount blurStrength |
Architecture
engine/
opmodel.py the model: Param (typed, validated) · Context · Operation
ops/
video.py 10 video operations
audio.py 5 audio operations
image.py 4 image operations
operations.py registry aggregator + run_operation (validate → probe → build → run)
ffmpeg.py process layer: binary resolution, -progress pipe:1 parsing,
bounded stderr tails, version capability probe, per-OS null device
probe.py ffprobe → MediaInfo (dimensions, duration, bitrates, audio presence)
cli.py CLI face
server.py MCP face (one typed tool per op, generated from the registry)
webserver.py web face (stdlib http.server + SSE job streaming)
web/index.html the entire frontend — one self-contained page, no build step
recon/ upstream recipe inventory + porting assessment
An operation is data plus one function:
Operation(
id="video_to_gif", category="video",
description="Video to animated GIF (two-pass palette) or WebP.",
params=[Param("gifFpsPreset", "choice", 12, choices=[8, 10, 12, 15, 24, 25, 30, 50, 60]), ...],
build=_build_gif, # Context -> [Pass, ...] (the ffmpeg invocations)
output_ext=lambda p: "webp" if p.get("gifExportWebp") else "gif",
needs_probe=False, # True = run ffprobe first, results in ctx.info
)
run_operation validates params against the declared specs, probes the input if the op
asks for it, calls build to get one or more ffmpeg Passes (multi-pass recipes like
palette-gen GIF just return two), and executes them with live progress. Temp files
created via ctx.tempfile() are cleaned up automatically.
To add an operation: write a _build_<op>(ctx) function and append an
Operation(...) entry in the right engine/ops/*.py module. That's the whole job — the
CLI subcommand, the typed MCP tool, and the web form all materialize from the entry.
Portability
- ffmpeg version probe —
-fps_mode passthroughonly exists on ffmpeg ≥ 5.0;engine/ffmpeg.pyparsesffmpeg -versiononce and falls back to the legacy-vsync passthroughon older builds (verified end-to-end on 4.4.2; the modern-flag path is the same one the upstream recipes used against ffmpeg 6.x). - Per-OS null device —
/dev/nullvsNULchosen at runtime. - Binary resolution —
PATHlookup withFFMPEG_BIN/FFPROBE_BINenv overrides; no.exeassumptions. - Machine-readable progress —
-progress pipe:1key-value stream instead of parsing locale/version-dependent stderrtime=lines. - Python — 3.9+, stdlib only (dataclasses,
argparse,http.server,subprocess). The MCP face additionally followsfastmcp's Python requirement.
Provenance
The recipes were ported and adapted from media-by-outlaw2082 (GPL-3.0), an Electron GUI over ffmpeg — credit to Outlaw2082 for the original operation set and command recipes. This project reimplements them as a headless, dependency-free engine, keeping the recipes' behavior while fixing a few issues found during the port:
rotate_video"Mirror" was exposed upstream but had no filter mapping (silent no-op); it now emitshflip.trim_audio/audio_settingsrelied on ffmpeg's default-encoder-by-extension guess; the codec is now set explicitly.audio_speedlabeled its input "percent" but clamped a raw 0.5–2.0 factor; it now uses the same percent scale asvideo_speedwith an atempo chain (no 2× ceiling).sharpness_imagemapped its slider tounsharpamounts up to 20, outside ffmpeg's valid[-2, 5]range (errored on every build); remapped to 0–5.
The recon/ directory documents the original operation inventory and the porting
assessment.
License
GPL-3.0 — inherited from the upstream project the recipes derive from, and kept gladly.
推荐服务器
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 模型以安全和受控的方式获取实时的网络信息。