chore: support channel and executable path params (#90)

Fixes https://github.com/microsoft/playwright-mcp/issues/89
This commit is contained in:
Pavel Feldman 2025-03-31 15:30:08 -07:00 committed by GitHub
parent d316441142
commit 9042c03faa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 2 deletions

View File

@ -14,6 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { fork } from 'child_process';
import path from 'path';
import * as playwright from 'playwright'; import * as playwright from 'playwright';
export type ContextOptions = { export type ContextOptions = {
@ -69,6 +72,25 @@ export class Context {
this._console.length = 0; this._console.length = 0;
} }
async install(): Promise<string> {
const channel = this._options.launchOptions?.channel || 'chrome';
const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js');
const child = fork(cli, ['install', channel], {
stdio: 'pipe',
});
const output: string[] = [];
child.stdout?.on('data', data => output.push(data.toString()));
child.stderr?.on('data', data => output.push(data.toString()));
return new Promise((resolve, reject) => {
child.on('close', code => {
if (code === 0)
resolve(channel);
else
reject(new Error(`Failed to install browser: ${output.join('')}`));
});
});
}
existingPage(): playwright.Page { existingPage(): playwright.Page {
if (!this._page) if (!this._page)
throw new Error('Navigate to a location to create a page'); throw new Error('Navigate to a location to create a page');
@ -119,11 +141,21 @@ export class Context {
return { browser, page }; return { browser, page };
} }
const context = await playwright.chromium.launchPersistentContext(this._options.userDataDir, this._options.launchOptions); const context = await this._launchPersistentContext();
const [page] = context.pages(); const [page] = context.pages();
return { page }; return { page };
} }
private async _launchPersistentContext(): Promise<playwright.BrowserContext> {
try {
return await playwright.chromium.launchPersistentContext(this._options.userDataDir, this._options.launchOptions);
} catch (error: any) {
if (error.message.includes('Executable doesn\'t exist'))
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
throw error;
}
}
async allFramesSnapshot() { async allFramesSnapshot() {
const page = this.existingPage(); const page = this.existingPage();
const visibleFrames = await page.locator('iframe').filter({ visible: true }).all(); const visibleFrames = await page.locator('iframe').filter({ visible: true }).all();

View File

@ -30,6 +30,7 @@ const commonTools: Tool[] = [
common.wait, common.wait,
common.pdf, common.pdf,
common.close, common.close,
common.install,
]; ];
const snapshotTools: Tool[] = [ const snapshotTools: Tool[] = [

View File

@ -40,10 +40,13 @@ program
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)') .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
.option('--port <port>', 'Port to listen on for SSE transport.') .option('--port <port>', 'Port to listen on for SSE transport.')
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.') .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
.option('--channel <channel>', 'Channel to use for browser, possible values: chrome, msedge, chromium. Default: chrome')
.option('--executable-path <path>', 'Path to the browser executable.')
.action(async options => { .action(async options => {
const launchOptions: LaunchOptions = { const launchOptions: LaunchOptions = {
headless: !!options.headless, headless: !!options.headless,
channel: 'chrome', channel: options.channel ?? 'chrome',
executablePath: options.executablePath,
}; };
const userDataDir = options.userDataDir ?? await createUserDataDir(); const userDataDir = options.userDataDir ?? await createUserDataDir();
const serverList = new ServerList(() => createServer({ const serverList = new ServerList(() => createServer({

View File

@ -174,3 +174,20 @@ export const chooseFile: ToolFactory = snapshot => ({
}, snapshot); }, snapshot);
}, },
}); });
export const install: Tool = {
schema: {
name: 'browser_install',
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
inputSchema: zodToJsonSchema(z.object({})),
},
handle: async context => {
const channel = await context.install();
return {
content: [{
type: 'text',
text: `Browser ${channel} installed`,
}],
};
},
};

View File

@ -36,6 +36,7 @@ test('test tool list', async ({ client, visionClient }) => {
'browser_wait', 'browser_wait',
'browser_save_as_pdf', 'browser_save_as_pdf',
'browser_close', 'browser_close',
'browser_install',
]); ]);
const { tools: visionTools } = await visionClient.listTools(); const { tools: visionTools } = await visionClient.listTools();
@ -53,6 +54,7 @@ test('test tool list', async ({ client, visionClient }) => {
'browser_wait', 'browser_wait',
'browser_save_as_pdf', 'browser_save_as_pdf',
'browser_close', 'browser_close',
'browser_install',
]); ]);
}); });
@ -353,3 +355,14 @@ test('save as pdf', async ({ client }) => {
}); });
expect(response).toHaveTextContent(/^Saved as.*page-[^:]+.pdf$/); expect(response).toHaveTextContent(/^Saved as.*page-[^:]+.pdf$/);
}); });
test('executable path', async ({ startClient }) => {
const client = await startClient({ args: [`--executable-path=bogus`] });
const response = await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
});
expect(response).toContainTextContent(`executable doesn't exist`);
});

View File

@ -71,6 +71,7 @@ export const test = baseTest.extend<Fixtures>({
cdpEndpoint: async ({ }, use, testInfo) => { cdpEndpoint: async ({ }, use, testInfo) => {
const port = 3200 + (+process.env.TEST_PARALLEL_INDEX!); const port = 3200 + (+process.env.TEST_PARALLEL_INDEX!);
const browser = await chromium.launchPersistentContext(testInfo.outputPath('user-data-dir'), { const browser = await chromium.launchPersistentContext(testInfo.outputPath('user-data-dir'), {
channel: 'chrome',
args: [`--remote-debugging-port=${port}`], args: [`--remote-debugging-port=${port}`],
}); });
await use(`http://localhost:${port}`); await use(`http://localhost:${port}`);