Skip to content

Commit 6ce038a

Browse files
committed
feat: add isolatedContext routing for parallel agent page resolution
Add optional `isolatedContext` parameter to all page-dependent tools so parallel agents can resolve pages by context name instead of relying on the global selected-page pointer. When an agent creates a page with `new_page(isolatedContext: "my-agent")`, all subsequent tool calls can pass `isolatedContext: "my-agent"` to operate on the correct page without race conditions from other agents calling `select_page` concurrently. McpContext tracks per-context selected pages and resolvePageByContext() looks up the right page by context name. When the parameter is omitted, tools fall back to getSelectedPage() (fully backward compatible). Updated tools: take_screenshot, take_snapshot, wait_for, navigate_page, resize_page, emulate, click_at, fill, fill_form, upload_file, press_key, evaluate_script, performance_start_trace, performance_stop_trace, screencast_start.
1 parent 25638f0 commit 6ce038a

15 files changed

Lines changed: 479 additions & 49 deletions

src/McpContext.ts

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ 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>();
134136
#textSnapshot: TextSnapshot | null = null;
135137
#networkCollector: NetworkCollector;
136138
#consoleCollector: ConsoleCollector;
@@ -315,15 +317,18 @@ export class McpContext implements Context {
315317
return this.#networkCollector.getById(this.getSelectedPage(), reqid);
316318
}
317319

318-
async emulate(options: {
319-
networkConditions?: string | null;
320-
cpuThrottlingRate?: number | null;
321-
geolocation?: GeolocationOptions | null;
322-
userAgent?: string | null;
323-
colorScheme?: 'dark' | 'light' | 'auto' | null;
324-
viewport?: Viewport | null;
325-
}): Promise<void> {
326-
const page = this.getSelectedPage();
320+
async emulate(
321+
options: {
322+
networkConditions?: string | null;
323+
cpuThrottlingRate?: number | null;
324+
geolocation?: GeolocationOptions | null;
325+
userAgent?: string | null;
326+
colorScheme?: 'dark' | 'light' | 'auto' | null;
327+
viewport?: Viewport | null;
328+
},
329+
targetPage?: Page,
330+
): Promise<void> {
331+
const page = targetPage ?? this.getSelectedPage();
327332
const currentSettings = this.#emulationSettingsMap.get(page) ?? {};
328333
const newSettings: EmulationSettings = {...currentSettings};
329334
let timeoutsNeedUpdate = false;
@@ -500,6 +505,41 @@ export class McpContext implements Context {
500505
return page;
501506
}
502507

508+
resolvePageByContext(isolatedContext?: string): Page {
509+
if (isolatedContext === undefined) {
510+
return this.getSelectedPage();
511+
}
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+
);
541+
}
542+
503543
getPageById(pageId: number): Page {
504544
const page = this.#pages.find(p => this.#pageIdMap.get(p) === pageId);
505545
if (!page) {
@@ -534,6 +574,12 @@ export class McpContext implements Context {
534574
void newPage.emulateFocusedPage(true).catch(error => {
535575
this.logger('Error turning on focused page emulation', error);
536576
});
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+
}
537583
}
538584

539585
#updateSelectedPageTimeouts() {
@@ -729,8 +775,9 @@ export class McpContext implements Context {
729775
async createTextSnapshot(
730776
verbose = false,
731777
devtoolsData: DevToolsData | undefined = undefined,
778+
targetPage?: Page,
732779
): Promise<void> {
733-
const page = this.getSelectedPage();
780+
const page = targetPage ?? this.getSelectedPage();
734781
const rootNode = await page.accessibility.snapshot({
735782
includeIframes: true,
736783
interestingOnly: !verbose,
@@ -881,8 +928,12 @@ export class McpContext implements Context {
881928
return this.#networkCollector.getIdForResource(request);
882929
}
883930

884-
waitForTextOnPage(text: string[], timeout?: number): Promise<Element> {
885-
const page = this.getSelectedPage();
931+
waitForTextOnPage(
932+
text: string[],
933+
timeout?: number,
934+
targetPage?: Page,
935+
): Promise<Element> {
936+
const page = targetPage ?? this.getSelectedPage();
886937
const frames = page.frames();
887938

888939
let locator = this.#locatorClass.race(

src/McpResponse.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export class McpResponse implements Response {
238238
await context.createTextSnapshot(
239239
this.#snapshotParams.verbose,
240240
this.#devToolsData,
241+
this.#snapshotParams.page,
241242
);
242243
const textSnapshot = context.getTextSnapshot();
243244
if (textSnapshot) {

src/tools/ToolDefinition.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface ImageContentData {
5454
export interface SnapshotParams {
5555
verbose?: boolean;
5656
filePath?: string;
57+
page?: Page;
5758
}
5859

5960
export interface DevToolsData {
@@ -108,6 +109,7 @@ export type Context = Readonly<{
108109
recordedTraces(): TraceResult[];
109110
storeTraceRecording(result: TraceResult): void;
110111
getSelectedPage(): Page;
112+
resolvePageByContext(isolatedContext?: string): Page;
111113
getDialog(): Dialog | undefined;
112114
clearDialog(): void;
113115
getPageById(pageId: number): Page;
@@ -116,14 +118,23 @@ export type Context = Readonly<{
116118
selectPage(page: Page): void;
117119
getElementByUid(uid: string): Promise<ElementHandle<Element>>;
118120
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
119-
emulate(options: {
120-
networkConditions?: string | null;
121-
cpuThrottlingRate?: number | null;
122-
geolocation?: GeolocationOptions | null;
123-
userAgent?: string | null;
124-
colorScheme?: 'dark' | 'light' | 'auto' | null;
125-
viewport?: Viewport | null;
126-
}): Promise<void>;
121+
emulate(
122+
options: {
123+
networkConditions?: string | null;
124+
cpuThrottlingRate?: number | null;
125+
geolocation?: GeolocationOptions | null;
126+
userAgent?: string | null;
127+
colorScheme?: 'dark' | 'light' | 'auto' | null;
128+
viewport?: Viewport | null;
129+
},
130+
targetPage?: Page,
131+
): Promise<void>;
132+
getNetworkConditions(): string | null;
133+
getCpuThrottlingRate(): number;
134+
getGeolocation(): GeolocationOptions | null;
135+
getViewport(): Viewport | null;
136+
getUserAgent(): string | null;
137+
getColorScheme(): 'dark' | 'light' | null;
127138
saveTemporaryFile(
128139
data: Uint8Array<ArrayBufferLike>,
129140
mimeType: 'image/png' | 'image/jpeg' | 'image/webp',
@@ -136,7 +147,11 @@ export type Context = Readonly<{
136147
action: () => Promise<unknown>,
137148
options?: {timeout?: number},
138149
): Promise<void>;
139-
waitForTextOnPage(text: string[], timeout?: number): Promise<Element>;
150+
waitForTextOnPage(
151+
text: string[],
152+
timeout?: number,
153+
page?: Page,
154+
): Promise<Element>;
140155
getDevToolsData(): Promise<DevToolsData>;
141156
/**
142157
* Returns a reqid for a cdpRequestId.
@@ -179,6 +194,18 @@ export function defineTool<
179194
export const CLOSE_PAGE_ERROR =
180195
'The last open page cannot be closed. It is fine to keep it open.';
181196

197+
export const isolatedContextSchema = {
198+
isolatedContext: zod
199+
.string()
200+
.optional()
201+
.describe(
202+
'The name of the isolated browser context to resolve the page from. ' +
203+
'When provided, the tool operates on the page belonging to this context ' +
204+
'instead of the globally selected page. ' +
205+
'Use this to avoid race conditions when multiple agents work in parallel.',
206+
),
207+
};
208+
182209
export const timeoutSchema = {
183210
timeout: zod
184211
.number()

src/tools/emulation.ts

Lines changed: 6 additions & 2 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} from './ToolDefinition.js';
11+
import {defineTool, isolatedContextSchema} from './ToolDefinition.js';
1212

1313
const throttlingOptions: [string, ...string[]] = [
1414
'No emulation',
@@ -24,6 +24,7 @@ export const emulate = defineTool({
2424
readOnlyHint: false,
2525
},
2626
schema: {
27+
...isolatedContextSchema,
2728
networkConditions: zod
2829
.enum(throttlingOptions)
2930
.optional()
@@ -104,6 +105,9 @@ export const emulate = defineTool({
104105
),
105106
},
106107
handler: async (request, _response, context) => {
107-
await context.emulate(request.params);
108+
const page = context.resolvePageByContext(
109+
request.params.isolatedContext,
110+
);
111+
await context.emulate(request.params, page);
108112
},
109113
});

0 commit comments

Comments
 (0)