Skip to content

Commit a99cc35

Browse files
committed
chore: add support for listing extension pages on list_pages tool
1 parent e5973fd commit a99cc35

5 files changed

Lines changed: 157 additions & 38 deletions

File tree

src/McpContext.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export class McpContext implements Context {
109109
null;
110110

111111
#nextPageId = 1;
112+
#extensionPages = new WeakMap<Target, Page>();
112113

113114
#extensionServiceWorkerMap = new WeakMap<Target, string>();
114115
#nextExtensionServiceWorkerId = 1;
@@ -538,6 +539,32 @@ export class McpContext implements Context {
538539
async createPagesSnapshot(): Promise<Page[]> {
539540
const {pages: allPages, isolatedContextNames} = await this.#getAllPages();
540541

542+
const allTargets = this.browser.targets();
543+
const extensionTargets = allTargets.filter(target => {
544+
return (
545+
target.url().startsWith('chrome-extension://') &&
546+
(target.type() === 'page' || target.type() === 'other')
547+
);
548+
});
549+
550+
for (const target of extensionTargets) {
551+
let page = await target.page();
552+
if (!page) {
553+
page = this.#extensionPages.get(target) ?? null;
554+
if (!page) {
555+
try {
556+
page = await target.asPage();
557+
this.#extensionPages.set(target, page);
558+
} catch (e) {
559+
this.logger('Failed to get page for extension target', e);
560+
}
561+
}
562+
}
563+
if (page && !allPages.includes(page)) {
564+
allPages.push(page);
565+
}
566+
}
567+
541568
for (const page of allPages) {
542569
let mcpPage = this.#mcpPages.get(page);
543570
if (!mcpPage) {

src/McpResponse.ts

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {DevTools} from './third_party/index.js';
1616
import type {
1717
ConsoleMessage,
1818
ImageContent,
19+
Page,
1920
ResourceType,
2021
TextContent,
2122
} from './third_party/index.js';
@@ -42,6 +43,7 @@ interface TraceInsightData {
4243
export class McpResponse implements Response {
4344
#includePages = false;
4445
#includeExtensionServiceWorkers = false;
46+
#includeExtensionPages = false;
4547
#snapshotParams?: SnapshotParams;
4648
#attachedNetworkRequestId?: number;
4749
#attachedNetworkRequestOptions?: {
@@ -94,6 +96,7 @@ export class McpResponse implements Response {
9496

9597
if (this.#args.categoryExtensions) {
9698
this.#includeExtensionServiceWorkers = value;
99+
this.#includeExtensionPages = value;
97100
}
98101
}
99102

@@ -501,6 +504,7 @@ export class McpResponse implements Response {
501504
pages?: object[];
502505
pagination?: object;
503506
extensionServiceWorkers?: object[];
507+
extensionPages?: object[];
504508
} = {};
505509

506510
const response = [`# ${toolName} response`];
@@ -564,34 +568,68 @@ Call ${handleDialog.name} to handle it before continuing.`);
564568
}
565569

566570
if (this.#includePages) {
567-
const parts = [`## Pages`];
568-
for (const page of context.getPages()) {
569-
const isolatedContextName = context.getIsolatedContextName(page);
570-
const contextLabel = isolatedContextName
571-
? ` isolatedContext=${isolatedContextName}`
572-
: '';
573-
parts.push(
574-
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
575-
);
571+
const allPages = context.getPages();
572+
573+
const {regularPages, extensionPages} = allPages.reduce(
574+
(acc: {regularPages: Page[]; extensionPages: Page[]}, page: Page) => {
575+
if (page.url().startsWith('chrome-extension://')) {
576+
acc.extensionPages.push(page);
577+
} else {
578+
acc.regularPages.push(page);
579+
}
580+
return acc;
581+
},
582+
{regularPages: [], extensionPages: []},
583+
);
584+
585+
if (regularPages.length) {
586+
const parts = [`## Pages`];
587+
for (const page of regularPages) {
588+
const isolatedContextName = context.getIsolatedContextName(page);
589+
const contextLabel = isolatedContextName
590+
? ` isolatedContext=${isolatedContextName}`
591+
: '';
592+
parts.push(
593+
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
594+
);
595+
}
596+
response.push(...parts);
597+
structuredContent.pages = regularPages.map(page => {
598+
const isolatedContextName = context.getIsolatedContextName(page);
599+
const entry: {
600+
id: number | undefined;
601+
url: string;
602+
selected: boolean;
603+
isolatedContext?: string;
604+
} = {
605+
id: context.getPageId(page),
606+
url: page.url(),
607+
selected: context.isPageSelected(page),
608+
};
609+
if (isolatedContextName) {
610+
entry.isolatedContext = isolatedContextName;
611+
}
612+
return entry;
613+
});
576614
}
577-
response.push(...parts);
578-
structuredContent.pages = context.getPages().map(page => {
579-
const isolatedContextName = context.getIsolatedContextName(page);
580-
const entry: {
581-
id: number | undefined;
582-
url: string;
583-
selected: boolean;
584-
isolatedContext?: string;
585-
} = {
586-
id: context.getPageId(page),
587-
url: page.url(),
588-
selected: context.isPageSelected(page),
589-
};
590-
if (isolatedContextName) {
591-
entry.isolatedContext = isolatedContextName;
615+
616+
if (this.#includeExtensionPages) {
617+
if (extensionPages.length) {
618+
response.push(`## Extension Pages`);
619+
for (const page of extensionPages) {
620+
response.push(
621+
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}`,
622+
);
623+
}
624+
structuredContent.extensionPages = extensionPages.map(page => {
625+
return {
626+
id: context.getPageId(page),
627+
url: page.url(),
628+
selected: context.isPageSelected(page),
629+
};
630+
});
592631
}
593-
return entry;
594-
});
632+
}
595633
}
596634

597635
if (this.#includeExtensionServiceWorkers) {

src/browser.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ import {puppeteer} from './third_party/index.js';
2020

2121
let browser: Browser | undefined;
2222

23-
function makeTargetFilter() {
24-
const ignoredPrefixes = new Set([
25-
'chrome://',
26-
'chrome-extension://',
27-
'chrome-untrusted://',
28-
]);
23+
function makeTargetFilter(enableExtensions = false) {
24+
const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
25+
if (!enableExtensions) {
26+
ignoredPrefixes.add('chrome-extension://');
27+
}
2928

3029
return function targetFilter(target: Target): boolean {
3130
if (target.url() === 'chrome://newtab/') {
@@ -51,14 +50,15 @@ export async function ensureBrowserConnected(options: {
5150
devtools: boolean;
5251
channel?: Channel;
5352
userDataDir?: string;
53+
enableExtensions?: boolean;
5454
}) {
55-
const {channel} = options;
55+
const {channel, enableExtensions} = options;
5656
if (browser?.connected) {
5757
return browser;
5858
}
5959

6060
const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
61-
targetFilter: makeTargetFilter(),
61+
targetFilter: makeTargetFilter(enableExtensions),
6262
defaultViewport: null,
6363
handleDevToolsAsPage: true,
6464
};
@@ -215,7 +215,7 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
215215
try {
216216
const browser = await puppeteer.launch({
217217
channel: puppeteerChannel,
218-
targetFilter: makeTargetFilter(),
218+
targetFilter: makeTargetFilter(options.enableExtensions),
219219
executablePath,
220220
defaultViewport: null,
221221
userDataDir,

src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export async function createMcpServer(
7979
: undefined,
8080
userDataDir: serverArgs.userDataDir,
8181
devtools,
82+
enableExtensions: serverArgs.categoryExtensions,
8283
})
8384
: await ensureBrowserLaunched({
8485
headless: serverArgs.headless,

tests/tools/pages.test.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import type {Dialog} from 'puppeteer-core';
1212
import sinon from 'sinon';
1313

1414
import type {ParsedArguments} from '../../src/cli.js';
15-
import {installExtension} from '../../src/tools/extensions.js';
15+
import {
16+
installExtension,
17+
triggerExtensionAction,
18+
} from '../../src/tools/extensions.js';
1619
import {
1720
listPages,
1821
newPage,
@@ -23,7 +26,7 @@ import {
2326
handleDialog,
2427
getTabId,
2528
} from '../../src/tools/pages.js';
26-
import {html, withMcpContext} from '../utils.js';
29+
import {extractExtensionId, html, withMcpContext} from '../utils.js';
2730

2831
const EXTENSION_PATH = path.join(
2932
import.meta.dirname,
@@ -46,8 +49,58 @@ describe('pages', () => {
4649
assert.ok(response.includePages);
4750
});
4851
});
52+
it(`list pages for extension pages with --category-extensions`, async () => {
53+
await withMcpContext(
54+
async (response, context) => {
55+
await installExtension.handler(
56+
{params: {path: EXTENSION_PATH}},
57+
response,
58+
context,
59+
);
60+
61+
const extensionId = extractExtensionId(response);
62+
assert.ok(extensionId);
63+
64+
await triggerExtensionAction.handler(
65+
{params: {id: extensionId}},
66+
response,
67+
context,
68+
);
69+
const popupTarget = await context.browser.waitForTarget(
70+
t => t.type() === 'page' && t.url().includes('chrome-extension://'),
71+
);
72+
73+
response.resetResponseLineForTesting();
74+
75+
const listPageDef = listPages({
76+
categoryExtensions: true,
77+
} as ParsedArguments);
78+
await listPageDef.handler(
79+
{params: {}, page: context.getSelectedMcpPage()},
80+
response,
81+
context,
82+
);
83+
84+
const result = await response.handle(listPageDef.name, context);
85+
const textContent = result.content.find(c => c.type === 'text') as {
86+
type: 'text';
87+
text: string;
88+
};
89+
assert.ok(textContent);
90+
91+
assert.ok(textContent.text.includes(popupTarget.url()));
92+
},
93+
{
94+
executablePath: process.env.CANARY_EXECUTABLE_PATH,
95+
},
96+
{
97+
categoryExtensions: true,
98+
} as ParsedArguments,
99+
);
100+
});
101+
49102
for (const categoryExtensions of [true, false]) {
50-
it(`list pages ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async () => {
103+
it(`list pages for extension service workers ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async () => {
51104
await withMcpContext(
52105
async (response, context) => {
53106
await installExtension.handler(

0 commit comments

Comments
 (0)