Skip to content

Commit d8c5974

Browse files
committed
chore(mcp): introduce browser_run_code
1 parent 90c84f1 commit d8c5974

File tree

5 files changed

+158
-12
lines changed

5 files changed

+158
-12
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import mouse from './tools/mouse';
2626
import navigate from './tools/navigate';
2727
import network from './tools/network';
2828
import pdf from './tools/pdf';
29+
import runCode from './tools/runCode';
2930
import snapshot from './tools/snapshot';
3031
import screenshot from './tools/screenshot';
3132
import tabs from './tools/tabs';
@@ -49,6 +50,7 @@ export const browserTools: Tool<any>[] = [
4950
...network,
5051
...mouse,
5152
...pdf,
53+
...runCode,
5254
...screenshot,
5355
...snapshot,
5456
...tabs,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 vm from 'vm';
18+
19+
import { ManualPromise } from 'playwright-core/lib/utils';
20+
21+
import { z } from '../../sdk/bundle';
22+
import { defineTabTool } from './tool';
23+
24+
const codeSchema = z.object({
25+
code: z.string().describe(`Playwright code snippet to run. The snippet should access the \`page\` object to interact with the page. Can make multiple statements. For example: \`await page.getByRole('button', { name: 'Submit' }).click();\``),
26+
});
27+
28+
const runCode = defineTabTool({
29+
capability: 'core',
30+
schema: {
31+
name: 'browser_run_code',
32+
title: 'Run Playwright code',
33+
description: 'Run Playwright code snippet',
34+
inputSchema: codeSchema,
35+
type: 'action',
36+
},
37+
38+
handle: async (tab, params, response) => {
39+
response.setIncludeSnapshot();
40+
response.addCode(params.code);
41+
const __end__ = new ManualPromise<void>();
42+
const context = {
43+
page: tab.page,
44+
__end__,
45+
};
46+
vm.createContext(context);
47+
await tab.waitForCompletion(async () => {
48+
const snippet = `(async () => {
49+
try {
50+
${params.code};
51+
__end__.resolve();
52+
} catch (e) {
53+
__end__.reject(e);
54+
}
55+
})()`;
56+
vm.runInContext(snippet, context);
57+
await __end__;
58+
});
59+
},
60+
});
61+
62+
export default [
63+
runCode,
64+
];

tests/mcp/capabilities.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ test('test snapshot tool list', async ({ client }) => {
3636
'browser_network_requests',
3737
'browser_press_key',
3838
'browser_resize',
39+
'browser_run_code',
3940
'browser_snapshot',
4041
'browser_tabs',
4142
'browser_take_screenshot',
@@ -67,6 +68,7 @@ test('test tool list proxy mode', async ({ startClient }) => {
6768
'browser_network_requests',
6869
'browser_press_key',
6970
'browser_resize',
71+
'browser_run_code',
7072
'browser_snapshot',
7173
'browser_tabs',
7274
'browser_take_screenshot',

tests/mcp/generator.spec.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,34 @@ test('generator tools intent', async ({ startClient }) => {
2828
if (tool.inputSchema.properties?.intent)
2929
toolsWithIntent.push(tool.name);
3030
}
31+
toolsWithIntent.sort();
3132

3233
expect(toolsWithIntent).toEqual([
34+
'browser_click',
3335
'browser_close',
34-
'browser_resize',
35-
'browser_handle_dialog',
36+
'browser_drag',
3637
'browser_evaluate',
3738
'browser_file_upload',
3839
'browser_fill_form',
40+
'browser_handle_dialog',
41+
'browser_hover',
3942
'browser_install',
40-
'browser_press_key',
41-
'browser_type',
42-
'browser_navigate',
43-
'browser_navigate_back',
44-
'browser_mouse_move_xy',
4543
'browser_mouse_click_xy',
4644
'browser_mouse_drag_xy',
47-
'browser_click',
48-
'browser_drag',
49-
'browser_hover',
45+
'browser_mouse_move_xy',
46+
'browser_navigate',
47+
'browser_navigate_back',
48+
'browser_press_key',
49+
'browser_resize',
50+
'browser_run_code',
5051
'browser_select_option',
5152
'browser_tabs',
52-
'browser_wait_for',
53+
'browser_type',
5354
'browser_verify_element_visible',
54-
'browser_verify_text_visible',
5555
'browser_verify_list_visible',
56+
'browser_verify_text_visible',
5657
'browser_verify_value',
58+
'browser_wait_for',
5759
]);
5860
});
5961

tests/mcp/run-code.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
19+
test('browser_run_code', async ({ client, server }) => {
20+
server.setContent('/', `
21+
<button onclick="console.log('Submit')">Submit</button>
22+
`, 'text/html');
23+
await client.callTool({
24+
name: 'browser_navigate',
25+
arguments: { url: server.PREFIX },
26+
});
27+
28+
expect(await client.callTool({
29+
name: 'browser_run_code',
30+
arguments: {
31+
code: 'await page.getByRole("button", { name: "Submit" }).click()',
32+
},
33+
})).toHaveResponse({
34+
code: `await page.getByRole(\"button\", { name: \"Submit\" }).click()`,
35+
consoleMessages: expect.stringContaining('- [LOG] Submit'),
36+
});
37+
});
38+
39+
test('browser_run_code block', async ({ client, server }) => {
40+
server.setContent('/', `
41+
<button onclick="console.log('Submit')">Submit</button>
42+
`, 'text/html');
43+
await client.callTool({
44+
name: 'browser_navigate',
45+
arguments: { url: server.PREFIX },
46+
});
47+
48+
expect(await client.callTool({
49+
name: 'browser_run_code',
50+
arguments: {
51+
code: 'await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();',
52+
},
53+
})).toHaveResponse({
54+
code: expect.stringContaining(`await page.getByRole(\"button\", { name: \"Submit\" }).click()`),
55+
consoleMessages: expect.stringMatching(/\[LOG\] Submit.*\n.*\[LOG\] Submit/),
56+
});
57+
});
58+
59+
test('browser_run_code no-require', async ({ client, server }) => {
60+
server.setContent('/', `
61+
<button onclick="console.log('Submit')">Submit</button>
62+
`, 'text/html');
63+
await client.callTool({
64+
name: 'browser_navigate',
65+
arguments: { url: server.PREFIX },
66+
});
67+
68+
expect(await client.callTool({
69+
name: 'browser_run_code',
70+
arguments: {
71+
code: `require('fs');`,
72+
},
73+
})).toHaveResponse({
74+
result: expect.stringContaining(`ReferenceError: require is not defined`),
75+
});
76+
});

0 commit comments

Comments
 (0)