mcpField
V
Video Extract

Video Extract

本地服务
github.com媒体与设计75 周前更新

关于 Video Extract

Video Extract 是供 AI Agent 使用的本地 MCP 服务,可从链接或本地文件下载视频、获取转录并筛选关键帧。视频获取与分析可分开使用,转录优先采用字幕,处理在本机完成,无需 API 密钥或上传。

主要功能

  1. 01解析并下载 URL 中的视频
  2. 02获取完整视频或指定片段
  3. 03获取字幕或本地转录语音
  4. 04依据场景和画面线索筛选关键帧
  5. 05批量处理视频并运行后台任务

适用场景

  1. 01查看视频转录和关键内容
  2. 02仅下载长视频中的指定片段
  3. 03使用浏览器 Cookie 分析受年龄限制的媒体

MCP 相关

MCP 服务向 MCP 客户端提供视频获取和分析工具。处理在本机运行,结果以摘要和文件路径返回。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"com-yanlinglabs-video-extract": {
"command": "npx",
"args": [
"-y",
"@yanlinglabs/video-extract-mcp"
],
"env": {
"VIDEO_EXTRACT_COOKIES_FROM_BROWSER": "<VIDEO_EXTRACT_COOKIES_FROM_BROWSER>",
"VIDEO_EXTRACT_MODELS_DIR": "<VIDEO_EXTRACT_MODELS_DIR>",
"VIDEO_EXTRACT_MAX_CONCURRENCY": "<VIDEO_EXTRACT_MAX_CONCURRENCY>"
}
}
}
}
启动命令npx -y @yanlinglabs/video-extract-mcp
需要配置的环境变量
VIDEO_EXTRACT_COOKIES_FROM_BROWSERSet to 'auto' to borrow browser cookies only when a platform refuses a download (age-restricted, members-only, or bot-checked media).
VIDEO_EXTRACT_MODELS_DIRWhere local speech models live. Defaults to ~/.cache/video-extract-mcp/models; they are fetched on demand.
VIDEO_EXTRACT_MAX_CONCURRENCYHow many analyses may run at once. Default 2; each peaks at about 2 GB when Whisper transcribes, 1.1 GB with SenseVoice.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npm@yanlinglabs/video-extract-mcp0.16.2

工具

3

收录的工具列表 · 更新于 2 周前

参数类型必填说明
videosarray是传入 analyze_video 或 resolve_video 时使用的相同视频 URL 或文件路径。

详细介绍

video-extract-mcp

给 AI Agent 一个视频链接:下载文件、读取转录文本,或只获取关键帧——这一切都在你自己的机器上完成。

两种用途,可单独使用:

获取视频。 YouTube 链接、TikTok 视频、微信视频号分享 URL、原始 .mp4 文件,或来自鲜为人知的网站的页面——它都能解析并下载整个视频,或只下载你指定的片段。如果你只需要这些,到此为止即可;不会强迫你进行任何分析。

或读取视频内容。 获取转录文本(平台提供字幕时使用实际字幕,否则使用本地语音识别),以及一小组重要关键帧——经过去重、感知场景并评分,而不是成千上万张几乎相同的静帧。

专为 AI Agent 打造。提供三个 MCP 工具,无云服务、无需 API 密钥、无需 Python。

npm 许可证:MIT Node TypeScript CI MCP MCP 注册表


claude mcp add --scope user video-extract -- npx -y @yanlinglabs/video-extract-mcp@latest
codex mcp add video-extract -- npx -y @yanlinglabs/video-extract-mcp@latest

功能

从几乎任何地方获取视频。 粘贴链接即可解析:YouTube、TikTok、Instagram、X、Facebook 和 Reels、Twitch、Vimeo、Reddit、微信视频号、裸 .mp4 或 .m3u8 文件,或来自鲜为人知的网站的页面。本地文件也适用。遇到不支持的内容时,会返回清晰的失败信息,而不是直接崩溃。

然后,你可以选择要完成哪些任务:

  • 仅查询信息。 获取标题、创作者、时长、章节列表、描述、评论数——无需下载任何字节。对于长视频,这样就能找到值得分析的片段。
  • 下载文件。 下载整个视频,或只下载第 300–420 秒的片段。平台支持分段获取时,只会传输该片段,而不会下载完整视频。
  • 获取转录文本。 平台提供字幕时使用实际字幕,支持任何已发布的语言;没有字幕时则使用本地语音识别。结果会注明所用方式。仅请求字幕时完全跳过下载——只需几秒,而不是几分钟。
  • 获取关键帧。 不会每隔 N 帧取一帧,而是提取场景变化、画面文字出现以及真正新颖的画面——经过去重和评分;对于一小时的视频,通常只需几十帧。你也可以选择均匀采样,或指定某一秒获取精确帧。
  • 一次处理多个视频。 传入视频列表并获取每个视频的结果;一个视频处理失败不会影响其他视频。
  • 访问私有或受速率限制的内容。 指定浏览器 Cookie 后,即可获取受年龄限制、仅限会员或仅限关注者观看的媒体,并绕过阻止匿名下载的机器人检测。
  • 在后台运行长任务。 一小时视频的完整分析需要几分钟;可将其作为后台任务运行,通过本地 HTTP 端点报告进度,即使客户端放弃等待,也能取回结果。

所有操作都在你的机器上完成。无需 API 密钥,无需上传,也不依赖第三方服务。

为什么要做这个

说到底有两个问题。首先是如何获取视频——每个平台都用不同的机制隐藏媒体,而且都不希望脚本抓取视频。其次是如何读取视频:大模型无法观看视频,而常见的变通办法是每隔 N 帧就把一帧塞进上下文窗口;这会在前后 98% 都相同的帧上浪费大量上下文,却仍然会漏掉画面没有其他变化时出现的那张新幻灯片。

video-extract-mcp 同时解决这两个问题:

  • 几乎从任何地方获取视频。 一条解析器链覆盖主流平台、直接媒体 URL 和通用网站;平台允许时会使用分段下载,并支持需要登录的网站使用 Cookie。下载是这款工具的完整用途,而不是通往其他功能的一个步骤。

  • 转录内容如实标明来源。 只要视频带有平台字幕,就优先使用平台自己的字幕——先用人工编写的字幕,否则使用平台自动生成的字幕。只有视频没有字幕,或已有字幕无法获取时,才会在本地转录音频(使用 Whisper 或 SenseVoice)。结果会通过 transcript.source 告诉你所用的来源;如果是本地转录,transcript.asrReason 会说明原因(no_captions 或 captions_failed;获取失败也会列在 warnings 中)。

  • 经过挑选的关键帧,而非抽样帧。 场景边界检测、模糊度/质量筛选、屏幕文字新颖度(能识别字幕,因此烧录字幕不会让冗余帧保留下来)和图像嵌入相似度,都会参与迭代式的多样性筛选。

  • 输出写入磁盘,而非塞进上下文。 工具回复只包含精简摘要和文件路径。35 帧的清单和完整转录文本,不该出现在只需要从中提取三个数字的 Agent 对话中。

  • 所有内容都在你的机器上运行。 不调用第三方 API、不上传数据、不需要密钥。较长的分析可以作为 MCP 后台任务运行——工具会立即返回句柄并推送进度;参见下文的“后台任务”。

快速开始

先安装系统程序;它们无法通过 npm 安装。你需要 ffmpeg(包含 ffprobe)、yt-dlp、Deno(yt-dlp 处理 YouTube 时需要)、带有中文语言数据的 tesseract,以及 Node ≥ 22.12。

macOS(已测试的平台):

brew install node ffmpeg yt-dlp tesseract tesseract-lang    # Homebrew's yt-dlp brings Deno along

Windows(尚未在此系统上运行;这些程序来自软件包清单),在 PowerShell 中执行:

winget install OpenJS.NodeJS.LTS
winget install yt-dlp.yt-dlp            # also installs Deno and an ffmpeg build with ffprobe
winget install tesseract-ocr.tesseract

然后打开新的终端。Tesseract 安装程序不会将自身添加到 PATH,而 winget 不会添加中文语言数据;INSTALL.md 介绍了这两个步骤。

Linux: 你的发行版提供的 yt-dlp 通常太旧,无法处理 YouTube,而且 Deno 没有打包提供。INSTALL.md 介绍了 Debian/Ubuntu 和 Fedora 的安装步骤。

安装完成后,重启你的 Agent,确保它启动的服务能找到新安装的程序。

然后将 MCP 客户端指向此软件包。有两种方法,区别值得你花 30 秒了解。

选项 A — npx,无需安装。 最简单,而且会自动获取新版本。

Claude Code:

claude mcp add --scope user video-extract -- npx -y @yanlinglabs/video-extract-mcp@latest

Codex:

codex mcp add video-extract -- npx -y @yanlinglabs/video-extract-mcp@latest

其他 Agent? 将 INSTALL.md 提供给它,它就能自行安装。文档涵盖 Gemini CLI、Grok、opencode、Hermes Agent、OpenClaw、Pi、Kilo Code、Cline 和 DeepSeek Harness,说明了每个配置方案的验证方式,并列出各自需要调高的工具调用超时时间:完整分析需要几分钟,而有些工具调用会更早停止等待。

保留 @latest——没有它,npx 会固定使用首次缓存的版本,之后再也不会更新。

选项 B — 全局安装。 启动更快,并且可以将 video-extract 状态 CLI 作为真正的命令使用。

npm install -g @yanlinglabs/video-extract-mcp

然后注册它——Claude Code:

claude mcp add --scope user video-extract -- video-extract-mcp

Codex:

codex mcp add video-extract -- video-extract-mcp
npx(A)全局安装(B)
更新仅当配置中带有 @latest 时才会自动更新——单独使用 npx -y @yanlinglabs/video-extract-mcp 会固定在它缓存的第一个版本,且永不更新手动更新:npm update -g @yanlinglabs/video-extract-mcp。运行更新命令前,会一直使用已安装的版本
启动约 0.9 秒(每次启动时都要解析 npm)约 0.1 秒
在 shell 中运行 video-extract status不在 PATH 中——需要运行 npx -y -p @yanlinglabs/video-extract-mcp video-extract status可直接运行
工作目录不能是此软件包自身的检出目录(见下文)无关

两者都不会影响 Agent 的能力:Agent 会通过回复中提供的 statusUrl,使用 HTTP 检查后台任务,而不会运行 shell 命令。CLI 面向的是人类用户。

或者,在任何 MCP 客户端的配置中使用——A 对应 "command": "npx", "args": ["-y", "@yanlinglabs/video-extract-mcp@latest"],B 对应 "command": "video-extract-mcp" 且不带参数:

{
  "mcpServers": {
    "video-extract": {
      "command": "npx",
      "args": ["-y", "@yanlinglabs/video-extract-mcp@latest"]
    }
  }
}

使用 npx 有一个坑,而且只有贡献者会遇到。 在此软件包自身的 git 检出目录中运行时,npx @yanlinglabs/video-extract-mcp 会失败并提示 command not found——npx 会看到本地 package.json 声明了这个名称,于是尝试在尚未填充内容的本地 node_modules/.bin 中查找二进制文件,最后放弃。由于 MCP 客户端会将工作目录设为你的项目,因此选项 A 在此代码库中无法使用。若要开发这个工具,请将该项目指向你的构建版本——claude mcp add --scope local video-extract -- node "$PWD/dist/mcp.js"——这样 npm run build 的结果会立即生效,无需往返发布。其他场景下,npx 都可以正常使用。

对于任何带字幕的视频,这就足够了——得益于优先使用字幕的转录策略,大多数视频都符合这一条件。视觉模型会在首次使用时自行下载。

只有完全没有字幕的视频才需要语音模型,并且会在首次遇到这类视频时自动下载。只会下载该视频所需的引擎——中文/日语/韩语模型为 233 MB,Whisper 为 1.3 GB——并保存到 ~/.cache/video-extract-mcp/models。设置 VIDEO_EXTRACT_AUTO_FETCH_MODELS=0 可保持手动下载,或者自行预先下载:

npx -y -p @yanlinglabs/video-extract-mcp@latest video-extract cookies   # installs the package, then exits
curl -fsSL https://raw.githubusercontent.com/yanlingLabs/video-extract-mcp/main/scripts/fetch-models.sh \
  | bash -s -- ~/.cache/video-extract-mcp/models

工具默认会在 ~/.cache/video-extract-mcp/models 中查找模型。使用 VIDEO_EXTRACT_MODELS_DIR 可覆盖此路径。如果禁用了下载或下载失败,没有字幕的视频仍会返回帧,并记录说明缺少转录内容原因的警告——此时功能会降级运行,而不是失败。

从源代码构建(贡献者)
git clone https://github.com/yanlingLabs/video-extract-mcp.git
cd video-extract-mcp
npm install && npm run build
./scripts/fetch-models.sh    # into ./models, which takes precedence when present
npm run preflight            # verifies ffmpeg / ffprobe / yt-dlp / tesseract
环境变量
变量用途
VIDEO_EXTRACT_MODELS_DIR语音模型的存放位置。如果该目录存在,默认为 ./models;否则默认为 ~/.cache/video-extract-mcp/models。
VIDEO_EXTRACT_AUTO_FETCH_MODELS设置为 0 可阻止按需下载语音模型。之后需要由你负责下载(scripts/fetch-models.sh);没有字幕的视频会降级处理,并给出相应警告。
VIDEO_EXTRACT_COOKIES_FILENetscape 格式的 Cookie jar 文件路径,可一次性用于所有 yt-dlp 来源——YouTube、Instagram、Facebook、X、TikTok、Twitch 等。请参阅需要身份验证的来源。
VIDEO_EXTRACT_COOKIES_FROM_BROWSER改为从本地浏览器加载 Cookie:chrome、firefox、safari、edge、brave、chromium、opera、vivaldi、whale,也可指定 browser:profile。设置了 VIDEO_EXTRACT_COOKIES_FILE 时,此项会被忽略。
VIDEO_EXTRACT_WECHAT_COOKIE用于微信视频号链接的元宝会话 Cookie。此项为可选,因为 userCookies 调用也可以从你的浏览器获取会话(请参阅每次调用,无需重启)。这是有意与上述设置分开的——它使用不同的协议和凭据。
VIDEO_EXTRACT_MAX_CONCURRENCY限制并发执行的 analyze_video 项数——普通调用和后台任务、批处理项目和独立调用都会计入同一限制。默认值为 2(每次分析可能需要约 2 GB;请参阅下方的内存说明)。resolve_video 不受此限制:它不会加载模型,因此无需节流。
VIDEO_EXTRACT_TASK_TTL_MS已完成的后台任务句柄在过期前可查询的时长。默认值为 1800000(30 分钟)。0(或任何非正值)表示句柄永不过期。此设置仅影响内存中的句柄——工具绝不会删除已写入 destinationPath 的文件,无论句柄是否过期。
VIDEO_EXTRACT_STATUS_PORT固定 localhost /status 端点的端口(请参阅查看进度)。未设置时,每次启动会选择一个临时端口(默认:启用端点)。值 0 会完全禁用该端点——请注意,这与上方的 VIDEO_EXTRACT_TASK_TTL_MS 不同,后者的 0 表示永不过期,而非禁用。
需要身份验证的来源

很多媒体并非公开内容,而解决方法与平台本身要求的一样:使用 Cookie。提供 Cookie 有两种方式:每次调用时提供,无需配置;或在环境中设置为长期配置。

每次调用,无需重启:userCookies

两个工具都接受 userCookies: true。这表示“用户同意本次调用访问自己的浏览器”:服务会从你的默认浏览器读取 Cookie,并且只将其发送到正在获取内容的网站。Cookie 不会保留,因此 Agent 下次必须再次询问——工具说明会要求它每次都先征得你的同意,就像 shell 工具在离开沙盒前会先询问一样。无需设置环境变量,也无需重启。

当请求因未发送 Cookie 而被拒绝(auth_required、auth_expired、rate_limited)时,回复会说明这一点,并建议向你询问。每个项目都会报告发送了什么,但绝不会报告 Cookie 值:

cookies含义
"none"未发送任何内容。
"browser:safari"(或 chrome、firefox、……)通过 userCookies 或 VIDEO_EXTRACT_COOKIES_FROM_BROWSER 获取的该浏览器 Cookie。
"cookies_file"VIDEO_EXTRACT_COOKIES_FILE 指定的 Cookie jar。
"wechat_cookie"VIDEO_EXTRACT_WECHAT_COOKIE。
需要登录时,它会在自己的页面上提示你。 一个无人提及的网站突然弹出登录页面,会让人感到不安。因此,服务不会直接打开登录页面,而是在浏览器中打开一个由它自己提供的小页面,地址为 127.0.0.1。页面只有几行纯文本,类似终端输出。它会指出你的 MCP 客户端(“Claude Code 正在尝试使用你的浏览器 Cookie 下载……”),说明缺少哪个网站的 Cookie(“未找到下载微信视频号所需的 yuanbao.tencent.com Cookie”),并以绿色显示登录链接。对于 YouTube、Instagram、TikTok、X、Facebook、Twitch、Vimeo、Reddit 和 Bilibili,链接指向网站自己的登录页面;其他网站则指向其主页。下方有一行实时状态信息和两个控件:[我已登录] 和 [不登录];后者会立即停止等待,并告知 Agent 不要重试。页面不会保存任何凭据;Cookie 仍从浏览器读取。浏览器会切到前台并显示该页面。调用最多等待 3 分钟;如果你未在此期间登录,调用就会失败,并提示你完成登录后重试。

实际使用时的微信登录过程(macOS、Safari、Claude Code):Safari 切到前台并打开该页面,绿色链接打开 yuanbao.tencent.com,用户用二维码登录,返回后按下 [我已登录],下载随即继续。

  • 微信视频号: 当浏览器中没有 yuanbao.tencent.com 会话时。页面会说明,微信视频号视频通过 yuanbao.tencent.com(腾讯的 AI 助手)获取,因此该网站会要求你使用微信登录。服务会自行通过检查 yuanbao 的 getuserinfo 检测登录状态,无需再按任何按钮。
  • 任何其他网站: 当网站仍不接受你浏览器中的 Cookie(例如内容为私密、年龄限制、仅会员可看,或触发“确认你不是机器人”检查)时。没有简单的方法可以检查你是否已登录任意网站,而且每次重试都会向该网站发起请求,因此只有在收到登录信号后才会重试:按下 我已登录,或该网站出现新的 Cookie 名称(登录会添加会话 Cookie;仅值发生变化不算)。最多重试 3 次;由 Cookie 触发时,间隔 10 秒。如果之后仍被拒绝,回复会说明该账号可能无权访问此视频。

每次使用微信会话前,都会通过 yuanbao 的 getuserinfo 进行检查。当令牌过期时,该响应会附带更新后的令牌,服务现在会使用它,而不是像早期版本那样将其丢弃。保留该令牌或许能减少手动粘贴的 VIDEO_EXTRACT_WECHAT_COOKIE 过期的次数,但尚未经过测量。从浏览器获取的会话会保存在内存中,并在每次使用时续期,因此后续调用会跳过浏览器读取。每次调用仍需传入 userCookies 才能使用该会话,而且会话绝不会写入磁盘。

值得注意:

  • “你的浏览器”指默认浏览器,因为登录页面会在那里打开,之后也会从那里读取 Cookie。如果 yt-dlp 无法读取默认浏览器(例如 Arc),则会退而使用一个已安装且可读取的浏览器,并在该浏览器中打开页面。
  • 读取 Chrome 系浏览器时会显示 macOS 钥匙串提示(“Chrome Safe Storage”)。选择“始终允许”可避免反复提示;出于同样的原因,等待登录时服务每 15 秒检查一次这些浏览器,而不是每 5 秒检查一次。Safari 不需要提示,但运行服务的应用可能需要“完全磁盘访问权限”才能读取 Cookie;如果需要,回复会说明这一点。
  • Firefox 可能不会立即显示新登录状态: yt-dlp 只会复制 cookies.sqlite,不会复制写前日志;最近的更改可能会一直留在日志中,直到 Firefox 将其合并。
  • 端到端测试仅在 macOS 上使用 Safari 完成,且只测试了微信。尚未实际观察过其他网站(YouTube、Instagram 等)的登录流程。Linux(xdg-settings)和 Windows(UserChoice)默认浏览器查询仅覆盖了代码路径测试。
常驻配置

一个 jar 即可覆盖所有 yt-dlp 来源——文件中的 Cookie 按域名划分,因此一次导出即可同时验证 YouTube、Instagram、Facebook、X、TikTok 和 Twitch。这并非仅适用于 YouTube 的设置。和任何环境变量一样,这些设置会在服务启动时生效。

export VIDEO_EXTRACT_COOKIES_FROM_BROWSER=auto       # recommended: borrow only when blocked

auto 会延迟处理,这正是它的优点。 普通请求完全不会发送 Cookie。只有当某个平台确实拒绝请求时,服务才会使用你的默认浏览器(与 userCookies 使用的浏览器相同),仅为该请求重试一次并附带其 Cookie,然后停止——不会循环重试。只有确实遇到拦截时,你才需要访问凭据存储;这也能避免每次播放公开视频时,借用的会话都被轮换掉。

另外两种模式会主动处理——每次请求都发送 Cookie:

直接读取浏览器的存储(chrome、safari、edge、brave 等):

export VIDEO_EXTRACT_COOKIES_FROM_BROWSER=firefox

或者指定导出的 Netscape 格式 jar:

export VIDEO_EXTRACT_COOKIES_FILE=~/cookies.txt

两者都设置时,以文件为准——它们是可选方案,不是成对使用的设置。

请留意操作系统钥匙串提示。 所有基于 Chrome 的浏览器都会使用系统密钥环加密其 Cookie 存储,因此首次读取时会弹出对话框,必须批准——在 macOS 上,提示为“Chrome Safe Storage”。Firefox 和 Safari 不会弹出此提示。如果请求在未发送 Cookie 的情况下被拒绝,回复会建议询问你是否使用 userCookies,并提醒你注意该提示,而不是让你自行发现。

除了登录之外,它还能解锁:受年龄限制和仅限会员观看的 YouTube 内容、大部分 Instagram 和 Facebook 内容、大量 X 内容、仅限订阅者观看的 Twitch 内容——以及 rate_limited / “请登录以确认你不是机器人”;与已登录的请求相比,匿名抓取更容易遇到此限制。

使用前值得了解以下三点:

  • 调用方无法指定 Cookie 文件或浏览器。 如果 Agent 可以这样做,就能读取机器上的任意文件,或选用机器上的任意会话。它最多只能传入 userCookies: true,这只表示在征求你同意后,为该次调用使用你的默认浏览器。
  • 你的 jar 永远不会被修改。 --cookies FILE 不只是读取该文件,还会在退出时重写它;工具会将你的 jar 复制到私有临时文件,把副本交给 yt-dlp,然后删除副本。代价是刷新后的 Cookie 不会写回。
  • 从当前已登录的浏览器导出 Cookie 可能会导致你退出登录。 平台会轮换会话 Cookie,而从活动会话中复制的 Cookie 会失效——YouTube 尤其容易出现这种情况。yt-dlp 的建议是:在隐私/无痕窗口中导出,然后不要退出登录就关闭该窗口。

路径错误会明确报错,而不会悄悄改为匿名抓取——无法读取的 jar 表示配置有误,不应被降级处理;静默返回匿名结果会让你去排查错误的原因。

检查实际配置内容,全程不会输出任何 Cookie 值:

$ video-extract cookies
cookie jar: /Users/you/cookies.txt
  .youtube.com      12 cookies  expires in 23 days
  .instagram.com     4 cookies  expires in 3 days
  .x.com             2 cookies  EXPIRED 5 days ago
  3 domain(s), 18 cookies, 1 with expired cookies

它会报告域名、数量和到期时间——绝不会显示名称或值,因此输出可以安全地粘贴到 issue 中。它还会指出大家常犯的错误:导出工具设置为 JSON 而非 Netscape 格式时,生成的文件看起来正常,却不包含 yt-dlp 可读取的内容;否则,这个问题可能直到更晚的时候才会在不相关的视频上表现为令人困惑的 auth_required。脚本调用时可添加 --json。

三种使用方式

MCP 服务是主要入口,但同一引擎还提供另外两种使用方式。

作为 CLI,这是在接入 Agent 之前,最快了解它能做什么的方式:

npm run cli -- "https://youtube.com/watch?v=..." --max-frames 10 --out ./output

# just the transcript, no frames
npm run cli -- "<url>" --frames none --out ./output

# one exact frame at 7s, as cheap as this gets
npm run cli -- "<url>" --start 7 --end 7 --frames even --max-frames 1 --no-transcript --out ./output

它会将 manifest.json 和帧图像写入 --out,并将清单打印到 stdout。

如果你想把该 JSON 传到其他地方,请直接调用构建后的入口点——npm run 会在 stdout 中加上自己的横幅行,因此 npm run cli 的输出本身不是有效的 JSON:

npm run build
node dist/cli.js "<url>" --max-frames 10 | jq '.transcript.source'

作为库,如果你希望在不让 Agent 参与的情况下运行整个处理流程:

import { analyzeVideo } from '@yanlinglabs/video-extract-mcp/dist/analyze.js';

const manifest = await analyzeVideo('https://youtube.com/watch?v=...', {
  start: 30, end: 90, frames: 'key', maxFrames: 12, outDir: './output',
});
console.log(manifest.transcript?.source);   // 'manual' | 'auto' | 'asr'
console.log(manifest.frames.map((f) => f.image));

对于预期中的失败,analyzeVideo 不会抛出异常——遇到 DRM 页面或失效链接时,会返回一个清单,其中 source.status 不是 'ok',并附有可读的原因。也请检查 processing.warnings:任何失败后被跳过的可选阶段都会在其中记录一条信息。

请注意,CLI 和库这两种方式都会运行编译后的输出。语音和视觉模型在独立的工作进程中运行,这些进程会在编译后的模块旁边查找;因此,直接运行 TypeScript 源文件会导致这些工作进程无法解析——它们会降级为警告而非错误,提示很容易被忽略。出于这个原因,npm run cli 会先进行构建。

工具

两个负责实际处理的工具,外加一个查询工具。

工具集刻意保持精简。早期版本有四个工具,其描述不得不着重强调哪些接受 URL、哪些接受本地路径——这说明设计有问题,而不是警告还不够醒目。

resolve_video — 查找视频,可选获取视频
resolve_video({
  destinationPath: string,          // required — shared by every item below
  videos: [{                        // one entry per video, at least one
    url:             string,        // required
    returnVideo?:    boolean,       // default false: metadata only, no download
    start?:          number,        // seconds; only with returnVideo: true
    end?:            number,
    comments?:       boolean,       // default false — slow on popular videos
  }],
})

单个视频——最常见的情况,直接写入 destinationPath:

resolve_video({
  destinationPath: "./out",
  videos: [{ url: "https://youtube.com/watch?v=..." }],
})
// -> ./out/metadata.json (+ the caption file it points at, e.g. auto.en-orig.vtt, when the video has captions)

一次调用处理多个视频——每个视频都会按数组顺序写入自己的子目录:video-1/、video-2/……

resolve_video({
  destinationPath: "./out",
  videos: [
    { url: "https://youtube.com/watch?v=..." },
    { url: "https://tiktok.com/@user/video/...", returnVideo: true },
  ],
})
// -> ./out/video-1/metadata.json
// -> ./out/video-2/metadata.json + source.mp4 (returnVideo: true)

默认情况下,它不会下载任何大型内容。你会获得标题、创作者、时长、平台提供的章节列表(如果有),以及简短的描述预览。通常这些信息足以决定下一步怎么做——而且还可以结合范围设置,构成让整个流程高效运转的工作流:

查看章节 → 发现演示从 12:04 开始 → 仅分析 12:04–20:00 → 跳过 90% 的下载、转录和帧处理。

analyze_video — 执行实际处理
analyze_video({
  destinationPath: string,                      // required — shared by every item below
  videos: [{                                     // one entry per video, at least one
    pathOrUrl:       string,                     // URL *or* a local file — both work
    start?:          number,                     // seconds
    end?:            number,                     // end === start means one instant
    frames?:         "key" | "even" | "none",    // default "key"
    maxFrames?:      number,                     // default 35
    transcript?:     boolean,                    // default true
    language?:       string,                     // optional override, e.g. "zh"
  }],
})

单个视频——最常见的情况,直接写入 destinationPath:

analyze_video({
  destinationPath: "./out",
  videos: [{ pathOrUrl: "https://youtube.com/watch?v=..." }],
})
// -> ./out/manifest.json, ./out/transcript.json, frame images

一次调用处理多个视频——每个视频都会按数组顺序写入自己的子目录:video-1/、video-2/……其中一个项目失败不会影响其他项目:

analyze_video({
  destinationPath: "./out",
  videos: [
    { pathOrUrl: "https://youtube.com/watch?v=...", maxFrames: 10 },
    { pathOrUrl: "./local-clip.mp4", frames: "none" },
  ],
})
// -> ./out/video-1/manifest.json, transcript.json, frame images
// -> ./out/video-2/manifest.json, transcript.json, no frame images (frames: "none")
  • "key" 会运行重要性筛选器,并返回去重后的最佳帧。
  • "even" 会在范围内均匀采样——maxFrames 设置采样密度,因此 30 秒内的 60 帧相当于 2 fps。
  • "none" 不返回任何帧。这样可以只获取转录文本。
  • 精确获取一帧:start: 7, end: 7, frames: "even", maxFrames: 1, transcript: false。

两种模式下,帧筛选都会限制在 start–end 范围内,转录文本也只覆盖选定范围。

get_status — 超时后获取结果
get_status({
  videos: [string, ...]        // required — the same URLs or paths you passed before
})

每个视频返回一条记录:finished 表示已完成,并包含原始调用本应返回的结果;running 表示当前已完成的处理阶段;或者返回 unknown。只有在调用确实超时时才使用它——详见下文。

恢复客户端已停止等待的调用

有些客户端会限制等待工具调用的时长。发生这种情况时,工作会继续进行——服务端会继续运行并写入文件;只有客户端停止监听。之后如需获取结果,请查询该视频:

get_status({ videos: ["https://youtube.com/watch?v=..."] })

传入你提供给 analyze_video 或 resolve_video 的同一个 URL 或文件路径;也可以一次查询多个。已完成的视频会返回与原始调用完全相同的结果;仍在运行的视频会返回已进行到的阶段。

无需提前准备任何内容——这正是这样设计的原因。每次调用都必须提供 URL,因此你始终有这个信息;而恢复功能之所以重要,恰恰是因为没人想到要提前设置。匹配必须完全一致:添加或移除 ?si= 参数都会变成不同的字符串,并返回 unknown,因为猜测你指的是哪个视频,比坦言不知道更糟。

记录保存在服务端内存中,并会在 VIDEO_EXTRACT_TASK_TTL_MS 指定的时间后过期,因此重启后的服务端也会返回 unknown——并会明确说明这一点,因为无论如何,destinationPath 中的文件才是持久化结果。

后台任务

这两个工具都支持任务模式。作为普通 MCP 工具调用时,上面的所有示例在任何客户端上都会完全按所示运行,无论客户端是否了解任务机制——但会有约 150ms 的最低延迟;对于实际下载而言几乎察觉不到,但对于廉价的纯元数据调用来说并非为零。作为任务调用时——MCP 客户端会使用(实验性的)MCP tasks 能力标记该调用——工具会立即返回句柄,而不是阻塞,并在工作运行期间推送进度。这对于 analyze_video 尤其重要,因为处理实际视频可能需要几分钟。

  • 状态消息描述批次的进度:对于当前正在运行的项目,会显示 "video 2/3: transcribing";对于正在等待并发名额的项目,会显示 "queued, 1 ahead"。客户端通过轮询查看状态(大约每 150ms 一次),因此每次轮询看到的都是一个快照,而不是实时逐阶段推送——在两次轮询之间开始并完成的阶段可能会被合并而不显示。
  • 取消是实实在在的,而非做做样子——且针对整个任务,而非单个项目。 如果任务中的项目都尚未开始执行,则可以完全取消:不会运行任何内容,也不会写入任何文件。一旦任何项目开始执行,整个任务就会拒绝取消——两个工具都一样——并显示消息,说明任务会完成并交付结果,而不是悄无声息地消失;一个包含五个视频的批次只要有一个项目已经开始运行,即使另外四个仍在排队,也会拒绝取消。resolve_video 从不排队,因此对正在运行的 resolve_video 任务发起取消,总会遇到拒绝取消的情况。
  • 句柄仅保存在内存中。 任务完成后,句柄会在 VIDEO_EXTRACT_TASK_TTL_MS 指定的时间后过期(默认 30 分钟),无论如何都会随服务端进程一起失效——即使仍有句柄待处理,服务端进程也会在标准输入关闭后立即退出。已写入 destinationPath 的文件不受影响——无论句柄是否过期,工具都不会删除这些文件。
  • 普通调用在任何地方都能使用,但有一个注意事项。 任务支持要求 MCP 客户端实现实验性的 tasks 能力;如果没有实现这项能力,两个工具都会按上述文档所述同步运行,只是会有上述约 150ms 的最低延迟。
  • 有些客户端完全无法使用任务——Claude 桌面应用就是其中之一,而且在被询问时会明确说明。在这种情况下,流程会使用普通调用,可能会触及客户端自身的时间限制;服务端仍会继续工作,而 get_status 可在之后获取结果。

查看进度

statusMessage(上文)是每次轮询时的快照——很有用,但内容会合并,任务完成后也会消失。对于生命周期更长的场景——从另一个终端查看、客户端重启后查看,或查看机器上所有服务正在处理的每个视频——每个服务也会运行一个小型本地状态通道,默认开启。它唯一的原则是:服务只报告观察结果,从不作出判断。 此通道中的任何响应都不会说任务“已过期”“卡住”或“正常”,也不会编造完成百分比——该由询问者通过轮询两次并进行比较来判断。

CLI。 安装后,video-extract status [--watch] [--json] [url...] 会发现机器上所有正在运行的 video-extract-mcp 服务——每个 MCP 客户端都会启动自己的服务进程,此命令会合并所有进程的数据,而不只是你当前正在交互的那个——并呈现一个视图:

https://youtu.be/AbC123xyz  resolving → downloading → transcribing  (45s in stage) · asrWorker pid 4122 · cpu 38.2s · workdir 892 MB
https://youtu.be/DeF456uvw  resolving → downloading → transcribing → frames  (done 2m ago)
https://tiktok.com/@u/video/789  resolving → downloading  (372s in stage) · yt-dlp pid 4210 · cpu 12.4s · workdir 412 MB
https://youtu.be/JkL012rst  queued
server pid 4098 · up 14m · cap 2 · running 2 · queued 1

每个字段都名副其实:按触发顺序排列的原始阶段名称、原始已用时间、实际执行工作的子进程及其累计 CPU 用量;对于仍在进行的项目,还会显示工作目录目前的字节数;已完成项目的目录不会再变化,因此其所在行不包含 workdir 子句。--watch 会每秒在原位置重新渲染,直到你按下 Ctrl-C;--json 会以纯 JSON 输出相同数据,合并所有正在运行的服务,且不含 ANSI 控制字节,供脚本使用而非阅读;任何位置参数都会将输出筛选为仅包含对应 URL(可重复指定)。没有正在运行的服务时,人类可读格式会显示 no live video-extract servers;--json 则输出 []——两种情况下退出码均为 0。

端点。 每个服务还会运行一个仅限 localhost 访问的 GET /status——默认绑定到一个临时端口——上面的 CLI 本身就是该端点的客户端。Agent 可以通过两种方式获取其 URL,因此无需通过 shell 命令查找:已完成结果中的 statusUrl 字段(端点禁用时为 null);以及对于目前只有句柄的任务模式调用方,句柄回复自身的 statusMessage,前缀为 status: <url>——端点禁用时则完全没有此前缀,而不是显示 null:

curl http://127.0.0.1:PORT/status
curl 'http://127.0.0.1:PORT/status?url=https://youtu.be/AbC123xyz'   # repeatable

VIDEO_EXTRACT_STATUS_PORT 可固定端口,而非随机选择;字面值 0 会完全禁用端点(见下方环境变量表)。

区分缓慢与卡住。 服务只报告观察结果,从不作出判断——轮询两次并比较 CPU 和字节数,即可区分缓慢下载与卡住。对于超大视频,下载静默六分钟很常见;而在 Wi-Fi 不佳时,这可能是个问题。此通道不会猜测是哪一种情况。间隔几秒获取一次 /status(或运行 video-extract status),比较相关项目的 childCpuSeconds 和工作目录字节数——数值变化说明仍在工作,数值不变则说明确实没有进展。

停止任务。 此通道没有 cancel 子命令——停止操作就是对状态输出显示的 PID 执行普通的 kill。可以指定两个目标,二者的行为有意不同:终止某个项目的子进程会使该项目如实失败,但批处理会继续,任务本身仍会完成;终止服务自身的 PID 则会停止所有任务,已写入 destinationPath 的内容会保留下来,效果与服务正常退出完全相同。

半下载文件会被清理。 下载进行中时会写入 .part 文件,只有在所有字节都已到达后才会重命名,因此即使进程被终止,也不会留下看起来像已完成视频的文件。崩溃、终止进程或重启后遗留的文件,会在下次调用该目录时被清理。

只会删除本工具自身创建的文件——它自己的部分下载文件和临时目录。绝不会删除清单、转录文本、帧或已完成的视频,也不会删除浏览器或你自己运行 yt-dlp 时留下的 .part 文件。 **你获得的是结果,而非中间产物。**处理流水线生成的内容远多于最终返回的内容——包括过滤前的所有场景边界候选,以及在 frames: "key" 模式下重新编码后的标准化视频。这些工作都在一个临时子目录中完成,项目处理完毕后该目录会被删除,因此 destinationPath 中会收到清单、转录文本、回复中明确列出的帧,以及 videoPath 指向的唯一视频文件。

服务退出时,内存中的状态历史也会随之消失——按设计,不会跨重启持久保存(参见 docs/follow-ups.md)。这不会导致重要内容丢失:无论如何,destinationPath 中的文件都是持久记录。

“重要帧”究竟意味着什么

每个候选帧都会根据以下信号评分:

信号捕捉内容
场景边界硬切、镜头切换——在边界后约 250–500 毫秒采样,以获取新场景,而非转场
屏幕文字新颖度文字发生变化的幻灯片;会考虑空间位置,避免将持续显示的字幕栏识别为“新内容”
视觉质量在参与竞争前排除运动模糊和失焦帧
嵌入相似度SigLIP 视觉嵌入,避免两帧看起来相同却都被保留

选择过程会迭代进行,并考虑多样性(最大边际相关性),而不是使用固定的加权总和——因此,选中一帧会改变下一帧的价值。每个返回帧都带有其 importance 分数和被选中的原因。

支持的来源

实际测试过的代码路径包括:YouTube、TikTok、Facebook 和 Reels、X/Twitter、Instagram、Twitch、Vimeo、Reddit、WeChat Channels,以及直接的 .mp4/.m3u8 URL。许多其他网站也能通过 yt-dlp 的通用提取功能正常工作。有些网站则不行;这类情况会返回明确的失败状态,而不会抛出异常。

特别值得一提的是对 WeChat Channels(视频号)的支持:它通过有文档说明的请求序列进行无头解析,不需要浏览器自动化或 MITM 代理。它需要已登录的 yuanbao.tencent.com 会话:可以通过浏览器中的 userCookies 调用获取(如果尚未登录,该调用会打开网站供你登录),也可以通过 VIDEO_EXTRACT_WECHAT_COOKIE 环境变量提供。该协议是在不参考现有实现的前提下,通过对 Tencent 自行提供的前端进行干净室分析和认证探测得出的——这是刻意为之,因为广为人知的那个实现采用 MIT + Commons Clause 许可证,会限制商业用途。

值得了解的设计约束

内存占用取决于并发数,不是固定上限。 语音识别和视觉嵌入都是重型模型,因此在一次分析中不会同时运行:它们各自在独立的工作进程中运行,并在下一个任务启动前退出。峰值取决于使用的语音模型。每次分析的实测内存占用:使用 Whisper 转录时约 2.0 GB(对于任何没有可用字幕的非 CJK 视频,Whisper 工作进程单独就会达到约 1.9 GB,26 分钟语音期间会缓慢升至约 2.1 GB);使用 SenseVoice 时约 1.1 GB(中文、日语、韩语、粤语);由字幕提供转录文本时低于 1 GB(此时图像嵌入阶段最占内存,约 0.8 GB)。总占用 ≈ 并发数 × 单次占用。默认上限为 2 ⇒ 最坏情况下需预留约 4 GB。VIDEO_EXTRACT_MAX_CONCURRENCY=1 会将并发限制为一次分析,约 2 GB;调至 4 则约为 8 GB。

仅使用一个 Node 运行时。 没有 Python 辅助进程,也不会启动第二种语言的运行时。语音识别使用 sherpa-onnx-node;视觉嵌入使用 @huggingface/transformers。

范围请求确实有效。 对于 yt-dlp 来源,若请求两小时视频的 30–340 秒片段,下载量约为五分钟的媒体内容,而不是两小时。直接 URL 和微信视频会先下载,再在本地裁剪。无论哪种方式,获取的片段都从零开始——回复会说明这一点,并提供偏移量。

降级情况一目了然。 如果 OCR 失败、嵌入失败,或语音识别出错,任务仍会继续,并记录警告。空转录文本始终可与视频完全没有语音的情况区分开来。

轻量请求开销低。 单帧请求会跳过场景检测、质量筛选、OCR、嵌入、转录,以及视频重新编码。实测耗时约 240 毫秒,无论源视频是 6 秒还是 5 分钟。

仅请求转录文本时,不会下载视频。 对带字幕的视频请求 frames: "none",媒体文件根本不会被获取——字幕即可回答问题,流水线中的其他步骤也不需要该文件。在一段 27 分钟的 YouTube 视频上实测,下载量为 888 KB,而不是 285 MB。此时回复会直接省略 videoPath,因为没有可供指向的本地文件。添加 start/end 范围后,则仍会像之前一样下载。

状态

截至 0.16.0(2026-09-23),已在 macOS(Apple Silicon)上针对真实视频完成实时测试:

  • 验收矩阵使用真实 URL 运行,已执行的 10 行全部通过(docs/acceptance-matrix.md)。测试涵盖带人工字幕和无字幕(Whisper)的 YouTube 视频、TikTok、Facebook、需要登录的 YouTube 视频(干净地返回 auth_required)、直接 MP4、嵌入网页的视频、微信视频号,以及中文 Bilibili 视频(SenseVoice)。范围下载测试行首次运行失败,因为 YouTube 拒绝了范围请求并返回 403。现在,遇到范围请求被拒绝时会回退为下载整个视频并在本地裁剪,因此该测试行已通过。DRM 测试行未运行;当时没有可用的 DRM 页面。
  • 从 X、Twitch 和 Instagram 下载视频也已通过实时测试,无需 Cookie。Vimeo 现在要求在 yt-dlp(2026.08.19)中使用已登录账户,因此需要提供 userCookies 并登录 Vimeo;尚未测试 Reddit。
  • 已使用 Safari 端到端测试浏览器 Cookie 和登录(userCookies),其中包括通过登录页面实际登录微信。
  • 821 项自动化测试全部通过,其中包括使用真实 MCP 客户端进行端到端测试的集成测试;CI 也会在 Linux 和 macOS 上使用 Node 22 和 26 运行这些测试。
  • 内存占用和单帧延迟均为实测数据,并非估算值。

尚未进行实时测试的环境:Intel Mac、CI 测试套件以外的 Linux,以及 Windows。代码中没有任何内容依赖 macOS,因此大部分功能应可在其他平台上运行,但所依赖的原生库因平台而异(请参阅平台支持)。

如果你能在其他平台上运行 npm run matrix,或针对上文未列出的站点运行测试,这将是对本项目最有价值的贡献。请见下文。

贡献

我们真诚欢迎贡献,也为你准备了清晰的入门路径。CONTRIBUTING.md 提供了完整说明,包括环境设置、一个否则会浪费你第一个小时的构建陷阱、测试标准,以及那些会悄然导致问题的不变量。简要来说:

优先做价值最高的事: 在非 Apple Silicon macOS 平台上,为文档中提到的环境变量设置真实 URL,然后运行 npm run matrix,并根据结果提交 issue。

也欢迎参与这些已有背景说明的事项: docs/follow-ups.md 记录了所有有意推迟的事项及其原因,包括基于真实视频素材校准选择器权重、文件末尾的候选边界、对直连来源和微信来源进行字节范围获取等。这些并非含糊的“适合新手的好 issue”标签;每项都说明了尝试过什么,以及为何搁置。

简单介绍一下项目规范:

  • 测试应当能在代码有问题时失败。本项目评审中最常见的问题,是测试无论如何都会通过——如果你添加了测试,请修改它所保护的内容,并确认测试会失败。
  • 不使用 Python。只使用 Node 运行时。
  • src/types.ts 是共享类型的唯一事实来源。
  • 保持每次分析的分阶段处理不变量:在单个视频的流水线中,耗费资源的阶段(语音识别、视觉嵌入)必须顺序运行,绝不并发——正是这一原则将每个并发分析的速率控制在约 2 GB(Whisper)或 1.1 GB(SenseVoice),而不是两者之和。不同视频之间则按设计最多同时运行 VIDEO_EXTRACT_MAX_CONCURRENCY 个分析。
npm test          # full suite
npm run typecheck # strict, with noUncheckedIndexedAccess
npm run matrix    # acceptance matrix (honest about skips)

要求

Node≥ 22.12
系统二进制文件ffmpeg、ffprobe、yt-dlp、Deno(yt-dlp 用于 YouTube 的 JavaScript 运行时)、tesseract(中文 OCR 需搭配 chi_sim)。各操作系统的步骤:INSTALL.md
模型约 1.5 GB,由 scripts/fetch-models.sh 下载——Silero VAD、Whisper small、SenseVoice
平台在 macOS/arm64 上开发;请参阅平台支持

语音识别会根据语言选择路线:zh、yue、ja、ko → SenseVoice;其他语言 → Whisper。不支持基于音频检测语言,因为已安装的库无论实际说的是什么,都会返回固定值——如果你知道语言,请提供 language。

平台支持

本代码在设计上不依赖特定平台,但其运行所需的原生库依赖平台,因此也决定了支持范围:

平台状态
macOS,Apple Silicon在此开发并进行了实测:使用真实 URL 运行验收矩阵,并从九个平台下载。语音识别库随附的运行时声明最低要求为 macOS 15.5;较早版本未经测试。
macOS,Intel自 1.24 起,onnxruntime-node 未再提供 Intel Mac 二进制文件,因此图像嵌入会使用 WebAssembly 后备方案:结果几乎相同,但速度慢好几倍,相关信息会记录在 processing.warnings 中。本地语音识别需要 macOS 15。尚未在真实的 Intel 硬件上运行。
macOS 12请参阅下文。
Linux(glibc)完整测试套件会在 CI 中运行;尚未针对真实 URL 进行实测。预计可以正常运行。
Windows尚未运行。引擎应该可以正常工作,但浏览器 Cookie 是薄弱环节:已知 Chrome 的应用绑定加密会阻止 yt-dlp 读取 Cookie。
macOS 12(Monterey)

一位贡献者在 macOS 12.7.6(Intel)上运行了该流水线,并记录了所需的操作(#5)。他们的设置修改了 onnxruntime 的二进制文件;而本次发布改用 WebAssembly 回退方案,该方案尚未在 macOS 12 上运行过。

  • 使用 Node 22,而非 26。 Node 26 无法在 macOS 12 上启动:其二进制文件需要一个 macOS 12 未导出的 libc++ 符号。
    dyld: Symbol not found: (__ZNSt3__122__libcpp_verbose_abortEPKcz)
      Expected in: /usr/lib/libc++.1.dylib
    
    此软件包支持 Node 22.12 及更高版本,因此请使用最新的 Node 22(nvm install 22)。
  • 图像嵌入需要使用回退方案。 onnxruntime 较旧的 Intel 二进制文件是基于比 macOS 12 更新的 libc++ 构建的,因此无法加载(Symbol not found: __ZNSt3__18to_charsEPcS0_d),而当前版本完全没有 Intel 二进制文件。回退方案不会使用原生库。
  • 没有字幕的视频无法进行本地语音识别。 sherpa-onnx 的 Intel 版本需要 macOS 15。有字幕的视频可以正常处理。
  • 不支持 OCR。 Homebrew 没有适用于 Monterey 的 tesseract bottle(运行 brew install --force-bottle tesseract 会返回 `--force-bottle` passed but tesseract has no bottle!),而从源代码构建则意味着要编译整个依赖树。帧仍会返回其嵌入、场景和转录窗口,并且 processing.warnings 会包含 ocr unavailable: ... spawn tesseract ENOENT;只有烧录在画面中的文字(以及微信视频号的 OCR 路径)无法识别。

许可证

MIT — 参见 LICENSE。


关键词:MCP 服务、Model Context Protocol、视频转录、关键帧提取、YouTube 转录、TikTok 下载器、微信视频号、Whisper、SenseVoice、SigLIP、yt-dlp、场景检测、AI Agent 工具、视频理解、本地 ASR、TypeScript、Node.js

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。