@@ -24,7 +24,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures';
2424import type { Config } from '../../packages/playwright/src/mcp/config' ;
2525import { ListRootsRequestSchema } from 'packages/playwright/lib/mcp/sdk/bundle' ;
2626
27- const test = baseTest . extend < { serverEndpoint : ( options ?: { args ?: string [ ] , noPort ?: boolean } ) => Promise < { url : URL , stderr : ( ) => string } > } > ( {
27+ const test = baseTest . extend < { serverEndpoint : ( options ?: { args ?: string [ ] , noPort ?: boolean } ) => Promise < { url : URL , stderr : ( ) => string , kill : ( ) => void } > } > ( {
2828 serverEndpoint : async ( { mcpHeadless } , use , testInfo ) => {
2929 let cp : ChildProcess | undefined ;
3030 const userDataDir = testInfo . outputPath ( 'user-data-dir' ) ;
@@ -55,7 +55,10 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP
5555 resolve ( match [ 1 ] ) ;
5656 } ) ) ;
5757
58- return { url : new URL ( url ) , stderr : ( ) => stderr } ;
58+ return { url : new URL ( url ) , stderr : ( ) => stderr , kill : ( ) => {
59+ cp ?. kill ( 'SIGTERM' ) ;
60+ cp = undefined ;
61+ } } ;
5962 } ) ;
6063 cp ?. kill ( 'SIGTERM' ) ;
6164 } ,
@@ -245,6 +248,73 @@ test('http transport browser lifecycle (persistent, multiclient)', async ({ serv
245248 await client2 . close ( ) ;
246249} ) ;
247250
251+ test ( 'http transport shared context' , async ( { serverEndpoint, server } ) => {
252+ const { url, stderr, kill } = await serverEndpoint ( { args : [ '--shared-browser-context' ] } ) ;
253+
254+ // Create first client and navigate
255+ const transport1 = new StreamableHTTPClientTransport ( new URL ( '/mcp' , url ) ) ;
256+ const client1 = new Client ( { name : 'test1' , version : '1.0.0' } ) ;
257+ await client1 . connect ( transport1 ) ;
258+ await client1 . callTool ( {
259+ name : 'browser_navigate' ,
260+ arguments : { url : server . HELLO_WORLD } ,
261+ } ) ;
262+
263+ // Create second client - should reuse the same browser context
264+ const transport2 = new StreamableHTTPClientTransport ( new URL ( '/mcp' , url ) ) ;
265+ const client2 = new Client ( { name : 'test2' , version : '1.0.0' } ) ;
266+ await client2 . connect ( transport2 ) ;
267+
268+ // Get tabs from second client - should see the tab created by first client
269+ const tabsResult = await client2 . callTool ( {
270+ name : 'browser_tabs' ,
271+ arguments : { action : 'list' } ,
272+ } ) ;
273+
274+ // Should have at least one tab (the one created by client1)
275+ expect ( tabsResult . content [ 0 ] ?. text ) . toContain ( 'tabs' ) ;
276+
277+ await transport1 . terminateSession ( ) ;
278+ await client1 . close ( ) ;
279+
280+ // Second client should still work since context is shared
281+ await client2 . callTool ( {
282+ name : 'browser_snapshot' ,
283+ arguments : { } ,
284+ } ) ;
285+
286+ await transport2 . terminateSession ( ) ;
287+ await client2 . close ( ) ;
288+
289+ await expect ( async ( ) => {
290+ const lines = stderr ( ) . split ( '\n' ) ;
291+ expect ( lines . filter ( line => line . match ( / c r e a t e h t t p s e s s i o n / ) ) . length ) . toBe ( 2 ) ;
292+ expect ( lines . filter ( line => line . match ( / d e l e t e h t t p s e s s i o n / ) ) . length ) . toBe ( 2 ) ;
293+
294+ // Should have only one context creation since it's shared
295+ expect ( lines . filter ( line => line . match ( / c r e a t e s h a r e d b r o w s e r c o n t e x t / ) ) . length ) . toBe ( 1 ) ;
296+
297+ // Should see client connect/disconnect messages
298+ expect ( lines . filter ( line => line . match ( / s h a r e d c o n t e x t c l i e n t c o n n e c t e d / ) ) . length ) . toBe ( 2 ) ;
299+ expect ( lines . filter ( line => line . match ( / s h a r e d c o n t e x t c l i e n t d i s c o n n e c t e d / ) ) . length ) . toBe ( 2 ) ;
300+ expect ( lines . filter ( line => line . match ( / c r e a t e c o n t e x t / ) ) . length ) . toBe ( 2 ) ;
301+ expect ( lines . filter ( line => line . match ( / c l o s e c o n t e x t / ) ) . length ) . toBe ( 2 ) ;
302+
303+ // Context should only close when the server shuts down.
304+ expect ( lines . filter ( line => line . match ( / c l o s e b r o w s e r c o n t e x t c o m p l e t e \( p e r s i s t e n t \) / ) ) . length ) . toBe ( 0 ) ;
305+ } ) . toPass ( ) ;
306+
307+ kill ( ) ;
308+
309+ if ( process . platform !== 'win32' ) {
310+ await expect ( async ( ) => {
311+ const lines = stderr ( ) . split ( '\n' ) ;
312+ // Context should only close when the server shuts down.
313+ expect ( lines . filter ( line => line . match ( / c l o s e b r o w s e r c o n t e x t c o m p l e t e \( p e r s i s t e n t \) / ) ) . length ) . toBe ( 1 ) ;
314+ } ) . toPass ( ) ;
315+ }
316+ } ) ;
317+
248318test ( 'http transport (default)' , async ( { serverEndpoint } ) => {
249319 const { url } = await serverEndpoint ( ) ;
250320 const transport = new StreamableHTTPClientTransport ( url ) ;
0 commit comments