Compare commits

...

12 Commits

Author SHA1 Message Date
Pavel Feldman
f531b2c9cb chore: mark v0.0.59 (#1340) 2026-01-25 11:20:28 -08:00
Pavel Feldman
4b62f68979 chore: render nicer config (#1339) 2026-01-25 11:06:30 -08:00
Pavel Feldman
5b497bcca8 chore: roll pw to latest (#1338) 2026-01-25 11:00:23 -08:00
sno
00b9c54515 fix: update extension README link to packages/extension (#1335) 2026-01-25 10:17:32 -08:00
Pavel Feldman
79111366a9 chore: mark v0.0.58 (#1336) 2026-01-23 17:18:55 -08:00
Pavel Feldman
956b79a1ab chore: mark v0.0.57 (#1332) 2026-01-23 16:08:05 -08:00
Pavel Feldman
b58ad48e0a chore: roll pw to latest (#1334) 2026-01-23 16:06:09 -08:00
Pavel Feldman
41fba2bd71 chore: roll playwright to latest (#1331) 2026-01-23 19:31:12 +00:00
Dmitry Gozman
cd2b589338 chore: add "playwright-cli" binary (#1330) 2026-01-23 17:52:48 +00:00
Dmitry Gozman
fbd62cd838 chore: roll to 1.59.0-alpha-1769176698000 (#1327) 2026-01-23 17:24:40 +00:00
Dmitry Gozman
6aab683338 chore: monorepo (#1325) 2026-01-23 10:37:33 +00:00
Yury Semikhatsky
85c64bbe0f chore: add cli package (#1320) 2026-01-22 07:54:03 +00:00
60 changed files with 2467 additions and 2147 deletions

View File

@@ -16,14 +16,12 @@ jobs:
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- run: npm ci
- run: npm run lint
- name: Ensure no changes
run: git diff --exit-code
test:
test_mcp:
strategy:
fail-fast: false
matrix:
@@ -42,8 +40,9 @@ jobs:
run: npx playwright install --with-deps
- name: Run tests
run: npm run test
working-directory: ./packages/playwright-mcp
test_docker:
test_mcp_docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -71,14 +70,12 @@ jobs:
# Used for the Docker tests to share the test-results folder with the container.
umask 0000
npm run test -- --project=chromium-docker
working-directory: ./packages/playwright-mcp
env:
MCP_IN_DOCKER: 1
test_extension:
runs-on: macos-latest
defaults:
run:
working-directory: ./extension
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20
@@ -88,19 +85,17 @@ jobs:
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Playwright install
run: npx playwright install --with-deps
- name: Build extension
run: npm run build
working-directory: ./packages/extension
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: extension
path: ./extension/dist
retention-days: 7
- name: Install MCP server
run: |
cd ..
npm ci
npx playwright install chromium
- name: Run tests
run: |
if [[ "$(uname)" == "Linux" ]]; then
@@ -109,3 +104,4 @@ jobs:
npm run test
fi
shell: bash
working-directory: ./packages/extension

View File

@@ -7,7 +7,7 @@ on:
types: [published]
jobs:
publish-canary-npm:
publish-mcp-canary-npm:
if: github.event.schedule || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
@@ -38,16 +38,19 @@ jobs:
- name: Update package.json version
run: |
npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version
working-directory: ./packages/playwright-mcp
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run lint
- run: npm run ctest
working-directory: ./packages/playwright-mcp
- name: Publish to npm with next tag
run: npm publish --tag next
working-directory: ./packages/playwright-mcp
publish-release-npm:
publish-mcp-release-npm:
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
@@ -66,9 +69,11 @@ jobs:
- run: npx playwright install --with-deps
- run: npm run lint
- run: npm run ctest
working-directory: ./packages/playwright-mcp
- run: npm publish
working-directory: ./packages/playwright-mcp
publish-release-docker:
publish-mcp-release-docker:
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
@@ -93,8 +98,7 @@ jobs:
id: build-push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile # Adjust path if your Dockerfile is elsewhere
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
@@ -127,17 +131,16 @@ jobs:
node-version: 20
cache: 'npm'
- name: Install extension dependencies
working-directory: ./extension
run: npm ci
- name: Build extension
working-directory: ./extension
working-directory: ./packages/extension
run: npm run build
- name: Get extension version
id: get-version
working-directory: ./extension
working-directory: ./packages/extension
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Package extension
working-directory: ./extension
working-directory: ./packages/extension
run: |
cd dist
zip -r ../playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip .
@@ -146,4 +149,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
gh release upload ${{github.event.release.tag_name}} ./extension/playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip
gh release upload ${{github.event.release.tag_name}} ./packages/extension/playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip

2
.gitignore vendored
View File

@@ -1,5 +1,3 @@
lib/
dist/
node_modules/
test-results/
playwright-report/

View File

@@ -1,6 +0,0 @@
**/*
README.md
LICENSE
!cli.js
!index.*
!config.d.ts

View File

@@ -16,6 +16,7 @@ WORKDIR /app
RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=bind,source=packages/playwright-mcp/package.json,target=packages/playwright-mcp/package.json \
npm ci --omit=dev && \
# Install system dependencies for playwright
npx -y playwright-core install-deps chromium
@@ -28,10 +29,11 @@ FROM base AS builder
RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=bind,source=packages/playwright-mcp/package.json,target=packages/playwright-mcp/package.json \
npm ci
# Copy the rest of the app
COPY *.json *.js *.ts .
COPY packages/playwright-mcp/*.json packages/playwright-mcp/*.js packages/playwright-mcp/*.ts .
# ------------------------------
# Browser
@@ -59,7 +61,7 @@ RUN chown -R ${USERNAME}:${USERNAME} node_modules
USER ${USERNAME}
COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}
COPY --chown=${USERNAME}:${USERNAME} cli.js package.json ./
COPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./
# 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"]

222
README.md
View File

@@ -324,119 +324,50 @@ Playwright MCP server supports following arguments. They can be provided in the
<!--- Options generated by update-readme.js -->
```
> npx @playwright/mcp@latest --help
--allowed-hosts <hosts...> comma-separated list of hosts this
server is allowed to serve from.
Defaults to the host the server is bound
to. Pass '*' to disable the host check.
--allowed-origins <origins> semicolon-separated list of TRUSTED
origins to allow the browser to request.
Default is to allow all.
Important: *does not* serve as a
security boundary and *does not* affect
redirects.
--allow-unrestricted-file-access allow access to files outside of the
workspace roots. Also allows
unrestricted access to file:// URLs. By
default access to file system is
restricted to workspace root directories
(or cwd if no roots are configured)
only, and navigation to file:// URLs is
blocked.
--blocked-origins <origins> semicolon-separated list of origins to
block the browser from requesting.
Blocklist is evaluated before allowlist.
If used without the allowlist, requests
not matching the blocklist are still
allowed.
Important: *does not* serve as a
security boundary and *does not* affect
redirects.
--block-service-workers block service workers
--browser <browser> browser or chrome channel to use,
possible values: chrome, firefox,
webkit, msedge.
--caps <caps> comma-separated list of additional
capabilities to enable, possible values:
vision, pdf.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--cdp-header <headers...> CDP headers to send with the connect
request, multiple can be specified.
--config <path> path to the configuration file.
--console-level <level> level of console messages to return:
"error", "warning", "info", "debug".
Each level includes the messages of more
severe levels.
--device <device> device to emulate, for example: "iPhone
15"
--executable-path <path> path to the browser executable.
--extension Connect to a running browser instance
(Edge/Chrome only). Requires the
"Playwright MCP Bridge" browser
extension to be installed.
--grant-permissions <permissions...> List of permissions to grant to the
browser context, for example
"geolocation", "clipboard-read",
"clipboard-write".
--headless run browser in headless mode, headed by
default
--host <host> host to bind server to. Default is
localhost. Use 0.0.0.0 to bind to all
interfaces.
--ignore-https-errors ignore https errors
--init-page <path...> path to TypeScript file to evaluate on
Playwright page object
--init-script <path...> path to JavaScript file to add as an
initialization script. The script will
be evaluated in every page before any of
the page's scripts. Can be specified
multiple times.
--isolated keep the browser profile in memory, do
not save it to disk.
--image-responses <mode> whether to send image responses to the
client. Can be "allow" or "omit",
Defaults to "allow".
--no-sandbox disable the sandbox for all process
types that are normally sandboxed.
--output-dir <path> path to the directory for output files.
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy,
for example
".com,chromium.org,.domain.com"
--proxy-server <proxy> specify proxy server, for example
"http://myproxy:3128" or
"socks5://myproxy:8080"
--save-session Whether to save the Playwright MCP
session into the output directory.
--save-trace Whether to save the Playwright Trace of
the session into the output directory.
--save-video <size> Whether to save the video of the session
into the output directory. For example
"--save-video=800x600"
--secrets <path> path to a file containing secrets in the
dotenv format
--shared-browser-context reuse the same browser context between
all connected HTTP clients.
--snapshot-mode <mode> when taking snapshots for responses,
specifies the mode to use. Can be
"incremental", "full", or "none".
Default is incremental.
--storage-state <path> path to the storage state file for
isolated sessions.
--test-id-attribute <attribute> specify the attribute to use for test
ids, defaults to "data-testid"
--timeout-action <timeout> specify action timeout in milliseconds,
defaults to 5000ms
--timeout-navigation <timeout> specify navigation timeout in
milliseconds, defaults to 60000ms
--user-agent <ua string> specify user agent string
--user-data-dir <path> path to the user data directory. If not
specified, a temporary directory will be
created.
--viewport-size <size> specify browser viewport size in pixels,
for example "1280x720"
```
| Option | Description |
|--------|-------------|
| --allowed-hosts <hosts...> | comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.<br>*env* `PLAYWRIGHT_MCP_ALLOWED_HOSTS` |
| --allowed-origins <origins> | semicolon-separated list of TRUSTED origins to allow the browser to request. Default is to allow all. Important: *does not* serve as a security boundary and *does not* affect redirects.<br>*env* `PLAYWRIGHT_MCP_ALLOWED_ORIGINS` |
| --allow-unrestricted-file-access | allow access to files outside of the workspace roots. Also allows unrestricted access to file:// URLs. By default access to file system is restricted to workspace root directories (or cwd if no roots are configured) only, and navigation to file:// URLs is blocked.<br>*env* `PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS` |
| --blocked-origins <origins> | semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed. Important: *does not* serve as a security boundary and *does not* affect redirects.<br>*env* `PLAYWRIGHT_MCP_BLOCKED_ORIGINS` |
| --block-service-workers | block service workers<br>*env* `PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS` |
| --browser <browser> | browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.<br>*env* `PLAYWRIGHT_MCP_BROWSER` |
| --caps <caps> | comma-separated list of additional capabilities to enable, possible values: vision, pdf.<br>*env* `PLAYWRIGHT_MCP_CAPS` |
| --cdp-endpoint <endpoint> | CDP endpoint to connect to.<br>*env* `PLAYWRIGHT_MCP_CDP_ENDPOINT` |
| --cdp-header <headers...> | CDP headers to send with the connect request, multiple can be specified.<br>*env* `PLAYWRIGHT_MCP_CDP_HEADER` |
| --codegen <lang> | specify the language to use for code generation, possible values: "typescript", "none". Default is "typescript".<br>*env* `PLAYWRIGHT_MCP_CODEGEN` |
| --config <path> | path to the configuration file.<br>*env* `PLAYWRIGHT_MCP_CONFIG` |
| --console-level <level> | level of console messages to return: "error", "warning", "info", "debug". Each level includes the messages of more severe levels.<br>*env* `PLAYWRIGHT_MCP_CONSOLE_LEVEL` |
| --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` |
| --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` |
| --ignore-https-errors | ignore https errors<br>*env* `PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS` |
| --init-page <path...> | path to TypeScript file to evaluate on Playwright page object<br>*env* `PLAYWRIGHT_MCP_INIT_PAGE` |
| --init-script <path...> | path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.<br>*env* `PLAYWRIGHT_MCP_INIT_SCRIPT` |
| --isolated | keep the browser profile in memory, do not save it to disk.<br>*env* `PLAYWRIGHT_MCP_ISOLATED` |
| --image-responses <mode> | whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".<br>*env* `PLAYWRIGHT_MCP_IMAGE_RESPONSES` |
| --no-sandbox | disable the sandbox for all process types that are normally sandboxed.<br>*env* `PLAYWRIGHT_MCP_NO_SANDBOX` |
| --output-dir <path> | path to the directory for output files.<br>*env* `PLAYWRIGHT_MCP_OUTPUT_DIR` |
| --output-mode <mode> | whether to save snapshots, console messages, network logs to a file or to the standard output. Can be "file" or "stdout". Default is "stdout".<br>*env* `PLAYWRIGHT_MCP_OUTPUT_MODE` |
| --port <port> | port to listen on for SSE transport.<br>*env* `PLAYWRIGHT_MCP_PORT` |
| --proxy-bypass <bypass> | comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"<br>*env* `PLAYWRIGHT_MCP_PROXY_BYPASS` |
| --proxy-server <proxy> | specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"<br>*env* `PLAYWRIGHT_MCP_PROXY_SERVER` |
| --save-session | Whether to save the Playwright MCP session into the output directory.<br>*env* `PLAYWRIGHT_MCP_SAVE_SESSION` |
| --save-trace | Whether to save the Playwright Trace of the session into the output directory.<br>*env* `PLAYWRIGHT_MCP_SAVE_TRACE` |
| --save-video <size> | Whether to save the video of the session into the output directory. For example "--save-video=800x600"<br>*env* `PLAYWRIGHT_MCP_SAVE_VIDEO` |
| --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` |
| --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` |
| --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` |
| --timeout-action <timeout> | specify action timeout in milliseconds, defaults to 5000ms<br>*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` |
| --timeout-navigation <timeout> | specify navigation timeout in milliseconds, defaults to 60000ms<br>*env* `PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION` |
| --user-agent <ua string> | specify user agent string<br>*env* `PLAYWRIGHT_MCP_USER_AGENT` |
| --user-data-dir <path> | path to the user data directory. If not specified, a temporary directory will be created.<br>*env* `PLAYWRIGHT_MCP_USER_DATA_DIR` |
| --viewport-size <size> | specify browser viewport size in pixels, for example "1280x720"<br>*env* `PLAYWRIGHT_MCP_VIEWPORT_SIZE` |
<!--- End of options generated section -->
@@ -484,7 +415,7 @@ state [here](https://playwright.dev/docs/auth).
**Browser Extension**
The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [extension/README.md](extension/README.md) for installation and setup instructions.
The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [packages/extension/README.md](packages/extension/README.md) for installation and setup instructions.
### Initial state
@@ -659,6 +590,11 @@ npx @playwright/mcp@latest --config path/to/config.json
*/
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?: {
/**
* The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
@@ -705,13 +641,18 @@ npx @playwright/mcp@latest --config path/to/config.json
* When taking snapshots for responses, specifies the mode to use.
*/
mode?: 'incremental' | 'full' | 'none';
}
};
/**
* Whether to allow file uploads from anywhere on the file system.
* By default (false), file uploads are restricted to paths within the MCP roots only.
*/
allowUnrestrictedFileAccess?: boolean;
/**
* Specify the language to use for code generation.
*/
codegen?: 'typescript' | 'none';
}
```
@@ -811,7 +752,7 @@ http.createServer(async (req, res) => {
- Title: Click
- Description: Perform click on a web page
- Parameters:
- `element` (string): 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): Exact target element reference from the page snapshot
- `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
- `button` (string, optional): Button to click, defaults to left
@@ -832,7 +773,8 @@ http.createServer(async (req, res) => {
- Title: Get console messages
- Description: Returns all console messages
- Parameters:
- `level` (string, optional): Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
- `level` (string): Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
- `filename` (string, optional): Filename to save the console messages to. If not provided, messages are returned as text.
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
@@ -892,7 +834,7 @@ http.createServer(async (req, res) => {
- Title: Hover mouse
- Description: Hover over element on page
- Parameters:
- `element` (string): 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): Exact target element reference from the page snapshot
- Read-only: **false**
@@ -909,7 +851,7 @@ http.createServer(async (req, res) => {
- **browser_navigate_back**
- Title: Go back
- Description: Go back to the previous page
- Description: Go back to the previous page in the history
- Parameters: None
- Read-only: **false**
@@ -919,7 +861,8 @@ http.createServer(async (req, res) => {
- Title: List network requests
- Description: Returns all network requests since loading the page
- Parameters:
- `includeStatic` (boolean, optional): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.
- `includeStatic` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.
- `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text.
- Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js -->
@@ -956,7 +899,7 @@ http.createServer(async (req, res) => {
- Title: Select option
- Description: Select an option in a dropdown
- Parameters:
- `element` (string): 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): Exact target element reference from the page snapshot
- `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
- Read-only: **false**
@@ -976,7 +919,7 @@ http.createServer(async (req, res) => {
- Title: Take a screenshot
- Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
- Parameters:
- `type` (string, optional): Image format for the screenshot. Default is png.
- `type` (string): Image format for the screenshot. Default is png.
- `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.
- `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
- `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
@@ -989,7 +932,7 @@ http.createServer(async (req, res) => {
- Title: Type text
- Description: Type text into editable element
- Parameters:
- `element` (string): 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): Exact target element reference from the page snapshot
- `text` (string): Text to type into the element
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
@@ -1046,18 +989,25 @@ http.createServer(async (req, res) => {
- Title: Click
- Description: Click left mouse button at a given position
- Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `x` (number): X coordinate
- `y` (number): Y coordinate
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_mouse_down**
- Title: Press mouse down
- Description: Press mouse down
- Parameters:
- `button` (string, optional): Button to press, defaults to left
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_mouse_drag_xy**
- Title: Drag mouse
- Description: Drag left mouse button to a given position
- Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `startX` (number): Start X coordinate
- `startY` (number): Start Y coordinate
- `endX` (number): End X coordinate
@@ -1070,11 +1020,29 @@ http.createServer(async (req, res) => {
- Title: Move mouse
- Description: Move mouse to a given position
- Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `x` (number): X coordinate
- `y` (number): Y coordinate
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_mouse_up**
- Title: Press mouse up
- Description: Press mouse up
- Parameters:
- `button` (string, optional): Button to press, defaults to left
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->
- **browser_mouse_wheel**
- Title: Scroll mouse wheel
- Description: Scroll mouse wheel
- Parameters:
- `deltaX` (number): X delta
- `deltaY` (number): Y delta
- Read-only: **false**
</details>
<details>
@@ -1100,7 +1068,7 @@ http.createServer(async (req, res) => {
- Title: Create locator for element
- Description: Generate locator for the given element to use in tests
- Parameters:
- `element` (string): 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): Exact target element reference from the page snapshot
- Read-only: **true**

File diff suppressed because it is too large Load Diff

2142
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,30 @@
{
"name": "@playwright/mcp",
"version": "0.0.56",
"description": "Playwright Tools for MCP",
"name": "playwright-mcp-internal",
"version": "0.0.59",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/playwright-mcp.git"
},
"homepage": "https://playwright.dev",
"engines": {
"node": ">=18"
},
"author": {
"name": "Microsoft Corporation"
},
"license": "Apache-2.0",
"scripts": {
"lint": "npm run update-readme",
"update-readme": "node update-readme.js",
"docker-build": "docker build --no-cache -t playwright-mcp-dev:latest .",
"docker-rm": "docker rm playwright-mcp-dev",
"docker-run": "docker run -it -p 8080:8080 --name playwright-mcp-dev playwright-mcp-dev:latest",
"test": "playwright test",
"ctest": "playwright test --project=chrome",
"ftest": "playwright test --project=firefox",
"wtest": "playwright test --project=webkit",
"dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker",
"npm-publish": "npm run clean && npm run test && npm publish",
"copy-config": "cp ../playwright/packages/playwright/src/mcp/config.d.ts . && perl -pi -e \"s|import type \\* as playwright from 'playwright-core';|import type * as playwright from 'playwright';|\" ./config.d.ts",
"roll": "npm run copy-config && npm run lint"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"dependencies": {
"playwright": "1.58.0-alpha-2026-01-16",
"playwright-core": "1.58.0-alpha-2026-01-16"
},
"bin": {
"mcp-server-playwright": "cli.js"
"lint": "npm run lint --workspaces",
"test": "npm run test --workspaces",
"build": "npm run build --workspaces"
},
"workspaces": [
"packages/*"
],
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.24.0",
"@playwright/test": "1.58.0-alpha-2026-01-16",
"@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.59.0-alpha-1769364499000",
"@types/node": "^24.3.0"
}
}

1
packages/extension/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist/

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Playwright MCP Bridge",
"version": "0.0.56",
"version": "0.0.59",
"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": [

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp-extension",
"version": "0.0.56",
"version": "0.0.59",
"description": "Playwright MCP Browser Extension",
"private": true,
"repository": {
@@ -19,6 +19,7 @@
"build": "tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts",
"watch": "tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts",
"test": "playwright test",
"lint": "tsc --project .",
"clean": "rm -rf dist"
},
"devDependencies": {

View File

@@ -16,7 +16,7 @@
import { defineConfig } from '@playwright/test';
import type { TestOptions } from '../tests/fixtures';
import type { TestOptions } from '../playwright-mcp/tests/fixtures';
export default defineConfig<TestOptions>({
testDir: './tests',

View File

@@ -17,11 +17,11 @@
import fs from 'fs';
import path from 'path';
import { chromium } from 'playwright';
import { test as base, expect } from '../../tests/fixtures';
import { test as base, expect } from '../../playwright-mcp/tests/fixtures';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { BrowserContext } from 'playwright';
import type { StartClient } from '../../tests/fixtures';
import type { StartClient } from '../../playwright-mcp/tests/fixtures';
type BrowserWithExtension = {
userDataDir: string;
@@ -134,7 +134,7 @@ test(`navigate with extension`, async ({ browserWithExtension, startClient, serv
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});
@@ -166,7 +166,7 @@ test(`snapshot of an existing page`, async ({ browserWithExtension, startClient,
await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();
expect(await navigateResponse).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
expect(browserContext.pages()).toHaveLength(4);
@@ -187,7 +187,7 @@ test(`extension not installed timeout`, async ({ browserWithExtension, startClie
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
result: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'),
error: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'),
isError: true,
});
@@ -216,7 +216,7 @@ testWithOldExtensionVersion(`works with old extension version`, async ({ browser
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});
@@ -242,7 +242,7 @@ test(`extension needs update`, async ({ browserWithExtension, startClient, serve
await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`);
expect(await navigateResponse).toHaveResponse({
result: expect.stringContaining('Extension connection timeout.'),
error: expect.stringContaining('Extension connection timeout.'),
isError: true,
});
});
@@ -267,10 +267,9 @@ test(`custom executablePath`, async ({ startClient, server, useShortConnectionTi
const navigateResponse = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
timeout: 1000,
});
expect(await navigateResponse).toHaveResponse({
result: expect.stringContaining('Extension connection timeout.'),
error: expect.stringContaining('Extension connection timeout.'),
isError: true,
});
expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?');
@@ -300,6 +299,6 @@ test(`bypass connection dialog with token`, async ({ browserWithExtension, start
});
expect(await navigateResponse).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});

2
packages/playwright-mcp/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
README.md
LICENSE

View File

@@ -0,0 +1,7 @@
**/*
!README.md
!LICENSE
!cli.js
!playwright-cli.js
!index.*
!config.d.ts

View File

@@ -16,7 +16,17 @@
import type * as playwright from 'playwright';
export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing';
export type ToolCapability =
'core' |
'core-input' |
'core-navigation' |
'core-tabs' |
'core-install' |
'core-input' |
'vision' |
'pdf' |
'testing' |
'tracing';
export type Config = {
/**
@@ -147,6 +157,11 @@ export type Config = {
*/
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?: {
/**
* The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
@@ -193,11 +208,16 @@ export type Config = {
* When taking snapshots for responses, specifies the mode to use.
*/
mode?: 'incremental' | 'full' | 'none';
}
};
/**
* Whether to allow file uploads from anywhere on the file system.
* By default (false), file uploads are restricted to paths within the MCP roots only.
*/
allowUnrestrictedFileAccess?: boolean;
/**
* Specify the language to use for code generation.
*/
codegen?: 'typescript' | 'none';
};

View File

@@ -0,0 +1,45 @@
{
"name": "@playwright/mcp",
"version": "0.0.59",
"description": "Playwright Tools for MCP",
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/playwright-mcp.git"
},
"homepage": "https://playwright.dev",
"engines": {
"node": ">=18"
},
"author": {
"name": "Microsoft Corporation"
},
"license": "Apache-2.0",
"scripts": {
"lint": "node update-readme.js",
"test": "playwright test",
"ctest": "playwright test --project=chrome",
"ftest": "playwright test --project=firefox",
"wtest": "playwright test --project=webkit",
"dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker",
"build": "echo OK",
"npm-publish": "npm run lint && npm run test && npm publish",
"copy-config": "cp ../../../playwright/packages/playwright/src/mcp/config.d.ts . && perl -pi -e \"s|import type \\* as playwright from 'playwright-core';|import type * as playwright from 'playwright';|\" ./config.d.ts",
"roll": "npm run copy-config && npm run lint"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"dependencies": {
"minimist": "^1.2.5",
"playwright": "1.59.0-alpha-1769364499000",
"playwright-core": "1.59.0-alpha-1769364499000"
},
"bin": {
"mcp": "cli.js",
"playwright-cli": "./playwright-cli.js"
}
}

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
/**
* 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.
*/
const { program } = require('playwright/lib/mcp/terminal/program');
const packageJSON = require('./package.json');
program({ version: packageJSON.version }).catch(e => {
console.error(e.message);
process.exit(1);
});

View File

@@ -0,0 +1,32 @@
/**
* 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 '@playwright/test';
import childProcess from 'child_process';
function runCLI(args: string[]) {
return childProcess.spawnSync(process.execPath, [require.resolve('../playwright-cli'), ...args], { encoding: 'utf-8' });
}
test('prints help', async () => {
const { stdout } = runCLI(['--help']);
expect(stdout).toContain('Usage: playwright-cli <command>');
});
test('prints version', async () => {
const { stdout } = runCLI(['--version']);
expect(stdout).toContain(require('../package.json').version);
});

View File

@@ -33,7 +33,7 @@ test('browser_click', async ({ client, server }) => {
arguments: { url: server.PREFIX },
})).toHaveResponse({
code: `await page.goto('${server.PREFIX}');`,
pageState: expect.stringContaining(`- button \"Submit\" [ref=e2]`),
snapshot: expect.stringContaining(`- button \"Submit\" [ref=e2]`),
});
expect(await client.callTool({
@@ -44,6 +44,6 @@ test('browser_click', async ({ client, server }) => {
},
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Submit' }).click();`,
pageState: expect.stringContaining(`button "Submit" [active] [ref=e2]`),
snapshot: expect.stringContaining(`button "Submit" [active] [ref=e2]`),
});
});

View File

@@ -22,11 +22,6 @@ test('browser_navigate', async ({ client, server }) => {
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: `await page.goto('${server.HELLO_WORLD}');`,
pageState: `- Page URL: ${server.HELLO_WORLD}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Hello, world!
\`\`\``,
snapshot: expect.stringContaining(`generic [active] [ref=e1]: Hello, world!`),
});
});

View File

@@ -229,7 +229,7 @@ export const expect = baseExpect.extend({
expect(parsed).not.toEqual(expect.objectContaining(object));
else
expect(parsed).toEqual(expect.objectContaining(object));
} catch (e) {
} catch (e: any) {
return {
pass: isNot,
message: () => e.message,
@@ -250,10 +250,12 @@ function parseResponse(response: any) {
const text = response.content[0].text;
const sections = parseSections(text);
const error = sections.get('Error');
const result = sections.get('Result');
const code = sections.get('Ran Playwright code');
const tabs = sections.get('Open tabs');
const pageState = sections.get('Page state');
const snapshot = sections.get('Snapshot');
const consoleMessages = sections.get('New console messages');
const modalState = sections.get('Modal state');
const downloads = sections.get('Downloads');
@@ -262,10 +264,12 @@ function parseResponse(response: any) {
const attachments = response.content.slice(1);
return {
error,
result,
code: codeNoFrame,
tabs,
pageState,
snapshot,
consoleMessages,
modalState,
downloads,

View File

@@ -18,14 +18,15 @@
const fs = require('fs')
const path = require('path')
const { zodToJsonSchema } = require('zod-to-json-schema')
const { execSync } = require('child_process');
const { browserTools } = require('playwright/lib/mcp/browser/tools');
const capabilities = {
'core-navigation': 'Core automation',
'core': 'Core automation',
'core-tabs': 'Tab management',
'core-input': 'Core automation',
'core-install': 'Browser installation',
'vision': 'Coordinate-based (opt-in via --caps=vision)',
'pdf': 'PDF generation (opt-in via --caps=pdf)',
@@ -33,7 +34,15 @@ const capabilities = {
'tracing': 'Tracing (opt-in via --caps=tracing)',
};
const toolsByCapability = Object.fromEntries(Object.entries(capabilities).map(([capability, title]) => [title, browserTools.filter(tool => tool.capability === capability).sort((a, b) => a.schema.name.localeCompare(b.schema.name))]));
/** @type {Record<string, any[]>} */
const toolsByCapability = {};
for (const [capability, title] of Object.entries(capabilities)) {
let tools = browserTools.filter(tool => tool.capability === capability && !tool.skillOnly);
tools = (toolsByCapability[title] || []).concat(tools);
toolsByCapability[title] = tools;
}
for (const [, tools] of Object.entries(toolsByCapability))
tools.sort((a, b) => a.schema.name.localeCompare(b.schema.name));
/**
* @param {any} tool
@@ -47,7 +56,7 @@ function formatToolForReadme(tool) {
lines.push(` - Title: ${tool.title}`);
lines.push(` - Description: ${tool.description}`);
const inputSchema = /** @type {any} */ (zodToJsonSchema(tool.inputSchema || {}));
const inputSchema = /** @type {any} */ (tool.inputSchema ? tool.inputSchema.toJSONSchema() : {});
const requiredParams = inputSchema.required || [];
if (inputSchema.properties && Object.keys(inputSchema.properties).length) {
lines.push(` - Parameters:`);
@@ -119,20 +128,56 @@ async function updateTools(content) {
*/
async function updateOptions(content) {
console.log('Listing options...');
const output = execSync('node cli.js --help');
execSync('node cli.js --help > help.txt');
const output = fs.readFileSync('help.txt');
fs.unlinkSync('help.txt');
const lines = output.toString().split('\n');
const firstLine = lines.findIndex(line => line.includes('--version'));
lines.splice(0, firstLine + 1);
const lastLine = lines.findIndex(line => line.includes('--help'));
lines.splice(lastLine);
/**
* @type {{ name: string, value: string }[]}
*/
const options = [];
for (let line of lines) {
if (line.startsWith(' --')) {
const l = line.substring(' --'.length);
const gapIndex = l.indexOf(' ');
const name = l.substring(0, gapIndex).trim();
const value = l.substring(gapIndex).trim();
options.push({ name, value });
} else {
const value = line.trim();
options[options.length - 1].value += ' ' + value;
}
}
const table = [];
table.push(`| Option | Description |`);
table.push(`|--------|-------------|`);
for (const option of options) {
const prefix = option.name.split(' ')[0];
const envName = `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_');
table.push(`| --${option.name} | ${option.value}<br>*env* \`${envName}\` |`);
}
if (process.env.PRINT_ENV) {
const envTable = [];
envTable.push(`| Environment |`);
envTable.push(`|-------------|`);
for (const option of options) {
const prefix = option.name.split(' ')[0];
const envName = `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_');
envTable.push(`| \`${envName}\` ${option.value} |`);
}
console.log(envTable.join('\n'));
}
const startMarker = `<!--- Options generated by ${path.basename(__filename)} -->`;
const endMarker = `<!--- End of options generated section -->`;
return updateSection(content, startMarker, endMarker, [
'```',
'> npx @playwright/mcp@latest --help',
...lines,
'```',
]);
return updateSection(content, startMarker, endMarker, table);
}
/**
@@ -143,14 +188,14 @@ async function updateConfig(content) {
console.log('Updating config schema from config.d.ts...');
const configPath = path.join(__dirname, 'config.d.ts');
const configContent = await fs.promises.readFile(configPath, 'utf-8');
// Extract the Config type definition
const configTypeMatch = configContent.match(/export type Config = (\{[\s\S]*?\n\});/);
if (!configTypeMatch)
throw new Error('Config type not found in config.d.ts');
const configType = configTypeMatch[1]; // Use capture group to get just the object definition
const startMarker = `<!--- Config generated by ${path.basename(__filename)} -->`;
const endMarker = `<!--- End of config generated section -->`;
return updateSection(content, startMarker, endMarker, [
@@ -160,14 +205,25 @@ async function updateConfig(content) {
]);
}
/**
* @param {string} filePath
*/
async function copyToPackage(filePath) {
await fs.promises.copyFile(path.join(__dirname, '../../', filePath), path.join(__dirname, filePath));
console.log(`${filePath} copied successfully`);
}
async function updateReadme() {
const readmePath = path.join(__dirname, 'README.md');
const readmePath = path.join(__dirname, '../../README.md');
const readmeContent = await fs.promises.readFile(readmePath, 'utf-8');
const withTools = await updateTools(readmeContent);
const withOptions = await updateOptions(withTools);
const withConfig = await updateConfig(withOptions);
await fs.promises.writeFile(readmePath, withConfig, 'utf-8');
console.log('README updated successfully');
await copyToPackage('README.md');
await copyToPackage('LICENSE');
}
updateReadme().catch(err => {