Skip to content

Commit fff0658

Browse files
authored
feat(mcp): add --init-script option (#37507)
1 parent 78977d8 commit fff0658

File tree

6 files changed

+102
-0
lines changed

6 files changed

+102
-0
lines changed

packages/playwright/src/mcp/browser/DEPS.list

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
../sdk/
44
../log.ts
55
../package.ts
6+
../../util.ts

packages/playwright/src/mcp/browser/browserContextFactory.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class BaseContextFactory implements BrowserContextFactory {
7979
testDebug(`create browser context (${this._logName})`);
8080
const browser = await this._obtainBrowser(clientInfo);
8181
const browserContext = await this._doCreateContext(browser);
82+
await addInitScript(browserContext, this.config.browser.initScript);
8283
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
8384
}
8485

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

267+
async function addInitScript(browserContext: playwright.BrowserContext, initScript: string[] | undefined) {
268+
for (const scriptPath of initScript ?? [])
269+
await browserContext.addInitScript({ path: path.resolve(scriptPath) });
270+
}
271+
265272
export class SharedContextFactory implements BrowserContextFactory {
266273
private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> | undefined;
267274
private _baseFactory: BrowserContextFactory;

packages/playwright/src/mcp/browser/config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import path from 'path';
2020

2121
import { devices } from 'playwright-core';
2222
import { dotenv } from 'playwright-core/lib/utilsBundle';
23+
import { fileExistsAsync } from '../../util';
2324

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

@@ -42,6 +43,7 @@ export type CLIOptions = {
4243
headless?: boolean;
4344
host?: string;
4445
ignoreHttpsErrors?: boolean;
46+
initScript?: string[];
4547
isolated?: boolean;
4648
imageResponses?: 'allow' | 'omit';
4749
sandbox?: boolean;
@@ -114,9 +116,19 @@ export async function resolveCLIConfig(cliOptions: CLIOptions): Promise<FullConf
114116
result = mergeConfig(result, configInFile);
115117
result = mergeConfig(result, envOverrides);
116118
result = mergeConfig(result, cliOverrides);
119+
await validateConfig(result);
117120
return result;
118121
}
119122

123+
async function validateConfig(config: FullConfig): Promise<void> {
124+
if (config.browser.initScript) {
125+
for (const script of config.browser.initScript) {
126+
if (!await fileExistsAsync(script))
127+
throw new Error(`Init script file does not exist: ${script}`);
128+
}
129+
}
130+
}
131+
120132
export function configFromCLIOptions(cliOptions: CLIOptions): Config {
121133
let browserName: 'chromium' | 'firefox' | 'webkit' | undefined;
122134
let channel: string | undefined;
@@ -200,6 +212,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
200212
contextOptions,
201213
cdpEndpoint: cliOptions.cdpEndpoint,
202214
cdpHeaders: cliOptions.cdpHeader,
215+
initScript: cliOptions.initScript,
203216
},
204217
server: {
205218
port: cliOptions.port,
@@ -241,6 +254,9 @@ function configFromEnv(): Config {
241254
options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
242255
options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
243256
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
257+
const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
258+
if (initScript)
259+
options.initScript = [initScript];
244260
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
245261
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit')
246262
options.imageResponses = 'omit';

packages/playwright/src/mcp/config.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export type Config = {
6868
* Remote endpoint to connect to an existing Playwright server.
6969
*/
7070
remoteEndpoint?: string;
71+
72+
/**
73+
* Paths to JavaScript files to add as initialization scripts.
74+
* The scripts will be evaluated in every page before any of the page's scripts.
75+
*/
76+
initScript?: string[];
7177
},
7278

7379
server?: {

packages/playwright/src/mcp/program.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function decorateCommand(command: Command, version: string) {
4343
.option('--headless', 'run browser in headless mode, headed by default')
4444
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
4545
.option('--ignore-https-errors', 'ignore https errors')
46+
.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.')
4647
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
4748
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
4849
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')

tests/mcp/init-script.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from './fixtures';
18+
import fs from 'fs';
19+
20+
21+
for (const context of ['isolated', 'persistent']) {
22+
test(`--init-script option loads and executes script (${context})`, async ({ startClient, server }, testInfo) => {
23+
// Create a temporary init script
24+
const initScriptPath = testInfo.outputPath('init-script1.js');
25+
const initScriptContent1 = `window.testInitScriptExecuted = true;`;
26+
await fs.promises.writeFile(initScriptPath, initScriptContent1);
27+
28+
const initScriptPath2 = testInfo.outputPath('init-script2.js');
29+
const initScriptContent2 = `console.log('Init script executed successfully');`;
30+
await fs.promises.writeFile(initScriptPath2, initScriptContent2);
31+
32+
// Start the client with the init script option
33+
const { client: client } = await startClient({
34+
args: [`--init-script=${initScriptPath}`, `--init-script=${initScriptPath2}`, ...(context === 'isolated' ? ['--isolated'] : [])]
35+
});
36+
37+
// Navigate to a page and verify the init script was executed
38+
await client.callTool({
39+
name: 'browser_navigate',
40+
arguments: { url: server.HELLO_WORLD },
41+
});
42+
43+
await client.callTool({
44+
name: 'browser_evaluate',
45+
arguments: { function: '() => console.log("Custom log")' }
46+
});
47+
48+
// Check that the init script variables are available
49+
expect(await client.callTool({
50+
name: 'browser_evaluate',
51+
arguments: { function: '() => window.testInitScriptExecuted' }
52+
})).toHaveResponse({
53+
result: 'true',
54+
});
55+
56+
expect(await client.callTool({
57+
name: 'browser_console_messages',
58+
})).toHaveResponse({
59+
result: expect.stringMatching(/Init script executed successfully.*Custom log/ms),
60+
});
61+
});
62+
}
63+
64+
test('--init-script option with non-existent file throws error', async ({ startClient }, testInfo) => {
65+
const nonExistentPath = testInfo.outputPath('non-existent-script.js');
66+
67+
// Attempting to start with a non-existent init script should fail
68+
await expect(startClient({
69+
args: [`--init-script=${nonExistentPath}`]
70+
})).rejects.toThrow();
71+
});

0 commit comments

Comments
 (0)