首页 › tutorials › 造ffmpeg MCP:让AI剪辑音视频

造 ffmpeg MCP:让 AI 直接剪辑音视频

概述

大模型天生不会"看"二进制媒体——它无法直接读取一段 mp4 的时长,也无法把视频切成片段。本教程带你用 Python 把 ffmpeg/ffprobe 命令行封装成一个 MCP 服务器,让 Claude Code 在对话里就能探测视频信息、截取片段、提取音频、生成缩略图、转换格式。你将掌握 MCP 工具开发中最常见、却很少被讲透的一类模式:CLI 包装器——如何安全地调用子进程、捕获输出、校验路径、防止命令注入,并把这一切暴露给 AI。适合已完成 Claude Code 入门、想动手造第一个真正"扩展 AI 能力边界"的 MCP 工具的开发者。

前置条件

为什么选 ffmpeg?因为它最能直观说明 MCP 的价值:把 AI 原本根本做不到的事(处理二进制音视频)变成一句自然语言就能完成。这种"能力跃迁"的演示效果,远比又一个查数据库的 demo 更有冲击力。


第一步:理解我们要解决的问题

在终端里,剪辑视频是这样做的:

# 截取 10~20 秒
ffmpeg -ss 10 -to 20 -i input.mp4 -c copy clip.mp4
# 提取音频
ffmpeg -i input.mp4 -vn -acodec libmp3lame audio.mp3
# 生成缩略图
ffmpeg -ss 5 -i input.mp4 -frames:v 1 thumb.jpg

但 Claude 自己跑不了这些命令——它没有调用 ffmpeg 的内置能力,更不会在对话中替你管理输入输出文件。MCP 要做的事,就是把这些命令"翻译"成一组带类型签名的函数(工具),让模型像调用普通函数一样去执行它们。整个链路如下:

flowchart LR
    U[用户: 把这个视频前10秒切出来] --> C[Claude Code]
    C -->|JSON-RPC over stdio| S[ffmpeg MCP 服务器 Python]
    S -->|subprocess.run args 列表| F[ffmpeg / ffprobe]
    F -->|输出文件| FS[本地磁盘 clip.mp4]
    FS -.结果回传.-> S
    S -.工具返回值.-> C
    C --> U

关键点:Claude 不直接碰 ffmpeg,而是通过你写的 MCP 服务器间接驱动。你的服务器就是AI 与真实系统之间的安全闸门。

第二步:环境与脚手架

先确认 ffmpeg 已安装:

ffmpeg -version | head -1
ffprobe -version | head -1

若没有,macOS 用 brew install ffmpeg,Ubuntu/Debian 用 sudo apt install ffmpeg,Windows 建议用 winget 或下载官方静态构建并加入 PATH。

建立项目目录与虚拟环境:

mkdir ffmpeg-mcp && cd ffmpeg-mcp
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "mcp[cli]"

mcp[cli] 是官方 Python SDK,内置 FastMCP(用装饰器声明工具的便捷层)和 mcp dev 调试器。安装好后建一个空文件:

touch server.py

第三步:编写 MCP 服务器

下面是完整服务器代码。逐段看完你就懂了 CLI 包装器的全部套路。把内容写入 server.py:

# server.py
import json
import subprocess
from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("ffmpeg-mcp")

# 允许操作的根目录,防止模型读写任意路径
ROOT = Path.cwd()

def _safe(p: str) -> Path:
    """把用户传入的路径限制在 ROOT 内,拒绝越界访问。"""
    resolved = (ROOT / p).resolve() if not Path(p).is_absolute() else Path(p).resolve()
    if not resolved.is_relative_to(ROOT):
        raise ValueError(f"路径越界,只允许在 {ROOT} 内操作: {p}")
    return resolved

def _run(args: list[str], timeout: int = 120) -> subprocess.CompletedProcess:
    """统一执行外部命令:参数列表形式,绝不走 shell,避免注入。"""
    return subprocess.run(
        args, capture_output=True, text=True,
        timeout=timeout, check=False,
    )

@mcp.tool()
def probe(path: str) -> dict:
    """探测音视频文件的时长、编码、分辨率、码率等信息。"""
    target = _safe(path)
    if not target.exists():
        return {"error": f"文件不存在: {path}"}
    r = _run([
        "ffprobe", "-v", "quiet",
        "-print_format", "json",
        "-show_format", "-show_streams",
        str(target),
    ])
    if r.returncode != 0:
        return {"error": r.stderr.strip()}
    return json.loads(r.stdout)

@mcp.tool()
def trim(path: str, start: float, end: float, output: str, reencode: bool = False) -> str:
    """截取 [start, end] 秒之间的片段到 output。reencode=True 时帧精确但较慢。"""
    if end <= start:
        return "错误:end 必须大于 start"
    src, dst = _safe(path), _safe(output)
    codec = [] if reencode else ["-c", "copy"]
    r = _run([
        "ffmpeg", "-y", "-ss", str(start), "-to", str(end),
        "-i", str(src), *codec, str(dst),
    ])
    if r.returncode != 0:
        return f"失败:{r.stderr.strip()[-300:]}"
    return f"已生成 {dst.relative_to(ROOT)}"

@mcp.tool()
def extract_audio(path: str, output: str, bitrate: str = "192k") -> str:
    """从视频中提取音频并转成 mp3。"""
    src, dst = _safe(path), _safe(output)
    r = _run([
        "ffmpeg", "-y", "-i", str(src), "-vn",
        "-acodec", "libmp3lame", "-b:a", bitrate, str(dst),
    ])
    return "完成" if r.returncode == 0 else f"失败:{r.stderr.strip()[-300:]}"

@mcp.tool()
def thumbnail(path: str, at: float, output: str, width: int = 320) -> str:
    """在 at 秒处截取一帧作为缩略图。"""
    src, dst = _safe(path), _safe(output)
    r = _run([
        "ffmpeg", "-y", "-ss", str(at), "-i", str(src),
        "-frames:v", "1", "-vf", f"scale={width}:-1", str(dst),
    ])
    return "完成" if r.returncode == 0 else f"失败:{r.stderr.strip()[-300:]}"

@mcp.tool()
def convert(path: str, output: str) -> str:
    """容器/编码格式转换,依据 output 后缀自动推断(如 .mp4 .webm .mov)。"""
    src, dst = _safe(path), _safe(output)
    r = _run(["ffmpeg", "-y", "-i", str(src), str(dst)])
    return "完成" if r.returncode == 0 else f"失败:{r.stderr.strip()[-300:]}"

if __name__ == "__main__":
    mcp.run()   # 默认 stdio 传输

为什么这样写——逐个套路拆解:

  1. _safe() 路径闸门。模型可能把 output 传成 ../../etc/evil。is_relative_to(ROOT) 会把任何想跳出工作目录的路径直接拒掉。这是 CLI 包装器的第一安全线。
  2. _run() 用参数列表而非字符串。注意我们传的是 ["ffmpeg", "-y", ...] 列表,绝不用 shell=True。这样即使文件名里含 ;rm -rf / 也只会被当成一个普通文件名,命令注入无从谈起。
  3. 每个工具都有明确的类型签名和 docstring。FastMCP 会把参数类型、返回类型、docstring 自动翻译成 JSON Schema 喂给模型——docstring 就是模型的说明书,写得越清楚,模型调用越准。
  4. capture_output=True + 返回末尾 300 字符。ffmpeg 失败信息往往在 stderr 末尾,截尾回传既省 token 又保留了诊断价值。
  5. -y 覆盖输出。避免 ffmpeg 卡在交互式确认上(stdio 通道没有 TTY,一旦等输入就会死锁)。
  6. timeout 兜底。防止模型给一个超大文件让进程跑飞,拖垮整个会话。

常见问题(第三步)

第四步:脱离 Claude 先自测

在接入 Claude 之前,先用普通 Python 直接验证逻辑正确。在项目根目录放一个测试视频(例如 sample.mp4),然后:

python -c "from server import probe, trim; print(probe('sample.mp4'))"

预期看到一段 JSON,包含 format.duration、streams[].codec_name、width/height 等。这一步能排除"ffmpeg 没装好/路径写错"这类与 MCP 无关的基础问题——永远不要带着未验证的逻辑去调试 MCP 链路,那会让你分不清是协议问题还是命令问题。

第五步:用 MCP Inspector 可视化调试

官方 SDK 自带一个图形调试器,能在不启动 Claude 的情况下手动点击调用每个工具、查看输入输出 schema。这是排查 MCP 问题最高效的手段:

mcp dev server.py

它会打开一个本地网页(通常是 http://localhost:6274)。页面里你能看到:

如果浏览器没自动弹出,手动打开终端里打印的 URL。该工具本质是 @modelcontextprotocol/inspector,也可以用 npx @modelcontextprotocol/inspector python server.py 启动。

常见问题(第五步)

第六步:把服务器注册到 Claude Code

确认无误后,把它接入 Claude Code。在项目目录下执行:

claude mcp add ffmpeg-mcp --scope local -- python $(pwd)/server.py

各参数含义:

验证注册成功:

claude mcp list
claude mcp get ffmpeg-mcp

list 应能看到 ffmpeg-mcp [connected],get 会打印它解析出的命令、参数与环境变量。

如果你更喜欢用 JSON 配置(适合团队共享),在项目根创建 .mcp.json:

{
  "mcpServers": {
    "ffmpeg-mcp": {
      "command": "python",
      "args": ["/absolute/path/to/ffmpeg-mcp/server.py"]
    }
  }
}

子进程不会继承你的 shell 环境。如果你用 uv 或 conda 管理 Python,command 要指向对应解释器的绝对路径,或在 env 字段里补上必要的环境变量,否则会找不到 mcp 包或 ffmpeg。

常见问题(第六步)

第七步:在对话里使用

启动 claude,在项目目录放好 sample.mp4,然后直接用自然语言下指令:

帮我看看 sample.mp4 的时长和分辨率,然后把第 5 到 10 秒切出来存成 clip.mp4,
再在第 7 秒截一张 320 宽的缩略图存成 thumb.jpg。

你会观察到 Claude 会自主、按顺序调用 probe → trim → thumbnail,每一步都用 MCP 工具(终端会显示 mcp__ffmpeg-mcp__... 的调用记录),并在完成后汇报生成的文件。整个过程中你没有敲一行 ffmpeg 命令——这就是 MCP 的威力:把领域工具变成 AI 的本能。


完整实战案例:一键产出"短视频预告"

目标:给定一个长视频,自动生成"前 3 秒静帧封面 + 中间精彩 10 秒片段 + 提取背景音乐 mp3"三件套。

把下面这段保存为 agent_task.md,在 claude 里让它执行,或直接口头描述:

对 sample.mp4 做如下处理,全部输出到当前目录:
1. 探测它总时长;
2. 截取总时长 40%~50% 这一段为 highlight.mp4(要帧精确,重新编码);
3. 在该片段第 1 秒截取封面 cover.jpg,宽 640;
4. 从整段视频提取 128k 的 mp3 名为 bgm.mp3。
完成后用 probe 报告 highlight.mp4 的实际时长。

Claude 会综合调用 probe(拿到总时长算出 40%~50% 的时间点)、trim(reencode=True)、thumbnail(width=640)、extract_audio(bitrate=128k),最后再用 probe 验证产物。这是一个模型无法独立完成、必须借助你造的 MCP 工具才能落地的真实任务——它正是评估一个 MCP 服务器好不好用的最佳试金石。

进阶:还可以加这些工具

Mermaid:一次"截取+缩略图"调用的时序

sequenceDiagram
    participant U as 用户
    participant C as Claude Code
    participant S as ffmpeg-mcp
    participant FF as ffmpeg/ffprobe
    U->>C: 把 sample.mp4 5~10秒切出来,再截第7秒的图
    C->>S: tools/call trim {path,start:5,end:10,output:"clip.mp4"}
    S->>S: _safe() 校验路径在 ROOT 内
    S->>FF: ffmpeg -ss 5 -to 10 -i sample.mp4 -c copy clip.mp4
    FF-->>S: returncode 0
    S-->>C: "已生成 clip.mp4"
    C->>S: tools/call thumbnail {path,at:7,output:"thumb.jpg",width:320}
    S->>FF: ffmpeg -ss 7 -i sample.mp4 -frames:v 1 -vf scale=320:-1 thumb.jpg
    FF-->>S: returncode 0
    S-->>C: "完成"
    C-->>U: 已生成 clip.mp4 与 thumb.jpg

注意 trim 与 thumbnail 是两次独立的工具调用——模型自行决定顺序、自行在两次调用间维护状态,你的服务器本身是无状态的,这能极大降低实现复杂度。

安全清单(CLI 包装器必读)

  1. 路径白名单:所有文件操作经 _safe() 收口,禁止越出工作目录。
  2. 参数列表执行:永不 shell=True,永不 os.system,永不字符串拼接命令。
  3. 超时与资源:设 timeout,对大文件考虑限制最大时长/分辨率。
  4. 覆盖确认:统一带 -y,杜绝交互死锁。
  5. 错误收敛:stderr 截尾返回,避免把数 MB 日志灌进上下文。
  6. 最小权限:服务器只暴露你确实需要的工具,别一股脑把 ffmpeg 全部参数映射成工具。

延伸阅读

训练营适用性评估

参考来源

  1. Connect Claude Code to tools via MCP — Claude Code Docs — 官方 claude mcp add 命令、scope 与 .mcp.json 配置权威说明
  2. Model Context Protocol Python SDK — GitHub — FastMCP 装饰器写法、mcp run/dev 与 stdio 传输实现
  3. How to Use the FFmpeg MCP Server with Claude — 社区 ffmpeg MCP 的实战用法与工具设计参考
  4. Build a Real MCP Server with FastMCP (2026 Guide) — FastMCP 工具/资源/提示词的现代写法与部署要点
  5. ffmpeg 官方文档 — -ss/-to/-c copy/drawtext/scale 等命令与滤镜参数细节

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入