自建 MCP 浏览器自动化服务:给 AI Agent 装上“能登录的手“
自建 MCP 浏览器自动化服务:给 AI Agent 装上"能登录的手"
本文记录我自己搭建的一套 MCP 浏览器自动化服务:把"能操作浏览器"(打开网页、登录、点击、输入、过人机验证)做成 MCP 工具,让 AI Agent 像调用普通函数一样操作真实浏览器,登录态跨调用保持。
技术栈:Hermes Agent + conhub MCP Server + 无头 Chromium + Playwright + CDP。京东扫码登录 + 选电脑只作为端到端验证案例——真正的重点是 MCP 服务本身怎么设计、怎么踩坑、怎么修。
一、为什么需要"浏览器 MCP"
AI Agent 能读文档、调 API、写代码,但有一类事做不了:需要登录态的真实网页操作——比如查电商价格、处理后台、填表单。普通 HTTP 请求做不到(要 JS 渲染、要登录 Cookie、要人机验证)。
方案对比:
| 方案 | 登录态保持 | 人机验证 | 结论 |
|---|---|---|---|
| browserless REST API | ❌ 每次独立 | ❌ 无法介入 | 放弃 |
| browserless CDP 直连 | ❌ 导航后 target 被关 | ❌ | 放弃 |
| 本地 Chromium 长驻 + MCP 会话管理 | ✅ | ✅ 人工介入通道 | 采用 |
关键决策:不接任何第三方打码平台/代理池(数据隐私 + 成本),人机验证走"人工介入通道"——需要扫码/滑块时,人在网页上看着画面操作,Agent 的浏览器同步执行。
二、架构设计
┌─────────────────────────────────────────────┐
│ conhub-mcp 容器 (:9100) │
│ │
│ MCP Server (Python, mcp==2.0.0) │
│ ├── browser_session_start/end 会话管理 │
│ ├── browser_goto/click/type/eval 操作 │
│ ├── browser_screenshot/content/pdf 取数 │
│ └── browser_viewer() → 人工介入地址 │
│ │ │
│ ▼ CDP (127.0.0.1:9222) │
│ Chromium 无头浏览器(容器启动即预热) │
│ │ │
│ viewer.py (:9223) ←── 实时投影 + 操作 │
│ (人工介入:扫码/滑块/点选/输入) │
└─────────────────────────────────────────────┘
三个关键设计:
1. Chromium 常驻 + 预热(消除冷启动)
容器启动时立即拉起 chromium(subprocess.Popen),MCP 调用时复用而不是现拉——首次调用不再有 120 秒冷启动超时:
# 容器启动预启动 chromium
def prewarm():
subprocess.Popen([
chrome, "--headless=new", "--no-sandbox",
"--disable-gpu", "--disable-dev-shm-usage",
f"--remote-debugging-port={9222}", "about:blank"
])
2. 会话管理(登录态跨调用保持)
MCP 每次调用都连接同一个 chromium 实例(connect_over_cdp),Cookie、登录态、DOM 全部保留在浏览器进程里:
async def session_start(self):
if self._browser: # 已连接 → 复用
self._page = self._ctx.pages[0]
return {"reused": True}
self._browser = await self._pw.chromium.connect_over_cdp(
f"http://127.0.0.1:{9222}")
3. 网页查看台(人工介入通道)
viewer.py 起一个 HTTP 服务(:9223),把 Agent 浏览器的画面实时投影成网页,并提供操作端点:
| 端点 | 功能 | 用途 |
|---|---|---|
GET /shot |
实时截图(1920x1080) | 看 Agent 在干嘛 |
POST /click {x,y} |
坐标点击 | 点选图片验证 |
POST /drag {x1,y1,x2,y2} |
鼠标拖拽 | 过滑块验证 |
POST /type {text} |
键盘输入 | 填验证码 |

前端把鼠标事件换算成截图坐标((clientX-rect.left) × naturalWidth/rect.width)POST 回后端,后端用 Playwright 的 mouse.click/drag/type 在真实浏览器里执行——画面永远和 Agent 操作同步。
三、10 个 MCP 工具
| 工具 | 功能 |
|---|---|
browser_session_start() |
启动/复用浏览器(秒回) |
browser_session_end() |
关闭浏览器 |
browser_goto(url) |
导航 |
browser_click(selector) |
点击(CSS 或按钮文字) |
browser_type(selector, text) |
输入(Vue/React 兼容) |
browser_eval(js) |
执行 JS 取数据 |
browser_wait(ms) |
等待 |
browser_screenshot() |
截图 |
browser_viewer() |
返回人工介入地址 |
browser_content/pdf/function/connect |
取文本/PDF/自定义脚本 |
Agent 侧用法(一次登录态会话):
1. browser_session_start() # 复用浏览器
2. browser_goto(url)
3. browser_type(selector, text) # 填表单
4. browser_click('Log In')
5. browser_wait(4000)
6. browser_eval(js) # 验证登录态 / 取数据
7. browser_viewer() # 需人工时给用户地址
四、搭建踩坑记录(重点)
坑 1:查看台"点不动"——CDP 连接冲突死锁 ⚠️ 最严重
现象:查看台能打开,但截图永远不刷新、点击毫无反应,请求永久卡死。
排查:curl -m 5 :9223/shot 超时;docker logs 看到 BrokenPipe。
根因:查看台的 /shot /click /drag /type 各开了一个独立 sync_playwright() 连接 CDP——与 MCP 会话(asyncio)的 CDP 连接互相抢端口,全部挂起。
修复:查看台操作全部复用主 asyncio loop,用 run_coroutine_threadsafe 提交到 MCP 的同一个事件循环:
def click_at(self, x, y):
fut = asyncio.run_coroutine_threadsafe(self._click_at_async(x, y), self._loop)
return fut.result(timeout=10) # 复用主连接,零冲突
修复后:/shot 0.1s、/click 0.03s 秒回。
坑 2:截图太小(780x437)
connect_over_cdp 的默认 context 没有 viewport,截图只有 780x437。修复:page.set_viewport_size({"width": 1920, "height": 1080})。
坑 3:mcp 库版本地狱
- 基础镜像自带 mcp 1.26.0 → 没有
mcp.server.mcpserver(session 管理) - 最新版 → import 报错
- 锁定
mcp==2.0.0才正常
坑 4:Dockerfile 构建细节
- 基础镜像 entrypoint 是 node(hermes-web-ui)会覆盖 CMD → 必须
--entrypoint python3 server.py - 构建时 DNS 解析失败 →
docker build --network=host - 镜像里没 pip → 用
uv pip install --python <venv> - chromium 要
playwright install chromium --no-shell固化进镜像(docker exec 装的重启就没了)
坑 5:Chrome 忽略远程调试地址参数
--remote-debugging-address=0.0.0.0 被忽略,chromium 只绑 127.0.0.1——所以不能用 TCP 转发器转发 9222,改成 viewer.py 在进程内直接起 HTTP 服务(:9223)读取 CDP。
五、端到端验证:京东扫码登录 + 选电脑
服务搭好后,用"京东买电脑"做了端到端验证。二维码登录只演示"人工介入通道"(正常的扫码/验证码场景),验证完即放弃该登录态。
流程
Agent: browser_goto('https://passport.jd.com/new/login.aspx')
Agent: 发现二维码 → browser_viewer() → 把查看台地址发给我
用户: 打开查看台 → 手机京东 App 扫码
Agent: browser_goto('www.jd.com') → 确认登录态保持
Agent: 搜索 → 解析 → 点详情页核实配置 → 比价
京东数据解析(新版 React DOM)
京东搜索页老选择器(#J_goodsList)全失效,实测有效方案:
- 商品卡 DOM:
[data-sku]元素 - 价格:卡片 innerText 用正则
¥\s*(\d{3,5}(?:\.\d{1,2})?)(价格是多行文本,先replace(/\s+/g,' ')) - 懒加载:滚动到底触发,等 3-4 秒
一条 browser_eval 抓整页商品:
(() => {
const items = Array.from(document.querySelectorAll('[data-sku]'));
const out = items.map(li => {
const t = li.innerText.replace(/\s+/g, ' ').trim();
const m = t.match(/¥\s*(\d{3,5}(?:\.\d{1,2})?)/);
const price = m ? parseFloat(m[1]) : 0;
const name = t.split('¥')[0].replace(/^广告\s*/, '').trim().slice(0, 75);
return {price, name};
}).filter(x => x.price > 0);
return JSON.stringify({count: out.length, items: out});
})()

防山寨(京东特有的坑)
京东搜"笔记本"前排混着大量山寨贴牌(“i9级”=洋垃圾、普惠之家/戴睿=杂牌)。过滤关键词:普惠 | i9级 | 二手 | 壹号本 | 戴睿 | Thinkpaid。区分三类店:京东自营 > 品牌官方授权店 > 山寨贴牌(绝不买)。
点详情页逐项核实(不按标题猜)
买电脑不能只看标题。逐个点进详情页,点「规格参数」读真实配置(CPU/显卡/内存/硬盘/屏幕/电池/到手价):

教训:第一次只看搜索结果标题就下结论被质疑——从此每个候选 SKU 都点详情页核实,拿不到的参数如实标"未确认",绝不编。
验证结果(5000 元编程笔记本)
| 机型 | 核实配置 | 到手价 | 店铺 |
|---|---|---|---|
| 机械革命无界14X/SE 斗战版 | Ultra5 226V / 16G / 512G / 2.8K高色域 | ¥4249 | 机械革命旗舰店 |
| 红米 REDMI Book 16 焕新版 | 13代i5标压 / 16G / 1TB / 16" | ¥3909 | 京东自营 |
| 惠普星Book 15 | R7-7730U / 16G / 512G / 1080P 45%NTSC | ¥4248 | 京东自营 |
| 联想小新Pro14 | i5-12450H / 16G / 1TB / 1080P | ¥4154 | 京东电竞官方旗舰店 |
| ThinkBook 14+ 锐龙版 | R7-8745HS / 16G / 512G | ¥5399 | 旗鹭专卖店(第三方) |
结论:5000 内编程性价比最高是机械革命无界14X 斗战版(2.8K 高色域 + 新酷睿 Ultra);预算紧选红米 Book 16 自营(1TB)。
六、京东访问的附加坑
- 移动端 ≠ PC 登录态:
item.m.jd.com是独立登录态,PC 登录了移动端仍显示"请登录"——不是限流 - 京东风控:搜索太频繁软限流(页面只剩历史热词)。对策:页面加载等 6-8s、切换间隔 20-30s、触发后冷却 60-90s
- 无头浏览器详情页参数折叠:部分 SKU 参数表不渲染,用「已选配置」+ 标题组合拿核心数据
七、总结
这套方案的真正价值:把"能操作浏览器"变成了 MCP 工具,Agent 可以带着登录态完成真实网页任务,人机验证通过网页查看台人工介入,全程不依赖第三方平台。验证案例(京东)证明链路完整可用。
后续方向:会话持久化(容器重启后登录态保留)、多浏览器实例、更细的验证码识别能力。
本文所有工具均为自建环境实测(Hermes Agent + conhub MCP + 无头 Chromium)。
更多推荐

所有评论(0)