news 2026/9/11 13:51:41

CLI-Anything Safari:用 Click 命令行驱动 safari-mcp,实现 macOS 浏览器自动化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CLI-Anything Safari:用 Click 命令行驱动 safari-mcp,实现 macOS 浏览器自动化

CLI-Anything Safari:用 Click 命令行驱动 safari-mcp,实现 macOS 浏览器自动化

【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything

clI-Anything 的 Safari 自动化 harness(cli-anything-safari)将一个标准 MCP 服务器包装成 84 个可直接调用的 Click 命令,使任何非 MCP 的 Agent 框架、bash 流水线、CI/cron 脚本和终端调试场景都能像操作普通 CLI 一样驱动真实 Safari 浏览器。读完本文,你将掌握该 CLI 的安装、命令结构、快照驱动工作流、安全模型与工具注册表再生成机制,并理解其 schema 驱动的“与上游 MCP 1:1 特征对等”设计原理。

背景:为什么需要一个 Safari 的 CLI 包装层

safari-mcp 描述的核心思路是:把 MCP 工具面“平移”到命令行,让不支持 MCP 的框架也能用。

该 CLI 的关键设计决策是schema 驱动:它不是手写 84 个命令,而是从捆绑的resources/tools.json工具注册表出发,在 import 时用 Click 动态生成每一个命令。这一设计保证了:

  • 特征对等:safari-mcp 暴露的每个工具、每个参数都能以相同名称和类型被调用(见 tool_registry.py 与 safari_cli.py 的_register_all_tools());
  • --help离线可用:内省命令只读本地注册表,不触碰网络、不启动 MCP 服务器;
  • 不干扰并发的 safari-mcp 实例(下文 Singleton-killer 一节会详细解释)。

一个需要提前明确的运行模型:每次 CLI 调用都会派生一个新的npx safari-mcp子进程,执行一次工具调用后立即退出(见 safari_backend.py)。因此如果你的 Agent 原生支持 MCP,直接使用 safari-mcp 会更快;本 CLI 的价值在于非 MCP 框架、bash 管道、CI/cron 和终端调试。

环境与安装

前提条件

  1. macOS(Darwin)——Safari MCP 仅支持 macOS;
  2. Node.js 18+——brew install node或从 Node.js 官网安装;
  3. Python 3.10+
  4. Safari开启Develop → Allow JavaScript from Apple Events菜单项。

安装步骤

cd safari/agent-harness pip install -e .

首次执行任意tool调用时,CLI 会通过npx自动下载safari-mcpnpm 包(约几 MB)。入口为main.py,命令名统一为cli-anything-safari

快速开始

# 探明工具面 cli-anything-safari tools count # → 84 cli-anything-safari tools list cli-anything-safari tools describe safari_click # 调用任意工具 cli-anything-safari tool navigate --url https://example.com cli-anything-safari --json tool snapshot cli-anything-safari tool click --ref 0_5 cli-anything-safari tool fill --selector "#email" --value "user@example.com" cli-anything-safari --json tool screenshot --full-page \ | python3 -c "import sys,json,base64; d=json.load(sys.stdin); open('/tmp/shot.jpg','wb').write(base64.b64decode(d['data']))" cli-anything-safari tool evaluate --script "document.title" # 交互式 REPL cli-anything-safari

其中tool evaluate --script是值得注意的参数名——上游 safari-mcp 的参数就叫script(而不是某些文档误写的code),test_parity.py 专门为此设了回归锁测试,防止文档与实现漂移。截图工具返回的是 Base64 编码的图片内容(_unwrap将 MCP 的ImageContent转为{"type":"image","data":"<base64>","mimeType":"image/jpeg"}),解码后即可落盘,见 safari_backend.py 的_unwrap()实现。

命令结构

命令用途
tool调用 safari-mcp 的任意一个工具(共 84 个,动态生成、schema 驱动)
tools内省捆绑的工具注册表(listdescribecount
raw逃生舱——按完整 MCP 名称 + 原始 JSON 参数调用工具
session进程内会话状态(上次 URL、当前标签页)
repl交互式 REPL(不指定子命令时默认进入)

每个子命令都可用cli-anything-safari <command> --help查看详细说明。

tool:schema 驱动的动态命令组

tool组的每个命令由注册表中的ToolSchema在 import 期构建(_register_all_tools())。参数映射规则(tool_registry.py):

  • 命名转换:MCP 的 camelCase 参数(如urlPattern)自动转为 kebab-case 的 CLI flag(--url-pattern),见_camel_to_kebab()
  • 类型映射:JSON Schema 的string/integer/number/boolean映射为 Click 对应类型,带enum的参数映射为click.Choice(大小写不敏感);
  • 布尔参数:以--flag/--no-flag成对形式暴露,必填布尔参数在 Click 层无法用required=True强制,因此在 runner 里显式校验(safari_cli.py);
  • object/array 参数:以 JSON 字符串传入,运行时用coerce_arg_value()解码(tool_registry.py),解码失败会给出友好错误而非堆栈回溯。

raw:绕过 schema 的逃生舱

当你手头已有一份 JSON 参数块、或想调用新工具时使用:

cli-anything-safari raw safari_evaluate --json-args '{"script":"document.title"}'

--json-args必须解码为 JSON 对象,且即使走 raw 路径,导航类工具的 URL 仍会经过安全校验(safari_cli.py),不会成为绕过安全层的后门。

tools:注册表内省

  • tools count——打印注册表工具数量(脚本友好,纯文本 84,--json输出{"tool_count": 84});
  • tools list [--filter <子串>]——列出工具,支持按名称子串过滤;
  • tools describe <名称>——支持完整名(safari_scroll)或短名(scroll)查询,输出每个参数的 CLI flag、类型、必填性、默认值和枚举取值。

内省命令(tools组)是唯一跳过后端可用性探测的子命令——因为tools只读本地注册表,见 safari_cli.py。

session 与 repl

Safari MCP 本身按调用无状态(每次派生新进程),但 CLI 在进程内保留极少量内存态(session.py):last_url(最近成功导航的 URL)与current_tab_index(最近活跃标签页索引)。session status可查看,REPL 的提示符会以tab<N> <url>形式展示当前上下文,方便在交互调试中感知所处页面。

REPL 支持的命令:

tool <name> 调用任意 safari-mcp 工具(用 tools list 查看名称) tools list 列出所有可用工具 tools describe <name> 查看工具完整 schema raw <name> 通过 JSON 参数调用工具 session status 查看当前会话状态 help 显示帮助 quit 退出 REPL

REPL 内部通过shlex.split解析输入行后复用同一cli.main()入口,并在捕获UsageError后继续循环(safari_cli.py)。

JSON 输出

所有命令都支持全局--json标志,输出结构化 JSON 供脚本与 Agent 解析:

cli-anything-safari --json tool snapshot cli-anything-safari --json tools list

错误也会统一以 JSON 形式输出({"error": ..., "type": ...}),且--json模式下错误信息同样保持机器可读(safari_cli.py)。

环境变量

透传给 safari-mcp 的变量

变量用途
SAFARI_PROFILESafari 配置文件名称(如 "Automation")
MCP_MAX_TABS每会话最大标签页数(默认 6)
MCP_MEMORY_CHECK_MS内存检查间隔(默认 60000 毫秒)
MCP_WEBKIT_LIMIT_MBWebKit 内存上限(默认 3000 MB)

这些变量在派生子进程时通过env=os.environ.copy()原样透传(safari_backend.py)。

CLI 自身消费的变量

变量用途
CLI_ANYTHING_SAFARI_BLOCK_PRIVATE设为1(或true)时阻止访问私有网络地址
CLI_ANYTHING_SAFARI_ALLOWED_SCHEMES覆盖允许的 URL scheme 白名单(逗号分隔)
CLI_ANYTHING_FORCE_INSTALLED测试模式:要求已安装 CLI 命令

其中私有网络阻止的默认值是关闭(开发友好,便于自动化本地仪表盘和开发服务器),test_security.py 明确验证了默认允许localhost127.0.0.1192.168.x.x

快照驱动工作流(推荐)

Safari MCP 的核心交互范式是“快照驱动”:snapshot返回结构化文本,其中每个可交互元素都带有ref ID,按 ref 点击比按 CSS 选择器更便宜、更可靠:

cli-anything-safari --json tool snapshot > /tmp/snap.json # Agent 读取 /tmp/snap.json,找到 ref 为 "3_12" 的 "Submit" 按钮 cli-anything-safari tool click --ref 3_12

关键约束:ref 在每次新快照后都会过期5_xx会变为6_xx),因此“快照 → 点击”要紧接着执行,不要在中间穿插其他快照操作。这一模式对应safari_snapshot/safari_accessibility_snapshot工具,CLI 不做重复的状态缓存(详见 tests/TEST.md 中关于 Session 无 undo/redo/snapshot 的设计说明)。

安全模型

多层防御

  • 标签页隔离:上游 safari-mcp 强制按会话隔离标签页所有权,避免一个会话操作另一个会话打开的标签页;
  • URL 校验:导航类工具(_URL_VALIDATED_TOOLS集合,按“参数名为url且类型为 string”这一启发式自动识别,见 safari_cli.py)在调用后端前先经过 security.py 的validate_url()。校验覆盖三层:
    1. 危险 scheme 黑名单filejavascriptdatavbscriptaboutchromewebkitsafarix-applefeed等 15 个 scheme 一律阻止;
    2. 私有网络访问:仅当CLI_ANYTHING_SAFARI_BLOCK_PRIVATE=1时启用,匹配 RFC 1918 网段、loopback、link-local 及对应 IPv6 前缀;
    3. scheme 白名单:默认只允许httphttps,可用CLI_ANYTHING_SAFARI_ALLOWED_SCHEMES覆盖。
  • Profile 隔离:用SAFARI_PROFILE单独创建自动化专用配置文件,与用户日常浏览数据分离。

威胁模型

validate_url()要防的是三类攻击(security.py 的模块注释):SSRF(Safari 可访问 localhost/内网)、scheme 注入(javascript:/file:/data:可在本地执行代码)、标签页所有权绕过。校验的边界情况(空串、纯空白、非字符串、缺少 scheme、缺少 hostname、未知 scheme)在 test_security.py 中有 22 个专项用例覆盖。

⚠️ Singleton-killer 警告

Safari MCP 在启动时会强制单实例:它会杀掉任何启动时间超过 10 秒的其他node …/safari-mcp/index.js进程。这意味着:

  • 运行cli-anything-safari(或任何其他 safari-mcp 客户端)会终止机器上并发的 safari-mcp 实例——包括正在为 Claude Code、Cursor 或其他 Agent 会话服务的那个;
  • 不要在两个 shell 里并行运行 CLI 调用;
  • 不要在本 CLI 运行期间,让其他 Agent 通过 MCP 传输层活跃使用 safari-mcp;
  • 这正是 E2E 测试套件必须用SAFARI_E2E=1门控的原因——直接运行会杀掉任何活跃的 safari-mcp 实例(tests/TEST.md 对此有详细论证,并说明端口 9224 被占用时上游会进入代理模式从而在实践中保护主实例,但门控仍作为防御性措施保留)。

工具注册表:如何再生成

safari-mcp升级后,需要重新生成捆绑的 schema 以保持特征对等:

python scripts/extract_tools.py \ /path/to/safari-mcp/index.js \ cli_anything/safari/resources/tools.json

extract_tools.py 是一个离线、零依赖的手写解析器:它扫描index.js源码中的server.tool(...)调用,用深度感知扫描器解析 Zod 修饰链(.optional().default().describe()、嵌套的z.array(z.object(...))),从而避免把嵌套字段的描述或可选性错误泄漏到外层参数。

再生成后运行对等测试,若 safari-mcp 工具数量变化则更新固定的工具计数:

python -m pytest cli_anything/safari/tests/test_parity.py

测试与质量保障

整个 harness 共95 个测试(详见 tests/TEST.md 的测试清单与实测结果),分层如下:

文件数量类别是否需要 Safari
test_core.py16单元测试(mock 后端与会话)
test_security.py36安全 / URL 校验
test_parity.py24CLI ↔ MCP schema 对等
test_full_e2e.py19E2E(CliRunner + 子进程)是(门控)

离线运行结果为 76 passed / 19 skipped;设置SAFARI_E2E=1 CLI_ANYTHING_FORCE_INSTALLED=1后全套 95 个测试通过,其中 3 个测试真实连接 Safari(列出标签页、导航并读取页面标题、子进程 JSON 往返)。实测日志还验证了截图工具返回的 Base64 解码后是合法的 JPEG(magic bytesff d8 ff e0),见 tests/TEST.md 的“Live verification log”一节。

其中parity 测试是本 CLI “与上游 MCP 完全一致”承诺的核心保障:它逐一断言注册表中的每个工具都能作为tool <short-name>被调用、每个 MCP 参数都有对应的 Click option、必填参数正确标记、枚举取值一致、tool组命令数与注册表严格相等(test_parity.py)。这类测试还锁定了若干历史上的解析器回归,例如safari_evaluate的参数必须是scriptsafari_run_script的参数必须是steps(数组)、safari_mock_route.response必须为必填 object 且描述来自外层.describe()而非嵌套的status字段。

重新运行测试:

# 离线套件(快,无需 Safari) python -m pytest cli_anything/safari/tests/ -v --tb=no # 含 E2E 的完整套件(需要 Safari + macOS + Apple Events) SAFARI_E2E=1 CLI_ANYTHING_FORCE_INSTALLED=1 \ python -m pytest cli_anything/safari/tests/ -v -s # 仅对等检查(“与 MCP 完全一致”承诺的核心) python -m pytest cli_anything/safari/tests/test_parity.py -v

故障排查

现象原因与解决
npx not found未安装 Node.js 18+:brew install node
safari-mcp package not found on npm registry网络问题或 npm 不可达,先检查npm view safari-mcp version
AppleScript execution failed未开启 Safari → Develop → Allow JavaScript from Apple Events
Tool cannot operate on tab it did not open标签页所有权保护触发,先打开新标签页再操作:cli-anything-safari tool new-tab --url https://example.com,随后再click
非 macOS 环境CLI 会在平台检查阶段直接拒绝(is_available()返回错误并退出)

此外,若捆绑的resources/tools.json缺失,_register_all_tools()会打印警告并提示重新运行 extract 脚本(safari_cli.py)。

总结

cli-anything-safari以“schema 驱动 + 动态命令生成”的方式,把 84 个 Safari MCP 工具完整、可内省、可脚本化地暴露到命令行:tool提供类型安全的参数映射,raw提供 JSON 逃生舱,tools提供离线内省,repl提供交互调试,而validate_url()与标签页隔离则守住浏览器自动化的安全底线。对于需要把 Safari 自动化接入 bash 流水线、CI/cron 或非 MCP Agent 框架的开发者,这是一个开箱即用的桥接方案;其“注册表对等测试锁死上游变更”的做法,也为其他 MCP→CLI 类 harness 提供了可复用的工程范式。想进一步了解设计取舍,可参考 HARNESS.md(harness 架构深析)、SAFARI.md(Safari 专项分析)与 tests/TEST.md(测试计划与结果)。

【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:49:06

内存泄露 Bug 的自动定位:基于 pprof 采样结果与 AI 堆栈分析

内存泄露 Bug 的自动定位&#xff1a;基于 pprof 采样结果与 AI 堆栈分析 在 Go 语言编写的后台长期运行微服务中&#xff0c;内存泄露&#xff08;Memory Leak&#xff09;往往是最折磨工程师的“慢性毒药”。它不像空指针解引用那样会立即触发 panic 并留下清晰的堆栈&#x…

作者头像 李华
网站建设 2026/9/11 13:46:14

随机森林RF分类建模实战:从原理到调参的完整指南

去年我接到一个客户流失预测的任务&#xff0c;数据是从业务系统直接导出的&#xff0c;二十多个字段里既有年龄、消费金额这样的连续值&#xff0c;也有性别、地区、注册渠道之类的离散值&#xff0c;缺失值大概占了百分之十几。一开始我用逻辑回归&#xff0c;光是特征工程就…

作者头像 李华
网站建设 2026/9/11 13:43:46

激光测距模组选型指南:三角法、相位法与ToF原理对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华