大模型天生不会"看"二进制媒体——它无法直接读取一段 mp4 的时长,也无法把视频切成片段。本教程带你用 Python 把 ffmpeg/ffprobe 命令行封装成一个 MCP 服务器,让 Claude Code 在对话里就能探测视频信息、截取片段、提取音频、生成缩略图、转换格式。你将掌握 MCP 工具开发中最常见、却很少被讲透的一类模式:CLI 包装器——如何安全地调用子进程、捕获输出、校验路径、防止命令注入,并把这一切暴露给 AI。适合已完成 Claude Code 入门、想动手造第一个真正"扩展 AI 能力边界"的 MCP 工具的开发者。
claude --version 有输出)ffmpeg 与 ffprobe 可用(ffmpeg -version 不报错)为什么选 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
下面是完整服务器代码。逐段看完你就懂了 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 传输
为什么这样写——逐个套路拆解:
_safe() 路径闸门。模型可能把 output 传成 ../../etc/evil。is_relative_to(ROOT) 会把任何想跳出工作目录的路径直接拒掉。这是 CLI 包装器的第一安全线。_run() 用参数列表而非字符串。注意我们传的是 ["ffmpeg", "-y", ...] 列表,绝不用 shell=True。这样即使文件名里含 ;rm -rf / 也只会被当成一个普通文件名,命令注入无从谈起。capture_output=True + 返回末尾 300 字符。ffmpeg 失败信息往往在 stderr 末尾,截尾回传既省 token 又保留了诊断价值。-y 覆盖输出。避免 ffmpeg 卡在交互式确认上(stdio 通道没有 TTY,一旦等输入就会死锁)。timeout 兜底。防止模型给一个超大文件让进程跑飞,拖垮整个会话。ModuleNotFoundError: No module named 'mcp':虚拟环境没激活,或装的是别的 mcp 包。确保 pip install "mcp[cli]"。-y,它在等你的确认。-c copy 是流复制(快但不帧精确)。需要精确时把 reencode=True,让它重新编码。在接入 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 链路,那会让你分不清是协议问题还是命令问题。
官方 SDK 自带一个图形调试器,能在不启动 Claude 的情况下手动点击调用每个工具、查看输入输出 schema。这是排查 MCP 问题最高效的手段:
mcp dev server.py
它会打开一个本地网页(通常是 http://localhost:6274)。页面里你能看到:
probe / trim / extract_audio / thumbnail / convert 五个工具及其参数表单;path: sample.mp4, start: 5, end: 10, output: clip.mp4,点 Run Tool;如果浏览器没自动弹出,手动打开终端里打印的 URL。该工具本质是
@modelcontextprotocol/inspector,也可以用npx @modelcontextprotocol/inspector python server.py启动。
@mcp.tool() 装饰器,且模块顶层没有报错(启动时的 import 异常会被吞掉,可在 server.py 顶部临时加 print 排查)。server.py 启动时抛了异常。在终端单独跑 python server.py 看报错——它应该静默等待 stdin,一旦打印了 traceback 就是 bug。确认无误后,把它接入 Claude Code。在项目目录下执行:
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 子进程的工作目录不一定是你当前目录,相对路径会失效。验证注册成功:
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 mcp list 显示连接失败:多半是启动命令路径/解释器不对。手动跑一遍注册时给的命令(python /abs/server.py),确认能正常进入等待状态。/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 原语擅长的事。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 是两次独立的工具调用——模型自行决定顺序、自行在两次调用间维护状态,你的服务器本身是无状态的,这能极大降低实现复杂度。
_safe() 收口,禁止越出工作目录。shell=True,永不 os.system,永不字符串拼接命令。timeout,对大文件考虑限制最大时长/分辨率。-y,杜绝交互死锁。ffmpeg 全部参数映射成工具。claude mcp add/add-json/list/get 全命令与三种 scope 详解FastMCP、mcp dev 调试器与传输协议权威实现probe + trim + thumbnail 三个工具,仍能跑通完整 demo;convert / extract_audio 作为课后扩展。claude mcp add 命令、scope 与 .mcp.json 配置权威说明FastMCP 装饰器写法、mcp run/dev 与 stdio 传输实现-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 等场景全覆盖。
