# McpContext Architecture and Browser Session Management in Chrome DevTools MCP

> Explore McpContext architecture and browser session management in Chrome DevTools MCP. Learn how it efficiently handles page state and DevTools integration.

- Repository: [ChromeDevTools/chrome-devtools-mcp](https://github.com/chromedevtools/chrome-devtools-mcp)
- Tags: architecture
- Published: 2026-02-16

---

**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`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/browser.ts)

The browser lifecycle—connecting to an existing Chrome instance or launching a fresh one—is isolated in [`src/browser.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/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`.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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`.

```typescript
// 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`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/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

```typescript
// 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:

```typescript
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`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/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.