Skip to content

Commit efd7b61

Browse files
authored
chore(mcp): introduce init-page (#38167)
1 parent c22c883 commit efd7b61

File tree

7 files changed

+63
-4
lines changed

7 files changed

+63
-4
lines changed

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

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

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type CLIOptions = {
4646
host?: string;
4747
ignoreHttpsErrors?: boolean;
4848
initScript?: string[];
49+
initPage?: string[];
4950
isolated?: boolean;
5051
imageResponses?: 'allow' | 'omit';
5152
sandbox?: boolean;
@@ -218,6 +219,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
218219
contextOptions,
219220
cdpEndpoint: cliOptions.cdpEndpoint,
220221
cdpHeaders: cliOptions.cdpHeader,
222+
initPage: cliOptions.initPage,
221223
initScript: cliOptions.initScript,
222224
},
223225
server: {
@@ -264,6 +266,9 @@ function configFromEnv(): Config {
264266
options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
265267
options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
266268
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
269+
const initPage = envToString(process.env.PLAYWRIGHT_MCP_INIT_PAGE);
270+
if (initPage)
271+
options.initPage = [initPage];
267272
const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
268273
if (initScript)
269274
options.initScript = [initScript];

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export class Context {
9090
const { browserContext } = await this._ensureBrowserContext();
9191
const page = await browserContext.newPage();
9292
this._currentTab = this._tabs.find(t => t.page === page)!;
93+
await this._currentTab.initializedPromise;
9394
return this._currentTab;
9495
}
9596

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { logUnhandledError } from '../log';
2323
import { ModalState } from './tools/tool';
2424
import { handleDialog } from './tools/dialogs';
2525
import { uploadFile } from './tools/files';
26+
import { requireOrImport } from '../../transform/transform';
2627

2728
import type { Context } from './context';
2829
import type { Page } from '../../../../playwright-core/src/client/page';
@@ -56,7 +57,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
5657
private _onPageClose: (tab: Tab) => void;
5758
private _modalStates: ModalState[] = [];
5859
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
59-
private _initializedPromise: Promise<void>;
60+
readonly initializedPromise: Promise<void>;
6061
private _needsFullSnapshot = false;
6162

6263
constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) {
@@ -83,7 +84,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
8384
page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
8485
page.setDefaultTimeout(this.context.config.timeouts.action);
8586
(page as any)[tabSymbol] = this;
86-
this._initializedPromise = this._initialize();
87+
this.initializedPromise = this._initialize();
8788
}
8889

8990
static forPage(page: playwright.Page): Tab | undefined {
@@ -107,6 +108,14 @@ export class Tab extends EventEmitter<TabEventsInterface> {
107108
const requests = await this.page.requests().catch(() => []);
108109
for (const request of requests)
109110
this._requests.add(request);
111+
for (const initPage of this.context.config.browser.initPage || []) {
112+
try {
113+
const { default: func } = await requireOrImport(initPage);
114+
await func({ page: this.page });
115+
} catch (e) {
116+
logUnhandledError(e);
117+
}
118+
}
110119
}
111120

112121
modalStates(): ModalState[] {
@@ -210,12 +219,12 @@ export class Tab extends EventEmitter<TabEventsInterface> {
210219
}
211220

212221
async consoleMessages(type?: 'error'): Promise<ConsoleMessage[]> {
213-
await this._initializedPromise;
222+
await this.initializedPromise;
214223
return this._consoleMessages.filter(message => type ? message.type === type : true);
215224
}
216225

217226
async requests(): Promise<Set<playwright.Request>> {
218-
await this._initializedPromise;
227+
await this.initializedPromise;
219228
return this._requests;
220229
}
221230

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export type Config = {
6969
*/
7070
remoteEndpoint?: string;
7171

72+
/**
73+
* Paths to TypeScript files to add as initialization scripts for Playwright page.
74+
*/
75+
initPage?: string[];
76+
7277
/**
7378
* Paths to JavaScript files to add as initialization scripts.
7479
* The scripts will be evaluated in every page before any of the page's scripts.

packages/playwright/src/mcp/program.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function decorateCommand(command: Command, version: string) {
5050
.option('--headless', 'run browser in headless mode, headed by default')
5151
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
5252
.option('--ignore-https-errors', 'ignore https errors')
53+
.option('--init-page <path...>', 'path to TypeScript file to evaluate on Playwright page object')
5354
.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.')
5455
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
5556
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')

tests/mcp/init-page.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 fs from 'fs';
18+
19+
import { test, expect } from './fixtures';
20+
21+
test('--init-page', async ({ startClient }) => {
22+
const initPagePath = test.info().outputPath('aa.ts');
23+
await fs.promises.writeFile(initPagePath, `
24+
export default async ({ page }) => {
25+
await page.setContent('<div>Hello world</div>');
26+
};
27+
`);
28+
const { client } = await startClient({
29+
args: [`--init-page=${initPagePath}`],
30+
});
31+
expect(await client.callTool({
32+
name: 'browser_snapshot',
33+
arguments: { url: 'about:blank' },
34+
})).toHaveResponse({
35+
pageState: expect.stringContaining('Hello world'),
36+
});
37+
});

0 commit comments

Comments
 (0)