Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/playwright/src/mcp/browser/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
../sdk/
../log.ts
../package.ts
../../util.ts
7 changes: 7 additions & 0 deletions packages/playwright/src/mcp/browser/browserContextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class BaseContextFactory implements BrowserContextFactory {
testDebug(`create browser context (${this._logName})`);
const browser = await this._obtainBrowser(clientInfo);
const browserContext = await this._doCreateContext(browser);
await addInitScript(browserContext, this.config.browser.initScript);
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
}

Expand Down Expand Up @@ -195,6 +196,7 @@ class PersistentContextFactory implements BrowserContextFactory {
};
try {
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
await addInitScript(browserContext, this.config.browser.initScript);
const close = () => this._closeBrowserContext(browserContext, userDataDir);
return { browserContext, close };
} catch (error: any) {
Expand Down Expand Up @@ -262,6 +264,11 @@ function createHash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
}

async function addInitScript(browserContext: playwright.BrowserContext, initScript: string[] | undefined) {
for (const scriptPath of initScript ?? [])
await browserContext.addInitScript({ path: path.resolve(scriptPath) });
}

export class SharedContextFactory implements BrowserContextFactory {
private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> | undefined;
private _baseFactory: BrowserContextFactory;
Expand Down
16 changes: 16 additions & 0 deletions packages/playwright/src/mcp/browser/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import path from 'path';

import { devices } from 'playwright-core';
import { dotenv } from 'playwright-core/lib/utilsBundle';
import { fileExistsAsync } from '../../util';

import { firstRootPath } from '../sdk/server';

Expand All @@ -42,6 +43,7 @@ export type CLIOptions = {
headless?: boolean;
host?: string;
ignoreHttpsErrors?: boolean;
initScript?: string[];
isolated?: boolean;
imageResponses?: 'allow' | 'omit';
sandbox?: boolean;
Expand Down Expand Up @@ -114,9 +116,19 @@ export async function resolveCLIConfig(cliOptions: CLIOptions): Promise<FullConf
result = mergeConfig(result, configInFile);
result = mergeConfig(result, envOverrides);
result = mergeConfig(result, cliOverrides);
await validateConfig(result);
return result;
}

async function validateConfig(config: FullConfig): Promise<void> {
if (config.browser.initScript) {
for (const script of config.browser.initScript) {
if (!await fileExistsAsync(script))
throw new Error(`Init script file does not exist: ${script}`);
}
}
}

export function configFromCLIOptions(cliOptions: CLIOptions): Config {
let browserName: 'chromium' | 'firefox' | 'webkit' | undefined;
let channel: string | undefined;
Expand Down Expand Up @@ -200,6 +212,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
contextOptions,
cdpEndpoint: cliOptions.cdpEndpoint,
cdpHeaders: cliOptions.cdpHeader,
initScript: cliOptions.initScript,
},
server: {
port: cliOptions.port,
Expand Down Expand Up @@ -241,6 +254,9 @@ function configFromEnv(): Config {
options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
if (initScript)
options.initScript = [initScript];
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit')
options.imageResponses = 'omit';
Expand Down
6 changes: 6 additions & 0 deletions packages/playwright/src/mcp/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export type Config = {
* Remote endpoint to connect to an existing Playwright server.
*/
remoteEndpoint?: string;

/**
* Paths to JavaScript files to add as initialization scripts.
* The scripts will be evaluated in every page before any of the page's scripts.
*/
initScript?: string[];
},

server?: {
Expand Down
1 change: 1 addition & 0 deletions packages/playwright/src/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function decorateCommand(command: Command, version: string) {
.option('--headless', 'run browser in headless mode, headed by default')
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
.option('--ignore-https-errors', 'ignore https errors')
.option('--init-script <path...>', 'path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page\'s scripts. Can be specified multiple times.')
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
Expand Down
71 changes: 71 additions & 0 deletions tests/mcp/init-script.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { test, expect } from './fixtures';
import fs from 'fs';


for (const context of ['isolated', 'persistent']) {
test(`--init-script option loads and executes script (${context})`, async ({ startClient, server }, testInfo) => {
// Create a temporary init script
const initScriptPath = testInfo.outputPath('init-script1.js');
const initScriptContent1 = `window.testInitScriptExecuted = true;`;
await fs.promises.writeFile(initScriptPath, initScriptContent1);

const initScriptPath2 = testInfo.outputPath('init-script2.js');
const initScriptContent2 = `console.log('Init script executed successfully');`;
await fs.promises.writeFile(initScriptPath2, initScriptContent2);

// Start the client with the init script option
const { client: client } = await startClient({
args: [`--init-script=${initScriptPath}`, `--init-script=${initScriptPath2}`, ...(context === 'isolated' ? ['--isolated'] : [])]
});

// Navigate to a page and verify the init script was executed
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});

await client.callTool({
name: 'browser_evaluate',
arguments: { function: '() => console.log("Custom log")' }
});

// Check that the init script variables are available
expect(await client.callTool({
name: 'browser_evaluate',
arguments: { function: '() => window.testInitScriptExecuted' }
})).toHaveResponse({
result: 'true',
});

expect(await client.callTool({
name: 'browser_console_messages',
})).toHaveResponse({
result: expect.stringMatching(/Init script executed successfully.*Custom log/ms),
});
});
}

test('--init-script option with non-existent file throws error', async ({ startClient }, testInfo) => {
const nonExistentPath = testInfo.outputPath('non-existent-script.js');

// Attempting to start with a non-existent init script should fail
await expect(startClient({
args: [`--init-script=${nonExistentPath}`]
})).rejects.toThrow();
});
Loading