内容
# browser-mcp
一个Model Context Protocol服务器,为Claude(或任何MCP客户端)提供一个完整的浏览器——打开页面,读取内容,点击,输入,截图,检查可访问性树,管理标签页,并模拟设备。由Playwright提供支持,具备隐身模式和持久化cookie/localStorage配置文件。HTTP传输,在其自己的进程中作为守护进程运行。
```
Claude Code ──HTTP──▶ browser-mcp ──Playwright──▶ Chromium (headless)
│
├── MCP 会话(每个客户端的 McpServer + 传输)
├── 命名配置文件(持久化 cookie / localStorage)
├── 每个配置文件的 BrowserContext(被会话重用)
├── 36 个工具:打开 / 读取 / 点击 / 输入 / 快照 / …
└── 网络环,AX 快照存储,CSRF-hardened HTTP
```
> **免责声明:** 此工具自动化浏览器交互,可能会违反所访问网站的服务条款。您对如何使用它以及与哪些网站交互承担全部责任。作者不对因使用它而产生的任何后果承担责任。
---
## 目录
- [为什么](#为什么)
- [快速开始](#快速开始)
- [功能](#功能)
- [命名配置文件](#命名配置文件)
- [工具参考](#工具参考)
- [REST API(用于脚本)](#rest-api用于脚本)
- [配置](#配置)
- [架构](#架构)
- [安全模型](#安全模型)
- [测试](#测试)
- [Docker](#docker)
- [平台说明](#平台说明)
- [开发](#开发)
- [FAQ](#faq)
- [许可证](#许可证)
---
## 为什么
Claude Code 内置的 `WebFetch` 可以抓取一个 URL 的文本,但它是一次性的:没有点击,没有表单填写,没有 cookie,没有状态。对于超出“粘贴此文章”的任何操作:
- **登录网站,然后执行 X 操作。** browser-mcp 在磁盘上的命名配置文件中保留 cookie/localStorage,因此身份验证会话在重启后仍然存在。只需通过 `browser_open_visible` 登录一次,第二天就可以无头状态下继续使用。
- **“这个 SPA 现在是什么?”** `browser_snapshot` 通过 Chrome DevTools Protocol 返回可访问性树(角色,名称,值,状态)。比在 React 仪表板上抓取 Markdown 更可靠。
- **表单驱动的工作流程。** `browser_click`/`browser_type` 使用 Playwright 的自动等待和角色/标签定位器 - 在标记更改时稳定,无需维护 CSS 选择器。`browser_expect` 重试断言直到超时,因此您不必手动编织等待。
- **捕获证据。** `browser_save` 编写 PDF / MHTML / 原始 HTML;`browser_screenshot` 捕获视口/全页/元素。对于“显示我提交错误时页面的样子”的交接非常有用。
- **调试 SPA 行为。** `browser_network_log` 公开一个最近 500 个请求的环形缓冲区 - URL,方法,状态,时间,失败原因 - 以便您可以找到页面静默吞噬的 401。
---
## 快速开始
### 1. 安装
```bash
# 已发布的软件包(首选)
npm install -g @graphmemory/browser-mcp
browser-mcp
# 或无需安装
npx -y @graphmemory/browser-mcp
# 或 Docker(请参见下面的“Docker”以获取身份验证要求)
docker run --rm -p 7777:7777 -e BROWSER_MCP_API_KEY=$(openssl rand -hex 32) \
ghcr.io/graph-memory/browser-mcp:latest
```
Chromium 在首次运行时会自动安装(`postinstall` 钩子)。
启动日志:
```
browser-mcp 监听 http://127.0.0.1:7777/mcp
健康检查 → http://127.0.0.1:7777/health
/mcp → 默认配置文件
/mcp/<name> → 命名配置文件(例如 /mcp/test1)
REST(json) → POST http://127.0.0.1:7777/api/v1/tools/<tool>?profile=<name>
REST 工具 → GET http://127.0.0.1:7777/api/v1/tools
认证 → 禁用(仅限回环)
cors_origin → null
max_sessions → 50
```
### 2. 在 Claude Code 中注册
添加到 `~/.claude.json`(用户全局)或 `.mcp.json`(项目本地):
```json
{
"mcpServers": {
"browser": {
"type": "http",
"url": "http://127.0.0.1:7777/mcp"
}
}
}
```
启用身份验证(当绑定到非回环接口时需要):
```json
{
"mcpServers": {
"browser": {
"type": "http",
"url": "http://127.0.0.1:7777/mcp",
"headers": { "Authorization": "Bearer ${BROWSER_MCP_API_KEY}" }
}
}
}
```
### 3. 第一次对话
用自然语言询问 Claude:
> 打开 example.com,找到“登录”链接,读取生成的页面,并用 test@example.com 填写电子邮件字段。
它将为您顺序执行 `browser_open` → `browser_click` → `browser_read` → `browser_type`。在对话中不断迭代,无需离开。
### 4. 示例流程(工具实际显示)
```
browser_open { url: "https://example.com" }
browser_snapshot { compact: true } # 我可以点击/输入什么
browser_click { target: "Sign in", target_type: "role", role: "button" }
browser_type { target: "Email", target_type: "label", text: "a@b.co" }
browser_expect { assertion: "url_matches", expected: "/dashboard$" }
browser_read { mode: "markdown" }
browser_save { format: "pdf", path: "./dashboard.pdf" }
browser_network_log { failed_only: true, limit: 20 }
browser_cookies { action: "get", urls: ["https://example.com/"] }
```
---
## 功能
- **36 个工具**,涵盖导航,读取,交互(点击/输入/按压/悬停/选择/检查/拖动/批量表单填写),断言,IO,网络 + 控制台检查(包括响应正文),cookie/localStorage,对话框,权限,地理位置和设备仿真。参见 [工具参考](#工具参考)。
- **持久化命名配置文件。** 每个 URL 路径(`/mcp/<name>`)都有自己的 cookie 和 localStorage,位于 `~/.browser-mcp/profiles/<name>/`。登录一次;重启 supervisor 或 Claude Code;会话仍然存在。
- **可访问性优先。** `browser_snapshot` 从 Chrome DevTools Protocol 中提取 AX 树(Playwright 1.40 删除了内置 AX API)。紧凑模式剥离装饰容器,仅保留交互元素 + 地标 - 完美适用于需要“可以点击什么”而无需阅读 React 生成的 DOM 噪音的代理。
- **可差异化的快照。** `browser_snapshot { store_as: "before" }` → 执行某些操作 → `{ diff_against: "before" }` 返回添加/删除/状态更改的节点作为紧凑的差异。自动紧凑以抑制无关噪音。
- **角色/标签定位器。** 每个交互工具都接受 `target_type: role|label|text|placeholder|testid|selector`。优先使用 `role` 表示按钮/链接,使用 `label` 表示表单字段 - 对标记更改稳健,无需维护 CSS。
- **`browser_expect` 带有重试。** 13 种断言类型(可见/隐藏/启用/禁用,text_equals/contains/matches,value_equals,count,url_equals/matches,title_equals/matches)。重试直到超时,因此您不需要单独的 `browser_wait` 来处理容易出错的条件。
- **`browser_read` 带有紧凑模式。** Markdown/text/HTML 提取;可选的 `compact` 标志剥离导航/头部/脚部/侧边栏/脚本/样式/SVG/iframe 和 ARIA 地标铬。默认为 `text` / `html`(否则页面将被大量无关内容淹没);对 `markdown` 关闭(Readability 已经选取文章)。
- **用于脚本的无状态 REST API。** 除了 MCP 之外,`/api/v1/tools/<tool>?profile=<name>` 还通过纯 JSON POST 公开相同的 36 个工具。任何语言的脚本都驱动与 MCP 代理相同的实时浏览器,并使用 `data` 中的**结构化 JSON**(不是 LLM 格式的文本)。`/api/v1/openapi.json` 处的 OpenAPI 3.1;`packages/browser-client-js/` 中有一个 Node JS/TS 客户端。参见 [REST API](#rest-api用于脚本)。
- **网络环形缓冲区。** 每个配置文件 500 个最近的请求,跨所有标签页。按标签页,URL 正则表达式,方法,min_status 或 failed_only 进行过滤。显示 UI 静默吞噬的 4xx/5xx。
- **可见模式用于登录。** `browser_open_visible` 关闭无头上下文并在可见窗口中重新打开以进行手动交互(CAPTCHA,SSO,2FA)。Cookie 进入持久配置文件;关闭窗口返回无头。
- **设备仿真。** `browser_configure` 应用视口/DSR/UA/移动设备/本地化/颜色方案预设。命名 `device_preset`(iphone-15,ipad-pro,pixel-8,desktop-retina…)原子化应用整个配置文件。
- **文件 IO。** `browser_save` 编写 PDF(仅限无头)/MHTML/HTML;`browser_upload` 使用 `<input type=file>` 并进行路径验证;`browser_download_wait` 捕获由点击或导航触发的下载,遵循服务器建议的文件名。
- **隐身插件。** 默认应用 `playwright-extra` + `puppeteer-extra-plugin-stealth`。如果它破坏了特定站点,请使用 `--no-stealth` 禁用。
- **Supervisor-env 隔离。** 所有 `BROWSER_MCP_*` 变量(API 密钥,主机,caps)都从 Chromium 的 env 中过滤掉,因此页面脚本无法指纹识别 supervisor 配置。
- **CSRF-hardened HTTP。** 需要 `Content-Type: application/json`,`Origin` 头部白名单,API 密钥与 `crypto.timingSafeEqual` 进行比较。
- **拒绝启动安全。** 绑定到非回环接口且无 API 密钥?退出并带有代码 2 和解释。覆盖 `--allow-insecure` 如果您知道自己在做什么。
- **会话 + 标签页 TTL。** 空闲 MCP 会话和 REST 配置文件持有者都在 `session_ttl`(默认为 30 分钟)后被回收;不活动的标签页在 `tab_ttl`(默认为 10 分钟)后自动关闭。对并发会话/持有者有硬上限(`max_sessions`,默认为 50;共享两个表面)。
- **多架构 Docker 镜像。** linux/amd64 + linux/arm64,非 root `browser` 用户,`tini` 作为 PID 1 用于僵尸回收,健康检查连接到 `/health`。
- **完整的测试套件。** 470 个测试(单元 + 集成对抗真正的无头 Chromium)涵盖每个工具处理程序,HTTP 服务器(身份验证/CSRF/会话生命周期)和 AX 树管道。参见 [测试](#测试)。
---
## 命名配置文件
每个 MCP 端点 URL 都可以包含一个配置文件名,该配置文件名隔离 cookie,localStorage 和浏览器状态:
```
http://127.0.0.1:7777/mcp → "default" 配置文件
http://127.0.0.1:7777/mcp/test1 → "test1" 配置文件
http://127.0.0.1:7777/mcp/my-scraper → "my-scraper" 配置文件
```
配置文件名称必须匹配 `^[a-zA-Z0-9_-]{1,64}$`(字母,数字,破折线,下划线;1-64 个字符)- 在 HTTP 层进行验证,因此路径遍历无法逃离基准目录。
配置文件存储在 `~/.browser-mcp/profiles/<name>/`(使用 `--profile-dir` 或 `BROWSER_MCP_PROFILE_DIR` 覆盖)。
多个 MCP 会话使用相同的配置文件共享一个 `BrowserContext`(因此一个会话中的登录在另一个会话中可见)。当配置文件上的最后一个会话过期时,上下文关闭,Chromium 退出。
每个会话都保留其**自己的活动标签页和快照存储**,因此同一配置文件上的并发客户端不会相互覆盖对方的“当前标签页”或在 `store_as` 名称上发生冲突。打开的标签页本身是共享的(通过 `browser_tabs_list` 对所有会话可见)。对于完全独立的并发客户端,请给每个客户端一个命名配置文件(`/mcp/<name>`)。
### 多配置文件 Claude Code 配置
```json
{
"mcpServers": {
"browser": {
"type": "http",
"url": "http://127.0.0.1:7777/mcp"
},
"browser-test": {
"type": "http",
"url": "http://127.0.0.1:7777/mcp/test"
}
}
}
```
---
## 工具参考
所有工具都接受结构化参数(zod 验证)。响应是单个块 `text` 内容(`browser_screenshot` 除外,它返回 `image`)。
### 定位器约定
每个元素定位工具通过相同的六种策略(`target_type`)解析 `target`:`text`,`role`,`label`,`placeholder`,`testid`,`selector`(CSS)。`exact` 切换为子字符串 vs 精确匹配(`testid`/`selector` 忽略);`role` 命名 `target_type: "role"` 的 ARIA 角色(默认为 `button`)。字符串在 zod 边界处长度限制为 2048。
一些**故意**的工具间差异(不一致):
- **默认 `target_type`** 大多数工具为 `text`,但表单导向的工具为 `label` - `browser_select_option`,`browser_check` 和 `browser_fill_form` 字段 - 因为 `label` 是表单控件最干净的句柄。(`fill_form` 的 `submit` 按钮仍然默认为 `text`,像 `browser_click` 一样。)
- **`browser_upload`** 使用较窄的集合 - `selector` / `label` / `testid`(默认 `selector`),没有 `role` / `exact` - 文件输入很少可以通过角色或文本进行地址定位。
- **`browser_drag`** 目标两个元素,因此使用 `source_*` / `target_*` 参数而不是单个 `role`,并且省略 `exact`。
- **`browser_download_wait`** 省略 `exact`(下载触发器类似于点击)。### `browser_open`
在新标签页中打开 URL,或如果提供 `tab_id`,则导航现有标签页。等待 DOMContentLoaded 加上短的请求空闲时间。不返回页面内容 - 调用 `browser_read` 之后。返回 HTTP 状态,URL 标题和 `tab_id`。
| 参数 | 类型 | 必需 | 描述 |
|-----------|------|----------|-------------|
| `url` | 字符串(URL) | 是 | 导航到的绝对 URL |
| `tab_id` | 字符串 | 否 | 如果设置,请导航此现有标签页而不是 `tab_id` 是活动的标签页 |
### `browser_read`
读取当前(或指定的)标签页。`mode=markdown`(默认)通过 Mozilla Readability 提取主要文章并转换为 Markdown。`mode=text` 返回 body innerText。`mode=html` 返回原始 HTML。
`compact=true` 剥离导航/头部/脚部/侧边栏/脚本/样式/SVG/框架和 ARIA 地标铬(`banner`,`navigation`,`complementary`,`contentinfo`,`search`)。默认为 `text` / `html`,关闭 `markdown`(Readability 已经选取文章)。对仪表板/SPA 非常有用,其中 Readability 退出。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|-----------|------|----------|---------|-------------|
| `mode` | `"markdown"` \| `"text"` \| `"html"` | 否 | `"markdown"` | 提取模式 |
| `selector` | 字符串 | 否 | — | CSS 选择器,缩小提取范围到特定元素 |
| `compact` | 布尔值 | 否 | 自动 | 剥离铬(见上文) |
| `max_chars` | 整数 | 否 | `50000` | 输出长度上限(也通过 `BROWSER_MCP_MAX_CHARS`) |
| `tab_id` | 字符串 | 否 | 活动标签页 | 读取的标签页 |
### `browser_find`
在当前页面查找文本出现。返回最多 `limit` 个片段,每个片段都有周围的上下文和一个稳定的 CSS 选择器,适用于 `browser_click` / `browser_type`。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|-----------|------|----------|---------|-------------|
| `query` | 字符串 | 是 | — | 子字符串(不区分大小写) |
| `limit` | 整数(1-50) | 否 | `10` | 最大匹配次数 |
| `tab_id` | 字符串 | 否 | 活动标签页 | 在哪个标签页中搜索 |
### `browser_click`
使用几种定位策略之一点击元素。Playwright 自动等待元素可见、启用和稳定;服务器额外等待点击后的网络空闲。
**策略优先级**(从最高到最低可靠性):`role` > `label` > `text` > `placeholder` > `testid` > `selector`。优先使用 `role` 表示按钮/链接,使用 `label` 表示表单字段。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|-----------|------|----------|---------|-------------|
| `target` | 字符串 | 是 | — | 元素的描述(见 `target_type`) |
| `target_type` | `text`\|`role`\|`label`\|`placeholder`\|`testid`\|`selector` | 否 | `text` | 定位策略 |
| `role` | ARIA 角色 | 否 | 当 `target_type="role"` 时为 `"button"` | 角色定位器所需 |
| `exact` | 布尔值 | 否 | `false` | 精确匹配 vs 子字符串 |
| `tab_id` | 字符串 | 否 | 活动标签页 | 操作的标签页 |
示例:
```json
{ "target": "Sign in", "target_type": "role", "role": "button" }
{ "target": "Home", "target_type": "role", "role": "link", "exact": true }
{ "target": "submit", "target_type": "testid" }
```
### `browser_type`
用文本填充输入/文本区域/可编辑内容。自动等待字段变为可操作。 使用 Playwright 的填充语义(替换现有值)。如果 `submit=true`,则在输入后按 Enter。
**表单的策略优先级**:`label` > `placeholder` > `testid` > `selector`。`label` 是大多数表单最稳健的。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|-----------|------|----------|---------|-------------|
| `target` | 字符串 | 是 | — | 目标元素(见 `target_type`) |
| `target_type` | 与 click 相同 | 否 | `text` | 定位策略 |
| `role` | ARIA 角色 | 否 | — | 对于 `role` 定位器(通常为 `textbox`) |
| `exact` | 布尔值 | 否 | `false` | 精确匹配 |
| `text` | 字符串 | 是 | — | 输入的文本 |
| `submit` | 布尔值 | 否 | `false` | 输入后按 Enter |
| `tab_id` | 字符串 | 否 | 活动标签页 | 操作的标签页 |
> **重大变化:** `target_type` 默认值为 `text`(以前为 `selector`),匹配 `browser_click`。已删除已弃用的 `selector` 别名 - 请传入 `target` 并使用 `target_type="selector"` 进行 CSS 选择。
### `browser_press`
按下键或和弦(Playwright 语法:`Enter`,`Tab`,`Escape`,`ArrowDown`,`Control+A`,`Meta+C`)。如果有 `target`(+ `target_type` / `role` / `exact`),则聚焦该元素;如果没有,则将键发送到页面的活动元素。
### `browser_hover`
将鼠标悬停在元素上以显示菜单/工具提示/仅在悬停时可见的控件。定位参数与 `browser_click` 相同。
### `browser_select_option`
在原生 `<select>` 中选择选项()。`by`:`value`(默认)/ `label` / `index`; `values` 是数组(为 `<select multiple>` 传递多个)。
### `browser_check`
**幂等** 将复选框/单选按钮设置为 checked/unchecked(与 `browser_click` 不同,后者会切换)。`checked`(默认为 `true`)确保状态;如果已经存在,则不执行操作。
### 浏览器工具列表(续)
(待续)
### `browser_drag`
拖拽一个元素到另一个元素上(HTML5 DnD / 可排序列表)。`source` + `source_type`/`source_role` 和 `target` + `target_type`/`target_role`.
### `browser_fill_form`
一次性填写整个表单。`fields[]` 按顺序应用;每个字段精确设置 `value`(文本)、`checked`(复选框/单选按钮)或 `options`(原生 `<select>`)。在第一个字段失败时中止(报告失败的字段)。`submit: true` 在最后一个字段上按 Enter;`submit: { target, target_type, role }` 在之后点击提交按钮。
### `browser_expect`
断言页面上的一个条件。在失败前最多重试 `timeout_ms` 次 - 不需要单独的 `browser_wait` 来处理容易出现问题的条件。返回 `PASS` 或 `FAIL`,并在错误体中包含 `expected` 和 `actual`。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `assertion` | 以下 13 个断言之一(见下文) | 是 | - | 要断言的内容 |
| `target` | 字符串 | 取决于断言类型 | - | 元素目标(对于元素/文本/计数/值断言必需) |
| `target_type` | 与 click 相同 | 否 | `text` | 定位器策略(之前为 `selector`;已更改) |
| `role` | ARIA 角色 | 否 | - | 用于 `role` 定位器 |
| `exact` | 布尔值 | 否 | `false` | 精确匹配 |
| `expected` | 字符串或数字 | 取决于断言类型 | - | 用于文本/值/计数/URL/标题;对于 `*_matches`,它是正则表达式 |
| `timeout_ms` | 整数(1-60000) | 否 | `5000` | 重试窗口 |
| `tab_id` | 字符串 | 否 | 活动标签 | 要检查的标签 |
**断言:** `visible`, `hidden`, `enabled`, `disabled`, `text_equals`, `text_contains`, `text_matches`, `value_equals`, `count`, `url_equals`, `url_matches`, `title_equals`, `title_matches`.
示例:
```json
{ "assertion": "visible", "target": "Sign in", "target_type": "role", "role": "button" }
{ "assertion": "text_contains", "target": "#status", "expected": "done" }
{ "assertion": "count", "target": "input", "expected": 3 }
{ "assertion": "url_matches", "expected": "/dashboard$" }
```
### `browser_snapshot`
返回一个**可访问性快照** - 一个从 Chrome 的可访问性 API 通过 CDP 获取的语义元素的紧凑树。
比在单页应用中抓取 Markdown 更可靠。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `selector` | 字符串 | 否 | - | 限定在以此 CSS 选择器为根的子树 |
| `max_depth` | 整数(0-50) | 否 | - | 截断更深的子元素,摘要为 `"N hidden children"` |
| `interesting_only` | 布尔值 | 否 | `true` | 修剪装饰性/隐藏的节点(Playwright 惯例) |
| `compact` | 布尔值 | 否 | 自动 | 仅保留交互式元素 + 结构性标志。差异比较时自动启用 |
| `store_as` | 字符串(1-64) | 否 | - | 将快照保存为此名称,以便稍后比较 |
| `diff_against` | 字符串(1-64) | 否 | - | 返回添加/删除/更改的内容,与存储的快照进行比较 |
| `format` | `"yaml"` 或 `"json"` | 否 | `yaml` | 输出格式 |
| `tab_id` | 字符串 | 否 | 活动标签 | 要快照的标签 |
示例输出(`yaml`):过滤掉装饰性的 `InlineTextBox` 节点,静态文本子元素如果其文本与父元素匹配,则会折叠,而匿名容器(`listitem`、`cell`)会继承其文本内容作为名称:
```
- RootWebArea "Login" [focused]
- heading "Login" [level=1]
- textbox "Email" [required]
- textbox "Password" [required]
- button "Sign in"
```
紧凑型仅保留交互式元素 + 结构性标志:
```
- form
- textbox "Email"
- textbox "Password"
- button "Sign in"
```
差异输出 - 在 `store_as: "before"` → 执行某些操作 → `diff_against: "before"`:
```
── 与 "before" 的差异 ──
添加(2):
+ listitem "buy milk"
+ listitem "walk dog"
更改(1):
~ button "Add" [-] → [focused]
```
**注意事项:** 差异比较基于路径(从根到节点的角色+名称链)。结构性更改会改变 sibling 顺序,从而导致在实际上未更改的节点上出现虚假的添加/删除对。适用于 "我点击了 X,出现了什么" 而不是 "检测到 exactly 一个元素更改"。文本框的值更改报告为 `changed`(值不包含在节点身份中)。
### `browser_permissions`
授予(或清除)浏览器权限 - 摄像头、麦克风、地理位置、通知、剪贴板等。在导航之前使用,以便永远不会出现提示。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `grant` | `"all"` 或 `"none"` 或数组 | 是 | - | 哪些权限 |
| `origin` | URL | 否 | 当前标签的来源 | 应用的来源(仅 http/https) |
| `tab_id` | 字符串 | 否 | 活动标签 | 使用的标签的来源 |
支持的权限:`geolocation`, `midi`, `midi-sysex`, `notifications`, `camera`, `microphone`, `background-sync`, `ambient-light-sensor`, `accelerometer`, `gyroscope`, `magnetometer`, `clipboard-read`, `clipboard-write`, `payment-handler`, `storage-access`.
### `browser_save`
将当前页面保存到磁盘。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `format` | `"pdf"` 或 `"mhtml"` 或 `"html"` | 是 | - | 输出格式 |
| `path` | 字符串 | 是 | - | 写入位置。默认:配置文件下载沙盒下(相对路径解析到此处;绝对路径在外部文件系统拒绝,除非 `BROWSER_MCP_ALLOW_ANY_WRITE_PATH=1`)。父目录创建。参见 [Security](#security-model) |
| `full_page` | 布尔值 | 否 | `false` | 仅 PDF:整个可滚动页面 |
| `landscape` | 布尔值 | 否 | `false` | 仅 PDF |
| `tab_id` | 字符串 | 否 | 活动标签 | 要保存的标签 |
- **pdf** - Chromium 的原生打印到 PDF。**仅限无头**(Playwright 限制)。
- **mhtml** - 具有内联资源的单文件存档。适用于离线交接。
- **html** - 原始 `page.content()`。
### `browser_upload`
将文件上传到 `<input type="file">`。调用前验证路径是否存在。对于 `<input multiple>`,传递多个文件;否则,一个文件。默认情况下,文件必须位于配置文件的上传沙盒下(参见 [Security](#security-model))。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `target` | 字符串 | 是 | - | 文件输入 |
| `target_type` | `selector` 或 `label` 或 `testid` | 否 | `selector` | 定位器策略 |
| `files` | 路径数组(1-32) | 是 | - | 默认情况下位于配置文件的上传沙盒下(路径在外部文件系统拒绝,除非 `BROWSER_MCP_ALLOW_ANY_UPLOAD_PATH=1`);每个文件验证为常规文件 |
| `tab_id` | 字符串 | 否 | 活动标签 | 要操作的标签 |
### `browser_download_wait`
触发下载(通过点击或导航)并捕获生成的文件。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `action` | `click` 或 `navigate` | 否 | `click` | 如何触发 |
| `target` | 字符串 | 如果点击则必填 | - | 启动下载的按钮/链接 |
| `target_type` | 与 click 相同 | 否 | `text` | 定位器策略 |
| `role` | ARIA 角色 | 否 | - | 用于角色定位器 |
| `url` | URL | 如果导航则必填 | - | 直接下载 URL |
| `save_to` | 字符串 | 是 | - | 保存位置。默认:配置文件下载沙盒下(绝对路径在外部文件系统拒绝,除非 `BROWSER_MCP_ALLOW_ANY_WRITE_PATH=1`)。以 `/` 结尾或现有目录 → 服务器建议的文件名 |
| `timeout_ms` | 整数(1-600000) | 否 | `60000` | 开始 + 完成的总等待时间 |
| `tab_id` | 字符串 | 否 | 活动标签 | 要操作的标签 |
### `browser_cookies`
读取、写入或清除配置文件中的 cookie。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `action` | `get` 或 `set` 或 `clear` | 是 | - | 操作 |
| `urls` | URL 数组 | 否 | - | `get`: 限定在这些 URL 上 |
| `cookies` | cookie 对象数组 | 如果设置则必填 | - | 每个需要(domain+path)或单个 URL。字段:`name`, `value`, `domain`, `path`, `url`, `expires`, `httpOnly`, `secure`, `sameSite` |
### `browser_storage`
读写活动标签的 `localStorage` 或 `sessionStorage`(按来源;`localStorage` 在配置文件中持久化)。与 `browser_cookies` 对称。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `action` | `get` 或 `set` 或 `remove` 或 `clear` | 是 | - | 操作 |
| `area` | `local` 或 `session` | 否 | `local` | 哪个存储 |
| `key` | 字符串 | 如果设置或删除则必填 | - | 存储键(`get` 可选 → 返回所有) |
| `value` | 字符串 | 如果设置则必填 | - | 要存储的值 |
| `tab_id` | 字符串 | 否 | 活动标签 | 使用的标签的来源 |
### `browser_network_log`
检查最近的网络请求。每个配置文件的环形缓冲区,最多 500 个,跨所有标签。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `tab_id` | 字符串 | 否 | 所有标签 | 仅来自此标签的条目 |
| `limit` | 整数(1-500) | 否 | `100` | 最大条目数 |
| `url_regex` | 字符串(正则表达式) | 否 | - | 仅匹配的 URL |
| `method` | HTTP 方法 | 否 | - | 按方法过滤 |
| `failed_only` | 布尔值 | 否 | `false` | 仅网络错误(ERR_*, 被阻止) |
| `min_status` | 整数(100-599) | 否 | - | 仅 ≥ 此状态的响应 |
输出:
```
── 3 条记录(共 47 条) ──
14:16:32.170 200 GET https://api/v1/users [xhr, 85ms]
14:16:32.220 404 GET https://api/v1/missing [xhr, 12ms]
14:16:32.300 FAIL(...) POST https://third-party/track [fetch, 2013ms]
```
### `browser_network_body`
返回捕获的 HTTP **响应体**(XHR/fetch 返回的内容)。仅捕获小型文本/JSON 响应(最新的 50 个,大小上限 ~256 KB)。按 `url_regex` / `method` 过滤;`index` 从最近的匹配项倒数(`0` = 最新)。与 `browser_network_log` 搭配使用以检查 API 调用返回的内容。
### `browser_console_log`
检查最近的浏览器控制台输出(环形缓冲区,每个配置文件最后 500 条):`console.log`/`info`/`warn`/`error`/`debug` 加上未捕获的页面错误(级别 `pageerror`)。按 `tab_id`、`level` 或 `text_regex` 过滤。输出按时间顺序排列,如 `browser_network_log`。
### `browser_handle_dialog`
设置下一个原生对话框(`alert`/`confirm`/`prompt`)的处理方式 - 在触发对话框的操作之前调用它。`action`: `accept`(默认)/ `dismiss`;`prompt_text` 填充 `prompt`;`persist: true` 应用于所有对话框。没有它,对话框会自动关闭。
### `browser_set_geolocation`
为上下文设置模拟的地理位置(`latitude`、`longitude`,可选 `accuracy`)。与 `browser_permissions`(授予 `geolocation`)搭配使用,以便页面的 `navigator.geolocation` 可以读取它。
### `browser_scroll`
滚动当前标签。`up`/`down` 按 `amount` 像素;`top`/`bottom` 跳转。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `direction` | `up`/`down`/`top`/`bottom` | 否 | `down` | 方向 |
| `amount` | 整数 | 否 | `800` | 像素(对于顶部/底部忽略) |
| `tab_id` | 字符串 | 否 | 活动标签 | 要操作的标签 |
### `browser_back` / `browser_forward` / `browser_reload`
导航历史记录。只有 `tab_id` 参数。`back`/`forward` 如果没有更多历史记录,则报告 `Already at earliest/latest history entry`。
### `browser_wait`
等待元素达到给定状态 **或** JavaScript 条件变为真。提供 `selector` 或 `condition` 中的一个。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `selector` | 字符串 | 二选一 | - | 要等待的 CSS 选择器 |
| `condition` | 字符串 | 二选一 | - | `condition` | 字符串 | 二选一 | JS 表达式轮询直到真 (`page.waitForFunction`),例如 `window.__ready === true` |
| `state` | `visible`/`hidden`/`attached`/`detached` | 否 | `visible` | 目标状态(使用 `selector`) |
| `timeout` | 整数 | 否 | `10000` | 最大等待时间(毫秒) |
| `tab_id` | 字符串 | 否 | 活动标签 | 要操作的标签 |
### `browser_evaluate`
在页面中执行 JavaScript 表达式并返回 JSON 序列化结果。
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `expression` | 字符串 | 是 | JavaScript 表达式(必须返回 JSON 可序列化) |
| `tab_id` | 字符串 | 否 | 要操作的标签 |
### `browser_tabs_list` / `browser_tab_switch` / `browser_tab_close`
列出标签(`→` 表示活动标签)、切换活动标签、关闭标签。
### `browser_open_visible`
在可见的(非无头)Chrome 窗口中打开 URL 以进行手动交互 - 登录、CAPTCHA、SSO、2FA。Cookie/localStorage 位于持久化配置文件中。关闭窗口返回到无头模式。
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `url` | 字符串(URL) | 是 | 要打开的 URL |
### `browser_screenshot`
拍摄 PNG 快照。默认视口;`full_page=true` 用于整个滚动页面;`selector` 用于特定元素(首先滚动到视图中)。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `full_page` | 布尔值 | 否 | `false` | 视口 vs 整个页面。忽略 `selector` 设置 |
| `selector` | 字符串 | 否 | - | 仅捕获此元素 |
| `tab_id` | 字符串 | 否 | 活动标签 | 要操作的标签 |
返回 `image` 内容块(PNG,base64)。
### `browser_configure`
运行时更改浏览器设置。所有参数均为可选 - 只需传递要更改的内容即可。某些更改会触发浏览器上下文重启(所有打开的标签页都会关闭,响应标志它)。
**无需重启(每个标签页):**
- `viewport_preset` - `mobile` / `tablet` / `desktop` / `desktop-hd` / `desktop-2k`
- `viewport_width` + `viewport_height` - 自定义
- `color_scheme` - `light` / `dark` / `no-preference`
- `tab_id` - 哪个标签页
**无需重启(上下文范围):**
- `user_agent` - 自定义
- `ua_preset` - `chrome-desktop` / `chrome-mobile` / `safari-desktop` / `safari-mobile` / `firefox-desktop`
- `locale` - 例如 `en-US`, `ru-RU`, `ja-JP`
- `extra_headers` - 自定义 HTTP 头(`{ "Authorization": "Bearer …" }`)合并到所有未来请求中。与上下文的标头集共享,因此显式 `User-Agent` / `Accept-Language` 在此处覆盖 `user_agent` / `locale` 设置的内容。
**需要重启:**
- `device_preset` - `iphone-15` / `iphone-se` / `ipad` / `ipad-pro` / `pixel-8` / `galaxy-s24` / `desktop-retina`
- `device_scale_factor` - 例如 2 表示视网膜,3 表示 iPhone
- `is_mobile` - 启用 `isMobile` + `hasTouch`
设备预设:
| 预设 | 视口 | 缩放比例 | 移动设备 | 触摸设备 |
| --- | --- | --- | --- | --- |
| `iphone-15` | 393×852 | 3× | 是 | 是 |
| `iphone-se` | 375×667 | 2× | 是 | 是 |
| `ipad` | 820×1180 | 2× | 是 | 是 |
| `ipad-pro` | 1024×1366 | 2× | 是 | 是 |
| `pixel-8` | 412×915 | 2.625× | 是 | 是 |
| `galaxy-s24` | 360×780 | 3× | 是 | 是 |
| `desktop-retina` | 1280×900 | 2× | 否 | 否 |
---
## REST API(用于脚本)
除了 MCP 之外,服务器还提供一个 **无状态 REST/JSON API**,位于 `/api/v1`,因此任何语言的脚本(curl、Python、JS)都可以驱动 **相同的实时浏览器**,而无需进行 MCP 握手 - 而不是返回 LLM 格式的文本,而是返回 **结构化 JSON**。
**模型。** 每个调用都是一个独立的 `POST` 请求;没有会话握手。`?profile=<name>` 选择要驱动的共享浏览器 - 使用 MCP 代理使用的相同配置文件,您可以共享其 cookie、标签页和网络日志记录。标签页是共享的,可以通过 URL 地址:显式传递正文中的 `tab_id` 以操作特定标签页(您的“活动标签页”独立于代理的;对于并发脚本,请始终传递 `tab_id`)。服务器为每个配置文件保留一个内存中的 *配置文件持有者*,它在空闲 TTL 与 MCP 会话相同(或在 `DELETE` 请求后立即释放)。与 `/api` 相关的身份验证、来源、内容类型和正文大小限制与 `/mcp` 相同。
**端点。**
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `POST` | `/api/v1/tools/<tool>?profile=<name>` | 运行工具。 正文 = JSON 参数(与 MCP 工具相同的架构)。 |
| `GET` | `/api/v1/tools` | 列出工具(名称 + 描述)。 |
| `GET` | `/api/v1/openapi.json` | OpenAPI 3.1 规范(使用 `openapi-typescript` 等生成客户端)。 |
| `DELETE` | `/api/v1/profiles/<name>` | 释放配置文件持有者(如果没有 MCP 会话持有它,则关闭其浏览器)。 |
**响应信封。** HTTP 状态反映 *传输* 部分;工具的结果在正文中。运行成功的工具 - 即使失败的工具(如 `browser_expect` 断言失败) - 返回 **200**:
```jsonc
// 200 - 成功
{ "ok": true, "data": { "tab_id": "t1", "url": "https://example.com/", "title": "Example", "status": 200 },
"content": [{ "type": "text", "text": "HTTP 200\nURL: …" }] }
// 200 - 工具级失败(ok:false,不是 HTTP 错误)
{ "ok": false, "error": { "message": "FAIL visible …" },
"data": { "ok": false, "assertion": "visible", "expected": "#login", "actual": "hidden" } }
```
非 2xx 状态码保留用于传输问题:**400** 参数无效(包含 zod `issues`);**401** 缺失/无效 Bearer;**403** 来源不允许;**404** 未知工具;**415** 非 JSON 正文;**503** 会话容量达到。
**curl 示例。**
```bash
curl -s -XPOST 'http://127.0.0.1:7777/api/v1/tools/browser_open?profile=work' \
-H 'content-type: application/json' -d '{"url":"https://example.com"}'
curl -s -XPOST 'http://127.0.0.1:7777/api/v1/tools/browser_read?profile=work' \
-H 'content-type: application/json' -d '{"mode":"markdown"}' | jq -r '.data.content'
```
# 使用身份验证:添加 -H 'authorization: Bearer <key>'
**JS/TS 客户端.** 一个轻量级**零运行时依赖**的客户端位于
[`packages/browser-client-js`](packages/browser-client-js/) (`@graphmemory/browser-client`),
其中包含从 OpenAPI 规范生成的工具/参数类型:
```ts
import { BrowserClient } from "@graphmemory/browser-client";
const b = new BrowserClient({ baseUrl: "http://127.0.0.1:7777", profile: "work" });
const { data } = await b.read({ mode: "markdown" });
```
**限制.** `browser_screenshot` 在 `content` 中返回 base64。文件路径工具
(`browser_save` / `browser_upload` / `browser_download_wait`)操作于**服务器**
的文件系统(由相同的 IO 保护机制隔离)。客户端针对 Node.js;浏览器标签调用者
需要匹配的 `--cors-origin` 和 CORS 预检请求,而服务器不发出这些请求。
---
## 配置
所有标志都是可选的 — 默认的仅限本地回环配置开箱即用。优先级:
**命令行标志 > 环境变量 > 默认值**。
| 标志 | 环境变量 | 默认值 | 备注 |
|---|---|---|---|
| `-p, --port` | `BROWSER_MCP_PORT` | `7777` | |
| `-H, --host` | `BROWSER_MCP_HOST` | `127.0.0.1` | |
| `--api-key` | `BROWSER_MCP_API_KEY` | *(关闭)* | **主机 ≠ 本地回环时必填** |
| `--allow-insecure` | `BROWSER_MCP_ALLOW_INSECURE` | `false` | 覆盖拒绝启动 |
| `--cors-origin` | `BROWSER_MCP_CORS_ORIGIN` | `null` | 以逗号分隔的来源或 `*` |
| `--max-sessions` | `BROWSER_MCP_MAX_SESSIONS` | `50` | 并发 MCP 会话 |
| `--session-ttl` | `BROWSER_MCP_SESSION_TTL_SEC` | `1800` | 空闲会话回收(30 分钟) |
| `--[no-]headless` | `BROWSER_MCP_HEADLESS` | `1` | `0` = 可见 |
| `--[no-]stealth` | `BROWSER_MCP_STEALTH` | `1` | `playwright-extra` + 隐身插件 |
| `--channel` | `BROWSER_MCP_CHANNEL` | `chrome` | Chromium 频道(`chrome`, `msedge`, `chromium`) |
| `--[no-]javascript` | `BROWSER_MCP_JAVASCRIPT` | `1` | |
| `--viewport` | `BROWSER_MCP_VIEWPORT` | `1280x900` | 宽高 |
| `--device-scale-factor` | `BROWSER_MCP_DEVICE_SCALE_FACTOR` | `1` | |
| `--[no-]mobile` | `BROWSER_MCP_MOBILE` | `0` | |
| `--user-agent` | `BROWSER_MCP_USER_AGENT` | — | |
| `--locale` | `BROWSER_MCP_LOCALE` | — | 接受语言 |
| `--color-scheme` | `BROWSER_MCP_COLOR_SCHEME` | — | |
| `--proxy` | `BROWSER_MCP_PROXY` | — | 例如 `http://proxy:8080` |
| `--proxy-bypass` | `BROWSER_MCP_PROXY_BYPASS` | — | 以逗号分隔的域名 |
| `--proxy-username` | `BROWSER_MCP_PROXY_USERNAME` | — | |
| `--proxy-password` | `BROWSER_MCP_PROXY_PASSWORD` | — | |
| `--tab-ttl` | `BROWSER_MCP_TAB_TTL_SEC` | `600` | 非活动标签回收(10 分钟) |
| `--max-chars` | `BROWSER_MCP_MAX_CHARS` | `50000` | `browser_read` 的上限 |
| `--max-html-bytes` | `BROWSER_MCP_MAX_HTML_BYTES` | `10000000` | JSDOM 解析前的 HTML 上限(OOM 防护) |
| `--settle-ms` | `BROWSER_MCP_SETTLE_MS` | `500` | 安静窗口时长 |
| `--settle-timeout-ms` | `BROWSER_MCP_SETTLE_TIMEOUT_MS` | `3000` | 导航/点击后硬性结算超时 |
| `--action-timeout-ms` | `BROWSER_MCP_ACTION_TIMEOUT_MS` | `10000` | 每动作超时:点击/键入/按下/悬停/选择/检查/拖动 |
| `--nav-timeout-ms` | `BROWSER_MCP_NAV_TIMEOUT_MS` | `30000` | 每导航超时:打开/导航/打开可见 |
| `--profile-dir` | `BROWSER_MCP_PROFILE_DIR` | `~/.browser-mcp/profiles` | 配置文件基准目录 |
### 安全选项(仅限环境变量 - 锋利的逃生阀)
| 环境变量 | 默认值 | 效果 |
|---|---|---|
| `BROWSER_MCP_ALLOW_FILE_URLS` | `0` | 允许 `file://` 在 `browser_open` / `browser_download_wait` 中 |
| `BROWSER_MCP_ALLOW_PRIVATE_NETWORKS` | `0` | 允许本地回环 / RFC1918 / 链路本地 / ULA IPs |
| `BROWSER_MCP_ALLOW_ANY_WRITE_PATH` | `0` | 为 `browser_save` / `browser_download_wait` 禁用写入沙盒 |
| `BROWSER_MCP_ALLOW_ANY_UPLOAD_PATH` | `0` | 为 `browser_upload` 禁用读取沙盒 |
| `BROWSER_MCP_SANDBOX_DIR` | `~/.browser-mcp` | 下载/上传沙盒基准目录 |
| `BROWSER_MCP_READ_BODY_TIMEOUT_MS` | `10000` | HTTP 正文读取的时钟上限(慢loris) |
| `BROWSER_MCP_NO_NETWORK_BODY` | `0` | `1` 禁用 `browser_network_body` 的被动响应体捕获 |
| `BROWSER_MCP_MAX_REQUEST_BYTES` | `1048576` | 最大接受的 HTTP 请求体(1 MiB)。**提高它会削弱 DoS 防护** - 仅在合法工具调用需要更大有效载荷时增加 |
### 资源调整(仅限环境变量)
每个配置文件的内存环容量。越大 = 更深的历史记录,但 RAM 成本更高。`browser_network_log` / `browser_console_log` 的 `limit` 参数
上限为匹配的环大小(您无法读取超过环所保存的内容)。
| 环境变量 | 默认值 | 效果 |
|---|---|---|
| `BROWSER_MCP_NET_RING` | `500` | 网络日志环容量(`browser_network_log`) |
| `BROWSER_MCP_CONSOLE_RING` | `500` | 控制台日志环容量(`browser_console_log`) |
| `BROWSER_MCP_BODY_RING` | `50` | 捕获的响应体环容量(`browser_network_body`) |
| `BROWSER_MCP_BODY_MAX_BYTES` | `262144` | 每个捕获的响应体最大大小(256 KiB);较大的主体被跳过 |
`browser-mcp --help` 打印 CLI 列表。
---
## 架构
### 进程拓扑
browser-mcp 是一个 Node.js 进程。每个命名配置文件在首次使用时都会启动自己的
Chromium(`launchPersistentContext`)。同一配置文件的并发用户
- 多个 MCP 会话和/或 REST 配置文件持有者 - 共用该上下文。当 MCP 会话和 REST
持有者都不再引用配置文件时,上下文才会关闭(引用计数)。
### HTTP 层
传输:`@modelcontextprotocol/sdk` 的 `StreamableHTTPServerTransport` 在
`node:http` 之上。一个 TCP 监听器服务于两个接口 - MCP (`/mcp`, `/mcp/<profile>`) 和无状态 REST API (`/api/v1/*`) - 以及 `/health`。
两个接口共用一个工具注册表(`src/registry.ts` → `TOOLS`),因此一个工具
被定义一次并在两个接口上公开;它们还共用每个配置文件的
`BrowserManager`(见下文),因此 REST 脚本和 MCP 代理在同一
配置文件上驱动同一个浏览器。
每个 MCP 客户端在 `initialize` 时都会获得一个会话(`mcp-session-id` 中的随机 UUID)。
会话有自己的 `McpServer` 实例和传输。REST 端对调用者来说是无会话的;
内部,它为每个配置文件保持一个 *配置文件持有者*(一个带有 `kind:"rest"` 的会话记录,没有传输)
缓存共享的浏览器 + 每个工具的处理程序。两种类型生活在同一个映射中,
共用 `max_sessions` 上限,并通过 60 秒间隔定时器在 `session_ttl` 后回收;
浏览器仅在 MCP 会话和 REST 持有者都不引用配置文件时关闭(引用计数)。
硬上限:`max_sessions`(503 溢出时),最大请求体(默认 1 MB,
`BROWSER_MCP_MAX_REQUEST_BYTES`),每个工具的 zod `.max(…)` 对每个用户字符串。
请求管道( `/mcp` 和 `/api` 的共享防护门):
```
→ 路由(/mcp[/<profile>] 或 /api/v1/…);否则 404
→ 源检查(允许列表;未设置源 = 原生客户端,始终允许)
→ Content-Type 检查(POST 需要 application/json)
→ 身份验证检查(Bearer,timingSafeEqual)
→ /mcp → 会话查找/创建 → MCP SDK transport.handleRequest
/api → 配置文件持有者查找/创建 → runTool → JSON 信封
(两者在创建时应用 max_sessions 上限)
```
`apps/browser-mcp/src/app.ts` 导出 `createApp()`,返回
`http.Server` - 导入时没有副作用,这使得集成测试变得微不足道。
`apps/browser-mcp/src/index.ts` 是一个薄的引导程序,
调用 `createApp().httpServer.listen()` 并连接 SIGINT/SIGTERM。
### BrowserManager
每个配置文件一个实例,由配置文件上的每个会话共用。持有:
- `BrowserContext`(Playwright 的持久上下文)。
- `tabs: Map<tab_id, Page>` - 每个标签的 8 个字符的 nanoid。
- `lastUsed: Map<tab_id, timestamp>` - 驱动 TTL 清理程序。
- `netLog` - 环(默认 500 条目)由页面
`request`/`requestfinished`/`requestfailed` 监听器馈送。
- `consoleLog` - 环(默认 500)`console.*` 消息和未捕获的
`pageerror`s,由 `page.on("console")` / `page.on("pageerror")`
(`browser_console_log`)。
- `bodyLog` - 环(默认 50)小文本/JSON 响应体
(≤256 KB 每个),在 `requestfinished`
(`browser_network_body`)中被动捕获。
- `_overrides` - `browser_configure` 的上下文级设置(在
下一个上下文创建时应用)。
环容量和每个主体大小上限可调 - 参见
[资源调整](#配置)(`BROWSER_MCP_NET_RING` / `CONSOLE_RING` /
`BODY_RING` / `BODY_MAX_BYTES`)。
每 60 秒,清理程序关闭不活动的标签,标签不活动时间超过 `tab_ttl`(当前活动的标签始终被保留)。
在 `shutdown()` 时,上下文被关闭 - 受限于 8 秒的竞赛,以便僵化的
Chromium 不会挂起监管者的 SIGTERM 路径或测试拆卸 - 以及所有
Chromium 子进程退出。
### 浏览器会话
工具处理程序不直接与 `BrowserManager` 交互:每个 MCP 会话都会获得一个
薄的 `BrowserSession` 外观层,涵盖共享管理器。它拥有每个会话的
**活动标签** (`currentTabId`) 和 **快照存储**(`store_as` /
`diff_against` 命名快照),在委托之前解析 `tab_id ?? currentTabId`
到具体 ID。其他一切 - 上下文、标签、cookie 和网络 / 控制台 / 正文环
- 在管理器上保持共享,因此一个会话中的登录或捕获的请求在其他会话中是可见的。
两个客户端在同一个配置文件上,因此不会相互覆盖对方的“当前标签”
或在快照名称上发生冲突;要完全独立的状态,请给每个客户端分配自己的命名配置文件。
### 无障碍快照管道
Playwright 1.40 删除了 `page.accessibility.snapshot()`,因此 browser-mcp
直接从 Chrome DevTools 协议中提取 AX 树:
```
CDP Accessibility.enable
├─ 完整树: Accessibility.getFullAXTree (用于顶级快照)
└─ 子树: DOM.querySelector → describeNode → getPartialAXTree
(带有 fetchRelatives,包括祖先)
↓
cdpAxToTree(nodes, interestingOnly):
- 选择根节点(父节点不在映射中)
- 递归遍历 childIds
- 跳过 ALWAYS_NOISY_ROLES(InlineTextBox)
- 如果 interestingOnly:展平忽略的节点
- 收集角色/名称/值/描述 + 选定的属性集
↓
collapseRedundantText(node):
- 如果父节点是匿名的并且有 StaticText 子节点,则提升它们的文本
作为父节点的名称(给出列表项 / 单元格 / 行标识)
- 删除 StaticText 子节点,其文本与父节点的名称完全匹配
↓
filterCompact(node): (如果 compact=true)
- 保持交互式角色(按钮,链接,文本框,复选框,选项,...)
- 保持结构化地标(标题,导航,主,表单,对话框,
列表,列表项,表格,行,单元格,...)
- 删除所有其他内容;提升单个有趣的后代
↓
renderAxNode(node): YAML-ish 输出,用于线路
- “ N 角色 “名称” [attr1=v1, attr2, …]"
- 缩进的子节点`
快照算法(`diffSnapshots`):
1. 将两个树展平为 `Map<path, AxNode>`,其中 `path = /role|name/role|name/…`
(值从签名中排除,因此文本框编辑看起来像
remove+add)。
2. `after` 中存在但 `before` 中不存在的键 → `added`。
3. `before` 中存在但 `after` 中不存在的键 → `removed`。
4. 键存在于 `after` 和 `before` 但 `stateSummary(a) !== stateSummary(b)` → `changed`。
状态摘要包括 `value`,`checked`,`pressed`, `pressed`, `selected`,
`disabled`, `expanded`, `focused`.
### 网络环缓冲区
```
page.on("request") ── reqStart.set(req, { ts, tab_id })
page.on("requestfinished") ──┐
page.on("requestfailed") ──┤── pushNet({ ts, tab_id, method, url, status?, duration_ms?, failed? })
▼
NetLog 环(容量 500)
```
`readNetLog` 按时间顺序遍历环,应用 tab/method/failedOnly/minStatus/urlRegex 过滤器,
然后切片最近的 `limit`。
### `browser_read` 的紧凑渲染
基于 JSDOM。`stripCompactDom(document)` 删除匹配的节点
`nav, header, footer, aside, script, style, noscript, template, svg, iframe,
[role=navigation|banner|contentinfo|complementary|search], [aria-hidden=true],
[hidden]`。三种口味:
- `htmlToMarkdown(html, url, max, fallback, compact)` - 剥离,然后
Readability → turndown。 如果 Readability 失败,则回退到 `plainTextFallback`
或原始正文 turndown。
- `stripCompactHtml(html, url)` - 剥离,返回 body innerHTML。
- `stripCompactText(html, url)` - 剥离,插入 `\n\n` 在块元素之前,
折叠内联空格,限制空白行。
### 关闭
SIGINT/SIGTERM 触发:
```
1. 停止接受新的 HTTP 连接(httpServer.close)
2. 清除会话回收间隔
3. 关闭所有 MCP 传输 + McpServer 实例(REST 持有者没有)
4. 关闭每个唯一的 BrowserManager(关闭 Chromium)
5. process.exit(0)
```
---
## 安全模型
browser-mcp 驱动您机器上的 Chromium - 可以访问 `/mcp` 或 `/api` 的任何人都可以
访问任意 URL,窃取已登录的会话 cookie,解决 CAPTCHA 验证码,在您的名义下
(在下面的防护措施之外)读取任意本地文件。 默认配置选择
所以这不会意外发生:
### 网络级防护
- **拒绝启动不安全配置。** 绑定到非本地回环主机(`0.0.0.0`,任何
LAN IP)并且没有设置 API 密钥? 以 2 为退出码和响亮的错误退出。
使用 `--allow-insecure` 覆盖(例如,隔离的 VM,内 Docker 网络)。
- **CSRF 防御。** 发送到 `/mcp` 和 `/api` 的 POST 请求必须带有
`Content-Type: application/json`(不是 CORS 简单类型 - 浏览器必须
预检,我们不回答 OPTIONS)。 如果存在 `Origin` 头部,
它必须与 `BROWSER_MCP_CORS_ORIGIN` 匹配(默认为 **空** - 只有本地
客户端,如 curl,Claude Code 和 Node 端脚本,它们不发送任何
`Origin`)。
- **请求体大小 + 慢loris。** 每个请求最大 1 MB;完整正文必须在
10 秒内到达(否则撕裂套接字)。 没有慢滴答 DoS 攻击。
- **基于时间的身份验证。** API 密钥比较使用 `crypto.timingSafeEqual`
以便令牌猜测不会受益于短路字符串比较。
- **会话上限。** `max_sessions`(默认 50)防止资源耗尽。
- **配置文件名称正则表达式。** `^[a-zA-Z0-9_-]{1,64}$` - 在 HTTP
层强制执行,以便 `../../etc/passwd` 无法逃脱配置文件基准目录。
### 工具级防护
工具接口(`browser_open`,`browser_save`,`browser_upload` 等)
否则会将可达的 `/mcp` 端点变成本地文件读取/写入基元。
默认拒绝,视情况允许:
- **导航 URL 白名单。** `browser_open`,`browser_download_wait`
(操作 = 导航)和 `browser_permissions`(来源)仅接受 `http:`,
`https:` 和 `about:blank` 默认情况下。 `file://`,`javascript:`,
`data:`,`chrome:`,`view-source:` 和原始私有 IP 主机(`127.0.0.0/8`,
`10/8`,`172.16–31/12`,`192.168/16`,`169.254/16`,`::1`,
`fc00::/7`,`fe80::/10`)被拒绝。 通过以下方式允许:
- `BROWSER_MCP_ALLOW_FILE_URLS=1` - 允许 `file://`(用于本地夹具)。
- `BROWSER_MCP_ALLOW_PRIVATE_NETWORKS=1` - 允许本地回环 / 内部网 /
云元数据 IP。 Docker-compose 设置中需要此功能,容器之间通过服务名称进行 curl。
- **下载/保存沙盒。** `browser_save` 和 `browser_download_wait`
默认写入 `~/.browser-mcp/downloads/<profile>/`。 相对路径解析为沙盒;
绝对路径如果逃离沙盒则被拒绝。 `BROWSER_MCP_ALLOW_ANY_WRITE_PATH=1`
禁用沙盒。
- **上传沙盒。** `browser_upload` 从 `~/.browser-mcp/uploads/<profile>/` 读取。
将文件放在那里,或设置 `BROWSER_MCP_ALLOW_ANY_UPLOAD_PATH=1`。 如果没有,
一个 `browser_open(attacker.com)` + `browser_upload({ files: ["/etc/passwd"] })`
泄露任何您的 uid 可以读取的文件。
- **沙盒基准目录。** 通过 `BROWSER_MCP_SANDBOX_DIR` 覆盖两个沙盒的根目录
(默认为 `~/.browser-mcp`)。
- **日志缩减。** 包含机密的工具参数 - cookie 值,键入文本
(`browser_type`),JS 表达式,文件系统路径,自定义标头
(`browser_configure.extra_headers`),`browser_storage` 值和
`browser_handle_dialog` 提示文本 - 在 `withLog` stderr
输出中被缩减,以便中心化日志收集器不会拾取密码或会话令牌。
- **`browser_evaluate` / `browser_storage` 结果上限。**
输出在 `BROWSER_MCP_MAX_CHARS` 处截断(默认为 50,000),因此页面返回
1 GB 数组不会使监管者 OOM。
- **被动响应体捕获。** `browser_network_body` 通过缓冲
小文本/JSON 响应(最新的 50 个,每个 ≤256 KB)**在内存中**
当它们到达时 - 因此登录 API 的 JSON(令牌,PII)对于
会话生命周期内是可检索的,在同一信任边界内,与配置文件 cookie 相同。
正文永远不会写入磁盘或日志。 设置 `BROWSER_MCP_NO_NETWORK_BODY=1`
以完全禁用捕获。 按配置文件(在同一会话中在该配置文件上共享)- 使用单独的命名配置文件
来隔离并发客户端。
### 构建时非攻击面
- **监管者上没有远程代码执行。** 在 `apps/browser-mcp/src/` 中没有
`eval`/`child_process`/`vm`/`Function()`。 `browser_evaluate`
在 Chromium 的渲染器沙箱中运行 JS,而不是在监管者上。
- **环境隔离。** `BROWSER_MCP_*` 环境变量(API 密钥,主机,限制)
在 Chromium 启动前被过滤掉,因此页面脚本无法
指纹识别监管者的配置。
Docker 镜像绑定到 `0.0.0.0` 并 **需要** 一个 API 密钥 -
拒绝启动检查在没有一个时启动。
健康检查端点
`GET /health` 返回包含状态、运行时间、会话/配置文件数量和活动配置摘要的 JSON。`sessions` 计数包括 MCP 会话和活动 REST 配置文件持有者。无需身份验证,安全可探查。不会泄露已访问的 URL、Cookie页面内容。
```json
{
"status": "ok "uptime_ms": 123456,
"sessions": 2,
"profiles": 1,
"config": { "host": "127.0.0.1", "port": 7777, "headless": true, "stealth": true, "auth": "}
```
---
## 测试
```bash
npm test # 运行 470 个测试()
npm run test:watch # 监视模式
test:coverage # 运行 + 在 coverage/ 下生成报告
npm run test:integration # 仅限 Play的测试
```
测试套件分为 48 个文件: **单元测试**(21 个文件):`render.ts`逻辑测试(紧凑助手),
AX 树操作(`cdpAxToTree`,使用合成 CDP 载荷,
`filterCompact`、`diffSnapshots`、`collapseRedundantText`、`renderAxNode`),
`config.ts` 助手,可调配置连接(操作/导航超时,环容量,请求体上限),
`log.ts`、`.ts`、`lib/url-safety.ts`、`lib/path.ts`,
日,配置文件名验证,网络控制台环缓冲区,
`resolveLocator` 路由,`insecureStartupProblem` 门控,
以及边缘分支的模拟驱动工具处理程序(快照差异溢出,
无标志 Cookie,PDF 无头错误,下载失败,无 HTTP 来源的权限)。
还有工具注册表,REST `runTool` 状态映射 + 路由解析,
以及 OpenAPI 生成(所有 36 个模式转换)。
- **集成测试**(27 个文件):通过 `BrowserManager` 驱动真正的无头 Chromium,
与本地 HTML 固定装置交互。涵盖每个工具处理程序,
Manager` 的公共包括代理配置上下文), HTTP 服务器(CSRF / 身份验证 / 会话生命周期 JSON-RPC / 会),
REST 接口(`/api/v1` 正常/错误路径,防护,者生命周期,
跨界面引用计数,OpenAPI),以及 AX 树管道端到端。
进程内 HTTP 测试服务器执行 /4xx/5支和失败的网络。
集成测试使用 `_MCP_HEADLESS= 和 `os.tmpdir的临时配置文件目录 —
您的本地 `~/.browser/profiles/` 不会被触 `SKIP_INTEGRATION=1` 以跳过 Playwright 支持的套件没有 Chromium 的环境中)。
覆盖率目标(vitest.config.ts):90 / 85% 函数 / 80% 分支 / 90% 语句。
上限wright 限制 — `(() => …)` 在 Chromium 的 V8,而不是 Node 的,因此即使端到端执行它们也无法被检测。
CI 在每次推送时运行完整的套件(Linux;`npx playwright install --with-deps chromium` 在工作流中)。
---
## Docker
Dockerfile 位于 `apps/browser-mcp/`,但构建上下文是**工作区根目录**(单个 `package-lock.json` 位于那里),因此使用 `-f` 从仓库根目录构建:
```bash
docker build -t browser-mcp -f apps/browser-mcp/Dockerfile .
docker run --rm -p 7777:7777 -e BROWSER_MCP_API_KEY=$(openssl rand -hex 32) browser-mcp
```
或者使用 docker-compose(docker-compose 文件为您设置 `context: ../..`):
```bash
# 一次性:在 docker-compose 文件旁边创建一个 .env 文件,包含一个真实的密钥
echo "BROWSER_MCP_API_KEY=$(openssl rand -hex 32)" > apps/browser-mcp/. compose -/browser-mcp/docker up
```
来自 的预构建镜像:
```bash
dockerrm -p 7777777 -e BROWSER_MCP_API_KEY=$(opensslhex 32) \
cr.io/graph-memory/browser:latest
```
容器:
- 使用 `tini`1,以便在 Chromium死亡时回收僵尸- 以专用的非 root` 用户运行 Chromium 在 Docker 卷(`browser`)中,将配置文件保存在 `/home/browser/.browser-mcp`。
- 使用 Playwright 捆绑的 Chromium设置 `BROWSER_MCP_CHANNEL=chromium` —
`chrome` 通道在镜像内不可用)。
- 健康每 30 秒 `node -e "fetch('127.0.0:7777/health')- 拒绝在没有ROWSER_MCP_API 的情况下启动(镜像绑定到.0.0`,因此启动防护被触发)。
**`_visible` 在 Docker作用**(无显示服务器在本地/桌面。
---
## 平台说明 **macOS / Linux — Node ≥ Playwright 在第一次运行时的 `postinstall` 钩子安装 Chromium。
` 通道安装的 Chrome。在系统上,请使用=chromium` 回退到 Playwright 的版本。
Docker 镜像自动设置此选项。
-ed Linux 容器** 可能需要额外的权限来运行 Chromium 沙箱。
官方 处理了这一点;如果不同的基础镜像,请确保 `libnssdbus-1-3 libgbm1 libasoundatk-bridge2.0-0`(和---
## 开发
此仓库是一个 **npm 工作区**。已发布的应用程序位于 `-mcp/`(包memory/browser-mcp`);/` 保留用于共享库。根使用 `-w` 应用程序,因此您可以在仓库根目录下运行所有内容:
```bash
git clonegithub.com/graph-memory/browser-mcp.git
cd browser-mcp
npm install # 安装所有工作区,node_modules 提升到根目录
npm run dev # 使用 tsx 运行(无构建步骤)
npm run build # 将 TypeScript 编译为 apps/browser-mcp/dist/
npm test # 完整的测试套件(470 个npm run test:coverage
```
布局:
/browser-mcp/(src/,testDockerfile,bin/browser-client-js/ @graphmemory/browser-client(REST)
```
### 发布流程bash
# 提升应用程序的版本(在工作区内运行)
npm version patch -w @graphmemory/browser-mcp # 或 minor
# 推送并
git push && git push --tags
```
触发两个独立的工作流:`publish.yml`(工作区包的 npm 发布 — **由完整的测试套件控制**)和.yml`(多架构镜像构建,通过 Dockerfile 编译 TypeScript 但不重新运行套件)。使用已通过 CI 的提交(`ci.yml`分支推送 / PR 上运行)标记一个提交,以便和镜像来自已测试的代码。
---
## 常见问题
**这是否取代 ``?** 不 — 它们是互补的WebFetch` 适用于一次性读取公共页面。browser-mcp 用于会话、身份验证、交互以及需要 JavaScript / Cookie / 状态的任何操作。
**为什么选择 Playwright 而不是 Puppeteer?** Playwright 具有更好的角色/标签定位器(无障碍树优先等待和跨浏览器尽管我们仅提供 Chromium)。此外,过去一年中稳定。
**我可以在同一台机器上运行多个 browser-mcp 实例吗?的 — 每个实例使用自己的端口(`--)。它们是具有不同配置文件基目录(`--profile-dir`)的独立**如何登录到阻止浏览器的站点?** 使用 `browser_open_visible`。Chromium 以可见的方式打开持久配置文件,以便您可以解决 CAPTCHA / 2FA。关闭窗口后,配置文件。后续来自代理的调用以无头模式运行在同一个配置文件上,并。
**配置文件数据存储** 默认情况下为/.browser-mcp/proname>/`(profile-dir` / `BROWSER_MCP_PROFILE_DIR` 覆盖)。这些是标准的 Chromium `user-data-dir`.sqlite、本地存储、 缓存等。以重置配置文件。
**我的常规 Chrome — browser-mcp 可以?** 否。Chromium 使用 singleton 文件锁定其 user-data-dir;一次只能有一个进程使用配置文件。关闭 Chrome 或使用专用的 browser-mcp 配置文件。
**`browser_save pdf` 显示“仅限无头模式”** 已知的 Playwright/Chromium 限制 — 打印到 PDF 需要无头浏览器。如果您已禁用无头`--no`),请 `mhtml`文件存档)或 `html`(原始)如何绕过特定机器人检测?** 首先隐身插件,请尝试 `--no-ste(某些站点检测隐身本身)。否则, 指纹识别是一场你不太可能用通用工具赢得的竞赛 — 考虑使用住宅代理(`--proxy socks5://user:pass@host:port`)或通过 `browser_open_visible` 进行手动会话。
**我可以拦截/模拟网络请求吗?** 尚未。目前,您只能通过 `browser_network_log` 观察请求。拦截/模拟暂时被排除 — 这是一个很大的表面,目前尚未成为实际使用中的阻碍。
**我可以将我的会话记录为 Playwright 脚本吗?** 尚未。与网络拦截的范围决策相同。
**为什么 CI 中没有 `test:windows`?** 尚未连接。代码在 Dockerfile 外没有 POSIX 特定内容,因此 Windows 应该有效 — 但 CI 尚未验证。
**`npm install` 显示 `rimraf@3` / `glob@7` / `inflight@1` 弃用警告。我应该担心吗?** 不。这些警告来自 `puppeteer-extra-plugin-stealth` → `…user-data-dir@2.4.1` → `rimraf@3` → `glob@7` → `inflight@1`。上游已保持沉默一年多,`…user-data-dir` 通过删除的回调 API 将 `rimraf@^3` 固定为 `rimraf@4+` 的更新会破坏插件,一旦其清理代码触发。清理代码处理插件自己的临时配置文件目录;browser-mcp 将其自己的持久 `profileDir` 传递给 `launchPersistentContext`,因此调用 rimraf 的路径不会被触及。无运行时影响,仅限安装时警告。
---
## 许可证
[Elastic License 2.0 (ELv2)](LICENSE)
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器