mcpField
S
Sallyport

Sallyport

本地服务
github.com/ginkida浏览器自动化103 周前更新

关于 Sallyport

Sallyport 是连接 Claude Code 或其他 MCP 客户端与 Chrome 的浏览器自动化桥接服务,面向需要通过 Agent 操作浏览器的场景。它通过 HMAC 配对、域名允许列表和按域名启用的 evaluate 限制操作范围,并支持多个会话共享浏览器。

主要功能

  1. 01通过 MCP/stdio 提供浏览器工具
  2. 02使用 HMAC-SHA256 验证通信
  3. 03按域名允许列表限制操作
  4. 04按域名选择启用 evaluate
  5. 05隔离会话标签页并记录审计

适用场景

  1. 01让 Claude Code 在允许的网站上操作页面
  2. 02限制浏览器自动化可访问的域名
  3. 03让多个会话并行控制各自标签页
  4. 04检查浏览器工具调用审计记录

MCP 相关

Python 守护进程通过 stdio 向 MCP 客户端提供工具,并通过 WebSocket 与 Chrome 扩展通信。客户端可调用导航、点击等浏览器操作,扩展按域名允许列表执行。

配置

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

暂未收录安装方式

我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。

代码仓库

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

Sallyport

CI CodeQL License: MIT Release

在 Claude Code(或任何 MCP 客户端)与 Chrome 之间搭建安全的浏览器自动化桥梁。它是 Kimi WebBridge 的替代方案,通过明确的安全边界取代隐式边界。

Claude Code ── MCP/stdio ──▶ daemon ── WS+HMAC ──▶ extension ── CDP ──▶ Chrome
状态数量
守护进程测试(pytest)539
扩展测试(vitest)1206
Lint / 类型检查(ruff、mypy、eslint、prettier、tsc)全部通过

包含内容

路径内容
extension/MV3 Chrome 扩展(TypeScript、esbuild、vitest)。以未打包扩展的形式加载。
daemon/Python MCP 服务。通过 stdio 使用 MCP 与 Claude Code 通信,并在 127.0.0.1:10086 上为扩展提供 WS 服务。
fixtures/两套测试共用的跨语言规范 JSON / HMAC 向量。
.pre-commit-config.yaml提交前执行快速 lint / 格式检查。
.github/workflows/ci.yml执行相同的检查,并在推送或 PR 时运行完整测试。

安全模型

更深入的威胁模型和已知限制见 SECURITY.md。 简而言之:原始 Kimi 扩展信任任何能够访问 127.0.0.1:10086 的进程,这在共享或遭入侵的机器上意味着 信任一切。Sallyport 在五个方面改变了默认设置:

  1. 每个帧都使用 HMAC-SHA256。 一个 32 字节的随机密钥存放在 ~/.config/sallyport/secret(chmod 600)中,并在首次运行时生成。两端 都会对每个 WS 帧签名,并验证时间戳偏差(≤ 30 s)和随机数的新鲜度 (滚动缓存 4096 个随机数,可防止重放,并在扩展服务工作线程被回收后持久保留)。一个 跨语言测试固定位于 pytest + vitest 中,确保规范 JSON 和 MAC 字节 逐字节兼容。
  2. 在扩展中强制执行域名允许列表。 对于主机不在 chrome.storage.local.sallyport_allowlist 中的 URL,工具拒绝运行。 模式可以是 example.com、*.example.com 或 https://x.com/path/*。校验器 会拒绝裸 *。
  3. evaluate 需要按域名明确启用。 即使域名已列入允许列表, 除非对应条目设置了 allowEvaluate: true,否则仍会拒绝任意 JS。其他 工具(click、fill、read_text 等)仅使用结构化 CDP 调用。
  4. 输入的纵深防御。 除非设置了 allowPassword=true,否则 fill 会拒绝 <input type=password>。守护进程拒绝绑定到任何非回环地址。 超过 16 MiB 的 WS 帧会被丢弃(1009)。
  5. 运行状态可见。 每次工具调用(及其结果——ok 或 error)都会追加到 chrome.storage.local.sallyport_audit(保留最近 500 条记录),可在弹出窗口中浏览并导出为 JSON。弹出窗口中的一键式 暂停 会断开 WS 连接并拒绝所有工具调用。

其他经过审慎考虑的选择:

  • 不注入内容脚本,不使用 <all_urls> 内容脚本。权限仅限调试器 API (以及弹出窗口固定图标的上下文菜单项)所需的权限 (tabs、debugger、storage、alarms、contextMenus)。
  • 每个标签页各自拥有无障碍引用(@e1、@e2)。对标签页 A 进行快照不会 使标签页 B 的引用失效,作用域为 A 的引用也无法解析到 B 中的节点。引用还会 绑定到其来源文档:发生任何跨文档导航后,包括页面自行发起的导航(点击链接、 提交表单、重定向),引用都会以 bad_ref 失败,而不会解析到新页面的 元素;并且在标签页仍打开时,ID 绝不会重新分配。
  • MCP 端的工具调用会按客户端串行执行,最多支持 8 个客户端并发运行。 扩展也会按标签页串行执行调用,以保护共享的浏览器状态。
  • 守护进程会在 stdin EOF(Claude Code 关闭)或 SIGINT/SIGTERM 时正常关闭:待处理调用会以 ExtensionNotConnected 失败, 客户端会收到 1001 关闭码,不会留下孤立任务。

扩展仍然信任的对象:任何拥有 ~/.config/sallyport/secret 读取权限的人。浏览器调试器终究就是浏览器 调试器——此桥接限制了它可操作的域名范围,以及可驱动它的对象。

设置

1. 构建扩展

扩展不在 PyPI 上——pip install sallyport(第 2 步)只会安装 守护进程。扩展位于此代码库的 extension/ 目录中,因此 你需要检出代码库才能构建:

git clone https://github.com/ginkida/sallyport
cd sallyport/extension
npm install
npm run build

输出会生成在 extension/dist/ 中。将其作为未打包扩展加载:

  1. chrome://extensions
  2. 启用开发者模式
  3. 加载已解压的扩展程序 → 选择 extension/dist

固定工具栏图标。

2. 安装守护进程

Sallyport 需要 Python ≥ 3.10(它使用 match 语句和 X | Y 类型语法)。请先运行 python --version 检查。

pip install --user sallyport

或者从源代码安装(用于开发):cd daemon && pip install --user -e .

这会在你的 PATH 上安装 sallyport-daemon 命令。请确认它已安装到位:

which sallyport-daemon   # should print a path; if not, add your Python
                      # user-scripts dir (e.g. ~/.local/bin) to PATH

首次运行时,守护进程会:

  • 在 ~/.config/sallyport/secret 中生成一个 32 字节的密钥(chmod 600)。
  • 将 base64 密钥打印到 stderr — 将其粘贴到扩展弹出窗口中。
  • 开始在 127.0.0.1:10086 上监听,并通过 stdio 使用 MCP 通信。

然后运行内置的设置检查,它会验证安装情况,并打印要粘贴到弹出窗口中的确切内容:

sallyport-daemon doctor

它会检查 Python 版本、密钥文件及其权限,以及端口是否空闲 — 随后打印配对密钥和剩余步骤。无法建立连接时,随时可以运行此检查。若只想重新打印密钥:

sallyport-daemon --show-secret
3. 在 Claude Code 中注册

添加一个 MCP 服务条目 — 可以直接编辑 ~/.claude/mcp.json,也可以运行:

# Use the ABSOLUTE path. A GUI-launched Claude Code often doesn't inherit your
# shell PATH, so a bare "sallyport-daemon" command silently fails to spawn.
claude mcp add sallyport "$(which sallyport-daemon)"

sallyport-daemon doctor 会打印这条确切命令,并预先解析好路径(如果控制台脚本不在 PATH 上,则会使用 python -m sallyport_daemon)— 如果不确定,可以直接从那里复制。结果如下:

{
  "mcpServers": {
    "sallyport": {
      "command": "/Users/you/.local/bin/sallyport-daemon",
      "args": []
    }
  }
}

重启 Claude Code。工具会显示为 mcp__sallyport__navigate、mcp__sallyport__click 等。

4. 配对扩展
  1. 打开弹出窗口。它会显示一个单卡片的 Pair 引导界面。
  2. 获取密钥。在终端中运行 sallyport-daemon doctor(或 --show-secret),然后复制打印出的内容。**注意:**当 Claude Code 为你启动守护进程时(步骤 3),首次运行提示会输出到守护进程的 stderr,而 Claude Code 不会显示它 — 因此,doctor/--show-secret 是查看密钥的可靠方式。
  3. 粘贴到弹出窗口的文本框中 — 你可以粘贴整段提示;弹出窗口会自动提取 base64 密钥,并显示 “✓ 已检测到密钥(32 字节)”。
  4. 点击 Pair。状态会变为 connected,并显示一个脉动的绿色指示灯,以及允许访问的网站和近期调用的概览。
5. 将第一个域名添加到允许列表

允许列表初始为空 — 默认情况下,每个工具都会拒绝所有 URL。在弹出窗口的 Allowlist 标签页中,添加一项,例如 example.com 或 *.github.com。仅当你确实需要在该主机上运行任意 JS 时,才勾选 allow evaluate()。

多个会话,共用一个浏览器(自动配置)

Sallyport 专为这种场景打造:你在使用 Chrome,同时一个或多个 Claude Code 会话在其中操作标签页。开箱即用:第一个 sallyport-daemon 会在后台启动一个小型代理程序并连接到它,之后每个会话都会连接到同一个代理程序。你可以按照步骤 3 在任意数量的项目文件夹中注册 Sallyport,无需进行其他设置。

# Nothing. Just start a second Claude Code session.

在底层,代理程序负责扩展连接和端口;每个会话都会通过密钥旁的 0600 Unix 域套接字(broker-10086.sock)中继其 MCP。此前,第二个会话会直接启动失败——因为第一个会话占用了端口——因此完全无法使用浏览器工具。

你将获得:

  • 会话不会互相阻塞。 不同会话的调用会并发执行(单个会话自身的调用仍按顺序执行,而且同一时间只有一个调用会操作某个标签页)。一个 Agent 等待 30 秒,不再会让其他 Agent 的下一次点击也卡住。
  • 每个 Agent 只操作自己的标签页。 Agent 只能查看和操作自己创建的标签页(不指定标签页调用 navigate 时会打开新标签页);它无法操作——甚至无法列出——你的标签页或其他 Agent 的标签页。list_tabs 只会向会话显示其自己的标签页。
  • 不会抢占你的焦点。 每个会话的标签页都位于各自未聚焦且已静音的窗口中。如果 Chrome 尝试将新窗口置于前台,就会重新聚焦你正在使用的窗口(仅当新窗口确实抢占了焦点时才会如此——不会将已切到后台的 Chrome 拉到前台)。在这些窗口中也可以截取屏幕截图,无需将其置于前台。Agent 关闭自己的标签页或离开该页面时,不会出现“离开网站?”/“关闭网站?”提示——Chrome 会将 Agent 窗口置于前台显示提示并等待你点击——但你自己的标签页,以及你使用过或当前正查看的 Agent 标签页,仍会触发此提示。
  • 你可以查看操作记录。 弹出窗口的审计标签页会在每一行标注所属会话(默认使用会话的文件夹名称),其Agent 标签页部分会列出各会话留下的标签页,并提供一键清理功能。
  • 你已登录。 这些都是你真实 Chrome 配置文件中的普通标签页,因此 Agent 可以操作你已登录的网站——始终受允许列表限制。这里不涉及无痕模式或独立配置文件:隔离的是谁可以操作哪个标签页,而不是用户身份。

实用设置项:

sallyport-daemon --no-broker            # single session, own the port directly
                                        # (or SALLYPORT_NO_BROKER=1)
sallyport-daemon --session-label review # name this session in the audit log
sallyport-daemon broker --idle-exit 3600  # start one explicitly, exit when idle
sallyport-daemon doctor --stop-broker   # stop it (needed after an upgrade:
                                        # a broker outlives `pip install -U`
                                        # and serves its build to everyone)

代理程序按设计会长期运行,因此 doctor 会将其视为预期配置,而非过时的守护进程:端口检查会报告正常,doctor --kill-stale 也会让它继续运行。

这是一个共享配置文件的软件级隔离,而不是操作系统级沙箱:安全底线仍然是“任何以你的身份运行的进程”(它可以读取密钥,并在允许列表范围内操作浏览器)。完整的安全模型以及标签页所有权和 MCP 客户端身份验证不变量,请参阅 SECURITY.md。

工具

名称说明
list_tabs不检查允许列表——列出标签页不受限制。
navigate根据允许列表检查目标 URL。waitFor={selector?,text?,absent?,timeoutMs?} 会在加载后轮询,直到页面真正可用(单页应用可能在“已加载”很久之后才渲染)。
reload通过 bypassCache=true 执行硬刷新。受允许列表限制;引用会失效。
history_go在标签页的会话历史记录中前进/后退——无需知道 URL 即可返回上一页。direction='back'|'forward',steps 可一次跳转多个条目(中间页面不会加载)。在任何操作发生前,会检查目标条目是否在允许列表中(Page.getNavigationHistory → Page.navigateToHistoryEntry,不使用 evaluate);跳得太远时返回 no_history,并说明实际能到达多远——两种错误都不会透露被阻止页面的主机名,以避免通过不在允许列表中的浏览历史进行探测。发生 timeout 时,不要假定操作失败:超时监视器触发前,跳转可能已经完成,因此请用 read_text/snapshot 检查,而不是盲目重试。反过来,如果 beforeunload 提示框取消了跳转(默认会关闭提示框),也会捕获这种情况——返回 navigation_cancelled,而不是在标签页实际上未到达目标页面时悄悄报告成功(未从前进/后退缓存中提供的跳转属于实时导航,因此检查会容许合法的跳转中重定向,并报告标签页实际到达的位置,而不只是所请求条目的 URL)。引用会失效;可选 waitFor。
close_tab必须提供 tabId——不会隐式回退(关闭错误的标签页会丢失工作)。
snapshot提供带稳定 @eN 引用(按标签页区分)的无障碍树,并剔除布局噪声。如果无障碍树看起来异常稀疏,会与 DOM 遍历结果(使用相同引用)交叉核对——适用于 Telegram Web K 等类似的单页应用。mode=auto|a11y|dom;compact=true → 仅返回可交互元素的扁平列表;selector 将范围限定为一个子树。
(embedded) observe可用于每个带 waitFor 参数的工具。操作完成后返回页面状态——observe:{snapshot:'compact'} 或 {text:true} → observed:{source, elements|tree, text?}——将操作和查看合并为一次调用,而非两次。对于 navigate/reload/history_go 后尤其有用,这些操作会使所有 @eN 失效,原本必须随后调用 snapshot。读取页面前会再次检查允许列表,因此重定向到允许列表之外的页面时会返回 skipped,且不读取任何内容;其中的失败不会导致操作失败。
read_text读取整页文本或指定引用处的文本。不会执行原始 JS。默认上限为 20 000 个字符(maxChars 可覆盖,上限为 200 000;截断结果带有 truncated/totalChars/nextOffset)。传入 offset=<nextOffset> 可从截断处继续,而无需从头重读——切片绝不会拆开代理对,因此边界处的表情符号不会导致结果无法签名。
get_state轻量的单元素探测(CSS 或 @eN)——返回 {exists, visible, tag, text, box?, inViewport?},无需完整快照。一次往返即可验证操作效果或重新检查引用。节点缺失时不会报错:返回 {exists:false, reason}(not_found/unknown_ref/detached),因此可安全轮询;{exists:null, reason:'unknown'} 表示无法在当前页面中定位 @eN——不代表它已消失。不会读取输入框的 .value(不会回读密码)。仅使用结构化 CDP。
console_tail获取标签页近期的页面控制台错误/警告和未捕获异常({enabled, entries:[{ts,level,text,origin}]})——可区分“处理程序抛出异常,页面卡住了”和“只是很慢”。需主动启用(弹出窗口设置,默认关闭;关闭时返回 {enabled:false})。从首次连接时开始捕获(不回放);条目会按允许列表过滤来源。仅捕获 CDP 事件,不使用 evaluate。
network_tail获取标签页近期的 XHR/fetch 响应({enabled, entries:[{ts,method,url,status,type,contentType,size,body?}]})——用于读取画布仪表板背后的数据(Metrika、Chart.js、WebGL),这些页面没有可读取的 DOM。直接获取精确 JSON,而非使用截图 + 视觉识别。需主动启用(弹出窗口中的“capture API responses”,默认关闭)。仅对文本类型的内容提供响应体,并设有大小上限;不会捕获身份验证标头;条目会按允许列表过滤来源;filter 可按 URL 子字符串缩小范围。仅捕获 CDP 事件,不使用 evaluate。
handle_dialog查看并控制原生 JS 对话框(alert/confirm/prompt/beforeunload)——打开的对话框会冻结页面的 JS,而且它属于浏览器 UI,其他工具都无法点击。启用处理后,每个对话框一打开就会立即得到响应(alert → OK,其他情况 → cancel),并被记录({enabled, armed, recent:[{ts,type,message,origin,response,armed}], truncated?});action='accept'|'dismiss'(加上 promptText)会为该标签页的下一个对话框设置一次性响应——先设置,再点击会打开 confirm() 的按钮。此设置与来源绑定到设置时所在的页面(跨源 iframe 无法劫持),并且在标签页顶层框架发生任何导航时会立即清除——包括点击链接、重定向、navigate/reload/history_go——因此绝不会作用于之后无关的对话框。需主动启用(弹出窗口中的“auto-handle JS dialogs”,默认关闭;关闭时返回 {enabled:false}——会话中取消勾选会立即停止自动响应,而不只是停止后续记录)。条目会按允许列表过滤来源。仅使用结构化 CDP,不使用 evaluate。
click调用 DOM .click()。使用 CSS 选择器或 @eN 引用。可选 waitFor,在同一次调用中轮询点击效果。
mouse_click使用真实的 Input.dispatchMouseEvent,执行完整的悬停→按下→释放流程。会自动调整落点以避开部分遮挡;目标被完全遮挡时会报告 covered/hitTarget/hitTargetRef。也可通过 x/y(视口 CSS 像素)手动指定落点。button 可选 left/middle/right,clickCount 为 1–3,可选 waitFor。
hover将指针悬停在元素/点上,不点击(仅执行 mouseMoved 前置事件)。适用于仅通过 CSS :hover 显示的菜单、工具提示和行操作 UI。使用 selector/@eN(自动调整落点,并报告 covered/hitTargetRef)或视口 x/y;可选 waitFor,用于悬停→等待菜单。其能力严格弱于 mouse_click;:hover 状态是暂时的。
fill如果没有 allowPassword=true,则阻止操作密码字段——检查会同时针对解析出的节点和按键实际到达的节点,并沿着焦点遍历开放的影子根和同源框架。method=insertText 会清空字段,并通过 CDP 使用真实输入事件输入文本(适用于忽略程序设置值的单页应用编辑器),之后会回读并报告 applied(yes/no/unclear)以及 len——写入后字段的长度,绝不会返回字段内容。可选 waitFor。
select_option在原生 <select> 中选择选项(无法通过 CDP 操作操作系统弹出菜单)。在 DOM 中设置值并触发 input/change,而不是打开菜单。可指定 value/label/index 之一;<select multiple> 可传数组。对于非 <select> 目标会返回 wrong_element——自定义 JS 组合框(react-select、MUI)仍应使用 click/find/reveal。可选 waitFor。
key_type通过 CDP 输入原始文本。如果焦点位于密码字段且没有 allowPassword=true,则会阻止操作。
send_keysMod+A、Shift+Tab 等。macOS 上 Mod = Cmd,其他系统上为 Ctrl。与 key_type 使用相同的密码字段检查。
screenshot以原生 MCP 图像块形式返回 PNG/JPEG。maxWidth 会缩小图像,使返回的图像宽度不超过指定像素数(会考虑显示器的设备像素比和任何 set_viewport 仿真);region={x,y,width,height} 可裁剪区域(相对于视口的 CSS 像素)。隐藏标签页会快速失败并返回 tab_not_visible;bringToFront=true 会先激活标签页(抢占焦点)。捕获内容过大而无法通过传输线路时,会返回 screenshot_too_large,而不是断开连接——可通过 maxWidth/region/format=jpeg(或较低的 deviceScaleFactor)缩小。
set_viewport仿真设备视口——用于测试响应式/移动端布局。设置按标签页生效,而不是调整窗口:不会干扰用户正在查看的内容,并且能提供窗口尺寸无法实现的设备像素比和移动端 <meta name=viewport> 处理。可指定 preset(mobile-small 375×667、mobile 393×852、mobile-large 412×915、tablet 820×1180、desktop 1280×800、desktop-wide 1920×1080)或显式指定 width+height,并设置 deviceScaleFactor(≤ 3)、mobile、touch、orientation。移动端预设还会提供移动端 Chrome UA 和匹配的 UA 客户端提示(mobileUserAgent=false 可禁用),让按 UA 识别的服务器发送移动端资源包——请在导航前设置视口。reset=true 可恢复实际视口;完全不传参数则读取当前视口。报告页面实际看到的内容,而非所请求的内容。引用会失效(断点变化会导致 DOM 重新挂载)。仅使用结构化 CDP,不使用 evaluate。
print_to_pdf将页面渲染为 PDF 并保存到下载沙箱——返回 {path, size, filename},因此文件字节不会进入模型上下文。与 screenshot 不同,它不需要可见标签页:后台 Agent 标签页也能正常打印,因此在捕获因 tab_not_visible 失败时可作为备用方案。landscape、printBackground(默认为 true)、scale(0.1–2)。filename 是不含路径分隔符的单个名称(默认为 print-<UTC timestamp>.pdf),文件会写入 ~/Downloads/sallyport/——与 save_to_file 使用相同的沙箱。超过桥接帧大小上限(约 9 MiB)时会返回 pdf_too_large。仅使用结构化 CDP,不使用 evaluate。
wait_for每 250 ms 轮询一次,直到选择器/@eN 引用可见(对于 CSS,任何匹配项都算——不一定是 click/fill 操作的第一个匹配项;设为 absent=true 时,则要求没有匹配项可见)和/或页面文本包含指定子字符串;absent=true 会一直等待,直到目标消失。timeoutMs ≤ 30 s;超时返回 {found:false},而不是错误。可替代无条件休眠。如果前一个操作支持内嵌 waitFor,优先使用它。
settle等待 DOM 停止变化(顶层文档在 stableMs 时间内没有子节点、文本或属性变更,默认 500 ms)——适用于“页面刚刚做了某些变化”但没有单个元素可供 wait_for 的情况。可捕捉长度相同的编辑和两次轮询之间发生的变化。不包括 iframe/影子根内容、CSS 动画和网络活动。等待期间发生导航时,会继续在新页面上等待。每 250 ms 轮询一次,上限为 30 s;页面一直未稳定时返回 {settled:false},而不是错误。
find语义元素定位器——在无障碍树中按 role/name/nameExact/value 匹配,而不是使用 CSS 选择器,并按完全匹配优先排序(limit ≤ 50)。不使用 evaluate,也不执行探测。
reveal滚动虚拟列表/容器并反复执行 snapshot,直到出现与 find 谓词匹配的元素——适用于无限滚动信息流和延迟渲染的表格。container 是可滚动的元素;每一步之间都会等待其子树静止(约 0.5–1.5 s),因此页面其他位置的时钟或动态信息不会拖慢操作;到达末尾的步骤会等待整个页面加载下一批内容。找到元素、滚动停滞、达到 maxSteps(≤ 40)或超时后停止。
scroll确定性滚动——不带谓词的 reveal 配套工具。使用 selector → scrollIntoView;或者滚动页面(或 selector 指定的容器),滚动量为 dx/dy(负值 = 向上/向左),也可指定 to='top'|'bottom'。返回 {x, y, scrollHeight, atBottom},以便延迟加载循环知道何时停止。使用固定的滚动探测,不使用 evaluate。
evaluate按域逐个主动启用。返回 {type, value}。
fetch_in_page使用页面 Cookie/身份验证执行 fetch()。返回 {status, contentType, headers, mode, data}。saveAs=<filename> 会将响应体直接写入下载沙箱,结果变为 {status, contentType, mode, path, size, filename},因此数 MiB 的资源不会进入 Agent 的上下文;而且一次调用即可替代旧的 fetch_in_page → save_to_file 组合(它还会将 returnAs 默认设为 base64,因为文本路径会以 UTF-8 解码二进制内容,导致有损转换)。序列化后的响应体超过 12 MiB 时会返回 fetch_too_large(在扩展程序端按转义后的形式测量——JSON 中的控制字符会占用 6 个字节);saveAs 不会提高此上限,因为写入守护进程前,响应体仍需通过桥接传输。受允许列表限制。
upload通过 DOM.setFileInputFiles 将本地文件附加到 <input type=file>。路径必须是绝对路径、不含 ..,且解析后必须位于 ~/Downloads/sallyport/ 下(通过 SALLYPORT_DOWNLOAD_DIR 覆盖)——与 save_to_file 使用相同的沙箱,并通过 Path.resolve() 阻止符号链接逃逸。目标必须确实是文件输入框。受允许列表限制。
save_to_file在守护进程本地执行——将 base64 写入 ~/Downloads/sallyport/<filename>(通过 SALLYPORT_DOWNLOAD_DIR 覆盖)。使用沙箱保护:不允许路径分隔符或 ..。
status由守护进程响应的健康检查:{connected, mode, version, port, pendingCalls, uptimeS, lastCalls, lastError, lastHandshakeError}。mode 为 broker(每次调用都必须显式提供所属的 tabId)或 standalone(回退到活动标签页)。lastCalls 是近期工具执行结果的环形记录({tool, ok, ms, code?}——绝不包含参数),lastError 是最近一次失败,因此循环可以将停滞归因到具体工具/代码;connected 为 false 时,lastHandshakeError 会说明扩展程序连接失败的原因(密钥错误、时钟偏差、未收到 hello)。无需浏览器往返,也不会排在正在运行的调用之后——请将其用作浏览器操作前的预检。
在 broker 模式下,使用显式的 tabId 指定自有标签页;不带 tabId 的
navigate 会创建一个 Agent 标签页。独立模式下,浏览器工具支持回退到
当前活动标签页,但 close_tab 始终需要 tabId。系统不会隐式记住“最近
操作的标签页”。

启用控制台或网络捕获后,捕获会从下一次工具调用开始。关闭任一设置时,会 立即清除所有标签页中对应捕获的缓冲区,并在浏览器中关闭该捕获。对于 network_tail,bodyPending: true 表示正文读取已排队或仍在进行;稍后 再次读取即可。每个标签页最多同时进行 4 次正文读取,全局最多同时进行 16 次; 其余读取会在每个标签页的队列中等待,队列容量由 100 条目的环形缓冲区限制, 因此,即使同时收到大量响应,也仍会获取每个正文。只有队列溢出时,响应才会 保留元数据,但带有 bodyOmitted: true 和 bodyOmissionReason: "capture_busy",且结果会标记为 truncated;过滤 无法找回这些正文。保留的正文负载在所有标签页间受到限制:每个标签页 10 MiB, 总计 40 MiB,按与单个结果预算相同的线上字节数计算,因此同一标签页中较早的 正文可能会被丢弃;其元数据仍会保留,并带有 bodyOmissionReason: "cache_limit",且一个标签页无法驱逐另一个标签页的正文。Chrome 自身的 捕获缓冲区上限为每个标签页 48 MB、每个响应 10 MB,按解码后的大小计算 — 正文中只要包含任意非 Latin-1 字符,就会以 UTF-16 存储,因此该情况下每个 响应的原始大小上限约为 5 MB(Chrome 默认值为 200 MB / 20 MB)。对于 Chrome 未保留的正文 — 超过单个响应上限、被较新的流量挤出,或根本未存储 (通常是空响应)— 元数据仍会保留,并带有 bodyOmissionReason: "unavailable";只有在请求可安全重复时(幂等的 GET),才使用 fetch_in_page 重新获取;切勿对 POST 或带签名的一次性调用这样做。这些限制 适用于正文负载,不包括临时解码、元数据或进程总内存。

对于按计划运行的 Agent,开销较低的迭代方式是:status(如果扩展已断开, 则跳过所有操作)→ 范围限定的读取(snapshot selector=… compact=true, read_text ref=…)→ 使用内嵌的 waitFor 执行操作,而不是单独调用 wait_for → 使用 get_state ref=…(单个元素)验证,而不是重新对整个页面 执行快照。受控标签页会自动保持唤醒,因此即使浏览器窗口在后台,循环也能继续 工作(权衡取舍请参阅故障排除)。

与 Kimi WebBridge 对比

Sallyport 实现了 Kimi 的常用工具(navigate、click、fill、 snapshot、screenshot、evaluate、mouse_click、upload、…),并新增 了一些自有工具(fetch_in_page、save_to_file、reload)。Kimi 的三项功能 在此处被有意省略:

Kimi 功能Sallyport 省略该功能的原因如果你需要此行为
network(通过 Network.enable 启动/停止/列出/查看 HTTP 捕获详情)Kimi 的版本会捕获认证标头、Cookie 和每个请求正文,且不设域名门槛 — Sallyport 省略的正是这种不受限制的形式。使用 network_tail:受限的子集 — 在弹出窗口中选择启用,仅捕获响应正文(不含标头/Cookie),并将来源限制为允许列表。或者针对特定 URL 使用 fetch_in_page。
_session(按 Agent 分组并着色的 Chrome 标签页)纯粹的装饰功能,会让标签页处理变复杂,却无法解决当前规模下的实际问题。使用 list_tabs 查找你打开的标签页。

find_tab 也被有意省略:Sallyport 的 list_tabs 会返回完整列表,由 Agent 在客户端进行筛选 — 一次往返即可,无需两次。

在本地测试

分为三层,从最快到最接近真实场景:

A. 仅使用 Wire——不使用 MCP 或 Claude Code

正在运行的 broker 占用了端口,因此请先停止它(sallyport-daemon doctor --stop-broker),或传入其他 --port,并更新弹出窗口中的 daemon URL。

确认 HMAC 配对、允许列表和审计日志正常,且不会触发任何工具。

# terminal: run the daemon in WS-only mode. Stays up until Ctrl-C —
# no stdin / no MCP client needed.
sallyport-daemon serve

默认情况下,sallyport-daemon(无子命令)会在标准输入中等待 MCP 客户端,并在遇到 EOF 时立即退出——适用于 Claude Code,但不便于本地测试。需要稳定运行的守护进程时,请使用 serve。

打开弹出窗口 → 配对 → 粘贴密钥 → 状态变为 已连接。 这就足以证明:WS 可达、HMAC 握手成功、权限正常。

B. 从 Shell 调用单个工具——不使用 Claude Code

sallyport-daemon exec <tool> key=value... 会调用一个工具后退出,并打印结果。可解析为 JSON 的值会按 JSON 处理,否则按字符串处理。

# Catalogue of tools (works offline, no extension needed):
sallyport-daemon list-tools

# Open a tab (the host must already be in the popup's allowlist):
sallyport-daemon exec navigate url=https://example.com newTab=true

# Read the page:
sallyport-daemon exec read_text

# Get the accessibility tree + refs:
sallyport-daemon exec snapshot

# Click something from snapshot (use a ref):
sallyport-daemon exec click selector=@e3

# Targeted screenshot:
sallyport-daemon exec screenshot format=jpeg quality=70

说明:

  • exec 可与正在运行的 Claude Code 会话同时使用:broker 运行时(通常如此),它会通过 broker 作为一次性客户端进行调用,而不会争抢端口。没有 broker 时,它会自行占用端口,因此不能与正在运行的会话同时使用——请停止会话,或在此处传入其他 --port,并更新弹出窗口中的 daemon URL。
  • 没有 broker 时,首次调用 exec 最多会等待 10 秒,让弹出窗口连接(可使用 --wait 30 延长等待时间)。配对完成后,扩展程序会自动重新连接。
  • 截图数据块在输出中会被截断——但传递给真正的 MCP 客户端时仍是完整的。
C. 使用 Claude Code 进行端到端测试

A 和 B 均正常后,注册 MCP 服务(参见“设置”→第 3 步),重启 Claude Code,并让它执行任意与网页相关的操作。查看弹出窗口中的审计标签页——每次调用都会显示在其中,并标记为 ok/error,同时列出目标 URL。

故障排除

症状解决方法
设置中的某些环节感觉不对运行 sallyport-daemon doctor——它会检查 Python 版本、密钥和权限以及端口,并重新显示配对信息块。
弹出窗口卡在 “正在连接到守护进程……”守护进程无法通过弹出窗口中的 URL 访问。确认它正在运行(在另一个 shell 中运行 sallyport-daemon serve)、Advanced → daemon URL 的设置与之匹配(默认值为 ws://127.0.0.1:10086/ws),且端口未被占用(运行 sallyport-daemon doctor)。单客户端规则意味着正在运行的 Claude Code 会话已经占用了该端口——这是预期行为;扩展会连接到该会话。
弹出窗口显示 “extension is not connected”守护进程未运行,或者弹出窗口显示已暂停——打开弹出窗口并点击恢复/重新连接。
domain_not_allowed在允许列表标签页中添加此主机。
evaluate_not_allowed编辑允许列表条目,并勾选 allow evaluate() 后重新添加。
password_field(来自 fill)fill 默认拒绝 <input type=password>。如果确实需要,请传入 allowPassword=true。
wrong_element(来自 upload)选择器定位到的不是 <input type=file>。重新执行 snapshot,并选择一个真正的文件输入框引用。
wrong_element(来自 select_option)目标不是原生 <select>,而是自定义 JS 组合框(react-select、MUI、Radix)。这些元素位于 DOM 中:使用 click/mouse_click 打开,然后 click 选项(使用 find/reveal 定位)。
unsafe_path(来自 upload)路径包含 ..、不是绝对路径,或解析后位于沙盒之外(默认路径为 ~/Downloads/sallyport/)。先通过 save_to_file 将文件放入沙盒,然后再上传。如果确实需要从其他位置上传,请通过 SALLYPORT_DOWNLOAD_DIR 扩大沙盒范围。
not_visible(来自 mouse_click)元素大小为零——很可能是 display:none 或已脱离页面。重新执行快照;如果它是按设计隐藏的,请操作显示它的切换控件。
mouse_click 报告 covered: true每个探测点上都有其他节点遮挡目标。结果包含 hitTarget(拦截点击的对象)和 hitTargetRef——该节点的 @eN;直接点击它,或使用 mouse_click x= y= 手动指定位置。
浏览器窗口处于后台时自动化停滞Chrome 会冻结后台标签页和被完全遮挡的窗口。桥接程序会自动让正在驱动的标签页保持唤醒状态(弹出窗口 → Advanced → keep automated tabs awake,默认开启;请注意,此时页面会认为自己处于聚焦且可见状态——例如,Telegram 会发送已读回执——并在连接期间持续渲染,这会增加动画页面的 CPU 占用)。如果页面必须在桥接程序连接之前保持运行,请在 chrome://settings/performance → “始终保持以下网站处于活动状态”中添加该网站,或使用专用的自动化配置文件,并设置 --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling。
多个会话感觉比单个会话慢它们共用一个浏览器:最多同时运行 8 个调用,之后的调用会排队。排队等待超时的调用会失败并返回 busy(可以安全重试——该调用从未发送)。请采用指数退避:maxConcurrentCalls 是一个常量,并且有意不提供每个调用方的争用信号(这会实时透露其他会话的繁忙程度)。
会话在运行过程中失去了浏览器工具它的代理已退出。会话会报告这一情况并以非零状态码退出;重新启动会话(新会话会启动一个全新的代理)。sallyport-daemon doctor 会显示当前状态。
Agent 在各处留下了标签页标签页数量有上限:同时打开的 Agent 标签页上限(弹出窗口 → 设置,默认值为 20)。超过上限后,打开新 Agent 标签页会先关闭价值最低的标签页——先关闭已结束会话留下的标签页,再关闭创建该标签页的会话中最近最少使用的标签页——绝不会关闭你看过的标签页,也不会关闭其他正在运行的会话的标签页。将其设为 0 可关闭此功能。弹出窗口 → Agent 标签页会列出每个会话打开的标签页(并标记会话已结束的标签页),并提供关闭所有 Agent 标签页。默认情况下,标签页会在创建它的会话结束后继续保留——关闭 Agent 尚未完成的工作正是 close_tab 设有门槛要防止的情况。对于已分派或单次运行的 Agent,可在弹出窗口中勾选会话断开连接时关闭其标签页;请注意,此设置适用于每个会话,包括你自己的交互式会话。
bad_ref@eN 引用已过期(快照已失效)或指向错误的标签页。对正确的标签页重新执行 snapshot。引用按标签页和快照分别生效。
弹出窗口中的 mac mismatch~/.config/sallyport/secret 中的密钥与弹出窗口中配对的密钥不再匹配。运行 sallyport-daemon --show-secret,复制密钥,点击取消配对 → 粘贴 → 配对。
timestamp skew时钟相差超过 30 秒。请检查 NTP。
守护进程启动时警告权限过于宽松运行 chmod 600 ~/.config/sallyport/secret。
WS 帧大于 16 MiB扩展会被静默断开连接,关闭代码为 1009。请降低截图质量,或截取区域截图。
轮换密钥
  1. rm ~/.config/sallyport/secret
  2. 重启运行 sallyport-daemon 的进程(Claude Code 会在下次调用时重新启动它)。新密钥会输出到 stderr。
  3. 在弹出窗口中:取消配对 → 粘贴新密钥 → 配对。

开发

# Extension
cd extension
npm install
npm run watch         # esbuild rebuild on save — reload extension in chrome://extensions to pick up
npm run typecheck     # tsc --noEmit
npm test              # vitest run
npm run lint          # eslint
npm run format        # prettier --write
npm run format:check  # prettier --check (CI gate)

# Daemon
cd daemon
pip install -e '.[dev]'
ruff check .
mypy
pytest -q
sallyport-daemon --verbose < <(sleep 99999)   # smoke-test a long-running daemon
真实浏览器捕获冒烟测试

使用 Node 22+ 和支持未打包扩展的 Chromium / Chrome for Testing 二进制文件,在 extension/ 中运行:

CHROME_BIN='/absolute/path/to/chromium' npm run test:browser

测试会创建一个隔离的临时配置文件,加载扩展,并使用 localhost 测试样例来测试真实的控制台/网络捕获、立即选择退出、重新启用、调试器分离/重新连接,以及弹出窗口对已结束会话的清理,同时保留活动标签页、人工查看的标签页和非 Agent 标签页。它还会验证键盘导航、可访问的标签页状态和较长的会话名称。测试结束后会删除配置文件,并停止浏览器和测试样例服务器。它不会使用你的常规浏览器配置文件或配对密钥。这是一项额外的集成检查;确定性单元测试仍会覆盖正文读取竞态和过载情况。

如需完整测试 MCP → 守护进程 → 扩展 → Chrome 流程,请在 daemon/.venv 中安装守护进程依赖并运行:

CHROME_BIN='/absolute/path/to/chromium' npm run test:e2e

SALLYPORT_TEST_PYTHON 可用于指定安装了守护进程依赖的其他 Python 解释器。此模式会在临时本地端口上启动一个独立守护进程,并使用新的测试密钥;随后验证 MCP 初始化和工具发现、经过身份验证的配对、允许列表拒绝、表单填写和点击、结果读取、密码拒绝、审计信息脱敏,以及等待文本和属性不断变化但长度不变的页面,其中包括轮询间隔内已还原的 DOM 变化,以及等待期间发生的导航。测试后会停止守护进程和浏览器。它不会连接到你常用的守护进程。

Pre-commit
pip install --user pre-commit
pre-commit install

这会在每次提交时运行行尾空格/json/yaml/合并冲突检查,以及 ruff、mypy、prettier、eslint 和 tsc。较耗时的测试套件保留在 CI 中,因为它们不适合作为提交门禁。

CI

.github/workflows/ci.yml 会针对 Python 3.10/3.11/3.12(守护进程)和 Node 20/22(扩展)进行矩阵测试,为两者运行 lint、类型检查、测试和构建,并在 main 上将 extension/dist 上传为构件。

添加新工具

五个文件;无论是扩展端工具还是守护进程本地工具,顺序都相同——两端的工具名称必须逐字节一致,因为路由使用字典查找。

  1. 实现。 扩展端:新建 extension/src/tools/<name>.ts 并导出一个 Tool。纯验证逻辑(路径/参数结构)应放在不依赖 Chrome 的同级模块中,例如 upload-path.ts,这样 vitest 无需 Chrome 测试框架即可覆盖这些逻辑。仅限守护进程:在 daemon/src/sallyport_daemon/local_tools.py 中添加协程,并在 LOCAL_TOOLS 中注册。
  2. 在扩展端注册。 在 extension/src/tools.ts 的 tools 映射中添加导入和条目。
  3. 声明 MCP 架构。 在 daemon/src/sallyport_daemon/mcp_server.py 中,将 Tool(name=..., description=..., inputSchema=...) 追加到 TOOLS。只要实现中缺少字段就会抛出异常,就应将该字段标记为 required——架构必须与行为一致(参见由 test_close_tab_requires_tab_id 捕获的 close_tab 回归问题)。
  4. 固定目录清单。 在 daemon/tests/test_mcp_server.py::test_tools_catalogue_covers_extension 的预期集合中添加新名称。守护进程和扩展注册表之间的不匹配会立即导致构建失败,而不是等到实际通信时才发现。
  5. 编写文档。 在 README.md 的 Tools 表中添加一行,并在 CHANGELOG.md 的 [Unreleased] / Added 下添加一条,描述用户可见的内容(参数、门控条件、值得了解的错误代码)。

然后运行 cd extension && npm test && npm run lint && npm run typecheck 和 cd daemon && ruff check . && mypy && pytest -q——全部通过后再宣布完成。

版本管理与发布

CHANGELOG.md 记录每项重要变更;项目采用 语义化版本。线上协议版本为 1 — 仅在不兼容变更时升级,并在同一次提交中更新跨语言 向量测试。

许可证

MIT — 详见 LICENSE。

线上协议

文档见 extension/src/protocol.ts 和 daemon/src/sallyport_daemon/protocol.py。信封结构:

{ v, ts, nonce, type, id?, body, mac }

mac = HMAC-SHA256(secret, canonical_json({v, ts, nonce, type, id?, body})), 采用 base64 编码。规范 JSON:键按 Unicode 码点排序、不含空白、 Unicode 字符直接传递,并采用跨语言一致的数字格式。

确切字节由 fixtures/canonical-vectors.json 中的 49 个跨语言向量 固定 — daemon/tests/test_vectors.py 和 extension/test/vectors.test.ts 都会读取该文件,并逐字节校验 一致性。如果更改规范化规则,请通过 python3 fixtures/generate.py 重新生成, 并在同一次提交中更新两个实现。

没有版本协商 — 双方都要求 v: 1。有不兼容变更时升级版本,并在同一次提交中更新两个向量测试。

评论

支持 Markdown

第一次接触 MCP?

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

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