把文件系统和联网能力接到 Agent 上之后,很多人会卡在同一个问题:工具能列出来,边界却只写在提示词里,模型想写就能写、想抓哪个域名就抓哪个域名。2026-11-04 发布的 v2.3.0 把一部分边界挪到了服务端:filesystem 服务器新增只读模式与 Roots 约束,fetch 服务器新增主机白名单与可调超时。本教程按官方 Release 说明与 filesystem / fetch 两份 README,把这套边界逐条配进 MCP 客户端并验收,主线不需要任何 API Key。
uv(fetch 走 uvx 时)、一个可编辑配置的 MCP 客户端、一个用来做验收的临时目录。不需要 API Key;若已接入付费模型客户端,升级期间请留意模型调用本身会产生费用。先看清版本地图:仓库 tag 不等于包版本
这次升级最容易踩的坑在版本号上。官方 Release 的仓库标签是 v2.3.0,但七个包各有自己的版本号,彼此并不相同。写配置、锁依赖或报问题时,要核对的是你实际安装的那个包,而不是仓库标签。
| 包 | 仓库 / 版本 | 本版要点 |
|---|---|---|
| @modelcontextprotocol/server-filesystem | npm / 2.2.0 | append_file、copy_file、file:// 资源模板、只读模式与 Roots 约束 |
| @modelcontextprotocol/server-memory | npm / 2.1.0 | 写入类工具声明 idempotentHint、轻量读模式、MEMORY_FILE_PATH |
| @modelcontextprotocol/server-sequential-thinking | npm / 2.1.1 | 工具描述压缩到 1024 字符、限制每次连接保留的思考历史 |
| @modelcontextprotocol/server-everything | npm / 2.2.1 | 资源提示词的参数取值说明与工具注解测试 |
| mcp-server-fetch | PyPI / 2026.11.4 | --allowed-hosts 主机白名单、可配置的 HTTP 请求超时 |
| mcp-server-git | PyPI / 2026.11.4 | git_merge、只读的 git_remote、git_diff 支持修订范围 |
| mcp-server-time | PyPI / 2026.11.4 | get_time_ago、省略时区参数时回落到本地时区 |
版本口径与依赖:PyPI 三个包用的是日期式版本 2026.11.4,不是 2.3.0。fetch 的 README 还写明它要求 MCP Python SDK 2.x(mcp>=2.3,<3),自写客户端或固定依赖时要把这条一起核对。本版还包含两项安全依赖修复:filesystem 把 minimatch 提到 ^11.0.4(GHSA-g3hv-75wg-m249)、把 qs 覆盖提到至少 6.16.1,锁了旧版本的部署应当同步更新。下面所有旗标名与参数以你安装当天的仓库 README 为准。
Step 1:核对已装版本,建立可回滚的配置快照
官方建议用 uvx 直接跑 fetch、用 npx 跑 filesystem,这两条路线都不需要预先固定版本,因此更需要先看清当前解析到的版本。把客户端配置文件备份一份,再逐条查。
# 查 npm 侧的实际版本(filesystem)
npm view @modelcontextprotocol/server-filesystem version
npm view @modelcontextprotocol/server-memory version
# 查 PyPI 侧的实际版本(fetch / git / time)
python -m pip index versions mcp-server-fetch
python -m pip show mcp-server-fetch
# 备份客户端配置后再改(路径按你的客户端调整)
cp claude_desktop_config.json claude_desktop_config.json.bak
# 用 uvx 拉起时显式指定这一版,便于复现
uvx [email protected] --help
filesystem 的 README 明确要求 Node.js 24 或更高(走 npx 或从源码运行时);旧版 Node 会直接起不来。若你用 Docker 镜像 mcp/filesystem,版本由镜像标签决定,同样要先确认。
Step 2:用 --read-only 把 filesystem 锁成只读
官方对 --read-only 的定义是:服务器只注册带有 readOnlyHint: true 注解的工具。所有写工具(write_file、edit_file、create_directory、move_file、append_file、copy_file 等)都不会出现在 tools/list 里,调用它们会返回未知工具错误。这是服务端强制,与客户端怎么配置无关。
# 命令行直跑(旗标可以放在目录前后任意位置)
mcp-server-filesystem --read-only /path/to/project /path/to/docs
# Claude Desktop 的 claude_desktop_config.json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"--read-only",
"/path/to/project"
]
}
}
}
# 验收:让 Agent 列出工具,再试着写一次
# 预期:工具表里没有 write_file / edit_file,写入请求报未知工具
把允许目录写在命令行里是官方推荐做法,也是两代协议下都能生效的写法。README 还说明,这个旗标对 npx 与 Docker 两种启动方式一视同仁,并同时作用于来自客户端 Roots 的目录。
参数解析的兼容性坑:本版起 CLI 解析器会把 --read-only 与 -- 当作选项。若你的目录名恰好就叫 --read-only 或 --,必须写在选项终止符 -- 之后,否则会被当成旗标吃掉。另外,--read-only 覆盖全部允许目录,没有「只锁其中一个」的能力,需要分级就看下一步。
Step 3:用 --read-only-dir 做目录级分级
如果 Agent 既要读规格文档、又要往产物目录写结果,用 --read-only-dir <path> 更合适。它可以重复多次,每个受保护目录写一次;该目录本身也算允许目录,不必再单独列一遍。
# 参考目录只读、工作目录可写(可重复加多个只读目录)
mcp-server-filesystem --read-only-dir /path/to/reference /path/to/workspace
# 客户端配置里的等价写法
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"--read-only-dir", "/path/to/reference",
"/path/to/workspace"
]
}
}
}
# 验收 1:往受保护目录写文件,应被拒绝并给出目录名
# 验收 2:往工作目录写文件,应正常成功
# 验收 3:list_allowed_directory 之后会列出只读目录
官方写清了几条细节:写工具仍会列出(因为别的目录可写),但对受保护目录内的目标一律拒绝;move_file 的源或目标只要落在只读目录里就被拒绝,避免把整个父目录搬走;copy_file 可以从只读目录拷出、不能拷入;路径按符号链接解析后的实际位置判定,所以链接绕不过去。
ro,做纵深防御。服务器边界是防模型误调用,系统权限是防配置被改掉。启动前必须存在:README 明确,受保护的只读目录在服务器启动时必须能解析成真实目录;目录缺失、不是目录或是悬空符号链接,会让服务器直接拒绝启动,而不是留一个没锁住的路径。它只认启动时那一份标记,运行期间改不了;即便叠加 --read-only,也只是把所有写工具一起撤掉。
Step 4:用 --confine-roots 关住客户端 Roots
旧协议的客户端可以用 MCP Roots 在运行时替换服务器端的允许目录。加上 --confine-roots 后,客户端给的 Roots 只能收窄到命令行目录范围内,不能替换或超出。官方给出的交集规则是:Root 落在命令行目录内就原样允许;Root 包住命令行目录(例如 /)会被收窄到该目录;落在所有命令行目录之外的 Root 被忽略并记日志。
# 把 Roots 关在启动目录里
mcp-server-filesystem --confine-roots /path/to/project /path/to/docs
# 三个开关叠加的典型生产写法
mcp-server-filesystem --read-only --confine-roots /path/to/project
# 验收:用支持 Roots 的旧协议客户端发一个越界 Root
# 预期:越界 Root 被忽略并记录;全部 Root 都无效时访问被收回
这里有个容易误判的前提:README 写明 Roots 是 2025 时代客户端的便利机制,在协议版本 2026-07-28 已弃用,新协议连接不能使用 Roots。也就是说,这个旗标主要服务于还在用旧协议握手的那批客户端;对 2026-07-28 客户端,命令行目录就是入口。
别把它当权限系统:--confine-roots 约束的是目录范围,不约束读写动作;--read-only-dir 的标记由 Roots 也抬不掉,反过来 Roots 说不了「哪个目录能写」。官方还提示,若所有 Root 都被忽略,服务器会当成一次无有效 Root 的更新:在曾有过有效 Root 之后,访问会一直被收回,直到客户端再给出一个范围内的 Root。
Step 5:给 fetch 加主机白名单与三级超时
fetch 服务器本版新增 --allowed-hosts。条目是纯主机名:精确主机名只匹配自己;*.example.com 匹配任意层子域但不包含基域,要连基域就再列一条;也可以写 IP 字面量。不带协议、端口和路径。检查覆盖请求页、robots.txt 以及两者的每一跳重定向,工具调用与提示词调用都算。
# 用 uvx 启动,只放行两个域
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": [
"mcp-server-fetch",
"--allowed-hosts", "docs.python.org", "*.wikipedia.org"
],
"env": { "FETCH_TIMEOUT_MS": "60000" }
}
}
}
# 或用旗标设置全局超时(--allowed-hosts 可重复写)
uvx mcp-server-fetch --allowed-hosts docs.python.org --timeout-ms=60000
# 单次调用覆盖全局值(工具参数,单位毫秒)
# fetch(url="https://docs.python.org/3/", timeout_ms=5000)
# 验收:请求白名单外的主机,应被拒绝并报出被拒的主机名
# 验收:跳转链里出现白名单外的主机,同样应被拒绝
超时有三个层级,越具体优先:工具参数 timeout_ms 管单次调用,启动参数 --timeout-ms=60000 管全部调用,环境变量 FETCH_TIMEOUT_MS 在没有启动参数时兜底。三者都接受 100 到 600000 毫秒的整数,超出范围服务器会拒绝启动;默认是 30000 毫秒。
两道独立检查与私钥风险:白名单与私有地址守卫是分开的,把 localhost 或 10.0.0.5 列进白名单并不会自动放行,还得加 --allow-private-ips;而加上后者也不会取消白名单。fetch 的 README 顶部有明确警示:该服务器能访问本地与内网地址,默认只挡非公网地址,且不防 DNS rebinding,也无法检查只有代理才能解析的主机名。另外,Python 库层面新增的 allowed_hosts 参数改变了函数签名,直接 import 该库写代码的项目要重新核一遍调用。
Step 6:收尾其余包升级与回归自查
除 filesystem 与 fetch,本版还有一批增量:git 新增 git_merge(默认只做 fast-forward,显式设 fast_forward_only=false 才允许生成合并提交,要求干净工作树,冲突或失败会中止)与只读的 git_remote(列出 remotes 并对 URL 中的凭据脱敏),git_diff 开始接受 main..feature 这类修订范围;time 新增 get_time_ago 并让省略的时区参数回落到本地时区;memory 给三个写入工具声明 idempotentHint 并补了轻量读模式;sequentialthinking 限制了每次连接保留的思考历史。
# 升级 PyPI 三个包
python -m pip install --upgrade mcp-server-fetch mcp-server-git mcp-server-time
# 升级 npm 侧(若用全局安装或锁文件,按项目方式执行)
npm install @modelcontextprotocol/[email protected]
# 确认依赖修复落在锁文件里
npm ls minimatch qs
# filesystem 本版还新增了 file:// 资源模板
# 用 resources/read 读 file:///path/to/project/README.md
# 注意 resources/list 刻意返回空,服务器不枚举磁盘
# 回归清单(逐条在客户端里跑一次)
# 1. tools/list 里写工具是否按预期消失
# 2. 受保护目录写入是否被拒
# 3. 白名单外主机与越界重定向是否被拒
# 4. git_merge / git_remote / get_time_ago 是否可用
# 5. 原有读取与目录列举是否正常
官方对 file:// 资源模板的说明也值得记下:文件可以用 resources/read 直接读,但 resources/list 返回空,服务器不枚举磁盘;要用 list_directory、directory_tree 或 search_files 先找到路径,再按 URI 读,目录本身不是资源,越界或符号链接指向外部会返回 -32602。
升级顺序建议:先在测试客户端上把六步跑一遍,确认你依赖的工具名还在(只读模式会真的把写工具从列表里撤掉),再改生产配置。若你的 Agent 流程里有「让模型自己写文件」这一步,换成只读后它会拿到未知工具错误,需要同步调整提示词与验收脚本,而不是只改 MCP 配置。
做到哪一步算成功
用下面四条验收,四条都过才算这套边界真的生效:① 能说清自己装的每个包的实际版本,而不是只记住仓库标签 v2.3.0;② 在只读配置下,tools/list 里查不到 write_file 与 edit_file,让 Agent 写文件会得到未知工具错误;③ 分级配置下,往受保护目录被拒绝、往工作目录正常,且移动整个父目录也绕不过去;④ fetch 对白名单外主机与越界重定向都拒绝,单次 timeout_ms 能覆盖全局值。做到这些,再把配置模板提交到仓库并写清楚回滚方式。
常见问题速查
| 你遇到的现象 | 大概率原因与处理 |
|---|---|
| 加了 --read-only 后 Agent 报未知工具 | 这是预期行为:写工具已从 tools/list 撤下。改流程或去掉该旗标,别改提示词硬拗 |
| 加了 --read-only-dir 服务器起不来 | 受保护目录不存在、不是目录或为悬空符号链接,README 要求启动时必须能解析 |
| 目录名恰好是 --read-only | 被 CLI 当成选项了;写在选项终止符 -- 之后 |
| 白名单里写了 localhost 仍被拒 | 私有地址守卫是另一道检查,需要另外加 --allow-private-ips |
| *.example.com 放行不了 example.com | 通配子域不含基域,基域要单独列一条 |
| 设了超时大页面仍下载很久 | 超时按连接与每次读分别计时,不限制整次下载总时长 |
| --confine-roots 好像没起作用 | 检查客户端协议版本:2026-07-28 连接不使用 Roots,命令行目录就是入口 |
| resources/list 返回空 | 服务器刻意不枚举磁盘,先用 list_directory 找路径再按 file:// URI 读 |
交付前自查:本教程的旗标名、参数范围与行为描述来自官方 v2.3.0 Release 说明以及 filesystem、fetch 两份 README;包版本、依赖约束与工具清单会随后续发布变化,请以你安装当天的仓库 README 与 tools/list 实测为准。所有边界都是服务端约束,不等于操作系统级隔离;处理含凭据、内网地址或不可信网页内容的场景时,仍要按公司合规要求评估,并对 Agent 的实际改动自行审 diff。