Skip to content

Commit 1b2053c

Browse files
committed
refactor: replace isolatedContext routing with pageId routing
Replace the isolatedContext-based page resolution with the more general pageId parameter. The isolatedContext approach only worked when agents used different browser contexts. pageId works for any multi-page scenario, uses an already-existing concept (page IDs), and does not depend on isolated contexts. - Rename isolatedContextSchema to pageIdSchema (string to number) - Replace resolvePageByContext() with resolvePageById() in McpContext - Remove per-context page tracking (#contextSelectedPage map) - Update all tool files to use pageId routing - Keep isolatedContext on new_page (browser context isolation, not routing) - Update tests to use pageId-based assertions
1 parent 63c7c90 commit 1b2053c

14 files changed

Lines changed: 77 additions & 295 deletions

src/McpContext.ts

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,6 @@ export class McpContext implements Context {
131131
#pages: Page[] = [];
132132
#pageToDevToolsPage = new Map<Page, Page>();
133133
#selectedPage?: Page;
134-
// Per-context selected page tracking for parallel agent support.
135-
#contextSelectedPage = new Map<string, Page>();
136134
#textSnapshot: TextSnapshot | null = null;
137135
#networkCollector: NetworkCollector;
138136
#consoleCollector: ConsoleCollector;
@@ -505,39 +503,11 @@ export class McpContext implements Context {
505503
return page;
506504
}
507505

508-
resolvePageByContext(isolatedContext?: string): Page {
509-
if (isolatedContext === undefined) {
506+
resolvePageById(pageId?: number): Page {
507+
if (pageId === undefined) {
510508
return this.getSelectedPage();
511509
}
512-
513-
// Try the per-context selected page first.
514-
const tracked = this.#contextSelectedPage.get(isolatedContext);
515-
if (tracked && !tracked.isClosed()) {
516-
return tracked;
517-
}
518-
519-
// Fall back: find any non-closed page in the context.
520-
const ctx = this.#isolatedContexts.get(isolatedContext);
521-
if (!ctx) {
522-
throw new Error(
523-
`No isolated context named "${isolatedContext}" exists. ` +
524-
`Create one first with new_page(isolatedContext: "${isolatedContext}").`,
525-
);
526-
}
527-
528-
for (const page of this.#pages) {
529-
if (
530-
!page.isClosed() &&
531-
this.#pageToIsolatedContextName.get(page) === isolatedContext
532-
) {
533-
this.#contextSelectedPage.set(isolatedContext, page);
534-
return page;
535-
}
536-
}
537-
538-
throw new Error(
539-
`No open page found in isolated context "${isolatedContext}".`,
540-
);
510+
return this.getPageById(pageId);
541511
}
542512

543513
getPageById(pageId: number): Page {
@@ -574,12 +544,6 @@ export class McpContext implements Context {
574544
void newPage.emulateFocusedPage(true).catch(error => {
575545
this.logger('Error turning on focused page emulation', error);
576546
});
577-
578-
// Track per-context selected page for parallel agent routing.
579-
const contextName = this.#pageToIsolatedContextName.get(newPage);
580-
if (contextName) {
581-
this.#contextSelectedPage.set(contextName, newPage);
582-
}
583547
}
584548

585549
#updateSelectedPageTimeouts() {

src/tools/ToolDefinition.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export type Context = Readonly<{
108108
recordedTraces(): TraceResult[];
109109
storeTraceRecording(result: TraceResult): void;
110110
getSelectedPage(): Page;
111-
resolvePageByContext(isolatedContext?: string): Page;
111+
resolvePageById(pageId?: number): Page;
112112
getDialog(): Dialog | undefined;
113113
clearDialog(): void;
114114
getPageById(pageId: number): Page;
@@ -182,15 +182,13 @@ export function defineTool<Schema extends zod.ZodRawShape>(
182182
export const CLOSE_PAGE_ERROR =
183183
'The last open page cannot be closed. It is fine to keep it open.';
184184

185-
export const isolatedContextSchema = {
186-
isolatedContext: zod
187-
.string()
185+
export const pageIdSchema = {
186+
pageId: zod
187+
.number()
188188
.optional()
189189
.describe(
190-
'The name of the isolated browser context to resolve the page from. ' +
191-
'When provided, the tool operates on the page belonging to this context ' +
192-
'instead of the globally selected page. ' +
193-
'Use this to avoid race conditions when multiple agents work in parallel.',
190+
'The ID of the page to operate on. When provided, the tool targets this specific page ' +
191+
'instead of the globally selected page.',
194192
),
195193
};
196194

src/tools/emulation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import {zod, PredefinedNetworkConditions} from '../third_party/index.js';
99

1010
import {ToolCategory} from './categories.js';
11-
import {defineTool, isolatedContextSchema} from './ToolDefinition.js';
11+
import {defineTool, pageIdSchema} from './ToolDefinition.js';
1212

1313
const throttlingOptions: [string, ...string[]] = [
1414
'No emulation',
@@ -24,7 +24,7 @@ export const emulate = defineTool({
2424
readOnlyHint: false,
2525
},
2626
schema: {
27-
...isolatedContextSchema,
27+
...pageIdSchema,
2828
networkConditions: zod
2929
.enum(throttlingOptions)
3030
.optional()
@@ -105,8 +105,8 @@ export const emulate = defineTool({
105105
),
106106
},
107107
handler: async (request, _response, context) => {
108-
const page = context.resolvePageByContext(
109-
request.params.isolatedContext,
108+
const page = context.resolvePageById(
109+
request.params.pageId,
110110
);
111111
await context.emulate(request.params, page);
112112
},

src/tools/input.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {ElementHandle, Page} from '../third_party/index.js';
1111
import {parseKey} from '../utils/keyboard.js';
1212

1313
import {ToolCategory} from './categories.js';
14-
import {defineTool, isolatedContextSchema} from './ToolDefinition.js';
14+
import {defineTool, pageIdSchema} from './ToolDefinition.js';
1515

1616
const dblClickSchema = zod
1717
.boolean()
@@ -83,15 +83,15 @@ export const clickAt = defineTool({
8383
conditions: ['computerVision'],
8484
},
8585
schema: {
86-
...isolatedContextSchema,
86+
...pageIdSchema,
8787
x: zod.number().describe('The x coordinate'),
8888
y: zod.number().describe('The y coordinate'),
8989
dblClick: dblClickSchema,
9090
includeSnapshot: includeSnapshotSchema,
9191
},
9292
handler: async (request, response, context) => {
93-
const page = context.resolvePageByContext(
94-
request.params.isolatedContext,
93+
const page = context.resolvePageById(
94+
request.params.pageId,
9595
);
9696
await context.waitForEventsAfterAction(async () => {
9797
await page.mouse.click(request.params.x, request.params.y, {
@@ -221,7 +221,7 @@ export const fill = defineTool({
221221
readOnlyHint: false,
222222
},
223223
schema: {
224-
...isolatedContextSchema,
224+
...pageIdSchema,
225225
uid: zod
226226
.string()
227227
.describe(
@@ -231,8 +231,8 @@ export const fill = defineTool({
231231
includeSnapshot: includeSnapshotSchema,
232232
},
233233
handler: async (request, response, context) => {
234-
const page = context.resolvePageByContext(
235-
request.params.isolatedContext,
234+
const page = context.resolvePageById(
235+
request.params.pageId,
236236
);
237237
await context.waitForEventsAfterAction(async () => {
238238
await fillFormElement(
@@ -289,7 +289,7 @@ export const fillForm = defineTool({
289289
readOnlyHint: false,
290290
},
291291
schema: {
292-
...isolatedContextSchema,
292+
...pageIdSchema,
293293
elements: zod
294294
.array(
295295
zod.object({
@@ -301,8 +301,8 @@ export const fillForm = defineTool({
301301
includeSnapshot: includeSnapshotSchema,
302302
},
303303
handler: async (request, response, context) => {
304-
const page = context.resolvePageByContext(
305-
request.params.isolatedContext,
304+
const page = context.resolvePageById(
305+
request.params.pageId,
306306
);
307307
for (const element of request.params.elements) {
308308
await context.waitForEventsAfterAction(async () => {
@@ -329,7 +329,7 @@ export const uploadFile = defineTool({
329329
readOnlyHint: false,
330330
},
331331
schema: {
332-
...isolatedContextSchema,
332+
...pageIdSchema,
333333
uid: zod
334334
.string()
335335
.describe(
@@ -351,9 +351,7 @@ export const uploadFile = defineTool({
351351
// a type=file element. In this case, we want to default to
352352
// Page.waitForFileChooser() and upload the file this way.
353353
try {
354-
const page = context.resolvePageByContext(
355-
request.params.isolatedContext,
356-
);
354+
const page = context.resolvePageById(request.params.pageId);
357355
const [fileChooser] = await Promise.all([
358356
page.waitForFileChooser({timeout: 3000}),
359357
handle.asLocator().click(),
@@ -366,9 +364,7 @@ export const uploadFile = defineTool({
366364
}
367365
}
368366
if (request.params.includeSnapshot) {
369-
const page = context.resolvePageByContext(
370-
request.params.isolatedContext,
371-
);
367+
const page = context.resolvePageById(request.params.pageId);
372368
response.includeSnapshot({ page });
373369
}
374370
response.appendResponseLine(`File uploaded from ${filePath}.`);
@@ -386,7 +382,7 @@ export const pressKey = defineTool({
386382
readOnlyHint: false,
387383
},
388384
schema: {
389-
...isolatedContextSchema,
385+
...pageIdSchema,
390386
key: zod
391387
.string()
392388
.describe(
@@ -395,8 +391,8 @@ export const pressKey = defineTool({
395391
includeSnapshot: includeSnapshotSchema,
396392
},
397393
handler: async (request, response, context) => {
398-
const page = context.resolvePageByContext(
399-
request.params.isolatedContext,
394+
const page = context.resolvePageById(
395+
request.params.pageId,
400396
);
401397
const tokens = parseKey(request.params.key);
402398
const [key, ...modifiers] = tokens;

src/tools/pages.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {ToolCategory} from './categories.js';
1212
import {
1313
CLOSE_PAGE_ERROR,
1414
defineTool,
15-
isolatedContextSchema,
15+
pageIdSchema,
1616
timeoutSchema,
1717
} from './ToolDefinition.js';
1818

@@ -135,7 +135,7 @@ export const navigatePage = defineTool({
135135
readOnlyHint: false,
136136
},
137137
schema: {
138-
...isolatedContextSchema,
138+
...pageIdSchema,
139139
type: zod
140140
.enum(['url', 'back', 'forward', 'reload'])
141141
.optional()
@@ -162,8 +162,8 @@ export const navigatePage = defineTool({
162162
...timeoutSchema,
163163
},
164164
handler: async (request, response, context) => {
165-
const page = context.resolvePageByContext(
166-
request.params.isolatedContext,
165+
const page = context.resolvePageById(
166+
request.params.pageId,
167167
);
168168
const options = {
169169
timeout: request.params.timeout,
@@ -287,13 +287,13 @@ export const resizePage = defineTool({
287287
readOnlyHint: false,
288288
},
289289
schema: {
290-
...isolatedContextSchema,
290+
...pageIdSchema,
291291
width: zod.number().describe('Page width'),
292292
height: zod.number().describe('Page height'),
293293
},
294294
handler: async (request, response, context) => {
295-
const page = context.resolvePageByContext(
296-
request.params.isolatedContext,
295+
const page = context.resolvePageById(
296+
request.params.pageId,
297297
);
298298

299299
try {

src/tools/performance.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717

1818
import {ToolCategory} from './categories.js';
1919
import type {Context, Response} from './ToolDefinition.js';
20-
import {defineTool, isolatedContextSchema} from './ToolDefinition.js';
20+
import {defineTool, pageIdSchema} from './ToolDefinition.js';
2121

2222
const filePathSchema = zod
2323
.string()
@@ -34,7 +34,7 @@ export const startTrace = defineTool({
3434
readOnlyHint: false,
3535
},
3636
schema: {
37-
...isolatedContextSchema,
37+
...pageIdSchema,
3838
reload: zod
3939
.boolean()
4040
.describe(
@@ -56,8 +56,8 @@ export const startTrace = defineTool({
5656
}
5757
context.setIsRunningPerformanceTrace(true);
5858

59-
const page = context.resolvePageByContext(
60-
request.params.isolatedContext,
59+
const page = context.resolvePageById(
60+
request.params.pageId,
6161
);
6262
const pageUrlForTracing = page.url();
6363

@@ -124,15 +124,15 @@ export const stopTrace = defineTool({
124124
readOnlyHint: false,
125125
},
126126
schema: {
127-
...isolatedContextSchema,
127+
...pageIdSchema,
128128
filePath: filePathSchema,
129129
},
130130
handler: async (request, response, context) => {
131131
if (!context.isRunningPerformanceTrace()) {
132132
return;
133133
}
134-
const page = context.resolvePageByContext(
135-
request.params.isolatedContext,
134+
const page = context.resolvePageById(
135+
request.params.pageId,
136136
);
137137
await stopTracingAndAppendOutput(
138138
page,

src/tools/screencast.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {zod} from '../third_party/index.js';
1212
import type {ScreenRecorder} from '../third_party/index.js';
1313

1414
import {ToolCategory} from './categories.js';
15-
import {defineTool, isolatedContextSchema} from './ToolDefinition.js';
15+
import {defineTool, pageIdSchema} from './ToolDefinition.js';
1616

1717
async function generateTempFilePath(): Promise<string> {
1818
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
@@ -29,7 +29,7 @@ export const startScreencast = defineTool({
2929
conditions: ['screencast'],
3030
},
3131
schema: {
32-
...isolatedContextSchema,
32+
...pageIdSchema,
3333
path: zod
3434
.string()
3535
.optional()
@@ -48,8 +48,8 @@ export const startScreencast = defineTool({
4848
const filePath = request.params.path ?? (await generateTempFilePath());
4949
const resolvedPath = path.resolve(filePath);
5050

51-
const page = context.resolvePageByContext(
52-
request.params.isolatedContext,
51+
const page = context.resolvePageById(
52+
request.params.pageId,
5353
);
5454

5555
let recorder: ScreenRecorder;

src/tools/screenshot.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {zod} from '../third_party/index.js';
88
import type {ElementHandle, Page} from '../third_party/index.js';
99

1010
import {ToolCategory} from './categories.js';
11-
import {defineTool, isolatedContextSchema} from './ToolDefinition.js';
11+
import {defineTool, pageIdSchema} from './ToolDefinition.js';
1212

1313
export const screenshot = defineTool({
1414
name: 'take_screenshot',
@@ -19,7 +19,7 @@ export const screenshot = defineTool({
1919
readOnlyHint: false,
2020
},
2121
schema: {
22-
...isolatedContextSchema,
22+
...pageIdSchema,
2323
format: zod
2424
.enum(['png', 'jpeg', 'webp'])
2525
.default('png')
@@ -60,8 +60,8 @@ export const screenshot = defineTool({
6060
if (request.params.uid) {
6161
pageOrHandle = await context.getElementByUid(request.params.uid);
6262
} else {
63-
pageOrHandle = context.resolvePageByContext(
64-
request.params.isolatedContext,
63+
pageOrHandle = context.resolvePageById(
64+
request.params.pageId,
6565
);
6666
}
6767

0 commit comments

Comments
 (0)