关于 plex-mcp
plex-mcp 是供 AI 助手访问 Plex Media Server 的 MCP 服务,涵盖媒体库及家庭成员观看记录,并支持目标明确的整理操作;不会修改媒体文件或元数据。
主要功能
- 01浏览并筛选媒体库项目
- 02搜索元数据和人物
- 03查看家庭观看记录
- 04管理播放列表和合集
- 05更新观看状态与评分
- 06可选播放控制
适用场景
- 01按导演或类型查找未看影片
- 02比较家庭成员的观看进度
- 03根据媒体库内容创建合集
- 04查看新增内容和继续观看项目
MCP 相关
向 MCP 客户端提供 Plex 媒体库、发现、历史记录和整理工具,以及提示词和媒体库资源。可通过 PLEX_READ_ONLY=1 禁用写入;播放工具为可选项。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y @dustinhansen/plex-mcpPLEX_URLPlex server base URL, e.g. http://<plex-host>:32400PLEX_TOKEN密钥Owner/admin X-Plex-Token (run `npx @dustinhansen/plex-mcp login`)PLEX_ALLOW_PLAYBACKSet to 1 to enable play-to-client toolsPLEX_READ_ONLYSet to 1 to register no write or playback toolsTAUTULLI_URLOptional Tautulli base URLTAUTULLI_API_KEY密钥Optional Tautulli API keyMCP_PATH_SECRET密钥The endpoint's only credential: a long random string in the URL path, e.g. from `openssl rand -hex 32`本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
22收录的工具列表 · 更新于 5 天前
此工具无需参数。
详细介绍
plex-mcp
一个 MCP 服务,可让你的 AI 助手全面了解你的 Plex Media Server:每个媒体库、已观看和未观看的内容、每位家庭成员的观看记录,以及安全的内容整理写入操作(播放列表、合集、观看状态、评分、plex.tv 观看清单)。
它直接调用 Plex 的 HTTP API(没有客户端库,仅有 2 个运行时依赖),以 npm 包和 Docker 镜像的形式发布,并且支持离线测试。
此服务中的任何操作都无法删除、移动或修改媒体文件或元数据。
你可以询问什么
- “我还没看过哪些 Scorsese 的电影?”
- “挑 3 部未观看的 90 年代惊悚片,时长不超过 100 分钟。”
- “今晚该接着看什么?”(每部剧集下一集尚未观看的剧集)
- “还有谁看过 Severance?谁看到的进度最靠后?”
- “孩子们这个月看了什么?”
- “本周电视节目库里新增了什么?”
- “把 Denis Villeneuve 执导且我还没评分的内容制作成合集。”
快速开始
需要 Node 22+ 和你拥有的 Plex Media Server。
1. 登录并获取凭据(通过 plex.tv PIN 流程,无需寻找令牌):
npx @dustinhansen/plex-mcp login
它会打印一个链接,等待你在浏览器中批准,然后打印 PLEX_URL=… 和 PLEX_TOKEN=…。添加 --write <file> 还可以将这两行追加到文件中。该文件会保存你的令牌,因此在 macOS 和 Linux 上,新建文件仅你本人可读。已有文件会保留其权限。--write 不接受以 - 开头的文件名,因此请使用 ./-name。(更喜欢手动操作?Plex Web → 播放任意项目 → ⋮ → 获取信息 → 查看 XML → URL 中的 X-Plex-Token。)
2. 将其添加到客户端。 任何能够启动本地 (stdio)服务的 MCP 客户端都可以使用。大多数客户端都需要将服务作为命令,并提供参数和环境变量。以下是 2 个 CLI 示例:
# Claude Code
claude mcp add plex -e PLEX_URL=http://<plex-host>:32400 -e PLEX_TOKEN=<token> -- npx -y @dustinhansen/plex-mcp
# Codex
codex mcp add plex --env PLEX_URL=http://<plex-host>:32400 --env PLEX_TOKEN=<token> -- npx -y @dustinhansen/plex-mcp
使用 JSON 文件进行配置的客户端(Claude Desktop、Cursor,以及任何读取 .mcp.json 的客户端)大多采用这种格式。客户端文档会说明文件的位置,以及顶层键名称是否有所不同:
{
"mcpServers": {
"plex": {
"command": "npx",
"args": ["-y", "@dustinhansen/plex-mcp"],
"env": {
"PLEX_URL": "http://<plex-host>:32400",
"PLEX_TOKEN": "<token>"
}
}
}
}
在浏览器或手机上运行的助手(claude.ai、ChatGPT)无法启动本地进程。它们需要以 HTTP 模式运行服务,并能从供应商的云端访问。请参阅 HTTP 模式和 Docker 和 docs/remote-access.md。
如果希望获取所有用户的观看记录,请使用服务所有者的管理员令牌。
工具
此服务默认注册 22 个工具,下面的可选工具还可额外增加最多 4 个。每个工具都声明了标题和全部 4 项 MCP 注解(只读、破坏性、幂等、开放世界)。规范要求客户端将这些注解视为提示,因此 安全模型 取决于服务注册的内容。
浏览与阅读
| 工具 | 功能 |
|---|---|
list_libraries | 列出所有分区及其 libraryType(Movies / TV Shows / Music / Photos / Other Videos)、扫描器类型、代理,以及是否正在扫描。 |
list_media | 可查询任意媒体库、任意类型(电影/剧集/季/集/艺术家/专辑/曲目/合集)。筛选条件:已观看、评分、流派、年份、年代、contentRating、合集、国家/地区、制片厂、厂牌、maxDurationMin、addedSince、lastViewedBefore、lastViewedAfter。最近观看时间筛选条件接受 ISO 日期或 -3y 这类相对值。从未观看的项目会被视为早于任何日期,因此应将 lastViewedBefore 与 watched: "watched" 搭配使用,以排除这些项目。排序方式:标题、年份、评分、观众评分、用户评分、添加时间、最近观看时间、观看次数、时长、随机。filters 对象会指出每个 genre、collection、country、studio 或 label 筛选条件匹配的名称,因此你可以看出名称是否匹配了更长的名称。collection 未指定 kind 时,会列出该合集包含的类型,因此电视剧库中的剧集合集会列出剧集。没有标签的智能合集会报错,并指出传给 get_children 的 ratingKey。支持分页(offset/limit/nextOffset)。fields 会将每条记录投影为所需的键。detail: true 会为每条记录添加演员、剧组和简介等字段,并将 limit 上限设为 25。 |
search | 在所有媒体库中进行全局搜索,包括人物、合集和播放列表。externalId(imdb:tt0113277、tmdb:949、tvdb:12345)会改为使用 Plex 保存的 ID 查找电影或剧集。 |
get_item | 根据 ratingKey 获取完整元数据和文件信息(路径、编解码器、大小及所有版本)。labels 列出该项目的 Plex 标签;如果没有标签,则为空数组。 |
get_children | 获取剧集(季)、季(集)、艺术家(专辑)、专辑(曲目)、合集(项目)或播放列表(项目;带有 playlist: true)的子项。电影、剧集或曲目没有子项,调用会失败并说明原因。支持分页(offset/limit/nextOffset)。fields 会将每条记录投影为所需的键。detail: true 会为每条记录添加演员、剧组和简介等字段,并将 limit 上限设为 25。 |
get_extras | 获取项目的预告片、花絮和幕后内容。 |
person_credits | 按导演/演员/编剧/作曲家查询影视作品列表,并支持已观看筛选。支持分页(offset/limit/nextOffset)。fields 会将每条记录投影为所需的键。detail: true 会为每条记录添加演员、剧组和简介等字段,并将 limit 上限设为 25。 |
library_changes | 获取指定时间点(-7d 或 ISO 日期)以来新增或更新的内容,分别以 added 和 updated 列表返回,每个列表的条数不超过 limit。detail: true 会为每条记录添加演员、剧组和简介等字段,并将每个列表的 limit 上限设为 25。 |
library_stats | 汇总已观看和未观看内容,并按流派/年代/评分细分,还可选择按用户统计播放次数。 |
发现
| 工具 | 功能 |
|---|---|
similar_items | Plex 的“更多类似内容”,可按是否已观看筛选项目。 |
random_pick | 从符合任意一组 list_media 筛选条件的项目中随机挑选 N 个(“给我惊喜、未观看、80 年代、时长不到 2 小时”)。每个结果都是 list_media 返回的同类记录。filters 会列出匹配的标签筛选条件,与 list_media 中相同;如果未指定 kind,collection 会挑选该合集包含的类型。没有标签的智能合集会报错,并指出应传给 get_children 的 ratingKey。detail: true 会为每条记录添加演员、剧组和简介等字段。user 会以 Plex Home 用户身份进行筛选,因此“未观看”表示该用户未观看。 |
continue_watching | Plex 的“继续观看”列表:每部正在追剧的剧集列出下一集未观看剧集,并包含看了一部分的电影,同时显示每项已播放到的位置。detail: true 会改为返回完整项目记录,并将 limit 上限设为 25。 |
get_watchlist | 服务器所有者的 plex.tv 待看列表,并标明各标题是否在此处的媒体库中,以及若在其中则显示其 ratingKey。支持分页(offset/limit)。 |
此外还有两个 MCP 提示词:movie_night 和 whats_new,以及一个
plex://libraries 资源。
历史记录与家庭用户
| 工具 | 功能 |
|---|---|
watch_history | 谁在何时观看了什么。可按用户、媒体库、时间范围或项目筛选。 |
now_playing / list_users | 实时播放会话和服务器上的所有账户。 |
continue_watching、list_media、random_pick 和 library_stats 接受
user 参数,可查看 Plex Home 用户视角下的媒体库(其观看状态和“接下来观看”列表)。Plex 仅允许对 Home(受管理)用户进行此操作。对于好友,watch_history 是适用于所有人的视图。
管理(写入)
除非设置了 PLEX_READ_ONLY=1,否则会注册。
| 工具 | 功能 |
|---|---|
set_watched | 将项目标记为已观看/未观看(仅接受明确指定的 ratingKey)。 |
rate_item | 设置或清除服务器所有者的星级评分。 |
manage_playlist / manage_collection | 创建、添加、移除、删除。删除只会移除分组,不会删除媒体。 |
manage_watchlist | 将媒体库中的电影和剧集添加到服务器所有者的 plex.tv 待看列表,或将其移除(仅接受明确指定的 ratingKey)。 |
scan_library | 为某个分区触发“扫描媒体库文件”,也可指定单个文件夹。 |
manage_watchlist 会将每个 ratingKey 解析为 plex.tv 用来识别项目的 Plex GUID。它仅接受电影和剧集,也就是 Plex 待看列表支持的类型;如果某个项目由旧版代理匹配(没有 Plex GUID),则会拒绝操作。这两项检查都会在发送任何内容之前执行。
可选:播放(默认关闭)
设置 PLEX_ALLOW_PLAYBACK=1 可注册 list_clients 和 play_item
(“在我的手机上播放这张专辑”)。这两个工具都使用 Plex 的播放器控制 API,因此目标必须是会向服务器通告自身的播放器。我们已使用 Plexamp 测试播放功能。当前的电视和桌面版 Plex 应用可能完全不会出现在 list_clients 中:即使应用已打开,Plex for Apple TV 和 Plex for Mac 也未显示。默认关闭,因为这是此处唯一会影响现实世界的功能。即使设置了该变量,PLEX_READ_ONLY=1 仍会阻止注册这两个工具。
可选:Tautulli
设置 TAUTULLI_URL 和 TAUTULLI_API_KEY 可添加 tautulli_stats(家庭统计:观看次数最多的内容、最活跃的用户、某段时间内的热门项目)和 tautulli_history(每次播放的记录,包含平台、播放器和已观看百分比)。Plex 自带的历史记录可满足基本需求。Tautulli 则提供许多管理员已经在收集的详细信息。它们需要 Tautulli 2.18.0 或更高版本,这是首个支持从请求标头读取 API 密钥的版本。
安全模型
- 不会修改媒体。 不存在删除、移动、重命名或编辑媒体文件或元数据的代码路径。删除播放列表或合集只会移除分组。
- 写入操作都需明确指定目标。 每次写入都会指明目标:通过 ratingKey 指定条目,通过 id 指定播放列表或合集,通过标题指定媒体库。扫描媒体库时可以将范围缩小到一个文件夹。创建合集时也要指定其媒体库:多个读取工具默认使用 Movies,因为读取不会造成任何更改,但写入操作绝不会自行选择目标。合集的类型不是目标,而是由指定的媒体库决定(电影、剧集,或音乐媒体库中的专辑),因此始终与合集的归属位置一致。若
kind与媒体库不符,则会在写入前拒绝操作;若媒体库类型不支持该工具提供的任何合集类型,也会拒绝操作。 没有任何操作会作用于“所有匹配项”。 - 播放功能需通过环境变量选择启用。 未启用时,不会注册相关工具。
PLEX_READ_ONLY=1会移除所有写入功能。 无论PLEX_ALLOW_PLAYBACK的值是什么,整理媒体的写入工具和播放工具都不会注册,因此客户端只能看到读取工具。Tautulli 工具属于读取工具,会保留。只读限制优先于选择启用项,因为配置为不进行任何更改的服务,不应仅因添加了另一个变量就开始更改内容。任何不是1、0或空值的值都会导致服务停止,而不是以启用写入的状态启动。- ID 必须由一串数字组成。 任何 ratingKey、播放列表 ID 或合集 ID 中只要包含其他字符,工具就会在请求发送到 Plex 之前拒绝操作(客户端也会拒绝任何无法解析到自身的请求路径)。我们这样决定,是因为工具会将 ID 放入请求路径中,而 Plex 会通过 GET 请求将条目标记为已观看、为条目评分,以及刷新媒体库。即使设置了
PLEX_READ_ONLY=1,包含..或?的 ID 也可能把读取操作变成写入操作。我们会拒绝无效 ID,而不是对其进行编码,因为真实的 Plex ID 无需编码。 - 错误信息绝不会回显来自 Plex、plex.tv 或 Tautulli 的上游响应正文,因为其中可能包含服务器路径和配置信息。请求失败时会指出上游服务、路径以及 HTTP 状态码或网络原因,例如当
PLEX_URL指向错误的主机或端口时,会显示Plex unreachable (ECONNREFUSED) on /library/sections。 - HTTP 模式未设置密钥时会拒绝启动。 见下文。
HTTP 模式和 Docker
默认情况下,服务通过 stdio 使用 MCP。设置 MCP_TRANSPORT=http,即可在 PORT(默认值为 8627)上提供可流式传输的 HTTP,并启用 /health 端点。Docker 镜像采用的就是这种模式。它实现的是 MCP 协议修订版 2025-11-25,与 SDK 1.x 系列使用的版本一致,而不是更新的 2026-07-28 修订版。在镜像之外,HTTP 模式仅监听 127.0.0.1。设置 MCP_HOST=0.0.0.0,即可接受来自其他机器的连接。该镜像已发布到
ghcr.io/dustinhansen/plex-mcp,支持 linux/amd64 和 linux/arm64,因此常见的做法是在已有容器运行环境中使用 Docker Compose:
services:
plex-mcp:
image: ghcr.io/dustinhansen/plex-mcp:latest
hostname: plex-mcp
restart: unless-stopped
ports:
- "127.0.0.1:8627:8627"
environment:
PLEX_URL: ${PLEX_URL:?}
PLEX_TOKEN: ${PLEX_TOKEN:?}
MCP_PATH_SECRET: ${MCP_PATH_SECRET:?}
端口仅发布在 127.0.0.1 上,因此这台主机之外的设备无法访问。要让局域网中的其他机器访问,请参阅
docs/remote-access.md。
${VAR:?} 这种形式会在缺少值时明确报错并阻止部署,而不是启动未经身份验证的容器。hostname 会确保容器重新创建后,发送给 plex.tv 的标识符保持不变(参见客户端标识符)。镜像会自行对 /health 执行 HEALTHCHECK,因此服务无需配置 healthcheck: 块。在与 compose 文件同一目录下的 .env 中填写这 3 个值(切勿将其放进镜像或仓库):
PLEX_URL=http://<plex-host>:32400
PLEX_TOKEN=<token> # from `npx @dustinhansen/plex-mcp login`
MCP_PATH_SECRET=$(openssl rand -hex 32) # the endpoint's only auth, so treat it as a password
不使用 compose 也可以尝试:
docker run --rm --hostname plex-mcp -p 127.0.0.1:8627:8627 \
-e PLEX_URL=http://<plex-host>:32400 -e PLEX_TOKEN=<token> \
-e MCP_PATH_SECRET=$(openssl rand -hex 32) \
ghcr.io/dustinhansen/plex-mcp:latest
MCP 端点为 http://127.0.0.1:8627/<MCP_PATH_SECRET>/mcp。密钥是具备写入能力的端点唯一的身份验证手段,因此未设置密钥时,服务会拒绝以 HTTP 模式启动(对于无法从不受信任网络访问的监听器,可通过 MCP_ALLOW_NO_AUTH=1 覆盖)。
- docs/remote-access.md:介绍如何通过局域网或公共 HTTPS 端点,将桌面或托管在云端的 MCP 客户端连接到 HTTP 模式的服务,以及相关的安全权衡。
- docs/docker.md:介绍更多 compose 设置、如何保持镜像为最新版本(Watchtower/Diun、固定版本)以及如何在本地构建。
环境变量
| 变量 | 必填 | 用途 |
|---|---|---|
PLEX_URL | 是 | 例如 http://<plex-host>:32400(若值不是 http 或 https URL,启动时会拒绝) |
PLEX_TOKEN | 是 | 所有者/管理员的 X-Plex-Token |
MCP_TRANSPORT | 否 | stdio(默认值)或 http(若为其他值,启动时会拒绝) |
PORT | 否 | HTTP 监听端口,范围为 0 至 65535 的整数(默认值为 8627) |
MCP_HOST | 否 | HTTP 监听地址(默认值为 127.0.0.1,镜像中为 0.0.0.0) |
MCP_PATH_SECRET | http 模式 | 能力 URL 密钥;http 模式下必填(至少 16 个字符,只能使用字母、数字、-、_、. 或 ~) |
MCP_ALLOW_NO_AUTH | 否 | 设为 1 时,以无密钥方式运行 http 模式(仅限受信任的网络) |
PLEX_ALLOW_PLAYBACK | 否 | 设为 1 时注册 list_clients / play_item |
PLEX_READ_ONLY | 否 | 设为 1 时,不注册任何写入或播放工具,无论 PLEX_ALLOW_PLAYBACK 的值为何(设为 0、留空或不设置时则会注册这些工具) |
TAUTULLI_URL、TAUTULLI_API_KEY | 否 | 两者都设置时注册 Tautulli 工具(仅设置其中一个,或 TAUTULLI_URL 不是 http 或 https URL 时,启动均会被拒绝) |
验证安装内容
这两个制品均由 release.yml 基于本仓库的带标签提交构建并发布,且都带有说明这一点的来源证明。
- npm。 npmjs.com 上的包页面会为每个版本显示来源证明徽章。在已安装该包的项目中,
npm audit signatures会检查注册表签名和来源证明。 - 镜像。
gh attestation verify oci://ghcr.io/dustinhansen/plex-mcp:vX.Y.Z --repo dustinhansen/plex-mcp会依据本仓库验证镜像的构建来源证明。
CI 和发布
.github/workflows/ci.yml 仅在 GitHub 托管的运行器上运行,触发条件为每个指向 main 的拉取请求、每次推送到 main,以及手动触发:
build:在 Node 22 上运行类型检查、仅编译的类型测试样例和离线测试套件(npm test)。artifact-smoke:在 Node 22(engines的最低版本)、Node 24 和 Node 26(高于最低版本的 LTS 版本线)上,安装打包后的 npm tarball(使用--omit=dev --offline),并通过 stdio 启动。container-smoke:为linux/amd64和linux/arm64构建镜像,分别在对应架构的运行器上运行,不使用仿真,然后针对本地 Plex 测试夹具启动。server-json:使用 MCP 注册表的验证端点,通过mcp-publisher validate检查server.json;发布所用的release.yml也会固定使用同一版本。该操作会将文件发送到注册表,但不会发布任何内容。
其令牌为只读,且没有任何作业会读取密钥,因此来自分叉仓库的拉取请求也会运行所有检查。npm test 中的 test/build/workflow-contract.test.ts 会确保两个工作流符合既定格式:操作固定到完整的提交 SHA、每个作业都设有超时、默认令牌为只读,并且只有在允许列表中明确列出的作业才有写入权限和发布密钥。
.github/workflows/release.yml 仅在手动发布 GitHub Release 时运行。它会针对标签重新运行 CI 检查,将标签与所有版本字段进行核对,然后暂存 npm 版本并发布镜像。我在 npm 上通过 2FA 批准后,npm 版本才会正式发布。之后我会批准 registry 作业,该作业会发布注册表条目:
| 发布 | npm @dustinhansen/plex-mcp | 镜像 ghcr.io/dustinhansen/plex-mcp | MCP 注册表 |
|---|---|---|---|
vX.Y.Z | X.Y.Z,使用 dist-tag latest,并附带来源证明 | :vX.Y.Z,适用于 amd64 和 arm64,附带证明;:latest 指向该版本 | io.github.dustinhansen/plex-mcp,版本为 X.Y.Z |
vX.Y.Z-rc.N,标记为预发布 | X.Y.Z-rc.N,使用 dist-tag next,并附带来源证明 | :vX.Y.Z-rc.N,附带证明;:latest 保持不变 | 不发布 |
发布检查清单和仓库设置见docs/publishing.md。
开发
npm install
npm test # build emitted JavaScript, then run offline compatibility tests
npm run smoke # build, then live checks against a real server (needs PLEX_URL + PLEX_TOKEN)
冒烟测试套件会针对运行中的服务检查每个 list_media 筛选条件:已观看、评分、年份、年代、类型、内容分级、合集、国家/地区、工作室、标签、时长、添加时间范围、上次观看时间早于指定时间以及上次观看时间晚于指定时间。如果存在 Home 用户,也会检查其视图。除“未观看”和“未评分”外,每项筛选检查都必须匹配到结果。“未观看”与“已观看”的总数必须等于整个媒体库的总数,“未评分”与“已评分”的总数也必须如此。如果服务中没有可用于测试某项筛选条件的内容,检查会记录一行跳过信息,并注明筛选条件。
它还会对 1 个普通合集、1 个剧集合集和 1 个智能合集运行 get_children 检查,因为 Plex 有 2 条路径可用于获取合集成员,而这两条路径都不一定总能正常响应。每项返回的总数都必须与合集自身的 childCount 相等。如果服务中某类合集没有包含成员的合集,检查会记录一行跳过信息,并注明该合集类型。
plex-mcp 使用 AI 编码助手(主要是 Claude Code)开发。每项变更都会通过拉取请求合并,CI 中的每个作业都是 main 上的必需检查。这涵盖类型检查、离线测试套件、npm 包和容器冒烟测试,以及 MCP 注册表对 server.json 的检查。
CHANGELOG.md 记录了各次发布。
设计说明
有限的工具集
plex-mcp 注册的是一小组工具,而不是覆盖整个 Plex API。我们这样决定,是因为每注册一个工具,都会在发起任何调用之前占用 MCP 客户端的上下文,而模型使用能够纳入上下文的工具集时表现更好。我们会先尝试将新功能作为现有工具的参数。你会看到,list_media 包含了筛选条件,否则这些条件会分散在十几个按类型划分的工具中。按照同样的原则,每个 Plex 列表只用 1 个工具。continue_watching 会读取待播列表,我们用你在 Plex 界面中看到的“继续观看”这一行来为它命名。它的 detail 参数会返回完整的项目记录,因此再为同一列表添加一个工具毫无意义。所以,如果 Plex 能做某件工具未提供的事,那就是我们有意省略的功能。
不过,我们愿意在未来扩展工具集。请提交一个功能请求议题,并告诉我们你会用它做什么,因为具体用途能帮助我们判断应该将它作为哪个现有工具的参数。我们通过议题跟踪器接收请求,而不是 GitHub Discussions,因为我只会在那个地方进行筛选。
Tautulli
提供 Tautulli 工具,是因为它们能回答 Plex 自身历史记录无法回答的问题。Plex 会记录谁在何时播放了什么,但它只知道播放设备,以及会话进行期间播放到了哪里。Tautulli 会记录每次播放的平台、播放设备和观看百分比,并按你指定的天数范围统计观看次数最多的项目和最活跃的用户。现有工具没有任何参数能够返回这些信息。
它们仍符合有限工具集的原则。除非你同时设置 TAUTULLI_URL 和 TAUTULLI_API_KEY,否则不会注册这两个工具,因此不会占用客户端的上下文。这两个工具都只读取数据,因此在 PLEX_READ_ONLY=1 时仍可使用;安全模型中关于不得回显上游响应正文的规则也适用于它们。
API 密钥通过 X-Api-Key 标头传递,绝不会放在请求 URL 中。我们这样决定,是因为 URL 会出现在 plex-mcp 与 Tautulli 之间任何代理的日志中,也会出现在 Tautulli 自身的访问日志中,而标头通常不会。出于同样的原因,Tautulli 的 API 参考也推荐使用标头。Tautulli 从 2.18.0 版开始读取标头,因此该版本是最低要求。如果你运行的是更早版本的 Tautulli,每次调用都会因缺少密钥而失败(HTTP 400),错误信息会提示你先检查版本。我们否决了继续将密钥放在 URL 中并告知用户这一做法,因为这只是在记录暴露问题,而不是消除它。我们也否决了先尝试标头、失败后再回退到 URL 的做法,因为回退机制无法区分旧版 Tautulli、错误密钥或错误请求,而且每次触发时都会再次将密钥放回 URL 中。
观看清单
观看清单只接受媒体库中的项目。我们这样决定,是因为任何从搜索结果中选择目标的写入操作,都会违反每次写入都要明确指定目标的规则。你会看到,manage_watchlist 像其他写入操作一样通过 ratingKey 指定项目;这里的媒体库中不存在的标题没有 ratingKey(要添加这类支持,需要按标题搜索 plex.tv Discover)。
get_watchlist 仍会显示这些标题,但其 ratingKey 为 null。
观看清单本身位于 plex.tv,而不是你的服务器上。它仍归在整理类写入操作中,因为这是服务器所有者的“我想看这个”清单。
远程访问
远程访问通过能力 URL 实现,由服务自身检查密钥。我们将检查放在进程内,因为任何转发所有路径的反向代理都会公开每个路径,包括裸路径 /mcp。端点 URL 中的路径密钥可授权每个请求。默认传输方式 stdio 完全没有网络暴露面。HTTP 模式使用可流式 HTTP(而非已弃用的 SSE 传输),并且是无状态的(每个请求都会创建一个全新的服务,所有实际状态都保存在 Plex 中)。请参阅 docs/remote-access.md。
HTTP 模式遵循传输方式的 2 条本地安全规则。除非 MCP_HOST 另有指定,否则它会监听环回地址;对于 Origin 既不是环回地址、也不是 MCP_HOST 指定的具体地址、也不是 null 的浏览器请求,它会返回 403。像镜像中的 0.0.0.0 这样的通配符 MCP_HOST 并未指定具体地址。此检查可阻止 DNS 重绑定(重绑定页面的 Origin 会指向攻击者的域名),且会在检查路径密钥之前执行。我们没有改为匹配请求的 Host 标头,因为重绑定请求的 Host 会指向同一域名,因此会通过检查。本地文件和沙盒页面发送的 null 来源会通过检查。服务不会发送 CORS 标头,因此此类页面无法读取响应,而且仍然需要密钥。没有允许来源设置。docs/remote-access.md 中的客户端不是浏览器页面,也不会发送 Origin,因此如果你在其他域名上托管浏览器客户端,该客户端会被拒绝。
监听地址由 MCP_HOST 指定,而不是通用变量 HOST。我们这样命名,是因为 shell 或 CI 环境可能出于自身原因导出 HOST,而继承的 HOST=0.0.0.0 会在不知不觉间将绑定地址从环回地址改到其他地址。因此,服务会忽略 HOST。我们保留了 PORT 这个普通名称,因为继承的端口可能导致服务无法访问,但不会让它脱离环回地址。
可选规范功能
我们有意没有实现几项可选的 MCP 规范功能。没有工具声明 outputSchema,因为 claude.ai 会丢弃任何输出架构带有 SDK 添加的 draft-07 $schema 标记的工具。我曾在自己的另一个 MCP 服务上发现了这一点。由于 HTTP 模式是无状态的(见上文),因此没有会话。此外,也没有针对每个工具的速率限制。通过 stdio 只有 1 个客户端,无需限制;通过 HTTP,路径密钥和 1 MiB 请求正文大小上限则是缓解措施。无论使用哪种传输方式,单次工具调用中数组元素和对象成员的总数最多为 1000 个(是任一工具接受的最长 ratingKeys 列表的两倍),因此低于正文大小上限的请求不会在检查参数时耗尽内存。
不过,我们愿意在未来添加这些功能。如果你的客户端需要其中某项,请创建一个 功能请求议题, 并说明它能让你实现什么。
发布
我们发布的所有内容都来自某个发布版本,绝不来自 main。我们这样设置,是为了让 npm 包和你安装的镜像都能追溯到构建它们的已标记提交(两者都带有构建来源证明)。只有手动发布的 GitHub 发布版本会触发 release.yml,该工作流会暂存 npm 包,并发布镜像和注册表条目。
工作流只能暂存 npm 版本;我在 npm 上通过 2FA 批准后,该版本才会正式发布。我们这样设置,是为了防止遭到入侵的工作流、操作或 GitHub 账户自行向 npm 发布版本。注册表条目需要等 npm 版本正式发布,而 npm 在我批准版本时不会发送事件,因此 registry 作业会在 GitHub 环境中等待,直到我也在那里批准它。每次正式发布需要 2 次批准(预发布版本会跳过注册表),而且无论如何我都会参与每次发布。我拒绝了在 npm 批准后手动启动单独的注册表工作流,因为它需要自行维护一份预发布版本检查。我也拒绝了轮询 npm 直到版本出现的作业,因为它会一直占用运行器,而且如果我批准得慢,还会失败。
我手动制作发布版本,因为 Release Please 之类的发布自动化会通过工作流打开发布 PR;除非使用长期有效的令牌,否则 GitHub 会让该 PR 的检查一直处于等待状态,直到有人手动批准。我的发布频率不高,而且由我独自负责,因此不值得为了自动化而一直保留那个令牌。CI 仅在 GitHub 托管的运行器上运行,arm64 镜像会在 arm64 运行器上原生构建,而不是通过仿真构建。
注册表条目
注册表条目会将镜像列在 npm 包旁边。我们同时列出两者,因为如果条目只列出 npm 包,那么通过官方 MCP 注册表发现 plex-mcp 的人就会看不到始终运行的 HTTP 服务(npm 包通过 stdio 运行,每个客户端对应 1 个进程)。镜像条目说明了安全的本地默认配置:端口仅发布到回环地址,路径密钥放在端点 URL 中。如果你需要向局域网或远程网络开放访问,请参阅 docs/remote-access.md。镜像带有 io.modelcontextprotocol.server.name 标签,注册表的包文档要求使用该标签作为所有权证明;还有一项测试会确保该标签、server.json 以及 package.json 中的 mcpName 和版本保持一致。
发布的端口
这里所有可直接复制粘贴的示例都只会将端口发布到回环地址。我们这样决定,是因为人们会从这个页面复制示例,因此示例发布的内容会直接成为默认设置,无须任何人主动选择。这里以及 docs/docker.md 中的 compose 和 docker run 示例都会发布 127.0.0.1:8627:8627,与注册表条目中的映射相同;一项测试会确保这 3 处保持一致。(镜像会设置 MCP_HOST=0.0.0.0,因为如果容器只监听自身的回环地址,就无法通过任何端口映射访问;因此,只有端口映射决定谁能访问服务。)
我们拒绝了在示例中保留 8627:8627,并将注册表说明改为称其会向局域网开放端口的做法。路径密钥仍然可以保护整个局域网都能访问的端口。但这样一来,README 描述的安全默认配置就会与注册表条目中的不同。
注册表验证
每个拉取请求都会询问注册表是否接受 server.json。我们这样做是因为发布版本会包含其带标签提交中的 server.json,而已发布的版本不可更改,因此如果注册表在此时拒绝某个文件,就只能通过发布下一个版本来修复。仅靠架构无法捕获注册表拒绝的所有问题;如果没有这项检查,注册表就要等到发布版本实际发布时,才会首次检查该文件。因此,server-json CI 作业会使用 release.yml 固定的版本运行 mcp-publisher validate。由于 release.yml 会先运行 CI 检查,每次发布前都会再次进行验证。
我们否决了仅在首次发布前进行一次性检查的做法,因为之后的每次发布都会编辑该文件。我们也否决了只在 release.yml 中进行检查的做法,因为它会在首次发布期间才首次运行,而从那以后,也只有在版本已打上标签后才会发现失败。该作业的下载 URL 和 SHA-256 值取自 release.yml,工作流契约测试会在两者不一致时失败,因此升级到新版 mcp-publisher 意味着要同时编辑两个工作流。代价是每次 CI 运行都会调用注册表 1 次;注册表宕机期间,检查会失败,直到有人重新运行。
公开历史
公开历史从 0.9.0 开始,起点是一个合并提交,而不是我开发该项目时使用的私有历史记录的重写副本。我们这样选择,是因为精心整理的重写很容易出错,而且强制推送无法清除托管平台自行保留的内容,例如已关闭拉取请求的引用。0.9.0 之后的更改都会作为拉取请求合并到此仓库。
密钥扫描
此处通过 GitHub 的密钥扫描与推送保护来拦截密钥,因此,携带已知令牌格式的推送会在到达仓库前被阻止。gitleaks 是我在发布前手动运行的检查,没有对应的 CI 作业。我们做出这个决定,是因为 CI 作业大多只会查找推送保护已经拦截的相同令牌格式。gitleaks 主要新增的是其 generic-api-key 规则,而在此仓库的历史记录中,该规则只标记过一个测试夹具。.gitleaks.toml 允许这个夹具(test/server.test.ts 中的路径密钥,按规则、文件和值匹配),并保留所有默认规则,因此 gitleaks git . 和 gitleaks dir . 都不会报告泄漏。检查任何发现之前,都应先将其视为真实问题。
默认端口
默认端口是 8627。我希望此服务拥有自己的默认端口。我将其从 3300 移开,因为 IANA 将 3300 分配给 Ceph 监视器(Proxmox 的 Ceph 集成会打开该端口),而且它位于 SAP 网关使用的 3300–3399 范围内,因此在 Proxmox+Ceph 节点上使用默认设置运行容器,会与存储集群冲突。IANA 注册表中未分配 8627,而且它避开了 Plex、常见的媒体服务器配套应用、Ceph 的 6800–7568 OSD 范围、Kubernetes 的 30000–32767 NodePort 范围,以及拥挤的 3000/5000/8000/8080 开发服务器端口。在手机键盘上,627 对应 MCP。如果需要其他端口,请设置 PORT。
运行时
服务器以生成的 JavaScript 运行。我们以这种方式发布,是为了让安装包仅包含 dist/ 和生产依赖,无需 TypeScript 编译器。TypeScript 会在测试、冒烟运行、打包和 Docker 构建之前完成检查和编译。源代码保留 ESM .js 导入说明符,以便 Node 在构建后解析相同的模块图。测试和脚本也使用 TypeScript,由 Node 自带的类型剥离功能运行,无需单独构建步骤;它们导入编译后的 dist/,从而测试实际发布的内容。类型剥离不会进行任何检查,因此 npm run typecheck 会再对它们运行一次 tsc(test/tsconfig.json)。
容器镜像
容器镜像基于 Debian slim(node:*-slim),而不是 Alpine。我们不再使用
node:*-alpine,因为 Node 的 musl 构建在 CI 交叉构建 arm64 时,总会触发 QEMU 用户模式
模拟漏洞(qemu: uncaught target signal 4 (Illegal instruction)),随后构建会卡住,而不是正常报错。基于 Debian glibc 的 Node 不会走到这条
代码路径,而且额外的镜像体积并不值得为了优化而再次引入这个漏洞。
现在 CI 会原生构建 arm64,不再涉及 QEMU,但正是这个漏洞让镜像继续使用 Debian slim。
如果你添加 compose healthcheck:,不要让它调用 wget 或 curl。精简镜像中两者都不存在,因此探测每次都会失败,即使服务正常响应也是如此。镜像自身的
HEALTHCHECK 会改用 node -e 和 fetch。
Plex 的特殊情况
分页之所以有效,是因为代码会通过 HTTP 标头发送容器分页参数(Plex 会静默忽略查询参数)。人员、流派、合集、国家和 标签筛选器接收名称,代码会将它们解析为 Plex 标签 ID。工作室则按名称匹配(Plex 会将工作室的 ID 列为其名称经两次 URL 编码后的形式,因此筛选器永远无法匹配)。未知的媒体库或用户会触发错误,并列出有效项。
如果你按仅在其他媒体库中使用的 label 筛选,就不会得到任何条目(Plex 会在每个媒体库下列出服务器上的所有标签)。任何媒体库都没有的
label 会触发与未知流派相同的错误。
get_children 可能会发起 2 次请求,因为 Plex 有 2 条路径可访问合集成员,但这两条路径都不是每次都能返回结果。元数据路径
(/library/metadata/<ratingKey>/children)不会为剧集合集和大多数智能合集返回结果。合集路径
(/library/collections/<ratingKey>/children)不会为已保存查询指定了其他服务器的智能合集返回结果。因此,代码会先读取元数据路径,只有该路径返回空结果时才尝试合集路径;这也意味着空剧集或专辑同样需要发起 2 次请求。我们不采用单独读取合集路径的方案,因为这样会漏掉这些智能合集,而且剧集、季、艺人和专辑都会返回 404。我们也不先查询条目类型,因为那样会给每次调用都增加一个请求,而且仍然需要备用路径。我们也不支持 collection 参数,因为省略该参数的用户会静默得到 0。
如果请求的是电影、剧集或曲目,Plex 会对元数据路径返回 400。因此,代码会将此处的 400 视为条目没有子项,并明确说明这一点,提示你停止操作,而不是重试。未知的 ratingKey,以及未设置 playlist: true 的播放列表 ratingKey,则会返回 404。我们保留 Plex 的 404,因为对这两种情况说“没有子项”都是错误的。
如果 collection 筛选器没有指定 kind,就会列出该合集包含的类型(Plex 中合集的 subtype)。电视媒体库的合集可能包含剧集,而按媒体库的顶层类型列出时结果会为空,因此 list_media 和 random_pick 会先读取 subtype。这样只会在调用中指定了合集但未指定类型时,额外读取一次媒体库的合集。如果你传入了 kind,代码会按原样发送。如果该合集包含工具没有对应类型的内容,或 Plex 没有列出该标题的合集,调用会使用未指定合集时采用的类型,因为列表必须按某种类型筛选。我们不采用在结果为 0 时改用另一种类型重试的方案,因为重试无法区分类型错误与筛选器确实未匹配到任何内容。我们也不在空结果中添加说明,因为这样仍会让你为了获取服务器已有的答案而发起第二次调用。要求 collection 必须搭配 kind 会导致当前所有可用的电影调用都无法使用,因此我们也不采用这种方案。
如果在 list_media 或 random_pick 的 collection 筛选条件中指定一个没有标签的智能合集,就会收到错误,其中会指出其 ratingKey 和 get_children。该筛选条件是 Plex 标签查询,而大多数智能合集都没有标签。如果没有这项检查,你会得到 No collection,或者名称会回退到较长的标签,从而列出另一个合集的成员。此检查会读取媒体库的合集,仅在没有标签与名称完全匹配时才会执行。如果智能合集确实有完全匹配的标签,则会像往常一样列出内容。我们没有改动这一点,因为我们检查的 3 个合集,其列出的总数都与各自的 childCount 相同。我们否决了在 list_media 中列出智能合集成员的做法,因为这样筛选条件就不再是 Plex 查询,其他每个筛选条件和排序方式都需要单独处理,而 get_children 已经可以完成这项工作。我们也否决了只在文档中说明这一限制的做法,因为 No collection 仍会产生误导,列出的成员也仍会有误。
每用户视图会在 Plex 允许的情况下切换为该用户自己的令牌(Home 用户),因为 viewCount 反映的是发起请求的账户。Plex 不允许对好友这样做。对于好友,可用的视图是 watch_history,而 library_stats 会根据观看历史统计每个账户的播放次数。切换到 Home 用户需要在 plex.tv 上执行 2 个步骤(plex.tv 通过 uuid 而非数字 id 切换用户,而且服务器会拒绝 plex.tv 返回的令牌,因此代码会用 plex.tv 列出的、该用户在此服务器上的访问令牌替换它)。如果 Home 用户受 PIN 保护,则会在任何切换发生前拒绝操作,并提示使用 watch_history,因为 plex.tv 只有在提供该用户的 PIN 后才会切换到该用户。我们不会将 PIN 设为工具参数,因为每个工具参数都会经过 AI 对话,而 PIN 会留在对话历史记录中。
即使 Plex 自带的 guid= 筛选条件做不到,search 使用 externalId 仍能通过 IMDb、TMDB 或 TVDB id 查找电影或剧集(在当前 Plex 代理中,该筛选条件只会匹配项目自身的 plex:// guid)。因此,工具会读取附带这些 id 的每个电影和剧集媒体库,并在进程内进行匹配。在一个视频媒体库约有 4,800 个项目的服务器上,读取所有项目用时不到 2 秒。
play_item 会先将项目加入播放队列,然后告知播放器播放该队列。我们之所以这样决定,是因为 Plexamp 会接受不带队列的播放命令(返回 200),但之后什么也不播放,因此工具会在播放实际上并未开始时报告成功。Plex 只有在请求中指定客户端时才会创建播放队列,因此两个播放请求都会携带与 login 使用的相同标识符(参见客户端标识符)。
客户端标识符
每台运行 plex-mcp login 的机器都会在 plex.tv 上显示为一台独立设备,
因此你可以在“已授权设备”中区分不同安装,并移除其中一台,而不必
将其他设备登出。plex-mcp 发送的标识符由 plex-mcp- 加上机器主机名
和用户名的哈希组成。同一台机器重启后仍会使用该标识符,而且无需
任何文件来保存它。
我们考虑过让所有安装都使用同一个固定标识符,但最终否决了这个方案,
因为 plex.tv 会将一个标识符视为一台设备。我们曾使用同一标识符登录两次,
plex.tv 两次都返回了相同的令牌,并且只列出一台设备;移除该设备会将两次
登录都登出。我们也考虑过在首次运行时生成一个随机标识符并将其保存到文件,
但也否决了这个方案,因为它需要一个可写路径,而容器不会保留状态,并且除非
传入 --write,否则 login 不会写入任何内容。
令牌属于运行 login 的设备。如果你使用来自 npx @dustinhansen/plex-mcp login 的
令牌运行容器,那么设备就是运行该命令的机器;两个使用相同令牌的安装也会被视为
同一台设备。若要让两个安装保持独立,请在不同的机器或用户账户上分别运行
login。compose 和 docker run 示例会将主机名设为 plex-mcp,这样容器
重建后发送的标识符仍保持不变(除非设置主机名,否则容器的主机名就是其容器 ID)。
缓存的名称
只要服务持续运行,库名称和账户名称就会一直缓存。如果你请求的名称不在缓存中,
服务会再次从 Plex 读取列表,然后才告知你该名称不存在。我们选择这种方式,是因为
容器运行期间添加或重命名的库恰好是缓存中尚不存在的名称,因此无需重启,也不会
为已知名称额外发起请求,就能找到它。我们否决了定时缓存,因为它需要增加一个时钟,
以处理多出的一个情况(按旧名称请求已删除的库),而这种情况本来就会返回 Plex 404。
来自 plex.tv 的 Plex Home 用户列表也采用相同的缓存方式,因此带有 user 参数的
调用不必每次都等待 plex.tv。Home 用户可能会发生库无法发生的变化(添加或移除 PIN,
或用户离开 Home),因此在 PIN 验证被拒绝或切换失败后,下次调用会重新读取列表。
如果 Plex 对 Home 用户切换后的令牌返回 401,服务就会丢弃该令牌,因为 Plex 拒绝的
令牌(例如访问权限已撤销)否则会一直导致每次调用失败,直到重启。下次调用时会重新
切换用户。
如果你在服务运行期间为某位 Home 用户设置了 PIN,那么在服务重新读取 Home 用户列表
(重启后、遇到不认识的名称时,或切换被拒绝或失败后)或者 Plex 对其令牌返回 401 之前,
该用户不会被拒绝。在此之前,带有其 user 参数的调用仍会正常工作。我们保留了这个
空档,因为 PIN 验证拒绝只反映 plex.tv 允许服务执行的操作。plex.tv 只有在提供该用户
的 PIN 后才会切换到受保护的用户,而服务持有的令牌是在 PIN 设置之前签发的。该令牌
能够读取的所有内容,服务所有者的令牌也都能读取,因此这个空档不会向运行该服务的人
展示任何新内容。我们否决了定时重新读取用户列表,因为这会增加我们上文针对库所否决的
时钟。我们也否决了在每次带有 user 参数的调用时重新读取列表,因为这会让 plex.tv
再次产生等待,而缓存的目的正是消除这种等待。
依赖项
plex-mcp 有两个运行时依赖项:@modelcontextprotocol/sdk 和 zod,并且没有测试框架。
我们将它保持得如此精简,是因为与媒体库一起运行、持有服务所有者令牌的服务应该易于
快速审查。测试使用 node:test,并通过注入的 fetch 模拟 Plex。
不在范围内:任何类型的媒体修改(参见安全模型)、Plex 服务管理,以及 *arr-stack 集成。
许可证
MIT。参见 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。