Skip to content

Commit f5ad237

Browse files
committed
feat: wire session tools into MCP server registration
Integrate SessionManager into main.ts: - Extract getBrowser() from getContext() for lazy browser resolution - Session tools (create/list/close) are intercepted and handled directly by SessionManager, bypassing placeholder handlers - All non-session tools get an optional sessionId parameter that routes execution to the session's isolated context and mutex - Default context behavior preserved when sessionId is omitted - All telemetry (ClearcutLogger) remains intact
1 parent 5bbfa72 commit f5ad237

2 files changed

Lines changed: 145 additions & 41 deletions

File tree

package-lock.json

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 132 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import {logger, saveLogsToFile} from './logger.js';
1616
import {McpContext} from './McpContext.js';
1717
import {McpResponse} from './McpResponse.js';
1818
import {Mutex} from './Mutex.js';
19+
import {SessionManager} from './SessionManager.js';
1920
import {ClearcutLogger} from './telemetry/ClearcutLogger.js';
2021
import {computeFlagUsage} from './telemetry/flagUtils.js';
2122
import {bucketizeLatency} from './telemetry/metricUtils.js';
2223
import {
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+
124142
const 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

146164
const 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+
148215
function 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

Comments
 (0)