Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@

**Parameters:**

- **pageIdx** (number) **(required)**: The index of the page to close. Call [`list_pages`](#list_pages) to list pages.
- **pageId** (number) **(required)**: The ID of the page to close. Call [`list_pages`](#list_pages) to list pages.

---

Expand Down Expand Up @@ -172,7 +172,7 @@

**Parameters:**

- **pageIdx** (number) **(required)**: The index of the page to select. Call [`list_pages`](#list_pages) to get available pages.
- **pageId** (number) **(required)**: The ID of the page to select. Call [`list_pages`](#list_pages) to get available pages.
- **bringToFront** (boolean) _(optional)_: Whether to focus the page and bring it to the top.

---
Expand Down
22 changes: 17 additions & 5 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export class McpContext implements Context {
#geolocationMap = new WeakMap<Page, GeolocationOptions>();
#dialog?: Dialog;

#pageIdMap = new WeakMap<Page, number>();
#nextPageId = 1;

#nextSnapshotId = 1;
#traceResults: TraceResult[] = [];

Expand Down Expand Up @@ -246,11 +249,11 @@ export class McpContext implements Context {
this.#consoleCollector.addPage(page);
return page;
}
async closePage(pageIdx: number): Promise<void> {
async closePage(pageId: number): Promise<void> {
if (this.#pages.length === 1) {
throw new Error(CLOSE_PAGE_ERROR);
}
const page = this.getPageByIdx(pageIdx);
const page = this.getPageById(pageId);
await page.close({runBeforeUnload: false});
}

Expand Down Expand Up @@ -327,15 +330,18 @@ export class McpContext implements Context {
return page;
}

getPageByIdx(idx: number): Page {
const pages = this.#pages;
const page = pages[idx];
getPageById(pageId: number): Page {
const page = this.#pages.find(p => this.#pageIdMap.get(p) === pageId);
if (!page) {
throw new Error('No page found');
}
return page;
}

getPageId(page: Page): number | undefined {
return this.#pageIdMap.get(page);
}

#dialogHandler = (dialog: Dialog): void => {
this.#dialog = dialog;
};
Expand Down Expand Up @@ -417,6 +423,12 @@ export class McpContext implements Context {
this.#options.experimentalIncludeAllPages,
);

for (const page of allPages) {
if (!this.#pageIdMap.has(page)) {
this.#pageIdMap.set(page, this.#nextPageId++);
}
}

this.#pages = allPages.filter(page => {
// If we allow debugging DevTools windows, return all pages.
// If we are in regular mode, the user should only see non-DevTools page.
Expand Down
4 changes: 1 addition & 3 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,10 @@ Call ${handleDialog.name} to handle it before continuing.`);

if (this.#includePages) {
const parts = [`## Pages`];
let idx = 0;
for (const page of context.getPages()) {
parts.push(
`${idx}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}`,
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}`,
);
idx++;
}
response.push(...parts);
}
Expand Down
5 changes: 3 additions & 2 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ export type Context = Readonly<{
getSelectedPage(): Page;
getDialog(): Dialog | undefined;
clearDialog(): void;
getPageByIdx(idx: number): Page;
getPageById(pageId: number): Page;
getPageId(page: Page): number | undefined;
isPageSelected(page: Page): boolean;
newPage(): Promise<Page>;
closePage(pageIdx: number): Promise<void>;
closePage(pageId: number): Promise<void>;
selectPage(page: Page): void;
getElementByUid(uid: string): Promise<ElementHandle<Element>>;
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
Expand Down
14 changes: 6 additions & 8 deletions src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,18 @@ export const selectPage = defineTool({
readOnlyHint: true,
},
schema: {
pageIdx: zod
pageId: zod
.number()
.describe(
`The index of the page to select. Call ${listPages.name} to get available pages.`,
`The ID of the page to select. Call ${listPages.name} to get available pages.`,
),
bringToFront: zod
.boolean()
.optional()
.describe('Whether to focus the page and bring it to the top.'),
},
handler: async (request, response, context) => {
const page = context.getPageByIdx(request.params.pageIdx);
const page = context.getPageById(request.params.pageId);
context.selectPage(page);
response.setIncludePages(true);
if (request.params.bringToFront) {
Expand All @@ -59,15 +59,13 @@ export const closePage = defineTool({
readOnlyHint: false,
},
schema: {
pageIdx: zod
pageId: zod
.number()
.describe(
'The index of the page to close. Call list_pages to list pages.',
),
.describe('The ID of the page to close. Call list_pages to list pages.'),
},
handler: async (request, response, context) => {
try {
await context.closePage(request.params.pageIdx);
await context.closePage(request.params.pageId);
} catch (err) {
if (err.message === CLOSE_PAGE_ERROR) {
response.appendResponseLine(err.message);
Expand Down
2 changes: 1 addition & 1 deletion tests/McpResponse.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Testing 2
exports[`McpResponse > list pages 1`] = `
# test response
## Pages
0: about:blank [selected]
1: about:blank [selected]
`;

exports[`McpResponse > returns correctly formatted snapshot for a simple tree 1`] = `
Expand Down
4 changes: 2 additions & 2 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('e2e', () => {
content: [
{
type: 'text',
text: '# list_pages response\n## Pages\n0: about:blank [selected]',
text: '# list_pages response\n## Pages\n1: about:blank [selected]',
},
],
});
Expand All @@ -72,7 +72,7 @@ describe('e2e', () => {
content: [
{
type: 'text',
text: '# list_pages response\n## Pages\n0: about:blank [selected]',
text: '# list_pages response\n## Pages\n1: about:blank [selected]',
},
],
});
Expand Down
32 changes: 16 additions & 16 deletions tests/tools/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ describe('pages', () => {
describe('new_page', () => {
it('create a page', async () => {
await withMcpContext(async (response, context) => {
assert.strictEqual(context.getPageByIdx(0), context.getSelectedPage());
assert.strictEqual(context.getPageById(1), context.getSelectedPage());
await newPage.handler(
{params: {url: 'about:blank'}},
response,
context,
);
assert.strictEqual(context.getPageByIdx(1), context.getSelectedPage());
assert.strictEqual(context.getPageById(2), context.getSelectedPage());
assert.ok(response.includePages);
});
});
Expand All @@ -47,17 +47,17 @@ describe('pages', () => {
it('closes a page', async () => {
await withMcpContext(async (response, context) => {
const page = await context.newPage();
assert.strictEqual(context.getPageByIdx(1), context.getSelectedPage());
assert.strictEqual(context.getPageByIdx(1), page);
await closePage.handler({params: {pageIdx: 1}}, response, context);
assert.strictEqual(context.getPageById(2), context.getSelectedPage());
assert.strictEqual(context.getPageById(2), page);
await closePage.handler({params: {pageId: 2}}, response, context);
assert.ok(page.isClosed());
assert.ok(response.includePages);
});
});
it('cannot close the last page', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await closePage.handler({params: {pageIdx: 0}}, response, context);
await closePage.handler({params: {pageId: 1}}, response, context);
assert.deepStrictEqual(
response.responseLines[0],
`The last open page cannot be closed. It is fine to keep it open.`,
Expand All @@ -71,24 +71,24 @@ describe('pages', () => {
it('selects a page', async () => {
await withMcpContext(async (response, context) => {
await context.newPage();
assert.strictEqual(context.getPageByIdx(1), context.getSelectedPage());
await selectPage.handler({params: {pageIdx: 0}}, response, context);
assert.strictEqual(context.getPageByIdx(0), context.getSelectedPage());
assert.strictEqual(context.getPageById(2), context.getSelectedPage());
await selectPage.handler({params: {pageId: 1}}, response, context);
assert.strictEqual(context.getPageById(1), context.getSelectedPage());
assert.ok(response.includePages);
});
});
it('selects a page and keeps it focused in the background', async () => {
await withMcpContext(async (response, context) => {
await context.newPage();
assert.strictEqual(context.getPageByIdx(1), context.getSelectedPage());
assert.strictEqual(context.getPageById(2), context.getSelectedPage());
assert.strictEqual(
await context.getPageByIdx(0).evaluate(() => document.hasFocus()),
await context.getPageById(1).evaluate(() => document.hasFocus()),
false,
);
await selectPage.handler({params: {pageIdx: 0}}, response, context);
assert.strictEqual(context.getPageByIdx(0), context.getSelectedPage());
await selectPage.handler({params: {pageId: 1}}, response, context);
assert.strictEqual(context.getPageById(1), context.getSelectedPage());
assert.strictEqual(
await context.getPageByIdx(0).evaluate(() => document.hasFocus()),
await context.getPageById(1).evaluate(() => document.hasFocus()),
true,
);
assert.ok(response.includePages);
Expand All @@ -115,8 +115,8 @@ describe('pages', () => {
it('throws an error if the page was closed not by the MCP server', async () => {
await withMcpContext(async (response, context) => {
const page = await context.newPage();
assert.strictEqual(context.getPageByIdx(1), context.getSelectedPage());
assert.strictEqual(context.getPageByIdx(1), page);
assert.strictEqual(context.getPageById(2), context.getSelectedPage());
assert.strictEqual(context.getPageById(2), page);

await page.close();

Expand Down