Skip to content

Commit 2722080

Browse files
committed
chore: experimental devtools
1 parent 11a634e commit 2722080

File tree

6 files changed

+93
-31
lines changed

6 files changed

+93
-31
lines changed

src/McpContext.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,16 @@ export class McpContext implements Context {
8989

9090
#nextSnapshotId = 1;
9191
#traceResults: TraceResult[] = [];
92-
93-
private constructor(browser: Browser, logger: Debugger) {
92+
#devtools = false;
93+
94+
private constructor(
95+
browser: Browser,
96+
logger: Debugger,
97+
options: {
98+
devtools: boolean;
99+
},
100+
) {
101+
this.#devtools = options.devtools;
94102
this.browser = browser;
95103
this.logger = logger;
96104

@@ -123,8 +131,14 @@ export class McpContext implements Context {
123131
await this.#consoleCollector.init();
124132
}
125133

126-
static async from(browser: Browser, logger: Debugger) {
127-
const context = new McpContext(browser, logger);
134+
static async from(
135+
browser: Browser,
136+
logger: Debugger,
137+
options: {
138+
devtools: boolean;
139+
},
140+
) {
141+
const context = new McpContext(browser, logger, options);
128142
await context.#init();
129143
return context;
130144
}
@@ -302,6 +316,16 @@ export class McpContext implements Context {
302316
*/
303317
async createPagesSnapshot(): Promise<Page[]> {
304318
this.#pages = await this.browser.pages();
319+
// if (this.#devtools) {
320+
// for (const target of this.browser.targets()) {
321+
// if (
322+
// target.type() === 'other' &&
323+
// target.url().startsWith('devtools://')
324+
// ) {
325+
// this.#pages.push(await target.asPage());
326+
// }
327+
// }
328+
// }
305329
return this.#pages;
306330
}
307331

src/browser.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,45 +11,53 @@ import path from 'node:path';
1111
import type {
1212
Browser,
1313
ChromeReleaseChannel,
14-
ConnectOptions,
1514
LaunchOptions,
1615
Target,
1716
} from 'puppeteer-core';
1817
import puppeteer from 'puppeteer-core';
1918

2019
let browser: Browser | undefined;
2120

22-
const ignoredPrefixes = new Set([
23-
'chrome://',
24-
'chrome-extension://',
25-
'chrome-untrusted://',
26-
'devtools://',
27-
]);
21+
function makeTargetFilter(devtools: boolean) {
22+
const ignoredPrefixes = new Set([
23+
'chrome://',
24+
'chrome-extension://',
25+
'chrome-untrusted://',
26+
]);
2827

29-
function targetFilter(target: Target): boolean {
30-
if (target.url() === 'chrome://newtab/') {
31-
return true;
28+
if (!devtools) {
29+
ignoredPrefixes.add('devtools://');
3230
}
33-
for (const prefix of ignoredPrefixes) {
34-
if (target.url().startsWith(prefix)) {
35-
return false;
31+
return function targetFilter(target: Target): boolean {
32+
if (target.url() === 'chrome://newtab/') {
33+
return true;
3634
}
37-
}
38-
return true;
35+
for (const prefix of ignoredPrefixes) {
36+
if (target.url().startsWith(prefix)) {
37+
return false;
38+
}
39+
}
40+
return true;
41+
};
3942
}
4043

41-
const connectOptions: ConnectOptions = {
42-
targetFilter,
43-
};
44-
45-
export async function ensureBrowserConnected(browserURL: string) {
44+
export async function ensureBrowserConnected(options: {
45+
browserURL: string;
46+
devtools: boolean;
47+
}) {
4648
if (browser?.connected) {
4749
return browser;
4850
}
4951
browser = await puppeteer.connect({
50-
...connectOptions,
51-
browserURL,
52+
targetFilter: makeTargetFilter(options.devtools),
53+
browserURL: options.browserURL,
5254
defaultViewport: null,
55+
// @ts-expect-error no types.
56+
_isPageTarget(target) {
57+
return (
58+
target.type() === 'other' && target.url().startsWith('devtools://')
59+
);
60+
},
5361
});
5462
return browser;
5563
}
@@ -68,7 +76,8 @@ interface McpLaunchOptions {
6876
height: number;
6977
};
7078
args?: string[];
71-
}
79+
devtools: boolean;
80+
};
7281

7382
export async function launch(options: McpLaunchOptions): Promise<Browser> {
7483
const {channel, executablePath, customDevTools, headless, isolated} = options;
@@ -101,6 +110,9 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
101110
args.push('--screen-info={3840x2160}');
102111
}
103112
let puppeteerChannel: ChromeReleaseChannel | undefined;
113+
if (options.devtools) {
114+
args.push('--auto-open-devtools-for-tabs');
115+
}
104116
if (!executablePath) {
105117
puppeteerChannel =
106118
channel && channel !== 'stable'
@@ -110,15 +122,21 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
110122

111123
try {
112124
const browser = await puppeteer.launch({
113-
...connectOptions,
114125
channel: puppeteerChannel,
126+
targetFilter: makeTargetFilter(options.devtools),
115127
executablePath,
116128
defaultViewport: null,
117129
userDataDir,
118130
pipe: true,
119131
headless,
120132
args,
121133
acceptInsecureCerts: options.acceptInsecureCerts,
134+
// @ts-expect-error no types.
135+
_isPageTarget(target) {
136+
return (
137+
target.type() === 'other' && target.url().startsWith('devtools://')
138+
);
139+
},
122140
});
123141
if (options.logFile) {
124142
// FIXME: we are probably subscribing too late to catch startup logs. We

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ export const cliOptions = {
8888
type: 'boolean',
8989
description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`,
9090
},
91+
experimentalDevtools: {
92+
type: 'boolean' as const,
93+
describe: 'Whether to enable automation over DevTools targets',
94+
hidden: true,
95+
},
9196
} satisfies Record<string, YargsOptions>;
9297

9398
export function parseArguments(version: string, argv = process.argv) {

src/main.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,12 @@ async function getContext(): Promise<McpContext> {
7373
if (args.proxyServer) {
7474
extraArgs.push(`--proxy-server=${args.proxyServer}`);
7575
}
76+
const devtools = args.experimentalDevtools ?? false;
7677
const browser = args.browserUrl
77-
? await ensureBrowserConnected(args.browserUrl)
78+
? await ensureBrowserConnected({
79+
browserURL: args.browserUrl,
80+
devtools,
81+
})
7882
: await ensureBrowserLaunched({
7983
headless: args.headless,
8084
executablePath: args.executablePath,
@@ -85,10 +89,13 @@ async function getContext(): Promise<McpContext> {
8589
viewport: args.viewport,
8690
args: extraArgs,
8791
acceptInsecureCerts: args.acceptInsecureCerts,
92+
devtools,
8893
});
8994

9095
if (context?.browser !== browser) {
91-
context = await McpContext.from(browser, logger);
96+
context = await McpContext.from(browser, logger, {
97+
devtools,
98+
});
9299
}
93100
return context;
94101
}
@@ -143,6 +150,9 @@ function registerTool(tool: ToolDefinition): void {
143150
isError: true,
144151
};
145152
}
153+
} catch (err) {
154+
logger(`${tool.name} error: ${err.message}`);
155+
throw err;
146156
} finally {
147157
guard.dispose();
148158
}

tests/browser.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('browser', () => {
2121
isolated: false,
2222
userDataDir: folderPath,
2323
executablePath: executablePath(),
24+
devtools: false,
2425
});
2526
try {
2627
try {
@@ -29,6 +30,7 @@ describe('browser', () => {
2930
isolated: false,
3031
userDataDir: folderPath,
3132
executablePath: executablePath(),
33+
devtools: false,
3234
});
3335
await browser2.close();
3436
assert.fail('not reached');
@@ -55,6 +57,7 @@ describe('browser', () => {
5557
width: 1501,
5658
height: 801,
5759
},
60+
devtools: false,
5861
});
5962
try {
6063
const [page] = await browser.pages();

tests/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export async function withBrowser(
3535
}),
3636
);
3737
const response = new McpResponse();
38-
const context = await McpContext.from(browser, logger('test'));
38+
const context = await McpContext.from(browser, logger('test'), {
39+
devtools: false,
40+
});
3941

4042
await cb(response, context);
4143
}

0 commit comments

Comments
 (0)