From e286b77e19d2114f363355d22bad890801cabdad Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 17 Sep 2025 18:01:05 -0700 Subject: [PATCH 1/5] feat(mcp): support shared browser context Fixes https://github.com/microsoft/playwright-mcp/issues/1045 --- .../src/mcp/browser/browserContextFactory.ts | 43 ++++++++++++ .../src/mcp/browser/browserServerBackend.ts | 2 + packages/playwright/src/mcp/browser/config.ts | 2 + .../playwright/src/mcp/browser/watchdog.ts | 2 + packages/playwright/src/mcp/config.d.ts | 5 ++ packages/playwright/src/mcp/program.ts | 1 + tests/mcp/http.spec.ts | 67 ++++++++++++++++++- tests/mcp/sse.spec.ts | 64 +++++++++++++++++- 8 files changed, 182 insertions(+), 4 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index f6b7a55e12f42..ab0a52c94ef0b 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -31,6 +31,8 @@ import type { LaunchOptions } from '../../../../playwright-core/src/client/types import type { ClientInfo } from '../sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { + if (config.sharedHttpContext) + return new SharedContextFactory(config); if (config.browser.remoteEndpoint) return new RemoteContextFactory(config); if (config.browser.cdpEndpoint) @@ -259,3 +261,44 @@ async function startTraceServer(config: FullConfig, tracesDir: string): Promise< function createHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); } + +export class SharedContextFactory implements BrowserContextFactory { + private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; + private _baseFactory: BrowserContextFactory; + private static _allContexts: Set = new Set(); + + constructor(config: FullConfig) { + const baseConfig = { ...config, sharedHttpContext: false }; + this._baseFactory = contextFactory(baseConfig); + SharedContextFactory._allContexts.add(this); + } + + async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + if (!this._contextPromise) { + testDebug('create shared browser context'); + this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, toolName); + } + + const { browserContext } = await this._contextPromise; + testDebug(`shared context client connected`); + return { + browserContext, + close: async () => { + testDebug(`shared context client disconnected`); + }, + }; + } + + private async _dispose() { + const contextPromise = this._contextPromise; + this._contextPromise = undefined; + if (!contextPromise) + return; + const { close } = await contextPromise; + await close(); + } + + static async disposeAll() { + await Promise.all([...SharedContextFactory._allContexts].map(context => context._dispose())); + } +} diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts index bd7ab7fbda7be..28ca63e342d97 100644 --- a/packages/playwright/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -75,6 +75,8 @@ export class BrowserServerBackend implements ServerBackend { } serverClosed() { + if (this._config.sharedHttpContext) + return; void this._context?.dispose().catch(logUnhandledError); } } diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 4e7f6e9d9679b..67524a504d0ea 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -52,6 +52,7 @@ export type CLIOptions = { saveSession?: boolean; saveTrace?: boolean; secrets?: Record; + sharedHttpContext?: boolean; storageState?: string; timeoutAction?: number; timeoutNavigation?: number; @@ -212,6 +213,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { saveSession: cliOptions.saveSession, saveTrace: cliOptions.saveTrace, secrets: cliOptions.secrets, + sharedHttpContext: cliOptions.sharedHttpContext, outputDir: cliOptions.outputDir, imageResponses: cliOptions.imageResponses, timeouts: { diff --git a/packages/playwright/src/mcp/browser/watchdog.ts b/packages/playwright/src/mcp/browser/watchdog.ts index c342e039f505b..80ebcd05a162d 100644 --- a/packages/playwright/src/mcp/browser/watchdog.ts +++ b/packages/playwright/src/mcp/browser/watchdog.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { SharedContextFactory } from './browserContextFactory'; import { Context } from './context'; export function setupExitWatchdog() { @@ -25,6 +26,7 @@ export function setupExitWatchdog() { // eslint-disable-next-line no-restricted-properties setTimeout(() => process.exit(0), 15000); await Context.disposeAll(); + await SharedContextFactory.disposeAll(); // eslint-disable-next-line no-restricted-properties process.exit(0); }; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 845f0ede6cb47..e244671d0915d 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -100,6 +100,11 @@ export type Config = { */ saveTrace?: boolean; + /** + * Reuse the same browser context between all connected HTTP clients. + */ + sharedHttpContext?: boolean; + /** * Secrets are used to prevent LLM from getting sensitive data while * automating scenarios such as authentication. diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index acc349ba00756..0802516bca34a 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -53,6 +53,7 @@ export function decorateCommand(command: Command, version: string) { .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) + .option('--shared-http-context', 'reuse the same browser context between all connected HTTP clients.') .option('--storage-state ', 'path to the storage state file for isolated sessions.') .option('--timeout-action ', 'specify action timeout in milliseconds, defaults to 5000ms', numberParser) .option('--timeout-navigation ', 'specify navigation timeout in milliseconds, defaults to 60000ms', numberParser) diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 4a498072b12d5..4399b1e020105 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -24,7 +24,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; import { ListRootsRequestSchema } from 'packages/playwright/lib/mcp/sdk/bundle'; -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string, kill: () => void }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; const userDataDir = testInfo.outputPath('user-data-dir'); @@ -55,7 +55,11 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP resolve(match[1]); })); - return { url: new URL(url), stderr: () => stderr }; + return { url: new URL(url), stderr: () => stderr, kill: () => { + cp?.kill('SIGTERM'); + cp = undefined; + } + }; }); cp?.kill('SIGTERM'); }, @@ -245,6 +249,65 @@ test('http transport browser lifecycle (persistent, multiclient)', async ({ serv await client2.close(); }); +test('http transport shared context', async ({ serverEndpoint, server }) => { + const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-http-context'] }); + + // Create first client and navigate + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test1', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // Create second client - should reuse the same browser context + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test2', version: '1.0.0' }); + await client2.connect(transport2); + + // Get tabs from second client - should see the tab created by first client + const tabsResult = await client2.callTool({ + name: 'browser_tabs', + arguments: { action: 'list' }, + }); + + // Should have at least one tab (the one created by client1) + expect(tabsResult.content[0]?.text).toContain('tabs'); + + await transport1.terminateSession(); + await client1.close(); + + // Second client should still work since context is shared + await client2.callTool({ + name: 'browser_snapshot', + arguments: {}, + }); + + await transport2.terminateSession(); + await client2.close(); + + kill(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + // Should have only one context creation since it's shared + expect(lines.filter(line => line.match(/create shared browser context/)).length).toBe(1); + + // Should see client connect/disconnect messages + expect(lines.filter(line => line.match(/shared context client connected/)).length).toBe(2); + expect(lines.filter(line => line.match(/shared context client disconnected/)).length).toBe(2); + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + // Context should only close when last client disconnects + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); +}); + test('http transport (default)', async ({ serverEndpoint }) => { const { url } = await serverEndpoint(); const transport = new StreamableHTTPClientTransport(url); diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index d67a24a98b2ac..0a1e52619a0a5 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -23,7 +23,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string, kill: () => void }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; const userDataDir = testInfo.outputPath('user-data-dir'); @@ -54,7 +54,10 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP resolve(match[1]); })); - return { url: new URL(url), stderr: () => stderr }; + return { url: new URL(url), stderr: () => stderr, kill: () => { + cp?.kill('SIGTERM'); + cp = undefined; + } }; }); cp?.kill('SIGTERM'); }, @@ -229,3 +232,60 @@ test('sse transport browser lifecycle (persistent, multiclient)', async ({ serve await client1.close(); await client2.close(); }); + +test('sse transport shared context', async ({ serverEndpoint, server }) => { + const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-http-context'] }); + + // Create first client and navigate + const transport1 = new SSEClientTransport(new URL('/sse', url)); + const client1 = new Client({ name: 'test1', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // Create second client - should reuse the same browser context + const transport2 = new SSEClientTransport(new URL('/sse', url)); + const client2 = new Client({ name: 'test2', version: '1.0.0' }); + await client2.connect(transport2); + + // Get tabs from second client - should see the tab created by first client + const tabsResult = await client2.callTool({ + name: 'browser_tabs', + arguments: { action: 'list' }, + }); + + // Should have at least one tab (the one created by client1) + expect(tabsResult.content[0]?.text).toContain('tabs'); + + await client1.close(); + + // Second client should still work since context is shared + await client2.callTool({ + name: 'browser_snapshot', + arguments: {}, + }); + + await client2.close(); + + kill(); + // console.log(stderr()); + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + // Should have only one context creation since it's shared + expect(lines.filter(line => line.match(/create shared browser context/)).length).toBe(1); + + // Should see client connect/disconnect messages + expect(lines.filter(line => line.match(/shared context client connected/)).length).toBe(2); + expect(lines.filter(line => line.match(/shared context client disconnected/)).length).toBe(2); + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + // Context should only close when last client disconnects + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); +}); From 1ac823e7587707526ab99e53ca811b8effadc55e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 Sep 2025 10:15:50 -0700 Subject: [PATCH 2/5] fix lint --- tests/mcp/http.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 4399b1e020105..9520e57fd66bb 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -56,10 +56,9 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP })); return { url: new URL(url), stderr: () => stderr, kill: () => { - cp?.kill('SIGTERM'); - cp = undefined; - } - }; + cp?.kill('SIGTERM'); + cp = undefined; + } }; }); cp?.kill('SIGTERM'); }, From 5c3a79782ce5dda85ce36f1042b9a00346528b78 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 Sep 2025 10:22:27 -0700 Subject: [PATCH 3/5] factory singleton --- .../src/mcp/browser/browserContextFactory.ts | 25 ++++++++++++------- .../src/mcp/browser/browserServerBackend.ts | 2 -- .../playwright/src/mcp/browser/watchdog.ts | 2 +- tests/mcp/http.spec.ts | 13 +++++++--- tests/mcp/sse.spec.ts | 16 ++++++++---- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index ab0a52c94ef0b..0da95fe65aea9 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -32,7 +32,7 @@ import type { ClientInfo } from '../sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { if (config.sharedHttpContext) - return new SharedContextFactory(config); + return SharedContextFactory.create(config); if (config.browser.remoteEndpoint) return new RemoteContextFactory(config); if (config.browser.cdpEndpoint) @@ -265,12 +265,19 @@ function createHash(data: string): string { export class SharedContextFactory implements BrowserContextFactory { private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; private _baseFactory: BrowserContextFactory; - private static _allContexts: Set = new Set(); + private static _instance: SharedContextFactory | undefined; - constructor(config: FullConfig) { + static create(config: FullConfig) { + if (SharedContextFactory._instance) + throw new Error('SharedContextFactory already exists'); const baseConfig = { ...config, sharedHttpContext: false }; - this._baseFactory = contextFactory(baseConfig); - SharedContextFactory._allContexts.add(this); + const baseFactory = contextFactory(baseConfig); + SharedContextFactory._instance = new SharedContextFactory(baseFactory); + return SharedContextFactory._instance; + } + + private constructor(baseFactory: BrowserContextFactory) { + this._baseFactory = baseFactory; } async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { @@ -289,6 +296,10 @@ export class SharedContextFactory implements BrowserContextFactory { }; } + static async dispose() { + await SharedContextFactory._instance?._dispose(); + } + private async _dispose() { const contextPromise = this._contextPromise; this._contextPromise = undefined; @@ -297,8 +308,4 @@ export class SharedContextFactory implements BrowserContextFactory { const { close } = await contextPromise; await close(); } - - static async disposeAll() { - await Promise.all([...SharedContextFactory._allContexts].map(context => context._dispose())); - } } diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts index 28ca63e342d97..bd7ab7fbda7be 100644 --- a/packages/playwright/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -75,8 +75,6 @@ export class BrowserServerBackend implements ServerBackend { } serverClosed() { - if (this._config.sharedHttpContext) - return; void this._context?.dispose().catch(logUnhandledError); } } diff --git a/packages/playwright/src/mcp/browser/watchdog.ts b/packages/playwright/src/mcp/browser/watchdog.ts index 80ebcd05a162d..a437accab7ce6 100644 --- a/packages/playwright/src/mcp/browser/watchdog.ts +++ b/packages/playwright/src/mcp/browser/watchdog.ts @@ -26,7 +26,7 @@ export function setupExitWatchdog() { // eslint-disable-next-line no-restricted-properties setTimeout(() => process.exit(0), 15000); await Context.disposeAll(); - await SharedContextFactory.disposeAll(); + await SharedContextFactory.dispose(); // eslint-disable-next-line no-restricted-properties process.exit(0); }; diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 9520e57fd66bb..c0ab9d7e1054b 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -286,8 +286,6 @@ test('http transport shared context', async ({ serverEndpoint, server }) => { await transport2.terminateSession(); await client2.close(); - kill(); - await expect(async () => { const lines = stderr().split('\n'); expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); @@ -302,9 +300,18 @@ test('http transport shared context', async ({ serverEndpoint, server }) => { expect(lines.filter(line => line.match(/create context/)).length).toBe(2); expect(lines.filter(line => line.match(/close context/)).length).toBe(2); - // Context should only close when last client disconnects + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(0); + }).toPass(); + + kill(); + + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); }).toPass(); + }); test('http transport (default)', async ({ serverEndpoint }) => { diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index 0a1e52619a0a5..eec13f1e27326 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -269,12 +269,10 @@ test('sse transport shared context', async ({ serverEndpoint, server }) => { await client2.close(); - kill(); - // console.log(stderr()); await expect(async () => { const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); - expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2); // Should have only one context creation since it's shared expect(lines.filter(line => line.match(/create shared browser context/)).length).toBe(1); @@ -285,7 +283,15 @@ test('sse transport shared context', async ({ serverEndpoint, server }) => { expect(lines.filter(line => line.match(/create context/)).length).toBe(2); expect(lines.filter(line => line.match(/close context/)).length).toBe(2); - // Context should only close when last client disconnects + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(0); + }).toPass(); + + kill(); + + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); }).toPass(); }); From 9e3e0377d760ac4e731344e523a41263cd619804 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 Sep 2025 10:49:45 -0700 Subject: [PATCH 4/5] rename to --shared-browser-context --- packages/playwright/src/mcp/browser/browserContextFactory.ts | 4 ++-- packages/playwright/src/mcp/browser/config.ts | 4 ++-- packages/playwright/src/mcp/config.d.ts | 2 +- packages/playwright/src/mcp/program.ts | 2 +- tests/mcp/http.spec.ts | 2 +- tests/mcp/sse.spec.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 0da95fe65aea9..97cf9f12201af 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -31,7 +31,7 @@ import type { LaunchOptions } from '../../../../playwright-core/src/client/types import type { ClientInfo } from '../sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { - if (config.sharedHttpContext) + if (config.sharedBrowserContext) return SharedContextFactory.create(config); if (config.browser.remoteEndpoint) return new RemoteContextFactory(config); @@ -270,7 +270,7 @@ export class SharedContextFactory implements BrowserContextFactory { static create(config: FullConfig) { if (SharedContextFactory._instance) throw new Error('SharedContextFactory already exists'); - const baseConfig = { ...config, sharedHttpContext: false }; + const baseConfig = { ...config, sharedBrowserContext: false }; const baseFactory = contextFactory(baseConfig); SharedContextFactory._instance = new SharedContextFactory(baseFactory); return SharedContextFactory._instance; diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 67524a504d0ea..b41d07f585452 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -52,7 +52,7 @@ export type CLIOptions = { saveSession?: boolean; saveTrace?: boolean; secrets?: Record; - sharedHttpContext?: boolean; + sharedBrowserContext?: boolean; storageState?: string; timeoutAction?: number; timeoutNavigation?: number; @@ -213,7 +213,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { saveSession: cliOptions.saveSession, saveTrace: cliOptions.saveTrace, secrets: cliOptions.secrets, - sharedHttpContext: cliOptions.sharedHttpContext, + sharedBrowserContext: cliOptions.sharedBrowserContext, outputDir: cliOptions.outputDir, imageResponses: cliOptions.imageResponses, timeouts: { diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index e244671d0915d..aed8c4745b8ec 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -103,7 +103,7 @@ export type Config = { /** * Reuse the same browser context between all connected HTTP clients. */ - sharedHttpContext?: boolean; + sharedBrowserContext?: boolean; /** * Secrets are used to prevent LLM from getting sensitive data while diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 0802516bca34a..852f4b5a75851 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -53,7 +53,7 @@ export function decorateCommand(command: Command, version: string) { .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) - .option('--shared-http-context', 'reuse the same browser context between all connected HTTP clients.') + .option('--shared-browser-context', 'reuse the same browser context between all connected HTTP clients.') .option('--storage-state ', 'path to the storage state file for isolated sessions.') .option('--timeout-action ', 'specify action timeout in milliseconds, defaults to 5000ms', numberParser) .option('--timeout-navigation ', 'specify navigation timeout in milliseconds, defaults to 60000ms', numberParser) diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index c0ab9d7e1054b..ef1ade0d77a02 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -249,7 +249,7 @@ test('http transport browser lifecycle (persistent, multiclient)', async ({ serv }); test('http transport shared context', async ({ serverEndpoint, server }) => { - const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-http-context'] }); + const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-browser-context'] }); // Create first client and navigate const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index eec13f1e27326..b4cdad45dd9bc 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -234,7 +234,7 @@ test('sse transport browser lifecycle (persistent, multiclient)', async ({ serve }); test('sse transport shared context', async ({ serverEndpoint, server }) => { - const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-http-context'] }); + const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-browser-context'] }); // Create first client and navigate const transport1 = new SSEClientTransport(new URL('/sse', url)); From df6e3f4b267ea0bc2b390954c87d5da9ac8f01cf Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 Sep 2025 14:06:48 -0700 Subject: [PATCH 5/5] fix tests on windows --- tests/mcp/http.spec.ts | 13 +++++++------ tests/mcp/sse.spec.ts | 12 +++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index ef1ade0d77a02..458b2e6c6e0ef 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -306,12 +306,13 @@ test('http transport shared context', async ({ serverEndpoint, server }) => { kill(); - await expect(async () => { - const lines = stderr().split('\n'); - // Context should only close when the server shuts down. - expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); - }).toPass(); - + if (process.platform !== 'win32') { + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); + } }); test('http transport (default)', async ({ serverEndpoint }) => { diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index b4cdad45dd9bc..c214e44157724 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -289,9 +289,11 @@ test('sse transport shared context', async ({ serverEndpoint, server }) => { kill(); - await expect(async () => { - const lines = stderr().split('\n'); - // Context should only close when the server shuts down. - expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); - }).toPass(); + if (process.platform !== 'win32') { + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); + } });