#
tokens: 49573/50000 75/95 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/cloudflare/playwright-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       ├── cf_ci.yml
│       ├── cf_publish_release_npm.yml
│       ├── ci.yml
│       └── publish.yml
├── .gitignore
├── .npmignore
├── cli.js
├── cloudflare
│   ├── .npmignore
│   ├── example
│   │   ├── .gitignore
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.toml
│   ├── index.d.ts
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   ├── index.ts
│   │   └── package.ts
│   ├── tsconfig.json
│   └── vite.config.ts
├── config.d.ts
├── Dockerfile
├── docs
│   └── imgs
│       ├── claudemcp.gif
│       ├── playground-ai-screenshot.png
│       ├── todomvc-screenshot-1.png
│       ├── todomvc-screenshot-2.png
│       └── todomvc-screenshot-3.png
├── eslint.config.mjs
├── examples
│   └── generate-test.md
├── index.d.ts
├── index.js
├── LICENSE
├── package-lock.json
├── package.json
├── playwright.config.ts
├── README.md
├── SECURITY.md
├── src
│   ├── browserContextFactory.ts
│   ├── browserServer.ts
│   ├── config.ts
│   ├── connection.ts
│   ├── context.ts
│   ├── fileUtils.ts
│   ├── httpServer.ts
│   ├── index.ts
│   ├── javascript.ts
│   ├── manualPromise.ts
│   ├── package.ts
│   ├── pageSnapshot.ts
│   ├── program.ts
│   ├── server.ts
│   ├── tab.ts
│   ├── tools
│   │   ├── common.ts
│   │   ├── console.ts
│   │   ├── dialogs.ts
│   │   ├── files.ts
│   │   ├── install.ts
│   │   ├── keyboard.ts
│   │   ├── navigate.ts
│   │   ├── network.ts
│   │   ├── pdf.ts
│   │   ├── screenshot.ts
│   │   ├── snapshot.ts
│   │   ├── tabs.ts
│   │   ├── testing.ts
│   │   ├── tool.ts
│   │   ├── utils.ts
│   │   ├── vision.ts
│   │   └── wait.ts
│   ├── tools.ts
│   └── transport.ts
├── tests
│   ├── browser-server.spec.ts
│   ├── capabilities.spec.ts
│   ├── cdp.spec.ts
│   ├── config.spec.ts
│   ├── console.spec.ts
│   ├── core.spec.ts
│   ├── device.spec.ts
│   ├── dialogs.spec.ts
│   ├── files.spec.ts
│   ├── fixtures.ts
│   ├── headed.spec.ts
│   ├── iframes.spec.ts
│   ├── install.spec.ts
│   ├── launch.spec.ts
│   ├── library.spec.ts
│   ├── network.spec.ts
│   ├── pdf.spec.ts
│   ├── request-blocking.spec.ts
│   ├── screenshot.spec.ts
│   ├── sse.spec.ts
│   ├── tabs.spec.ts
│   ├── testserver
│   │   ├── cert.pem
│   │   ├── index.ts
│   │   ├── key.pem
│   │   └── san.cnf
│   ├── trace.spec.ts
│   ├── wait.spec.ts
│   └── webdriver.spec.ts
├── tsconfig.all.json
├── tsconfig.json
└── utils
    ├── copyright.js
    ├── generate-links.js
    └── update-readme.js
```

# Files

--------------------------------------------------------------------------------
/cloudflare/example/.gitignore:
--------------------------------------------------------------------------------

```
1 | .wrangler
2 | node_modules
3 | 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
1 | **/*
2 | README.md
3 | LICENSE
4 | !lib/**/*.js
5 | !cli.js
6 | !index.*
7 | !config.d.ts
8 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | lib/
2 | node_modules/
3 | test-results/
4 | playwright-report/
5 | .vscode/mcp.json
6 | 
7 | .idea
8 | .DS_Store
9 | 
```

--------------------------------------------------------------------------------
/cloudflare/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # This ignores everything by default, except for package.json and LICENSE and README.md.
 2 | # See https://docs.npmjs.com/misc/developers
 3 | **/*
 4 | 
 5 | # Include sources from lib, but not map files.
 6 | !lib/**/*.js
 7 | 
 8 | # Include playwright core and test entry points
 9 | !index.d.ts
10 | 
```

--------------------------------------------------------------------------------
/cloudflare/example/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Cloudflare Playwright MCP Example
  2 | 
  3 | [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/playwright-mcp/tree/main/cloudflare/example)
  4 | 
  5 | ### Overview
  6 | 
  7 | This project demonstrates how to use [Playwright with Cloudflare Workers](https://github.com/cloudflare/playwright) as a Model Control Protocol (MCP) server using [Cloudflare Playwright MCP](https://github.com/cloudflare/playwright-mcp).
  8 | 
  9 | It enables AI assistants to control a browser through a set of tools, allowing them to perform web automation tasks like navigation, typing, clicking, and taking screenshots.
 10 | 
 11 | The server can be used with various AI platforms including Cloudflare AI Playground, Claude Desktop, and GitHub Copilot in VS Code.
 12 | 
 13 | ### Deploy
 14 | 
 15 | Follow these steps to set up and deploy the project:
 16 | 
 17 | 1. Install dependencies:
 18 | ```bash
 19 | npm ci
 20 | ```
 21 | 
 22 | 2. Deploy to Cloudflare Workers:
 23 | 
 24 | ```bash
 25 | npx wrangler deploy
 26 | ```
 27 | 
 28 | ### Use with Cloudflare AI Playground
 29 | 
 30 | ![alt text](https://github.com/cloudflare/playwright-mcp/raw/main/docs/imgs/playground-ai-screenshot.png)
 31 | 
 32 | [Cloudflare playground AI](https://playground.ai.cloudflare.com/) is a great way to test MCP servers using LLM models available in Workers AI.
 33 | 
 34 | - Navigate to https://playground.ai.cloudflare.com/
 35 | - Ensure model is set to `llama-3.3-70b-instruct-fp8-fast`
 36 | - In **MCP Servers**, set **URL** to `https://[my-mcp-url].workers.dev/sse`
 37 | - Click **Connect**
 38 | - Status should update to **Connected** and it should list 14 available tools
 39 | 
 40 | You can now start to interact with the model, and it will run necessary tools to accomplish what was requested.
 41 | 
 42 | > [!TIP]
 43 | > For best results, give simple instructions consisting of one single action, e. g., "Create a new todo entry", "Go to cloudflare site", "Take a screenshot"
 44 | 
 45 | Example of a conversation:
 46 | 
 47 | | Role | Message |
 48 | |------|---------|
 49 | | **User** | Go to demo.playwright.dev/todomvc |
 50 | | **Assistant** | *runs browser_navigate tool*<br> The page has been navigated to https://demo.playwright.dev/todomvc/. The page title is "React • TodoMVC". The page snapshot shows the content of the page, including a heading, a textbox, and several links. |
 51 | | **User** | Create some todo entry |
 52 | | **Assistant** |  *runs browser_type tool*<br> A new todo entry "Buy groceries" has been created. The page snapshot shows the updated content of the page, including the new todo entry. |
 53 | | **User** | Nice. Now create a todo in parrot style |
 54 | | **Assistant** | *runs browser_type tool*<br> A new todo entry "Polly wants a cracker" has been created in parrot style. The page snapshot shows the updated content of the page, including the new todo entry. |
 55 | | **User** | And create another todo in yoda style |
 56 | | **Assistant** | *runs browser_type tool*<br> A new todo entry "A walk, I must take" has been created in Yoda style. The page snapshot shows the updated content of the page, including the new todo entry. |
 57 | | **User** | Take a screenshot |
 58 | | **Assistant** | *runs browser_take_screenshot tool*<br> ![alt text](https://github.com/cloudflare/playwright-mcp/raw/main/docs/imgs/todomvc-screenshot-1.png) A screenshot of the current page has been taken. The screenshot is in PNG format and is 56KB in size. |
 59 | | **User** | Now click the checkbox of the parrot todo entry |
 60 | | **Assistant** | *runs browser_clickt tool*<br> The checkbox of the parrot todo entry "Polly wants a cracker" has been clicked. The page snapshot shows the updated content of the page, with the checkbox now checked. |
 61 | | **User** | Take a screenshot |
 62 | | **Assistant** | *runs browser_take_screenshot tool*<br> ![alt text](https://github.com/cloudflare/playwright-mcp/raw/main/docs/imgs/todomvc-screenshot-2.png) A screenshot of the current page has been taken. The screenshot is in PNG format and is 58KB in size. |
 63 | | **User** | Remove completed todo entries |
 64 | | **Assistant** | *runs browser_clickt tool*<br> The "Clear completed" button has been clicked. The page snapshot shows the updated content of the page, with the completed todo entry "Polly wants a cracker" removed. |
 65 | | **User** | Take a final screenshot |
 66 | | **Assistant** | *runs browser_take_screenshot tool*<br> ![alt text](https://github.com/cloudflare/playwright-mcp/raw/main/docs/imgs/todomvc-screenshot-3.png) A final screenshot of the current page has been taken. The screenshot is in PNG format and is 50KB in size. |
 67 | 
 68 | ### Use with Claude Desktop
 69 | 
 70 | As of now, [Claude Desktop](https://claude.ai/download) only supports local MCP servers. To use `playground-mcp` with Claude Desktop we make use of [mcp-remote](https://github.com/geelen/mcp-remote), a tool that proxies remote MCP servers and exposes them locally. Use the following configuration:
 71 | 
 72 | 1. Open the configuration file for Claude Desktop.
 73 | 2. Add the following JSON snippet under the `mcpServers` section:
 74 | 
 75 | ```json
 76 | {
 77 |   "mcpServers": {
 78 |     "cloudflare-playwright-mcp": {
 79 |       "command": "npx",
 80 |       "args": [
 81 |         "mcp-remote",
 82 |         "https://[my-mcp-url].workers.dev/sse"
 83 |       ]
 84 |     }
 85 |   }
 86 | }
 87 | ```
 88 | 
 89 | 3. Save the configuration file and **restart** Claude Desktop to apply the changes.
 90 | 
 91 | This setup ensures that Claude Desktop can communicate with the Cloudflare Playwright MCP server.
 92 | 
 93 | Here's an example of a session opening the TODO demo app, adding "buy lemons" and doing a screenshot, taking advantage of playwright-mcp tools and Browser Rendering:
 94 | 
 95 | ![alt text](https://github.com/cloudflare/playwright-mcp/raw/main/docs/imgs/claudemcp.gif)
 96 | 
 97 | ### Configure in VSCode
 98 | 
 99 | You can install the Playwright MCP server using the [VS Code CLI](https://code.visualstudio.com/docs/configure/command-line):
100 | 
101 | ```bash
102 | # For VS Code
103 | code --add-mcp '{"name":"cloudflare-playwright","type":"sse","url":"https://[my-mcp-url].workers.dev/sse"}'
104 | ```
105 | 
106 | ```bash
107 | # For VS Code Insiders
108 | code-insiders --add-mcp '{"name":"cloudflare-playwright","type":"sse","url":"https://[my-mcp-url].workers.dev/sse"}'
109 | ```
110 | 
111 | After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
112 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Cloudflare Playwright MCP
  2 | 
  3 | [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/playwright-mcp/tree/main/cloudflare/example)
  4 | 
  5 | ### Overview
  6 | 
  7 | This project leverages Playwright for automated browser testing and integrates with Cloudflare Workers, [Browser Rendering](https://developers.cloudflare.com/browser-rendering/) and [`@cloudflare/playwright`](https://github.com/cloudflare/playwright) for deployment.
  8 | 
  9 | ### Build and Deploy
 10 | 
 11 | Follow these steps to set up and deploy the project:
 12 | 
 13 | 1. Install dependencies:
 14 | ```bash
 15 | npm ci
 16 | ```
 17 | 
 18 | 2. Build:
 19 | ```bash
 20 | cd cloudflare
 21 | npm run build
 22 | ```
 23 | 
 24 | 3. Deploy to Cloudflare Workers:
 25 | 
 26 | ```bash
 27 | cd cloudflare/example
 28 | npm ci
 29 | npx wrangler deploy
 30 | ```
 31 | 
 32 | ### Use with Cloudflare AI Playground
 33 | 
 34 | ![alt text](docs/imgs/playground-ai-screenshot.png)
 35 | 
 36 | [Cloudflare playground AI](https://playground.ai.cloudflare.com/) is a great way to test MCP servers using LLM models available in Workers AI.
 37 | 
 38 | - Navigate to https://playground.ai.cloudflare.com/
 39 | - Ensure model is set to `llama-3.3-70b-instruct-fp8-fast`
 40 | - In **MCP Servers**, set **URL** to `https://[my-mcp-url].workers.dev/sse`
 41 | - Click **Connect**
 42 | - Status should update to **Connected** and it should list 14 available tools
 43 | 
 44 | You can now start to interact with the model, and it will run necessary tools to accomplish what was requested.
 45 | 
 46 | > [!TIP]
 47 | > For best results, give simple instructions consisting of one single action, e. g., "Create a new todo entry", "Go to cloudflare site", "Take a screenshot"
 48 | 
 49 | 
 50 | Example of a conversation:
 51 | 
 52 | | Role | Message |
 53 | |------|---------|
 54 | | **User** | Go to demo.playwright.dev/todomvc |
 55 | | **Assistant** | *runs browser_navigate tool*<br> The page has been navigated to https://demo.playwright.dev/todomvc/. The page title is "React • TodoMVC". The page snapshot shows the content of the page, including a heading, a textbox, and several links. |
 56 | | **User** | Create some todo entry |
 57 | | **Assistant** |  *runs browser_type tool*<br> A new todo entry "Buy groceries" has been created. The page snapshot shows the updated content of the page, including the new todo entry. |
 58 | | **User** | Nice. Now create a todo in parrot style |
 59 | | **Assistant** | *runs browser_type tool*<br> A new todo entry "Polly wants a cracker" has been created in parrot style. The page snapshot shows the updated content of the page, including the new todo entry. |
 60 | | **User** | And create another todo in yoda style |
 61 | | **Assistant** | *runs browser_type tool*<br> A new todo entry "A walk, I must take" has been created in Yoda style. The page snapshot shows the updated content of the page, including the new todo entry. |
 62 | | **User** | Take a screenshot |
 63 | | **Assistant** | *runs browser_take_screenshot tool*<br> ![alt text](docs/imgs/todomvc-screenshot-1.png) A screenshot of the current page has been taken. The screenshot is in PNG format and is 56KB in size. |
 64 | | **User** | Now click the checkbox of the parrot todo entry |
 65 | | **Assistant** | *runs browser_clickt tool*<br> The checkbox of the parrot todo entry "Polly wants a cracker" has been clicked. The page snapshot shows the updated content of the page, with the checkbox now checked. |
 66 | | **User** | Take a screenshot |
 67 | | **Assistant** | *runs browser_take_screenshot tool*<br> ![alt text](docs/imgs/todomvc-screenshot-2.png) A screenshot of the current page has been taken. The screenshot is in PNG format and is 58KB in size. |
 68 | | **User** | Remove completed todo entries |
 69 | | **Assistant** | *runs browser_clickt tool*<br> The "Clear completed" button has been clicked. The page snapshot shows the updated content of the page, with the completed todo entry "Polly wants a cracker" removed. |
 70 | | **User** | Take a final screenshot |
 71 | | **Assistant** | *runs browser_take_screenshot tool*<br> ![alt text](docs/imgs/todomvc-screenshot-3.png) A final screenshot of the current page has been taken. The screenshot is in PNG format and is 50KB in size. |
 72 | 
 73 | ### Use with Claude Desktop
 74 | 
 75 | As of now, [Claude Desktop](https://claude.ai/download) only supports local MCP servers. To use `playground-mcp` with Claude Desktop we make use of [mcp-remote](https://github.com/geelen/mcp-remote), a tool that proxies remote MCP servers and exposes them locally. Use the following configuration:
 76 | 
 77 | 1. Open the configuration file for Claude Desktop.
 78 | 2. Add the following JSON snippet under the `mcpServers` section:
 79 | 
 80 | ```json
 81 | {
 82 |   "mcpServers": {
 83 |     "cloudflare-playwright-mcp": {
 84 |       "command": "npx",
 85 |       "args": [
 86 |         "mcp-remote",
 87 |         "https://[my-mcp-url].workers.dev/sse"
 88 |       ]
 89 |     }
 90 |   }
 91 | }
 92 | ```
 93 | 
 94 | 3. Save the configuration file and **restart** Claude Desktop to apply the changes.
 95 | 
 96 | This setup ensures that Claude Desktop can communicate with the Cloudflare Playwright MCP server.
 97 | 
 98 | Here's an example of a session opening the TODO demo app, adding "buy lemons" and doing a screenshot, taking advantage of playwright-mcp tools and Browser Rendering:
 99 | 
100 | ![alt text](docs/imgs/claudemcp.gif)
101 | 
102 | ### Configure in VSCode
103 | 
104 | You can install the Playwright MCP server using the [VS Code CLI](https://code.visualstudio.com/docs/configure/command-line):
105 | 
106 | ```bash
107 | # For VS Code
108 | code --add-mcp '{"name":"cloudflare-playwright","type":"sse","url":"https://[my-mcp-url].workers.dev/sse"}'
109 | ```
110 | 
111 | ```bash
112 | # For VS Code Insiders
113 | code-insiders --add-mcp '{"name":"cloudflare-playwright","type":"sse","url":"https://[my-mcp-url].workers.dev/sse"}'
114 | ```
115 | 
116 | After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
117 | </details>
118 | 
119 | ### Tool Modes
120 | 
121 | The tools are available in two modes:
122 | 
123 | 1. **Snapshot Mode** (default): Uses accessibility snapshots for better performance and reliability
124 | 2. **Vision Mode**: Uses screenshots for visual-based interactions
125 | 
126 | Vision Mode works best with the computer use models that are able to interact with elements using
127 | X Y coordinate space, based on the provided screenshot.
128 | 
129 | <!--- Tools generated by update-readme.js -->
130 | 
131 | <details>
132 | <summary><b>Interactions</b></summary>
133 | 
134 | <!-- NOTE: This has been generated via update-readme.js -->
135 | 
136 | - **browser_snapshot**
137 |   - Title: Page snapshot
138 |   - Description: Capture accessibility snapshot of the current page, this is better than screenshot
139 |   - Parameters: None
140 |   - Read-only: **true**
141 | 
142 | <!-- NOTE: This has been generated via update-readme.js -->
143 | 
144 | - **browser_click**
145 |   - Title: Click
146 |   - Description: Perform click on a web page
147 |   - Parameters:
148 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
149 |     - `ref` (string): Exact target element reference from the page snapshot
150 |     - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
151 |   - Read-only: **false**
152 | 
153 | <!-- NOTE: This has been generated via update-readme.js -->
154 | 
155 | - **browser_drag**
156 |   - Title: Drag mouse
157 |   - Description: Perform drag and drop between two elements
158 |   - Parameters:
159 |     - `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element
160 |     - `startRef` (string): Exact source element reference from the page snapshot
161 |     - `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element
162 |     - `endRef` (string): Exact target element reference from the page snapshot
163 |   - Read-only: **false**
164 | 
165 | <!-- NOTE: This has been generated via update-readme.js -->
166 | 
167 | - **browser_hover**
168 |   - Title: Hover mouse
169 |   - Description: Hover over element on page
170 |   - Parameters:
171 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
172 |     - `ref` (string): Exact target element reference from the page snapshot
173 |   - Read-only: **true**
174 | 
175 | <!-- NOTE: This has been generated via update-readme.js -->
176 | 
177 | - **browser_type**
178 |   - Title: Type text
179 |   - Description: Type text into editable element
180 |   - Parameters:
181 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
182 |     - `ref` (string): Exact target element reference from the page snapshot
183 |     - `text` (string): Text to type into the element
184 |     - `submit` (boolean, optional): Whether to submit entered text (press Enter after)
185 |     - `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.
186 |   - Read-only: **false**
187 | 
188 | <!-- NOTE: This has been generated via update-readme.js -->
189 | 
190 | - **browser_select_option**
191 |   - Title: Select option
192 |   - Description: Select an option in a dropdown
193 |   - Parameters:
194 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
195 |     - `ref` (string): Exact target element reference from the page snapshot
196 |     - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
197 |   - Read-only: **false**
198 | 
199 | <!-- NOTE: This has been generated via update-readme.js -->
200 | 
201 | - **browser_press_key**
202 |   - Title: Press a key
203 |   - Description: Press a key on the keyboard
204 |   - Parameters:
205 |     - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
206 |   - Read-only: **false**
207 | 
208 | <!-- NOTE: This has been generated via update-readme.js -->
209 | 
210 | - **browser_wait_for**
211 |   - Title: Wait for
212 |   - Description: Wait for text to appear or disappear or a specified time to pass
213 |   - Parameters:
214 |     - `time` (number, optional): The time to wait in seconds
215 |     - `text` (string, optional): The text to wait for
216 |     - `textGone` (string, optional): The text to wait for to disappear
217 |   - Read-only: **true**
218 | 
219 | <!-- NOTE: This has been generated via update-readme.js -->
220 | 
221 | - **browser_file_upload**
222 |   - Title: Upload files
223 |   - Description: Upload one or multiple files
224 |   - Parameters:
225 |     - `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
226 |   - Read-only: **false**
227 | 
228 | <!-- NOTE: This has been generated via update-readme.js -->
229 | 
230 | - **browser_handle_dialog**
231 |   - Title: Handle a dialog
232 |   - Description: Handle a dialog
233 |   - Parameters:
234 |     - `accept` (boolean): Whether to accept the dialog.
235 |     - `promptText` (string, optional): The text of the prompt in case of a prompt dialog.
236 |   - Read-only: **false**
237 | 
238 | </details>
239 | 
240 | <details>
241 | <summary><b>Navigation</b></summary>
242 | 
243 | <!-- NOTE: This has been generated via update-readme.js -->
244 | 
245 | - **browser_navigate**
246 |   - Title: Navigate to a URL
247 |   - Description: Navigate to a URL
248 |   - Parameters:
249 |     - `url` (string): The URL to navigate to
250 |   - Read-only: **false**
251 | 
252 | <!-- NOTE: This has been generated via update-readme.js -->
253 | 
254 | - **browser_navigate_back**
255 |   - Title: Go back
256 |   - Description: Go back to the previous page
257 |   - Parameters: None
258 |   - Read-only: **true**
259 | 
260 | <!-- NOTE: This has been generated via update-readme.js -->
261 | 
262 | - **browser_navigate_forward**
263 |   - Title: Go forward
264 |   - Description: Go forward to the next page
265 |   - Parameters: None
266 |   - Read-only: **true**
267 | 
268 | </details>
269 | 
270 | <details>
271 | <summary><b>Resources</b></summary>
272 | 
273 | <!-- NOTE: This has been generated via update-readme.js -->
274 | 
275 | - **browser_take_screenshot**
276 |   - Title: Take a screenshot
277 |   - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
278 |   - Parameters:
279 |     - `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
280 |     - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
281 |     - `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.
282 |     - `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.
283 |   - Read-only: **true**
284 | 
285 | <!-- NOTE: This has been generated via update-readme.js -->
286 | 
287 | - **browser_pdf_save**
288 |   - Title: Save as PDF
289 |   - Description: Save page as PDF
290 |   - Parameters:
291 |     - `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.
292 |   - Read-only: **true**
293 | 
294 | <!-- NOTE: This has been generated via update-readme.js -->
295 | 
296 | - **browser_network_requests**
297 |   - Title: List network requests
298 |   - Description: Returns all network requests since loading the page
299 |   - Parameters: None
300 |   - Read-only: **true**
301 | 
302 | <!-- NOTE: This has been generated via update-readme.js -->
303 | 
304 | - **browser_console_messages**
305 |   - Title: Get console messages
306 |   - Description: Returns all console messages
307 |   - Parameters: None
308 |   - Read-only: **true**
309 | 
310 | </details>
311 | 
312 | <details>
313 | <summary><b>Utilities</b></summary>
314 | 
315 | <!-- NOTE: This has been generated via update-readme.js -->
316 | 
317 | - **browser_install**
318 |   - Title: Install the browser specified in the config
319 |   - Description: Install the browser specified in the config. Call this if you get an error about the browser not being installed.
320 |   - Parameters: None
321 |   - Read-only: **false**
322 | 
323 | <!-- NOTE: This has been generated via update-readme.js -->
324 | 
325 | - **browser_close**
326 |   - Title: Close browser
327 |   - Description: Close the page
328 |   - Parameters: None
329 |   - Read-only: **true**
330 | 
331 | <!-- NOTE: This has been generated via update-readme.js -->
332 | 
333 | - **browser_resize**
334 |   - Title: Resize browser window
335 |   - Description: Resize the browser window
336 |   - Parameters:
337 |     - `width` (number): Width of the browser window
338 |     - `height` (number): Height of the browser window
339 |   - Read-only: **true**
340 | 
341 | </details>
342 | 
343 | <details>
344 | <summary><b>Tabs</b></summary>
345 | 
346 | <!-- NOTE: This has been generated via update-readme.js -->
347 | 
348 | - **browser_tab_list**
349 |   - Title: List tabs
350 |   - Description: List browser tabs
351 |   - Parameters: None
352 |   - Read-only: **true**
353 | 
354 | <!-- NOTE: This has been generated via update-readme.js -->
355 | 
356 | - **browser_tab_new**
357 |   - Title: Open a new tab
358 |   - Description: Open a new tab
359 |   - Parameters:
360 |     - `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
361 |   - Read-only: **true**
362 | 
363 | <!-- NOTE: This has been generated via update-readme.js -->
364 | 
365 | - **browser_tab_select**
366 |   - Title: Select a tab
367 |   - Description: Select a tab by index
368 |   - Parameters:
369 |     - `index` (number): The index of the tab to select
370 |   - Read-only: **true**
371 | 
372 | <!-- NOTE: This has been generated via update-readme.js -->
373 | 
374 | - **browser_tab_close**
375 |   - Title: Close a tab
376 |   - Description: Close a tab
377 |   - Parameters:
378 |     - `index` (number, optional): The index of the tab to close. Closes current tab if not provided.
379 |   - Read-only: **false**
380 | 
381 | </details>
382 | 
383 | <details>
384 | <summary><b>Testing</b></summary>
385 | 
386 | <!-- NOTE: This has been generated via update-readme.js -->
387 | 
388 | - **browser_generate_playwright_test**
389 |   - Title: Generate a Playwright test
390 |   - Description: Generate a Playwright test for given scenario
391 |   - Parameters:
392 |     - `name` (string): The name of the test
393 |     - `description` (string): The description of the test
394 |     - `steps` (array): The steps of the test
395 |   - Read-only: **true**
396 | 
397 | </details>
398 | 
399 | <details>
400 | <summary><b>Vision mode</b></summary>
401 | 
402 | <!-- NOTE: This has been generated via update-readme.js -->
403 | 
404 | - **browser_screen_capture**
405 |   - Title: Take a screenshot
406 |   - Description: Take a screenshot of the current page
407 |   - Parameters: None
408 |   - Read-only: **true**
409 | 
410 | <!-- NOTE: This has been generated via update-readme.js -->
411 | 
412 | - **browser_screen_move_mouse**
413 |   - Title: Move mouse
414 |   - Description: Move mouse to a given position
415 |   - Parameters:
416 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
417 |     - `x` (number): X coordinate
418 |     - `y` (number): Y coordinate
419 |   - Read-only: **true**
420 | 
421 | <!-- NOTE: This has been generated via update-readme.js -->
422 | 
423 | - **browser_screen_click**
424 |   - Title: Click
425 |   - Description: Click left mouse button
426 |   - Parameters:
427 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
428 |     - `x` (number): X coordinate
429 |     - `y` (number): Y coordinate
430 |   - Read-only: **false**
431 | 
432 | <!-- NOTE: This has been generated via update-readme.js -->
433 | 
434 | - **browser_screen_drag**
435 |   - Title: Drag mouse
436 |   - Description: Drag left mouse button
437 |   - Parameters:
438 |     - `element` (string): Human-readable element description used to obtain permission to interact with the element
439 |     - `startX` (number): Start X coordinate
440 |     - `startY` (number): Start Y coordinate
441 |     - `endX` (number): End X coordinate
442 |     - `endY` (number): End Y coordinate
443 |   - Read-only: **false**
444 | 
445 | <!-- NOTE: This has been generated via update-readme.js -->
446 | 
447 | - **browser_screen_type**
448 |   - Title: Type text
449 |   - Description: Type text
450 |   - Parameters:
451 |     - `text` (string): Text to type into the element
452 |     - `submit` (boolean, optional): Whether to submit entered text (press Enter after)
453 |   - Read-only: **false**
454 | 
455 | <!-- NOTE: This has been generated via update-readme.js -->
456 | 
457 | - **browser_press_key**
458 |   - Title: Press a key
459 |   - Description: Press a key on the keyboard
460 |   - Parameters:
461 |     - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
462 |   - Read-only: **false**
463 | 
464 | <!-- NOTE: This has been generated via update-readme.js -->
465 | 
466 | - **browser_wait_for**
467 |   - Title: Wait for
468 |   - Description: Wait for text to appear or disappear or a specified time to pass
469 |   - Parameters:
470 |     - `time` (number, optional): The time to wait in seconds
471 |     - `text` (string, optional): The text to wait for
472 |     - `textGone` (string, optional): The text to wait for to disappear
473 |   - Read-only: **true**
474 | 
475 | <!-- NOTE: This has been generated via update-readme.js -->
476 | 
477 | - **browser_file_upload**
478 |   - Title: Upload files
479 |   - Description: Upload one or multiple files
480 |   - Parameters:
481 |     - `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
482 |   - Read-only: **false**
483 | 
484 | <!-- NOTE: This has been generated via update-readme.js -->
485 | 
486 | - **browser_handle_dialog**
487 |   - Title: Handle a dialog
488 |   - Description: Handle a dialog
489 |   - Parameters:
490 |     - `accept` (boolean): Whether to accept the dialog.
491 |     - `promptText` (string, optional): The text of the prompt in case of a prompt dialog.
492 |   - Read-only: **false**
493 | 
494 | </details>
495 | 
496 | 
497 | <!--- End of tools generated section -->
498 | 
```

--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
 1 | <!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
 2 | 
 3 | ## Security
 4 | 
 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
 6 | 
 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
 8 | 
 9 | ## Reporting Security Issues
10 | 
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 | 
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
14 | 
15 | If you prefer to submit without logging in, send email to [[email protected]](mailto:[email protected]).  If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
16 | 
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 
18 | 
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 | 
21 |   * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 |   * Full paths of source file(s) related to the manifestation of the issue
23 |   * The location of the affected source code (tag/branch/commit or direct URL)
24 |   * Any special configuration required to reproduce the issue
25 |   * Step-by-step instructions to reproduce the issue
26 |   * Proof-of-concept or exploit code (if possible)
27 |   * Impact of the issue, including how an attacker might exploit the issue
28 | 
29 | This information will help us triage your report more quickly.
30 | 
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
32 | 
33 | ## Preferred Languages
34 | 
35 | We prefer all communications to be in English.
36 | 
37 | ## Policy
38 | 
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
40 | 
41 | <!-- END MICROSOFT SECURITY.MD BLOCK -->
42 | 
```

--------------------------------------------------------------------------------
/tsconfig.all.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "extends": "./tsconfig.json",
3 |   "include": ["**/*.ts", "**/*.js"],
4 | }
5 | 
```

--------------------------------------------------------------------------------
/cloudflare/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "module": "ESNext",
 4 |     "moduleResolution": "Bundler",
 5 |     "noEmit": true,
 6 |     "types": [
 7 |       "@cloudflare/workers-types",
 8 |     ]
 9 |   }
10 | }
11 | 
```

--------------------------------------------------------------------------------
/cloudflare/example/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "module": "ESNext",
 4 |     "moduleResolution": "Bundler",
 5 |     "noEmit": true,
 6 |     "skipLibCheck": true
 7 |   },
 8 |   "include": [
 9 |     "worker-configuration.d.ts",
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ESNext",
 4 |     "esModuleInterop": true,
 5 |     "moduleResolution": "nodenext",
 6 |     "strict": true,
 7 |     "module": "NodeNext",
 8 |     "rootDir": "src",
 9 |     "outDir": "./lib",
10 |     "resolveJsonModule": true
11 |   },
12 |   "include": [
13 |     "src",
14 |   ],
15 | }
16 | 
```

--------------------------------------------------------------------------------
/cloudflare/example/wrangler.toml:
--------------------------------------------------------------------------------

```toml
 1 | name = "playwright-mcp-example"
 2 | main = "src/index.ts"
 3 | compatibility_date = "2025-03-10"
 4 | compatibility_flags = ["nodejs_compat"]
 5 | 
 6 | [browser]
 7 | binding = "BROWSER"
 8 | 
 9 | [[migrations]]
10 | tag = "v1"
11 | new_sqlite_classes = ["PlaywrightMCP"]
12 | 
13 | [[durable_objects.bindings]]
14 | name = "MCP_OBJECT"
15 | class_name = "PlaywrightMCP"
16 | 
```

--------------------------------------------------------------------------------
/utils/generate-links.js:
--------------------------------------------------------------------------------

```javascript
1 | const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["@playwright/mcp@latest"] });
2 | const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
3 | // Github markdown does not allow linking to `vscode:` directly, so you can use our redirect:
4 | const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
5 | 
6 | console.log(urlForGithub);
```

--------------------------------------------------------------------------------
/.github/workflows/cf_ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: "Playwright MCP for Cloudflare - CI"
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   build:
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |       - uses: actions/checkout@v4
14 |       - uses: actions/setup-node@v4
15 |         with:
16 |           node-version: '20.x'
17 |           registry-url: 'https://registry.npmjs.org'
18 | 
19 |       - run: |
20 |           npm ci
21 |           cd cloudflare
22 |           npm run build
23 | 
```

--------------------------------------------------------------------------------
/examples/generate-test.md:
--------------------------------------------------------------------------------

```markdown
 1 | Use Playwright tools to generate test for scenario:
 2 | 
 3 | ## GitHub PR Checks Navigation Checklist
 4 | 
 5 | 1. Open the [Microsoft Playwright GitHub repository](https://github.com/microsoft/playwright).
 6 | 2. Click on the **Pull requests** tab.
 7 | 3. Find and open the pull request titled **"chore: make noWaitAfter a default"**.
 8 | 4. Switch to the **Checks** tab for that pull request.
 9 | 5. Expand the **infra** check suite to view its jobs.
10 | 6. Click on the **docs & lint** job to view its details.
11 | 
```

--------------------------------------------------------------------------------
/utils/copyright.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
```

--------------------------------------------------------------------------------
/cloudflare/example/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { env } from 'cloudflare:workers';
 2 | 
 3 | import { createMcpAgent } from '@cloudflare/playwright-mcp';
 4 | 
 5 | export const PlaywrightMCP = createMcpAgent(env.BROWSER);
 6 | 
 7 | export default {
 8 |   fetch(request: Request, env: Env, ctx: ExecutionContext) {
 9 |     const { pathname }  = new URL(request.url);
10 | 
11 |     switch (pathname) {
12 |       case '/sse':
13 |       case '/sse/message':
14 |         return PlaywrightMCP.serveSSE('/sse').fetch(request, env, ctx);
15 |       case '/mcp':
16 |         return PlaywrightMCP.serve('/mcp').fetch(request, env, ctx);
17 |       default:
18 |         return new Response('Not Found', { status: 404 });
19 |     }
20 |   },
21 | };
22 | 
```

--------------------------------------------------------------------------------
/cloudflare/example/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@cloudflare/playwright-mcp-template",
 3 |   "version": "0.0.1-next",
 4 |   "description": "Cloudflare Playwright Tools for MCP - Template",
 5 |   "repository": {
 6 |     "type": "git",
 7 |     "url": "git+https://github.com/cloudflare/playwright-mcp.git"
 8 |   },
 9 |   "homepage": "https://github.com/cloudflare/playwright-mcp",
10 |   "engines": {
11 |     "node": ">=18"
12 |   },
13 |   "license": "Apache-2.0",
14 |   "scripts": {
15 |     "build": "tsc",
16 |     "deploy": "wrangler deploy"
17 |   },
18 |   "dependencies": {
19 |     "@cloudflare/playwright-mcp": "^0.0.5"
20 |   },
21 |   "devDependencies": {
22 |     "@types/node": "^22.14.1",
23 |     "typescript": "^5.8.2",
24 |     "wrangler": "^4.26.0"
25 |   }
26 | }
27 | 
```

--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | /**
 3 |  * Copyright (c) Microsoft Corporation.
 4 |  *
 5 |  * Licensed under the Apache License, Version 2.0 (the "License");
 6 |  * you may not use this file except in compliance with the License.
 7 |  * You may obtain a copy of the License at
 8 |  *
 9 |  * http://www.apache.org/licenses/LICENSE-2.0
10 |  *
11 |  * Unless required by applicable law or agreed to in writing, software
12 |  * distributed under the License is distributed on an "AS IS" BASIS,
13 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 |  * See the License for the specific language governing permissions and
15 |  * limitations under the License.
16 |  */
17 | 
18 | import './lib/program.js';
19 | 
```

--------------------------------------------------------------------------------
/cloudflare/src/package.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import json from '../package.json';
18 | 
19 | export const packageJSON = json;
20 | 
```

--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | /**
 3 |  * Copyright (c) Microsoft Corporation.
 4 |  *
 5 |  * Licensed under the Apache License, Version 2.0 (the "License");
 6 |  * you may not use this file except in compliance with the License.
 7 |  * You may obtain a copy of the License at
 8 |  *
 9 |  * http://www.apache.org/licenses/LICENSE-2.0
10 |  *
11 |  * Unless required by applicable law or agreed to in writing, software
12 |  * distributed under the License is distributed on an "AS IS" BASIS,
13 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 |  * See the License for the specific language governing permissions and
15 |  * limitations under the License.
16 |  */
17 | 
18 | import { createConnection } from './lib/index.js';
19 | export { createConnection };
20 | 
```

--------------------------------------------------------------------------------
/src/package.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import fs from 'node:fs';
18 | import url from 'node:url';
19 | import path from 'node:path';
20 | 
21 | const __filename = url.fileURLToPath(import.meta.url);
22 | export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
23 | 
```

--------------------------------------------------------------------------------
/tests/install.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('browser_install', async ({ client, mcpBrowser }) => {
20 |   test.skip(mcpBrowser !== 'chromium', 'Test only chromium');
21 |   expect(await client.callTool({
22 |     name: 'browser_install',
23 |   })).toContainTextContent(`No open pages available.`);
24 | });
25 | 
```

--------------------------------------------------------------------------------
/cloudflare/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@cloudflare/playwright-mcp",
 3 |   "version": "0.0.1-next",
 4 |   "description": "Cloudflare Playwright Tools for MCP",
 5 |   "type": "module",
 6 |   "repository": {
 7 |     "type": "git",
 8 |     "url": "git+https://github.com/cloudflare/playwright-mcp.git"
 9 |   },
10 |   "homepage": "https://playwright.dev",
11 |   "engines": {
12 |     "node": ">=18"
13 |   },
14 |   "license": "Apache-2.0",
15 |   "scripts": {
16 |     "build": "npm ci && npx vite build"
17 |   },
18 |   "exports": {
19 |     "./package.json": "./package.json",
20 |     ".": {
21 |       "types": "./index.d.ts",
22 |       "import": "./lib/esm/index.js",
23 |       "require": "./lib/cjs/index.js",
24 |       "default": "./lib/esm/index.js"
25 |     }
26 |   },
27 |   "dependencies": {
28 |     "@cloudflare/playwright": "^0.0.11",
29 |     "@modelcontextprotocol/sdk": "^1.17.0",
30 |     "agents": "^0.0.109",
31 |     "yaml": "^2.8.0",
32 |     "zod-to-json-schema": "^3.24.6"
33 |   },
34 |   "devDependencies": {
35 |     "@cloudflare/workers-types": "^4.20250725.0",
36 |     "vite": "^7.0.6"
37 |   }
38 | }
39 | 
```

--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | /**
 3 |  * Copyright (c) Microsoft Corporation.
 4 |  *
 5 |  * Licensed under the Apache License, Version 2.0 (the "License");
 6 |  * you may not use this file except in compliance with the License.
 7 |  * You may obtain a copy of the License at
 8 |  *
 9 |  * http://www.apache.org/licenses/LICENSE-2.0
10 |  *
11 |  * Unless required by applicable law or agreed to in writing, software
12 |  * distributed under the License is distributed on an "AS IS" BASIS,
13 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 |  * See the License for the specific language governing permissions and
15 |  * limitations under the License.
16 |  */
17 | 
18 | import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
19 | import type { Config } from './config.js';
20 | import type { BrowserContext } from 'playwright';
21 | 
22 | export type Connection = {
23 |   server: Server;
24 |   close(): Promise<void>;
25 | };
26 | 
27 | export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Connection>;
28 | export {};
29 | 
```

--------------------------------------------------------------------------------
/cloudflare/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { McpAgent } from 'agents/mcp';
 2 | import { env } from 'cloudflare:workers';
 3 | 
 4 | import type { BrowserEndpoint } from '@cloudflare/playwright';
 5 | 
 6 | import { endpointURLString } from '@cloudflare/playwright';
 7 | import { createConnection } from '../../src/index.js';
 8 | import { ToolCapability } from '../../config.js';
 9 | 
10 | import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
11 | 
12 | type Options = {
13 |   vision?: boolean;
14 |   capabilities?: ToolCapability[];
15 | };
16 | 
17 | export function createMcpAgent(endpoint: BrowserEndpoint, options?: Options): typeof McpAgent<typeof env, {}, {}> {
18 |   const cdpEndpoint = typeof endpoint === 'string'
19 |     ? endpoint
20 |     : endpoint instanceof URL
21 |       ? endpoint.toString()
22 |       : endpointURLString(endpoint);
23 | 
24 |   const connection = createConnection({
25 |     capabilities: ['core', 'tabs', 'pdf', 'history', 'wait', 'files', 'testing'],
26 |     browser: {
27 |       cdpEndpoint,
28 |     },
29 |     ...options,
30 |   });
31 | 
32 |   return class PlaywrightMcpAgent extends McpAgent<typeof env, {}, {}> {
33 |     server = connection.then(server => server.server as unknown as Server);
34 | 
35 |     async init() {
36 |       // do nothing
37 |     }
38 |   };
39 | }
40 | 
```

--------------------------------------------------------------------------------
/tests/trace.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import fs from 'fs';
18 | import path from 'path';
19 | 
20 | import { test, expect } from './fixtures.js';
21 | 
22 | test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => {
23 |   const outputDir = testInfo.outputPath('output');
24 | 
25 |   const { client } = await startClient({
26 |     args: ['--save-trace', `--output-dir=${outputDir}`],
27 |   });
28 | 
29 |   expect(await client.callTool({
30 |     name: 'browser_navigate',
31 |     arguments: { url: server.HELLO_WORLD },
32 |   })).toContainTextContent(`Navigate to http://localhost`);
33 | 
34 |   expect(fs.existsSync(path.join(outputDir, 'traces', 'trace.trace'))).toBeTruthy();
35 | });
36 | 
```

--------------------------------------------------------------------------------
/tests/library.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | import { test, expect } from './fixtures.js';
17 | import fs from 'node:fs/promises';
18 | import child_process from 'node:child_process';
19 | 
20 | test('library can be used from CommonJS', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/456' } }, async ({}, testInfo) => {
21 |   const file = testInfo.outputPath('main.cjs');
22 |   await fs.writeFile(file, `
23 |     import('@playwright/mcp')
24 |       .then(playwrightMCP => playwrightMCP.createConnection())
25 |       .then(() => console.log('OK'));
26 |  `);
27 |   expect(child_process.execSync(`node ${file}`, { encoding: 'utf-8' })).toContain('OK');
28 | });
29 | 
```

--------------------------------------------------------------------------------
/tests/console.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('browser_console_messages', async ({ client, server }) => {
20 |   server.setContent('/', `
21 |     <!DOCTYPE html>
22 |     <html>
23 |       <script>
24 |         console.log("Hello, world!");
25 |         console.error("Error");
26 |       </script>
27 |     </html>
28 |   `, 'text/html');
29 | 
30 |   await client.callTool({
31 |     name: 'browser_navigate',
32 |     arguments: {
33 |       url: server.PREFIX,
34 |     },
35 |   });
36 | 
37 |   const resource = await client.callTool({
38 |     name: 'browser_console_messages',
39 |   });
40 |   expect(resource).toHaveTextContent([
41 |     '[LOG] Hello, world!',
42 |     '[ERROR] Error',
43 |   ].join('\n'));
44 | });
45 | 
```

--------------------------------------------------------------------------------
/tests/webdriver.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('do not falsely advertise user agent as a test driver', async ({ client, server, mcpBrowser }) => {
20 |   test.skip(mcpBrowser === 'firefox');
21 |   test.skip(mcpBrowser === 'webkit');
22 |   server.route('/', (req, res) => {
23 |     res.writeHead(200, { 'Content-Type': 'text/html' });
24 |     res.end(`
25 |       <body></body>
26 |       <script>
27 |         document.body.textContent = 'webdriver: ' + navigator.webdriver;
28 |       </script>
29 |     `);
30 |   });
31 | 
32 |   expect(await client.callTool({
33 |     name: 'browser_navigate',
34 |     arguments: {
35 |       url: server.PREFIX,
36 |     },
37 |   })).toContainTextContent('webdriver: false');
38 | });
39 | 
```

--------------------------------------------------------------------------------
/.github/workflows/cf_publish_release_npm.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: "Playwright MCP for Cloudflare - Publish NPM"
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [published]
 6 | 
 7 | jobs:
 8 |   cf-release:
 9 |     name: "Publish Playwright MCP for Cloudflare to NPM"
10 |     runs-on: ubuntu-latest
11 |     steps:
12 |       - uses: actions/checkout@v4
13 |       - uses: actions/setup-node@v4
14 |         with:
15 |           node-version: "20.x"
16 |           registry-url: "https://registry.npmjs.org"
17 | 
18 |       - name: Determine version
19 |         id: version
20 |         run: |
21 |           if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
22 |             echo "Triggered by tag: ${GITHUB_REF#refs/tags/}"
23 |             VERSION="${GITHUB_REF#refs/tags/}"
24 |           else
25 |             echo "❌ Unexpected trigger: ${GITHUB_REF}"
26 |             exit 1
27 |           fi
28 |           # Set the output for later steps
29 |           echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
30 | 
31 |       - name: Run npm ci
32 |         run: npm ci
33 | 
34 |       - name: Run playwright-mcp-cloudflare build
35 |         run: |
36 |           cd cloudflare
37 |           npm version ${{ steps.version.outputs.VERSION }} --no-git-tag-version
38 |           npm run build
39 |       
40 |       - name: Publish to npm
41 |         run: |
42 |           cd cloudflare
43 |           cp ../README.md .  
44 |           npm publish
45 |         env:
46 |           NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
47 | 
```

--------------------------------------------------------------------------------
/tests/device.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('--device should work', async ({ startClient, server, mcpMode }) => {
20 |   const { client } = await startClient({
21 |     args: ['--device', 'iPhone 15'],
22 |   });
23 | 
24 |   server.route('/', (req, res) => {
25 |     res.writeHead(200, { 'Content-Type': 'text/html' });
26 |     res.end(`
27 |       <head>
28 |         <meta name="viewport" content="width=device-width, initial-scale=1">
29 |       </head>
30 |       <body></body>
31 |       <script>
32 |         document.body.textContent = window.innerWidth + "x" + window.innerHeight;
33 |       </script>
34 |     `);
35 |   });
36 | 
37 |   expect(await client.callTool({
38 |     name: 'browser_navigate',
39 |     arguments: {
40 |       url: server.PREFIX,
41 |     },
42 |   })).toContainTextContent(`393x659`);
43 | });
44 | 
```

--------------------------------------------------------------------------------
/tests/network.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('browser_network_requests', async ({ client, server }) => {
20 |   server.setContent('/', `
21 |     <button onclick="fetch('/json')">Click me</button>
22 |   `, 'text/html');
23 | 
24 |   server.setContent('/json', JSON.stringify({ name: 'John Doe' }), 'application/json');
25 | 
26 |   await client.callTool({
27 |     name: 'browser_navigate',
28 |     arguments: {
29 |       url: server.PREFIX,
30 |     },
31 |   });
32 | 
33 |   await client.callTool({
34 |     name: 'browser_click',
35 |     arguments: {
36 |       element: 'Click me button',
37 |       ref: 'e2',
38 |     },
39 |   });
40 | 
41 |   await expect.poll(() => client.callTool({
42 |     name: 'browser_network_requests',
43 |   })).toHaveTextContent(`[GET] ${`${server.PREFIX}`} => [200] OK
44 | [GET] ${`${server.PREFIX}json`} => [200] OK`);
45 | });
46 | 
```

--------------------------------------------------------------------------------
/src/tools/console.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool } from './tool.js';
19 | 
20 | const console = defineTool({
21 |   capability: 'core',
22 |   schema: {
23 |     name: 'browser_console_messages',
24 |     title: 'Get console messages',
25 |     description: 'Returns all console messages',
26 |     inputSchema: z.object({}),
27 |     type: 'readOnly',
28 |   },
29 |   handle: async context => {
30 |     const messages = context.currentTabOrDie().consoleMessages();
31 |     const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
32 |     return {
33 |       code: [`// <internal code to get console messages>`],
34 |       action: async () => {
35 |         return {
36 |           content: [{ type: 'text', text: log }]
37 |         };
38 |       },
39 |       captureSnapshot: false,
40 |       waitForNetwork: false,
41 |     };
42 |   },
43 | });
44 | 
45 | export default [
46 |   console,
47 | ];
48 | 
```

--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { defineConfig } from '@playwright/test';
18 | 
19 | import type { TestOptions } from './tests/fixtures.js';
20 | 
21 | export default defineConfig<TestOptions>({
22 |   testDir: './tests',
23 |   fullyParallel: true,
24 |   forbidOnly: !!process.env.CI,
25 |   retries: process.env.CI ? 2 : 0,
26 |   workers: process.env.CI ? 1 : undefined,
27 |   reporter: 'list',
28 |   projects: [
29 |     { name: 'chrome' },
30 |     { name: 'msedge', use: { mcpBrowser: 'msedge' } },
31 |     { name: 'chromium', use: { mcpBrowser: 'chromium' } },
32 |     ...process.env.MCP_IN_DOCKER ? [{
33 |       name: 'chromium-docker',
34 |       grep: /browser_navigate|browser_click/,
35 |       use: {
36 |         mcpBrowser: 'chromium',
37 |         mcpMode: 'docker' as const
38 |       }
39 |     }] : [],
40 |     { name: 'firefox', use: { mcpBrowser: 'firefox' } },
41 |     { name: 'webkit', use: { mcpBrowser: 'webkit' } },
42 |   ],
43 | });
44 | 
```

--------------------------------------------------------------------------------
/src/fileUtils.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import os from 'node:os';
18 | import path from 'node:path';
19 | 
20 | import type { FullConfig } from './config.js';
21 | 
22 | export function cacheDir() {
23 |   let cacheDirectory: string;
24 |   if (process.platform === 'linux')
25 |     cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
26 |   else if (process.platform === 'darwin')
27 |     cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
28 |   else if (process.platform === 'win32')
29 |     cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
30 |   else
31 |     throw new Error('Unsupported platform: ' + process.platform);
32 |   return path.join(cacheDirectory, 'ms-playwright');
33 | }
34 | 
35 | export async function userDataDir(browserConfig: FullConfig['browser']) {
36 |   return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
37 | }
38 | 
```

--------------------------------------------------------------------------------
/cloudflare/index.d.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | /**
 3 |  * Copyright (c) Microsoft Corporation.
 4 |  *
 5 |  * Licensed under the Apache License, Version 2.0 (the "License");
 6 |  * you may not use this file except in compliance with the License.
 7 |  * You may obtain a copy of the License at
 8 |  *
 9 |  * http://www.apache.org/licenses/LICENSE-2.0
10 |  *
11 |  * Unless required by applicable law or agreed to in writing, software
12 |  * distributed under the License is distributed on an "AS IS" BASIS,
13 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 |  * See the License for the specific language governing permissions and
15 |  * limitations under the License.
16 |  */
17 | 
18 | import { env } from 'cloudflare:workers';
19 | import { McpAgent } from 'agents/mcp';
20 | import { BrowserEndpoint } from '@cloudflare/playwright';
21 | 
22 | type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files';
23 | 
24 | type Options = {
25 |     /**
26 |      * Enable vision capabilities (e.g., visual automation or OCR).
27 |      */
28 |     vision?: boolean;
29 |     /**
30 |      * List of enabled tool capabilities. Possible values:
31 |      *   - 'core': Core browser automation features.
32 |      *   - 'tabs': Tab management features.
33 |      *   - 'pdf': PDF generation and manipulation.
34 |      *   - 'history': Browser history access.
35 |      *   - 'wait': Wait and timing utilities.
36 |      *   - 'files': File upload/download support.
37 |      */
38 |     capabilities?: ToolCapability[];
39 | };
40 | export declare function createMcpAgent(endpoint: BrowserEndpoint, options?: Options): typeof McpAgent<typeof env, {}, {}>;
41 | export {};
42 | 
```

--------------------------------------------------------------------------------
/src/tools/keyboard.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool, type ToolFactory } from './tool.js';
19 | 
20 | const pressKey: ToolFactory = captureSnapshot => defineTool({
21 |   capability: 'core',
22 | 
23 |   schema: {
24 |     name: 'browser_press_key',
25 |     title: 'Press a key',
26 |     description: 'Press a key on the keyboard',
27 |     inputSchema: z.object({
28 |       key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
29 |     }),
30 |     type: 'destructive',
31 |   },
32 | 
33 |   handle: async (context, params) => {
34 |     const tab = context.currentTabOrDie();
35 | 
36 |     const code = [
37 |       `// Press ${params.key}`,
38 |       `await page.keyboard.press('${params.key}');`,
39 |     ];
40 | 
41 |     const action = () => tab.page.keyboard.press(params.key);
42 | 
43 |     return {
44 |       code,
45 |       action,
46 |       captureSnapshot,
47 |       waitForNetwork: true
48 |     };
49 |   },
50 | });
51 | 
52 | export default (captureSnapshot: boolean) => [
53 |   pressKey(captureSnapshot),
54 | ];
55 | 
```

--------------------------------------------------------------------------------
/tests/iframes.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('stitched aria frames', async ({ client }) => {
20 |   expect(await client.callTool({
21 |     name: 'browser_navigate',
22 |     arguments: {
23 |       url: `data:text/html,<h1>Hello</h1><iframe src="data:text/html,<button>World</button><main><iframe src='data:text/html,<p>Nested</p>'></iframe></main>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>`,
24 |     },
25 |   })).toContainTextContent(`
26 | \`\`\`yaml
27 | - generic [active] [ref=e1]:
28 |   - heading "Hello" [level=1] [ref=e2]
29 |   - iframe [ref=e3]:
30 |     - generic [active] [ref=f1e1]:
31 |       - button "World" [ref=f1e2]
32 |       - main [ref=f1e3]:
33 |         - iframe [ref=f1e4]:
34 |           - paragraph [ref=f2e2]: Nested
35 | \`\`\``);
36 | 
37 |   expect(await client.callTool({
38 |     name: 'browser_click',
39 |     arguments: {
40 |       element: 'World',
41 |       ref: 'f1e2',
42 |     },
43 |   })).toContainTextContent(`// Click World`);
44 | });
45 | 
```

--------------------------------------------------------------------------------
/src/pageSnapshot.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import * as playwright from 'playwright';
18 | import { callOnPageNoTrace } from './tools/utils.js';
19 | 
20 | type PageEx = playwright.Page & {
21 |   _snapshotForAI: () => Promise<string>;
22 | };
23 | 
24 | export class PageSnapshot {
25 |   private _page: playwright.Page;
26 |   private _text!: string;
27 | 
28 |   constructor(page: playwright.Page) {
29 |     this._page = page;
30 |   }
31 | 
32 |   static async create(page: playwright.Page): Promise<PageSnapshot> {
33 |     const snapshot = new PageSnapshot(page);
34 |     await snapshot._build();
35 |     return snapshot;
36 |   }
37 | 
38 |   text(): string {
39 |     return this._text;
40 |   }
41 | 
42 |   private async _build() {
43 |     const snapshot = await callOnPageNoTrace(this._page, page => (page as PageEx)._snapshotForAI());
44 |     this._text = [
45 |       `- Page Snapshot`,
46 |       '```yaml',
47 |       snapshot,
48 |       '```',
49 |     ].join('\n');
50 |   }
51 | 
52 |   refLocator(params: { element: string, ref: string }): playwright.Locator {
53 |     return this._page.locator(`aria-ref=${params.ref}`).describe(params.element);
54 |   }
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/tools/pdf.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool } from './tool.js';
19 | 
20 | import * as javascript from '../javascript.js';
21 | import { outputFile } from '../config.js';
22 | 
23 | const pdfSchema = z.object({
24 |   filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
25 | });
26 | 
27 | const pdf = defineTool({
28 |   capability: 'pdf',
29 | 
30 |   schema: {
31 |     name: 'browser_pdf_save',
32 |     title: 'Save as PDF',
33 |     description: 'Save page as PDF',
34 |     inputSchema: pdfSchema,
35 |     type: 'readOnly',
36 |   },
37 | 
38 |   handle: async (context, params) => {
39 |     const tab = context.currentTabOrDie();
40 |     const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
41 | 
42 |     const code = [
43 |       `// Save page as ${fileName}`,
44 |       `await page.pdf(${javascript.formatObject({ path: fileName })});`,
45 |     ];
46 | 
47 |     return {
48 |       code,
49 |       action: async () => tab.page.pdf({ path: fileName }).then(() => {}),
50 |       captureSnapshot: false,
51 |       waitForNetwork: false,
52 |     };
53 |   },
54 | });
55 | 
56 | export default [
57 |   pdf,
58 | ];
59 | 
```

--------------------------------------------------------------------------------
/src/tools/files.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool, type ToolFactory } from './tool.js';
19 | 
20 | const uploadFile: ToolFactory = captureSnapshot => defineTool({
21 |   capability: 'files',
22 | 
23 |   schema: {
24 |     name: 'browser_file_upload',
25 |     title: 'Upload files',
26 |     description: 'Upload one or multiple files',
27 |     inputSchema: z.object({
28 |       paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
29 |     }),
30 |     type: 'destructive',
31 |   },
32 | 
33 |   handle: async (context, params) => {
34 |     const modalState = context.modalStates().find(state => state.type === 'fileChooser');
35 |     if (!modalState)
36 |       throw new Error('No file chooser visible');
37 | 
38 |     const code = [
39 |       `// <internal code to chose files ${params.paths.join(', ')}`,
40 |     ];
41 | 
42 |     const action = async () => {
43 |       await modalState.fileChooser.setFiles(params.paths);
44 |       context.clearModalState(modalState);
45 |     };
46 | 
47 |     return {
48 |       code,
49 |       action,
50 |       captureSnapshot,
51 |       waitForNetwork: true,
52 |     };
53 |   },
54 |   clearsModalState: 'fileChooser',
55 | });
56 | 
57 | export default (captureSnapshot: boolean) => [
58 |   uploadFile(captureSnapshot),
59 | ];
60 | 
```

--------------------------------------------------------------------------------
/src/tools/network.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool } from './tool.js';
19 | 
20 | import type * as playwright from 'playwright';
21 | 
22 | const requests = defineTool({
23 |   capability: 'core',
24 | 
25 |   schema: {
26 |     name: 'browser_network_requests',
27 |     title: 'List network requests',
28 |     description: 'Returns all network requests since loading the page',
29 |     inputSchema: z.object({}),
30 |     type: 'readOnly',
31 |   },
32 | 
33 |   handle: async context => {
34 |     const requests = context.currentTabOrDie().requests();
35 |     const log = [...requests.entries()].map(([request, response]) => renderRequest(request, response)).join('\n');
36 |     return {
37 |       code: [`// <internal code to list network requests>`],
38 |       action: async () => {
39 |         return {
40 |           content: [{ type: 'text', text: log }]
41 |         };
42 |       },
43 |       captureSnapshot: false,
44 |       waitForNetwork: false,
45 |     };
46 |   },
47 | });
48 | 
49 | function renderRequest(request: playwright.Request, response: playwright.Response | null) {
50 |   const result: string[] = [];
51 |   result.push(`[${request.method().toUpperCase()}] ${request.url()}`);
52 |   if (response)
53 |     result.push(`=> [${response.status()}] ${response.statusText()}`);
54 |   return result.join(' ');
55 | }
56 | 
57 | export default [
58 |   requests,
59 | ];
60 | 
```

--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import common from './tools/common.js';
18 | import console from './tools/console.js';
19 | import dialogs from './tools/dialogs.js';
20 | import files from './tools/files.js';
21 | import install from './tools/install.js';
22 | import keyboard from './tools/keyboard.js';
23 | import navigate from './tools/navigate.js';
24 | import network from './tools/network.js';
25 | import pdf from './tools/pdf.js';
26 | import snapshot from './tools/snapshot.js';
27 | import tabs from './tools/tabs.js';
28 | import screenshot from './tools/screenshot.js';
29 | import testing from './tools/testing.js';
30 | import vision from './tools/vision.js';
31 | import wait from './tools/wait.js';
32 | 
33 | import type { Tool } from './tools/tool.js';
34 | 
35 | export const snapshotTools: Tool<any>[] = [
36 |   ...common(true),
37 |   ...console,
38 |   ...dialogs(true),
39 |   ...files(true),
40 |   ...install,
41 |   ...keyboard(true),
42 |   ...navigate(true),
43 |   ...network,
44 |   ...pdf,
45 |   ...screenshot,
46 |   ...snapshot,
47 |   ...tabs(true),
48 |   ...testing,
49 |   ...wait(true),
50 | ];
51 | 
52 | export const visionTools: Tool<any>[] = [
53 |   ...common(false),
54 |   ...console,
55 |   ...dialogs(false),
56 |   ...files(false),
57 |   ...install,
58 |   ...keyboard(false),
59 |   ...navigate(false),
60 |   ...network,
61 |   ...pdf,
62 |   ...tabs(false),
63 |   ...testing,
64 |   ...vision,
65 |   ...wait(false),
66 | ];
67 | 
```

--------------------------------------------------------------------------------
/src/tools/dialogs.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool, type ToolFactory } from './tool.js';
19 | 
20 | const handleDialog: ToolFactory = captureSnapshot => defineTool({
21 |   capability: 'core',
22 | 
23 |   schema: {
24 |     name: 'browser_handle_dialog',
25 |     title: 'Handle a dialog',
26 |     description: 'Handle a dialog',
27 |     inputSchema: z.object({
28 |       accept: z.coerce.boolean().describe('Whether to accept the dialog.'),
29 |       promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
30 |     }),
31 |     type: 'destructive',
32 |   },
33 | 
34 |   handle: async (context, params) => {
35 |     const dialogState = context.modalStates().find(state => state.type === 'dialog');
36 |     if (!dialogState)
37 |       throw new Error('No dialog visible');
38 | 
39 |     if (params.accept)
40 |       await dialogState.dialog.accept(params.promptText);
41 |     else
42 |       await dialogState.dialog.dismiss();
43 | 
44 |     context.clearModalState(dialogState);
45 | 
46 |     const code = [
47 |       `// <internal code to handle "${dialogState.dialog.type()}" dialog>`,
48 |     ];
49 | 
50 |     return {
51 |       code,
52 |       captureSnapshot,
53 |       waitForNetwork: false,
54 |     };
55 |   },
56 | 
57 |   clearsModalState: 'dialog',
58 | });
59 | 
60 | export default (captureSnapshot: boolean) => [
61 |   handleDialog(captureSnapshot),
62 | ];
63 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { createConnection as createConnectionImpl } from './connection.js';
18 | import type { Connection } from '../index.js';
19 | import { resolveConfig } from './config.js';
20 | import { contextFactory } from './browserContextFactory.js';
21 | 
22 | import type { Config } from '../config.js';
23 | import type { BrowserContext } from 'playwright';
24 | import type { BrowserContextFactory } from './browserContextFactory.js';
25 | 
26 | export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Connection> {
27 |   const config = await resolveConfig(userConfig);
28 |   const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config.browser);
29 |   return createConnectionImpl(config, factory);
30 | }
31 | 
32 | class SimpleBrowserContextFactory implements BrowserContextFactory {
33 |   private readonly _contextGetter: () => Promise<BrowserContext>;
34 | 
35 |   constructor(contextGetter: () => Promise<BrowserContext>) {
36 |     this._contextGetter = contextGetter;
37 |   }
38 | 
39 |   async createContext(): Promise<{ browserContext: BrowserContext, close: () => Promise<void> }> {
40 |     const browserContext = await this._contextGetter();
41 |     return {
42 |       browserContext,
43 |       close: () => browserContext.close()
44 |     };
45 |   }
46 | }
47 | 
```

--------------------------------------------------------------------------------
/tests/headed.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | for (const mcpHeadless of [false, true]) {
20 |   test.describe(`mcpHeadless: ${mcpHeadless}`, () => {
21 |     test.use({ mcpHeadless });
22 |     test.skip(process.platform === 'linux', 'Auto-detection wont let this test run on linux');
23 |     test.skip(({ mcpMode, mcpHeadless }) => mcpMode === 'docker' && !mcpHeadless, 'Headed mode is not supported in docker');
24 |     test('browser', async ({ client, server, mcpBrowser }) => {
25 |       test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Only chrome is supported for this test');
26 |       server.route('/', (req, res) => {
27 |         res.writeHead(200, { 'Content-Type': 'text/html' });
28 |         res.end(`
29 |           <body></body>
30 |           <script>
31 |             document.body.textContent = navigator.userAgent;
32 |           </script>
33 |         `);
34 |       });
35 | 
36 |       const response = await client.callTool({
37 |         name: 'browser_navigate',
38 |         arguments: {
39 |           url: server.PREFIX,
40 |         },
41 |       });
42 | 
43 |       expect(response).toContainTextContent(`Mozilla/5.0`);
44 |       if (mcpHeadless)
45 |         expect(response).toContainTextContent(`HeadlessChrome`);
46 |       else
47 |         expect(response).not.toContainTextContent(`HeadlessChrome`);
48 |     });
49 |   });
50 | }
51 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@playwright/mcp",
 3 |   "version": "0.0.30",
 4 |   "description": "Playwright Tools for MCP",
 5 |   "type": "module",
 6 |   "repository": {
 7 |     "type": "git",
 8 |     "url": "git+https://github.com/microsoft/playwright-mcp.git"
 9 |   },
10 |   "homepage": "https://playwright.dev",
11 |   "engines": {
12 |     "node": ">=18"
13 |   },
14 |   "author": {
15 |     "name": "Microsoft Corporation"
16 |   },
17 |   "license": "Apache-2.0",
18 |   "scripts": {
19 |     "build": "tsc",
20 |     "lint": "npm run update-readme && eslint . && tsc --noEmit",
21 |     "update-readme": "node utils/update-readme.js",
22 |     "watch": "tsc --watch",
23 |     "test": "playwright test",
24 |     "ctest": "playwright test --project=chrome",
25 |     "ftest": "playwright test --project=firefox",
26 |     "wtest": "playwright test --project=webkit",
27 |     "run-server": "node lib/browserServer.js",
28 |     "clean": "rm -rf lib",
29 |     "npm-publish": "npm run clean && npm run build && npm run test && npm publish"
30 |   },
31 |   "exports": {
32 |     "./package.json": "./package.json",
33 |     ".": {
34 |       "types": "./index.d.ts",
35 |       "default": "./index.js"
36 |     }
37 |   },
38 |   "dependencies": {
39 |     "@modelcontextprotocol/sdk": "^1.11.0",
40 |     "commander": "^13.1.0",
41 |     "debug": "^4.4.1",
42 |     "mime": "^4.0.7",
43 |     "playwright": "1.54.1",
44 |     "ws": "^8.18.1",
45 |     "zod-to-json-schema": "^3.24.4"
46 |   },
47 |   "devDependencies": {
48 |     "@eslint/eslintrc": "^3.2.0",
49 |     "@eslint/js": "^9.19.0",
50 |     "@playwright/test": "1.54.1",
51 |     "@stylistic/eslint-plugin": "^3.0.1",
52 |     "@types/chrome": "^0.0.315",
53 |     "@types/debug": "^4.1.12",
54 |     "@types/node": "^22.13.10",
55 |     "@types/ws": "^8.18.1",
56 |     "@typescript-eslint/eslint-plugin": "^8.26.1",
57 |     "@typescript-eslint/parser": "^8.26.1",
58 |     "@typescript-eslint/utils": "^8.26.1",
59 |     "eslint": "^9.19.0",
60 |     "eslint-plugin-import": "^2.31.0",
61 |     "eslint-plugin-notice": "^1.0.0",
62 |     "typescript": "^5.8.2"
63 |   },
64 |   "bin": {
65 |     "mcp-server-playwright": "cli.js"
66 |   }
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { createConnection } from './connection.js';
18 | import { contextFactory } from './browserContextFactory.js';
19 | 
20 | import type { FullConfig } from './config.js';
21 | import type { Connection } from './connection.js';
22 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
23 | import type { BrowserContextFactory } from './browserContextFactory.js';
24 | 
25 | export class Server {
26 |   readonly config: FullConfig;
27 |   private _connectionList: Connection[] = [];
28 |   private _browserConfig: FullConfig['browser'];
29 |   private _contextFactory: BrowserContextFactory;
30 | 
31 |   constructor(config: FullConfig) {
32 |     this.config = config;
33 |     this._browserConfig = config.browser;
34 |     this._contextFactory = contextFactory(this._browserConfig);
35 |   }
36 | 
37 |   async createConnection(transport: Transport): Promise<Connection> {
38 |     const connection = createConnection(this.config, this._contextFactory);
39 |     this._connectionList.push(connection);
40 |     await connection.server.connect(transport);
41 |     return connection;
42 |   }
43 | 
44 |   setupExitWatchdog() {
45 |     let isExiting = false;
46 |     const handleExit = async () => {
47 |       if (isExiting)
48 |         return;
49 |       isExiting = true;
50 |       setTimeout(() => process.exit(0), 15000);
51 |       await Promise.all(this._connectionList.map(connection => connection.close()));
52 |       process.exit(0);
53 |     };
54 | 
55 |     process.stdin.on('close', handleExit);
56 |     process.on('SIGINT', handleExit);
57 |     process.on('SIGTERM', handleExit);
58 |   }
59 | }
60 | 
```

--------------------------------------------------------------------------------
/src/tools/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
18 | import type { z } from 'zod';
19 | import type { Context } from '../context.js';
20 | import type * as playwright from 'playwright';
21 | import type { ToolCapability } from '../../config.js';
22 | 
23 | export type ToolSchema<Input extends InputType> = {
24 |   name: string;
25 |   title: string;
26 |   description: string;
27 |   inputSchema: Input;
28 |   type: 'readOnly' | 'destructive';
29 | };
30 | 
31 | type InputType = z.Schema;
32 | 
33 | export type FileUploadModalState = {
34 |   type: 'fileChooser';
35 |   description: string;
36 |   fileChooser: playwright.FileChooser;
37 | };
38 | 
39 | export type DialogModalState = {
40 |   type: 'dialog';
41 |   description: string;
42 |   dialog: playwright.Dialog;
43 | };
44 | 
45 | export type ModalState = FileUploadModalState | DialogModalState;
46 | 
47 | export type ToolActionResult = { content?: (ImageContent | TextContent)[] } | undefined | void;
48 | 
49 | export type ToolResult = {
50 |   code: string[];
51 |   action?: () => Promise<ToolActionResult>;
52 |   captureSnapshot: boolean;
53 |   waitForNetwork: boolean;
54 |   resultOverride?: ToolActionResult;
55 | };
56 | 
57 | export type Tool<Input extends InputType = InputType> = {
58 |   capability: ToolCapability;
59 |   schema: ToolSchema<Input>;
60 |   clearsModalState?: ModalState['type'];
61 |   handle: (context: Context, params: z.output<Input>) => Promise<ToolResult>;
62 | };
63 | 
64 | export type ToolFactory = (snapshot: boolean) => Tool<any>;
65 | 
66 | export function defineTool<Input extends InputType>(tool: Tool<Input>): Tool<Input> {
67 |   return tool;
68 | }
69 | 
```

--------------------------------------------------------------------------------
/src/tools/install.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { fork } from 'child_process';
18 | import path from 'path';
19 | 
20 | import { z } from 'zod';
21 | import { defineTool } from './tool.js';
22 | 
23 | import { fileURLToPath } from 'node:url';
24 | 
25 | const install = defineTool({
26 |   capability: 'install',
27 |   schema: {
28 |     name: 'browser_install',
29 |     title: 'Install the browser specified in the config',
30 |     description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
31 |     inputSchema: z.object({}),
32 |     type: 'destructive',
33 |   },
34 | 
35 |   handle: async context => {
36 |     const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
37 |     const cliUrl = import.meta.resolve('playwright/package.json');
38 |     const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
39 |     const child = fork(cliPath, ['install', channel], {
40 |       stdio: 'pipe',
41 |     });
42 |     const output: string[] = [];
43 |     child.stdout?.on('data', data => output.push(data.toString()));
44 |     child.stderr?.on('data', data => output.push(data.toString()));
45 |     await new Promise<void>((resolve, reject) => {
46 |       child.on('close', code => {
47 |         if (code === 0)
48 |           resolve();
49 |         else
50 |           reject(new Error(`Failed to install browser: ${output.join('')}`));
51 |       });
52 |     });
53 |     return {
54 |       code: [`// Browser ${channel} installed`],
55 |       captureSnapshot: false,
56 |       waitForNetwork: false,
57 |     };
58 |   },
59 | });
60 | 
61 | export default [
62 |   install,
63 | ];
64 | 
```

--------------------------------------------------------------------------------
/src/tools/common.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool, type ToolFactory } from './tool.js';
19 | 
20 | const close = defineTool({
21 |   capability: 'core',
22 | 
23 |   schema: {
24 |     name: 'browser_close',
25 |     title: 'Close browser',
26 |     description: 'Close the page',
27 |     inputSchema: z.object({}),
28 |     type: 'readOnly',
29 |   },
30 | 
31 |   handle: async context => {
32 |     await context.close();
33 |     return {
34 |       code: [`await page.close()`],
35 |       captureSnapshot: false,
36 |       waitForNetwork: false,
37 |     };
38 |   },
39 | });
40 | 
41 | const resize: ToolFactory = captureSnapshot => defineTool({
42 |   capability: 'core',
43 |   schema: {
44 |     name: 'browser_resize',
45 |     title: 'Resize browser window',
46 |     description: 'Resize the browser window',
47 |     inputSchema: z.object({
48 |       width: z.coerce.number().describe('Width of the browser window'),
49 |       height: z.coerce.number().describe('Height of the browser window'),
50 |     }),
51 |     type: 'readOnly',
52 |   },
53 | 
54 |   handle: async (context, params) => {
55 |     const tab = context.currentTabOrDie();
56 | 
57 |     const code = [
58 |       `// Resize browser window to ${params.width}x${params.height}`,
59 |       `await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
60 |     ];
61 | 
62 |     const action = async () => {
63 |       await tab.page.setViewportSize({ width: params.width, height: params.height });
64 |     };
65 | 
66 |     return {
67 |       code,
68 |       action,
69 |       captureSnapshot,
70 |       waitForNetwork: true
71 |     };
72 |   },
73 | });
74 | 
75 | export default (captureSnapshot: boolean) => [
76 |   close,
77 |   resize(captureSnapshot)
78 | ];
79 | 
```

--------------------------------------------------------------------------------
/src/javascript.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | // adapted from:
18 | // - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
19 | // - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/server/codegen/javascript.ts
20 | 
21 | // NOTE: this function should not be used to escape any selectors.
22 | export function escapeWithQuotes(text: string, char: string = '\'') {
23 |   const stringified = JSON.stringify(text);
24 |   const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
25 |   if (char === '\'')
26 |     return char + escapedText.replace(/[']/g, '\\\'') + char;
27 |   if (char === '"')
28 |     return char + escapedText.replace(/["]/g, '\\"') + char;
29 |   if (char === '`')
30 |     return char + escapedText.replace(/[`]/g, '`') + char;
31 |   throw new Error('Invalid escape char');
32 | }
33 | 
34 | export function quote(text: string) {
35 |   return escapeWithQuotes(text, '\'');
36 | }
37 | 
38 | export function formatObject(value: any, indent = '  '): string {
39 |   if (typeof value === 'string')
40 |     return quote(value);
41 |   if (Array.isArray(value))
42 |     return `[${value.map(o => formatObject(o)).join(', ')}]`;
43 |   if (typeof value === 'object') {
44 |     const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
45 |     if (!keys.length)
46 |       return '{}';
47 |     const tokens: string[] = [];
48 |     for (const key of keys)
49 |       tokens.push(`${key}: ${formatObject(value[key])}`);
50 |     return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
51 |   }
52 |   return String(value);
53 | }
54 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   lint:
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |     - uses: actions/checkout@v4
14 |     - name: Use Node.js 18
15 |       uses: actions/setup-node@v4
16 |       with:
17 |         node-version: '18'
18 |         cache: 'npm'
19 |     - name: Install dependencies
20 |       run: npm ci
21 |     - run: npm run build
22 |     - name: Run ESLint
23 |       run: npm run lint
24 |     - name: Ensure no changes
25 |       run: git diff --exit-code
26 | 
27 |   test:
28 |     strategy:
29 |       fail-fast: false
30 |       matrix:
31 |         os: [ubuntu-latest, macos-latest, windows-latest]
32 |     runs-on: ${{ matrix.os }}
33 |     steps:
34 |     - uses: actions/checkout@v4
35 |     - name: Use Node.js 18
36 |       uses: actions/setup-node@v4
37 |       with:
38 |         # https://github.com/microsoft/playwright-mcp/issues/344
39 |         node-version: '18.19'
40 |         cache: 'npm'
41 |     - name: Install dependencies
42 |       run: npm ci
43 |     - name: Playwright install
44 |       run: npx playwright install --with-deps
45 |     - name: Install MS Edge
46 |       # MS Edge is not preinstalled on macOS runners.
47 |       if: ${{ matrix.os == 'macos-latest' }}
48 |       run: npx playwright install msedge
49 |     - name: Build
50 |       run: npm run build
51 |     - name: Run tests
52 |       run: npm test
53 | 
54 |   test_docker:
55 |     runs-on: ubuntu-latest
56 |     steps:
57 |     - uses: actions/checkout@v4
58 |     - name: Use Node.js 18
59 |       uses: actions/setup-node@v4
60 |       with:
61 |         node-version: '18'
62 |         cache: 'npm'
63 |     - name: Install dependencies
64 |       run: npm ci
65 |     - name: Playwright install
66 |       run: npx playwright install --with-deps chromium
67 |     - name: Build
68 |       run: npm run build
69 |     - name: Set up Docker Buildx
70 |       uses: docker/setup-buildx-action@v3
71 |     - name: Build and push
72 |       uses: docker/build-push-action@v6
73 |       with:
74 |         tags: playwright-mcp-dev:latest
75 |         cache-from: type=gha
76 |         cache-to: type=gha,mode=max
77 |         load: true
78 |     - name: Run tests
79 |       shell: bash
80 |       run: |
81 |         # Used for the Docker tests to share the test-results folder with the container.
82 |         umask 0000
83 |         npm run test -- --project=chromium-docker
84 |       env:
85 |         MCP_IN_DOCKER: 1
86 | 
```

--------------------------------------------------------------------------------
/src/tools/testing.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool } from './tool.js';
19 | 
20 | const generateTestSchema = z.object({
21 |   name: z.string().describe('The name of the test'),
22 |   description: z.string().describe('The description of the test'),
23 |   steps: z.array(z.string()).describe('The steps of the test'),
24 | });
25 | 
26 | const generateTest = defineTool({
27 |   capability: 'testing',
28 | 
29 |   schema: {
30 |     name: 'browser_generate_playwright_test',
31 |     title: 'Generate a Playwright test',
32 |     description: 'Generate a Playwright test for given scenario',
33 |     inputSchema: generateTestSchema,
34 |     type: 'readOnly',
35 |   },
36 | 
37 |   handle: async (context, params) => {
38 |     return {
39 |       resultOverride: {
40 |         content: [{
41 |           type: 'text',
42 |           text: instructions(params),
43 |         }],
44 |       },
45 |       code: [],
46 |       captureSnapshot: false,
47 |       waitForNetwork: false,
48 |     };
49 |   },
50 | });
51 | 
52 | const instructions = (params: { name: string, description: string, steps: string[] }) => [
53 |   `## Instructions`,
54 |   `- You are a playwright test generator.`,
55 |   `- You are given a scenario and you need to generate a playwright test for it.`,
56 |   '- DO NOT generate test code based on the scenario alone. DO run steps one by one using the tools provided instead.',
57 |   '- Only after all steps are completed, emit a Playwright TypeScript test that uses @playwright/test based on message history',
58 |   '- Save generated test file in the tests directory',
59 |   `Test name: ${params.name}`,
60 |   `Description: ${params.description}`,
61 |   `Steps:`,
62 |   ...params.steps.map((step, index) => `- ${index + 1}. ${step}`),
63 | ].join('\n');
64 | 
65 | export default [
66 |   generateTest,
67 | ];
68 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | ARG PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
 2 | 
 3 | # ------------------------------
 4 | # Base
 5 | # ------------------------------
 6 | # Base stage: Contains only the minimal dependencies required for runtime
 7 | # (node_modules and Playwright system dependencies)
 8 | FROM node:22-bookworm-slim AS base
 9 | 
10 | ARG PLAYWRIGHT_BROWSERS_PATH
11 | ENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH}
12 | 
13 | # Set the working directory
14 | WORKDIR /app
15 | 
16 | RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
17 |     --mount=type=bind,source=package.json,target=package.json \
18 |     --mount=type=bind,source=package-lock.json,target=package-lock.json \
19 |   npm ci --omit=dev && \
20 |   # Install system dependencies for playwright
21 |   npx -y playwright-core install-deps chromium
22 | 
23 | # ------------------------------
24 | # Builder
25 | # ------------------------------
26 | FROM base AS builder
27 | 
28 | RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
29 |     --mount=type=bind,source=package.json,target=package.json \
30 |     --mount=type=bind,source=package-lock.json,target=package-lock.json \
31 |   npm ci
32 | 
33 | # Copy the rest of the app
34 | COPY *.json *.js *.ts .
35 | COPY src src/
36 | 
37 | # Build the app
38 | RUN npm run build
39 | 
40 | # ------------------------------
41 | # Browser
42 | # ------------------------------
43 | # Cache optimization:
44 | # - Browser is downloaded only when node_modules or Playwright system dependencies change
45 | # - Cache is reused when only source code changes
46 | FROM base AS browser
47 | 
48 | RUN npx -y playwright-core install --no-shell chromium
49 | 
50 | # ------------------------------
51 | # Runtime
52 | # ------------------------------
53 | FROM base
54 | 
55 | ARG PLAYWRIGHT_BROWSERS_PATH
56 | ARG USERNAME=node
57 | ENV NODE_ENV=production
58 | 
59 | # Set the correct ownership for the runtime user on production `node_modules`
60 | RUN chown -R ${USERNAME}:${USERNAME} node_modules
61 | 
62 | USER ${USERNAME}
63 | 
64 | COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}
65 | COPY --chown=${USERNAME}:${USERNAME} cli.js package.json ./
66 | COPY --from=builder --chown=${USERNAME}:${USERNAME} /app/lib /app/lib
67 | 
68 | # Run in headless and only with chromium (other browsers need more dependencies not included in this image)
69 | ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"]
70 | 
```

--------------------------------------------------------------------------------
/tests/config.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import fs from 'node:fs';
18 | 
19 | import { Config } from '../config.js';
20 | import { test, expect } from './fixtures.js';
21 | 
22 | test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => {
23 |   server.setContent('/', `
24 |     <title>Title</title>
25 |     <body>Hello, world!</body>
26 |   `, 'text/html');
27 | 
28 |   const config: Config = {
29 |     browser: {
30 |       userDataDir: testInfo.outputPath('user-data-dir'),
31 |     },
32 |   };
33 |   const configPath = testInfo.outputPath('config.json');
34 |   await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
35 | 
36 |   const { client } = await startClient({ args: ['--config', configPath] });
37 |   expect(await client.callTool({
38 |     name: 'browser_navigate',
39 |     arguments: { url: server.PREFIX },
40 |   })).toContainTextContent(`Hello, world!`);
41 | 
42 |   const files = await fs.promises.readdir(config.browser!.userDataDir!);
43 |   expect(files.length).toBeGreaterThan(0);
44 | });
45 | 
46 | test.describe(() => {
47 |   test.use({ mcpBrowser: '' });
48 |   test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => {
49 |     const config: Config = {
50 |       browser: {
51 |         browserName: 'firefox',
52 |       },
53 |     };
54 |     const configPath = testInfo.outputPath('config.json');
55 |     await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
56 | 
57 |     const { client } = await startClient({ args: ['--config', configPath] });
58 |     expect(await client.callTool({
59 |       name: 'browser_navigate',
60 |       arguments: { url: 'data:text/html,<script>document.title = navigator.userAgent</script>' },
61 |     })).toContainTextContent(`Firefox`);
62 |   });
63 | });
64 | 
```

--------------------------------------------------------------------------------
/tests/wait.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('browser_wait_for(text)', async ({ client, server }) => {
20 |   server.setContent('/', `
21 |     <script>
22 |       function update() {
23 |         setTimeout(() => {
24 |           document.querySelector('div').textContent = 'Text to appear';
25 |         }, 1000);
26 |       }
27 |     </script>
28 |     <body>
29 |       <button onclick="update()">Click me</button>
30 |       <div>Text to disappear</div>
31 |     </body>
32 |   `, 'text/html');
33 | 
34 |   await client.callTool({
35 |     name: 'browser_navigate',
36 |     arguments: { url: server.PREFIX },
37 |   });
38 | 
39 |   await client.callTool({
40 |     name: 'browser_click',
41 |     arguments: {
42 |       element: 'Click me',
43 |       ref: 'e2',
44 |     },
45 |   });
46 | 
47 |   expect(await client.callTool({
48 |     name: 'browser_wait_for',
49 |     arguments: { text: 'Text to appear' },
50 |   })).toContainTextContent(`- generic [ref=e3]: Text to appear`);
51 | });
52 | 
53 | test('browser_wait_for(textGone)', async ({ client, server }) => {
54 |   server.setContent('/', `
55 |     <script>
56 |       function update() {
57 |         setTimeout(() => {
58 |           document.querySelector('div').textContent = 'Text to appear';
59 |         }, 1000);
60 |       }
61 |     </script>
62 |     <body>
63 |       <button onclick="update()">Click me</button>
64 |       <div>Text to disappear</div>
65 |     </body>
66 |   `, 'text/html');
67 | 
68 |   await client.callTool({
69 |     name: 'browser_navigate',
70 |     arguments: { url: server.PREFIX },
71 |   });
72 | 
73 |   await client.callTool({
74 |     name: 'browser_click',
75 |     arguments: {
76 |       element: 'Click me',
77 |       ref: 'e2',
78 |     },
79 |   });
80 | 
81 |   expect(await client.callTool({
82 |     name: 'browser_wait_for',
83 |     arguments: { textGone: 'Text to disappear' },
84 |   })).toContainTextContent(`- generic [ref=e3]: Text to appear`);
85 | });
86 | 
```

--------------------------------------------------------------------------------
/src/tools/wait.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | import { defineTool, type ToolFactory } from './tool.js';
19 | 
20 | const wait: ToolFactory = captureSnapshot => defineTool({
21 |   capability: 'wait',
22 | 
23 |   schema: {
24 |     name: 'browser_wait_for',
25 |     title: 'Wait for',
26 |     description: 'Wait for text to appear or disappear or a specified time to pass',
27 |     inputSchema: z.object({
28 |       time: z.coerce.number().optional().describe('The time to wait in seconds'),
29 |       text: z.string().optional().describe('The text to wait for'),
30 |       textGone: z.string().optional().describe('The text to wait for to disappear'),
31 |     }),
32 |     type: 'readOnly',
33 |   },
34 | 
35 |   handle: async (context, params) => {
36 |     if (!params.text && !params.textGone && !params.time)
37 |       throw new Error('Either time, text or textGone must be provided');
38 | 
39 |     const code: string[] = [];
40 | 
41 |     if (params.time) {
42 |       code.push(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`);
43 |       await new Promise(f => setTimeout(f, Math.min(10000, params.time! * 1000)));
44 |     }
45 | 
46 |     const tab = context.currentTabOrDie();
47 |     const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
48 |     const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
49 | 
50 |     if (goneLocator) {
51 |       code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
52 |       await goneLocator.waitFor({ state: 'hidden' });
53 |     }
54 | 
55 |     if (locator) {
56 |       code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
57 |       await locator.waitFor({ state: 'visible' });
58 |     }
59 | 
60 |     return {
61 |       code,
62 |       captureSnapshot,
63 |       waitForNetwork: false,
64 |     };
65 |   },
66 | });
67 | 
68 | export default (captureSnapshot: boolean) => [
69 |   wait(captureSnapshot),
70 | ];
71 | 
```

--------------------------------------------------------------------------------
/src/tools/navigate.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Copyright (c) Microsoft Corporation.
  3 |  *
  4 |  * Licensed under the Apache License, Version 2.0 (the "License");
  5 |  * you may not use this file except in compliance with the License.
  6 |  * You may obtain a copy of the License at
  7 |  *
  8 |  * http://www.apache.org/licenses/LICENSE-2.0
  9 |  *
 10 |  * Unless required by applicable law or agreed to in writing, software
 11 |  * distributed under the License is distributed on an "AS IS" BASIS,
 12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 |  * See the License for the specific language governing permissions and
 14 |  * limitations under the License.
 15 |  */
 16 | 
 17 | import { z } from 'zod';
 18 | import { defineTool, type ToolFactory } from './tool.js';
 19 | 
 20 | const navigate: ToolFactory = captureSnapshot => defineTool({
 21 |   capability: 'core',
 22 | 
 23 |   schema: {
 24 |     name: 'browser_navigate',
 25 |     title: 'Navigate to a URL',
 26 |     description: 'Navigate to a URL',
 27 |     inputSchema: z.object({
 28 |       url: z.string().describe('The URL to navigate to'),
 29 |     }),
 30 |     type: 'destructive',
 31 |   },
 32 | 
 33 |   handle: async (context, params) => {
 34 |     const tab = await context.ensureTab();
 35 |     await tab.navigate(params.url);
 36 | 
 37 |     const code = [
 38 |       `// Navigate to ${params.url}`,
 39 |       `await page.goto('${params.url}');`,
 40 |     ];
 41 | 
 42 |     return {
 43 |       code,
 44 |       captureSnapshot,
 45 |       waitForNetwork: false,
 46 |     };
 47 |   },
 48 | });
 49 | 
 50 | const goBack: ToolFactory = captureSnapshot => defineTool({
 51 |   capability: 'history',
 52 |   schema: {
 53 |     name: 'browser_navigate_back',
 54 |     title: 'Go back',
 55 |     description: 'Go back to the previous page',
 56 |     inputSchema: z.object({}),
 57 |     type: 'readOnly',
 58 |   },
 59 | 
 60 |   handle: async context => {
 61 |     const tab = await context.ensureTab();
 62 |     await tab.page.goBack();
 63 |     const code = [
 64 |       `// Navigate back`,
 65 |       `await page.goBack();`,
 66 |     ];
 67 | 
 68 |     return {
 69 |       code,
 70 |       captureSnapshot,
 71 |       waitForNetwork: false,
 72 |     };
 73 |   },
 74 | });
 75 | 
 76 | const goForward: ToolFactory = captureSnapshot => defineTool({
 77 |   capability: 'history',
 78 |   schema: {
 79 |     name: 'browser_navigate_forward',
 80 |     title: 'Go forward',
 81 |     description: 'Go forward to the next page',
 82 |     inputSchema: z.object({}),
 83 |     type: 'readOnly',
 84 |   },
 85 |   handle: async context => {
 86 |     const tab = context.currentTabOrDie();
 87 |     await tab.page.goForward();
 88 |     const code = [
 89 |       `// Navigate forward`,
 90 |       `await page.goForward();`,
 91 |     ];
 92 |     return {
 93 |       code,
 94 |       captureSnapshot,
 95 |       waitForNetwork: false,
 96 |     };
 97 |   },
 98 | });
 99 | 
100 | export default (captureSnapshot: boolean) => [
101 |   navigate(captureSnapshot),
102 |   goBack(captureSnapshot),
103 |   goForward(captureSnapshot),
104 | ];
105 | 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish
 2 | on:
 3 |   release:
 4 |     types: [published]
 5 | jobs:
 6 |   publish-npm:
 7 |     runs-on: ubuntu-latest
 8 |     permissions:
 9 |       contents: read
10 |       id-token: write # Needed for npm provenance
11 |     steps:
12 |       - uses: actions/checkout@v4
13 |       - uses: actions/setup-node@v4
14 |         with:
15 |           node-version: 18
16 |           registry-url: https://registry.npmjs.org/
17 |       - run: npm ci
18 |       - run: npx playwright install --with-deps
19 |       - run: npm run build
20 |       - run: npm run lint
21 |       - run: npm run ctest
22 |       - run: npm publish --provenance
23 |         env:
24 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
25 | 
26 |   publish-docker:
27 |     runs-on: ubuntu-latest
28 |     permissions:
29 |       contents: read
30 |       id-token: write # Needed for OIDC login to Azure
31 |     environment: allow-publishing-docker-to-acr
32 |     steps:
33 |       - uses: actions/checkout@v4
34 |       - name: Set up QEMU # Needed for multi-platform builds (e.g., arm64 on amd64 runner)
35 |         uses: docker/setup-qemu-action@v3
36 |       - name: Set up Docker Buildx # Needed for multi-platform builds
37 |         uses: docker/setup-buildx-action@v3
38 |       - name: Azure Login via OIDC
39 |         uses: azure/login@v2
40 |         with:
41 |           client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
42 |           tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
43 |           subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}
44 |       - name: Login to ACR
45 |         run: az acr login --name playwright
46 |       - name: Build and push Docker image
47 |         id: build-push
48 |         uses: docker/build-push-action@v6
49 |         with:
50 |           context: .
51 |           file: ./Dockerfile # Adjust path if your Dockerfile is elsewhere
52 |           platforms: linux/amd64,linux/arm64
53 |           push: true
54 |           tags: |
55 |             playwright.azurecr.io/public/playwright/mcp:${{ github.event.release.tag_name }}
56 |             playwright.azurecr.io/public/playwright/mcp:latest
57 |       - uses: oras-project/setup-oras@v1
58 |       - name: Set oras tags
59 |         run: |
60 |           attach_eol_manifest() {
61 |             local image="$1"
62 |             local today=$(date -u +'%Y-%m-%d')
63 |             # oras is re-using Docker credentials, so we don't need to login.
64 |             # Following the advice in https://portal.microsofticm.com/imp/v3/incidents/incident/476783820/summary
65 |             oras attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation "vnd.microsoft.artifact.lifecycle.end-of-life.date=$today" $image
66 |           }
67 |           # for each tag, attach the eol manifest
68 |           for tag in $(echo ${{ steps.build-push.outputs.metadata['image.name'] }} | tr ',' '\n'); do
69 |             attach_eol_manifest $tag
70 |           done
71 | 
```

--------------------------------------------------------------------------------
/tests/browser-server.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | import path from 'path';
17 | import url from 'node:url';
18 | 
19 | import { spawn } from 'child_process';
20 | import { test as baseTest, expect } from './fixtures.js';
21 | 
22 | import type { ChildProcess } from 'child_process';
23 | 
24 | const __filename = url.fileURLToPath(import.meta.url);
25 | 
26 | const test = baseTest.extend<{ agentEndpoint: (options?: { args?: string[] }) => Promise<{ url: URL, stdout: () => string }> }>({
27 |   agentEndpoint: async ({}, use) => {
28 |     let cp: ChildProcess | undefined;
29 |     await use(async (options?: { args?: string[] }) => {
30 |       if (cp)
31 |         throw new Error('Process already running');
32 | 
33 |       cp = spawn('node', [
34 |         path.join(path.dirname(__filename), '../lib/browserServer.js'),
35 |         ...(options?.args || []),
36 |       ], {
37 |         stdio: 'pipe',
38 |         env: {
39 |           ...process.env,
40 |           DEBUG: 'pw:mcp:test',
41 |           DEBUG_COLORS: '0',
42 |           DEBUG_HIDE_DATE: '1',
43 |         },
44 |       });
45 |       let stdout = '';
46 |       const url = await new Promise<string>(resolve => cp!.stdout?.on('data', data => {
47 |         stdout += data.toString();
48 |         const match = stdout.match(/Listening on (http:\/\/.*)/);
49 |         if (match)
50 |           resolve(match[1]);
51 |       }));
52 | 
53 |       return { url: new URL(url), stdout: () => stdout };
54 |     });
55 |     cp?.kill('SIGTERM');
56 |   },
57 | });
58 | 
59 | test.skip(({ mcpBrowser }) => mcpBrowser !== 'chrome', 'Agent is CDP-only for now');
60 | 
61 | test('browser lifecycle', async ({ agentEndpoint, startClient, server }) => {
62 |   const { url: agentUrl } = await agentEndpoint();
63 |   const { client: client1 } = await startClient({ args: ['--browser-agent', agentUrl.toString()] });
64 |   expect(await client1.callTool({
65 |     name: 'browser_navigate',
66 |     arguments: { url: server.HELLO_WORLD },
67 |   })).toContainTextContent('Hello, world!');
68 | 
69 |   const { client: client2 } = await startClient({ args: ['--browser-agent', agentUrl.toString()] });
70 |   expect(await client2.callTool({
71 |     name: 'browser_navigate',
72 |     arguments: { url: server.HELLO_WORLD },
73 |   })).toContainTextContent('Hello, world!');
74 | 
75 |   await client1.close();
76 |   await client2.close();
77 | });
78 | 
```

--------------------------------------------------------------------------------
/tests/pdf.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import fs from 'fs';
18 | 
19 | import { test, expect } from './fixtures.js';
20 | 
21 | test('save as pdf unavailable', async ({ startClient, server }) => {
22 |   const { client } = await startClient({ args: ['--caps="no-pdf"'] });
23 |   await client.callTool({
24 |     name: 'browser_navigate',
25 |     arguments: { url: server.HELLO_WORLD },
26 |   });
27 | 
28 |   expect(await client.callTool({
29 |     name: 'browser_pdf_save',
30 |   })).toHaveTextContent(/Tool \"browser_pdf_save\" not found/);
31 | });
32 | 
33 | test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => {
34 |   const { client } = await startClient({
35 |     config: { outputDir: testInfo.outputPath('output') },
36 |   });
37 | 
38 |   test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
39 | 
40 |   expect(await client.callTool({
41 |     name: 'browser_navigate',
42 |     arguments: { url: server.HELLO_WORLD },
43 |   })).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
44 | 
45 |   const response = await client.callTool({
46 |     name: 'browser_pdf_save',
47 |   });
48 |   expect(response).toHaveTextContent(/Save page as.*page-[^:]+.pdf/);
49 | });
50 | 
51 | test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server }, testInfo) => {
52 |   const outputDir = testInfo.outputPath('output');
53 |   test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
54 |   const { client } = await startClient({
55 |     config: { outputDir },
56 |   });
57 | 
58 |   expect(await client.callTool({
59 |     name: 'browser_navigate',
60 |     arguments: { url: server.HELLO_WORLD },
61 |   })).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
62 | 
63 |   expect(await client.callTool({
64 |     name: 'browser_pdf_save',
65 |     arguments: {
66 |       filename: 'output.pdf',
67 |     },
68 |   })).toEqual({
69 |     content: [
70 |       {
71 |         type: 'text',
72 |         text: expect.stringContaining(`output.pdf`),
73 |       },
74 |     ],
75 |   });
76 | 
77 |   const files = [...fs.readdirSync(outputDir)];
78 | 
79 |   expect(fs.existsSync(outputDir)).toBeTruthy();
80 |   const pdfFiles = files.filter(f => f.endsWith('.pdf'));
81 |   expect(pdfFiles).toHaveLength(1);
82 |   expect(pdfFiles[0]).toMatch(/^output.pdf$/);
83 | });
84 | 
```

--------------------------------------------------------------------------------
/tests/request-blocking.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18 | import { test, expect } from './fixtures.ts';
19 | 
20 | const BLOCK_MESSAGE = /Blocked by Web Inspector|NS_ERROR_FAILURE|net::ERR_BLOCKED_BY_CLIENT/g;
21 | 
22 | const fetchPage = async (client: Client, url: string) => {
23 |   const result = await client.callTool({
24 |     name: 'browser_navigate',
25 |     arguments: {
26 |       url,
27 |     },
28 |   });
29 | 
30 |   return JSON.stringify(result, null, 2);
31 | };
32 | 
33 | test('default to allow all', async ({ server, client }) => {
34 |   server.setContent('/ppp', 'content:PPP', 'text/html');
35 |   const result = await fetchPage(client, server.PREFIX + 'ppp');
36 |   expect(result).toContain('content:PPP');
37 | });
38 | 
39 | test('blocked works', async ({ startClient }) => {
40 |   const { client } = await startClient({
41 |     args: ['--blocked-origins', 'microsoft.com;example.com;playwright.dev']
42 |   });
43 |   const result = await fetchPage(client, 'https://example.com/');
44 |   expect(result).toMatch(BLOCK_MESSAGE);
45 | });
46 | 
47 | test('allowed works', async ({ server, startClient }) => {
48 |   server.setContent('/ppp', 'content:PPP', 'text/html');
49 |   const { client } = await startClient({
50 |     args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`]
51 |   });
52 |   const result = await fetchPage(client, server.PREFIX + 'ppp');
53 |   expect(result).toContain('content:PPP');
54 | });
55 | 
56 | test('blocked takes precedence', async ({ startClient }) => {
57 |   const { client } = await startClient({
58 |     args: [
59 |       '--blocked-origins', 'example.com',
60 |       '--allowed-origins', 'example.com',
61 |     ],
62 |   });
63 |   const result = await fetchPage(client, 'https://example.com/');
64 |   expect(result).toMatch(BLOCK_MESSAGE);
65 | });
66 | 
67 | test('allowed without blocked blocks all non-explicitly specified origins', async ({ startClient }) => {
68 |   const { client } = await startClient({
69 |     args: ['--allowed-origins', 'playwright.dev'],
70 |   });
71 |   const result = await fetchPage(client, 'https://example.com/');
72 |   expect(result).toMatch(BLOCK_MESSAGE);
73 | });
74 | 
75 | test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => {
76 |   server.setContent('/ppp', 'content:PPP', 'text/html');
77 |   const { client } = await startClient({
78 |     args: ['--blocked-origins', 'example.com'],
79 |   });
80 |   const result = await fetchPage(client, server.PREFIX + 'ppp');
81 |   expect(result).toContain('content:PPP');
82 | });
83 | 
```

--------------------------------------------------------------------------------
/tests/capabilities.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { test, expect } from './fixtures.js';
18 | 
19 | test('test snapshot tool list', async ({ client }) => {
20 |   const { tools } = await client.listTools();
21 |   expect(new Set(tools.map(t => t.name))).toEqual(new Set([
22 |     'browser_click',
23 |     'browser_console_messages',
24 |     'browser_drag',
25 |     'browser_file_upload',
26 |     'browser_generate_playwright_test',
27 |     'browser_handle_dialog',
28 |     'browser_hover',
29 |     'browser_select_option',
30 |     'browser_type',
31 |     'browser_close',
32 |     'browser_install',
33 |     'browser_navigate_back',
34 |     'browser_navigate_forward',
35 |     'browser_navigate',
36 |     'browser_network_requests',
37 |     'browser_pdf_save',
38 |     'browser_press_key',
39 |     'browser_resize',
40 |     'browser_snapshot',
41 |     'browser_tab_close',
42 |     'browser_tab_list',
43 |     'browser_tab_new',
44 |     'browser_tab_select',
45 |     'browser_take_screenshot',
46 |     'browser_wait_for',
47 |   ]));
48 | });
49 | 
50 | test('test vision tool list', async ({ visionClient }) => {
51 |   const { tools: visionTools } = await visionClient.listTools();
52 |   expect(new Set(visionTools.map(t => t.name))).toEqual(new Set([
53 |     'browser_close',
54 |     'browser_console_messages',
55 |     'browser_file_upload',
56 |     'browser_generate_playwright_test',
57 |     'browser_handle_dialog',
58 |     'browser_install',
59 |     'browser_navigate_back',
60 |     'browser_navigate_forward',
61 |     'browser_navigate',
62 |     'browser_network_requests',
63 |     'browser_pdf_save',
64 |     'browser_press_key',
65 |     'browser_resize',
66 |     'browser_screen_capture',
67 |     'browser_screen_click',
68 |     'browser_screen_drag',
69 |     'browser_screen_move_mouse',
70 |     'browser_screen_type',
71 |     'browser_tab_close',
72 |     'browser_tab_list',
73 |     'browser_tab_new',
74 |     'browser_tab_select',
75 |     'browser_wait_for',
76 |   ]));
77 | });
78 | 
79 | test('test capabilities', async ({ startClient }) => {
80 |   const { client } = await startClient({
81 |     args: ['--caps="core"'],
82 |   });
83 |   const { tools } = await client.listTools();
84 |   const toolNames = tools.map(t => t.name);
85 |   expect(toolNames).not.toContain('browser_file_upload');
86 |   expect(toolNames).not.toContain('browser_pdf_save');
87 |   expect(toolNames).not.toContain('browser_screen_capture');
88 |   expect(toolNames).not.toContain('browser_screen_click');
89 |   expect(toolNames).not.toContain('browser_screen_drag');
90 |   expect(toolNames).not.toContain('browser_screen_move_mouse');
91 |   expect(toolNames).not.toContain('browser_screen_type');
92 | });
93 | 
```

--------------------------------------------------------------------------------
/cloudflare/vite.config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import path from 'path';
  2 | 
  3 | import { defineConfig } from 'vite';
  4 | 
  5 | // https://vitejs.dev/config/
  6 | export default defineConfig({
  7 |   resolve: {
  8 |     alias: {
  9 |       // https://workers-nodejs-compat-matrix.pages.dev/
 10 |       'async_hooks': 'node:async_hooks',
 11 |       'assert': 'node:assert',
 12 |       'buffer': 'node:buffer',
 13 |       'child_process': 'node:child_process',
 14 |       'constants': 'node:constants',
 15 |       'crypto': 'node:crypto',
 16 |       'dns': 'node:dns',
 17 |       'events': 'node:events',
 18 |       'http': 'node:http',
 19 |       'http2': 'node:http2',
 20 |       'https': 'node:https',
 21 |       'inspector': 'node:inspector',
 22 |       'module': 'node:module',
 23 |       'net': 'node:net',
 24 |       'os': 'node:os',
 25 |       'path': 'node:path',
 26 |       'process': 'node:process',
 27 |       'readline': 'node:readline',
 28 |       'stream': 'node:stream',
 29 |       'tls': 'node:tls',
 30 |       'url': 'node:url',
 31 |       'util': 'node:util',
 32 |       'zlib': 'node:zlib',
 33 | 
 34 |       'playwright-core': '@cloudflare/playwright',
 35 |       'playwright': '@cloudflare/playwright/test',
 36 |       'node:fs': '@cloudflare/playwright/fs',
 37 |       'fs': '@cloudflare/playwright/fs',
 38 | 
 39 |       './package.js': path.resolve(__dirname, './src/package.ts'),
 40 |     },
 41 |   },
 42 |   build: {
 43 |     assetsInlineLimit: 0,
 44 |     // skip code obfuscation
 45 |     minify: false,
 46 |     lib: {
 47 |       name: '@cloudflare/playwright',
 48 |       entry: [
 49 |         path.resolve(__dirname, './src/index.ts'),
 50 |       ],
 51 |     },
 52 |     // prevents __defProp, __defNormalProp, __publicField in compiled code
 53 |     target: 'esnext',
 54 |     rollupOptions: {
 55 |       output: [
 56 |         {
 57 |           format: 'es',
 58 |           dir: 'lib/esm',
 59 |           preserveModules: true,
 60 |           preserveModulesRoot: 'src',
 61 |           entryFileNames: '[name].js',
 62 |           chunkFileNames: '[name].js',
 63 |         },
 64 |         {
 65 |           format: 'cjs',
 66 |           dir: 'lib/cjs',
 67 |           preserveModules: true,
 68 |           preserveModulesRoot: 'src',
 69 |           entryFileNames: '[name].js',
 70 |           chunkFileNames: '[name].js',
 71 |           exports: 'named',
 72 |         },
 73 |       ],
 74 |       external: [
 75 |         'node:async_hooks',
 76 |         'node:assert',
 77 |         'node:browser',
 78 |         'node:buffer',
 79 |         'node:child_process',
 80 |         'node:constants',
 81 |         'node:crypto',
 82 |         'node:dns',
 83 |         'node:events',
 84 |         'node:http',
 85 |         'node:http2',
 86 |         'node:https',
 87 |         'node:inspector',
 88 |         'node:module',
 89 |         'node:net',
 90 |         'node:os',
 91 |         'node:path',
 92 |         'node:process',
 93 |         'node:readline',
 94 |         'node:stream',
 95 |         'node:timers',
 96 |         'node:tls',
 97 |         'node:url',
 98 |         'node:util',
 99 |         'node:zlib',
100 | 
101 |         '@cloudflare/playwright',
102 |         '@cloudflare/playwright/test',
103 |         '@cloudflare/playwright/fs',
104 |         'cloudflare:workers',
105 | 
106 |         /@modelcontextprotocol\/sdk\/.*/,
107 |         'agents/mcp',
108 |         'yaml',
109 |         'zod',
110 |         'zod-to-json-schema',
111 |       ]
112 |     },
113 |     commonjsOptions: {
114 |       transformMixedEsModules: true,
115 |       extensions: ['.ts', '.js'],
116 |       include: [
117 |         path.resolve(__dirname, '../src/**/*'),
118 |         /node_modules/,
119 |       ],
120 |     }
121 |   },
122 | });
123 | 
```

--------------------------------------------------------------------------------
/tests/cdp.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import url from 'node:url';
18 | import path from 'node:path';
19 | import { spawnSync } from 'node:child_process';
20 | import { test, expect } from './fixtures.js';
21 | 
22 | test('cdp server', async ({ cdpServer, startClient, server }) => {
23 |   await cdpServer.start();
24 |   const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
25 |   expect(await client.callTool({
26 |     name: 'browser_navigate',
27 |     arguments: { url: server.HELLO_WORLD },
28 |   })).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
29 | });
30 | 
31 | test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
32 |   const browserContext = await cdpServer.start();
33 |   const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
34 | 
35 |   const [page] = browserContext.pages();
36 |   await page.goto(server.HELLO_WORLD);
37 | 
38 |   expect(await client.callTool({
39 |     name: 'browser_click',
40 |     arguments: {
41 |       element: 'Hello, world!',
42 |       ref: 'f0',
43 |     },
44 |   })).toHaveTextContent(`Error: No current snapshot available. Capture a snapshot or navigate to a new location first.`);
45 | 
46 |   expect(await client.callTool({
47 |     name: 'browser_snapshot',
48 |   })).toHaveTextContent(`
49 | - Ran Playwright code:
50 | \`\`\`js
51 | // <internal code to capture accessibility snapshot>
52 | \`\`\`
53 | 
54 | - Page URL: ${server.HELLO_WORLD}
55 | - Page Title: Title
56 | - Page Snapshot
57 | \`\`\`yaml
58 | - generic [active] [ref=e1]: Hello, world!
59 | \`\`\`
60 | `);
61 | });
62 | 
63 | test('should throw connection error and allow re-connecting', async ({ cdpServer, startClient, server }) => {
64 |   const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
65 | 
66 |   server.setContent('/', `
67 |     <title>Title</title>
68 |     <body>Hello, world!</body>
69 |   `, 'text/html');
70 | 
71 |   expect(await client.callTool({
72 |     name: 'browser_navigate',
73 |     arguments: { url: server.PREFIX },
74 |   })).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`);
75 |   await cdpServer.start();
76 |   expect(await client.callTool({
77 |     name: 'browser_navigate',
78 |     arguments: { url: server.PREFIX },
79 |   })).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
80 | });
81 | 
82 | // NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
83 | const __filename = url.fileURLToPath(import.meta.url);
84 | 
85 | test('does not support --device', async () => {
86 |   const result = spawnSync('node', [
87 |     path.join(__filename, '../../cli.js'), '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234',
88 |   ]);
89 |   expect(result.error).toBeUndefined();
90 |   expect(result.status).toBe(1);
91 |   expect(result.stderr.toString()).toContain('Device emulation is not supported with cdpEndpoint.');
92 | });
93 | 
```

--------------------------------------------------------------------------------
/src/tools/utils.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import type * as playwright from 'playwright';
18 | import type { Context } from '../context.js';
19 | import type { Tab } from '../tab.js';
20 | 
21 | export async function waitForCompletion<R>(context: Context, tab: Tab, callback: () => Promise<R>): Promise<R> {
22 |   const requests = new Set<playwright.Request>();
23 |   let frameNavigated = false;
24 |   let waitCallback: () => void = () => {};
25 |   const waitBarrier = new Promise<void>(f => { waitCallback = f; });
26 | 
27 |   const requestListener = (request: playwright.Request) => requests.add(request);
28 |   const requestFinishedListener = (request: playwright.Request) => {
29 |     requests.delete(request);
30 |     if (!requests.size)
31 |       waitCallback();
32 |   };
33 | 
34 |   const frameNavigateListener = (frame: playwright.Frame) => {
35 |     if (frame.parentFrame())
36 |       return;
37 |     frameNavigated = true;
38 |     dispose();
39 |     clearTimeout(timeout);
40 |     void tab.waitForLoadState('load').then(waitCallback);
41 |   };
42 | 
43 |   const onTimeout = () => {
44 |     dispose();
45 |     waitCallback();
46 |   };
47 | 
48 |   tab.page.on('request', requestListener);
49 |   tab.page.on('requestfinished', requestFinishedListener);
50 |   tab.page.on('framenavigated', frameNavigateListener);
51 |   const timeout = setTimeout(onTimeout, 10000);
52 | 
53 |   const dispose = () => {
54 |     tab.page.off('request', requestListener);
55 |     tab.page.off('requestfinished', requestFinishedListener);
56 |     tab.page.off('framenavigated', frameNavigateListener);
57 |     clearTimeout(timeout);
58 |   };
59 | 
60 |   try {
61 |     const result = await callback();
62 |     if (!requests.size && !frameNavigated)
63 |       waitCallback();
64 |     await waitBarrier;
65 |     await context.waitForTimeout(1000);
66 |     return result;
67 |   } finally {
68 |     dispose();
69 |   }
70 | }
71 | 
72 | export function sanitizeForFilePath(s: string) {
73 |   const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
74 |   const separator = s.lastIndexOf('.');
75 |   if (separator === -1)
76 |     return sanitize(s);
77 |   return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
78 | }
79 | 
80 | export async function generateLocator(locator: playwright.Locator): Promise<string> {
81 |   try {
82 |     return await (locator as any)._generateLocatorString();
83 |   } catch (e) {
84 |     if (e instanceof Error && /locator._generateLocatorString: No element matching locator/.test(e.message))
85 |       throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.');
86 |     throw e;
87 |   }
88 | }
89 | 
90 | export async function callOnPageNoTrace<T>(page: playwright.Page, callback: (page: playwright.Page) => Promise<T>): Promise<T> {
91 |   return await (page as any)._wrapApiCall(() => callback(page), { internal: true });
92 | }
93 | 
```

--------------------------------------------------------------------------------
/src/tools/tabs.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Copyright (c) Microsoft Corporation.
  3 |  *
  4 |  * Licensed under the Apache License, Version 2.0 (the "License");
  5 |  * you may not use this file except in compliance with the License.
  6 |  * You may obtain a copy of the License at
  7 |  *
  8 |  * http://www.apache.org/licenses/LICENSE-2.0
  9 |  *
 10 |  * Unless required by applicable law or agreed to in writing, software
 11 |  * distributed under the License is distributed on an "AS IS" BASIS,
 12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 |  * See the License for the specific language governing permissions and
 14 |  * limitations under the License.
 15 |  */
 16 | 
 17 | import { z } from 'zod';
 18 | import { defineTool, type ToolFactory } from './tool.js';
 19 | 
 20 | const listTabs = defineTool({
 21 |   capability: 'tabs',
 22 | 
 23 |   schema: {
 24 |     name: 'browser_tab_list',
 25 |     title: 'List tabs',
 26 |     description: 'List browser tabs',
 27 |     inputSchema: z.object({}),
 28 |     type: 'readOnly',
 29 |   },
 30 | 
 31 |   handle: async context => {
 32 |     await context.ensureTab();
 33 |     return {
 34 |       code: [`// <internal code to list tabs>`],
 35 |       captureSnapshot: false,
 36 |       waitForNetwork: false,
 37 |       resultOverride: {
 38 |         content: [{
 39 |           type: 'text',
 40 |           text: await context.listTabsMarkdown(),
 41 |         }],
 42 |       },
 43 |     };
 44 |   },
 45 | });
 46 | 
 47 | const selectTab: ToolFactory = captureSnapshot => defineTool({
 48 |   capability: 'tabs',
 49 | 
 50 |   schema: {
 51 |     name: 'browser_tab_select',
 52 |     title: 'Select a tab',
 53 |     description: 'Select a tab by index',
 54 |     inputSchema: z.object({
 55 |       index: z.coerce.number().describe('The index of the tab to select'),
 56 |     }),
 57 |     type: 'readOnly',
 58 |   },
 59 | 
 60 |   handle: async (context, params) => {
 61 |     await context.selectTab(params.index);
 62 |     const code = [
 63 |       `// <internal code to select tab ${params.index}>`,
 64 |     ];
 65 | 
 66 |     return {
 67 |       code,
 68 |       captureSnapshot,
 69 |       waitForNetwork: false
 70 |     };
 71 |   },
 72 | });
 73 | 
 74 | const newTab: ToolFactory = captureSnapshot => defineTool({
 75 |   capability: 'tabs',
 76 | 
 77 |   schema: {
 78 |     name: 'browser_tab_new',
 79 |     title: 'Open a new tab',
 80 |     description: 'Open a new tab',
 81 |     inputSchema: z.object({
 82 |       url: z.string().optional().describe('The URL to navigate to in the new tab. If not provided, the new tab will be blank.'),
 83 |     }),
 84 |     type: 'readOnly',
 85 |   },
 86 | 
 87 |   handle: async (context, params) => {
 88 |     await context.newTab();
 89 |     if (params.url)
 90 |       await context.currentTabOrDie().navigate(params.url);
 91 | 
 92 |     const code = [
 93 |       `// <internal code to open a new tab>`,
 94 |     ];
 95 |     return {
 96 |       code,
 97 |       captureSnapshot,
 98 |       waitForNetwork: false
 99 |     };
100 |   },
101 | });
102 | 
103 | const closeTab: ToolFactory = captureSnapshot => defineTool({
104 |   capability: 'tabs',
105 | 
106 |   schema: {
107 |     name: 'browser_tab_close',
108 |     title: 'Close a tab',
109 |     description: 'Close a tab',
110 |     inputSchema: z.object({
111 |       index: z.coerce.number().optional().describe('The index of the tab to close. Closes current tab if not provided.'),
112 |     }),
113 |     type: 'destructive',
114 |   },
115 | 
116 |   handle: async (context, params) => {
117 |     await context.closeTab(params.index);
118 |     const code = [
119 |       `// <internal code to close tab ${params.index}>`,
120 |     ];
121 |     return {
122 |       code,
123 |       captureSnapshot,
124 |       waitForNetwork: false
125 |     };
126 |   },
127 | });
128 | 
129 | export default (captureSnapshot: boolean) => [
130 |   listTabs,
131 |   newTab(captureSnapshot),
132 |   selectTab(captureSnapshot),
133 |   closeTab(captureSnapshot),
134 | ];
135 | 
```

--------------------------------------------------------------------------------
/src/manualPromise.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Copyright (c) Microsoft Corporation.
  3 |  *
  4 |  * Licensed under the Apache License, Version 2.0 (the "License");
  5 |  * you may not use this file except in compliance with the License.
  6 |  * You may obtain a copy of the License at
  7 |  *
  8 |  * http://www.apache.org/licenses/LICENSE-2.0
  9 |  *
 10 |  * Unless required by applicable law or agreed to in writing, software
 11 |  * distributed under the License is distributed on an "AS IS" BASIS,
 12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 |  * See the License for the specific language governing permissions and
 14 |  * limitations under the License.
 15 |  */
 16 | 
 17 | export class ManualPromise<T = void> extends Promise<T> {
 18 |   private _resolve!: (t: T) => void;
 19 |   private _reject!: (e: Error) => void;
 20 |   private _isDone: boolean;
 21 | 
 22 |   constructor() {
 23 |     let resolve: (t: T) => void;
 24 |     let reject: (e: Error) => void;
 25 |     super((f, r) => {
 26 |       resolve = f;
 27 |       reject = r;
 28 |     });
 29 |     this._isDone = false;
 30 |     this._resolve = resolve!;
 31 |     this._reject = reject!;
 32 |   }
 33 | 
 34 |   isDone() {
 35 |     return this._isDone;
 36 |   }
 37 | 
 38 |   resolve(t: T) {
 39 |     this._isDone = true;
 40 |     this._resolve(t);
 41 |   }
 42 | 
 43 |   reject(e: Error) {
 44 |     this._isDone = true;
 45 |     this._reject(e);
 46 |   }
 47 | 
 48 |   static override get [Symbol.species]() {
 49 |     return Promise;
 50 |   }
 51 | 
 52 |   override get [Symbol.toStringTag]() {
 53 |     return 'ManualPromise';
 54 |   }
 55 | }
 56 | 
 57 | export class LongStandingScope {
 58 |   private _terminateError: Error | undefined;
 59 |   private _closeError: Error | undefined;
 60 |   private _terminatePromises = new Map<ManualPromise<Error>, string[]>();
 61 |   private _isClosed = false;
 62 | 
 63 |   reject(error: Error) {
 64 |     this._isClosed = true;
 65 |     this._terminateError = error;
 66 |     for (const p of this._terminatePromises.keys())
 67 |       p.resolve(error);
 68 |   }
 69 | 
 70 |   close(error: Error) {
 71 |     this._isClosed = true;
 72 |     this._closeError = error;
 73 |     for (const [p, frames] of this._terminatePromises)
 74 |       p.resolve(cloneError(error, frames));
 75 |   }
 76 | 
 77 |   isClosed() {
 78 |     return this._isClosed;
 79 |   }
 80 | 
 81 |   static async raceMultiple<T>(scopes: LongStandingScope[], promise: Promise<T>): Promise<T> {
 82 |     return Promise.race(scopes.map(s => s.race(promise)));
 83 |   }
 84 | 
 85 |   async race<T>(promise: Promise<T> | Promise<T>[]): Promise<T> {
 86 |     return this._race(Array.isArray(promise) ? promise : [promise], false) as Promise<T>;
 87 |   }
 88 | 
 89 |   async safeRace<T>(promise: Promise<T>, defaultValue?: T): Promise<T> {
 90 |     return this._race([promise], true, defaultValue);
 91 |   }
 92 | 
 93 |   private async _race(promises: Promise<any>[], safe: boolean, defaultValue?: any): Promise<any> {
 94 |     const terminatePromise = new ManualPromise<Error>();
 95 |     const frames = captureRawStack();
 96 |     if (this._terminateError)
 97 |       terminatePromise.resolve(this._terminateError);
 98 |     if (this._closeError)
 99 |       terminatePromise.resolve(cloneError(this._closeError, frames));
100 |     this._terminatePromises.set(terminatePromise, frames);
101 |     try {
102 |       return await Promise.race([
103 |         terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),
104 |         ...promises
105 |       ]);
106 |     } finally {
107 |       this._terminatePromises.delete(terminatePromise);
108 |     }
109 |   }
110 | }
111 | 
112 | function cloneError(error: Error, frames: string[]) {
113 |   const clone = new Error();
114 |   clone.name = error.name;
115 |   clone.message = error.message;
116 |   clone.stack = [error.name + ':' + error.message, ...frames].join('\n');
117 |   return clone;
118 | }
119 | 
120 | function captureRawStack(): string[] {
121 |   const stackTraceLimit = Error.stackTraceLimit;
122 |   Error.stackTraceLimit = 50;
123 |   const error = new Error();
124 |   const stack = error.stack || '';
125 |   Error.stackTraceLimit = stackTraceLimit;
126 |   return stack.split('\n');
127 | }
128 | 
```

--------------------------------------------------------------------------------
/src/connection.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
18 | import { CallToolRequestSchema, ListToolsRequestSchema, Tool as McpTool } from '@modelcontextprotocol/sdk/types.js';
19 | import { zodToJsonSchema } from 'zod-to-json-schema';
20 | 
21 | import { Context } from './context.js';
22 | import { snapshotTools, visionTools } from './tools.js';
23 | import { packageJSON } from './package.js';
24 | 
25 | import { FullConfig } from './config.js';
26 | 
27 | import type { BrowserContextFactory } from './browserContextFactory.js';
28 | 
29 | export function createConnection(config: FullConfig, browserContextFactory: BrowserContextFactory): Connection {
30 |   const allTools = config.vision ? visionTools : snapshotTools;
31 |   const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
32 |   const context = new Context(tools, config, browserContextFactory);
33 |   const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, {
34 |     capabilities: {
35 |       tools: {},
36 |     }
37 |   });
38 | 
39 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
40 |     return {
41 |       tools: tools.map(tool => ({
42 |         name: tool.schema.name,
43 |         description: tool.schema.description,
44 |         inputSchema: zodToJsonSchema(tool.schema.inputSchema),
45 |         annotations: {
46 |           title: tool.schema.title,
47 |           readOnlyHint: tool.schema.type === 'readOnly',
48 |           destructiveHint: tool.schema.type === 'destructive',
49 |           openWorldHint: true,
50 |         },
51 |       })) as McpTool[],
52 |     };
53 |   });
54 | 
55 |   server.setRequestHandler(CallToolRequestSchema, async request => {
56 |     const errorResult = (...messages: string[]) => ({
57 |       content: [{ type: 'text', text: messages.join('\n') }],
58 |       isError: true,
59 |     });
60 |     const tool = tools.find(tool => tool.schema.name === request.params.name);
61 |     if (!tool)
62 |       return errorResult(`Tool "${request.params.name}" not found`);
63 | 
64 | 
65 |     const modalStates = context.modalStates().map(state => state.type);
66 |     if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
67 |       return errorResult(`The tool "${request.params.name}" can only be used when there is related modal state present.`, ...context.modalStatesMarkdown());
68 |     if (!tool.clearsModalState && modalStates.length)
69 |       return errorResult(`Tool "${request.params.name}" does not handle the modal state.`, ...context.modalStatesMarkdown());
70 | 
71 |     try {
72 |       return await context.run(tool, request.params.arguments);
73 |     } catch (error) {
74 |       return errorResult(String(error));
75 |     }
76 |   });
77 | 
78 |   return new Connection(server, context);
79 | }
80 | 
81 | export class Connection {
82 |   readonly server: McpServer;
83 |   readonly context: Context;
84 | 
85 |   constructor(server: McpServer, context: Context) {
86 |     this.server = server;
87 |     this.context = context;
88 |     this.server.oninitialized = () => {
89 |       this.context.clientVersion = this.server.getClientVersion();
90 |     };
91 |   }
92 | 
93 |   async close() {
94 |     await this.server.close();
95 |     await this.context.close();
96 |   }
97 | }
98 | 
```

--------------------------------------------------------------------------------
/config.d.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Copyright (c) Microsoft Corporation.
  3 |  *
  4 |  * Licensed under the Apache License, Version 2.0 (the "License");
  5 |  * you may not use this file except in compliance with the License.
  6 |  * You may obtain a copy of the License at
  7 |  *
  8 |  * http://www.apache.org/licenses/LICENSE-2.0
  9 |  *
 10 |  * Unless required by applicable law or agreed to in writing, software
 11 |  * distributed under the License is distributed on an "AS IS" BASIS,
 12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 |  * See the License for the specific language governing permissions and
 14 |  * limitations under the License.
 15 |  */
 16 | 
 17 | import type * as playwright from 'playwright';
 18 | 
 19 | export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install' | 'testing';
 20 | 
 21 | export type Config = {
 22 |   /**
 23 |    * The browser to use.
 24 |    */
 25 |   browser?: {
 26 |     /**
 27 |      * Use browser agent (experimental).
 28 |      */
 29 |     browserAgent?: string;
 30 | 
 31 |     /**
 32 |      * The type of browser to use.
 33 |      */
 34 |     browserName?: 'chromium' | 'firefox' | 'webkit';
 35 | 
 36 |     /**
 37 |      * Keep the browser profile in memory, do not save it to disk.
 38 |      */
 39 |     isolated?: boolean;
 40 | 
 41 |     /**
 42 |      * Path to a user data directory for browser profile persistence.
 43 |      * Temporary directory is created by default.
 44 |      */
 45 |     userDataDir?: string;
 46 | 
 47 |     /**
 48 |      * Launch options passed to
 49 |      * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
 50 |      *
 51 |      * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
 52 |      */
 53 |     launchOptions?: playwright.LaunchOptions;
 54 | 
 55 |     /**
 56 |      * Context options for the browser context.
 57 |      *
 58 |      * This is useful for settings options like `viewport`.
 59 |      */
 60 |     contextOptions?: playwright.BrowserContextOptions;
 61 | 
 62 |     /**
 63 |      * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
 64 |      */
 65 |     cdpEndpoint?: string;
 66 | 
 67 |     /**
 68 |      * Remote endpoint to connect to an existing Playwright server.
 69 |      */
 70 |     remoteEndpoint?: string;
 71 |   },
 72 | 
 73 |   server?: {
 74 |     /**
 75 |      * The port to listen on for SSE or MCP transport.
 76 |      */
 77 |     port?: number;
 78 | 
 79 |     /**
 80 |      * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
 81 |      */
 82 |     host?: string;
 83 |   },
 84 | 
 85 |   /**
 86 |    * List of enabled tool capabilities. Possible values:
 87 |    *   - 'core': Core browser automation features.
 88 |    *   - 'tabs': Tab management features.
 89 |    *   - 'pdf': PDF generation and manipulation.
 90 |    *   - 'history': Browser history access.
 91 |    *   - 'wait': Wait and timing utilities.
 92 |    *   - 'files': File upload/download support.
 93 |    *   - 'install': Browser installation utilities.
 94 |    */
 95 |   capabilities?: ToolCapability[];
 96 | 
 97 |   /**
 98 |    * Run server that uses screenshots (Aria snapshots are used by default).
 99 |    */
100 |   vision?: boolean;
101 | 
102 |   /**
103 |    * Whether to save the Playwright trace of the session into the output directory.
104 |    */
105 |   saveTrace?: boolean;
106 | 
107 |   /**
108 |    * The directory to save output files.
109 |    */
110 |   outputDir?: string;
111 | 
112 |   network?: {
113 |     /**
114 |      * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
115 |      */
116 |     allowedOrigins?: string[];
117 | 
118 |     /**
119 |      * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
120 |      */
121 |     blockedOrigins?: string[];
122 |   };
123 | 
124 |   /**
125 |    * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.
126 |    */
127 |   imageResponses?: 'allow' | 'omit' | 'auto';
128 | };
129 | 
```

--------------------------------------------------------------------------------
/src/tools/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Copyright (c) Microsoft Corporation.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  */
16 | 
17 | import { z } from 'zod';
18 | 
19 | import { defineTool } from './tool.js';
20 | import * as javascript from '../javascript.js';
21 | import { outputFile } from '../config.js';
22 | import { generateLocator } from './utils.js';
23 | 
24 | import type * as playwright from 'playwright';
25 | 
26 | const screenshotSchema = z.object({
27 |   raw: z.coerce.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
28 |   filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
29 |   element: z.string().optional().describe('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.'),
30 |   ref: z.string().optional().describe('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.'),
31 | }).refine(data => {
32 |   return !!data.element === !!data.ref;
33 | }, {
34 |   message: 'Both element and ref must be provided or neither.',
35 |   path: ['ref', 'element']
36 | });
37 | 
38 | const screenshot = defineTool({
39 |   capability: 'core',
40 |   schema: {
41 |     name: 'browser_take_screenshot',
42 |     title: 'Take a screenshot',
43 |     description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
44 |     inputSchema: screenshotSchema,
45 |     type: 'readOnly',
46 |   },
47 | 
48 |   handle: async (context, params) => {
49 |     const tab = context.currentTabOrDie();
50 |     const snapshot = tab.snapshotOrDie();
51 |     const fileType = params.raw ? 'png' : 'jpeg';
52 |     const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
53 |     const options: playwright.PageScreenshotOptions = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
54 |     const isElementScreenshot = params.element && params.ref;
55 | 
56 |     const code = [
57 |       `// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`,
58 |     ];
59 | 
60 |     const locator = params.ref ? snapshot.refLocator({ element: params.element || '', ref: params.ref }) : null;
61 | 
62 |     if (locator)
63 |       code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
64 |     else
65 |       code.push(`await page.screenshot(${javascript.formatObject(options)});`);
66 | 
67 |     const includeBase64 = context.clientSupportsImages();
68 |     const action = async () => {
69 |       const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
70 |       return {
71 |         content: includeBase64 ? [{
72 |           type: 'image' as 'image',
73 |           data: screenshot.toString('base64'),
74 |           mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg',
75 |         }] : []
76 |       };
77 |     };
78 | 
79 |     return {
80 |       code,
81 |       action,
82 |       captureSnapshot: true,
83 |       waitForNetwork: false,
84 |     };
85 |   }
86 | });
87 | 
88 | export default [
89 |   screenshot,
90 | ];
91 | 
```

--------------------------------------------------------------------------------
/src/tab.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Copyright (c) Microsoft Corporation.
  3 |  *
  4 |  * Licensed under the Apache License, Version 2.0 (the "License");
  5 |  * you may not use this file except in compliance with the License.
  6 |  * You may obtain a copy of the License at
  7 |  *
  8 |  * http://www.apache.org/licenses/LICENSE-2.0
  9 |  *
 10 |  * Unless required by applicable law or agreed to in writing, software
 11 |  * distributed under the License is distributed on an "AS IS" BASIS,
 12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 |  * See the License for the specific language governing permissions and
 14 |  * limitations under the License.
 15 |  */
 16 | 
 17 | import * as playwright from 'playwright';
 18 | 
 19 | import { PageSnapshot } from './pageSnapshot.js';
 20 | 
 21 | import type { Context } from './context.js';
 22 | import { callOnPageNoTrace } from './tools/utils.js';
 23 | 
 24 | export class Tab {
 25 |   readonly context: Context;
 26 |   readonly page: playwright.Page;
 27 |   private _consoleMessages: playwright.ConsoleMessage[] = [];
 28 |   private _requests: Map<playwright.Request, playwright.Response | null> = new Map();
 29 |   private _snapshot: PageSnapshot | undefined;
 30 |   private _onPageClose: (tab: Tab) => void;
 31 | 
 32 |   constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) {
 33 |     this.context = context;
 34 |     this.page = page;
 35 |     this._onPageClose = onPageClose;
 36 |     page.on('console', event => this._consoleMessages.push(event));
 37 |     page.on('request', request => this._requests.set(request, null));
 38 |     page.on('response', response => this._requests.set(response.request(), response));
 39 |     page.on('close', () => this._onClose());
 40 |     page.on('filechooser', chooser => {
 41 |       this.context.setModalState({
 42 |         type: 'fileChooser',
 43 |         description: 'File chooser',
 44 |         fileChooser: chooser,
 45 |       }, this);
 46 |     });
 47 |     page.on('dialog', dialog => this.context.dialogShown(this, dialog));
 48 |     page.on('download', download => {
 49 |       void this.context.downloadStarted(this, download);
 50 |     });
 51 |     page.setDefaultNavigationTimeout(60000);
 52 |     page.setDefaultTimeout(5000);
 53 |   }
 54 | 
 55 |   private _clearCollectedArtifacts() {
 56 |     this._consoleMessages.length = 0;
 57 |     this._requests.clear();
 58 |   }
 59 | 
 60 |   private _onClose() {
 61 |     this._clearCollectedArtifacts();
 62 |     this._onPageClose(this);
 63 |   }
 64 | 
 65 |   async title(): Promise<string> {
 66 |     return await callOnPageNoTrace(this.page, page => page.title());
 67 |   }
 68 | 
 69 |   async waitForLoadState(state: 'load', options?: { timeout?: number }): Promise<void> {
 70 |     await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(() => {}));
 71 |   }
 72 | 
 73 |   async navigate(url: string) {
 74 |     this._clearCollectedArtifacts();
 75 | 
 76 |     const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(() => {}));
 77 |     try {
 78 |       await this.page.goto(url, { waitUntil: 'domcontentloaded' });
 79 |     } catch (_e: unknown) {
 80 |       const e = _e as Error;
 81 |       const mightBeDownload =
 82 |         e.message.includes('net::ERR_ABORTED') // chromium
 83 |         || e.message.includes('Download is starting'); // firefox + webkit
 84 |       if (!mightBeDownload)
 85 |         throw e;
 86 |       // on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
 87 |       const download = await Promise.race([
 88 |         downloadEvent,
 89 |         new Promise(resolve => setTimeout(resolve, 1000)),
 90 |       ]);
 91 |       if (!download)
 92 |         throw e;
 93 |     }
 94 | 
 95 |     // Cap load event to 5 seconds, the page is operational at this point.
 96 |     await this.waitForLoadState('load', { timeout: 5000 });
 97 |   }
 98 | 
 99 |   hasSnapshot(): boolean {
100 |     return !!this._snapshot;
101 |   }
102 | 
103 |   snapshotOrDie(): PageSnapshot {
104 |     if (!this._snapshot)
105 |       throw new Error('No snapshot available');
106 |     return this._snapshot;
107 |   }
108 | 
109 |   consoleMessages(): playwright.ConsoleMessage[] {
110 |     return this._consoleMessages;
111 |   }
112 | 
113 |   requests(): Map<playwright.Request, playwright.Response | null> {
114 |     return this._requests;
115 |   }
116 | 
117 |   async captureSnapshot() {
118 |     this._snapshot = await PageSnapshot.create(this.page);
119 |   }
120 | }
121 | 
```

--------------------------------------------------------------------------------
/tests/files.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Copyright (c) Microsoft Corporation.
  3 |  *
  4 |  * Licensed under the Apache License, Version 2.0 (the "License");
  5 |  * you may not use this file except in compliance with the License.
  6 |  * You may obtain a copy of the License at
  7 |  *
  8 |  * http://www.apache.org/licenses/LICENSE-2.0
  9 |  *
 10 |  * Unless required by applicable law or agreed to in writing, software
 11 |  * distributed under the License is distributed on an "AS IS" BASIS,
 12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 |  * See the License for the specific language governing permissions and
 14 |  * limitations under the License.
 15 |  */
 16 | 
 17 | import { test, expect } from './fixtures.js';
 18 | import fs from 'fs/promises';
 19 | 
 20 | test('browser_file_upload', async ({ client, server }, testInfo) => {
 21 |   server.setContent('/', `
 22 |     <input type="file" />
 23 |     <button>Button</button>
 24 |   `, 'text/html');
 25 | 
 26 |   expect(await client.callTool({
 27 |     name: 'browser_navigate',
 28 |     arguments: { url: server.PREFIX },
 29 |   })).toContainTextContent(`
 30 | \`\`\`yaml
 31 | - generic [active] [ref=e1]:
 32 |   - button "Choose File" [ref=e2]
 33 |   - button "Button" [ref=e3]
 34 | \`\`\``);
 35 | 
 36 |   {
 37 |     expect(await client.callTool({
 38 |       name: 'browser_file_upload',
 39 |       arguments: { paths: [] },
 40 |     })).toHaveTextContent(`
 41 | The tool "browser_file_upload" can only be used when there is related modal state present.
 42 | ### Modal state
 43 | - There is no modal state present
 44 |       `.trim());
 45 |   }
 46 | 
 47 |   expect(await client.callTool({
 48 |     name: 'browser_click',
 49 |     arguments: {
 50 |       element: 'Textbox',
 51 |       ref: 'e2',
 52 |     },
 53 |   })).toContainTextContent(`### Modal state
 54 | - [File chooser]: can be handled by the "browser_file_upload" tool`);
 55 | 
 56 |   const filePath = testInfo.outputPath('test.txt');
 57 |   await fs.writeFile(filePath, 'Hello, world!');
 58 | 
 59 |   {
 60 |     const response = await client.callTool({
 61 |       name: 'browser_file_upload',
 62 |       arguments: {
 63 |         paths: [filePath],
 64 |       },
 65 |     });
 66 | 
 67 |     expect(response).not.toContainTextContent('### Modal state');
 68 |   }
 69 | 
 70 |   {
 71 |     const response = await client.callTool({
 72 |       name: 'browser_click',
 73 |       arguments: {
 74 |         element: 'Textbox',
 75 |         ref: 'e2',
 76 |       },
 77 |     });
 78 | 
 79 |     expect(response).toContainTextContent('- [File chooser]: can be handled by the \"browser_file_upload\" tool');
 80 |   }
 81 | 
 82 |   {
 83 |     const response = await client.callTool({
 84 |       name: 'browser_click',
 85 |       arguments: {
 86 |         element: 'Button',
 87 |         ref: 'e3',
 88 |       },
 89 |     });
 90 | 
 91 |     expect(response).toContainTextContent(`Tool "browser_click" does not handle the modal state.
 92 | ### Modal state
 93 | - [File chooser]: can be handled by the "browser_file_upload" tool`);
 94 |   }
 95 | });
 96 | 
 97 | test('clicking on download link emits download', async ({ startClient, server, mcpMode }, testInfo) => {
 98 |   const { client } = await startClient({
 99 |     config: { outputDir: testInfo.outputPath('output') },
100 |   });
101 | 
102 |   server.setContent('/', `<a href="/download" download="test.txt">Download</a>`, 'text/html');
103 |   server.setContent('/download', 'Data', 'text/plain');
104 | 
105 |   expect(await client.callTool({
106 |     name: 'browser_navigate',
107 |     arguments: { url: server.PREFIX },
108 |   })).toContainTextContent('- link "Download" [ref=e2]');
109 |   await client.callTool({
110 |     name: 'browser_click',
111 |     arguments: {
112 |       element: 'Download link',
113 |       ref: 'e2',
114 |     },
115 |   });
116 |   await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent(`
117 | ### Downloads
118 | - Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`);
119 | });
120 | 
121 | test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => {
122 |   const { client } = await startClient({
123 |     config: { outputDir: testInfo.outputPath('output') },
124 |   });
125 | 
126 |   test.skip(mcpBrowser === 'webkit' && process.platform === 'linux', 'https://github.com/microsoft/playwright/blob/8e08fdb52c27bb75de9bf87627bf740fadab2122/tests/library/download.spec.ts#L436');
127 |   server.route('/download', (req, res) => {
128 |     res.writeHead(200, {
129 |       'Content-Type': 'text/plain',
130 |       'Content-Disposition': 'attachment; filename=test.txt',
131 |     });
132 |     res.end('Hello world!');
133 |   });
134 | 
135 |   expect(await client.callTool({
136 |     name: 'browser_navigate',
137 |     arguments: {
138 |       url: server.PREFIX + 'download',
139 |     },
140 |   })).toContainTextContent('### Downloads');
141 | });
142 | 
```
Page 1/3FirstPrevNextLast