Skip to content

Commit e548822

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 8487a20 commit e548822

14 files changed

Lines changed: 70 additions & 327 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: 3 additions & 5 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,9 +105,7 @@ export const emulate = defineTool({
105105
),
106106
},
107107
handler: async (request, _response, context) => {
108-
const page = context.resolvePageByContext(
109-
request.params.isolatedContext,
110-
);
108+
const page = context.resolvePageById(request.params.pageId);
111109
await context.emulate(request.params, page);
112110
},
113111
});

src/tools/input.ts

Lines changed: 18 additions & 31 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,16 +83,14 @@ 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,
95-
);
93+
const page = context.resolvePageById(request.params.pageId);
9694
await context.waitForEventsAfterAction(async () => {
9795
await page.mouse.click(request.params.x, request.params.y, {
9896
clickCount: request.params.dblClick ? 2 : 1,
@@ -104,7 +102,7 @@ export const clickAt = defineTool({
104102
: `Successfully clicked at the coordinates`,
105103
);
106104
if (request.params.includeSnapshot) {
107-
response.includeSnapshot({ page });
105+
response.includeSnapshot({page});
108106
}
109107
},
110108
});
@@ -202,8 +200,7 @@ async function fillFormElement(
202200
const timeoutPerChar = 10; // ms
203201
const targetPage = page ?? context.getSelectedPage();
204202
const fillTimeout =
205-
targetPage.getDefaultTimeout() +
206-
value.length * timeoutPerChar;
203+
targetPage.getDefaultTimeout() + value.length * timeoutPerChar;
207204
await handle.asLocator().setTimeout(fillTimeout).fill(value);
208205
}
209206
} catch (error) {
@@ -221,7 +218,7 @@ export const fill = defineTool({
221218
readOnlyHint: false,
222219
},
223220
schema: {
224-
...isolatedContextSchema,
221+
...pageIdSchema,
225222
uid: zod
226223
.string()
227224
.describe(
@@ -231,9 +228,7 @@ export const fill = defineTool({
231228
includeSnapshot: includeSnapshotSchema,
232229
},
233230
handler: async (request, response, context) => {
234-
const page = context.resolvePageByContext(
235-
request.params.isolatedContext,
236-
);
231+
const page = context.resolvePageById(request.params.pageId);
237232
await context.waitForEventsAfterAction(async () => {
238233
await fillFormElement(
239234
request.params.uid,
@@ -244,7 +239,7 @@ export const fill = defineTool({
244239
});
245240
response.appendResponseLine(`Successfully filled out the element`);
246241
if (request.params.includeSnapshot) {
247-
response.includeSnapshot({ page });
242+
response.includeSnapshot({page});
248243
}
249244
},
250245
});
@@ -289,7 +284,7 @@ export const fillForm = defineTool({
289284
readOnlyHint: false,
290285
},
291286
schema: {
292-
...isolatedContextSchema,
287+
...pageIdSchema,
293288
elements: zod
294289
.array(
295290
zod.object({
@@ -301,9 +296,7 @@ export const fillForm = defineTool({
301296
includeSnapshot: includeSnapshotSchema,
302297
},
303298
handler: async (request, response, context) => {
304-
const page = context.resolvePageByContext(
305-
request.params.isolatedContext,
306-
);
299+
const page = context.resolvePageById(request.params.pageId);
307300
for (const element of request.params.elements) {
308301
await context.waitForEventsAfterAction(async () => {
309302
await fillFormElement(
@@ -316,7 +309,7 @@ export const fillForm = defineTool({
316309
}
317310
response.appendResponseLine(`Successfully filled out the form`);
318311
if (request.params.includeSnapshot) {
319-
response.includeSnapshot({ page });
312+
response.includeSnapshot({page});
320313
}
321314
},
322315
});
@@ -329,7 +322,7 @@ export const uploadFile = defineTool({
329322
readOnlyHint: false,
330323
},
331324
schema: {
332-
...isolatedContextSchema,
325+
...pageIdSchema,
333326
uid: zod
334327
.string()
335328
.describe(
@@ -351,9 +344,7 @@ export const uploadFile = defineTool({
351344
// a type=file element. In this case, we want to default to
352345
// Page.waitForFileChooser() and upload the file this way.
353346
try {
354-
const page = context.resolvePageByContext(
355-
request.params.isolatedContext,
356-
);
347+
const page = context.resolvePageById(request.params.pageId);
357348
const [fileChooser] = await Promise.all([
358349
page.waitForFileChooser({timeout: 3000}),
359350
handle.asLocator().click(),
@@ -366,10 +357,8 @@ export const uploadFile = defineTool({
366357
}
367358
}
368359
if (request.params.includeSnapshot) {
369-
const page = context.resolvePageByContext(
370-
request.params.isolatedContext,
371-
);
372-
response.includeSnapshot({ page });
360+
const page = context.resolvePageById(request.params.pageId);
361+
response.includeSnapshot({page});
373362
}
374363
response.appendResponseLine(`File uploaded from ${filePath}.`);
375364
} finally {
@@ -386,7 +375,7 @@ export const pressKey = defineTool({
386375
readOnlyHint: false,
387376
},
388377
schema: {
389-
...isolatedContextSchema,
378+
...pageIdSchema,
390379
key: zod
391380
.string()
392381
.describe(
@@ -395,9 +384,7 @@ export const pressKey = defineTool({
395384
includeSnapshot: includeSnapshotSchema,
396385
},
397386
handler: async (request, response, context) => {
398-
const page = context.resolvePageByContext(
399-
request.params.isolatedContext,
400-
);
387+
const page = context.resolvePageById(request.params.pageId);
401388
const tokens = parseKey(request.params.key);
402389
const [key, ...modifiers] = tokens;
403390

@@ -415,7 +402,7 @@ export const pressKey = defineTool({
415402
`Successfully pressed key: ${request.params.key}`,
416403
);
417404
if (request.params.includeSnapshot) {
418-
response.includeSnapshot({ page });
405+
response.includeSnapshot({page});
419406
}
420407
},
421408
});

src/tools/pages.ts

Lines changed: 5 additions & 9 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,9 +162,7 @@ export const navigatePage = defineTool({
162162
...timeoutSchema,
163163
},
164164
handler: async (request, response, context) => {
165-
const page = context.resolvePageByContext(
166-
request.params.isolatedContext,
167-
);
165+
const page = context.resolvePageById(request.params.pageId);
168166
const options = {
169167
timeout: request.params.timeout,
170168
};
@@ -287,14 +285,12 @@ export const resizePage = defineTool({
287285
readOnlyHint: false,
288286
},
289287
schema: {
290-
...isolatedContextSchema,
288+
...pageIdSchema,
291289
width: zod.number().describe('Page width'),
292290
height: zod.number().describe('Page height'),
293291
},
294292
handler: async (request, response, context) => {
295-
const page = context.resolvePageByContext(
296-
request.params.isolatedContext,
297-
);
293+
const page = context.resolvePageById(request.params.pageId);
298294

299295
try {
300296
const browser = page.browser();

src/tools/performance.ts

Lines changed: 5 additions & 9 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,9 +56,7 @@ export const startTrace = defineTool({
5656
}
5757
context.setIsRunningPerformanceTrace(true);
5858

59-
const page = context.resolvePageByContext(
60-
request.params.isolatedContext,
61-
);
59+
const page = context.resolvePageById(request.params.pageId);
6260
const pageUrlForTracing = page.url();
6361

6462
if (request.params.reload) {
@@ -124,16 +122,14 @@ export const stopTrace = defineTool({
124122
readOnlyHint: false,
125123
},
126124
schema: {
127-
...isolatedContextSchema,
125+
...pageIdSchema,
128126
filePath: filePathSchema,
129127
},
130128
handler: async (request, response, context) => {
131129
if (!context.isRunningPerformanceTrace()) {
132130
return;
133131
}
134-
const page = context.resolvePageByContext(
135-
request.params.isolatedContext,
136-
);
132+
const page = context.resolvePageById(request.params.pageId);
137133
await stopTracingAndAppendOutput(
138134
page,
139135
response,

0 commit comments

Comments
 (0)