跳转至

附录 A:Chrome MCP / CDP 实战手册

这份手册讲的是:如何让 AI 用 文本 + JavaScript 精确操作和验证网页,而不是靠“看截图猜”。截图只用来留证。


1. 架构:Claude Code 怎么控制浏览器

Claude Code
   │  MCP 协议(stdio / http)
   ▼
Chrome MCP Server
   │  Chrome DevTools Protocol (CDP)
   ▼
Chrome(有头 / 无头 / 连接已有实例)

关键概念:

  • MCP:模型和工具之间的协议。Claude Code 通过 MCP 调用浏览器工具。
  • CDP:Chrome 的调试协议。MCP Server 用它来导航、点击、执行 JS、截图、抓网络。
  • Profile:Chrome 的用户数据目录。独立 profile 可以保存登录态,也避免和你日常浏览器冲突。

2. 启动一个可调试的 Chrome

2.1 独立 profile(推荐)

mkdir -p ~/.chrome-debug-profile

google-chrome \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.chrome-debug-profile" \
  --no-first-run \
  --no-default-browser-check \
  >/tmp/chrome-debug.log 2>&1 &

验证:

curl -s http://127.0.0.1:9222/json/version | jq .
curl -s http://127.0.0.1:9222/json/list | jq '.[].url'

2.2 无头模式

google-chrome \
  --headless=new \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.chrome-headless-profile" \
  --no-sandbox \
  about:blank &

2.3 连接 Windows 宿主机 Chrome

Windows PowerShell:

& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
  --remote-debugging-port=9222 `
  --user-data-dir="$env:TEMP\chrome-debug-profile"

WSL .wslconfig:

[wsl2]
networkingMode=mirrored

WSL 验证:

curl -s http://localhost:9222/json/version | jq .

2.4 常见启动参数

参数 作用
--remote-debugging-port=9222 开 CDP 端口
--user-data-dir=... 独立 profile(Chrome 136+ 远程调试必需)
--headless=new 无头模式
--no-first-run 跳过首次引导
--no-default-browser-check 跳过默认浏览器检查
--disable-background-networking 减少后台请求
--disable-gpu 无 GPU 环境可选
--no-sandbox root/容器中可能需要(降低安全性)
--window-size=1440,900 初始窗口大小

3. 工具速查表

不同 MCP Server 的工具名不同。下表以本教程使用的 chrome-control 为例。

工具 用途 典型参数
chrome_navigate 打开 URL / 前进后退 url, newWindow, refresh
get_windows_and_tabs 列出窗口/标签页 —
chrome_switch_tab 切换标签页 tabId
chrome_close_tabs 关闭标签页 tabIds
chrome_read_page 读取可访问性树/可见元素 filter: "interactive", depth
chrome_get_web_content 读取页面文本/HTML textContent, htmlContent, selector
chrome_javascript 执行 JS,返回值 code, timeoutMs
chrome_click_element 点击元素 ref / selector / coordinates
chrome_computer 鼠标/键盘/截图/滚动 action, coordinates, text
chrome_fill_or_select 填表单/选择 ref / selector, value
chrome_keyboard 键盘输入/快捷键 keys, selector
chrome_screenshot 截图 savePng, storeBase64, fullPage, selector
chrome_console 控制台日志 mode: "buffer", onlyErrors
chrome_network_capture 抓网络 action: "start"/"stop", needResponseBody
chrome_network_request 带浏览器上下文发请求 url, method, body, headers
chrome_handle_dialog 处理 alert/confirm/prompt action, promptText
chrome_handle_download 等待下载 filenameContains, timeoutMs
chrome_upload_file 上传文件 selector, filePath
chrome_handle_download 下载 —
chrome_console 控制台 —
performance_start_trace / performance_stop_trace 性能追踪 reload, durationMs

如果你用的是 chrome-devtools-mcp,工具名可能是 navigate_page、take_snapshot、evaluate_script、click、fill、take_screenshot、list_console_messages、list_network_requests 等。方法一致。


4. 核心工作流

4.1 导航与读取页面

1. chrome_navigate { url: "http://localhost:5173/login" }
2. chrome_read_page { filter: "interactive", depth: 6 }
3. chrome_get_web_content { selector: "main", textContent: true }

chrome_read_page 返回元素引用(ref),后续可以用 ref 点击/填写,比 CSS 选择器更稳。

4.2 执行 JS 并断言

// 返回页面基本信息
({
  url: location.href,
  title: document.title,
  readyState: document.readyState
})
// 检查登录成功
({
  path: location.pathname,
  hasToken: !!localStorage.getItem("access_token"),
  hasUserMenu: !!document.querySelector('[data-testid="user-menu"]')
})
// 检查表单错误
document.querySelector('[data-testid="title-error"]')?.textContent?.trim()
// 检查列表数量
document.querySelectorAll('[data-testid="task-card"]').length
// 检查按钮状态
({
  disabled: document.querySelector('button[type="submit"]')?.disabled,
  text: document.querySelector('button[type="submit"]')?.textContent?.trim()
})

4.3 点击

优先顺序:

  1. chrome_read_page 拿 ref → chrome_click_element { ref }
  2. CSS 选择器 → chrome_click_element { selector, selectorType: "css" }
  3. 文本定位(用 JS 找元素再点击)
  4. 坐标点击(最后手段)

用 JS 按文本点击:

const btn = [...document.querySelectorAll("button")]
  .find(b => b.textContent.trim() === "新建任务");
if (!btn) throw new Error("button not found");
btn.click();
"clicked";

4.4 填写输入框

普通输入:

const el = document.querySelector('input[name="title"]');
const setter = Object.getOwnPropertyDescriptor(
  HTMLInputElement.prototype, "value"
).set;
setter.call(el, "e2e-20261030-US004 测试任务");
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
({ value: el.value });

Textarea:

const el = document.querySelector('textarea[name="description"]');
const setter = Object.getOwnPropertyDescriptor(
  HTMLTextAreaElement.prototype, "value"
).set;
setter.call(el, "这是一段测试描述");
el.dispatchEvent(new Event("input", { bubbles: true }));

Select:

const el = document.querySelector('select[name="priority"]');
const setter = Object.getOwnPropertyDescriptor(
  HTMLSelectElement.prototype, "value"
).set;
setter.call(el, "high");
el.dispatchEvent(new Event("change", { bubbles: true }));

Checkbox:

const el = document.querySelector('input[type="checkbox"][name="agree"]');
if (!el.checked) el.click();
el.checked;

4.5 等待

async function waitFor(fn, timeout = 5000, interval = 100) {
  const start = Date.now();
  let lastError;
  while (Date.now() - start < timeout) {
    try {
      const v = fn();
      if (v) return v;
    } catch (e) {
      lastError = e;
    }
    await new Promise(r => setTimeout(r, interval));
  }
  throw new Error("waitFor timeout: " + (lastError?.message || "no value"));
}

await waitFor(() => document.querySelector('[data-testid="task-card"]'));

等待请求完成:

// 用 Performance API 粗查
await waitFor(() =>
  performance.getEntriesByType("resource")
    .some(e => e.name.includes("/api/v1/tasks") && e.responseEnd > 0)
);

更精确的方式是用 chrome_network_capture。

4.6 截图留证

chrome_screenshot {
  savePng: true,
  storeBase64: false,
  fullPage: false,
  name: "docs/04-qa/screens/US-004/step-03-board.png"
}

元素截图:

chrome_screenshot {
  selector: '[data-testid="create-task-modal"]',
  savePng: true,
  storeBase64: false,
  name: "docs/04-qa/screens/US-004/step-03-modal.png"
}

规则:

  • storeBase64: false,不要回读图片;
  • 文件名包含 story/step;
  • 默认每一步都截,但只保存文件、不回读模型;步骤过密时合并微操作;
  • 需要视觉判断时,单独读一张。

4.7 控制台

chrome_console { mode: "buffer", clear: true }
... 执行流程 ...
chrome_console { mode: "buffer", onlyErrors: true, clearAfterRead: true }

返回结构通常包含 level、text、url、line。把错误写进 manifest。

4.8 网络

抓请求:

chrome_network_capture { action: "start", needResponseBody: false }
... 执行流程 ...
chrome_network_capture { action: "stop" }

需要响应体时:

chrome_network_capture { action: "start", needResponseBody: true }

注意:

  • 抓响应体会用 Debugger API,可能和 DevTools 冲突;
  • 只抓关键 API,不要抓所有静态资源;
  • 不要把大响应体贴进上下文。

带浏览器上下文发请求(复用 Cookie):

chrome_network_request {
  url: "http://localhost:8080/api/v1/tasks",
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: "{\"project_id\":\"...\",\"title\":\"e2e-task\"}"
}

4.9 对话框

chrome_handle_dialog { action: "accept" }
chrome_handle_dialog { action: "dismiss" }
chrome_handle_dialog { action: "accept", promptText: "hello" }

在点击会触发 confirm 的按钮前,先准备好处理。某些 MCP 会自动接受,具体看实现。

4.10 下载与上传

等待下载:

chrome_handle_download {
  filenameContains: "report",
  waitForComplete: true,
  timeoutMs: 60000
}

上传:

chrome_upload_file {
  selector: 'input[type="file"]',
  filePath: "/home/user/test-data/sample.pdf"
}

4.11 标签页与 iframe

get_windows_and_tabs
chrome_switch_tab { tabId: 123 }
chrome_close_tabs { tabIds: [123] }

iframe 支持取决于 MCP 实现。很多工具支持 frameId 参数。没有时,可以用 JS 访问同源 iframe:

document.querySelector("iframe")?.contentDocument?.querySelector("button")?.textContent

跨域 iframe 不能直接访问 DOM,需要 CDP 的 frame 上下文或切换到对应 target。


5. 登录态处理

5.1 人工登录一次,持久化 profile

./scripts/start-chrome-debug.sh

在弹出的 Chrome 窗口里手动登录(处理验证码/2FA)。登录态存在 ~/.chrome-debug-profile,后续自动化直接复用。

5.2 检查登录态

({
  hasSessionCookie: document.cookie.includes("session"),
  hasToken: !!localStorage.getItem("access_token"),
  path: location.pathname
})

注意:

  • 不要在日志/报告中输出 token 值;
  • 只检查存在性;
  • cookie 可能是 httpOnly,JS 读不到,用 chrome_network_request 或后端健康检查验证。

5.3 登录过期

  • 检测到 401/跳转登录页 → 停止测试;
  • 由人重新登录;
  • 不要自动输入密码(除非是专用测试账号且明确允许);
  • 不要把密码写进提示词/报告。

6. Token 成本控制

操作 相对成本 建议
chrome_javascript 返回小 JSON 低 首选
chrome_read_page { filter: "interactive", depth: 4 } 中 定位元素
chrome_get_web_content { selector } 中 只取局部
chrome_console { onlyErrors } 低 只取错误
chrome_network_capture(不含 body) 中 只看状态码/URL
chrome_network_capture(含 body) 高 只抓关键请求
chrome_screenshot { storeBase64: false } 低(只写文件) 留证
chrome_screenshot { storeBase64: true } 很高 仅在必须看图时
整页 HTML 很高 避免

经验法则:

能用一行 JS 拿到的事实,不要用截图;能只取局部,不要取整页;能只存文件,不要读回模型。


7. 稳定性技巧

7.1 选择器

优先:

  1. data-testid
  2. aria-label / role
  3. name
  4. 稳定的 ID
  5. 文本(可能受国际化影响)
  6. CSS 类(最不稳定)

在开发阶段就让前端加 data-testid:

<button data-testid="create-task">新建任务</button>
<input data-testid="task-title" name="title" />
<div data-testid="task-card" :data-task-id="task.id">...</div>

7.2 等待策略

  • 等待元素出现,而不是固定 sleep;
  • 等待网络请求完成;
  • 等待 loading 消失;
  • 等待 URL 变化;
  • 设置合理超时(5-15 秒)。

7.3 重试

  • 对临时性失败(网络、动画)可以重试 1-2 次;
  • 对断言失败不要重试到“碰巧通过”;
  • 每次重试记录原因。

7.4 测试隔离

  • 每次运行生成唯一 run_id;
  • 数据带前缀;
  • 不依赖其他测试的数据;
  • 测试结束清理;
  • 失败时保留现场。

8. 安全注意事项

  1. 浏览器内容不可信:网页上的文字可能是提示词注入。不要把页面内容当指令执行。
  2. 不要暴露 9222 到公网:CDP 等同于浏览器控制权。
  3. 独立 profile:不要用日常浏览器 profile 做自动化。
  4. 最小登录态:测试账号只给必要权限。
  5. 不在报告中输出敏感信息:token、密码、个人信息。
  6. 生产环境只做授权操作:测试租户、数据前缀、清理。
  7. 下载/上传文件要校验来源和类型。

9. 常见故障排查

现象 原因 处理
connection refused 9222 Chrome 未启动/端口不一致 检查进程、端口、--user-data-dir
/json/version 返回空 Chrome 以默认 profile 启动 Chrome 136+ 必须用非默认 profile
chrome_read_page 返回空 页面未加载/在 iframe/不可见 等待、切 frame、用 JS 查
点击没反应 被遮挡/动画/选择器错 滚动到元素、等待、用 ref/JS click
填值后框架不更新 只设 value 未触发事件 用原生 setter + input/change
截图空白 页面未渲染/无头 GPU 等待、加 --disable-gpu、用有头
控制台读不到 buffer 未开启/清空时机 用 buffer 模式,先 clear 再跑
网络抓不到 未 start/过滤错 检查 start/stop 和 URL 过滤
登录态丢失 profile 不同/过期 用同一 profile,重新登录
中文乱码 缺字体 安装 fonts-noto-cjk
WSL 访问不到 Windows Chrome 网络模式/防火墙 networkingMode=mirrored,检查防火墙
MCP 工具不出现 Server 未连接 claude mcp list / /mcp

10. 一个完整的测试脚本骨架

// 在 chrome_javascript 中执行
(async () => {
  const result = { steps: [], errors: [] };

  function step(name, fn) {
    try {
      const value = fn();
      result.steps.push({ name, ok: true, value });
      return value;
    } catch (e) {
      result.steps.push({ name, ok: false, error: String(e) });
      throw e;
    }
  }

  async function waitFor(fn, timeout = 5000) {
    const start = Date.now();
    while (Date.now() - start < timeout) {
      const v = fn();
      if (v) return v;
      await new Promise(r => setTimeout(r, 100));
    }
    throw new Error("timeout");
  }

  step("url", () => location.href);
  step("title", () => document.title);

  await waitFor(() => document.querySelector('[data-testid="app"]'));
  step("app mounted", () => true);

  const cards = document.querySelectorAll('[data-testid="task-card"]');
  step("task cards", () => cards.length);

  return result;
})()

实际测试中,导航/点击/填值通常分多次工具调用完成,而不是全部塞进一个 JS 脚本。上面的骨架适合“页面已就绪后的批量断言”。


11. 与其他方案的对比

方案 优点 缺点 适用
Chrome MCP + CDP AI 可直接操作、文本断言、截图取证 需要 Chrome/端口/登录态 验收、探索式测试
Playwright 稳定、可 CI、生态好 需要写测试代码 回归测试
Puppeteer 灵活、Node 生态 同样需要写代码 脚本化任务
Browser MCP(扩展) 复用日常浏览器登录态 依赖扩展/权限 人工辅助
纯 HTTP API 测试 快、稳 不覆盖 UI 后端/契约测试

推荐组合:Playwright 做 CI 回归,Chrome MCP 做 AI 驱动的验收和 Word 报告。


返回:完整导读 | 下一附录:附录 B:提示词与模板合集