Compare commits

...

13 Commits

Author SHA1 Message Date
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
dependabot[bot]
b213c187b0 chore(deps-dev): bump qs from 6.14.0 to 6.14.1 (#1292) 2026-01-16 10:32:34 -08:00
Yury Semikhatsky
412f6dc6fe chore: mark v0.0.55 (#1310) 2026-01-16 10:31:07 -08:00
Yury Semikhatsky
4b1a6842b1 chore: roll 1.58.0-alpha-2026-01-16 (#1312) 2026-01-16 09:49:16 -08:00
Yury Semikhatsky
9cc61b4faf chore: roll 1.58.0-alpha-2026-01-15 (#1309) 2026-01-15 11:18:57 -08:00
Yury Semikhatsky
33b4c00923 docs: reformat cline instructions, follow up to #1297 (#1303) 2026-01-09 14:59:08 -08:00
60 changed files with 2403 additions and 2036 deletions

View File

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

View File

@@ -7,7 +7,7 @@ on:
types: [published] types: [published]
jobs: jobs:
publish-canary-npm: publish-mcp-canary-npm:
if: github.event.schedule || github.event_name == 'workflow_dispatch' if: github.event.schedule || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -38,16 +38,19 @@ jobs:
- name: Update package.json version - name: Update package.json version
run: | run: |
npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version
working-directory: ./packages/playwright-mcp
- run: npm ci - run: npm ci
- run: npx playwright install --with-deps - run: npx playwright install --with-deps
- run: npm run lint - run: npm run lint
- run: npm run ctest - run: npm run ctest
working-directory: ./packages/playwright-mcp
- name: Publish to npm with next tag - name: Publish to npm with next tag
run: npm publish --tag next run: npm publish --tag next
working-directory: ./packages/playwright-mcp
publish-release-npm: publish-mcp-release-npm:
if: github.event_name == 'release' if: github.event_name == 'release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -66,9 +69,11 @@ jobs:
- run: npx playwright install --with-deps - run: npx playwright install --with-deps
- run: npm run lint - run: npm run lint
- run: npm run ctest - run: npm run ctest
working-directory: ./packages/playwright-mcp
- run: npm publish - run: npm publish
working-directory: ./packages/playwright-mcp
publish-release-docker: publish-mcp-release-docker:
if: github.event_name == 'release' if: github.event_name == 'release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -93,8 +98,7 @@ jobs:
id: build-push id: build-push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . file: ./Dockerfile
file: ./Dockerfile # Adjust path if your Dockerfile is elsewhere
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
@@ -127,17 +131,16 @@ jobs:
node-version: 20 node-version: 20
cache: 'npm' cache: 'npm'
- name: Install extension dependencies - name: Install extension dependencies
working-directory: ./extension
run: npm ci run: npm ci
- name: Build extension - name: Build extension
working-directory: ./extension working-directory: ./packages/extension
run: npm run build run: npm run build
- name: Get extension version - name: Get extension version
id: get-version id: get-version
working-directory: ./extension working-directory: ./packages/extension
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Package extension - name: Package extension
working-directory: ./extension working-directory: ./packages/extension
run: | run: |
cd dist cd dist
zip -r ../playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip . zip -r ../playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip .
@@ -146,4 +149,4 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
run: | 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/ node_modules/
test-results/ test-results/
playwright-report/ 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 \ 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.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.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 && \ npm ci --omit=dev && \
# Install system dependencies for playwright # Install system dependencies for playwright
npx -y playwright-core install-deps chromium 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 \ 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.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.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 npm ci
# Copy the rest of the app # Copy the rest of the app
COPY *.json *.js *.ts . COPY packages/playwright-mcp/*.json packages/playwright-mcp/*.js packages/playwright-mcp/*.ts .
# ------------------------------ # ------------------------------
# Browser # Browser
@@ -59,7 +61,7 @@ RUN chown -R ${USERNAME}:${USERNAME} node_modules
USER ${USERNAME} 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} 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) # 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", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"]

View File

@@ -84,15 +84,11 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
<details> <details>
<summary>Cline</summary> <summary>Cline</summary>
Follow the instruction in the section [MCP Integration](https://docs.cline.bot/mcp/mcp-overview) Follow the instruction in the section [Configuring MCP Servers](https://docs.cline.bot/mcp/configuring-mcp-servers)
Connection Types **Example: Local Setup**
Local (stdio): Cline runs the server as a local process.
Remote (streamableHttp): hosted/cloud environments.
Legacy (sse): Only use if the host doesn't support the modern streamableHttp protocol.
Example: Local Setup Add the following to your [`cline_mcp_settings.json`](https://docs.cline.bot/mcp/configuring-mcp-servers#editing-mcp-settings-files) file:
Add the following to your cline_mcp_settings.json file:
```json ```json
{ {
@@ -367,6 +363,10 @@ Playwright MCP server supports following arguments. They can be provided in the
--cdp-endpoint <endpoint> CDP endpoint to connect to. --cdp-endpoint <endpoint> CDP endpoint to connect to.
--cdp-header <headers...> CDP headers to send with the connect --cdp-header <headers...> CDP headers to send with the connect
request, multiple can be specified. request, multiple can be specified.
--codegen <lang> specify the language to use for code
generation, possible values:
"typescript", "none". Default is
"typescript".
--config <path> path to the configuration file. --config <path> path to the configuration file.
--console-level <level> level of console messages to return: --console-level <level> level of console messages to return:
"error", "warning", "info", "debug". "error", "warning", "info", "debug".
@@ -404,6 +404,10 @@ Playwright MCP server supports following arguments. They can be provided in the
--no-sandbox disable the sandbox for all process --no-sandbox disable the sandbox for all process
types that are normally sandboxed. types that are normally sandboxed.
--output-dir <path> path to the directory for output files. --output-dir <path> path to the directory for output files.
--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".
--port <port> port to listen on for SSE transport. --port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy, --proxy-bypass <bypass> comma-separated domains to bypass proxy,
for example for example
@@ -580,6 +584,11 @@ npx @playwright/mcp@latest --config path/to/config.json
*/ */
cdpHeaders?: Record<string, string>; cdpHeaders?: Record<string, string>;
/**
* Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout.
*/
cdpTimeout?: number;
/** /**
* Remote endpoint to connect to an existing Playwright server. * Remote endpoint to connect to an existing Playwright server.
*/ */
@@ -658,6 +667,11 @@ 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".
@@ -704,13 +718,18 @@ 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?: 'incremental' | 'full' | 'none';
} };
/** /**
* Whether to allow file uploads from anywhere on the file system. * 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. * By default (false), file uploads are restricted to paths within the MCP roots only.
*/ */
allowUnrestrictedFileAccess?: boolean; allowUnrestrictedFileAccess?: boolean;
/**
* Specify the language to use for code generation.
*/
codegen?: 'typescript' | 'none';
} }
``` ```
@@ -810,7 +829,7 @@ http.createServer(async (req, res) => {
- Title: Click - Title: Click
- Description: Perform click on a web page - Description: Perform click on a web page
- Parameters: - 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 - `ref` (string): Exact target element reference from the page snapshot
- `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
- `button` (string, optional): Button to click, defaults to left - `button` (string, optional): Button to click, defaults to left
@@ -831,7 +850,8 @@ http.createServer(async (req, res) => {
- Title: Get console messages - Title: Get console messages
- Description: Returns all console messages - Description: Returns all console messages
- Parameters: - 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** - Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->
@@ -891,7 +911,7 @@ http.createServer(async (req, res) => {
- Title: Hover mouse - Title: Hover mouse
- Description: Hover over element on page - Description: Hover over element on page
- Parameters: - 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 - `ref` (string): Exact target element reference from the page snapshot
- Read-only: **false** - Read-only: **false**
@@ -908,7 +928,7 @@ http.createServer(async (req, res) => {
- **browser_navigate_back** - **browser_navigate_back**
- Title: Go back - Title: Go back
- Description: Go back to the previous page - Description: Go back to the previous page in the history
- Parameters: None - Parameters: None
- Read-only: **false** - Read-only: **false**
@@ -918,7 +938,8 @@ 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, 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** - Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->
@@ -955,7 +976,7 @@ http.createServer(async (req, res) => {
- Title: Select option - Title: Select option
- Description: Select an option in a dropdown - Description: Select an option in a dropdown
- Parameters: - 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 - `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. - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
- Read-only: **false** - Read-only: **false**
@@ -975,7 +996,7 @@ http.createServer(async (req, res) => {
- Title: Take a screenshot - 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. - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
- Parameters: - 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. - `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. - `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. - `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.
@@ -988,7 +1009,7 @@ http.createServer(async (req, res) => {
- Title: Type text - Title: Type text
- Description: Type text into editable element - Description: Type text into editable element
- Parameters: - 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 - `ref` (string): Exact target element reference from the page snapshot
- `text` (string): Text to type into the element - `text` (string): Text to type into the element
- `submit` (boolean, optional): Whether to submit entered text (press Enter after) - `submit` (boolean, optional): Whether to submit entered text (press Enter after)
@@ -1045,18 +1066,25 @@ http.createServer(async (req, res) => {
- Title: Click - Title: Click
- Description: Click left mouse button at a given position - Description: Click left mouse button at a given position
- Parameters: - Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `x` (number): X coordinate - `x` (number): X coordinate
- `y` (number): Y coordinate - `y` (number): Y coordinate
- Read-only: **false** - Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js --> <!-- 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** - **browser_mouse_drag_xy**
- Title: Drag mouse - Title: Drag mouse
- Description: Drag left mouse button to a given position - Description: Drag left mouse button to a given position
- Parameters: - Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `startX` (number): Start X coordinate - `startX` (number): Start X coordinate
- `startY` (number): Start Y coordinate - `startY` (number): Start Y coordinate
- `endX` (number): End X coordinate - `endX` (number): End X coordinate
@@ -1069,11 +1097,29 @@ http.createServer(async (req, res) => {
- Title: Move mouse - Title: Move mouse
- Description: Move mouse to a given position - Description: Move mouse to a given position
- Parameters: - Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `x` (number): X coordinate - `x` (number): X coordinate
- `y` (number): Y coordinate - `y` (number): Y coordinate
- Read-only: **false** - 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>
<details> <details>
@@ -1099,7 +1145,7 @@ http.createServer(async (req, res) => {
- Title: Create locator for element - Title: Create locator for element
- Description: Generate locator for the given element to use in tests - Description: Generate locator for the given element to use in tests
- Parameters: - 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 - `ref` (string): Exact target element reference from the page snapshot
- Read-only: **true** - Read-only: **true**

File diff suppressed because it is too large Load Diff

2148
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,30 @@
{ {
"name": "@playwright/mcp", "name": "playwright-mcp-internal",
"version": "0.0.55", "version": "0.0.58",
"description": "Playwright Tools for MCP", "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/microsoft/playwright-mcp.git" "url": "git+https://github.com/microsoft/playwright-mcp.git"
}, },
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
"engines": {
"node": ">=18"
},
"author": { "author": {
"name": "Microsoft Corporation" "name": "Microsoft Corporation"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"lint": "npm run update-readme",
"update-readme": "node update-readme.js",
"docker-build": "docker build --no-cache -t playwright-mcp-dev:latest .", "docker-build": "docker build --no-cache -t playwright-mcp-dev:latest .",
"docker-rm": "docker rm playwright-mcp-dev", "docker-rm": "docker rm playwright-mcp-dev",
"docker-run": "docker run -it -p 8080:8080 --name playwright-mcp-dev playwright-mcp-dev:latest", "docker-run": "docker run -it -p 8080:8080 --name playwright-mcp-dev playwright-mcp-dev:latest",
"test": "playwright test", "lint": "npm run lint --workspaces",
"ctest": "playwright test --project=chrome", "test": "npm run test --workspaces",
"ftest": "playwright test --project=firefox", "build": "npm run build --workspaces"
"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-07",
"playwright-core": "1.58.0-alpha-2026-01-07"
},
"bin": {
"mcp-server-playwright": "cli.js"
}, },
"workspaces": [
"packages/*"
],
"devDependencies": { "devDependencies": {
"@modelcontextprotocol/sdk": "^1.24.0", "@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.58.0-alpha-2026-01-07", "@playwright/test": "1.59.0-alpha-1769217009000",
"@types/node": "^24.3.0" "@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, "manifest_version": 3,
"name": "Playwright MCP Bridge", "name": "Playwright MCP Bridge",
"version": "0.0.55", "version": "0.0.58",
"description": "Share browser tabs with Playwright MCP server", "description": "Share browser tabs with Playwright MCP server",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB",
"permissions": [ "permissions": [

View File

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

View File

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

View File

@@ -17,11 +17,11 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { chromium } from 'playwright'; 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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { BrowserContext } from 'playwright'; import type { BrowserContext } from 'playwright';
import type { StartClient } from '../../tests/fixtures'; import type { StartClient } from '../../playwright-mcp/tests/fixtures';
type BrowserWithExtension = { type BrowserWithExtension = {
userDataDir: string; userDataDir: string;
@@ -134,7 +134,7 @@ test(`navigate with extension`, async ({ browserWithExtension, startClient, serv
await selectorPage.getByRole('button', { name: 'Allow' }).click(); await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({ 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(); await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();
expect(await navigateResponse).toHaveResponse({ 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); expect(browserContext.pages()).toHaveLength(4);
@@ -187,7 +187,7 @@ test(`extension not installed timeout`, async ({ browserWithExtension, startClie
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({ })).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, isError: true,
}); });
@@ -216,7 +216,7 @@ testWithOldExtensionVersion(`works with old extension version`, async ({ browser
await selectorPage.getByRole('button', { name: 'Allow' }).click(); await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({ 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`); await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`);
expect(await navigateResponse).toHaveResponse({ expect(await navigateResponse).toHaveResponse({
result: expect.stringContaining('Extension connection timeout.'), error: expect.stringContaining('Extension connection timeout.'),
isError: true, isError: true,
}); });
}); });
@@ -267,10 +267,9 @@ test(`custom executablePath`, async ({ startClient, server, useShortConnectionTi
const navigateResponse = await client.callTool({ const navigateResponse = await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
timeout: 1000,
}); });
expect(await navigateResponse).toHaveResponse({ expect(await navigateResponse).toHaveResponse({
result: expect.stringContaining('Extension connection timeout.'), error: expect.stringContaining('Extension connection timeout.'),
isError: true, isError: true,
}); });
expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?'); 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({ 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'; 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 = { export type Config = {
/** /**
@@ -64,6 +74,11 @@ export type Config = {
*/ */
cdpHeaders?: Record<string, string>; cdpHeaders?: Record<string, string>;
/**
* Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout.
*/
cdpTimeout?: number;
/** /**
* Remote endpoint to connect to an existing Playwright server. * Remote endpoint to connect to an existing Playwright server.
*/ */
@@ -142,6 +157,11 @@ 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".
@@ -188,11 +208,16 @@ 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?: 'incremental' | 'full' | 'none';
} };
/** /**
* Whether to allow file uploads from anywhere on the file system. * 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. * By default (false), file uploads are restricted to paths within the MCP roots only.
*/ */
allowUnrestrictedFileAccess?: boolean; 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.58",
"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-1769217009000",
"playwright-core": "1.59.0-alpha-1769217009000"
},
"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 }, arguments: { url: server.PREFIX },
})).toHaveResponse({ })).toHaveResponse({
code: `await page.goto('${server.PREFIX}');`, code: `await page.goto('${server.PREFIX}');`,
pageState: expect.stringContaining(`- button \"Submit\" [ref=e2]`), snapshot: expect.stringContaining(`- button \"Submit\" [ref=e2]`),
}); });
expect(await client.callTool({ expect(await client.callTool({
@@ -44,6 +44,6 @@ test('browser_click', async ({ client, server }) => {
}, },
})).toHaveResponse({ })).toHaveResponse({
code: `await page.getByRole('button', { name: 'Submit' }).click();`, 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 }, arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({ })).toHaveResponse({
code: `await page.goto('${server.HELLO_WORLD}');`, code: `await page.goto('${server.HELLO_WORLD}');`,
pageState: `- Page URL: ${server.HELLO_WORLD} snapshot: expect.stringContaining(`generic [active] [ref=e1]: Hello, world!`),
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Hello, world!
\`\`\``,
}); });
}); });

View File

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

View File

@@ -18,14 +18,15 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const { zodToJsonSchema } = require('zod-to-json-schema')
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { browserTools } = require('playwright/lib/mcp/browser/tools'); const { browserTools } = require('playwright/lib/mcp/browser/tools');
const capabilities = { const capabilities = {
'core-navigation': 'Core automation',
'core': 'Core automation', 'core': 'Core automation',
'core-tabs': 'Tab management', 'core-tabs': 'Tab management',
'core-input': 'Core automation',
'core-install': 'Browser installation', 'core-install': 'Browser installation',
'vision': 'Coordinate-based (opt-in via --caps=vision)', 'vision': 'Coordinate-based (opt-in via --caps=vision)',
'pdf': 'PDF generation (opt-in via --caps=pdf)', 'pdf': 'PDF generation (opt-in via --caps=pdf)',
@@ -33,7 +34,15 @@ const capabilities = {
'tracing': 'Tracing (opt-in via --caps=tracing)', '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 * @param {any} tool
@@ -47,7 +56,7 @@ function formatToolForReadme(tool) {
lines.push(` - Title: ${tool.title}`); lines.push(` - Title: ${tool.title}`);
lines.push(` - Description: ${tool.description}`); 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 || []; const requiredParams = inputSchema.required || [];
if (inputSchema.properties && Object.keys(inputSchema.properties).length) { if (inputSchema.properties && Object.keys(inputSchema.properties).length) {
lines.push(` - Parameters:`); lines.push(` - Parameters:`);
@@ -160,14 +169,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() { 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 readmeContent = await fs.promises.readFile(readmePath, 'utf-8');
const withTools = await updateTools(readmeContent); const withTools = await updateTools(readmeContent);
const withOptions = await updateOptions(withTools); const withOptions = await updateOptions(withTools);
const withConfig = await updateConfig(withOptions); const withConfig = await updateConfig(withOptions);
await fs.promises.writeFile(readmePath, withConfig, 'utf-8'); await fs.promises.writeFile(readmePath, withConfig, 'utf-8');
console.log('README updated successfully'); console.log('README updated successfully');
await copyToPackage('README.md');
await copyToPackage('LICENSE');
} }
updateReadme().catch(err => { updateReadme().catch(err => {