
1. 为什么 AI Agent 需要一双能登录的手先说结论MCP 浏览器自动化服务本质是把打开网页、登录、点击、输入、截图、取数据这些动作封装成 MCP 工具让 AI Agent 像调用普通函数一样操作一个真实浏览器而且登录态能跨多次调用保持住。它适合谁适合那些已经用上 Claude Code、Cline、Codex 这类编码 Agent却发现它们只能读文档、调 API、写代码一碰到需要登录态的真实网页就抓瞎的开发者。我自己就卡在这个点上很久。Agent 能帮我查文档、生成脚本、跑单元测试但我要它去后台系统里核对一条订单状态、去电商页面比一下价格、去某个管理后台填一张表单它立刻无能为力。原因很直接普通 HTTP 请求拿不到 JS 渲染后的 DOM带不上登录 Cookie遇到人机验证更是直接歇菜。我试过几条路踩得挺惨。第一条是 browserless 的 REST API每次请求都是独立会话登录态根本传不过去人机验证也没法介入放弃。第二条是 browserless 的 CDP 直连导航之后 target 就被关掉了会话断得莫名其妙也放弃。最后走通的是本地 Chromium 长驻加 MCP 会话管理这条路浏览器进程一直活着MCP 每次调用都连到同一个实例上Cookie、登录态、DOM 全留在浏览器进程里人机验证则通过一个网页查看台让人工介入。这里有个关键决策我想强调不接任何第三方打码平台也不接代理池。一是数据隐私二是成本三是很多场景根本不该把登录凭证交给外部服务。人机验证走人工介入通道——需要扫码或滑块时人自己看着画面操作Agent 的浏览器同步执行画面永远一致。所以这篇文章不是讲怎么注册账号而是讲一套能真正跑起来的 MCP 浏览器自动化服务怎么设计、怎么配、怎么排障。技术栈是 Hermes Agent 加自建 MCP Server底层是无头 Chromium 加 Playwright 加 CDP。我会把可复制的配置、会话保持的代码、以及通过统一 Key 通道接入的验证动作都写清楚。你跟着做能拿到一个 Agent 可以带着登录态完成真实网页任务的服务。2. TaoToken 统一 Key 接入 MCP 服务的前置准备在动手写 MCP 服务之前先把模型侧的接入通道理顺。因为你的 Agent 要驱动浏览器它本身得先能稳定调用大模型而多模型、多 Key 来回切换是件很烦的事。我的做法是用 TaoToken 做统一 Key 通道一个 Key 走通模型对话和编码 Agent省得每个工具单独配一遍。先说清楚 TaoToken 是什么、能做什么。它是一个统一的模型 API 接入通道官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。你在这里拿到一个 Key就能在支持自定义 Base URL 的客户端里接入包括 Claude Code、Cline、Codex 这类编码 Agent也包括你自己写的 MCP 服务里调模型的部分。适合谁适合手上同时跑好几个 Agent 工具、不想为每个工具单独管理 Key 和额度的开发者。前置准备分三步。第一步去控制台创建 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 在 API Keys 页面新建一个 Key复制出来存好。这个 Key 后面要填进 MCP 服务的环境变量也会填进编码 Agent 的配置里。第二步确认你要用的模型 ID。不同客户端对模型名的写法略有差异但核心就是 Base URL 加 Key 加 Model ID 这三件套。你可以先在模型对话页面验证一下 Key 是否可用打开 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 选一个模型发一句话能正常返回就说明 Key 和额度都没问题。这一步别跳过很多人后面报 401 就是因为 Key 根本没生效。第三步如果你打算长期跑编码 Agent 或者让它驱动浏览器做多步任务建议看一下 Coding Plan。地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 。它的定位是给长期编码和 Agent 场景用的比按次调用更划算尤其是你要让 Agent 反复做打开页面、分析、点击、再分析这种多轮任务时。这里我要提醒一个常见误区MCP 浏览器自动化服务和模型接入是两件事但经常被混在一起。MCP 服务负责操作浏览器模型负责决定操作什么。你的 MCP 服务本身可以完全不调模型只暴露工具模型调用发生在 Agent 那一侧。所以 TaoToken 的 Key 主要配在 Agent 侧MCP 服务侧只在需要它自己调模型做页面理解时才用得上。把这两层分清楚后面排障会轻松很多。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 里面有各客户端的 Base URL 和配置示例配之前扫一眼能少走弯路。3. 可复制的 MCP 服务端配置与会话保持实现这一节是重点我直接把能跑的配置和代码贴出来。整个服务分三块Chromium 常驻预热、MCP 会话管理、网页查看台。先看目录结构我放在/opt/mcp-browser下/opt/mcp-browser ├── server.py # MCP Server 主入口 ├── viewer.py # 人工介入查看台 ├── requirements.txt ├── Dockerfile └── .env # 放 TAOTOKEN_API_KEY 等先配.env把统一 Key 和模型信息放进去。注意这里用的是 TaoToken 的 API 地址不带任何多余参数# .env TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODEL你的模型ID CDP_PORT9222 VIEWER_PORT9223然后是requirements.txt。这里有个大坑我后面排障会细讲mcp库版本必须锁死否则mcp.server.mcpserver这个模块根本不存在。mcp2.0.0 playwright1.47.0 fastapi0.115.0 uvicorn0.30.6 python-dotenv1.0.1接下来是server.py的核心部分。第一块是 Chromium 预热容器启动时就把浏览器拉起来避免首次调用 120 秒冷启动超时import asyncio import os import subprocess from dotenv import load_dotenv from mcp.server.mcpserver import MCPServer from playwright.async_api import async_playwright load_dotenv() CDP_PORT int(os.getenv(CDP_PORT, 9222)) CHROME /usr/bin/chromium def prewarm(): subprocess.Popen([ CHROME, --headlessnew, --no-sandbox, --disable-gpu, --disable-dev-shm-usage, f--remote-debugging-port{CDP_PORT}, about:blank, ]) server MCPServer(browser-automation) class BrowserSession: def __init__(self): self._pw None self._browser None self._ctx None self._page None async def start(self): if self._browser: self._page self._ctx.pages[0] return {reused: True} self._pw await async_playwright().start() self._browser await self._pw.chromium.connect_over_cdp( fhttp://127.0.0.1:{CDP_PORT} ) self._ctx self._browser.contexts[0] self._page self._ctx.pages[0] if self._ctx.pages else await self._ctx.new_page() await self._page.set_viewport_size({width: 1920, height: 1080}) return {reused: False} session BrowserSession()注意set_viewport_size这行不加的话截图只有 780x437这是connect_over_cdp默认 context 没有 viewport 导致的我踩过。第二块是 MCP 工具注册。工具名和功能对照如下你可以直接照抄工具功能browser_session_start启动或复用浏览器秒回browser_session_end关闭浏览器browser_goto导航到 URLbrowser_click点击支持 CSS 或按钮文字browser_type输入兼容 Vue/React 受控组件browser_eval执行 JS 取数据browser_wait等待毫秒browser_screenshot截图browser_viewer返回人工介入地址browser_content取页面文本或 PDF注册代码片段server.tool() async def browser_session_start(): return await session.start() server.tool() async def browser_goto(url: str): await session._page.goto(url, wait_untildomcontentloaded) return {url: session._page.url} server.tool() async def browser_type(selector: str, text: str): await session._page.fill(selector, text) return {ok: True} server.tool() async def browser_click(selector: str): await session._page.click(selector) return {ok: True} server.tool() async def browser_eval(js: str): result await session._page.evaluate(js) return {result: result} server.tool() async def browser_viewer(): return {url: fhttp://127.0.0.1:{os.getenv(VIEWER_PORT, 9223)}/}第三块是网页查看台viewer.py这是人工介入通道。它起一个 HTTP 服务把 Agent 浏览器的画面实时投影成网页并提供点击、拖拽、输入端点。这里有个最严重的坑查看台的操作不能各开一个独立的sync_playwright()连接去连 CDP否则会和 MCP 会话的 asyncio 连接抢端口全部挂起。正确做法是复用主事件循环import asyncio from fastapi import FastAPI from pydantic import BaseModel app FastAPI() main_loop None class ClickReq(BaseModel): x: int y: int app.post(/click) async def click(req: ClickReq): fut asyncio.run_coroutine_threadsafe( session._page.mouse.click(req.x, req.y), main_loop ) fut.result(timeout10) return {ok: True} app.get(/shot) async def shot(): data await session._page.screenshot(typejpeg, quality70) return {image: data.hex()}前端把鼠标事件换算成截图坐标公式是(clientX - rect.left) * naturalWidth / rect.widthPOST 回后端后端用 Playwright 的mouse.click在真实浏览器里执行。这样画面永远和 Agent 操作同步。最后是Dockerfile几个细节必须注意FROM hermes-web-ui:latest USER root WORKDIR /opt/mcp-browser COPY . . RUN uv pip install --python /usr/bin/python3 -r requirements.txt RUN playwright install chromium --no-shell ENTRYPOINT [python3, server.py]基础镜像的 entrypoint 是 node会覆盖 CMD所以必须显式写ENTRYPOINT。构建时如果 DNS 解析失败加--networkhost。chromium 一定要playwright install chromium --no-shell固化进镜像用docker exec临时装的重启就没了。4. 验证请求与一次完整的 Agent 驱动浏览器任务配置写完先做最小验证确认 MCP 服务和统一 Key 通道都通。启动容器docker build --networkhost -t mcp-browser:latest . docker run -d --name mcp-browser \ --networkhost \ --env-file .env \ mcp-browser:latest然后验证查看台是否秒回。这一步很关键如果这里卡住说明 CDP 连接冲突了curl -m 5 http://127.0.0.1:9223/shot正常应该 0.1 秒左右返回。如果超时去看docker logs mcp-browser大概率是BrokenPipe回到上一节的run_coroutine_threadsafe修复。接着验证 MCP 工具链路。我用一个简单的 Agent 侧调用序列来演示假设你的 Agent 已经通过 TaoToken 的 Base URL 和 Key 接好了模型1. browser_session_start() # 复用浏览器秒回 2. browser_goto(https://example.com) 3. browser_eval(document.title) # 返回页面标题 4. browser_screenshot() # 拿到截图 5. browser_viewer() # 需要人工时返回查看台地址如果第 3 步能返回标题说明 MCP 服务和浏览器会话都正常。如果返回 401 或模型侧报错那是 Agent 侧的 Key 没配对去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 重新确认 Key再对照接入文档检查 Base URL 写法。现在做一次完整的端到端任务让 Agent 驱动浏览器完成打开一个需要登录的页面、保持登录态、取回数据。我用一个通用的后台系统场景不涉及任何具体平台。流程如下Agent: browser_session_start() Agent: browser_goto(https://your-app.example.com/login) Agent: browser_type(#username, your_user) Agent: browser_type(#password, your_pass) Agent: browser_click(button[typesubmit]) Agent: browser_wait(4000) Agent: browser_eval(document.cookie) # 验证登录态 Agent: browser_goto(https://your-app.example.com/dashboard) Agent: browser_eval(document.querySelector(.order-count).innerText)关键在最后两步登录之后导航到另一个页面登录态依然保持因为 Cookie 留在同一个 Chromium 进程里。这就是能登录的手的核心价值。如果中途遇到人机验证Agent 调用browser_viewer()把查看台地址发给你你打开网页看着画面操作Agent 的浏览器同步执行验证完继续。验证成功的标志有三个browser_eval返回的 Cookie 非空、dashboard 页面能取到数据、查看台截图和 Agent 操作一致。三个都满足说明整条链路通了。5. 本篇常见错误排查对照这一节我按真实报错来写你遇到问题直接对号入座。401 Unauthorized。这个最常见出现在 Agent 侧调模型时。原因通常是 Key 没填对、Base URL 写错、或者 Key 额度用完了。排查顺序先去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 确认 Key 存在且未过期再检查配置里的 Base URL 是不是https://taotoken.net/api最后去模型对话页面发一句话验证 Key 本身可用。如果模型对话能用但 Agent 不能用那就是 Agent 的配置文件路径写错了。local proxy failed。这个报错通常出现在客户端尝试走本地代理但代理没起来时。检查你的客户端配置里有没有多余的代理设置把它去掉直接用 Base URL 接入。MCP 服务本身不需要任何代理。reading choices 相关报错。这类报错一般出现在模型返回格式和客户端预期不一致时比如客户端期望 OpenAI 格式但返回了别的结构。确认你选的模型 ID 和客户端类型匹配接入文档里有对照表。OAuth 相关报错。如果你用的是 Claude Code 这类带 OAuth 流程的客户端报 OAuth 错误通常是认证方式选错了。改用 API Key 方式接入把 Base URL、Key、Model ID 三件套填全。这三件套缺一不可尤其是 Model ID很多人只填了前两个。CDP 连接冲突导致查看台点不动。现象是查看台能打开但截图不刷新、点击无反应、请求永久卡死。根因是查看台各端点开了独立sync_playwright()连接去抢 CDP 端口。修复就是上一节的run_coroutine_threadsafe所有操作复用主 asyncio loop。修复后/shot0.1 秒、/click0.03 秒。截图尺寸不对。connect_over_cdp默认 context 没有 viewport截图只有 780x437。加一行page.set_viewport_size({width: 1920, height: 1080})解决。mcp 库 import 报错。基础镜像自带mcp 1.26.0没有mcp.server.mcpserver装最新版又可能 import 报错。锁mcp2.0.0才正常。Docker 构建 DNS 失败。加--networkhost重新构建。Chrome 忽略远程调试地址参数。--remote-debugging-address0.0.0.0会被忽略chromium 只绑127.0.0.1。所以不能用 TCP 转发器转发 9222改成viewer.py在进程内直接起 HTTP 服务读 CDP。无头浏览器详情页参数折叠。部分页面的参数表不渲染用已选配置加标题组合拿核心数据别硬等。页面软限流。访问太频繁时页面可能只剩历史热词。对策是页面加载等 6 到 8 秒切换间隔 20 到 30 秒触发后冷却 60 到 90 秒。6. 把浏览器能力接进你的 Agent 工作流服务跑通之后真正要思考的是怎么把它接进日常工作流。我的经验是分两类场景一类是排障和接入调试一类是长期跑的多步任务。排障和接入调试阶段你需要频繁验证 Key、Base URL、模型 ID 是否正确这时候用 API Keys 页面加接入文档就够了。API Keys 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 。每次改完配置先去模型对话页面发一句话确认通道通再启动 MCP 服务能省掉一半的排查时间。长期跑编码 Agent 或者让 Agent 驱动浏览器做多步任务时建议走 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_browser_automationutm_campaignrewrite 。因为浏览器自动化任务往往是打开页面、分析、点击、再分析的多轮循环按次调用成本会上去长期方案更稳。最后说几个我踩过的实用技巧。第一会话持久化是下一步要做的现在容器重启登录态就没了可以把 Chromium 的 user-data-dir 挂到宿主机卷上。第二多浏览器实例可以用不同的 CDP 端口区分MCP 工具加一个 instance 参数。第三人机验证的人工介入通道不要做成自动打码数据隐私和合规风险都高人工介入反而是最稳的。第四Agent 驱动浏览器时每一步操作后都截图存档出问题能回溯。这套方案的价值不在于某个具体平台而在于把能操作浏览器变成了 MCP 工具Agent 可以带着登录态完成真实网页任务人机验证通过网页查看台人工介入全程不依赖第三方平台。你把这套配置跑起来再按自己的场景改工具集就能给 Agent 装上一双真正能干活的手。