Skip to content
Draft
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
32 changes: 28 additions & 4 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,16 @@ export class McpContext implements Context {

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

private constructor(browser: Browser, logger: Debugger) {
#devtools = false;

private constructor(
browser: Browser,
logger: Debugger,
options: {
devtools: boolean;
},
) {
this.#devtools = options.devtools;
this.browser = browser;
this.logger = logger;

Expand Down Expand Up @@ -85,8 +93,14 @@ export class McpContext implements Context {
await this.#consoleCollector.init();
}

static async from(browser: Browser, logger: Debugger) {
const context = new McpContext(browser, logger);
static async from(
browser: Browser,
logger: Debugger,
options: {
devtools: boolean;
},
) {
const context = new McpContext(browser, logger, options);
await context.#init();
return context;
}
Expand Down Expand Up @@ -229,6 +243,16 @@ export class McpContext implements Context {
*/
async createPagesSnapshot(): Promise<Page[]> {
this.#pages = await this.browser.pages();
if (this.#devtools) {
for (const target of this.browser.targets()) {
if (
target.type() === 'other' &&
target.url().startsWith('devtools://')
) {
this.#pages.push(await target.asPage());
}
}
}
return this.#pages;
}

Expand Down
64 changes: 38 additions & 26 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,45 @@ import fs from 'fs';

let browser: Browser | undefined;

const ignoredPrefixes = new Set([
'chrome://',
'chrome-extension://',
'chrome-untrusted://',
'devtools://',
]);
function makeTargetFilter(devtools: boolean) {
const ignoredPrefixes = new Set([
'chrome://',
'chrome-extension://',
'chrome-untrusted://',
]);

function targetFilter(target: Target): boolean {
if (target.url() === 'chrome://newtab/') {
return true;
if (!devtools) {
ignoredPrefixes.add('devtools://');
}
for (const prefix of ignoredPrefixes) {
if (target.url().startsWith(prefix)) {
return false;
return function targetFilter(target: Target): boolean {
if (target.url() === 'chrome://newtab/') {
return true;
}
}
return true;
for (const prefix of ignoredPrefixes) {
if (target.url().startsWith(prefix)) {
return false;
}
}
return true;
};
}

const connectOptions: ConnectOptions = {
targetFilter,
// We do not expect any single CDP command to take more than 10sec.
protocolTimeout: 10_000,
};

async function ensureBrowserConnected(browserURL: string) {
async function ensureBrowserConnected(options: {
browserURL: string;
devtools: boolean;
}) {
if (browser?.connected) {
return browser;
}
browser = await puppeteer.connect({
...connectOptions,
browserURL,
targetFilter: makeTargetFilter(options.devtools),
browserURL: options.browserURL,
defaultViewport: null,
});
return browser;
Expand All @@ -61,6 +68,7 @@ type McpLaunchOptions = {
userDataDir?: string;
headless: boolean;
isolated: boolean;
devtools: boolean;
};

export async function launch(options: McpLaunchOptions): Promise<Browser> {
Expand Down Expand Up @@ -91,6 +99,9 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
if (customDevTools) {
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
}
if (options.devtools) {
args.push('--auto-open-devtools-for-tabs');
}
let puppeterChannel: ChromeReleaseChannel | undefined;
if (!executablePath) {
puppeterChannel =
Expand All @@ -102,6 +113,7 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
try {
return await puppeteer.launch({
...connectOptions,
targetFilter: makeTargetFilter(options.devtools),
channel: puppeterChannel,
executablePath,
defaultViewport: null,
Expand Down Expand Up @@ -138,16 +150,16 @@ async function ensureBrowserLaunched(
return browser;
}

export async function resolveBrowser(options: {
browserUrl?: string;
executablePath?: string;
customDevTools?: string;
channel?: Channel;
headless: boolean;
isolated: boolean;
}) {
export async function resolveBrowser(
options: McpLaunchOptions & {
browserUrl?: string;
},
) {
const browser = options.browserUrl
? await ensureBrowserConnected(options.browserUrl)
? await ensureBrowserConnected({
browserURL: options.browserUrl,
devtools: options.devtools,
})
: await ensureBrowserLaunched(options);

return browser;
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export const cliOptions = {
describe: 'Save the logs to file.',
hidden: true,
},
experimentalDevTools: {
type: 'boolean' as const,
describe: 'Whether to enable automation over DevTools targets',
default: false,
hidden: true,
},
};

const yargsInstance = yargs(hideBin(process.argv))
Expand Down Expand Up @@ -155,9 +161,12 @@ async function getContext(): Promise<McpContext> {
customDevTools: args.customDevtools,
channel: args.channel as Channel,
isolated: args.isolated,
devtools: args.experimentalDevTools,
});
if (context?.browser !== browser) {
context = await McpContext.from(browser, logger);
context = await McpContext.from(browser, logger, {
devtools: args.experimentalDevTools,
});
}
return context;
}
Expand Down
2 changes: 2 additions & 0 deletions tests/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ describe('browser', () => {
headless: true,
isolated: false,
userDataDir: folderPath,
devtools: false,
});
try {
try {
const browser2 = await launch({
headless: true,
isolated: false,
userDataDir: folderPath,
devtools: false,
});
await browser2.close();
assert.fail('not reached');
Expand Down
4 changes: 3 additions & 1 deletion tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export async function withBrowser(
}),
);
const response = new McpResponse();
const context = await McpContext.from(browser, logger('test'));
const context = await McpContext.from(browser, logger('test'), {
devtools: false,
});

await cb(response, context);
}
Expand Down
Loading