Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/playwright/src/mcp/browser/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type CLIOptions = {
caps?: string[];
cdpEndpoint?: string;
cdpHeader?: Record<string, string>;
codegen?: 'typescript' | 'none';
config?: string;
consoleLevel?: 'error' | 'warning' | 'info' | 'debug';
device?: string;
Expand Down Expand Up @@ -249,6 +250,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
blockedOrigins: cliOptions.blockedOrigins,
},
allowUnrestrictedFileAccess: cliOptions.allowUnrestrictedFileAccess,
codegen: cliOptions.codegen,
saveSession: cliOptions.saveSession,
saveTrace: cliOptions.saveTrace,
saveVideo: cliOptions.saveVideo,
Expand Down
16 changes: 8 additions & 8 deletions packages/playwright/src/mcp/browser/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ export class Response {

// All the async snapshotting post-action is happening here.
// Everything below should race against modal states.
if (this._includeSnapshot !== 'none' && this._context.currentTab())
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
if (this._context.currentTab())
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshot !== 'none');
for (const tab of this._context.tabs())
await tab.updateTitle();
}
Expand All @@ -138,7 +138,7 @@ export class Response {
renderedResponse.results.push(...this._result);

// Add code if it exists.
if (this._code.length)
if (this._code.length && this._context.config.codegen !== 'none')
renderedResponse.code.push(...this._code);

// List browser tabs.
Expand All @@ -152,11 +152,11 @@ export class Response {
if (this._tabSnapshot?.modalStates.length) {
const modalStatesMarkdown = renderModalStates(this._tabSnapshot.modalStates);
renderedResponse.states.modal = modalStatesMarkdown.join('\n');
} else if (this._tabSnapshot) {
renderTabSnapshot(this._tabSnapshot, this._includeSnapshot, renderedResponse);
} else if (this._includeModalStates) {
const modalStatesMarkdown = renderModalStates(this._includeModalStates);
renderedResponse.states.modal = modalStatesMarkdown.join('\n');
} else if (this._tabSnapshot) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this move?

renderTabSnapshot(this._tabSnapshot, this._includeSnapshot, renderedResponse);
}

if (this._files.length) {
Expand Down Expand Up @@ -202,7 +202,7 @@ function renderTabSnapshot(tabSnapshot: TabSnapshot, includeSnapshot: 'none' | '
if (tabSnapshot.consoleMessages.length) {
const lines: string[] = [];
for (const message of tabSnapshot.consoleMessages)
lines.push(`- ${trim(message.toString(), 100)}`);
lines.push(`- ${trimMiddle(message.toString(), 100)}`);
response.updates.push({ category: 'console', content: lines.join('\n') });
}

Expand Down Expand Up @@ -254,10 +254,10 @@ function renderTabsMarkdown(tabs: Tab[], force: boolean = false): string[] {
return lines;
}

function trim(text: string, maxLength: number) {
function trimMiddle(text: string, maxLength: number) {
if (text.length <= maxLength)
return text;
return text.slice(0, maxLength) + '...';
return text.slice(0, Math.floor(maxLength / 2)) + '...' + text.slice(- 3 - Math.floor(maxLength / 2));
}

export class RenderedResponse {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/mcp/browser/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,11 @@ export class Tab extends EventEmitter<TabEventsInterface> {
return this._requests;
}

async captureSnapshot(): Promise<TabSnapshot> {
async captureSnapshot(includePageSnapshot: boolean): Promise<TabSnapshot> {
await this._initializedPromise;
let tabSnapshot: TabSnapshot | undefined;
const modalStates = await this._raceAgainstModalStates(async () => {
const snapshot = await this.page._snapshotForAI({ track: 'response' });
const snapshot = includePageSnapshot ? await this.page._snapshotForAI({ track: 'response' }) : { full: '', incremental: '' };
tabSnapshot = {
url: this.page.url(),
title: await this.page.title(),
Expand Down
12 changes: 11 additions & 1 deletion packages/playwright/src/mcp/browser/tools/navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ const navigate = defineTool({

handle: async (context, params, response) => {
const tab = await context.ensureTab();
await tab.navigate(params.url);
let url = params.url;
try {
new URL(url);
} catch (e) {
if (url.startsWith('localhost'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the right place for the check.

url = 'http://' + url;
else
url = 'https://' + url;
}

await tab.navigate(url);

response.setIncludeSnapshot();
response.addCode(`await page.goto('${params.url}');`);
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright/src/mcp/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,9 @@ export type Config = {
* By default (false), file uploads are restricted to paths within the MCP roots only.
*/
allowUnrestrictedFileAccess?: boolean;

/**
* Specify the language to use for code generation.
*/
codegen?: 'typescript' | 'none';
};
3 changes: 3 additions & 0 deletions packages/playwright/src/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function decorateCommand(command: Command, version: string) {
.option('--user-agent <ua string>', 'specify user agent string')
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280x720"', resolutionParser.bind(null, '--viewport-size'))
.option('--codegen <lang>', 'specify the language to use for code generation, possible values: "typescript", "none". Default is "typescript".', enumParser.bind(null, '--codegen', ['none', 'typescript']))
.addOption(new ProgramOption('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
.addOption(new ProgramOption('--daemon <socket>', 'run as daemon').hideHelp())
.action(async options => {
Expand Down Expand Up @@ -109,6 +110,8 @@ export function decorateCommand(command: Command, version: string) {
}

if (options.daemon) {
config.snapshot.mode = 'none';
config.codegen = 'none';
const serverBackendFactory: mcpServer.ServerBackendFactory = {
name: 'Playwright',
nameInConfig: 'playwright-daemon',
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright/src/mcp/terminal/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ addCommand('click <ref>', 'click an element using a ref from a snapshot, e.g. e6
});

addCommand('snapshot', 'get accessible snapshot of the current page', async () => {
await runMcpCommand('browser_snapshot', {});
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
await runMcpCommand('browser_snapshot', { filename: `snapshot-${timestamp}.md` });
});

addCommand('drag <startRef> <endRef>', 'drag from one element to another', async (startRef, endRef) => {
Expand Down
6 changes: 4 additions & 2 deletions tests/mcp/secrets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ await page.getByRole('textbox').press('Enter');` },
{ category: 'console', content: expect.stringContaining('[LOG] Key pressed: Enter , Text: <secret>X-PASSWORD</secret>') },
],
'dev.lowire/state': {
'page': expect.stringMatching(/textbox (\[active\] )?\[ref=e2\]: <secret>X-PASSWORD<\/secret>/),
page: expect.stringMatching(/textbox (\[active\] )?\[ref=e2\]: <secret>X-PASSWORD<\/secret>/),
},
});
}
Expand All @@ -187,7 +187,9 @@ await page.getByRole('textbox').press('Enter');` },
'dev.lowire/history': [
{ category: 'result', content: expect.stringContaining('[LOG] Key pressed: Enter , Text: <secret>X-PASSWORD</secret>') },
],
'dev.lowire/state': {},
'dev.lowire/state': {
page: expect.any(String),
},
});
}
});
3 changes: 2 additions & 1 deletion tests/mcp/snapshot-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ test('should respect --snapshot-mode=none', async ({ startClient, server }) => {
url: server.PREFIX,
},
})).toHaveResponse({
pageState: undefined
pageState: `- Page URL: ${server.PREFIX}/
- Page Title:`
});
});

Expand Down