Skip to content

feat: add storage-isolated browser contexts#991

Merged
OrKoN merged 6 commits intoChromeDevTools:mainfrom
RobertWsp:feat/multi-session-support
Feb 23, 2026
Merged

feat: add storage-isolated browser contexts#991
OrKoN merged 6 commits intoChromeDevTools:mainfrom
RobertWsp:feat/multi-session-support

Conversation

@RobertWsp
Copy link
Contributor

@RobertWsp RobertWsp commented Feb 18, 2026

Summary

Adds storage-isolated browser contexts via an optional isolatedContext parameter on the new_page tool, following the simplified design proposed by @OrKoN in #926.

Pages created with the same isolatedContext name share cookies, localStorage, and storage. Pages in different isolated contexts (or the default context) are fully isolated — ideal for testing multi-user real-time features like chat, notifications, or collaborative editing.

Changes

new_page tool

  • New optional isolatedContext: string parameter
  • If specified, creates/reuses a named BrowserContext and opens a page in it
  • If omitted, uses the default browser context (existing behavior unchanged)

McpContext

  • #isolatedContexts Map: LLM-provided names → Puppeteer BrowserContext instances
  • #pageToIsolatedContextName WeakMap: GC-safe page → context name reverse lookup
  • Auto-discovery: externally created browser contexts get isolated-context-1, isolated-context-2, etc.
  • getIsolatedContextName(page): returns the isolated context name for a page (used by response formatting)
  • page.browserContext() used for context membership detection (no custom target event forwarding needed)
  • No context cleanup in dispose() or closePage() — either the entire browser is closed or we disconnect without destroying state

McpResponse

  • Page list includes isolatedContext=${name} labels (both text and structured JSON output)

ToolDefinition

  • Context interface extended with getIsolatedContextName(page) method

What's NOT included (by design)

  • No TargetEventEmitter: Puppeteer forwards target events from BrowserContextBrowser internally
  • No context cleanup: Browser contexts are not closed on dispose() or page close, per maintainer guidance
  • No about:blank cleanup: Default context and isolated contexts coexist side-by-side

Example

> new_page url="https://app.example.com/chat" isolatedContext="userA"
> new_page url="https://app.example.com/chat" isolatedContext="userB"
> list_pages

Page 1: [app.example.com/chat] isolatedContext=userA
Page 2: [app.example.com/chat] isolatedContext=userB [selected]

Pages in different isolated contexts have fully independent cookies, localStorage, IndexedDB, and WebSocket connections.

Tests

  • 6 new tests covering isolatedContext feature in tests/tools/pages.test.ts
  • All existing tests pass (333+)
  • Zero type errors, lint clean

Closes #926

Copy link
Collaborator

@OrKoN OrKoN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR but I think we can implement it without new tools, see #926 (comment)

@RobertWsp RobertWsp force-pushed the feat/multi-session-support branch from af249c2 to 2763aa0 Compare February 19, 2026 13:55
@RobertWsp
Copy link
Contributor Author

Thanks for the feedback, @OrKoN — you're right, the simpler design is much better.

I've reworked the PR to follow your suggestion from #926 exactly:

  • Removed create_session, list_sessions, close_session tools entirely (deleted SessionManager.ts and session.ts)
  • Added optional browserContext: string parameter to the new_page tool
  • McpContext maintains a Map<string, BrowserContext> mapping LLM-provided names → Puppeteer BrowserContexts
  • Auto-naming: unknown contexts discovered via browser.browserContexts() get assigned browser-context-1, browser-context-2, etc.
  • Page list snapshots include pages from all mapped browser contexts, with browserContext=${name} in each row (both text and structured JSON)
  • Cleanup: closing the last page in a browser context automatically closes that context; dispose() cleans up all contexts

Additional details:

  • TargetEventEmitter abstraction so PageCollector/UniverseManager work with both Browser and BrowserContext
  • WeakMap<Page, string> for page → context name lookup (GC-safe)
  • On first browserContext creation, leftover about:blank pages from the default context are cleaned up

All 333 existing tests pass, zero type errors. I've also updated the PR description to reflect the new design.

Ready for another look when you have time.

@RobertWsp RobertWsp requested a review from OrKoN February 19, 2026 14:14
@RobertWsp RobertWsp force-pushed the feat/multi-session-support branch 2 times, most recently from 29c0800 to cb0f182 Compare February 20, 2026 13:31
@OrKoN OrKoN changed the title feat: add multi-session support using BrowserContext feat: add storage-isolated browser contexts Feb 20, 2026
Copy link
Collaborator

@OrKoN OrKoN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I think the PR needs some polishing (left comments)

@RobertWsp RobertWsp force-pushed the feat/multi-session-support branch from cb0f182 to f84a246 Compare February 20, 2026 13:59
@RobertWsp
Copy link
Contributor Author

Thanks for the thorough review @OrKoN! All 6 items addressed in the new force-pushed commit (f84a246). The diff is now just 5 files, +141/-22 lines — much cleaner.

Here's what changed:

  1. browserContextisolatedContext ✅ — Renamed everywhere (param, fields, methods, docs)

  2. DevtoolsUtils.ts ✅ — Reverted, no longer touched

  3. main.ts ✅ — Reverted, no longer touched

  4. createPagesSnapshot() refactored ✅ — Created a private #getAllPages() helper that calls browser.pages() once and uses page.browserContext() for reverse-lookup instead of iterating the contexts Map. Both createPagesSnapshot() and detectOpenDevToolsWindows() use this helper.

  5. package-lock.json ✅ — Reverted, no longer touched

  6. PageCollector.ts / TargetEventEmitter ✅ — Removed entirely. PageCollector, NetworkCollector, ConsoleCollector, and UniverseManager all receive Browser directly as before — target events are already forwarded from all browser contexts.

The commit is squashed into a single clean commit on top of main. All 446 tests pass.

@RobertWsp RobertWsp requested a review from OrKoN February 20, 2026 14:03
Copy link
Collaborator

@OrKoN OrKoN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think tests were lost?

@RobertWsp RobertWsp force-pushed the feat/multi-session-support branch from f84a246 to cd9c325 Compare February 20, 2026 14:17
@RobertWsp
Copy link
Contributor Author

RobertWsp commented Feb 20, 2026

Removed all BrowserContext.close() calls — both from dispose() and from closePage() (which was closing empty contexts when their last page was closed). Isolated contexts now persist until the browser itself is closed or the client disconnects.

The previous tests were for the TargetEventEmitter abstraction which was removed, so they weren't applicable. Added 6 new tests specifically for the isolatedContext feature covering:

  • Creating a page in an isolated context
  • Reusing the same context for the same name
  • Separate contexts for different names
  • isolatedContext label in page listing output
  • Backward compatibility (default context returns undefined)
  • Closing an isolated page without errors

@RobertWsp RobertWsp force-pushed the feat/multi-session-support branch from cd9c325 to 070c5eb Compare February 20, 2026 14:23
Copy link
Collaborator

@OrKoN OrKoN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think this mostly looks good, I will double check on Monday and I think we would be able to merge it then. cc @natorion @matthiasrohmer

@RobertWsp RobertWsp force-pushed the feat/multi-session-support branch from 070c5eb to 7a049be Compare February 20, 2026 14:27
@RobertWsp
Copy link
Contributor Author

RobertWsp commented Feb 20, 2026

Done. Removed the entire isFirstIsolatedContext block — default context and isolated contexts now coexist without interference.

@passtas
Copy link
Contributor

passtas commented Feb 22, 2026

Hey @RobertWsp and @OrKoN , thanks for this PR - the isolatedContext feature is exactly what we needed. I've been using it to test cache behavior across different user roles (logged-in vs guest, different account types) by running parallel agents that each get their own isolated session to crawl the site in parallel. Works great for catching stale-cache bugs where one user sees another's cached data.

Ran into a race condition with the shared selectedPage state though.

The problem: isolatedContext correctly isolates cookies/storage, but all tools that operate on a page (take_screenshot, take_snapshot, navigate_page, evaluate_script, press_key, etc.) resolve the target via the global getSelectedPage(). Since select_page and take_screenshot are two separate tool calls, another agent can call select_page in between, causing agent A to screenshot agent B's page.

Reproduced it reliably: two isolated contexts, different URLs, two agents selecting+screenshotting in parallel. The agent that selects first consistently captures the wrong page.

Suggested fix: add an optional pageId parameter to tools that depend on getSelectedPage(). Something like a resolvePage(pageId?) helper that uses getPageById(pageId) when provided, falling back to getSelectedPage() when omitted (fully backward compatible). This makes page targeting atomic within a single tool call:

// Before (race-prone, two calls):
select_page(pageId: 2)
take_screenshot()           // might capture wrong page

// After (atomic, single call):
take_screenshot(pageId: 2)  // resolves page internally

The infrastructure is already there (getPageById, #pageIdMap). The main tools that would need it: take_screenshot, take_snapshot, navigate_page, evaluate_script, click_at, press_key, upload_file, resize_page, wait_for, and the performance tracing tools.

Should I open a separate issue to track this or here would be the right place?

@OrKoN
Copy link
Collaborator

OrKoN commented Feb 23, 2026

@passtas we specifically decided against a design where each tool needs a page id because usually multiple operations are performed on one page. Having pageId is each tool call increases the tokens consumed for each LLM iteration. Do you have evals that prove that this works better (in terms of token usage and ability of LLMs to use the tools)? cc @natorion

@passtas
Copy link
Contributor

passtas commented Feb 23, 2026

@OrKoN, that is of course fair point. I'd expect some marginal increase in token usage as the agent will have to remember and pass through the pageId within the same session and impact of additional description tokens for the correct tool use.

I guess the trick would also be to have it backward compatible by making this attribute optional, so that use will have to opt-in by putting specific prompt about context isolation etc., if he wouldn't he would still have current behaviour.

  resolvePage(pageId?: number): Page {
    if (pageId !== undefined) {
      return this.getPageById(pageId);
    }
    return this.getSelectedPage();
  }

And the description:

export const pageIdSchema = {
  pageId: zod
    .number()
    .optional()
    .describe(
      'The ID of the page to operate on. If omitted, uses the currently selected page. Use this to avoid race conditions when multiple agents work in parallel.',
    ),
};

My subjective thinking is that without ability to do meaningful work (all mentioned tools use) within browser isolated context - it doesn't make sense to release this PR, as it will lead to the unexpected behaviour.

As a user of agents and teams of agents, I'd like those subagents to be able to work accurately in within their sessions. Browser context isolation provides a bit of that capability, but doesn't enable those separate sessions to work to the 100% of the mcp capability.

I did the fix locally to see if it will work, and it does - opens separate isolated windows and does operations with it locally without loosing that pageId. Of course it requires the finesse to make sure it it works up to the standard.

Let me think on the potential eval to prove myself right or wrong about the token usage hypothesis. I'd also like to explore if the isolatedContextName may be of any help instead of explicit page selection.
Any pointers to similar scenarios how I can demonstrate it?

Thanks.

@OrKoN
Copy link
Collaborator

OrKoN commented Feb 23, 2026

My subjective thinking is that without ability to do meaningful work (all mentioned tools use) within browser isolated context - it doesn't make sense to release this PR, as it will lead to the unexpected behaviour.

@passtas I am not sure I follow, the select_page can be used to switch to a page in the isolated context.

@OrKoN
Copy link
Collaborator

OrKoN commented Feb 23, 2026

// Before (race-prone, two calls):
select_page(pageId: 2)
take_screenshot()           // might capture wrong page

this is not race-prone because we have an internal mutex that processes all commands one by one.

RobertWsp and others added 2 commits February 23, 2026 11:44
…port

Add optional `isolatedContext` parameter to `new_page` tool that creates
pages in isolated browser contexts (separate cookies, storage, WebSocket
connections). This enables testing multi-user scenarios where an LLM needs
simultaneous sessions as different users.

Implementation:
- new_page accepts optional isolatedContext string parameter
- McpContext manages a Map of named BrowserContexts
- Pages created with the same context name share an isolated environment
- Pages list displays context labels for easy identification
- Uses page.browserContext() for reverse-lookup instead of iterating contexts

Closes ChromeDevTools#926
@OrKoN OrKoN force-pushed the feat/multi-session-support branch from 9640004 to c03a05a Compare February 23, 2026 10:44
@passtas
Copy link
Contributor

passtas commented Feb 23, 2026

// Before (race-prone, two calls):
select_page(pageId: 2)
take_screenshot()           // might capture wrong page

this is not race-prone because we have an internal mutex that processes all commands one by one.

that's precisely what creates the problem. Here's the sequence:

Step Mutex processes #selectedPage after Correct?
1 Agent A: select_page(2) Page 2 Yes
2 Agent B: select_page(3) Page 3 Yes
3 Agent A: take_screenshot() Page 3 (captures wrong page) No, wanted Page 2

Each call is atomic, but between Agent A's select_page and take_screenshot, Agent B's select_page can run and mutate the shared #selectedPage.

The mutex guarantees no two tool calls execute simultaneously, but it doesn't guarantee that a two-call sequence from one agent runs without another agent's calls interleaving. Agent A has no way to hold the mutex across both calls.

@OrKoN
Copy link
Collaborator

OrKoN commented Feb 23, 2026

@passtas this is not correct usage of the tools, before any tool call is made the right page needs to be selected.

@OrKoN
Copy link
Collaborator

OrKoN commented Feb 23, 2026

Also, multi-agent flow is not very common, each client should have its own MCP server instance in the current design. Can you describe your set up in more details? perhaps it's best to continue the discussion on the feature issue.

@passtas
Copy link
Contributor

passtas commented Feb 23, 2026

Created #1019, so let's continue there.
On the teams of agents, this is what everyone is hyping about right now https://code.claude.com/docs/en/agent-teams

@OrKoN OrKoN added this pull request to the merge queue Feb 23, 2026
Merged via the queue into ChromeDevTools:main with commit 59f6477 Feb 23, 2026
17 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Feb 25, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.18.0](chrome-devtools-mcp-v0.17.3...chrome-devtools-mcp-v0.18.0)
(2026-02-24)


### 🎉 Features

* `--slim` mode for maximum token savings
([#958](#958))
([c402b43](c402b43))
* add a new skill for accessibility debugging and auditing with Chrome
DevTools MCP.
([#1002](#1002))
([b0c6d04](b0c6d04))
* add experimental screencast recording tools
([#941](#941))
([33446d4](33446d4))
* add skill to debug and optimize LCP
([#993](#993))
([2cd9b95](2cd9b95))
* add storage-isolated browser contexts
([#991](#991))
([59f6477](59f6477))
* add take_memory_snapshot tool
([#1023](#1023))
([7ffdc5e](7ffdc5e))
* support any-match text arrays in wait_for
([#1011](#1011))
([496ab1b](496ab1b))
* support type_text
([#1026](#1026))
([b5d01b5](b5d01b5))


### 🛠️ Fixes

* detect X server display on Linux
([#1027](#1027))
([1746ed9](1746ed9))


### ♻️ Chores

* cleanup string and structured console formatters
([#1005](#1005))
([0d78685](0d78685))
* extract version in a seprate file
([#1032](#1032))
([0106865](0106865))
* move emulation settings to context
([#1000](#1000))
([bc3c40e](bc3c40e))
* optimize slim tool descriptions and params
([#1028](#1028))
([ca6635d](ca6635d))
* simplify JavaScript code examples, update code block language, and
refine descriptions in a11y debugging skill documentation.
([#1009](#1009))
([5cedcaa](5cedcaa))
* types for JSON output of IssueFormatter
([#1007](#1007))
([9ef4479](9ef4479))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
wolfib pushed a commit to wolfib/chrome-devtools-mcp that referenced this pull request Mar 10, 2026
## Summary

Adds storage-isolated browser contexts via an optional `isolatedContext`
parameter on the `new_page` tool, following the simplified design
proposed by @OrKoN in ChromeDevTools#926.

Pages created with the same `isolatedContext` name share cookies,
localStorage, and storage. Pages in different isolated contexts (or the
default context) are fully isolated — ideal for testing multi-user
real-time features like chat, notifications, or collaborative editing.

## Changes

### `new_page` tool
- New optional `isolatedContext: string` parameter
- If specified, creates/reuses a named `BrowserContext` and opens a page
in it
- If omitted, uses the default browser context (existing behavior
unchanged)

### `McpContext`
- `#isolatedContexts` Map: LLM-provided names → Puppeteer
`BrowserContext` instances
- `#pageToIsolatedContextName` WeakMap: GC-safe page → context name
reverse lookup
- Auto-discovery: externally created browser contexts get
`isolated-context-1`, `isolated-context-2`, etc.
- `getIsolatedContextName(page)`: returns the isolated context name for
a page (used by response formatting)
- `page.browserContext()` used for context membership detection (no
custom target event forwarding needed)
- No context cleanup in `dispose()` or `closePage()` — either the entire
browser is closed or we disconnect without destroying state

### `McpResponse`
- Page list includes `isolatedContext=${name}` labels (both text and
structured JSON output)

### `ToolDefinition`
- `Context` interface extended with `getIsolatedContextName(page)`
method

## What's NOT included (by design)
- **No `TargetEventEmitter`**: Puppeteer forwards target events from
`BrowserContext` → `Browser` internally
- **No context cleanup**: Browser contexts are not closed on `dispose()`
or page close, per maintainer guidance
- **No `about:blank` cleanup**: Default context and isolated contexts
coexist side-by-side

## Example

```
> new_page url="https://app.example.com/chat" isolatedContext="userA"
> new_page url="https://app.example.com/chat" isolatedContext="userB"
> list_pages

Page 1: [app.example.com/chat] isolatedContext=userA
Page 2: [app.example.com/chat] isolatedContext=userB [selected]
```

Pages in different isolated contexts have fully independent cookies,
localStorage, IndexedDB, and WebSocket connections.

## Tests

- 6 new tests covering `isolatedContext` feature in
`tests/tools/pages.test.ts`
- All existing tests pass (333+)
- Zero type errors, lint clean

Closes ChromeDevTools#926

---------

Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
wolfib pushed a commit to wolfib/chrome-devtools-mcp that referenced this pull request Mar 10, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.18.0](ChromeDevTools/chrome-devtools-mcp@chrome-devtools-mcp-v0.17.3...chrome-devtools-mcp-v0.18.0)
(2026-02-24)


### 🎉 Features

* `--slim` mode for maximum token savings
([ChromeDevTools#958](ChromeDevTools#958))
([c402b43](ChromeDevTools@c402b43))
* add a new skill for accessibility debugging and auditing with Chrome
DevTools MCP.
([ChromeDevTools#1002](ChromeDevTools#1002))
([b0c6d04](ChromeDevTools@b0c6d04))
* add experimental screencast recording tools
([ChromeDevTools#941](ChromeDevTools#941))
([33446d4](ChromeDevTools@33446d4))
* add skill to debug and optimize LCP
([ChromeDevTools#993](ChromeDevTools#993))
([2cd9b95](ChromeDevTools@2cd9b95))
* add storage-isolated browser contexts
([ChromeDevTools#991](ChromeDevTools#991))
([59f6477](ChromeDevTools@59f6477))
* add take_memory_snapshot tool
([ChromeDevTools#1023](ChromeDevTools#1023))
([7ffdc5e](ChromeDevTools@7ffdc5e))
* support any-match text arrays in wait_for
([ChromeDevTools#1011](ChromeDevTools#1011))
([496ab1b](ChromeDevTools@496ab1b))
* support type_text
([ChromeDevTools#1026](ChromeDevTools#1026))
([b5d01b5](ChromeDevTools@b5d01b5))


### 🛠️ Fixes

* detect X server display on Linux
([ChromeDevTools#1027](ChromeDevTools#1027))
([1746ed9](ChromeDevTools@1746ed9))


### ♻️ Chores

* cleanup string and structured console formatters
([ChromeDevTools#1005](ChromeDevTools#1005))
([0d78685](ChromeDevTools@0d78685))
* extract version in a seprate file
([ChromeDevTools#1032](ChromeDevTools#1032))
([0106865](ChromeDevTools@0106865))
* move emulation settings to context
([ChromeDevTools#1000](ChromeDevTools#1000))
([bc3c40e](ChromeDevTools@bc3c40e))
* optimize slim tool descriptions and params
([ChromeDevTools#1028](ChromeDevTools#1028))
([ca6635d](ChromeDevTools@ca6635d))
* simplify JavaScript code examples, update code block language, and
refine descriptions in a11y debugging skill documentation.
([ChromeDevTools#1009](ChromeDevTools#1009))
([5cedcaa](ChromeDevTools@5cedcaa))
* types for JSON output of IssueFormatter
([ChromeDevTools#1007](ChromeDevTools#1007))
([9ef4479](ChromeDevTools@9ef4479))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Multi-session support for parallel browser instances

3 participants