Skip to content

Commit aaf56cb

Browse files
committed
feat: update listPages to return extension service workers
1 parent ff7ac7c commit aaf56cb

9 files changed

Lines changed: 185 additions & 17 deletions

File tree

src/McpContext.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fs from 'node:fs/promises';
88
import os from 'node:os';
99
import path from 'node:path';
1010

11+
import type {ParsedArguments} from './cli.js';
1112
import type {TargetUniverse} from './DevtoolsUtils.js';
1213
import {
1314
extractUrlLikeFromDevToolsTitle,
@@ -29,6 +30,7 @@ import type {
2930
ScreenRecorder,
3031
SerializedAXNode,
3132
Viewport,
33+
Target,
3234
} from './third_party/index.js';
3335
import {Locator} from './third_party/index.js';
3436
import {PredefinedNetworkConditions} from './third_party/index.js';
@@ -50,6 +52,12 @@ export interface TextSnapshotNode extends SerializedAXNode {
5052
children: TextSnapshotNode[];
5153
}
5254

55+
export interface ExtensionServiceWorker {
56+
url: string;
57+
target: Target;
58+
id: string;
59+
}
60+
5361
export interface GeolocationOptions {
5462
latitude: number;
5563
longitude: number;
@@ -129,6 +137,8 @@ export class McpContext implements Context {
129137
#nextIsolatedContextId = 1;
130138

131139
#pages: Page[] = [];
140+
#extensionServiceWorkers: ExtensionServiceWorker[] = [];
141+
132142
#pageToDevToolsPage = new Map<Page, Page>();
133143
#selectedPage?: Page;
134144
#textSnapshot: TextSnapshot | null = null;
@@ -146,6 +156,9 @@ export class McpContext implements Context {
146156
#pageIdMap = new WeakMap<Page, number>();
147157
#nextPageId = 1;
148158

159+
#extensionServiceWorkerMap = new WeakMap<Target, string>();
160+
#nextExtensionServiceWorkerId = 1;
161+
149162
#nextSnapshotId = 1;
150163
#traceResults: TraceResult[] = [];
151164

@@ -185,6 +198,7 @@ export class McpContext implements Context {
185198

186199
async #init() {
187200
const pages = await this.createPagesSnapshot();
201+
await this.createExtensionServiceWorkersSnapshot();
188202
await this.#networkCollector.init(pages);
189203
await this.#consoleCollector.init(pages);
190204
await this.#devtoolsUniverseManager.init(pages);
@@ -494,7 +508,7 @@ export class McpContext implements Context {
494508
}
495509
if (page.isClosed()) {
496510
throw new Error(
497-
`The selected page has been closed. Call ${listPages.name} to see open pages.`,
511+
`The selected page has been closed. Call ${listPages({} as ParsedArguments).name} to see open pages.`,
498512
);
499513
}
500514
return page;
@@ -584,6 +598,41 @@ export class McpContext implements Context {
584598
}
585599
}
586600

601+
/**
602+
* Creates a snapshot of the extension service workers.
603+
*/
604+
async createExtensionServiceWorkersSnapshot(): Promise<
605+
ExtensionServiceWorker[]
606+
> {
607+
const allTargets = await this.browser.targets();
608+
609+
const serviceWorkers = allTargets.filter(target => {
610+
return (
611+
target.type() === 'service_worker' &&
612+
target.url().includes('chrome-extension://')
613+
);
614+
});
615+
616+
for (const serviceWorker of serviceWorkers) {
617+
if (!this.#extensionServiceWorkerMap.has(serviceWorker)) {
618+
this.#extensionServiceWorkerMap.set(
619+
serviceWorker,
620+
'sw-' + this.#nextExtensionServiceWorkerId++,
621+
);
622+
}
623+
}
624+
625+
this.#extensionServiceWorkers = serviceWorkers.map(serviceWorker => {
626+
return {
627+
target: serviceWorker,
628+
id: this.#extensionServiceWorkerMap.get(serviceWorker)!,
629+
url: serviceWorker.url(),
630+
};
631+
});
632+
633+
return this.#extensionServiceWorkers;
634+
}
635+
587636
async createPagesSnapshot(): Promise<Page[]> {
588637
const allPages = await this.#getAllPages();
589638

@@ -677,6 +726,16 @@ export class McpContext implements Context {
677726
}
678727
}
679728

729+
getExtensionServiceWorkers(): ExtensionServiceWorker[] {
730+
return this.#extensionServiceWorkers;
731+
}
732+
733+
getExtensionServiceWorkerId(
734+
extensionServiceWorker: ExtensionServiceWorker,
735+
): string | undefined {
736+
return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
737+
}
738+
680739
getPages(): Page[] {
681740
return this.#pages;
682741
}

src/McpResponse.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface TraceInsightData {
3838

3939
export class McpResponse implements Response {
4040
#includePages = false;
41+
#includeExtensionServiceWorkers = false;
4142
#snapshotParams?: SnapshotParams;
4243
#attachedNetworkRequestId?: number;
4344
#attachedNetworkRequestOptions?: {
@@ -88,6 +89,10 @@ export class McpResponse implements Response {
8889
this.#listExtensions = true;
8990
}
9091

92+
setIncludeExtensionServiceWorkers(value: boolean): void {
93+
this.#includeExtensionServiceWorkers = value;
94+
}
95+
9196
setIncludeNetworkRequests(
9297
value: boolean,
9398
options?: PaginationOptions & {
@@ -233,6 +238,10 @@ export class McpResponse implements Response {
233238
await context.createPagesSnapshot();
234239
}
235240

241+
if (this.#includeExtensionServiceWorkers) {
242+
await context.createExtensionServiceWorkersSnapshot();
243+
}
244+
236245
let snapshot: SnapshotFormatter | string | undefined;
237246
if (this.#snapshotParams) {
238247
await context.createTextSnapshot(
@@ -438,6 +447,7 @@ export class McpResponse implements Response {
438447
};
439448
pages?: object[];
440449
pagination?: object;
450+
extensionServiceWorkers?: object[];
441451
} = {};
442452

443453
const response = [`# ${toolName} response`];
@@ -532,6 +542,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
532542
});
533543
}
534544

545+
if (this.#includeExtensionServiceWorkers) {
546+
if (!context.getExtensionServiceWorkers().length) {
547+
response.push(`## Extension Service Workers`);
548+
}
549+
550+
for (const extensionServiceWorker of context.getExtensionServiceWorkers()) {
551+
response.push(
552+
`${extensionServiceWorker.id}: ${extensionServiceWorker.url}`,
553+
);
554+
}
555+
structuredContent.extensionServiceWorkers = context
556+
.getExtensionServiceWorkers()
557+
.map(extensionServiceWorker => {
558+
return {
559+
id: extensionServiceWorker.id,
560+
url: extensionServiceWorker.url,
561+
};
562+
});
563+
}
564+
535565
if (this.#tabId) {
536566
structuredContent.tabId = this.#tabId;
537567
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface Response {
9696
insightName: InsightName,
9797
): void;
9898
setListExtensions(): void;
99+
setIncludeExtensionServiceWorkers(value: boolean): void;
99100
}
100101

101102
/**

src/tools/pages.ts

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

7+
import type {ParsedArguments} from '../cli.js';
78
import {logger} from '../logger.js';
89
import type {Dialog} from '../third_party/index.js';
910
import {zod} from '../third_party/index.js';
1011

1112
import {ToolCategory} from './categories.js';
1213
import {CLOSE_PAGE_ERROR, defineTool, timeoutSchema} from './ToolDefinition.js';
1314

14-
export const listPages = defineTool({
15-
name: 'list_pages',
16-
description: `Get a list of pages open in the browser.`,
17-
annotations: {
18-
category: ToolCategory.NAVIGATION,
19-
readOnlyHint: true,
20-
},
21-
schema: {},
22-
handler: async (_request, response) => {
23-
response.setIncludePages(true);
24-
},
15+
export const listPages = defineTool((args) => {
16+
return {
17+
name: 'list_pages',
18+
description: `Get a list of pages ${args.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`,
19+
annotations: {
20+
category: ToolCategory.NAVIGATION,
21+
readOnlyHint: true,
22+
},
23+
schema: {},
24+
handler: async (_request, response) => {
25+
response.setIncludePages(true);
26+
if(args.categoryExtensions) {
27+
response.setIncludeExtensionServiceWorkers(true);
28+
}
29+
},
30+
};
2531
});
2632

2733
export const selectPage = defineTool({
@@ -35,7 +41,7 @@ export const selectPage = defineTool({
3541
pageId: zod
3642
.number()
3743
.describe(
38-
`The ID of the page to select. Call ${listPages.name} to get available pages.`,
44+
`The ID of the page to select. Call ${listPages({} as ParsedArguments).name} to get available pages.`,
3945
),
4046
bringToFront: zod
4147
.boolean()
@@ -372,7 +378,7 @@ export const getTabId = defineTool({
372378
pageId: zod
373379
.number()
374380
.describe(
375-
`The ID of the page to get the tab ID for. Call ${listPages.name} to get available pages.`,
381+
`The ID of the page to get the tab ID for. Call ${listPages({} as ParsedArguments).name} to get available pages.`,
376382
),
377383
},
378384
handler: async (request, response, context) => {

src/tools/tools.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ export const createTools = (args: ParsedArguments) => {
4242
const tools: ToolDefinition[] = [];
4343
for (const tool of rawTools) {
4444
if (typeof tool === 'function') {
45-
// @ts-expect-error none of the tools for now implement the function type tool has type "never"
46-
tools.push(tool(args) as ToolDefinition);
45+
tools.push(tool(args));
4746
} else {
4847
tools.push(tool as ToolDefinition);
4948
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"manifest_version": 3,
3+
"name": "Test Extension with SW",
4+
"version": "1.0",
5+
"background": {
6+
"service_worker": "sw.js"
7+
},
8+
"action": {
9+
"default_popup": "popup.html"
10+
}
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!doctype html>
2+
<html>
3+
<body>
4+
<h1>Extension With Service Worker</h1>
5+
</body>
6+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('Service worker loaded');

tests/tools/pages.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
*/
66

77
import assert from 'node:assert';
8+
import path from 'node:path';
89
import {afterEach, describe, it} from 'node:test';
910

1011
import type {Dialog} from 'puppeteer-core';
1112
import sinon from 'sinon';
1213

14+
import type {ParsedArguments} from '../../src/cli.js';
15+
import {installExtension} from '../../src/tools/extensions.js';
1316
import {
1417
listPages,
1518
newPage,
@@ -22,6 +25,11 @@ import {
2225
} from '../../src/tools/pages.js';
2326
import {html, withMcpContext} from '../utils.js';
2427

28+
const EXTENSION_PATH = path.join(
29+
import.meta.dirname,
30+
'../../../tests/tools/fixtures/extension-sw',
31+
);
32+
2533
describe('pages', () => {
2634
afterEach(() => {
2735
sinon.restore();
@@ -30,10 +38,57 @@ describe('pages', () => {
3038
describe('list_pages', () => {
3139
it('list pages', async () => {
3240
await withMcpContext(async (response, context) => {
33-
await listPages.handler({params: {}}, response, context);
41+
await listPages({} as ParsedArguments).handler(
42+
{params: {}},
43+
response,
44+
context,
45+
);
3446
assert.ok(response.includePages);
3547
});
3648
});
49+
for (const categoryExtensions of [true, false]) {
50+
it(`list pages ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async () => {
51+
await withMcpContext(async (response, context) => {
52+
await installExtension.handler(
53+
{params: {path: EXTENSION_PATH}},
54+
response,
55+
context,
56+
);
57+
58+
const swTarget = await context.browser.waitForTarget(
59+
t =>
60+
t.type() === 'service_worker' &&
61+
t.url().includes('chrome-extension://'),
62+
);
63+
const swUrl = swTarget.url();
64+
65+
response.resetResponseLineForTesting();
66+
67+
const listPageDef = listPages({categoryExtensions} as ParsedArguments);
68+
await listPageDef.handler({params: {}}, response, context);
69+
70+
const result = await response.handle(listPageDef.name, context);
71+
const textContent = result.content.find(c => c.type === 'text') as {
72+
type: 'text';
73+
text: string;
74+
};
75+
assert.ok(textContent);
76+
77+
if (categoryExtensions) {
78+
assert.ok(textContent.text.includes(swUrl));
79+
const structured = result.structuredContent as {
80+
extensionServiceWorkers: Array<{url: string}>;
81+
};
82+
assert.deepStrictEqual(
83+
structured.extensionServiceWorkers.map(sw => sw.url),
84+
[swUrl],
85+
);
86+
} else {
87+
assert.ok(!textContent.text.includes(swUrl));
88+
}
89+
});
90+
});
91+
}
3792
});
3893
describe('new_page', () => {
3994
it('create a page', async () => {

0 commit comments

Comments
 (0)