Compare commits

...

5 Commits

Author SHA1 Message Date
Yury Semikhatsky
3971e4c093 feat(extension): group connected tabs into a green Playwright tab group (#1514)
## Summary
- Add `tabGroups` permission to the extension manifest
- When the connect page opens (via action button click or MCP relay
connect), place it into a green tab group titled "Playwright"
- When a tab is connected to an MCP session, add it to the same group
- When the active connection closes, remove the connected tab from the
group via `chrome.tabs.ungroup`
- Add three tests: group assigned to connect page, group shared with
connected tab, and group cleaned up on disconnect
2026-04-03 18:02:33 -07:00
Yury Semikhatsky
a07bc4e247 chore: roll 1.60.0-alpha-1775258971000 (#1513)
## Summary
- Roll Playwright to `1.60.0-alpha-1775258971000`
2026-04-03 16:56:56 -07:00
Lennon Wang
b214e8b387 docs(mcp): fix persistent profile directory name in README (#1507) 2026-04-02 15:53:36 -07:00
Pavel Feldman
d21e970b68 chore: mark v0.0.70 (#1504) 2026-03-31 17:11:11 -07:00
Pavel Feldman
8f56abaad2 chore: roll 1.60.0-alpha-1774999321000 (#1503) 2026-03-31 17:10:54 -07:00
8 changed files with 171 additions and 40 deletions

View File

@@ -372,6 +372,7 @@ Playwright MCP server supports following arguments. They can be provided in the
| --device <device> | device to emulate, for example: "iPhone 15"<br>*env* `PLAYWRIGHT_MCP_DEVICE` |
| --executable-path <path> | path to the browser executable.<br>*env* `PLAYWRIGHT_MCP_EXECUTABLE_PATH` |
| --extension | Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.<br>*env* `PLAYWRIGHT_MCP_EXTENSION` |
| --endpoint <endpoint> | Bound browser endpoint to connect to.<br>*env* `PLAYWRIGHT_MCP_ENDPOINT` |
| --grant-permissions <permissions...> | List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".<br>*env* `PLAYWRIGHT_MCP_GRANT_PERMISSIONS` |
| --headless | run browser in headless mode, headed by default<br>*env* `PLAYWRIGHT_MCP_HEADLESS` |
| --host <host> | host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.<br>*env* `PLAYWRIGHT_MCP_HOST` |
@@ -412,15 +413,17 @@ Persistent profile is located at the following locations and you can override it
```bash
# Windows
%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile
%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-{workspace-hash}
# macOS
- ~/Library/Caches/ms-playwright/mcp-{channel}-profile
- ~/Library/Caches/ms-playwright/mcp-{channel}-{workspace-hash}
# Linux
- ~/.cache/ms-playwright/mcp-{channel}-profile
- ~/.cache/ms-playwright/mcp-{channel}-{workspace-hash}
```
`{workspace-hash}` is derived from the MCP client's workspace root, so different projects get separate profiles automatically.
**Isolated**
In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser,
@@ -1018,6 +1021,7 @@ http.createServer(async (req, res) => {
- Parameters:
- `action` (string): Operation to perform
- `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed.
- `url` (string, optional): URL to navigate to in the new tab, used for new.
- Read-only: **false**
</details>

36
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "playwright-mcp-internal",
"version": "0.0.69",
"version": "0.0.70",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-mcp-internal",
"version": "0.0.69",
"version": "0.0.70",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
],
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.59.0-alpha-1774912654000",
"@playwright/test": "1.60.0-alpha-1775258971000",
"@types/node": "^24.3.0"
}
},
@@ -854,13 +854,13 @@
"link": true
},
"node_modules/@playwright/test": {
"version": "1.59.0-alpha-1774912654000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0-alpha-1774912654000.tgz",
"integrity": "sha512-/OjIU6mfPP7UisfqKLnKqjG8gDq4gwoek55z6VqaWnGgZnRdJwCNBao5Azymk5qmp7hzxu3rXdW0sFmsNXLEfQ==",
"version": "1.60.0-alpha-1775258971000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0-alpha-1775258971000.tgz",
"integrity": "sha512-6ZvWKDxCRxSNWtqcXdjjUfdiFkPXWb+q1EgfiG5JL5Hmpz1klCSfFgqssSpjOsZdjVlFTkPBruxClEaKCL3lgw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.0-alpha-1774912654000"
"playwright": "1.60.0-alpha-1775258971000"
},
"bin": {
"playwright": "cli.js"
@@ -2624,12 +2624,12 @@
}
},
"node_modules/playwright": {
"version": "1.59.0-alpha-1774912654000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0-alpha-1774912654000.tgz",
"integrity": "sha512-98v8NQ0ZaNTdOhwo24sq7Y79wuh+Q6TShG6DKgyQIUr4cBV5gDRIcRjTVZz5LhwYAwCOXCanvY5er47mowV8ww==",
"version": "1.60.0-alpha-1775258971000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0-alpha-1775258971000.tgz",
"integrity": "sha512-xaS8b8clhxs1uLiBQFdrKYPAxZPI1eZDshsK9LMvMyuNkwiFlK0FxWNWj01kA/dUf5/hm26xarLiM/Ah1DugyA==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.0-alpha-1774912654000"
"playwright-core": "1.60.0-alpha-1775258971000"
},
"bin": {
"playwright": "cli.js"
@@ -2646,9 +2646,9 @@
"link": true
},
"node_modules/playwright-core": {
"version": "1.59.0-alpha-1774912654000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0-alpha-1774912654000.tgz",
"integrity": "sha512-A4zBS+GASROUFrHTMeC0ySI1+f/Wjbm0OJIAUFu4q+0Si3tfLg742dGwq1AezmMBovINt6Fi7fOTjtGZCQQCEw==",
"version": "1.60.0-alpha-1775258971000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0-alpha-1775258971000.tgz",
"integrity": "sha512-SJ5hXQUf50wA/wI1Z9xkFoJq7IwLhfX4o7hXZJExxtGo3U8UQsS3AtD1TVWkO0KTeMdFnaQ8XE4BMa5wziegnw==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -3416,7 +3416,7 @@
},
"packages/extension": {
"name": "@playwright/mcp-extension",
"version": "0.0.69",
"version": "0.0.70",
"license": "Apache-2.0",
"devDependencies": {
"@types/chrome": "^0.0.315",
@@ -3441,11 +3441,11 @@
},
"packages/playwright-mcp": {
"name": "@playwright/mcp",
"version": "0.0.69",
"version": "0.0.70",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.0-alpha-1774912654000",
"playwright-core": "1.59.0-alpha-1774912654000"
"playwright": "1.60.0-alpha-1775258971000",
"playwright-core": "1.60.0-alpha-1775258971000"
},
"bin": {
"playwright-mcp": "cli.js"

View File

@@ -1,6 +1,6 @@
{
"name": "playwright-mcp-internal",
"version": "0.0.69",
"version": "0.0.70",
"private": true,
"repository": {
"type": "git",
@@ -26,7 +26,7 @@
],
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.59.0-alpha-1774912654000",
"@playwright/test": "1.60.0-alpha-1775258971000",
"@types/node": "^24.3.0"
}
}

View File

@@ -1,12 +1,13 @@
{
"manifest_version": 3,
"name": "Playwright MCP Bridge",
"version": "0.0.69",
"version": "0.0.70",
"description": "Share browser tabs with Playwright MCP server",
"permissions": [
"debugger",
"activeTab",
"tabs"
"tabs",
"tabGroups"
],
"host_permissions": [
"<all_urls>"

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp-extension",
"version": "0.0.69",
"version": "0.0.70",
"description": "Playwright MCP Browser Extension",
"private": true,
"repository": {

View File

@@ -35,6 +35,7 @@ type PageMessage = {
class TabShareExtension {
private _activeConnection: RelayConnection | undefined;
private _connectedTabId: number | null = null;
private _groupId: number | null = null;
private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();
constructor() {
@@ -96,6 +97,7 @@ class TabShareExtension {
// TODO: show error in the selector tab?
};
this._pendingTabSelection.set(selectorTabId, { connection });
await this._addTabToGroup(selectorTabId);
debugLog(`Connected to MCP relay`);
} catch (error: any) {
const message = `Failed to connect to MCP relay: ${error.message}`;
@@ -124,10 +126,12 @@ class TabShareExtension {
debugLog('MCP connection closed');
this._activeConnection = undefined;
void this._setConnectedTabId(null);
chrome.tabs.ungroup([tabId]).catch(() => {});
};
await Promise.all([
this._setConnectedTabId(tabId),
this._addTabToGroup(tabId),
chrome.tabs.update(tabId, { active: true }),
chrome.windows.update(windowId, { focused: true }),
]);
@@ -204,11 +208,31 @@ class TabShareExtension {
return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));
}
private async _addTabToGroup(tabId: number): Promise<void> {
try {
if (this._groupId !== null) {
try {
await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] });
await chrome.tabGroups.update(this._groupId, { color: 'green', title: 'Playwright' });
return;
} catch {
this._groupId = null;
}
}
this._groupId = await chrome.tabs.group({ tabIds: [tabId] });
await chrome.tabGroups.update(this._groupId, { color: 'green', title: 'Playwright' });
} catch (error: any) {
debugLog('Error adding tab to group:', error);
}
}
private async _onActionClicked(): Promise<void> {
await chrome.tabs.create({
const tab = await chrome.tabs.create({
url: chrome.runtime.getURL('status.html'),
active: true
});
if (tab.id)
await this._addTabToGroup(tab.id);
}
private async _disconnect(): Promise<void> {

View File

@@ -390,8 +390,101 @@ test(`bypass connection dialog with token`, async ({ browserWithExtension, start
});
});
test.describe('tab grouping', () => {
test('connect page is added to green Playwright group on relay connect', async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const client = await startWithExtensionFlag(browserWithExtension, startClient);
const connectPagePromise = browserContext.waitForEvent('page', page =>
page.url().startsWith(`chrome-extension://${extensionId}/connect.html`)
);
const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } });
const connectPage = await connectPagePromise;
// Wait for the tab list to appear — this means connectToMCPRelay was processed
// by the background and _addTabToGroup has been called.
await expect(connectPage.locator('.tab-item').first()).toBeVisible();
const group = await connectPage.evaluate(async () => {
const chrome = (window as any).chrome;
const tab = await chrome.tabs.getCurrent();
if (!tab || tab.groupId === -1)
return null;
const g = await chrome.tabGroups.get(tab.groupId);
return { color: g.color, title: g.title };
});
expect(group).toEqual({ color: 'green', title: 'Playwright' });
await connectPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
await navigatePromise;
});
test('connected tab is added to same Playwright group', async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const page = await browserContext.newPage();
await page.goto(server.HELLO_WORLD);
const client = await startWithExtensionFlag(browserWithExtension, startClient);
const connectPagePromise = browserContext.waitForEvent('page', page =>
page.url().startsWith(`chrome-extension://${extensionId}/connect.html`)
);
const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } });
const connectPage = await connectPagePromise;
await connectPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();
await navigatePromise;
const { connectGroupId, connectedGroupId } = await connectPage.evaluate(async () => {
const chrome = (window as any).chrome;
const connectTab = await chrome.tabs.getCurrent();
const [connectedTab] = await chrome.tabs.query({ title: 'Title' });
return {
connectGroupId: connectTab?.groupId,
connectedGroupId: connectedTab?.groupId,
};
});
expect(connectGroupId).not.toBe(-1);
expect(connectedGroupId).toBe(connectGroupId);
});
test('connected tab is removed from group on disconnect', async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const page = await browserContext.newPage();
await page.goto(server.HELLO_WORLD);
const client = await startWithExtensionFlag(browserWithExtension, startClient);
const connectPagePromise = browserContext.waitForEvent('page', page =>
page.url().startsWith(`chrome-extension://${extensionId}/connect.html`)
);
const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } });
const connectPage = await connectPagePromise;
await connectPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();
await navigatePromise;
await client.close();
await expect.poll(async () => {
return connectPage.evaluate(async () => {
const chrome = (window as any).chrome;
const [tab] = await chrome.tabs.query({ title: 'Title' });
return tab?.groupId ?? -1;
});
}).toBe(-1);
});
});
test.describe('CLI with extension', () => {
test('open <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {
test('attach <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {
const browserContext = await browserWithExtension.launch();
// Write config file with userDataDir
@@ -407,7 +500,7 @@ test.describe('CLI with extension', () => {
});
// Start the CLI command in the background
const cliPromise = cli('open', server.HELLO_WORLD, '--extension', `--config=cli-config.json`);
const cliPromise = cli('attach', '--extension', `--config=cli-config.json`);
// Wait for the confirmation page to appear
const confirmationPage = await confirmationPagePromise;
@@ -415,12 +508,21 @@ test.describe('CLI with extension', () => {
// Click the Connect button
await confirmationPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
{
// Wait for the CLI command to complete
const { output } = await cliPromise;
// Verify the output
expect(output).toContain(`### Page`);
expect(output).toContain(`- Page URL: chrome-extension://${extensionId}/connect.html?`);
expect(output).toContain(`- Page Title: Playwright MCP extension`);
}
{
const { output } = await cli('goto', server.HELLO_WORLD);
// Verify the output
expect(output).toContain(`### Page`);
expect(output).toContain(`- Page URL: ${server.HELLO_WORLD}`);
expect(output).toContain(`- Page Title: Title`);
}
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp",
"version": "0.0.69",
"version": "0.0.70",
"description": "Playwright Tools for MCP",
"repository": {
"type": "git",
@@ -33,8 +33,8 @@
}
},
"dependencies": {
"playwright": "1.59.0-alpha-1774912654000",
"playwright-core": "1.59.0-alpha-1774912654000"
"playwright": "1.60.0-alpha-1775258971000",
"playwright-core": "1.60.0-alpha-1775258971000"
},
"bin": {
"playwright-mcp": "cli.js"