Repository navigation
Description
Version
1.64.0-next (reproduced on main at 312030c; recording added in #42359)
Steps to reproduce
Two MCP clients on one server with --shared-browser-context, both using the devtools caps.
npx @playwright/mcp --port 8931 --shared-browser-context --caps=devtools
- Client A connects, navigates, calls
browser_start_recording. - Client B connects, calls
browser_start_recording. - Client A navigates (an action that should be recorded for A), then calls
browser_stop_recording. - Client B navigates, then calls
browser_stop_recording.
Expected behavior
Either B's browser_start_recording is refused because a recording is already in progress on the
shared context, or the two recordings are kept separate. Whichever is chosen, A's actions should not
end up in B's transcript, and nobody should silently get an empty result.
Actual behavior
A's action is handed to B, and A is told nothing was recorded:
A start -> "Recording started. Call browser_stop_recording to retrieve the recorded actions."
B start -> "Recording started. Call browser_stop_recording to retrieve the recorded actions."
A stop -> "Recording stopped. No actions were recorded."
B stop -> "Recording stopped. Recorded actions:
await page.goto('http://localhost:8907/empty.html');"
That goto is A's navigation from step 3, not B's. So:
- B's start is not refused even though the shared context is already recording.
- A loses its recording entirely and is told "No actions were recorded", with no error.
- B receives an action performed by a different client.
Additional context
browser_start_recording guards on per-Context state
(packages/playwright-core/src/tools/backend/context.ts:241):
async startRecording() {
if (this._recordedActions)
throw new Error('Recording is already in progress.');but with --shared-browser-context each client gets its own Context while sharing one
BrowserContext object. program.ts:137:
const browserContext = config.browser.isolated ? await browser.newContext(config.browser.contextOptions) : browser.contexts()[0];and the sink on the client BrowserContext is a single field, not a set
(packages/playwright-core/src/client/browserContext.ts:88, assigned at :543), so B's
_enableRecorder overwrites A's sink. A's later stopRecording then calls _disableRecorder() on
the shared context, which is why B's own recorder also stops.
Note --isolated is unaffected, since that branch gives each client its own newContext(). Only
--shared-browser-context shares the object.
Existing coverage: tests/mcp/recorder.spec.ts uses a single-client fixture, and "start twice" and
"stop without start" are tested and behave as asserted on one client. The two-client shared-context
interleaving is untested. tests/mcp/http.spec.ts:394 already has a two-client shared-context
harness that this can be reproduced in with a few lines.
I have not opened a PR. Mine keep getting closed over an unsigned CLA on my side, so this is more
useful to you as a report, and the right fix here looks like a design call anyway (refuse the second
start versus make the sink per-client) rather than something I should pick for you.
I verified the transcript above by running it rather than reasoning from the source, since the
outcome surprised me: I expected B's result to be truncated, not to contain A's action. if I have
mischaracterised the intended behaviour of --shared-browser-context, happy to be corrected. freshman
in college, thanks for reading :)
Environment
System:
OS: macOS 26.5.2
CPU: (10) arm64 Apple M4
Binaries:
Node: 26.0.0
npm: 11.12.1
npmPackages:
playwright monorepo at main (1.64.0-next)
Activity
bd7343567-design commentedon Sep 8, 2026
A start -> "Recording started. Call browser_stop_recording to retrieve the recorded actions."
B start -> "Recording started. Call browser_stop_recording to retrieve the recorded actions."
A stop -> "Recording stopped. No actions were recorded."
B stop -> "Recording stopped. Recorded actions:
await page.goto('http://localhost:8907/empty.html');"
bd7343567-design commentedon Sep 8, 2026
A start -> "Recording started. Call browser_stop_recording to retrieve the recorded actions."
B start -> "Recording started. Call browser_stop_recording to retrieve the recorded actions."
A stop -> "Recording stopped. No actions were recorded."
B stop -> "Recording stopped. Recorded actions:
await page.goto('http://localhost:8907/empty.html');"
fix(recorder): split showRecorder and startRecording, deliver events …