@@ -16,12 +16,14 @@ import {logger, saveLogsToFile} from './logger.js';
1616import { McpContext } from './McpContext.js' ;
1717import { McpResponse } from './McpResponse.js' ;
1818import { Mutex } from './Mutex.js' ;
19+ import { SessionManager } from './SessionManager.js' ;
1920import { ClearcutLogger } from './telemetry/ClearcutLogger.js' ;
2021import { computeFlagUsage } from './telemetry/flagUtils.js' ;
2122import { bucketizeLatency } from './telemetry/metricUtils.js' ;
2223import {
2324 McpServer ,
2425 StdioServerTransport ,
26+ zod ,
2527 type CallToolResult ,
2628 SetLevelRequestSchema ,
2729} from './third_party/index.js' ;
@@ -75,52 +77,68 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
7577 return { } ;
7678} ) ;
7779
78- let context : McpContext ;
79- async function getContext ( ) : Promise < McpContext > {
80+ const devtools = args . experimentalDevtools ?? false ;
81+ const mcpContextOptions = {
82+ experimentalDevToolsDebugging : devtools ,
83+ experimentalIncludeAllPages : args . experimentalIncludeAllPages ,
84+ performanceCrux : args . performanceCrux ,
85+ } ;
86+
87+ async function getBrowser ( ) {
8088 const chromeArgs : string [ ] = ( args . chromeArg ?? [ ] ) . map ( String ) ;
8189 const ignoreDefaultChromeArgs : string [ ] = (
8290 args . ignoreDefaultChromeArg ?? [ ]
8391 ) . map ( String ) ;
8492 if ( args . proxyServer ) {
8593 chromeArgs . push ( `--proxy-server=${ args . proxyServer } ` ) ;
8694 }
87- const devtools = args . experimentalDevtools ?? false ;
88- const browser =
89- args . browserUrl || args . wsEndpoint || args . autoConnect
90- ? await ensureBrowserConnected ( {
91- browserURL : args . browserUrl ,
92- wsEndpoint : args . wsEndpoint ,
93- wsHeaders : args . wsHeaders ,
94- // Important: only pass channel, if autoConnect is true.
95- channel : args . autoConnect ? ( args . channel as Channel ) : undefined ,
96- userDataDir : args . userDataDir ,
97- devtools,
98- } )
99- : await ensureBrowserLaunched ( {
100- headless : args . headless ,
101- executablePath : args . executablePath ,
102- channel : args . channel as Channel ,
103- isolated : args . isolated ?? false ,
104- userDataDir : args . userDataDir ,
105- logFile,
106- viewport : args . viewport ,
107- chromeArgs,
108- ignoreDefaultChromeArgs,
109- acceptInsecureCerts : args . acceptInsecureCerts ,
110- devtools,
111- enableExtensions : args . categoryExtensions ,
112- } ) ;
95+ return args . browserUrl || args . wsEndpoint || args . autoConnect
96+ ? await ensureBrowserConnected ( {
97+ browserURL : args . browserUrl ,
98+ wsEndpoint : args . wsEndpoint ,
99+ wsHeaders : args . wsHeaders ,
100+ // Important: only pass channel, if autoConnect is true.
101+ channel : args . autoConnect ? ( args . channel as Channel ) : undefined ,
102+ userDataDir : args . userDataDir ,
103+ devtools,
104+ } )
105+ : await ensureBrowserLaunched ( {
106+ headless : args . headless ,
107+ executablePath : args . executablePath ,
108+ channel : args . channel as Channel ,
109+ isolated : args . isolated ?? false ,
110+ userDataDir : args . userDataDir ,
111+ logFile,
112+ viewport : args . viewport ,
113+ chromeArgs,
114+ ignoreDefaultChromeArgs,
115+ acceptInsecureCerts : args . acceptInsecureCerts ,
116+ devtools,
117+ enableExtensions : args . categoryExtensions ,
118+ } ) ;
119+ }
113120
121+ let context : McpContext ;
122+ async function getContext ( ) : Promise < McpContext > {
123+ const browser = await getBrowser ( ) ;
114124 if ( context ?. browser !== browser ) {
115- context = await McpContext . from ( browser , logger , {
116- experimentalDevToolsDebugging : devtools ,
117- experimentalIncludeAllPages : args . experimentalIncludeAllPages ,
118- performanceCrux : args . performanceCrux ,
119- } ) ;
125+ context = await McpContext . from ( browser , logger , mcpContextOptions ) ;
120126 }
121127 return context ;
122128}
123129
130+ const sessionManager = new SessionManager (
131+ getBrowser ,
132+ logger ,
133+ mcpContextOptions ,
134+ ) ;
135+
136+ const SESSION_TOOL_NAMES = new Set ( [
137+ 'create_session' ,
138+ 'list_sessions' ,
139+ 'close_session' ,
140+ ] ) ;
141+
124142const logDisclaimers = ( ) => {
125143 console . error (
126144 `chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
@@ -145,6 +163,55 @@ For more details, visit: https://github.com/ChromeDevTools/chrome-devtools-mcp#u
145163
146164const toolMutex = new Mutex ( ) ;
147165
166+ function registerSessionTool ( tool : ToolDefinition ) : void {
167+ server . registerTool (
168+ tool . name ,
169+ {
170+ description : tool . description ,
171+ inputSchema : tool . schema ,
172+ annotations : tool . annotations ,
173+ } ,
174+ async ( params ) : Promise < CallToolResult > => {
175+ const startTime = Date . now ( ) ;
176+ let success = false ;
177+ try {
178+ logger ( `${ tool . name } request: ${ JSON . stringify ( params , null , ' ' ) } ` ) ;
179+ let text : string ;
180+ if ( tool . name === 'create_session' ) {
181+ const info = await sessionManager . createSession ( {
182+ label : params . label as string | undefined ,
183+ url : params . url as string | undefined ,
184+ } ) ;
185+ text = JSON . stringify ( info , null , 2 ) ;
186+ } else if ( tool . name === 'list_sessions' ) {
187+ const sessions = sessionManager . listSessions ( ) ;
188+ text = JSON . stringify ( sessions , null , 2 ) ;
189+ } else if ( tool . name === 'close_session' ) {
190+ await sessionManager . closeSession ( params . sessionId as string ) ;
191+ text = `Session "${ params . sessionId } " closed.` ;
192+ } else {
193+ throw new Error ( `Unknown session tool: ${ tool . name } ` ) ;
194+ }
195+ success = true ;
196+ return { content : [ { type : 'text' , text} ] } ;
197+ } catch ( err ) {
198+ logger ( `${ tool . name } error:` , err , err ?. stack ) ;
199+ const errorText = err && 'message' in err ? err . message : String ( err ) ;
200+ return {
201+ content : [ { type : 'text' , text : errorText } ] ,
202+ isError : true ,
203+ } ;
204+ } finally {
205+ void clearcutLogger ?. logToolInvocation ( {
206+ toolName : tool . name ,
207+ success,
208+ latencyMs : bucketizeLatency ( Date . now ( ) - startTime ) ,
209+ } ) ;
210+ }
211+ } ,
212+ ) ;
213+ }
214+
148215function registerTool ( tool : ToolDefinition ) : void {
149216 if (
150217 tool . annotations . category === ToolCategory . EMULATION &&
@@ -182,33 +249,58 @@ function registerTool(tool: ToolDefinition): void {
182249 ) {
183250 return ;
184251 }
252+
253+ // Session tools get special handling via SessionManager.
254+ if ( SESSION_TOOL_NAMES . has ( tool . name ) ) {
255+ registerSessionTool ( tool ) ;
256+ return ;
257+ }
258+
259+ // Non-session tools get an optional sessionId parameter.
260+ const schemaWithSession = {
261+ ...tool . schema ,
262+ sessionId : zod
263+ . string ( )
264+ . optional ( )
265+ . describe (
266+ 'Session ID from create_session. Routes this tool to an isolated BrowserContext.' ,
267+ ) ,
268+ } ;
269+
185270 server . registerTool (
186271 tool . name ,
187272 {
188273 description : tool . description ,
189- inputSchema : tool . schema ,
274+ inputSchema : schemaWithSession ,
190275 annotations : tool . annotations ,
191276 } ,
192277 async ( params ) : Promise < CallToolResult > => {
193- const guard = await toolMutex . acquire ( ) ;
278+ const sessionId = params . sessionId as string | undefined ;
279+ const session = sessionId
280+ ? sessionManager . getSession ( sessionId )
281+ : undefined ;
282+ const activeMutex = session ?. mutex ?? toolMutex ;
283+ const guard = await activeMutex . acquire ( ) ;
194284 const startTime = Date . now ( ) ;
195285 let success = false ;
196286 try {
197287 logger ( `${ tool . name } request: ${ JSON . stringify ( params , null , ' ' ) } ` ) ;
198- const context = await getContext ( ) ;
199- logger ( `${ tool . name } context: resolved` ) ;
200- await context . detectOpenDevToolsWindows ( ) ;
288+ const activeContext = session ? session . context : await getContext ( ) ;
289+ logger (
290+ `${ tool . name } context: ${ sessionId ? `session ${ sessionId } ` : 'default' } ` ,
291+ ) ;
292+ await activeContext . detectOpenDevToolsWindows ( ) ;
201293 const response = new McpResponse ( ) ;
202294 await tool . handler (
203295 {
204296 params,
205297 } ,
206298 response ,
207- context ,
299+ activeContext ,
208300 ) ;
209301 const { content, structuredContent} = await response . handle (
210302 tool . name ,
211- context ,
303+ activeContext ,
212304 ) ;
213305 const result : CallToolResult & {
214306 structuredContent ?: Record < string , unknown > ;
0 commit comments