chore: introduce browser_evaluate (#678)

Fixes https://github.com/microsoft/playwright-mcp/issues/424
This commit is contained in:
Pavel Feldman 2025-07-16 15:02:47 -07:00 committed by GitHub
parent 825a97d66e
commit 012c906500
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 127 additions and 36 deletions

View File

@ -598,6 +598,22 @@ X Y coordinate space, based on the provided screenshot.
</details>
<details>
<summary><b>Evaluation</b></summary>
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_evaluate**
- Title: Evaluate JavaScript
- Description: Evaluate JavaScript expression on page or element
- Parameters:
- `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided
- `element` (string, optional): Human-readable element description used to obtain permission to interact with the element
- `ref` (string, optional): Exact target element reference from the page snapshot
- Read-only: **false**
</details>
<details>
<summary><b>Resources</b></summary>

28
package-lock.json generated
View File

@ -13,8 +13,8 @@
"commander": "^13.1.0",
"debug": "^4.4.1",
"mime": "^4.0.7",
"playwright": "1.55.0-alpha-1752540053000",
"playwright-core": "1.55.0-alpha-1752540053000",
"playwright": "1.55.0-alpha-1752701791000",
"playwright-core": "1.55.0-alpha-1752701791000",
"ws": "^8.18.1",
"zod-to-json-schema": "^3.24.4"
},
@ -24,7 +24,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@playwright/test": "1.55.0-alpha-1752540053000",
"@playwright/test": "1.55.0-alpha-1752701791000",
"@stylistic/eslint-plugin": "^3.0.1",
"@types/chrome": "^0.0.315",
"@types/debug": "^4.1.12",
@ -293,13 +293,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.55.0-alpha-1752540053000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-1752540053000.tgz",
"integrity": "sha512-lpiGWId9fRQMn8IXR3Bimbpn/6PLlM2rwn3eWg58BO4X+JjAWXHVmBHvbIkAqfR4v5rxzAJIMQi2XHvPsur3YA==",
"version": "1.55.0-alpha-1752701791000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-1752701791000.tgz",
"integrity": "sha512-mnitdsjXKPyKTjQQDJ78Or1xZSGcaoDzZVD/0BWFCvygn3nyNmGmiias/Mlfvzvgz9UWBbPeZYxU/bd2Lu+OrQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.55.0-alpha-1752540053000"
"playwright": "1.55.0-alpha-1752701791000"
},
"bin": {
"playwright": "cli.js"
@ -3301,12 +3301,12 @@
}
},
"node_modules/playwright": {
"version": "1.55.0-alpha-1752540053000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-1752540053000.tgz",
"integrity": "sha512-Nk6feSnITP79R936KspfwS5MrhbMTK+oK8en2O/mI3lHyAiiBOh8JfOwdvRLna33E7p9KVRWXM7DbdGmFXovAQ==",
"version": "1.55.0-alpha-1752701791000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-1752701791000.tgz",
"integrity": "sha512-PA3TvDz7uQ+Pde0uaii5/WpU5vntRJsYFsaSPoBzywIqzYFO1ugk1ZZ0q6z4/xHq0ha1UClvsv3P77B+u1fi+w==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.0-alpha-1752540053000"
"playwright-core": "1.55.0-alpha-1752701791000"
},
"bin": {
"playwright": "cli.js"
@ -3319,9 +3319,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.55.0-alpha-1752540053000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-1752540053000.tgz",
"integrity": "sha512-ZQaYt7sduxL1NaVfTg8oJdYGzt5XbFVVhdkuaofjwmsr8Yf245rZybb5YuhAwRT2h0MNJUH4UOdboQNDb6mqmw==",
"version": "1.55.0-alpha-1752701791000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-1752701791000.tgz",
"integrity": "sha512-mQhzhjJMiqnGNnYZv7M4yk1OcNTt1E72jrTLO7EqZuoeat4+qpcU0/mbK+RcTEass5a9YheoVFh6OIhruFMGVg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"

View File

@ -40,15 +40,15 @@
"commander": "^13.1.0",
"debug": "^4.4.1",
"mime": "^4.0.7",
"playwright": "1.55.0-alpha-1752540053000",
"playwright-core": "1.55.0-alpha-1752540053000",
"playwright": "1.55.0-alpha-1752701791000",
"playwright-core": "1.55.0-alpha-1752701791000",
"ws": "^8.18.1",
"zod-to-json-schema": "^3.24.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@playwright/test": "1.55.0-alpha-1752540053000",
"@playwright/test": "1.55.0-alpha-1752701791000",
"@stylistic/eslint-plugin": "^3.0.1",
"@types/chrome": "^0.0.315",
"@types/debug": "^4.1.12",

View File

@ -190,19 +190,19 @@ ${code.join('\n')}
result.push('');
}
if (this.tabs().length > 1)
result.push(await this.listTabsMarkdown(), '');
if (captureSnapshot && tab.hasSnapshot()) {
if (this.tabs().length > 1)
result.push(await this.listTabsMarkdown(), '');
if (this.tabs().length > 1)
result.push('### Current tab');
if (this.tabs().length > 1)
result.push('### Current tab');
result.push(
`- Page URL: ${tab.page.url()}`,
`- Page Title: ${await tab.title()}`
);
if (captureSnapshot && tab.hasSnapshot())
result.push(
`- Page URL: ${tab.page.url()}`,
`- Page Title: ${await tab.title()}`
);
result.push(tab.snapshotOrDie().text());
}
const content = actionResult?.content ?? [];

View File

@ -17,6 +17,7 @@
import common from './tools/common.js';
import console from './tools/console.js';
import dialogs from './tools/dialogs.js';
import evaluate from './tools/evaluate.js';
import files from './tools/files.js';
import install from './tools/install.js';
import keyboard from './tools/keyboard.js';
@ -35,6 +36,7 @@ export const snapshotTools: Tool<any>[] = [
...common(true),
...console,
...dialogs(true),
...evaluate,
...files(true),
...install,
...keyboard(true),

71
src/tools/evaluate.ts Normal file
View File

@ -0,0 +1,71 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { defineTool } from './tool.js';
import * as javascript from '../javascript.js';
import { generateLocator } from './utils.js';
import type * as playwright from 'playwright';
const evaluateSchema = z.object({
function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
});
const evaluate = defineTool({
capability: 'core',
schema: {
name: 'browser_evaluate',
title: 'Evaluate JavaScript',
description: 'Evaluate JavaScript expression on page or element',
inputSchema: evaluateSchema,
type: 'destructive',
},
handle: async (context, params) => {
const tab = context.currentTabOrDie();
const code: string[] = [];
let locator: playwright.Locator | undefined;
if (params.ref && params.element) {
const snapshot = tab.snapshotOrDie();
locator = snapshot.refLocator({ ref: params.ref, element: params.element });
code.push(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
} else {
code.push(`await page.evaluate(${javascript.quote(params.function)});`);
}
return {
code,
action: async () => {
const receiver = locator ?? tab.page as any;
const result = await receiver._evaluateFunction(params.function);
return {
content: [{ type: 'text', text: '- Result: ' + (JSON.stringify(result, null, 2) || 'undefined') }],
};
},
captureSnapshot: false,
waitForNetwork: false,
};
},
});
export default [
evaluate,
];

View File

@ -22,6 +22,7 @@ test('test snapshot tool list', async ({ client }) => {
'browser_click',
'browser_console_messages',
'browser_drag',
'browser_evaluate',
'browser_file_upload',
'browser_handle_dialog',
'browser_hover',

View File

@ -232,17 +232,14 @@ export const expect = baseExpect.extend({
};
},
toContainTextContent(response: Response, content: string | string[]) {
toContainTextContent(response: Response, content: string) {
const isNot = this.isNot;
try {
content = Array.isArray(content) ? content : [content];
const texts = (response.content as any).map(c => c.text);
for (let i = 0; i < texts.length; i++) {
if (isNot)
expect(texts[i]).not.toContain(content[i]);
else
expect(texts[i]).toContain(content[i]);
}
const texts = (response.content as any).map(c => c.text).join('\n');
if (isNot)
expect(texts).not.toContain(content);
else
expect(texts).toContain(content);
} catch (e) {
return {
pass: isNot,

View File

@ -24,6 +24,7 @@ import zodToJsonSchema from 'zod-to-json-schema'
import commonTools from '../lib/tools/common.js';
import consoleTools from '../lib/tools/console.js';
import dialogsTools from '../lib/tools/dialogs.js';
import evaluateTools from '../lib/tools/evaluate.js';
import filesTools from '../lib/tools/files.js';
import installTools from '../lib/tools/install.js';
import keyboardTools from '../lib/tools/keyboard.js';
@ -48,6 +49,9 @@ const categories = {
'Navigation': [
...navigateTools(true),
],
'Evaluation': [
...evaluateTools,
],
'Resources': [
...screenshotTools,
...pdfTools,