Skip to content

Commit 302e5a0

Browse files
authored
refactor: consistently use McpPage in tools (#1057)
Follow-up to caf601a The work is not complete and as a follow-up I will handle other Context methods and moved whatever is relevant to the McpPage. Also, duplicate getters would go away.
1 parent 21634e6 commit 302e5a0

29 files changed

+439
-326
lines changed

src/McpContext.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ import {PredefinedNetworkConditions} from './third_party/index.js';
3737
import {listPages} from './tools/pages.js';
3838
import {takeSnapshot} from './tools/snapshot.js';
3939
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
40-
import type {Context, DevToolsData} from './tools/ToolDefinition.js';
40+
import type {
41+
Context,
42+
DevToolsData,
43+
ContextPage,
44+
} from './tools/ToolDefinition.js';
4145
import type {TraceResult} from './trace-processing/parse.js';
4246
import type {
4347
EmulationSettings,
@@ -117,7 +121,7 @@ export class McpContext implements Context {
117121
null;
118122
#focusedPagePerContext = new Map<BrowserContext, Page>();
119123

120-
#requestPage?: Page;
124+
#requestPage?: ContextPage;
121125

122126
#nextPageId = 1;
123127

@@ -196,12 +200,12 @@ export class McpContext implements Context {
196200
// TODO: Refactor away mutable request state (e.g. per-request facade,
197201
// per-request context object, or another approach). Once resolved, the
198202
// global toolMutex could become per-BrowserContext for parallel execution.
199-
setRequestPage(page?: Page): void {
203+
setRequestPage(page?: ContextPage): void {
200204
this.#requestPage = page;
201205
}
202206

203207
#resolveTargetPage(): Page {
204-
return this.#requestPage ?? this.getSelectedPage();
208+
return this.#requestPage?.pptrPage ?? this.getSelectedPage();
205209
}
206210

207211
resolveCdpRequestId(cdpRequestId: string): number | undefined {
@@ -283,7 +287,7 @@ export class McpContext implements Context {
283287
async newPage(
284288
background?: boolean,
285289
isolatedContextName?: string,
286-
): Promise<Page> {
290+
): Promise<ContextPage> {
287291
let page: Page;
288292
if (isolatedContextName !== undefined) {
289293
let ctx = this.#isolatedContexts.get(isolatedContextName);
@@ -299,7 +303,7 @@ export class McpContext implements Context {
299303
this.selectPage(page);
300304
this.#networkCollector.addPage(page);
301305
this.#consoleCollector.addPage(page);
302-
return page;
306+
return this.#getMcpPage(page);
303307
}
304308
async closePage(pageId: number): Promise<void> {
305309
if (this.#pages.length === 1) {
@@ -489,7 +493,8 @@ export class McpContext implements Context {
489493
}
490494

491495
getDialog(page?: Page): Dialog | undefined {
492-
const targetPage = page ?? this.#requestPage ?? this.#selectedPage;
496+
const targetPage =
497+
page ?? this.#requestPage?.pptrPage ?? this.#selectedPage;
493498
if (!targetPage) {
494499
return undefined;
495500
}
@@ -516,11 +521,17 @@ export class McpContext implements Context {
516521
return page;
517522
}
518523

519-
resolvePageById(pageId?: number): Page {
524+
getSelectedMcpPage(): McpPage {
525+
const page = this.getSelectedPage();
526+
return this.#getMcpPage(page);
527+
}
528+
529+
resolvePageById(pageId?: number): McpPage {
520530
if (pageId === undefined) {
521-
return this.getSelectedPage();
531+
return this.getSelectedMcpPage();
522532
}
523-
return this.getPageById(pageId);
533+
const page = this.getPageById(pageId);
534+
return this.#getMcpPage(page);
524535
}
525536

526537
getPageById(pageId: number): Page {
@@ -551,7 +562,8 @@ export class McpContext implements Context {
551562
return this.#selectedPage === page;
552563
}
553564

554-
assertPageIsFocused(page: Page): void {
565+
assertPageIsFocused(pageToCheck: Page | ContextPage): void {
566+
const page = 'pptrPage' in pageToCheck ? pageToCheck.pptrPage : pageToCheck;
555567
const ctx = page.browserContext();
556568
const focused = this.#focusedPagePerContext.get(ctx);
557569
if (focused && focused !== page) {
@@ -564,7 +576,9 @@ export class McpContext implements Context {
564576
}
565577
}
566578

567-
selectPage(newPage: Page): void {
579+
selectPage(pageToSelect: Page | ContextPage): void {
580+
const newPage =
581+
'pptrPage' in pageToSelect ? pageToSelect.pptrPage : pageToSelect;
568582
const ctx = newPage.browserContext();
569583
const oldFocused = this.#focusedPagePerContext.get(ctx);
570584
if (oldFocused && oldFocused !== newPage && !oldFocused.isClosed()) {

src/McpPage.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {Dialog, Page, Viewport} from './third_party/index.js';
7+
import type {
8+
Dialog,
9+
ElementHandle,
10+
Page,
11+
Viewport,
12+
} from './third_party/index.js';
13+
import {takeSnapshot} from './tools/snapshot.js';
14+
import type {ContextPage} from './tools/ToolDefinition.js';
815
import type {
916
EmulationSettings,
1017
GeolocationOptions,
1118
TextSnapshot,
19+
TextSnapshotNode,
1220
} from './types.js';
1321

1422
/**
@@ -19,8 +27,8 @@ import type {
1927
* read/write access. The dialog field is private because it requires an
2028
* event listener lifecycle managed by the constructor/dispose pair.
2129
*/
22-
export class McpPage {
23-
readonly page: Page;
30+
export class McpPage implements ContextPage {
31+
readonly pptrPage: Page;
2432
readonly id: number;
2533

2634
// Snapshot
@@ -39,7 +47,7 @@ export class McpPage {
3947
#dialogHandler: (dialog: Dialog) => void;
4048

4149
constructor(page: Page, id: number) {
42-
this.page = page;
50+
this.pptrPage = page;
4351
this.id = id;
4452
this.#dialogHandler = (dialog: Dialog): void => {
4553
this.#dialog = dialog;
@@ -80,6 +88,41 @@ export class McpPage {
8088
}
8189

8290
dispose(): void {
83-
this.page.off('dialog', this.#dialogHandler);
91+
this.pptrPage.off('dialog', this.#dialogHandler);
92+
}
93+
94+
async getElementByUid(uid: string): Promise<ElementHandle<Element>> {
95+
if (!this.textSnapshot) {
96+
throw new Error(
97+
`No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`,
98+
);
99+
}
100+
const node = this.textSnapshot.idToNode.get(uid);
101+
if (!node) {
102+
throw new Error(`Element uid "${uid}" not found on page ${this.id}.`);
103+
}
104+
return this.#resolveElementHandle(node, uid);
105+
}
106+
107+
async #resolveElementHandle(
108+
node: TextSnapshotNode,
109+
uid: string,
110+
): Promise<ElementHandle<Element>> {
111+
const message = `Element with uid ${uid} no longer exists on the page.`;
112+
try {
113+
const handle = await node.elementHandle();
114+
if (!handle) {
115+
throw new Error(message);
116+
}
117+
return handle;
118+
} catch (error) {
119+
throw new Error(message, {
120+
cause: error,
121+
});
122+
}
123+
}
124+
125+
getAXNodeByUid(uid: string) {
126+
return this.textSnapshot?.idToNode.get(uid);
84127
}
85128
}

src/McpResponse.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,11 @@ export class McpResponse implements Response {
263263
await context.createTextSnapshot(
264264
this.#snapshotParams.verbose,
265265
this.#devToolsData,
266-
this.#snapshotParams.page,
266+
this.#snapshotParams.page?.pptrPage,
267+
);
268+
const textSnapshot = context.getTextSnapshot(
269+
this.#snapshotParams.page?.pptrPage,
267270
);
268-
const textSnapshot = context.getTextSnapshot(this.#snapshotParams.page);
269271
if (textSnapshot) {
270272
const formatter = new SnapshotFormatter(textSnapshot);
271273
if (this.#snapshotParams.filePath) {

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export async function createMcpServer(
181181
const page =
182182
serverArgs.experimentalPageIdRouting && params.pageId
183183
? context.resolvePageById(params.pageId)
184-
: context.getSelectedPage();
184+
: context.getSelectedMcpPage();
185185
context.setRequestPage(page);
186186
await tool.handler(
187187
{

src/tools/ToolDefinition.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export interface ImageContentData {
6060
export interface SnapshotParams {
6161
verbose?: boolean;
6262
filePath?: string;
63-
page?: Page;
63+
page?: ContextPage;
6464
}
6565

6666
export interface LighthouseData {
@@ -141,7 +141,10 @@ export type Context = Readonly<{
141141
getDialog(page?: Page): Dialog | undefined;
142142
clearDialog(page?: Page): void;
143143
getPageById(pageId: number): Page;
144-
newPage(background?: boolean, isolatedContextName?: string): Promise<Page>;
144+
newPage(
145+
background?: boolean,
146+
isolatedContextName?: string,
147+
): Promise<ContextPage>;
145148
closePage(pageId: number): Promise<void>;
146149
selectPage(page: Page): void;
147150
assertPageIsFocused(page: Page): void;
@@ -191,6 +194,12 @@ export type Context = Readonly<{
191194
getExtension(id: string): InstalledExtension | undefined;
192195
}>;
193196

197+
export type ContextPage = Readonly<{
198+
readonly pptrPage: Page;
199+
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
200+
getElementByUid(uid: string): Promise<ElementHandle<Element>>;
201+
}>;
202+
194203
export function defineTool<Schema extends zod.ZodRawShape>(
195204
definition: ToolDefinition<Schema>,
196205
): ToolDefinition<Schema>;
@@ -223,7 +232,7 @@ interface PageToolDefinition<
223232
Schema extends zod.ZodRawShape = zod.ZodRawShape,
224233
> extends BaseToolDefinition<Schema> {
225234
handler: (
226-
request: Request<Schema> & {page: Page},
235+
request: Request<Schema> & {page: ContextPage},
227236
response: Response,
228237
context: Context,
229238
) => Promise<void>;
@@ -233,7 +242,7 @@ export type DefinedPageTool<Schema extends zod.ZodRawShape = zod.ZodRawShape> =
233242
PageToolDefinition<Schema> & {
234243
pageScoped: true;
235244
handler: (
236-
request: Request<Schema> & {page: Page},
245+
request: Request<Schema> & {page: ContextPage},
237246
response: Response,
238247
context: Context,
239248
) => Promise<void>;

src/tools/emulation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,6 @@ export const emulate = definePageTool({
105105
},
106106
handler: async (request, _response, context) => {
107107
const page = request.page;
108-
await context.emulate(request.params, page);
108+
await context.emulate(request.params, page.pptrPage);
109109
},
110110
});

src/tools/input.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ export const clickAt = definePageTool({
9797
},
9898
handler: async (request, response, context) => {
9999
const page = request.page;
100-
context.assertPageIsFocused(page);
100+
context.assertPageIsFocused(page.pptrPage);
101101
await context.waitForEventsAfterAction(async () => {
102-
await page.mouse.click(request.params.x, request.params.y, {
102+
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
103103
clickCount: request.params.dblClick ? 2 : 1,
104104
});
105105
});
@@ -239,7 +239,7 @@ export const fill = definePageTool({
239239
request.params.uid,
240240
request.params.value,
241241
context as McpContext,
242-
page,
242+
page.pptrPage,
243243
);
244244
});
245245
response.appendResponseLine(`Successfully filled out the element`);
@@ -262,11 +262,13 @@ export const typeText = definePageTool({
262262
},
263263
handler: async (request, response, context) => {
264264
const page = request.page;
265-
context.assertPageIsFocused(page);
265+
context.assertPageIsFocused(page.pptrPage);
266266
await context.waitForEventsAfterAction(async () => {
267-
await page.keyboard.type(request.params.text);
267+
await page.pptrPage.keyboard.type(request.params.text);
268268
if (request.params.submitKey) {
269-
await page.keyboard.press(request.params.submitKey as KeyInput);
269+
await page.pptrPage.keyboard.press(
270+
request.params.submitKey as KeyInput,
271+
);
270272
}
271273
});
272274
response.appendResponseLine(
@@ -333,7 +335,7 @@ export const fillForm = definePageTool({
333335
element.uid,
334336
element.value,
335337
context as McpContext,
336-
page,
338+
page.pptrPage,
337339
);
338340
});
339341
}
@@ -364,7 +366,7 @@ export const uploadFile = definePageTool({
364366
const {uid, filePath} = request.params;
365367
const handle = (await context.getElementByUid(
366368
uid,
367-
request.page,
369+
request.page.pptrPage,
368370
)) as ElementHandle<HTMLInputElement>;
369371
try {
370372
try {
@@ -375,7 +377,7 @@ export const uploadFile = definePageTool({
375377
// Page.waitForFileChooser() and upload the file this way.
376378
try {
377379
const [fileChooser] = await Promise.all([
378-
request.page.waitForFileChooser({timeout: 3000}),
380+
request.page.pptrPage.waitForFileChooser({timeout: 3000}),
379381
handle.asLocator().click(),
380382
]);
381383
await fileChooser.accept([filePath]);
@@ -412,17 +414,17 @@ export const pressKey = definePageTool({
412414
},
413415
handler: async (request, response, context) => {
414416
const page = request.page;
415-
context.assertPageIsFocused(page);
417+
context.assertPageIsFocused(page.pptrPage);
416418
const tokens = parseKey(request.params.key);
417419
const [key, ...modifiers] = tokens;
418420

419421
await context.waitForEventsAfterAction(async () => {
420422
for (const modifier of modifiers) {
421-
await page.keyboard.down(modifier);
423+
await page.pptrPage.keyboard.down(modifier);
422424
}
423-
await page.keyboard.press(key);
425+
await page.pptrPage.keyboard.press(key);
424426
for (const modifier of modifiers.toReversed()) {
425-
await page.keyboard.up(modifier);
427+
await page.pptrPage.keyboard.up(modifier);
426428
}
427429
});
428430

src/tools/lighthouse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ export const lighthouseAudit = definePageTool({
8282
let result: RunnerResult | undefined;
8383
try {
8484
if (mode === 'navigation') {
85-
result = await navigation(page, page.url(), {
85+
result = await navigation(page.pptrPage, page.pptrPage.url(), {
8686
flags,
8787
});
8888
} else {
89-
result = await snapshot(page, {
89+
result = await snapshot(page.pptrPage, {
9090
flags,
9191
});
9292
}

src/tools/memory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const takeMemorySnapshot = definePageTool({
2424
handler: async (request, response, _context) => {
2525
const page = request.page;
2626

27-
await page.captureHeapSnapshot({
27+
await page.pptrPage.captureHeapSnapshot({
2828
path: request.params.filePath,
2929
});
3030

0 commit comments

Comments
 (0)