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 和终端调试。
环境与安装
前提条件
- macOS(Darwin)——Safari MCP 仅支持 macOS;
- Node.js 18+——
brew install node或从 Node.js 官网安装; - Python 3.10+;
- 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 | 内省捆绑的工具注册表(list、describe、count) |
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 退出 REPLREPL 内部通过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_PROFILE | Safari 配置文件名称(如 "Automation") |
MCP_MAX_TABS | 每会话最大标签页数(默认 6) |
MCP_MEMORY_CHECK_MS | 内存检查间隔(默认 60000 毫秒) |
MCP_WEBKIT_LIMIT_MB | WebKit 内存上限(默认 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 明确验证了默认允许localhost、127.0.0.1与192.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()。校验覆盖三层:- 危险 scheme 黑名单:
file、javascript、data、vbscript、about、chrome、webkit、safari、x-apple、feed等 15 个 scheme 一律阻止; - 私有网络访问:仅当
CLI_ANYTHING_SAFARI_BLOCK_PRIVATE=1时启用,匹配 RFC 1918 网段、loopback、link-local 及对应 IPv6 前缀; - scheme 白名单:默认只允许
http和https,可用CLI_ANYTHING_SAFARI_ALLOWED_SCHEMES覆盖。
- 危险 scheme 黑名单:
- 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.jsonextract_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.py | 16 | 单元测试(mock 后端与会话) | 否 |
test_security.py | 36 | 安全 / URL 校验 | 否 |
test_parity.py | 24 | CLI ↔ MCP schema 对等 | 否 |
test_full_e2e.py | 19 | E2E(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的参数必须是script、safari_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),仅供参考