3 Commits

Author SHA1 Message Date
Pavel Feldman
22043cb3ef chore: mark v0.0.35 (#938) 2025-08-23 15:34:49 -07:00
Yury Semikhatsky
0812df2f5e chore(extension): do not complain about old extension version (#937) 2025-08-22 17:53:30 -07:00
Pavel Feldman
3d1a60b7f3 chore: introduce form filling tool (#935) 2025-08-22 17:06:24 -07:00
13 changed files with 212 additions and 86 deletions

View File

@@ -494,6 +494,15 @@ http.createServer(async (req, res) => {
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_fill_form**
- Title: Fill form
- Description: Fill multiple form fields
- Parameters:
- `fields` (array): Fields to fill in
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_handle_dialog**
- Title: Handle a dialog
- Description: Handle a dialog

View File

@@ -1,26 +1,22 @@
{
"manifest_version": 3,
"name": "Playwright MCP Bridge",
"version": "0.0.34",
"version": "0.0.35",
"description": "Share browser tabs with Playwright MCP server",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB",
"permissions": [
"debugger",
"activeTab",
"tabs",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "lib/background.js",
"type": "module"
},
"action": {
"default_title": "Playwright MCP Bridge",
"default_icon": {
@@ -30,7 +26,6 @@
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",

View File

@@ -1,12 +1,12 @@
{
"name": "@playwright/mcp-extension",
"version": "0.0.34",
"version": "0.0.35",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@playwright/mcp-extension",
"version": "0.0.34",
"version": "0.0.35",
"license": "Apache-2.0",
"devDependencies": {
"@types/chrome": "^0.0.315",

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp-extension",
"version": "0.0.34",
"version": "0.0.35",
"description": "Playwright MCP Browser Extension",
"type": "module",
"private": true,

View File

@@ -22,8 +22,7 @@ import type { TabInfo } from './tabItem.js';
type Status =
| { type: 'connecting'; message: string }
| { type: 'connected'; message: string }
| { type: 'error'; message: string }
| { type: 'error'; versionMismatch: { pwMcpVersion: string; extensionVersion: string; downloadUrl: string } };
| { type: 'error'; message: string };
const ConnectApp: React.FC = () => {
const [tabs, setTabs] = useState<TabInfo[]>([]);
@@ -59,23 +58,6 @@ const ConnectApp: React.FC = () => {
return;
}
const pwMcpVersion = params.get('pwMcpVersion');
const extensionVersion = chrome.runtime.getManifest().version;
if (pwMcpVersion !== extensionVersion) {
const downloadUrl = params.get('downloadUrl') || `https://github.com/microsoft/playwright-mcp/releases/download/v${extensionVersion}/playwright-mcp-extension-v${extensionVersion}.zip`;
setShowButtons(false);
setShowTabList(false);
setStatus({
type: 'error',
versionMismatch: {
pwMcpVersion: pwMcpVersion || 'unknown',
extensionVersion,
downloadUrl
}
});
return;
}
void connectToMCPRelay(relayUrl);
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
@@ -199,50 +181,11 @@ const ConnectApp: React.FC = () => {
);
};
const VersionMismatchError: React.FC<{ pwMcpVersion: string; extensionVersion: string; downloadUrl: string }> = ({ pwMcpVersion, extensionVersion, downloadUrl }) => {
const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md';
const handleDownloadAndOpenExtensions = () => {
// Start download
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `playwright-mcp-extension-v${extensionVersion}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
if (tabs[0]?.id)
chrome.tabs.update(tabs[0].id, { url: 'chrome://extensions/' });
});
}, 1000); // Wait 1 second for download to initiate
};
return (
<div>
Incompatible Playwright MCP version: {pwMcpVersion} (extension version: {extensionVersion}).{' '}
<button
onClick={handleDownloadAndOpenExtensions}
className='link-button'
>Click here</button> to download the matching extension, then drag and drop it into the Chrome Extensions page.{' '}
See <a href={readmeUrl} target='_blank' rel='noopener noreferrer'>installation instructions</a> for more details.
</div>
);
};
const StatusBanner: React.FC<{ status: Status }> = ({ status }) => {
return (
<div className={`status-banner ${status.type}`}>
{'versionMismatch' in status ? (
<VersionMismatchError
pwMcpVersion={status.versionMismatch.pwMcpVersion}
extensionVersion={status.versionMismatch.extensionVersion}
downloadUrl={status.versionMismatch.downloadUrl}
/>
) : (
status.message
)}
{status.message}
</div>
);
};

View File

@@ -18,7 +18,6 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { chromium } from 'playwright';
import packageJSON from '../../package.json' assert { type: 'json' };
import { test as base, expect } from '../../tests/fixtures.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -117,7 +116,7 @@ async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension
return client;
}
const testWithOldVersion = test.extend({
const testWithOldExtensionVersion = test.extend({
pathToExtension: async ({}, use, testInfo) => {
const extensionDir = testInfo.outputPath('extension');
const oldPath = fileURLToPath(new URL('../dist', import.meta.url));
@@ -216,7 +215,7 @@ for (const [mode, startClientMethod] of [
await confirmationPagePromise;
});
testWithOldVersion(`extension version mismatch (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {
testWithOldExtensionVersion(`works with old extension version (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {
useShortConnectionTimeout(500);
// Prelaunch the browser, so that it is properly closed after the test.
@@ -233,19 +232,13 @@ for (const [mode, startClientMethod] of [
arguments: { url: server.HELLO_WORLD },
});
const confirmationPage = await confirmationPagePromise;
await expect(confirmationPage.locator('.status-banner')).toHaveText(`Incompatible Playwright MCP version: ${packageJSON.version} (extension version: 0.0.1). Click here to download the matching extension, then drag and drop it into the Chrome Extensions page. See installation instructions for more details.`);
const selectorPage = await confirmationPagePromise;
// For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({
result: expect.stringContaining('Extension connection timeout.'),
isError: true,
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
const downloadPromise = confirmationPage.waitForEvent('download');
await confirmationPage.locator('.status-banner').getByRole('button', { name: 'Click here' }).click();
const download = await downloadPromise;
expect(download.url()).toBe(`https://github.com/microsoft/playwright-mcp/releases/download/v0.0.1/playwright-mcp-extension-v0.0.1.zip`);
await download.cancel();
});
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@playwright/mcp",
"version": "0.0.34",
"version": "0.0.35",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@playwright/mcp",
"version": "0.0.34",
"version": "0.0.35",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp",
"version": "0.0.34",
"version": "0.0.35",
"description": "Playwright Tools for MCP",
"type": "module",
"repository": {

View File

@@ -29,7 +29,6 @@ import { WebSocket, WebSocketServer } from 'ws';
import { httpAddressToString } from '../mcp/http.js';
import { logUnhandledError } from '../utils/log.js';
import { ManualPromise } from '../mcp/manualPromise.js';
import { packageJSON } from '../utils/package.js';
import type websocket from 'ws';
import type { ClientInfo } from '../browserContextFactory.js';
@@ -120,7 +119,6 @@ export class CDPRelayServer {
version: clientInfo.version,
};
url.searchParams.set('client', JSON.stringify(client));
url.searchParams.set('pwMcpVersion', packageJSON.version);
if (toolName)
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
const href = url.toString();

View File

@@ -19,6 +19,7 @@ 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 form from './tools/form.js';
import install from './tools/install.js';
import keyboard from './tools/keyboard.js';
import navigate from './tools/navigate.js';
@@ -39,6 +40,7 @@ export const allTools: Tool<any>[] = [
...dialogs,
...evaluate,
...files,
...form,
...install,
...keyboard,
...navigate,

61
src/tools/form.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* 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 { defineTabTool } from './tool.js';
import { generateLocator } from './utils.js';
import * as javascript from '../utils/codegen.js';
const fillForm = defineTabTool({
capability: 'core',
schema: {
name: 'browser_fill_form',
title: 'Fill form',
description: 'Fill multiple form fields',
inputSchema: z.object({
fields: z.array(z.object({
name: z.string().describe('Human-readable field name'),
type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),
ref: z.string().describe('Exact target field reference from the page snapshot'),
value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'),
})).describe('Fields to fill in'),
}),
type: 'destructive',
},
handle: async (tab, params, response) => {
for (const field of params.fields) {
const locator = await tab.refLocator({ element: field.name, ref: field.ref });
const locatorSource = `await page.${await generateLocator(locator)}`;
if (field.type === 'textbox' || field.type === 'slider') {
await locator.fill(field.value);
response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`);
} else if (field.type === 'checkbox' || field.type === 'radio') {
await locator.setChecked(field.value === 'true');
response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`);
} else if (field.type === 'combobox') {
await locator.selectOption({ label: field.value });
response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`);
}
}
},
});
export default [
fillForm,
];

View File

@@ -24,6 +24,7 @@ test('test snapshot tool list', async ({ client }) => {
'browser_drag',
'browser_evaluate',
'browser_file_upload',
'browser_fill_form',
'browser_handle_dialog',
'browser_hover',
'browser_select_option',
@@ -54,6 +55,7 @@ test('test tool list proxy mode', async ({ startClient }) => {
'browser_drag',
'browser_evaluate',
'browser_file_upload',
'browser_fill_form',
'browser_handle_dialog',
'browser_hover',
'browser_select_option',

123
tests/form.spec.ts Normal file
View File

@@ -0,0 +1,123 @@
/**
* 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 { test, expect } from './fixtures.js';
test('browser_fill_form (textbox)', async ({ client, server }) => {
server.setContent('/', `
<!DOCTYPE html>
<html>
<body>
<form>
<label>
<input type="text" id="name" name="name" />
Name
</label>
<label>
<input type="email" id="email" name="email" />
Email
</label>
<label>
<input type="range" id="age" name="age" min="18" max="100" />
Age
</label>
<label>
<select id="country" name="country">
<option value="">Choose a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
Country
</label>
<label>
<input type="checkbox" name="subscribe" value="newsletter" />
Subscribe to newsletter
</label>
</form>
</body>
</html>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_fill_form',
arguments: {
fields: [
{
name: 'Name textbox',
type: 'textbox',
ref: 'e4',
value: 'John Doe'
},
{
name: 'Email textbox',
type: 'textbox',
ref: 'e6',
value: 'john.doe@example.com'
},
{
name: 'Age textbox',
type: 'slider',
ref: 'e8',
value: '25'
},
{
name: 'Country select',
type: 'combobox',
ref: 'e10',
value: 'United States'
},
{
name: 'Subscribe checkbox',
type: 'checkbox',
ref: 'e12',
value: 'true'
},
]
},
})).toHaveResponse({
code: `await page.getByRole('textbox', { name: 'Name' }).fill('John Doe');
await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com');
await page.getByRole('slider', { name: 'Age' }).fill('25');
await page.getByLabel('Choose a country United').selectOption('United States');
await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`,
});
const response = await client.callTool({
name: 'browser_snapshot',
arguments: {
},
});
expect.soft(response).toHaveResponse({
pageState: expect.stringMatching(/textbox "Name".*John Doe/),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringMatching(/textbox "Email".*john.doe@example.com/),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringMatching(/slider "Age".*"25"/),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringContaining('option \"United States\" [selected]'),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringContaining('checkbox \"Subscribe to newsletter\" [checked]'),
});
});