From 012c906500de032ac82b0d63df9db6e8fbfe17d7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 16 Jul 2025 15:02:47 -0700 Subject: [PATCH] chore: introduce browser_evaluate (#678) Fixes https://github.com/microsoft/playwright-mcp/issues/424 --- README.md | 16 +++++++++ package-lock.json | 28 +++++++-------- package.json | 6 ++-- src/context.ts | 20 +++++------ src/tools.ts | 2 ++ src/tools/evaluate.ts | 71 ++++++++++++++++++++++++++++++++++++++ tests/capabilities.spec.ts | 1 + tests/fixtures.ts | 15 ++++---- utils/update-readme.js | 4 +++ 9 files changed, 127 insertions(+), 36 deletions(-) create mode 100644 src/tools/evaluate.ts diff --git a/README.md b/README.md index 6fb96a3..6bcc17e 100644 --- a/README.md +++ b/README.md @@ -598,6 +598,22 @@ X Y coordinate space, based on the provided screenshot. +
+Evaluation + + + +- **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** + +
+
Resources diff --git a/package-lock.json b/package-lock.json index a24e9ac..20dc676 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 11a2fb7..757a52c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/context.ts b/src/context.ts index c7a0717..f63a250 100644 --- a/src/context.ts +++ b/src/context.ts @@ -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 ?? []; diff --git a/src/tools.ts b/src/tools.ts index 50a8756..2f20713 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -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[] = [ ...common(true), ...console, ...dialogs(true), + ...evaluate, ...files(true), ...install, ...keyboard(true), diff --git a/src/tools/evaluate.ts b/src/tools/evaluate.ts new file mode 100644 index 0000000..73820e5 --- /dev/null +++ b/src/tools/evaluate.ts @@ -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, +]; diff --git a/tests/capabilities.spec.ts b/tests/capabilities.spec.ts index 3d00859..cd7defd 100644 --- a/tests/capabilities.spec.ts +++ b/tests/capabilities.spec.ts @@ -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', diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 3c5fbad..3668a64 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -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, diff --git a/utils/update-readme.js b/utils/update-readme.js index 5e2ded1..144838d 100644 --- a/utils/update-readme.js +++ b/utils/update-readme.js @@ -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,