Skip to content

fix(extension): support multiple tabs opened by Playwright#1478

Draft
snomiao wants to merge 10 commits intomicrosoft:mainfrom
snomiao:fix-multi-tab
Draft

fix(extension): support multiple tabs opened by Playwright#1478
snomiao wants to merge 10 commits intomicrosoft:mainfrom
snomiao:fix-multi-tab

Conversation

@snomiao
Copy link
Contributor

@snomiao snomiao commented Mar 20, 2026

Fixes #1317

Problem

When using --extension mode, Playwright can only control the single tab that was manually selected in the extension popup. Any tab Playwright opens via tab-new (i.e. Target.createTarget) is created but immediately uncontrollable.

Root cause: _onDebuggerEvent in RelayConnection only forwards CDP events where source.tabId === this._debuggee.tabId. New tabs never have a debugger attached and their events are never forwarded — so Playwright sees them in tab-list but cannot interact with them.

Fix

relayConnection.ts

  • Add createTab command: creates a Chrome tab via chrome.tabs.create(), attaches the debugger, and returns { tabId, targetInfo } to the relay
  • Track Playwright-opened tabs in _playwrightTabIds: Set<number>
  • Forward CDP events from all Playwright-opened tabs (tagged with tabId for relay routing)
  • Route forwardCDPCommand to the correct debuggee by tabId
  • attachToTab now returns tabId so the relay can build its session→tab mapping
  • Add onPlaywrightTabCreated / onPlaywrightTabRemoved callbacks for the background script
  • Detach debugger from all Playwright tabs on close

background.ts

  • Track Playwright tab IDs; clear badges and the set when the connection closes
  • Show a blue badge on Playwright-managed tabs (distinct from the green on the initial connected tab)
  • Expose playwrightTabIds via getConnectionStatus
  • Clean up Playwright tab IDs in _onTabRemoved

status.tsx

  • Add a "Playwright managed tabs" section listing all tabs Playwright opened, clickable to focus

Demo: multi-tab control (playwright-cli)

Open two independent sessions in different workspaces simultaneously, each managing their own tabs:

# Workspace A — session ws-a
playwright-cli -s=ws-a tab-new
playwright-cli -s=ws-a goto https://example.com
playwright-cli -s=ws-a tab-new
playwright-cli -s=ws-a goto https://github.com
playwright-cli -s=ws-a tab-list
=== Session A ===
- 0: [Playwright MCP extension]
- 1: [Example Domain](https://example.com/)
- 2: (current) [GitHub](https://github.com/)
# Workspace B — session ws-b (runs concurrently, fully isolated)
playwright-cli -s=ws-b tab-new
playwright-cli -s=ws-b goto https://wikipedia.org
playwright-cli -s=ws-b tab-new
playwright-cli -s=ws-b goto https://news.ycombinator.com
playwright-cli -s=ws-b tab-list
=== Session B ===
- 0: [Playwright MCP extension]
- 1: [Wikipedia](https://www.wikipedia.org/)
- 2: (current) [Hacker News](https://news.ycombinator.com/)

Each session sees only its own tabs. Tab 1 in each was navigated externally by the user mid-test — and both sessions continued working independently without interfering.

Demo: MCP server (playwright-mcp)

Start the MCP server from a project directory, connect via HTTP, and call tools:

# Start server
PLAYWRIGHT_MCP_EXTENSION_ID=<your-extension-id> \
PLAYWRIGHT_MCP_EXTENSION_TOKEN=<your-token> \
playwright-mcp --extension --port 4321

Initialize and list available tools:

# Initialize session
curl -si -X POST http://localhost:4321/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

# List tools (with session ID from above response)
curl -s -X POST http://localhost:4321/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: <session-id>" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

Returns 21 registered tools:

- browser_close
- browser_resize
- browser_console_messages
- browser_handle_dialog
- browser_evaluate
- browser_file_upload
- browser_fill_form
- browser_press_key
- browser_type
- browser_navigate
- browser_navigate_back
- browser_network_requests
- browser_run_code
- browser_take_screenshot
- browser_snapshot
- browser_click
- browser_drag
- browser_hover
- browser_select_option
- browser_tabs
- browser_wait_for

For use with Claude Code or Cursor, add to your MCP config:

{
  "mcpServers": {
    "playwright": {
      "command": "playwright-mcp",
      "args": ["--extension"],
      "env": {
        "PLAYWRIGHT_MCP_EXTENSION_ID": "<your-extension-id>",
        "PLAYWRIGHT_MCP_EXTENSION_TOKEN": "<your-token>"
      }
    }
  }
}

When started from a project directory containing a .playwright/ marker (created by playwright-cli install), browser profiles and output are automatically stored in .playwright/mcp-profile/ and .playwright/mcp-output/ — no manual path config needed.

snomiao added 3 commits March 21, 2026 04:21
…multiple tabs

- Add createTab command: extension creates real Chrome tab via chrome.tabs.create()
- Forward CDP events from all Playwright-opened tabs (not just the initial tab)
- Route forwardCDPCommand by tabId to correct debuggee
- attachToTab now returns tabId for session tracking
- Update manifest name to 'Playwright MCP Bridge (multi-tab)' and version to 0.0.68.1
- status.html now shows a 'Playwright managed tabs' section listing all tabs opened by Playwright
- Extension icon shows blue ✓ badge on Playwright-managed tabs
- background.ts tracks playwright tab IDs via relay callbacks
- relayConnection.ts fires onPlaywrightTabCreated/onPlaywrightTabRemoved callbacks
- Bump version to 0.0.68.2
- Use inferred Set type: Set<number> = new Set() -> = new Set<number>()
- Use nullish coalescing for url default (|| -> ??)
- Fix ternary indentation to 4-space continuation
- Remove stale comment on forwardCDPCommand
- Fix double space in import
@snomiao
Copy link
Contributor Author

snomiao commented Mar 20, 2026

@microsoft-github-policy-service agree

snomiao added 7 commits March 21, 2026 08:15
…olated

- background.ts: replace single _activeConnection with _connections Map keyed by relay URL
- Each relay URL (UUID-based) gets its own ConnectionState with its own tab set
- Multiple simultaneous MCP instances no longer conflict
- status.html shows all active connections, each with their own tab lists
- disconnect accepts optional mcpRelayUrl to target specific instance
- getConnectionStatus returns full connections array (with legacy compat fields)
- Bump version to 0.0.68.3
When a .playwright/ marker directory exists in the project tree,
default PLAYWRIGHT_MCP_OUTPUT_DIR to .playwright/mcp-output/ and
PLAYWRIGHT_MCP_USER_DATA_DIR to .playwright/mcp-profile/. This gives
each project automatic workspace-level isolation — different projects
get different browser profiles and output dirs without any manual config.

Falls back to upstream defaults when no workspace is found.
Explicitly set env vars always take precedence.
In --extension mode the relay opens connect.html in the user's running
Chrome by matching user-data-dir. Setting a workspace-local profile dir
caused a new Chrome window to spawn instead of reusing the existing one.
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.

Allow the MCP server (with --extension) to manage multiple tabs

1 participant