Skip to content

Commit 5d7f2c0

Browse files
committed
chore: experimental devtools
1 parent 139ce60 commit 5d7f2c0

6 files changed

Lines changed: 79 additions & 30 deletions

File tree

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: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,39 @@ import puppeteer from 'puppeteer-core';
1919

2020
let browser: Browser | undefined;
2121

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

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

41-
const connectOptions: ConnectOptions = {
42-
targetFilter,
43-
};
44-
45-
export async function ensureBrowserConnected(browserURL: string) {
45+
export async function ensureBrowserConnected(options: {
46+
browserURL: string;
47+
devtools: boolean;
48+
}) {
4649
if (browser?.connected) {
4750
return browser;
4851
}
4952
browser = await puppeteer.connect({
50-
...connectOptions,
51-
browserURL,
53+
targetFilter: makeTargetFilter(options.devtools),
54+
browserURL: options.browserURL,
5255
defaultViewport: null,
5356
});
5457
return browser;
@@ -68,7 +71,8 @@ interface McpLaunchOptions {
6871
height: number;
6972
};
7073
args?: string[];
71-
}
74+
devtools: boolean;
75+
};
7276

7377
export async function launch(options: McpLaunchOptions): Promise<Browser> {
7478
const {channel, executablePath, customDevTools, headless, isolated} = options;
@@ -101,6 +105,10 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
101105
args.push('--screen-info={3840x2160}');
102106
}
103107
let puppeteerChannel: ChromeReleaseChannel | undefined;
108+
if (options.devtools) {
109+
args.push('--auto-open-devtools-for-tabs');
110+
}
111+
let puppeterChannel: ChromeReleaseChannel | undefined;
104112
if (!executablePath) {
105113
puppeteerChannel =
106114
channel && channel !== 'stable'
@@ -110,8 +118,8 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
110118

111119
try {
112120
const browser = await puppeteer.launch({
113-
...connectOptions,
114121
channel: puppeteerChannel,
122+
targetFilter: makeTargetFilter(options.devtools),
115123
executablePath,
116124
defaultViewport: null,
117125
userDataDir,

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ 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+
default: false,
95+
hidden: true,
96+
},
9197
} satisfies Record<string, YargsOptions>;
9298

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

src/main.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ async function getContext(): Promise<McpContext> {
7474
extraArgs.push(`--proxy-server=${args.proxyServer}`);
7575
}
7676
const browser = args.browserUrl
77-
? await ensureBrowserConnected(args.browserUrl)
77+
? await ensureBrowserConnected({
78+
browserURL: args.browserUrl,
79+
devtools: args.experimentalDevTools,
80+
})
7881
: await ensureBrowserLaunched({
7982
headless: args.headless,
8083
executablePath: args.executablePath,
@@ -85,10 +88,13 @@ async function getContext(): Promise<McpContext> {
8588
viewport: args.viewport,
8689
args: extraArgs,
8790
acceptInsecureCerts: args.acceptInsecureCerts,
91+
devtools: args.experimentalDevTools,
8892
});
8993

9094
if (context?.browser !== browser) {
91-
context = await McpContext.from(browser, logger);
95+
context = await McpContext.from(browser, logger, {
96+
devtools: args.experimentalDevTools,
97+
});
9298
}
9399
return context;
94100
}

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)