<!-- title: 造ffmpeg MCP：让AI剪辑音视频 -->

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

## 概述

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

## 前置条件

- 已安装 Claude Code 并能正常运行（`claude --version` 有输出）
- Python 3.10 及以上，会基本虚拟环境操作
- 系统 `ffmpeg` 与 `ffprobe` 可用（`ffmpeg -version` 不报错）
- 了解 JSON 与命令行基础；不要求事先懂 MCP 协议细节

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

---

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

在终端里，剪辑视频是这样做的：

```bash
# 截取 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 要做的事，就是把这些命令"翻译"成一组带类型签名的函数（工具），让模型像调用普通函数一样去执行它们。整个链路如下：

```mermaid
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 已安装：

```bash
ffmpeg -version | head -1
ffprobe -version | head -1
```

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

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

```bash
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` 调试器。安装好后建一个空文件：

```bash
touch server.py
```

## 第三步：编写 MCP 服务器

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

```python
# 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` 兜底**。防止模型给一个超大文件让进程跑飞，拖垮整个会话。

### 常见问题（第三步）

- **`ModuleNotFoundError: No module named 'mcp'`**：虚拟环境没激活，或装的是别的 `mcp` 包。确保 `pip install "mcp[cli]"`。
- **ffmpeg 一直"卡住"**：99% 是少了 `-y`，它在等你的确认。
- **截取后片段开头黑屏/音画不同步**：`-c copy` 是流复制（快但不帧精确）。需要精确时把 `reencode=True`，让它重新编码。

## 第四步：脱离 Claude 先自测

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

```bash
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 问题最高效的手段：

```bash
mcp dev server.py
```

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

- 左侧 **Tools** 列表，自动列出 `probe / trim / extract_audio / thumbnail / convert` 五个工具及其参数表单；
- 点一个工具，填入 `path: sample.mp4, start: 5, end: 10, output: clip.mp4`，点 **Run Tool**；
- 右侧实时显示 JSON-RPC 请求与返回值。

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

### 常见问题（第五步）

- **Inspector 里工具数是 0**：检查函数有没有加 `@mcp.tool()` 装饰器，且模块顶层没有报错（启动时的 import 异常会被吞掉，可在 `server.py` 顶部临时加 `print` 排查）。
- **连接后立刻断开**：`server.py` 启动时抛了异常。在终端单独跑 `python server.py` 看报错——它应该静默等待 stdin，一旦打印了 traceback 就是 bug。

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

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

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

各参数含义：

- `ffmpeg-mcp`：服务器名，对话里会用 `mcp__ffmpeg-mcp__<tool>` 形式引用；
- `--scope local`：仅对当前项目、当前用户生效（默认即 local）。换成 `project` 会写入项目根的 `.mcp.json`，可随 git 分享给团队；`user` 则全局生效；
- `-- python /abs/server.py`：`--` 之后是启动命令。**务必用绝对路径**，因为 MCP 子进程的工作目录不一定是你当前目录，相对路径会失效。

验证注册成功：

```bash
claude mcp list
claude mcp get ffmpeg-mcp
```

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

如果你更喜欢用 JSON 配置（适合团队共享），在项目根创建 `.mcp.json`：

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

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

### 常见问题（第六步）

- **`claude mcp list` 显示连接失败**：多半是启动命令路径/解释器不对。手动跑一遍注册时给的命令（`python /abs/server.py`），确认能正常进入等待状态。
- **Claude 里看不到工具**：在对话里输入 `/mcp` 查看连接状态与可用工具列表；若状态是 failed，重启 `claude` 让它重连。
- **启动太慢被超时**：通过环境变量放宽，例如 `claude mcp add ... -e MCP_TIMEOUT=30000 ...`（毫秒）。

## 第七步：在对话里使用

启动 `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 服务器好不好用的最佳试金石。

## 进阶：还可以加这些工具

- `concat(paths: list[str], output: str)`：拼接多个片段，写一个临时 concat 列表文件再喂给 ffmpeg。
- `overlay_text(path, text, output)`：用 `drawtext` 滤镜打字幕/水印，注意字体路径要绝对。
- `volume(path, db: float, output)`：调整音量 `volume={db}dB`。
- `reverse(path, output)`：倒放（小文件才好玩，大文件极耗内存）。
- 把 `probe` 同时作为 **MCP Resource** 暴露：`@mcp.resource("video://{path}")`，让模型能像读文件一样"订阅"视频元信息，而不必主动调用工具——这正是 Resources 原语擅长的事。

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

```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` 全部参数映射成工具。

## 延伸阅读

- [Claude Code 官方 MCP 接入文档](https://code.claude.com/docs/en/mcp) —— `claude mcp add/add-json/list/get` 全命令与三种 scope 详解
- [Model Context Protocol Python SDK](https://github.com/modelcontextprotocol/python-sdk) —— `FastMCP`、`mcp dev` 调试器与传输协议权威实现
- [ffmpeg-micro-mcp](https://github.com/javidjamae/ffmpeg-micro-mcp) —— 社区已有的精简版 ffmpeg MCP，可与本文实现对照学习
- [FastMCP 文档站](https://gofastmcp.com/getting-started/quickstart) —— 工具、资源、提示词三原语的现代写法
- [MCP 规范与 Inspector](https://modelcontextprotocol.io) —— JSON-RPC 协议细节、工具发现与调用流程

## 训练营适用性评估

- **适用类型**：中后端/工具链方向的 MCP 专项训练营；面向已过入门关、希望做出"能展示给非技术同事看"的实战成果的开发者。
- **教学时长**：约 3.5~4 小时（讲解 1h + 编码 1.5h + Inspector 调试 0.5h + 自由扩展 1h）。
- **难度梯度**：中等。核心难点不在 MCP 协议，而在子进程安全与路径治理——这正是把它作为"CLI 包装器"教学样本的价值。
- **可裁剪**：若时间紧，可只做 `probe + trim + thumbnail` 三个工具，仍能跑通完整 demo；`convert / extract_audio` 作为课后扩展。
- **效果评估**：以"学员能否独立加一个新工具（如 concat）并正确通过路径校验"作为通关标准。

## 参考来源

1. [Connect Claude Code to tools via MCP — Claude Code Docs](https://code.claude.com/docs/en/mcp) — 官方 `claude mcp add` 命令、scope 与 `.mcp.json` 配置权威说明
2. [Model Context Protocol Python SDK — GitHub](https://github.com/modelcontextprotocol/python-sdk) — `FastMCP` 装饰器写法、`mcp run/dev` 与 stdio 传输实现
3. [How to Use the FFmpeg MCP Server with Claude](https://www.ffmpeg-micro.com/blog/ffmpeg-mcp-server-tutorial) — 社区 ffmpeg MCP 的实战用法与工具设计参考
4. [Build a Real MCP Server with FastMCP (2026 Guide)](https://www.danilchenko.dev/posts/fastmcp-mcp-server/) — FastMCP 工具/资源/提示词的现代写法与部署要点
5. [ffmpeg 官方文档](https://ffmpeg.org/ffmpeg.html) — `-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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
