Compare commits

...

8 Commits

Author SHA1 Message Date
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
Yury Semikhatsky
a3b0d127df chore: mark v0.0.69 (#1499) 2026-03-30 16:45:38 -07:00
Yury Semikhatsky
c7285818ed chore: roll 1.59.0-alpha-1774912654000 (#1500) 2026-03-30 16:39:05 -07:00
Yury Semikhatsky
3e8fc820a6 chore: roll 1.59.0-alpha-1774903871000 (#1497)
Fixes https://github.com/microsoft/playwright-mcp/issues/1496
2026-03-30 15:56:05 -07:00
Pavel Feldman
9521308275 docs: update upstream pointer 2026-03-27 08:26:36 -07:00
Dan Kiuna
eed21856dc docs(readme): add Kiro MCP server installation button (#1472) 2026-03-20 08:13:11 -07:00
Yury Semikhatsky
b2df777103 devops: add devcontainer.json (#1473) 2026-03-18 22:31:11 -07:00
12 changed files with 150 additions and 88 deletions

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json",
"name": "Playwright",
"image": "mcr.microsoft.com/playwright:v1.58.2-noble",
"privileged": true,
"init": true,
"remoteUser": "pwuser",
"features": {
"ghcr.io/devcontainers/features/desktop-lite:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
"forwardPorts": [
6080
],
"portsAttributes": {
"6080": {
"label": "noVNC"
}
}
}

View File

@@ -53,7 +53,6 @@ FROM base
ARG PLAYWRIGHT_BROWSERS_PATH ARG PLAYWRIGHT_BROWSERS_PATH
ARG USERNAME=node ARG USERNAME=node
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PLAYWRIGHT_MCP_OUTPUT_DIR=/tmp/playwright-output
# Set the correct ownership for the runtime user on production `node_modules` # Set the correct ownership for the runtime user on production `node_modules`
RUN chown -R ${USERNAME}:${USERNAME} node_modules RUN chown -R ${USERNAME}:${USERNAME} node_modules
@@ -63,5 +62,8 @@ USER ${USERNAME}
COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH} COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}
COPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./ COPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./
# Current working directory must be writable as MCP may need to create default output dir in it.
WORKDIR /home/${USERNAME}
# Run in headless and only with chromium (other browsers need more dependencies not included in this image) # Run in headless and only with chromium (other browsers need more dependencies not included in this image)
ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"] ENTRYPOINT ["node", "/app/cli.js", "--headless", "--browser", "chromium", "--no-sandbox"]

View File

@@ -240,6 +240,8 @@ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to you
<details> <details>
<summary>Kiro</summary> <summary>Kiro</summary>
[![Add to Kiro](https://kiro.dev/images/add-to-kiro.svg)](https://kiro.dev/launch/mcp/add?name=playwright&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40playwright%2Fmcp%40latest%22%5D%7D)
Follow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`: Follow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`:
```json ```json
@@ -370,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` | | --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` | | --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` | | --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` | | --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` | | --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` | | --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` |
@@ -388,7 +391,7 @@ Playwright MCP server supports following arguments. They can be provided in the
| --save-session | Whether to save the Playwright MCP session into the output directory.<br>*env* `PLAYWRIGHT_MCP_SAVE_SESSION` | | --save-session | Whether to save the Playwright MCP session into the output directory.<br>*env* `PLAYWRIGHT_MCP_SAVE_SESSION` |
| --secrets <path> | path to a file containing secrets in the dotenv format<br>*env* `PLAYWRIGHT_MCP_SECRETS` | | --secrets <path> | path to a file containing secrets in the dotenv format<br>*env* `PLAYWRIGHT_MCP_SECRETS` |
| --shared-browser-context | reuse the same browser context between all connected HTTP clients.<br>*env* `PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT` | | --shared-browser-context | reuse the same browser context between all connected HTTP clients.<br>*env* `PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT` |
| --snapshot-mode <mode> | when taking snapshots for responses, specifies the mode to use. Can be "incremental", "full", or "none". Default is incremental.<br>*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` | | --snapshot-mode <mode> | when taking snapshots for responses, specifies the mode to use. Can be "full" or "none". Default is "full".<br>*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` |
| --storage-state <path> | path to the storage state file for isolated sessions.<br>*env* `PLAYWRIGHT_MCP_STORAGE_STATE` | | --storage-state <path> | path to the storage state file for isolated sessions.<br>*env* `PLAYWRIGHT_MCP_STORAGE_STATE` |
| --test-id-attribute <attribute> | specify the attribute to use for test ids, defaults to "data-testid"<br>*env* `PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE` | | --test-id-attribute <attribute> | specify the attribute to use for test ids, defaults to "data-testid"<br>*env* `PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE` |
| --timeout-action <timeout> | specify action timeout in milliseconds, defaults to 5000ms<br>*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` | | --timeout-action <timeout> | specify action timeout in milliseconds, defaults to 5000ms<br>*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` |
@@ -602,9 +605,9 @@ npx @playwright/mcp@latest --config path/to/config.json
sharedBrowserContext?: boolean; sharedBrowserContext?: boolean;
/** /**
* Secrets are used to prevent LLM from getting sensitive data while * Secrets are used to replace matching plain text in the tool responses to prevent the LLM
* automating scenarios such as authentication. * from accidentally getting sensitive data. It is a convenience and not a security feature,
* Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative. * make sure to always examine information coming in and from the tool on the client.
*/ */
secrets?: Record<string, string>; secrets?: Record<string, string>;
@@ -613,11 +616,6 @@ npx @playwright/mcp@latest --config path/to/config.json
*/ */
outputDir?: string; outputDir?: string;
/**
* Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to "stdout".
*/
outputMode?: 'file' | 'stdout';
console?: { console?: {
/** /**
* The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
@@ -676,12 +674,14 @@ npx @playwright/mcp@latest --config path/to/config.json
/** /**
* When taking snapshots for responses, specifies the mode to use. * When taking snapshots for responses, specifies the mode to use.
*/ */
mode?: 'incremental' | 'full' | 'none'; mode?: 'full' | 'none';
}; };
/** /**
* Whether to allow file uploads from anywhere on the file system. * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally
* By default (false), file uploads are restricted to paths within the MCP roots only. * wandering outside its intended workspace. It is a convenience defense to catch unintended
* file access, not a secure boundary; a deliberate attempt to reach other directories can be
* easily worked around, so always rely on client-level permissions for true security.
*/ */
allowUnrestrictedFileAccess?: boolean; allowUnrestrictedFileAccess?: boolean;
@@ -843,6 +843,7 @@ http.createServer(async (req, res) => {
- `element` (string, optional): Human-readable element description used to obtain permission to interact with the element - `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 - `ref` (string, optional): Exact target element reference from the page snapshot
- `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available. - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available.
- `filename` (string, optional): Filename to save the result to. If not provided, result is returned as text.
- Read-only: **false** - Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->
@@ -907,7 +908,10 @@ http.createServer(async (req, res) => {
- Title: List network requests - Title: List network requests
- Description: Returns all network requests since loading the page - Description: Returns all network requests since loading the page
- Parameters: - Parameters:
- `includeStatic` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false. - `static` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.
- `requestBody` (boolean): Whether to include request body. Defaults to false.
- `requestHeaders` (boolean): Whether to include request headers. Defaults to false.
- `filter` (string, optional): Only return requests whose URL matches this regexp (e.g. "/api/.*user").
- `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text. - `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text.
- Read-only: **true** - Read-only: **true**
@@ -936,7 +940,8 @@ http.createServer(async (req, res) => {
- Title: Run Playwright code - Title: Run Playwright code
- Description: Run Playwright code snippet - Description: Run Playwright code snippet
- Parameters: - Parameters:
- `code` (string): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }` - `code` (string, optional): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }`
- `filename` (string, optional): Load code from the specified file. If both code and filename are provided, code will be ignored.
- Read-only: **false** - Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->
@@ -959,6 +964,7 @@ http.createServer(async (req, res) => {
- Parameters: - Parameters:
- `filename` (string, optional): Save snapshot to markdown file instead of returning it in the response. - `filename` (string, optional): Save snapshot to markdown file instead of returning it in the response.
- `selector` (string, optional): Element selector of the root element to capture a partial snapshot instead of the whole page - `selector` (string, optional): Element selector of the root element to capture a partial snapshot instead of the whole page
- `depth` (number, optional): Limit the depth of the snapshot tree
- Read-only: **true** - Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->
@@ -1248,6 +1254,16 @@ http.createServer(async (req, res) => {
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->
- **browser_resume**
- Title: Resume paused script execution
- Description: Resume script execution after it was paused. When called with step set to true, execution will pause again before the next action.
- Parameters:
- `step` (boolean, optional): When true, execution will pause again before the next action, allowing step-by-step debugging.
- `location` (string, optional): Pause execution at a specific <file>:<line>, e.g. "example.spec.ts:42".
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_start_tracing** - **browser_start_tracing**
- Title: Start tracing - Title: Start tracing
- Description: Start trace recording - Description: Start trace recording
@@ -1260,6 +1276,7 @@ http.createServer(async (req, res) => {
- Title: Start video - Title: Start video
- Description: Start video recording - Description: Start video recording
- Parameters: - Parameters:
- `filename` (string, optional): Filename to save the video.
- `size` (object, optional): Video size - `size` (object, optional): Video size
- Read-only: **true** - Read-only: **true**
@@ -1276,8 +1293,18 @@ http.createServer(async (req, res) => {
- **browser_stop_video** - **browser_stop_video**
- Title: Stop video - Title: Stop video
- Description: Stop video recording - Description: Stop video recording
- Parameters: None
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_video_chapter**
- Title: Video chapter
- Description: Add a chapter marker to the video recording. Shows a full-screen chapter card with blurred backdrop.
- Parameters: - Parameters:
- `filename` (string, optional): Filename to save the video - `title` (string): Chapter title
- `description` (string, optional): Chapter description
- `duration` (number, optional): Duration in milliseconds to show the chapter card
- Read-only: **true** - Read-only: **true**
</details> </details>

64
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{ {
"name": "playwright-mcp-internal", "name": "playwright-mcp-internal",
"version": "0.0.68", "version": "0.0.70",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-mcp-internal", "name": "playwright-mcp-internal",
"version": "0.0.68", "version": "0.0.70",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2", "@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.59.0-alpha-1773608981000", "@playwright/test": "1.60.0-alpha-1774999321000",
"@types/node": "^24.3.0" "@types/node": "^24.3.0"
} }
}, },
@@ -854,13 +854,13 @@
"link": true "link": true
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.59.0-alpha-1773608981000", "version": "1.60.0-alpha-1774999321000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0-alpha-1773608981000.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0-alpha-1774999321000.tgz",
"integrity": "sha512-px+GAf8KIaMcPsCUPG3+xqPRSIPHgnizH7ygUjo6OXT1AigXTNCsIIVrPY3C5GjouM2MI4CQOkIKcSEjO84ZTg==", "integrity": "sha512-vUvFOjH6jnQ2/noiZe24f5sr+SSOk15Qz49/C9XbwELDZ9So2kys7NhiEfzkTWcnieHRurL04YVl44h2mwDbPw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.59.0-alpha-1773608981000" "playwright": "1.60.0-alpha-1774999321000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -2585,11 +2585,10 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.3.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
"dev": true, "dev": true,
"license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
@@ -2603,11 +2602,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@@ -2626,12 +2624,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.59.0-alpha-1773608981000", "version": "1.60.0-alpha-1774999321000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0-alpha-1773608981000.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0-alpha-1774999321000.tgz",
"integrity": "sha512-nb+BzawNj48eH6NdxecsysLuhCAB/p18FG7LLJp3MBfRGUkCAFtax0CFo/BhD+r0V4+0EW7llPK0p4cJQEIwUQ==", "integrity": "sha512-Bd5DkzYKG+2g1jLO6NeTXmGLbBYSFffJIOsR4l4hUBkJvzvGGdLZ7jZb2tOtb0WIoWXQKdQj3Ap6WthV4DBS8w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.59.0-alpha-1773608981000" "playwright-core": "1.60.0-alpha-1774999321000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -2648,9 +2646,9 @@
"link": true "link": true
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.59.0-alpha-1773608981000", "version": "1.60.0-alpha-1774999321000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0-alpha-1773608981000.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0-alpha-1774999321000.tgz",
"integrity": "sha512-w6E5Q0Wleek3Wp7gtlSPGXuKeQ5eg6QPPJNNwgMHQRpkxgqOwgN2mX7x6Z52HJE10HFC88U5HQzOLMbag928Lg==", "integrity": "sha512-ams3Zo4VXxeOg5ZTTh16GkE8g48Bmxo/9pg9gXl9SVKlVohCU7Jaog7XntY8yFuzENA6dJc1Fz7Z/NNTm9nGEw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@@ -3102,11 +3100,10 @@
} }
}, },
"node_modules/tinyglobby/node_modules/picomatch": { "node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3356,11 +3353,10 @@
} }
}, },
"node_modules/vite/node_modules/picomatch": { "node_modules/vite/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3420,7 +3416,7 @@
}, },
"packages/extension": { "packages/extension": {
"name": "@playwright/mcp-extension", "name": "@playwright/mcp-extension",
"version": "0.0.68", "version": "0.0.70",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.315", "@types/chrome": "^0.0.315",
@@ -3445,11 +3441,11 @@
}, },
"packages/playwright-mcp": { "packages/playwright-mcp": {
"name": "@playwright/mcp", "name": "@playwright/mcp",
"version": "0.0.68", "version": "0.0.70",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.59.0-alpha-1773608981000", "playwright": "1.60.0-alpha-1774999321000",
"playwright-core": "1.59.0-alpha-1773608981000" "playwright-core": "1.60.0-alpha-1774999321000"
}, },
"bin": { "bin": {
"playwright-mcp": "cli.js" "playwright-mcp": "cli.js"

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Playwright MCP Bridge", "name": "Playwright MCP Bridge",
"version": "0.0.68", "version": "0.0.70",
"description": "Share browser tabs with Playwright MCP server", "description": "Share browser tabs with Playwright MCP server",
"permissions": [ "permissions": [
"debugger", "debugger",

View File

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

View File

@@ -123,6 +123,14 @@ const test = base.extend<TestFixtures>({
}, },
}); });
function cliEnv() {
return {
PLAYWRIGHT_SERVER_REGISTRY: test.info().outputPath('registry'),
PLAYWRIGHT_DAEMON_SESSION_DIR: test.info().outputPath('daemon'),
PLAYWRIGHT_SOCKETS_DIR: path.join(test.info().project.outputDir, 'ds', String(test.info().parallelIndex)),
};
}
async function runCli( async function runCli(
args: string[], args: string[],
options: { mcpBrowser?: string, testInfo: any }, options: { mcpBrowser?: string, testInfo: any },
@@ -133,7 +141,7 @@ async function runCli(
const testInfo = options.testInfo; const testInfo = options.testInfo;
// Path to the terminal CLI // Path to the terminal CLI
const cliPath = path.join(__dirname, '../../../node_modules/playwright/lib/cli/client/program.js'); const cliPath = path.join(__dirname, '../../../node_modules/playwright-core/lib/tools/cli-client/cli.js');
return new Promise<CliResult>((resolve, reject) => { return new Promise<CliResult>((resolve, reject) => {
let stdout = ''; let stdout = '';
@@ -143,9 +151,7 @@ async function runCli(
cwd: testInfo.outputPath(), cwd: testInfo.outputPath(),
env: { env: {
...process.env, ...process.env,
PLAYWRIGHT_DAEMON_INSTALL_DIR: testInfo.outputPath(), ...cliEnv(),
PLAYWRIGHT_DAEMON_SESSION_DIR: testInfo.outputPath('daemon'),
PLAYWRIGHT_DAEMON_SOCKETS_DIR: path.join(testInfo.project.outputDir, 'daemon-sockets'),
PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser, PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser,
PLAYWRIGHT_MCP_HEADLESS: 'false', PLAYWRIGHT_MCP_HEADLESS: 'false',
}, },
@@ -213,8 +219,7 @@ test(`navigate with extension`, async ({ browserWithExtension, startClient, serv
}); });
const selectorPage = await confirmationPagePromise; const selectorPage = await confirmationPagePromise;
// For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({ expect(await navigateResponse).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
@@ -295,8 +300,7 @@ testWithOldExtensionVersion(`works with old extension version`, async ({ browser
}); });
const selectorPage = await confirmationPagePromise; const selectorPage = await confirmationPagePromise;
// For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({ expect(await navigateResponse).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type * as playwright from 'playwright'; import type * as playwright from '../../..';
export type ToolCapability = export type ToolCapability =
'config' | 'config' |
@@ -143,9 +143,9 @@ export type Config = {
sharedBrowserContext?: boolean; sharedBrowserContext?: boolean;
/** /**
* Secrets are used to prevent LLM from getting sensitive data while * Secrets are used to replace matching plain text in the tool responses to prevent the LLM
* automating scenarios such as authentication. * from accidentally getting sensitive data. It is a convenience and not a security feature,
* Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative. * make sure to always examine information coming in and from the tool on the client.
*/ */
secrets?: Record<string, string>; secrets?: Record<string, string>;
@@ -154,11 +154,6 @@ export type Config = {
*/ */
outputDir?: string; outputDir?: string;
/**
* Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to "stdout".
*/
outputMode?: 'file' | 'stdout';
console?: { console?: {
/** /**
* The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
@@ -217,12 +212,14 @@ export type Config = {
/** /**
* When taking snapshots for responses, specifies the mode to use. * When taking snapshots for responses, specifies the mode to use.
*/ */
mode?: 'incremental' | 'full' | 'none'; mode?: 'full' | 'none';
}; };
/** /**
* Whether to allow file uploads from anywhere on the file system. * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally
* By default (false), file uploads are restricted to paths within the MCP roots only. * wandering outside its intended workspace. It is a convenience defense to catch unintended
* file access, not a secure boundary; a deliberate attempt to reach other directories can be
* easily worked around, so always rely on client-level permissions for true security.
*/ */
allowUnrestrictedFileAccess?: boolean; allowUnrestrictedFileAccess?: boolean;

View File

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

View File

@@ -1,3 +1,3 @@
# Where is the source? # Where is the source?
Playwright MCP source code is located in the [Playwright monorepo](https://github.com/microsoft/playwright/blob/main/packages/playwright/src/mcp). Please refer to the contributor's guide in [CONTRIBUTING.md](../CONTRIBUTING.md) for more details. Playwright MCP source code is located in the [Playwright monorepo](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/tools/mcp). Please refer to the contributor's guide in [CONTRIBUTING.md](../CONTRIBUTING.md) for more details.

View File

@@ -74,10 +74,10 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}, },
startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => { startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => {
const configDir = path.dirname(test.info().config.configFile!);
const clients: Client[] = []; const clients: Client[] = [];
await use(async options => { await use(async options => {
const cwd = testInfo.outputPath();
const args: string[] = mcpArgs ?? []; const args: string[] = mcpArgs ?? [];
if (process.env.CI && process.platform === 'linux') if (process.env.CI && process.platform === 'linux')
args.push('--no-sandbox'); args.push('--no-sandbox');
@@ -90,7 +90,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
if (options?.config) { if (options?.config) {
const configFile = testInfo.outputPath('config.json'); const configFile = testInfo.outputPath('config.json');
await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2)); await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));
args.push(`--config=${path.relative(configDir, configFile)}`); args.push(`--config=${path.relative(cwd, configFile)}`);
} }
const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined);
@@ -103,7 +103,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}; };
}); });
} }
const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken); const { transport, stderr } = await createTransport(args, cwd, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);
let stderrBuffer = ''; let stderrBuffer = '';
stderr?.on('data', data => { stderr?.on('data', data => {
if (process.env.PWMCP_DEBUG) if (process.env.PWMCP_DEBUG)
@@ -182,12 +182,14 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}, },
}); });
async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{ async function createTransport(args: string[], cwd: string, mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{
transport: Transport, transport: Transport,
stderr: Stream | null, stderr: Stream | null,
}> { }> {
if (mcpMode === 'docker') { if (mcpMode === 'docker') {
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`]; const relCwd = path.relative(test.info().project.outputDir, cwd);
const dockerCwd = path.posix.join('/app/test-results', relCwd.split(path.sep).join('/'));
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`, '-w', dockerCwd];
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command: 'docker', command: 'docker',
args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args], args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],
@@ -201,7 +203,7 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'],
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command: 'node', command: 'node',
args: [path.join(__dirname, '../cli.js'), ...args], args: [path.join(__dirname, '../cli.js'), ...args],
cwd: path.dirname(test.info().config.configFile!), cwd,
stderr: 'pipe', stderr: 'pipe',
env: { env: {
...process.env, ...process.env,
@@ -222,7 +224,7 @@ type Response = Awaited<ReturnType<Client['callTool']>>;
export const expect = baseExpect.extend({ export const expect = baseExpect.extend({
toHaveResponse(response: Response, object: any) { toHaveResponse(response: Response, object: any) {
const parsed = parseResponse(response); const parsed = parseResponse(response, test.info().outputPath());
const isNot = this.isNot; const isNot = this.isNot;
try { try {
if (isNot) if (isNot)
@@ -246,7 +248,7 @@ export function formatOutput(output: string): string[] {
return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean);
} }
function parseResponse(response: any) { function parseResponse(response: any, cwd: string) {
const text = response.content[0].text; const text = response.content[0].text;
const sections = parseSections(text); const sections = parseSections(text);
@@ -255,7 +257,7 @@ function parseResponse(response: any) {
const code = sections.get('Ran Playwright code'); const code = sections.get('Ran Playwright code');
const tabs = sections.get('Open tabs'); const tabs = sections.get('Open tabs');
const pageState = sections.get('Page state'); const pageState = sections.get('Page state');
const snapshot = sections.get('Snapshot'); const snapshotSection = sections.get('Snapshot');
const consoleMessages = sections.get('New console messages'); const consoleMessages = sections.get('New console messages');
const modalState = sections.get('Modal state'); const modalState = sections.get('Modal state');
const downloads = sections.get('Downloads'); const downloads = sections.get('Downloads');
@@ -263,6 +265,19 @@ function parseResponse(response: any) {
const isError = response.isError; const isError = response.isError;
const attachments = response.content.slice(1); const attachments = response.content.slice(1);
let snapshot: string | undefined;
if (snapshotSection) {
const match = snapshotSection.match(/\[Snapshot\]\(([^)]+)\)/);
if (match) {
try {
snapshot = fs.readFileSync(path.resolve(cwd, match[1]), 'utf-8');
} catch {
}
} else {
snapshot = snapshotSection.replace(/^```yaml\n?/, '').replace(/\n?```$/, '');
}
}
return { return {
error, error,
result, result,