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
2 changes: 1 addition & 1 deletion docs/src/getting-started-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Playwright MCP provides tools for all common browser interactions:

### Running Playwright code

For complex interactions that go beyond individual tool calls, use the `browser_run_code` tool to execute Playwright scripts directly:
For complex interactions that go beyond individual tool calls, use the `browser_run_code_unsafe` tool to execute Playwright scripts directly. This tool runs arbitrary JavaScript in the Playwright server process and is RCE-equivalent — only enable it for trusted MCP clients:

```txt
Run this Playwright code to verify the todo count:
Expand Down
2 changes: 1 addition & 1 deletion examples/todomvc/.claude/agents/playwright-test-planner.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: playwright-test-planner
description: Use this agent when you need to create comprehensive test plan for a web application or website
tools: Glob, Grep, Read, LS, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_run_code, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page, mcp__playwright-test__planner_save_plan
tools: Glob, Grep, Read, LS, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_run_code_unsafe, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page, mcp__playwright-test__planner_save_plan
model: sonnet
color: green
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tools:
- playwright-test/browser_navigate_back
- playwright-test/browser_network_requests
- playwright-test/browser_press_key
- playwright-test/browser_run_code
- playwright-test/browser_run_code_unsafe
- playwright-test/browser_select_option
- playwright-test/browser_snapshot
- playwright-test/browser_take_screenshot
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/tools/backend/runCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const codeSchema = z.object({
const runCode = defineTabTool({
capability: 'core',
schema: {
name: 'browser_run_code',
title: 'Run Playwright code',
description: 'Run Playwright code snippet',
name: 'browser_run_code_unsafe',
title: 'Run Playwright code (unsafe)',
description: 'Run a Playwright code snippet. Unsafe: executes arbitrary JavaScript in the Playwright server process and is RCE-equivalent.',
inputSchema: codeSchema,
type: 'action',
},
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/tools/cli-daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ const runCode = declareCommand({
options: z.object({
filename: z.string().optional().describe('Load code from the specified file.'),
}),
toolName: 'browser_run_code',
toolName: 'browser_run_code_unsafe',
toolParams: ({ code, filename }) => ({ code, filename }),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ tools:
- playwright-test/browser_network_request
- playwright-test/browser_network_requests
- playwright-test/browser_press_key
- playwright-test/browser_run_code
- playwright-test/browser_run_code_unsafe
- playwright-test/browser_select_option
- playwright-test/browser_snapshot
- playwright-test/browser_take_screenshot
Expand Down
6 changes: 3 additions & 3 deletions tests/extension/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ test(`protocolVersion defaults to 1`, async ({ startExtensionClient, server, pro
process.env.PLAYWRIGHT_EXTENSION_PROTOCOL = saved;
});

test(`browser_run_code can evaluate in a web worker`, async ({ startExtensionClient, server, protocolVersion }) => {
test(`browser_run_code_unsafe can evaluate in a web worker`, async ({ startExtensionClient, server, protocolVersion }) => {
test.skip(protocolVersion === 1, 'Multi-tab not supported in protocol v1');
server.setContent('/worker.js', `
self.onmessage = (e) => self.postMessage('echo:' + e.data);
Expand Down Expand Up @@ -109,7 +109,7 @@ test(`browser_run_code can evaluate in a web worker`, async ({ startExtensionCli
await navigateResponse;

const runCodeResponse = await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: {
code: `async (page) => {
const worker = page.workers().length ? page.workers()[0] : await page.waitForEvent('worker');
Expand Down Expand Up @@ -144,7 +144,7 @@ test(`browser_run_code can evaluate in a web worker`, async ({ startExtensionCli
});

const runCodeResponse2 = await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: {
code: `async (page) => {
const worker = page.workers().length ? page.workers()[0] : await page.waitForEvent('worker');
Expand Down
2 changes: 1 addition & 1 deletion tests/mcp/capabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test('test snapshot tool list', async ({ client }) => {
'browser_network_requests',
'browser_press_key',
'browser_resize',
'browser_run_code',
'browser_run_code_unsafe',
'browser_snapshot',
'browser_tabs',
'browser_take_screenshot',
Expand Down
2 changes: 1 addition & 1 deletion tests/mcp/generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test('generator tools intent', async ({ startClient }) => {
expect(toolsWithIntent).toContain('browser_press_key');
expect(toolsWithIntent).toContain('browser_press_sequentially');
expect(toolsWithIntent).toContain('browser_resize');
expect(toolsWithIntent).toContain('browser_run_code');
expect(toolsWithIntent).toContain('browser_run_code_unsafe');
expect(toolsWithIntent).toContain('browser_select_option');
expect(toolsWithIntent).toContain('browser_tabs');
});
Expand Down
28 changes: 14 additions & 14 deletions tests/mcp/run-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import fs from 'fs';

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

test('browser_run_code', async ({ client, server }) => {
test('browser_run_code_unsafe', async ({ client, server }) => {
server.setContent('/', `
<button onclick="console.log('Submit')">Submit</button>
`, 'text/html');
Expand All @@ -29,7 +29,7 @@ test('browser_run_code', async ({ client, server }) => {

const code = 'async (page) => await page.getByRole("button", { name: "Submit" }).click()';
const response = parseResponse(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: {
code,
},
Expand All @@ -38,7 +38,7 @@ test('browser_run_code', async ({ client, server }) => {
expect(content).toContain('[LOG] Submit');
});

test('browser_run_code block', async ({ client, server }) => {
test('browser_run_code_unsafe block', async ({ client, server }) => {
server.setContent('/', `
<button onclick="console.log('Submit')">Submit</button>
`, 'text/html');
Expand All @@ -48,7 +48,7 @@ test('browser_run_code block', async ({ client, server }) => {
});

const response = parseResponse(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: {
code: 'async (page) => { await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click(); }',
},
Expand All @@ -62,7 +62,7 @@ test('browser_run_code block', async ({ client, server }) => {
expect(content).toMatch(/\[LOG\] Submit.*\n.*\[LOG\] Submit/);
});

test('browser_run_code no-require', async ({ client, server }) => {
test('browser_run_code_unsafe no-require', async ({ client, server }) => {
server.setContent('/', `
<button onclick="console.log('Submit')">Submit</button>
`, 'text/html');
Expand All @@ -72,7 +72,7 @@ test('browser_run_code no-require', async ({ client, server }) => {
});

expect(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: {
code: `(page) => { require('fs'); }`,
},
Expand All @@ -82,7 +82,7 @@ test('browser_run_code no-require', async ({ client, server }) => {
});
});

test('browser_run_code return value', async ({ client, server }) => {
test('browser_run_code_unsafe return value', async ({ client, server }) => {
server.setContent('/', `
<button onclick="console.log('Submit')">Submit</button>
`, 'text/html');
Expand All @@ -94,7 +94,7 @@ test('browser_run_code return value', async ({ client, server }) => {
const code = 'async (page) => { await page.getByRole("button", { name: "Submit" }).click(); return { message: "Hello, world!" }; await page.getByRole("banner").click(); }';

const response = parseResponse(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: {
code,
},
Expand All @@ -108,7 +108,7 @@ test('browser_run_code return value', async ({ client, server }) => {
expect(content).toContain('[LOG] Submit');
});

test('browser_run_code route handler exception keeps server alive', async ({ client, server }) => {
test('browser_run_code_unsafe route handler exception keeps server alive', async ({ client, server }) => {
server.setContent('/', '<button>Submit</button>', 'text/html');
await client.callTool({
name: 'browser_navigate',
Expand All @@ -127,7 +127,7 @@ test('browser_run_code route handler exception keeps server alive', async ({ cli
});
}`;
expect(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: { code },
})).toHaveResponse({
error: expect.stringContaining('ReferenceError: URL is not defined'),
Expand All @@ -142,7 +142,7 @@ test('browser_run_code route handler exception keeps server alive', async ({ cli
expect(followUp.isError).toBeFalsy();
});

test('browser_run_code with filename', async ({ client, server }) => {
test('browser_run_code_unsafe with filename', async ({ client, server }) => {
server.setContent('/', `
<button onclick="console.log('Clicked')">Click</button>
`, 'text/html');
Expand All @@ -156,14 +156,14 @@ test('browser_run_code with filename', async ({ client, server }) => {
await fs.promises.writeFile(filePath, code);

const response = parseResponse(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: { filename: 'test-code.js' },
}));
const content = await consoleEntries(response);
expect(content).toContain('[LOG] Clicked');
});

test('browser_run_code with filename containing template literals', async ({ client, server }) => {
test('browser_run_code_unsafe with filename containing template literals', async ({ client, server }) => {
server.setContent('/', `
<button onclick="console.log('Done')">Submit</button>
`, 'text/html');
Expand All @@ -177,7 +177,7 @@ test('browser_run_code with filename containing template literals', async ({ cli
await fs.promises.writeFile(filePath, code);

const response = parseResponse(await client.callTool({
name: 'browser_run_code',
name: 'browser_run_code_unsafe',
arguments: { filename: 'template-code.js' },
}));
const content = await consoleEntries(response);
Expand Down
Loading