跳到主要内容

浏览器接入与上下文:launch/connect、McpContext、McpPage、多页与路径沙箱

30 秒导读: 上一章讲了「一个工具请求怎么被路由和执行」。但工具执行时手里那个 context 是从哪来的? 这一章讲地基三件事:①浏览器怎么起来(自己 launch 一个,还是 connect 到你已经开着的 Chrome); ②每个标签页怎么被建模成一个带稳定 idMcpPage、多个页面怎么被选中/切换; ③agent 想把截图/trace 写到磁盘时,文件路径怎么被限制在允许的目录里。

本章覆盖 src/index.ts(上下文装配)、src/browser.ts(浏览器进程)、src/McpContext.ts(页面清单 + 路径沙箱)、 src/McpPage.ts(单页封装的生命周期部分),以及入口 src/bin/chrome-devtools-mcp-main.ts(启动与关停)。 不讲文本快照与 uid 定位(见 03)、不讲自动等待细节(见 04)、 响应组装与采集器只在必要处提一句(见 05)。


1. 先建直觉:这一层在解决什么

一个 MCP 工具(比如 navigate_pagetake_screenshot)最终都要落到「某个 Chrome 标签页」上。可要落上去,得先回答三个很实在的问题。

  • 浏览器从哪来? 是这个 MCP 进程自己拉起一个 Chrome,还是连到你桌面上已经开着、正登录着的那个 Chrome?
  • "页面"是什么? Puppeteer 给的是 Page 对象(会随导航、随重连而变),但 agent 需要一个稳定的、可以用数字 id 指名道姓的东西:"操作 3 号页面"。
  • 写文件写到哪? 截图、trace、堆快照都要落盘。一个能操控浏览器的 MCP 如果还能往任意路径写文件,是危险的。得有个沙箱。

这一层就是这三个问题的答案。用一个类比:

McpContext 当成这次会话的"浏览器桌面管理器"——它握着唯一的浏览器句柄,维护一张"当前有哪些页、哪个是选中页"的清单,并且守着"文件只能写进这几个目录"的门。 把 McpPage 当成贴在每个标签页上的一张便利贴——写着它的稳定编号、它的对话框状态、它的仿真设置(视口/网络/CPU 节流)。


2. 顶层全景:从进程启动到一个可用的 context

先看一张"怎么读":从上到下是时间顺序——进程起来 → 第一次工具调用时才真正拉起浏览器(懒加载)→ 建好 context → 后续调用复用它。

bin/chrome-devtools-mcp-main.ts (进程入口)
│ parseArguments(VERSION) 解析 CLI flags
│ checkForUpdates(...) 后台查有没有新版本
│ createMcpServer(args) 装配 server + 注册工具
│ 连上 StdioServerTransport,挂 stdin EOF / SIGTERM 关停钩子

index.ts :: getContext() ← 每个工具调用都走这里(懒加载)

├─ 分叉①:要连别人的 Chrome?(browserUrl / wsEndpoint / autoConnect)
│ └─ browser.ts :: ensureBrowserConnected() → puppeteer.connect()

└─ 分叉②:否则自己起一个
└─ browser.ts :: ensureBrowserLaunched() → puppeteer.launch()


仅当 browser 变了,才重建 McpContext.from(browser, ...)
│ #init(): createPagesSnapshot() 把每个 Page 包成 McpPage
│ 订阅 targetcreated / targetdestroyed 保持清单同步

context → 交给 ToolHandler,工具在它上面操作页面 / 写文件

部件职责一句话:

部件干什么在哪
进程入口解析参数、装配 server、挂关停钩子src/bin/chrome-devtools-mcp-main.ts
getContext()懒加载浏览器 + 决定 launch/connect + 按需重建 contextsrc/index.ts:100
ensureBrowserLaunched / ensureBrowserConnected真正启动或连接 Chrome,维护进程级单例src/browser.ts:277 / :47
McpContext页面清单、选中页、路径沙箱、资源加载src/McpContext.ts:69
McpPage单个标签页的稳定 id、对话框、仿真设置src/McpPage.ts:61

3. 浏览器怎么起:launch vs connect 的分叉

3.1 分叉点在 getContext()

装配阶段并不启动浏览器。真正决定"launch 还是 connect"的,是每次工具调用都会走的 getContext()(src/index.ts:100 getContext)。判据只有一句话:

只要给了 browserUrlwsEndpointautoConnect 三者之一,就走 connect(连别人的 Chrome);否则走 launch(自己起一个)。

真实源码(src/index.ts:116-147):

const browser =
serverArgs.browserUrl || serverArgs.wsEndpoint || serverArgs.autoConnect
? await ensureBrowserConnected({ browserURL, wsEndpoint, wsHeaders,
channel: serverArgs.autoConnect ? channel : undefined, // 见下方说明
userDataDir, devtools, blocklist, allowlist })
: await ensureBrowserLaunched({ headless, executablePath, channel,
isolated, userDataDir, viewport, chromeArgs, ... blocklist, allowlist });

注意那行注释挑明的坑:channel 只在 autoConnect 为真时才传给 connect。因为 connect 分支里 channel/userDataDir 会触发"自动发现本地 Chrome"的逻辑(见 §3.3),普通的 browserUrl/wsEndpoint 连接不该被 channel 干扰。

proxyServerblocklist(--blockedUrlPattern)、allowlist(--allowedUrlPattern)在这里被整理成参数:proxy 拼成 --proxy-server=... 塞进 Chrome 启动参数(src/index.ts:105-107),而 block/allow 两张 URL 名单同时下发给 Puppeteer 的连接/启动选项 context(用于资源加载校验,见 §6.3)。

3.2 进程级单例:一个 MCP 进程只有一个浏览器

src/browser.ts 顶部有两个模块级变量(src/browser.ts:21-22):

let browser: Browser | undefined;
let browserMode: 'launched' | 'connected' | undefined;

ensureBrowserLaunched / ensureBrowserConnected 进来第一件事都是 if (browser?.connected) return browser;——已经有活的浏览器就直接复用。这就是"懒加载 + 单例":第一次工具调用才真的起浏览器,之后所有调用共享它。

browserMode 记录"这是我起的还是我连的",关停时要据此决定 close 还是 disconnect(见 §7)。代码特意先赋值 browserMode 再赋值 browser(src/browser.ts:131-133:284-286),并留了注释:否则并发的 closeBrowser() 可能看到 browser 已设、browserMode 还是 undefined,走错分支把"连来的"当"起的"给关了。

3.3 connect 的三种来源与 DevToolsActivePort 自动连

ensureBrowserConnected(src/browser.ts:47)按优先级选连接方式:

给了什么怎么连
wsEndpoint直接用作 browserWSEndpoint,可带 wsHeaders 自定义头
browserURL用 HTTP 端点(如 http://127.0.0.1:9222)让 Puppeteer 去问 ws 地址
channeluserDataDir(即 --autoConnect)从用户数据目录里读端口自己拼 ws 地址(见下)

最巧的是第三种。当给了 userDataDir 时,它去读该目录下的 DevToolsActivePort 文件(src/browser.ts:84 portPath)——这是 Chrome 开着远程调试时写下的一个两行文件:第一行端口、第二行 ws 路径。代码把它读出来、校验端口在 1–65535、拼成 ws://127.0.0.1:<port><path>(src/browser.ts:86-103):

const [rawPort, rawPath] = fileContent.split('\n').map(l => l.trim()).filter(Boolean);
const port = parseInt(rawPort, 10);
if (isNaN(port) || port <= 0 || port > 65535) throw new Error(...);
connectOptions.browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;

读不到就抛出一条带指引的错误:让用户去 chrome://inspect/#remote-debugging 打开远程调试。这样"连到我正开着、已登录的 Chrome"不需要用户手抄 ws 地址。

若既没 userDataDir 也没显式端点、只给了 channel,则把 stable→chrome、其余 →chrome-<channel> 交给 Puppeteer 自己找已安装的对应版本(src/browser.ts:116-118)。

3.4 launch 的关键选项

自己起浏览器走 launch()(src/browser.ts:185)。几个要点:

  • userDataDir / profile。 --isolated 时用临时目录、关闭后自动清理;否则落在 ~/.cache/chrome-devtools-mcp[-cli]/chrome-profile[-<channel>],让你的登录态在多次会话间保留(src/browser.ts:187-203)。同一个 userDataDir 只能被一个 Chrome 占用,冲突时报错并提示用 --isolated(src/browser.ts:262-268)。
  • headless。 无头模式追加 --screen-info={3840x2160} 给个大屏(src/browser.ts:212-214);有头模式才去 detectDisplay() 在 Linux 上补 DISPLAY(src/browser.ts:226-228)。
  • pipe: true。 用管道而非 WebSocket 与 Chrome 通信(src/browser.ts:237),更稳、少一个开放端口。
  • targetFilter。 launch 和 connect 都装了 makeTargetFilter(见 §4.1),把 chrome://、扩展页等无关目标过滤掉。

4. 页面怎么被建模:McpContext 的清单

浏览器有了,接下来把"标签页"变成 agent 能用的东西。这是 McpContext 的核心职责。

4.1 哪些 target 才算"页面":makeTargetFilter

Chrome 里有一大堆 target:普通网页、chrome:// 内部页、DevTools、扩展页、service worker……不是每个都该暴露给 agent。makeTargetFilter(src/browser.ts:24)定了规则:

  • chrome://newtab/chrome://inspect... 保留(可能是唯一开着的页)。
  • 其余 chrome://chrome-untrusted:// 丢弃;chrome-extension:// 默认也丢,除非开了 --categoryExtensions
  • 其它一律保留。

4.2 从 PageMcpPage:进程级 nextPageId

McpContext 用一个 Map<Page, McpPage>(src/McpContext.ts:80 #mcpPages)维护清单。包装发生在 #createMcpPage(src/McpContext.ts:474):同一个 Puppeteer Page 只包一次,新页就 new McpPage(page, nextPageId++, ...)

关键设计:nextPageId模块级、进程全局的计数器(src/McpContext.ts:67),不是每个 context 从 1 开始。注释道破用意:

Page id 从进程级计数器发放,好让它们跨所有 context(尤其是跨浏览器重连)保持唯一。重连前发出的 id 之后会"解析失败",而不是命中重连后浏览器里一个毫不相关的页。

也就是说:如果你握着"3 号页",浏览器断了又重连、页面清单全换了,你再用"3 号"不会误伤新浏览器里的某个页——它会干净地报"找不到",而不是安静地指向错的页。这是 agent 安全的重要一环。

4.3 清单怎么保持同步:快照 + 事件

清单有两种更新途径:

  • 主动快照 createPagesSnapshot()(src/McpContext.ts:490):调 #fetchBrowserPages() 拉当前所有页,给新页补 McpPage,并剪掉已经不存在的旧条目(dispose() 后从 map 删除)。new_page 等操作后都会重拍一次。
  • 被动事件:#init() 里订阅了 targetcreated / targetdestroyed(src/McpContext.ts:133-134)。新 target 出现就异步包成 McpPage(#onTargetCreated),target 销毁就找到对应 McpPage 释放掉(#onTargetDestroyed)。

#fetchBrowserPages()(src/McpContext.ts:527)还处理两类特殊页:默认过滤掉 devtools://(除非开了 DevTools 调试),并额外把 chrome-extension:// 类型为 page 的扩展页捞进来。

4.4 选中页与自动兜底

同一时刻只有一个"选中页",大多数 definePageTool 工具作用在它上面(#selectedPage,src/McpContext.ts:81)。

  • selectPage(page)(src/McpContext.ts:398):切换选中页,并调用该页的 updateTimeouts() 应用它自己的超时设置。
  • getSelectedMcpPage()(src/McpContext.ts:362):取选中页;若它已关闭,抛出"selected page has been closed,去 list_pages 看看"。
  • 自动兜底:每次 createPagesSnapshot() 末尾,如果选中页没了(!selectedPage || pptrPage.isClosed()),就自动选中清单里的第一个页(src/McpContext.ts:509-522)。这里有个精心的判据——只在选中页真的 isClosed() 时才兜底,而不是"它这一拍恰好没出现在快照里"就换,避免把一个活着、只是暂时没被列出的页悄悄换掉。发生了兜底就把 #selectedPageFallback 记下来,让下一条响应能提示 agent(getSelectedPageFallback()src/McpResponse.ts:1030 消费,细节属 05)。

4.5 新开页 newPage 与隔离上下文

newPage(background, isolatedContextName)(src/McpContext.ts:300)开新标签。亮点是 isolatedContextName:

不给名字 → 在默认 BrowserContext 里开页(共享 cookie/存储)。 给了名字 → 在一个以该名字为键的独立 BrowserContext 里开页;同名复用,不同名彼此完全隔离(不共享 cookie/存储)。

映射维护在 #isolatedContexts: Map<string, BrowserContext>(src/McpContext.ts:74)。这让 agent 能"用 A 账号开一组页、用 B 账号开另一组页"而互不串味——多智能体/多账号场景的基础。工具层的开关是 new_pageisolatedContext 参数(src/tools/pages.ts:115)。

dispose()故意不关闭这些隔离上下文(src/McpContext.ts:146-149):要么整个浏览器会被关掉,要么是 disconnect——不该在拆 context 时破坏浏览器状态。


5. context 什么时候重建:reconnect 与稳定性

回到 getContext()(src/index.ts:149-161)。它每次都算出 browser,但只在 context?.browser !== browser 时才重建 McpContext:

if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
...,
reconnected: context !== undefined, // 之前有过 context,说明这是重连
});
await updateRoots();
}
return context;

含义:

  • 同一个浏览器句柄 → 复用同一个 context,页面清单、选中页、id 都延续。
  • 浏览器换了(断线重连、连到另一实例) → 重建 context。此时 reconnected: context !== undefined 为真,McpContext 会把 #reconnectNotice 置位。它通过 consumeReconnectNotice()(src/McpContext.ts:380,一次性、读后即清)被 src/ToolHandler.ts:222 取用,在下一条响应里给 agent 一句"刚重连过"的提示。

配合 §4.2 的进程级 nextPageId:重连后 id 从上次的值继续往上发,旧 id 不会复用,所以重连是"page id 稳定/不误伤"的——旧 id 要么还指向同一逻辑页(若句柄没变),要么干净失败。


6. 路径沙箱:文件写入怎么受限

agent 会让 MCP 把截图、trace、堆快照写盘。McpContextMCP roots + realpath 两道闸把可写范围锁住。

6.1 roots:允许的目录清单

MCP 协议里客户端可以声明 "workspace roots"(允许操作的目录)。装配时,若客户端协商了 roots 能力,updateRoots() 会向客户端请求 roots/list 并存进 context(src/index.ts:61-74setRoots)。

roots()(src/McpContext.ts:202)返回"客户端声明的 roots + 永远追加一个 OS 临时目录":

roots(): Root[] {
return [ ...(this.#roots ?? []),
{ uri: pathToFileURL(os.tmpdir()).href, name: 'temp' } ];
}

关键行为:如果客户端没有协商 roots 能力,getContext 装配时会打印一条警告(src/index.ts:89-96),而可写范围就收缩到只剩临时目录——这正是本章要点里说的"roots 未协商时限制到 temp"。想恢复旧的"随便写"行为,得显式加 --allow-unrestricted-paths

6.2 validatePath:realpath 逐 root 比对

真正把门的是 validatePath(filePath)(src/McpContext.ts:216)。逻辑分层:

  1. 路径为空 → 放行(没有要写的东西)。
  2. 客户端从没协商 roots 开了 --allow-unrestricted-paths → 跳过校验(#allowUnrestrictedPaths,恢复旧行为)。
  3. 否则必须校验——注意注释强调:哪怕没有配置任何 workspace root,也不能因此跳过,因为 roots() 至少还有 temp 目录兜底(src/McpContext.ts:226-230)。

校验方式是用 realpath 把符号链接全解开再比前缀(src/McpContext.ts:232-284):

  • 把目标路径解析成规范路径 resolveCanonicalPath(filePath)(src/utils/files.ts:18)——它 fs.realpath 目标;若文件还不存在(ENOENT),就向上找到最近一个存在的祖先目录去 realpath,再把缺失段拼回来。这样"要写一个尚不存在的新文件"也能被规范化。
  • 把每个 root 也 fs.realpath 成规范路径(Promise.allSettled,解析不了的 root 跳过并告警)。
  • 只有当 canonicalPath === canonicalRootcanonicalPath.startsWith(canonicalRoot + path.sep) 才算命中,任一 root 命中即放行;都不命中就抛"Access denied: path … is not within any of the configured workspace roots"。

为什么用 realpath 而不是字符串前缀比对? 防"符号链接逃逸":/tmp/evil → /etc 这类软链,如果只按字符串比,/tmp/evil/passwd 看着像在 temp 里,realpath 之后暴露出真身在 /etc,就会被拦下。

6.3 三个入口都过这道闸

  • saveFile(data, clientPath, extension)(src/McpContext.ts:589):先 ensureExtension 把扩展名规范化(内部就调了 validatePath,src/McpContext.ts:296),再建目录、写文件。给用户指定路径的产物(如导出 trace)用它。

  • saveTemporaryFile(data, filename)(src/McpContext.ts:575):走 getTempFilePath(在 OS temp 下 mkdtemp 建唯一子目录,src/utils/files.ts:11),写前也 validatePath。截图等临时产物用它——因为 temp 永远是允许的 root。

  • loadResource(url)(src/McpContext.ts:746):DevTools 引擎需要拉资源时的统一入口(通过构造函数里的 overrideDevToolsGlobals 注入,src/McpContext.ts:110-114)。它按协议分流:

    • http/https → 先过 blocklist、再过 allowlist(URLPattern 匹配,src/McpContext.ts:721-744),才 fetch
    • file: → 先 validatePath(fileURLToPath(url)) 走同一套沙箱,再读。
    • 其它协议 → 拒绝。

    blocklist 是"命中即拒",allowlist 是"配了就必须命中其一,否则拒"。这与 §3.1 下发给 Puppeteer 的网络名单是同一份 URL 模式,只是这里管的是 DevTools 侧的资源加载。


7. McpPage 的生命周期部分

McpPage(src/McpPage.ts:61)把散落的 per-page 状态收拢成一个对象。本章只讲"页面生命周期"相关的部分——快照/uid 见 03,等待细节见 04

7.1 构造:装上对话框监听与采集器

构造函数(src/McpPage.ts:89)记下稳定 id、可选的 isolatedContextName,并立刻挂两样东西:

  • 对话框处理器:page.on('dialog', ...) 把弹出的 alert/confirm/beforeunload 存进 #dialog(src/McpPage.ts:103-106)。之后 getDialog()/throwIfDialogOpen()/clearDialog() 读写它;handle_dialog 工具据此接受或忽略弹窗。采集器只提一句:同时创建了 NetworkCollectorConsoleCollector(src/McpPage.ts:108)订阅网络/控制台事件,详见 05

7.2 init:聚焦仿真 + DevTools universe

init()(src/McpPage.ts:124)用 Promise.allSettled 并行做两件"失败也不该炸"的事:

  • emulateFocusedPage(true)(src/McpPage.ts:131-136):对每个页都伪装成"聚焦中"。注释点明动机——支持多智能体工作流:多个 agent 各自操作不同标签页时,后台页也不该因为"没聚焦"而暂停计时器、掉帧、停回调,否则自动化就不可靠了。
  • DevTools universe(src/McpPage.ts:138-148):建一条 CDP session 并 createTargetUniverse,作为复用真实 DevTools 引擎的通道(性能/内存那套,见 06)。失败就记日志、降级到不暴露 DevTools。

7.3 emulate 与仿真设置

emulate(options)(src/McpPage.ts:544)统一应用网络节流、CPU 节流、地理位置、UA、配色、视口、额外请求头,并把结果存进 emulationSettings(src/McpPage.ts:71)。两个细节:

  • 若配了 block/allowlist,网络节流被禁用并报错——两者在 Puppeteer 里冲突(src/McpPage.ts:556-562)。
  • 设视口可能触发 reload,所以特意updateTimeouts() 再设视口,免得这次 reload 被超时打断(src/McpPage.ts:652-656)。restoreEmulation() 则在需要时按已存设置重放一遍。

7.4 updateTimeouts:让节流不误报超时

updateTimeouts()(src/McpPage.ts:659)按当前 CPU/网络倍率放大默认超时:普通等待 5s × cpu 倍率,导航 10s × 网络倍率 × cpu 倍率。这样当 agent 故意把页面节流到"慢速 3G + 4× CPU"时,操作不会因为环境被人为拖慢而假性超时。选中一个页(selectPage)时也会调用它,保证当前页用的是它自己的超时。


8. 进程启停:入口与关停钩子

8.1 启动顺序

进程入口 src/bin/chrome-devtools-mcp-main.ts顶层 await 脚本,顺序:

  1. checkForUpdates(...)(src/bin/...main.ts:22):后台查有没有新版本,读 ~/.cache/chrome-devtools-mcp/latest.json,过期就 detached 子进程去查、结果下次用;有更新则打印一行提示(src/utils/check-for-updates.ts:27)。不阻塞主流程。
  2. parseArguments(VERSION)(src/bin/...main.ts:26):yargs 解析 CLI;middleware 里补默认 channel、按 CI 等环境变量关掉用量统计(src/bin/chrome-devtools-mcp-cli-options.ts:372)。CLI 层还声明了大量互斥关系(如 userDataDirbrowserUrl/wsEndpoint/isolated 冲突)。
  3. createMcpServer(args, {logFile})(src/index.ts:32):建 server、注册工具、装好 getContext
  4. 连上 StdioServerTransport(src/bin/...main.ts:78-79),然后 logDisclaimers 打印隐私提示。

懒加载再强调:走到这里浏览器还没起。第一次工具调用触发 getContext() 才拉起 Chrome。

8.2 关停:stdin EOF / 信号 → shutdown → closeBrowser

stdio 版 MCP 的退出约定是"客户端关掉传输"。入口挂了一组钩子(src/bin/...main.ts:58-72):stdinend/close,以及 SIGTERM/SIGINT/SIGHUP,都调 shutdown(reason)

shutdown(src/bin/...main.ts:41)做三件事:①用 shuttingDown 去重,只跑一次;②装一个 10 秒兜底定时器(.unref(),正常路径不拖住事件循环),万一 Chrome 拆解卡住就强制 exit(0);③await closeBrowser() 后退出。注释解释了为何非挂不可:否则 stdin 关闭后,活着的 Chrome 子进程仍 ref 住 Node 事件循环,服务器会一直挂着。

closeBrowser()(src/browser.ts:297)据 browserMode 分流:

模式动作为什么
launchedbrowser.close()是我起的 Chrome 子进程,要回收掉
connectedbrowser.disconnect()是连来的用户 Chrome,只断开、不能关掉人家

它进来先把模块级 browser/browserMode 清空再操作,是与 §3.2"先 mode 后 browser"配套的并发保护。


9. 巧妙之处(可带走的设计)

  • 进程级 page id,而非每 context 从 1 数(src/McpContext.ts:67)。一行计数器换来"重连后旧 id 干净失败而非误命中",是 agent 安全的隐形护栏。
  • realpath 沙箱防软链逃逸(src/McpContext.ts:247-267 + src/utils/files.ts:18)。不是比字符串前缀,而是解开所有符号链接后比,连"尚不存在的新文件"也能靠"最近存在祖先"规范化。
  • roots 未协商就退到 temp-only(src/index.ts:89-96src/McpContext.ts:216-230)。默认安全:不主动信任一个没声明工作区的客户端;要放开得显式 --allow-unrestricted-paths
  • 对每个页都伪装聚焦(src/McpPage.ts:131-136)。一句 emulateFocusedPage(true) 让多 agent 并行操作后台标签页不掉链子。
  • DevToolsActivePort 自动连(src/browser.ts:84-103)。免去用户手抄 ws 地址,把"连我正开着的 Chrome"变成一个 flag。
  • 兜底只认 isClosed()(src/McpContext.ts:509-513)。区分"页真没了"和"这一拍恰好没列出",不轻易替换选中页。

10. 边界与局限

  • 一个进程一个浏览器。想跑多个浏览器实例得开多个 MCP 进程(或用 --isolated 换独立 profile);同一个 userDataDir 被占用会直接报错。
  • connect 时不删除浏览器状态。隔离上下文与连来的浏览器在 disconnect 时都被保留(src/McpContext.ts:146-149closeBrowser 的 disconnect 分支),清理是用户的事。
  • 沙箱只管本 MCP 的文件写入/资源加载,不约束页面内 JS 能访问什么;它保护的是"MCP 别往你磁盘乱写",不是"网页别乱来"。
  • --allow-unrestricted-paths 关掉沙箱。开了它路径校验被跳过(src/McpContext.ts:223-225),等于回到旧的信任模型,慎用。
  • 扩展相关连接受版本限制:--categoryExtensions 目前只在管道(launch)连接下支持,autoConnect/browserUrl/wsEndpoint 暂不支持(见 CLI 选项描述 src/bin/chrome-devtools-mcp-cli-options.ts:253)。

11. 代码地图(导航索引)

主题文件符号
进程入口 / 启停钩子src/bin/chrome-devtools-mcp-main.tsshutdownprocess.stdin.on('end')
CLI 解析 / 互斥关系src/bin/chrome-devtools-mcp-cli-options.tsparseArgumentscliOptions
更新检查src/utils/check-for-updates.tscheckForUpdates
launch/connect 分叉 + 按需重建 contextsrc/index.tsgetContextupdateRoots
目标过滤src/browser.tsmakeTargetFilter
连接已有 Chrome / 自动连src/browser.tsensureBrowserConnected(DevToolsActivePort)
启动新 Chromesrc/browser.tslaunchensureBrowserLaunched
关停(close vs disconnect)src/browser.tscloseBrowserbrowserMode
页面清单 / 快照 / 事件src/McpContext.tscreatePagesSnapshot#fetchBrowserPages#onTargetCreated
稳定 page idsrc/McpContext.tsnextPageId#createMcpPage
选中页 / 兜底src/McpContext.tsselectPagegetSelectedMcpPagegetSelectedPageFallback
新开页 / 隔离上下文src/McpContext.tsnewPage#isolatedContexts
reconnect 提示src/McpContext.tsconsumeReconnectNotice
路径沙箱src/McpContext.ts / src/utils/files.tsrootsvalidatePath / resolveCanonicalPath
文件写入 / 资源加载src/McpContext.tssaveFilesaveTemporaryFileloadResource
单页封装 / 生命周期src/McpPage.tsconstructorinitemulateupdateTimeouts
页面级工具src/tools/pages.tsnewPageselectPagehandleDialog

想继续:页面里"agent 怎么看见并点中元素"接 03-snapshot-uid.md;"操作怎么可靠落地(自动等待)"接 04-autowait-input.md;"响应怎么组装、采集器怎么工作"接 05-response-collectors.md