feat(extension): support multi-tab - Playwright can open and control multiple tabs#39805
Open
snomiao wants to merge 1 commit intomicrosoft:mainfrom
Open
feat(extension): support multi-tab - Playwright can open and control multiple tabs#39805snomiao wants to merge 1 commit intomicrosoft:mainfrom
snomiao wants to merge 1 commit intomicrosoft:mainfrom
Conversation
…multiple tabs - Add createTab command to protocol: relay calls it when Playwright opens a new tab via Target.createTarget; extension creates a real Chrome tab and attaches the debugger - Add tabId to forwardCDPCommand/forwardCDPEvent: routes CDP traffic to the correct tab - cdpRelay: replace single _connectedTabInfo with _tabSessions map (sessionId → tabId) and _tabIdToSessionId map for reverse lookup from CDP events - Handle Target.createTarget: call createTab on extension, emit Target.attachedToTarget - Handle Target.getTargets: return all tracked tabs - Bump protocol VERSION to 2
Contributor
There was a problem hiding this comment.
Pull request overview
Adds multi-tab support to the MCP extension bridge by introducing a tab-creation command and routing CDP traffic/events to the correct tab across multiple concurrent tab sessions.
Changes:
- Bump MCP extension protocol version and add
createTab, plustabIdrouting metadata for CDP commands/events. - Replace single-tab tracking with per-tab session maps and implement
Target.createTarget/Target.getTargetshandling in the relay. - Route extension → Playwright CDP events back to the correct relay session using
tabId.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/playwright-core/src/tools/mcp/protocol.ts | Protocol v2: adds createTab and tabId fields for routing CDP commands/events per tab. |
| packages/playwright-core/src/tools/mcp/cdpRelay.ts | Implements multi-tab session tracking, handles target creation/listing, and routes messages/events by tab. |
Comment on lines
+316
to
+336
| case 'Target.createTarget': { | ||
| const { targetInfo, tabId } = await this._extensionConnection!.send('createTab', { url: params?.url }); | ||
| const tabSessionId = `pw-tab-${this._nextSessionId++}`; | ||
| this._tabSessions.set(tabSessionId, { targetInfo, tabId }); | ||
| if (tabId !== undefined) | ||
| this._tabIdToSessionId.set(tabId, tabSessionId); | ||
| this._sendToPlaywright({ | ||
| method: 'Target.attachedToTarget', | ||
| params: { | ||
| sessionId: tabSessionId, | ||
| targetInfo: { ...targetInfo, attached: true }, | ||
| waitingForDebugger: false, | ||
| } | ||
| }); | ||
| return { targetId: targetInfo.targetId }; | ||
| } | ||
| case 'Target.getTargets': { | ||
| return { | ||
| targetInfos: [...this._tabSessions.values()].map(s => ({ ...s.targetInfo, attached: true })), | ||
| }; | ||
| } |
There was a problem hiding this comment.
_tabSessions/_tabIdToSessionId entries are only ever added and never removed when tabs are closed or detached, so Target.getTargets can return stale targets and tabId reuse could route events/commands to the wrong session; consider deleting the mappings when handling Target.closeTarget and/or when receiving Target.detachedFromTarget/Target.targetDestroyed for a tab.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When using `--extension` mode, `CDPRelayServer` only tracked a single tab via `_connectedTabInfo`. Opening a new page via `browser.newPage()` sent `Target.createTarget` to the relay, which forwarded it as a raw CDP command — the extension had no mechanism to create a real Chrome tab, so the call failed silently.
Fix
`protocol.ts`
`cdpRelay.ts`
How it works
```
Playwright CDPRelayServer Extension
| | |
|-- Target.setAutoAttach ------>| |
| |-- attachToTab --------->|
| |<-- { tabId, targetInfo }|
|<-- Target.attachedToTarget ---| |
| | |
|-- Target.createTarget ------->| |
| |-- createTab(url) ------>|
| | (chrome.tabs.create) |
| |<-- { tabId, targetInfo }|
|<-- Target.attachedToTarget ---| |
| | |
|-- CDP cmd (sessionId=pw-tab-2)| |
| |-- forwardCDPCommand -->|
| | (tabId=456) |
| | (routes to Tab 2) |
```
Multi-client with HTTP Streamable (`/mcp`)
When running `playwright-mcp --extension --port N`, each `POST /mcp` session calls `createExtensionBrowser()` which creates a new `CDPRelayServer` with a unique UUID relay URL. The extension's multi-instance fix (microsoft/playwright-mcp#1478) isolates each relay connection independently:
```
playwright-mcp --extension --port 4321 (one shared process)
├── Claude session A → CDPRelayServer (uuid-aaa) → extension ConnectionState A → Tab 1
└── Claude session B → CDPRelayServer (uuid-bbb) → extension ConnectionState B → Tab 2
```
Two simultaneous Claude sessions can each control their own tabs with no conflicts, as long as each session is connected to a different browser tab (Chrome only allows one debugger per tab).
Related