McpContext Architecture and Browser Session Management in Chrome DevTools MCP

The McpContext class is a thin, test-friendly wrapper around Puppeteer that owns a single Browser instance, tracks open pages, maintains per-page state (network throttling, viewport, geolocation), and provides high-level helpers for accessibility snapshots and DevTools UI integration.

The chrome-devtools-mcp package abstracts the complexity of automating Chrome DevTools through a centralized context object. Understanding the McpContext architecture and its browser session management is essential for writing reliable automation scripts that interact with the DevTools front-end, capture network data, and manipulate browser state deterministically.

Browser Session Management in src/browser.ts

The browser lifecycle—connecting to an existing Chrome instance or launching a fresh one—is isolated in src/browser.ts. This module ensures that a singleton Browser instance is reused across calls and applies consistent target filtering to exclude internal Chrome URLs.

Target Filtering for Clean Session State

The makeTargetFilter() function returns a predicate that filters out chrome:// and chrome-extension:// URLs while allowing the new-tab page and chrome://inspect. This filter is passed to both puppeteer.connect() and puppeteer.launch() to ensure the test runner only sees relevant targets.

// src/browser.ts#L22-L44
const makeTargetFilter = () => {
  return (target: Target) => {
    const url = target.url();
    // Exclude chrome:// and chrome-extension:// except for specific allowed URLs
    if (url.startsWith('chrome://') && !allowedChromeUrls.includes(url)) {
      return false;
    }
    return !url.startsWith('chrome-extension://');
  };
};

Connecting to an Existing Chrome Instance

The ensureBrowserConnected() function supports attaching to a running Chrome via wsEndpoint, browserURL, channel, or a userDataDir containing a DevToolsActivePort file. It constructs connectOptions with the target filter, defaultViewport: null, and handleDevToolsAsPage: true to treat DevTools windows as standard pages.

// src/browser.ts#L46-L84
export async function ensureBrowserConnected(options: ConnectOptions): Promise<Browser> {
  if (browser) return browser;
  
  const connectOptions = {
    targetFilter: makeTargetFilter(),
    defaultViewport: null,
    handleDevToolsAsPage: true,
    // ... additional logic for wsEndpoint, browserURL, etc.
  };
  
  browser = await puppeteer.connect(connectOptions);
  return browser;
}

Launching Fresh Browser Instances

The launch() function computes a user data directory (isolated or shared), assembles launch arguments (including --auto-open-devtools-for-tabs when devtools: true), selects the correct Chrome channel, and forwards the targetFilter to Puppeteer. A module-level variable let browser: Browser | undefined; ensures the singleton pattern is maintained.

// src/browser.ts#L20-L23 and #L51-L106
let browser: Browser | undefined;

export async function launch(options: LaunchOptions): Promise<Browser> {
  if (browser) return browser;
  
  const launchOptions = {
    targetFilter: makeTargetFilter(),
    userDataDir: options.isolated ? generateTempDir() : options.userDataDir,
    args: options.devtools ? ['--auto-open-devtools-for-tabs'] : [],
    channel: options.channel || 'stable',
  };
  
  browser = await puppeteer.launch(launchOptions);
  return browser;
}

McpContext Core Architecture in src/McpContext.ts

The McpContext class serves as the central coordination point for browser automation. It maintains references to the singleton Browser, tracks the set of visible pages, manages per-page state, and provides high-level abstractions for DevTools integration.

State Management Members

McpContext encapsulates several private fields to maintain session state:

  • #pages: A snapshot of visible pages after filtering, optionally excluding DevTools windows.
  • #pageToDevToolsPage: Maps a regular page to its attached DevTools UI page.
  • #selectedPage: The active page for context-wide commands like navigation and snapshots.
  • #pageIdMap / #nextPageId: Assigns persistent numeric IDs to pages for stable references.
  • Per-page setting maps: #networkConditionsMap, #cpuThrottlingRateMap, and others store overrides in WeakMap instances keyed by Page.
// src/McpContext.ts#L8-L34
export class McpContext {
  private browser: Browser;
  #pages: Page[] = [];
  #pageToDevToolsPage = new WeakMap<Page, Page>();
  #selectedPage?: Page;
  #pageIdMap = new WeakMap<Page, number>();
  #nextPageId = 1;
  
  #networkConditionsMap = new WeakMap<Page, NetworkConditions>();
  #cpuThrottlingRateMap = new WeakMap<Page, number>();
  // ... additional WeakMaps for viewport, userAgent, etc.
}

Lifecycle and Initialization

The McpContext constructor is private; instances are created via the static from() method which handles async initialization. The #init method populates the page snapshot, assigns IDs, and initializes collectors.

// src/McpContext.ts#L43-L77
export static async from(browser: Browser, logger, options, locatorClass): Promise<McpContext> {
  const context = new McpContext(browser, logger, options, locatorClass);
  await context.#init();
  return context;
}

async #init(): Promise<void> {
  await this.createPagesSnapshot();
  this.#networkCollector = new NetworkCollector(this.#selectedPage!);
  this.#consoleCollector = new ConsoleCollector(this.#selectedPage!);
  this.#devtoolsUniverseManager = new UniverseManager(this.#selectedPage!);
}

Page Discovery and DevTools Integration

The createPagesSnapshot() method queries browser.pages() and updates the internal #pages array. It auto-selects the first page if none is selected, then calls detectOpenDevToolsWindows() to map regular pages to their DevTools UI counterparts.

// src/McpContext.ts#L87-L116
async createPagesSnapshot(): Promise<void> {
  const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
  this.#pages = allPages.filter(p => /* filtering logic */);
  
  if (!this.#selectedPage && this.#pages.length > 0) {
    await this.selectPage(this.#pages[0]);
  }
  
  await this.detectOpenDevToolsWindows();
}

The detectOpenDevToolsWindows() method iterates through pages, identifies URLs starting with devtools://, extracts the underlying inspected URL via extractUrlLikeFromDevToolsTitle, and links it to the matching regular page in #pageToDevToolsPage.

Per-Page State and Throttling

McpContext maintains per-page configuration through WeakMap instances. When settings like network conditions or CPU throttling are applied, they are stored in the corresponding map and applied to the selected page via #updateSelectedPageTimeouts.

// src/McpContext.ts#L85-L92 (conceptual)
setNetworkConditions(conditions: NetworkConditions): void {
  this.#networkConditionsMap.set(this.#selectedPage!, conditions);
  this.#updateSelectedPageTimeouts();
}

setCpuThrottlingRate(rate: number): void {
  this.#cpuThrottlingRateMap.set(this.#selectedPage!, rate);
  this.#updateSelectedPageTimeouts();
}

Event Collection with PageCollector

The PageCollector class hierarchy in src/PageCollector.ts abstracts per-page event collection. McpContext instantiates NetworkCollector and ConsoleCollector (both extending PageCollector) to gather network requests and console messages.

The generic PageCollector<T>:

  • Registers listeners via a callback supplied in the constructor
  • Stores resources in a WeakMap<Page, Array<Array<T>>> structure where each navigation pushes a fresh inner array
  • Maintains only the most recent #maxNavigationSaved (default 3) navigations
  • Generates stable numeric IDs via getIdForResource using a per-page incrementing counter
// src/PageCollector.ts#L44-L71
export abstract class PageCollector<T> {
  #storage = new WeakMap<Page, Array<Array<T>>>();
  #idCounter = new WeakMap<Page, number>();
  #maxNavigationSaved = 3;

  getIdForResource(page: Page, resource: T): number {
    if (!this.#idCounter.has(page)) {
      this.#idCounter.set(page, 0);
    }
    const id = this.#idCounter.get(page)!;
    this.#idCounter.set(page, id + 1);
    return id;
  }
}

Practical Implementation Example

The following example demonstrates the complete flow from browser launch to network data retrieval using the McpContext architecture:

import { ensureBrowserLaunched } from './browser.js';
import { McpContext } from './McpContext.js';

// 1. Launch or connect to Chrome
const browser = await ensureBrowserLaunched({
  devtools: true,
  headless: false,
  isolated: true,
  channel: 'stable',
});

// 2. Initialize context with collectors
const ctx = await McpContext.from(
  browser,
  console.error,
  { experimentalDevToolsDebugging: true, performanceCrux: false }
);

// 3. Configure page state
await ctx.setNetworkConditions('Slow 4G');
await ctx.setCpuThrottlingRate(2);

// 4. Navigate and wait for content
const page = ctx.getSelectedPage();
await page.goto('https://example.com');
await ctx.waitForTextOnPage('More information', 5000);

// 5. Capture accessibility snapshot
await ctx.createTextSnapshot();
const element = await ctx.getElementByUid('snapshot_12');

// 6. Retrieve collected network data
const requests = ctx.getNetworkRequests();
console.log('Captured requests:', requests.map(r => r.url()));

Summary

  • McpContext acts as a high-level façade that encapsulates Puppeteer complexity, providing a stable API for DevTools automation and test scripts.
  • Browser session management in src/browser.ts implements a singleton pattern with robust target filtering, supporting both remote debugging connections and fresh Chrome launches.
  • Per-page state tracking uses WeakMap instances to store network conditions, CPU throttling rates, and viewport settings without leaking memory when pages close.
  • Event collection is abstracted through the PageCollector hierarchy, which maintains navigation-aware storage and generates stable numeric IDs for network requests and console messages.
  • DevTools integration maps regular pages to their DevTools UI windows and exposes the underlying TargetUniverse for advanced debugging scenarios.

Frequently Asked Questions

How does McpContext handle multiple pages during a session?

McpContext maintains an internal #pages array that represents a filtered snapshot of visible pages. When createPagesSnapshot() is called, it queries browser.pages() and updates the internal state, auto-selecting the first available page if none is currently selected. Each page receives a persistent numeric ID stored in #pageIdMap, allowing stable references even as the browser navigates or opens new tabs.

What is the difference between ensureBrowserConnected and launch in browser.ts?

ensureBrowserConnected() attaches to an already-running Chrome instance using puppeteer.connect(), supporting connection via wsEndpoint, browserURL, or a userDataDir containing a DevToolsActivePort file. In contrast, launch() spawns a fresh Chrome process using puppeteer.launch(), computing isolated or shared user data directories and automatically opening DevTools windows when configured. Both methods apply the same targetFilter to exclude internal Chrome URLs and maintain the singleton Browser instance.

How does PageCollector prevent memory leaks when pages navigate or close?

PageCollector uses WeakMap instances to store per-page data, keyed by the Page object itself. When a page closes or is garbage collected, the associated entries in #storage and #idCounter become eligible for collection. Additionally, the collector implements a sliding window for navigation history, retaining only the most recent #maxNavigationSaved (default 3) navigations per page to prevent unbounded growth of request logs during long-running sessions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →