#
tokens: 48023/50000 60/67 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/justasmonkev/mcp-accessibility-scanner?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── .idea
│   ├── .gitignore
│   ├── copilot.data.migration.agent.xml
│   ├── copilot.data.migration.edit.xml
│   ├── modules.xml
│   ├── New folder (3).iml
│   └── vcs.xml
├── cli.js
├── config.d.ts
├── eslint.config.mjs
├── glama.json
├── index.d.ts
├── index.js
├── LICENSE
├── NOTICE.md
├── package-lock.json
├── package.json
├── README.md
├── server.json
├── src
│   ├── actions.d.ts
│   ├── browserContextFactory.ts
│   ├── browserServerBackend.ts
│   ├── config.ts
│   ├── context.ts
│   ├── DEPS.list
│   ├── extension
│   │   ├── cdpRelay.ts
│   │   ├── DEPS.list
│   │   ├── extensionContextFactory.ts
│   │   └── protocol.ts
│   ├── external-modules.d.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── DEPS.list
│   │   ├── http.ts
│   │   ├── inProcessTransport.ts
│   │   ├── manualPromise.ts
│   │   ├── mdb.ts
│   │   ├── proxyBackend.ts
│   │   ├── README.md
│   │   ├── server.ts
│   │   └── tool.ts
│   ├── program.ts
│   ├── response.ts
│   ├── sessionLog.ts
│   ├── tab.ts
│   ├── tools
│   │   ├── common.ts
│   │   ├── console.ts
│   │   ├── DEPS.list
│   │   ├── dialogs.ts
│   │   ├── evaluate.ts
│   │   ├── files.ts
│   │   ├── form.ts
│   │   ├── install.ts
│   │   ├── keyboard.ts
│   │   ├── mouse.ts
│   │   ├── navigate.ts
│   │   ├── network.ts
│   │   ├── pdf.ts
│   │   ├── screenshot.ts
│   │   ├── snapshot.ts
│   │   ├── tabs.ts
│   │   ├── tool.ts
│   │   ├── utils.ts
│   │   ├── verify.ts
│   │   └── wait.ts
│   ├── tools.ts
│   ├── utils
│   │   ├── codegen.ts
│   │   ├── fileUtils.ts
│   │   ├── guid.ts
│   │   ├── log.ts
│   │   └── package.ts
│   └── vscode
│       ├── DEPS.list
│       ├── host.ts
│       └── main.ts
├── tsconfig.all.json
└── tsconfig.json
```

# Files

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

```
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | 
```

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

```
 1 | /tmp
 2 | /out-tsc
 3 | 
 4 | /node_modules
 5 | npm-debug.log*
 6 | yarn-debug.log*
 7 | yarn-error.log*
 8 | /.pnp
 9 | .pnp.js
10 | 
11 | .vscode/*
12 | 
13 | # Playwright
14 | node_modules/
15 | /test-results/
16 | /playwright-report/
17 | /blob-report/
18 | /playwright/.cache/
19 | build/
20 | lib/
21 | 
```

--------------------------------------------------------------------------------
/src/mcp/README.md:
--------------------------------------------------------------------------------

```markdown
1 | - Generic MCP utils, no dependencies on anything.
2 | 
```

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

```markdown
  1 | 
  2 | # MCP Accessibility Scanner 🔍
  3 | 
  4 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/justasmonkev-mcp-accessibility-scanner-badge.png)](https://mseep.ai/app/justasmonkev-mcp-accessibility-scanner)
  5 | 
  6 | A Model Context Protocol (MCP) server that provides automated web accessibility scanning using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks, capture annotated screenshots, and generate detailed accessibility reports.
  7 | A powerful Model Context Protocol (MCP) server that provides automated web accessibility scanning and browser automation using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks, interact with web pages, manage persistent browser sessions, and generate detailed accessibility reports with visual annotations.
  8 | 
  9 | ## Features
 10 | 
 11 | ### Accessibility Scanning
 12 | ✅ Full WCAG 2.0/2.1/2.2 compliance checking (A, AA, AAA levels)  
 13 | 🖼️ Automatic screenshot capture with violation highlighting  
 14 | 📄 Detailed JSON reports with remediation guidance  
 15 | 🎯 Support for specific violation categories (color contrast, ARIA, forms, keyboard navigation, etc.)  
 16 | 
 17 | ### Browser Automation
 18 | 🖱️ Click, hover, and drag elements using accessibility snapshots  
 19 | ⌨️ Type text and handle keyboard inputs  
 20 | 🔍 Capture page snapshots to discover all interactive elements  
 21 | 📸 Take screenshots and save PDFs  
 22 | 🎯 Support for both element-based and coordinate-based interactions  
 23 | 
 24 | ### Advanced Features
 25 | 📑 Tab management for multi-page workflows  
 26 | 🌐 Monitor console messages and network requests  
 27 | ⏱️ Wait for dynamic content to load  
 28 | 📁 Handle file uploads and browser dialogs  
 29 | 🔄 Navigate through browser history
 30 | 
 31 | ## Installation
 32 | 
 33 | You can install the package using any of these methods:
 34 | 
 35 | Using npm:
 36 | ```bash
 37 | npm install -g mcp-accessibility-scanner
 38 | ```
 39 | 
 40 | ### Installation in VS Code
 41 | 
 42 | Install the Accessibility Scanner in VS Code using the VS Code CLI:
 43 | 
 44 | For VS Code:
 45 | ```bash
 46 | code --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["mcp-accessibility-scanner"]}'
 47 | ```
 48 | 
 49 | For VS Code Insiders:
 50 | ```bash
 51 | code-insiders --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["mcp-accessibility-scanner"]}'
 52 | ```
 53 | 
 54 | ## Configuration
 55 | 
 56 | Here's the Claude Desktop configuration:
 57 | 
 58 | ```json
 59 | {
 60 |   "mcpServers": {
 61 |     "accessibility-scanner": {
 62 |       "command": "npx",
 63 |       "args": ["-y", "mcp-accessibility-scanner"]
 64 |     }
 65 |   }
 66 | }
 67 | ```
 68 | 
 69 | ### Advanced Configuration
 70 | 
 71 | You can pass a configuration file to customize Playwright behavior:
 72 | 
 73 | ```json
 74 | {
 75 |   "mcpServers": {
 76 |     "accessibility-scanner": {
 77 |       "command": "npx",
 78 |       "args": ["-y", "mcp-accessibility-scanner", "--config", "/path/to/config.json"]
 79 |     }
 80 |   }
 81 | }
 82 | ```
 83 | 
 84 | #### Configuration Options
 85 | 
 86 | Create a `config.json` file with the following options:
 87 | 
 88 | ```json
 89 | {
 90 |   "browser": {
 91 |     "browserName": "chromium",
 92 |     "launchOptions": {
 93 |       "headless": true,
 94 |       "channel": "chrome"
 95 |     }
 96 |   },
 97 |   "timeouts": {
 98 |     "navigationTimeout": 60000,
 99 |     "defaultTimeout": 5000
100 |   },
101 |   "network": {
102 |     "allowedOrigins": ["example.com", "trusted-site.com"],
103 |     "blockedOrigins": ["ads.example.com"]
104 |   }
105 | }
106 | ```
107 | 
108 | **Available Options:**
109 | 
110 | - `browser.browserName`: Browser to use (`chromium`, `firefox`, `webkit`)
111 | - `browser.launchOptions.headless`: Run browser in headless mode (default: `true` on Linux without display, `false` otherwise)
112 | - `browser.launchOptions.channel`: Browser channel (`chrome`, `chrome-beta`, `msedge`, etc.)
113 | - `timeouts.navigationTimeout`: Maximum time for page navigation in milliseconds (default: `60000`)
114 | - `timeouts.defaultTimeout`: Default timeout for Playwright operations in milliseconds (default: `5000`)
115 | - `network.allowedOrigins`: List of origins to allow (blocks all others if specified)
116 | - `network.blockedOrigins`: List of origins to block
117 | 
118 | ## Available Tools
119 | 
120 | The MCP server provides comprehensive browser automation and accessibility scanning tools:
121 | 
122 | ### Core Accessibility Tool
123 | 
124 | #### `scan_page`
125 | Performs a comprehensive accessibility scan on the current page using Axe-core.
126 | 
127 | **Parameters:**
128 | - `violationsTag`: Array of WCAG/violation tags to check
129 | 
130 | **Supported Violation Tags:**
131 | - WCAG standards: `wcag2a`, `wcag2aa`, `wcag2aaa`, `wcag21a`, `wcag21aa`, `wcag21aaa`, `wcag22a`, `wcag22aa`, `wcag22aaa`
132 | - Section 508: `section508`
133 | - Categories: `cat.aria`, `cat.color`, `cat.forms`, `cat.keyboard`, `cat.language`, `cat.name-role-value`, `cat.parsing`, `cat.semantics`, `cat.sensory-and-visual-cues`, `cat.structure`, `cat.tables`, `cat.text-alternatives`, `cat.time-and-media`
134 | 
135 | ### Navigation Tools
136 | 
137 | #### `browser_navigate`
138 | Navigate to a URL.
139 | - Parameters: `url` (string)
140 | 
141 | #### `browser_navigate_back`
142 | Go back to the previous page.
143 | 
144 | #### `browser_navigate_forward`
145 | Go forward to the next page.
146 | 
147 | ### Page Interaction Tools
148 | 
149 | #### `browser_snapshot`
150 | Capture accessibility snapshot of the current page (better than screenshot for analysis).
151 | 
152 | #### `browser_click`
153 | Perform click on a web page element.
154 | - Parameters: `element` (description), `ref` (element reference), `doubleClick` (optional)
155 | 
156 | #### `browser_type`
157 | Type text into editable element.
158 | - Parameters: `element`, `ref`, `text`, `submit` (optional), `slowly` (optional)
159 | 
160 | #### `browser_hover`
161 | Hover over element on page.
162 | - Parameters: `element`, `ref`
163 | 
164 | #### `browser_drag`
165 | Perform drag and drop between two elements.
166 | - Parameters: `startElement`, `startRef`, `endElement`, `endRef`
167 | 
168 | #### `browser_select_option`
169 | Select an option in a dropdown.
170 | - Parameters: `element`, `ref`, `values` (array)
171 | 
172 | #### `browser_press_key`
173 | Press a key on the keyboard.
174 | - Parameters: `key` (e.g., 'ArrowLeft' or 'a')
175 | 
176 | ### Screenshot & Visual Tools
177 | 
178 | #### `browser_take_screenshot`
179 | Take a screenshot of the current page.
180 | - Parameters: `raw` (optional), `filename` (optional), `element` (optional), `ref` (optional)
181 | 
182 | #### `browser_pdf_save`
183 | Save page as PDF.
184 | - Parameters: `filename` (optional, defaults to `page-{timestamp}.pdf`)
185 | 
186 | ### Browser Management
187 | 
188 | #### `browser_close`
189 | Close the page.
190 | 
191 | #### `browser_resize`
192 | Resize the browser window.
193 | - Parameters: `width`, `height`
194 | 
195 | ### Tab Management
196 | 
197 | #### `browser_tab_list`
198 | List all open browser tabs.
199 | 
200 | #### `browser_tab_new`
201 | Open a new tab.
202 | - Parameters: `url` (optional)
203 | 
204 | #### `browser_tab_select`
205 | Select a tab by index.
206 | - Parameters: `index`
207 | 
208 | #### `browser_tab_close`
209 | Close a tab.
210 | - Parameters: `index` (optional, closes current tab if not provided)
211 | 
212 | ### Information & Monitoring Tools
213 | 
214 | #### `browser_console_messages`
215 | Returns all console messages from the page.
216 | 
217 | #### `browser_network_requests`
218 | Returns all network requests since loading the page.
219 | 
220 | ### Utility Tools
221 | 
222 | #### `browser_wait_for`
223 | Wait for text to appear/disappear or time to pass.
224 | - Parameters: `time` (optional), `text` (optional), `textGone` (optional)
225 | 
226 | #### `browser_handle_dialog`
227 | Handle browser dialogs (alerts, confirms, prompts).
228 | - Parameters: `accept` (boolean), `promptText` (optional)
229 | 
230 | #### `browser_file_upload`
231 | Upload files to the page.
232 | - Parameters: `paths` (array of absolute file paths)
233 | 
234 | ### Vision Mode Tools (Coordinate-based Interaction)
235 | 
236 | #### `browser_screen_capture`
237 | Take a screenshot for coordinate-based interaction.
238 | 
239 | #### `browser_screen_move_mouse`
240 | Move mouse to specific coordinates.
241 | - Parameters: `element`, `x`, `y`
242 | 
243 | #### `browser_screen_click`
244 | Click at specific coordinates.
245 | - Parameters: `element`, `x`, `y`
246 | 
247 | #### `browser_screen_drag`
248 | Drag from one coordinate to another.
249 | - Parameters: `element`, `startX`, `startY`, `endX`, `endY`
250 | 
251 | #### `browser_screen_type`
252 | Type text (coordinate-independent).
253 | - Parameters: `text`, `submit` (optional)
254 | 
255 | ## Usage Examples
256 | 
257 | ### Basic Accessibility Scan
258 | ```
259 | 1. Navigate to example.com using browser_navigate
260 | 2. Run scan_page with violationsTag: ["wcag21aa"]
261 | ```
262 | 
263 | ### Color Contrast Check
264 | ```
265 | 1. Use browser_navigate to go to example.com
266 | 2. Run scan_page with violationsTag: ["cat.color"]
267 | ```
268 | 
269 | ### Multi-step Workflow
270 | ```
271 | 1. Navigate to example.com with browser_navigate
272 | 2. Take a browser_snapshot to see available elements
273 | 3. Click the "Sign In" button using browser_click
274 | 4. Type "[email protected]" using browser_type
275 | 5. Run scan_page on the login page
276 | 6. Take a browser_take_screenshot to capture the final state
277 | ```
278 | 
279 | ### Page Analysis
280 | ```
281 | 1. Navigate to example.com
282 | 2. Use browser_snapshot to capture all interactive elements
283 | 3. Review console messages with browser_console_messages
284 | 4. Check network activity with browser_network_requests
285 | ```
286 | 
287 | ### Tab Management
288 | ```
289 | 1. Open a new tab with browser_tab_new
290 | 2. Navigate to different pages in each tab
291 | 3. Switch between tabs using browser_tab_select
292 | 4. List all tabs with browser_tab_list
293 | ```
294 | 
295 | ### Waiting for Dynamic Content
296 | ```
297 | 1. Navigate to a page
298 | 2. Use browser_wait_for to wait for specific text to appear
299 | 3. Interact with the dynamically loaded content
300 | ```
301 | 
302 | **Note:** Most interaction tools require element references from browser_snapshot. Always capture a snapshot before attempting to interact with page elements.
303 | 
304 | ## Development
305 | 
306 | Clone and set up the project:
307 | ```bash
308 | git clone https://github.com/JustasMonkev/mcp-accessibility-scanner.git
309 | cd mcp-accessibility-scanner
310 | npm install
311 | ```
312 | 
313 | ## License
314 | 
315 | MIT
316 | 
317 | 
```

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

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

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
1 | {
2 | 	"$schema": "https://glama.ai/mcp/schemas/server.json",
3 | 	"maintainers": ["JustasMonkev"]
4 | }
5 | 
```

--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="VcsDirectoryMappings">
4 |     <mapping directory="$PROJECT_DIR$" vcs="Git" />
5 |   </component>
6 | </project>
```

--------------------------------------------------------------------------------
/.idea/copilot.data.migration.edit.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="EditMigrationStateService">
4 |     <option name="migrationStatus" value="COMPLETED" />
5 |   </component>
6 | </project>
```

--------------------------------------------------------------------------------
/.idea/copilot.data.migration.agent.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="AgentMigrationStateService">
4 |     <option name="migrationStatus" value="COMPLETED" />
5 |   </component>
6 | </project>
```

--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="ProjectModuleManager">
4 |     <modules>
5 |       <module fileurl="file://$PROJECT_DIR$/.idea/New folder (3).iml" filepath="$PROJECT_DIR$/.idea/New folder (3).iml" />
6 |     </modules>
7 |   </component>
8 | </project>
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     // --- Module & JS Target ---
 4 |     "target": "ESNext",
 5 |     "module": "NodeNext",
 6 |     "moduleResolution": "NodeNext",
 7 | 
 8 |     // --- Output ---
 9 |     "rootDir": "src",
10 |     "outDir": "./lib",
11 | 
12 |     // --- Developer Experience & Interop ---
13 |     "strict": true,
14 |     "sourceMap": true, // <-- ADDED: Crucial for debugging!
15 |     "esModuleInterop": true,
16 |     "resolveJsonModule": true
17 |   },
18 |   "include": [
19 |     "src"
20 |   ]
21 | }
22 | 
```

--------------------------------------------------------------------------------
/NOTICE.md:
--------------------------------------------------------------------------------

```markdown
 1 | # NOTICE
 2 | 
 3 | ## mcp-accessibility-scanner
 4 | 
 5 | Copyright (c) 2024 Justas Monkev
 6 | 
 7 | This project is licensed under the MIT License.
 8 | 
 9 | ## Third-Party Code Attribution
10 | 
11 | This project includes code adapted from:
12 | 
13 | ### Microsoft Playwright MCP
14 | - Copyright (c) Microsoft Corporation
15 | - Licensed under the Apache License, Version 2.0
16 | - Original source: https://github.com/microsoft/playwright-mcp
17 | 
18 | Adapted code has been modified to:
19 | - Convert from ESM to CommonJS module format
20 | - Integrate with axe-core accessibility scanning
21 | - Work with existing Playwright instances rather than spawning new ones
22 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
 3 |   "name": "io.github.JustasMonkev/mcp-accessibility-scanner",
 4 |   "description": "MCP server for automated web accessibility scanning with Playwright and Axe-core.",
 5 |   "status": "active",
 6 |   "repository": {
 7 |     "url": "https://github.com/JustasMonkev/mcp-accessibility-scanner",
 8 |     "source": "github"
 9 |   },
10 |   "version": "1.1.1",
11 |   "packages": [
12 |     {
13 |       "registry_type": "npm",
14 |       "registry_base_url": "https://registry.npmjs.org",
15 |       "identifier": "mcp-accessibility-scanner",
16 |       "version": "1.1.1",
17 |       "transport": { "type": "stdio" },
18 |       "environment_variables": []
19 |     }
20 |   ]
21 | }
22 | 
```

--------------------------------------------------------------------------------
/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/utils/log.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 debug from 'debug';
18 | 
19 | const errorsDebug = debug('pw:mcp:errors');
20 | 
21 | export function logUnhandledError(error: unknown) {
22 |   errorsDebug(error);
23 | }
24 | 
25 | export const testDebug = debug('pw:mcp:test');
26 | 
```

--------------------------------------------------------------------------------
/src/utils/guid.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 crypto from 'crypto';
18 | 
19 | export function createGuid(): string {
20 |   return crypto.randomBytes(16).toString('hex');
21 | }
22 | 
23 | export function createHash(data: string): string {
24 |   return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
25 | }
26 | 
```

--------------------------------------------------------------------------------
/src/utils/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 'fs';
18 | import path from 'path';
19 | import url from 'url';
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 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool } from './tool.js';
19 | 
20 | const console = defineTabTool({
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 (tab, params, response) => {
30 |     tab.consoleMessages().map(message => response.addResult(message.toString()));
31 |   },
32 | });
33 | 
34 | export default [
35 |   console,
36 | ];
37 | 
```

--------------------------------------------------------------------------------
/src/extension/protocol.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 | // Whenever the commands/events change, the version must be updated. The latest
18 | // extension version should be compatible with the old MCP clients.
19 | export const VERSION = 1;
20 | 
21 | export type ExtensionCommand = {
22 |   'attachToTab': {
23 |     params: {};
24 |   };
25 |   'forwardCDPCommand': {
26 |     params: {
27 |       method: string,
28 |       sessionId?: string
29 |       params?: any,
30 |     };
31 |   };
32 | };
33 | 
34 | export type ExtensionEvents = {
35 |   'forwardCDPEvent': {
36 |     params: {
37 |       method: string,
38 |       sessionId?: string
39 |       params?: any,
40 |     };
41 |   };
42 | };
43 | 
```

--------------------------------------------------------------------------------
/src/mcp/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 { zodToJsonSchema } from 'zod-to-json-schema';
18 | 
19 | import type { z } from 'zod';
20 | import type * as mcpServer from './server.js';
21 | 
22 | export type ToolSchema<Input extends z.Schema> = {
23 |   name: string;
24 |   title: string;
25 |   description: string;
26 |   inputSchema: Input;
27 |   type: 'readOnly' | 'destructive';
28 | };
29 | 
30 | export function toMcpTool(tool: ToolSchema<any>): mcpServer.Tool {
31 |   return {
32 |     name: tool.name,
33 |     description: tool.description,
34 |     inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'],
35 |     annotations: {
36 |       title: tool.title,
37 |       readOnlyHint: tool.type === 'readOnly',
38 |       destructiveHint: tool.type === 'destructive',
39 |       openWorldHint: true,
40 |     },
41 |   };
42 | }
43 | 
44 | export function defineToolSchema<Input extends z.Schema>(tool: ToolSchema<Input>): ToolSchema<Input> {
45 |   return tool;
46 | }
47 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool } from './tool.js';
19 | 
20 | import * as javascript from '../utils/codegen.js';
21 | 
22 | const pdfSchema = z.object({
23 |   filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
24 | });
25 | 
26 | const pdf = defineTabTool({
27 |   capability: 'pdf',
28 | 
29 |   schema: {
30 |     name: 'browser_pdf_save',
31 |     title: 'Save as PDF',
32 |     description: 'Save page as PDF',
33 |     inputSchema: pdfSchema,
34 |     type: 'readOnly',
35 |   },
36 | 
37 |   handle: async (tab, params, response) => {
38 |     const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`);
39 |     response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
40 |     response.addResult(`Saved page as ${fileName}`);
41 |     await tab.page.pdf({ path: fileName });
42 |   },
43 | });
44 | 
45 | export default [
46 |   pdf,
47 | ];
48 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool } from './tool.js';
19 | 
20 | import type * as playwright from 'playwright';
21 | 
22 | const requests = defineTabTool({
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 (tab, params, response) => {
34 |     const requests = tab.requests();
35 |     [...requests.entries()].forEach(([req, res]) => response.addResult(renderRequest(req, res)));
36 |   },
37 | });
38 | 
39 | function renderRequest(request: playwright.Request, response: playwright.Response | null) {
40 |   const result: string[] = [];
41 |   result.push(`[${request.method().toUpperCase()}] ${request.url()}`);
42 |   if (response)
43 |     result.push(`=> [${response.status()}] ${response.statusText()}`);
44 |   return result.join(' ');
45 | }
46 | 
47 | export default [
48 |   requests,
49 | ];
50 | 
```

--------------------------------------------------------------------------------
/src/utils/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 | export function cacheDir() {
21 |   let cacheDirectory: string;
22 |   if (process.platform === 'linux')
23 |     cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
24 |   else if (process.platform === 'darwin')
25 |     cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
26 |   else if (process.platform === 'win32')
27 |     cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
28 |   else
29 |     throw new Error('Unsupported platform: ' + process.platform);
30 |   return path.join(cacheDirectory, 'ms-playwright');
31 | }
32 | 
33 | export function sanitizeForFilePath(s: string) {
34 |   const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
35 |   const separator = s.lastIndexOf('.');
36 |   if (separator === -1)
37 |     return sanitize(s);
38 |   return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
39 | }
40 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool } from './tool.js';
19 | 
20 | const uploadFile = defineTabTool({
21 |   capability: 'core',
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 (tab, params, response) => {
34 |     response.setIncludeSnapshot();
35 | 
36 |     const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
37 |     if (!modalState)
38 |       throw new Error('No file chooser visible');
39 | 
40 |     response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
41 | 
42 |     tab.clearModalState(modalState);
43 |     await tab.waitForCompletion(async () => {
44 |       await modalState.fileChooser.setFiles(params.paths);
45 |     });
46 |   },
47 |   clearsModalState: 'fileChooser',
48 | });
49 | 
50 | export default [
51 |   uploadFile,
52 | ];
53 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool } from './tool.js';
19 | 
20 | const handleDialog = defineTabTool({
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.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 (tab, params, response) => {
35 |     response.setIncludeSnapshot();
36 | 
37 |     const dialogState = tab.modalStates().find(state => state.type === 'dialog');
38 |     if (!dialogState)
39 |       throw new Error('No dialog visible');
40 | 
41 |     tab.clearModalState(dialogState);
42 |     await tab.waitForCompletion(async () => {
43 |       if (params.accept)
44 |         await dialogState.dialog.accept(params.promptText);
45 |       else
46 |         await dialogState.dialog.dismiss();
47 |     });
48 |   },
49 | 
50 |   clearsModalState: 'dialog',
51 | });
52 | 
53 | export default [
54 |   handleDialog,
55 | ];
56 | 
```

--------------------------------------------------------------------------------
/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, defineTabTool } from './tool.js';
19 | 
20 | const navigate = 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, response) => {
34 |     const tab = await context.ensureTab();
35 |     await tab.navigate(params.url);
36 | 
37 |     response.setIncludeSnapshot();
38 |     response.addCode(`await page.goto('${params.url}');`);
39 |   },
40 | });
41 | 
42 | const goBack = defineTabTool({
43 |   capability: 'core',
44 |   schema: {
45 |     name: 'browser_navigate_back',
46 |     title: 'Go back',
47 |     description: 'Go back to the previous page',
48 |     inputSchema: z.object({}),
49 |     type: 'readOnly',
50 |   },
51 | 
52 |   handle: async (tab, params, response) => {
53 |     await tab.page.goBack();
54 |     response.setIncludeSnapshot();
55 |     response.addCode(`await page.goBack();`);
56 |   },
57 | });
58 | 
59 | export default [
60 |   navigate,
61 |   goBack,
62 | ];
63 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool, defineTool } from './tool.js';
19 | 
20 | const close = defineTool({
21 |   capability: 'core',
22 |   schema: {
23 |     name: 'browser_close',
24 |     title: 'Close browser',
25 |     description: 'Close the page',
26 |     inputSchema: z.object({}),
27 |     type: 'readOnly',
28 |   },
29 | 
30 |   handle: async (context, params, response) => {
31 |     await context.closeBrowserContext();
32 |     response.setIncludeTabs();
33 |     response.addCode(`await page.close()`);
34 |   },
35 | });
36 | 
37 | const resize = defineTabTool({
38 |   capability: 'core',
39 |   schema: {
40 |     name: 'browser_resize',
41 |     title: 'Resize browser window',
42 |     description: 'Resize the browser window',
43 |     inputSchema: z.object({
44 |       width: z.number().describe('Width of the browser window'),
45 |       height: z.number().describe('Height of the browser window'),
46 |     }),
47 |     type: 'readOnly',
48 |   },
49 | 
50 |   handle: async (tab, params, response) => {
51 |     response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
52 | 
53 |     await tab.waitForCompletion(async () => {
54 |       await tab.page.setViewportSize({ width: params.width, height: params.height });
55 |     });
56 |   },
57 | });
58 | 
59 | export default [
60 |   close,
61 |   resize
62 | ];
63 | 
```

--------------------------------------------------------------------------------
/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 evaluate from './tools/evaluate.js';
21 | import files from './tools/files.js';
22 | import form from './tools/form.js';
23 | import install from './tools/install.js';
24 | import keyboard from './tools/keyboard.js';
25 | import mouse from './tools/mouse.js';
26 | import navigate from './tools/navigate.js';
27 | import network from './tools/network.js';
28 | import pdf from './tools/pdf.js';
29 | import snapshot from './tools/snapshot.js';
30 | import tabs from './tools/tabs.js';
31 | import screenshot from './tools/screenshot.js';
32 | import wait from './tools/wait.js';
33 | import verify from './tools/verify.js';
34 | 
35 | import type { Tool } from './tools/tool.js';
36 | import type { FullConfig } from './config.js';
37 | 
38 | export const allTools: Tool<any>[] = [
39 |   ...common,
40 |   ...console,
41 |   ...dialogs,
42 |   ...evaluate,
43 |   ...files,
44 |   ...form,
45 |   ...install,
46 |   ...keyboard,
47 |   ...navigate,
48 |   ...network,
49 |   ...mouse,
50 |   ...pdf,
51 |   ...screenshot,
52 |   ...snapshot,
53 |   ...tabs,
54 |   ...wait,
55 |   ...verify,
56 | ];
57 | 
58 | export function filteredTools(config: FullConfig) {
59 |   return allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
60 | }
61 | 
```

--------------------------------------------------------------------------------
/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 | import { fileURLToPath } from 'url';
20 | import { z } from 'zod';
21 | import { defineTool } from './tool.js';
22 | 
23 | 
24 | const install = defineTool({
25 |   capability: 'core-install',
26 |   schema: {
27 |     name: 'browser_install',
28 |     title: 'Install the browser specified in the config',
29 |     description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
30 |     inputSchema: z.object({}),
31 |     type: 'destructive',
32 |   },
33 | 
34 |   handle: async (context, params, response) => {
35 |     const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
36 |     const cliUrl = import.meta.resolve('playwright/package.json');
37 |     const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
38 |     const child = fork(cliPath, ['install', channel], {
39 |       stdio: 'pipe',
40 |     });
41 |     const output: string[] = [];
42 |     child.stdout?.on('data', data => output.push(data.toString()));
43 |     child.stderr?.on('data', data => output.push(data.toString()));
44 |     await new Promise<void>((resolve, reject) => {
45 |       child.on('close', code => {
46 |         if (code === 0)
47 |           resolve();
48 |         else
49 |           reject(new Error(`Failed to install browser: ${output.join('')}`));
50 |       });
51 |     });
52 |     response.setIncludeTabs();
53 |   },
54 | });
55 | 
56 | export default [
57 |   install,
58 | ];
59 | 
```

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

```json
 1 | {
 2 | 	"name": "mcp-accessibility-scanner",
 3 | 	"version": "2.0.1",
 4 | 	"mcpName": "io.github.JustasMonkev/mcp-accessibility-scanner",
 5 | 	"description": "A Model Context Protocol (MCP) server for performing automated accessibility scans of web pages using Playwright and Axe-core",
 6 | 	"type": "module",
 7 | 	"exports": {
 8 | 		"./package.json": "./package.json",
 9 | 		".": {
10 | 			"types": "./index.d.ts",
11 | 			"default": "./index.js"
12 | 		}
13 | 	},
14 | 	"bin": {
15 | 		"mcp-server-playwright": "cli.js"
16 | 	},
17 | 	"files": [
18 | 		"lib/**/*",
19 | 		"README.md",
20 | 		"LICENSE",
21 | 		"NOTICE.md"
22 | 	],
23 | 	"scripts": {
24 | 		"build": "tsc --project tsconfig.json",
25 | 		"lint": "npm run eslint . && tsc --noEmit",
26 | 		"watch": "tsc --watch",
27 | 		"run-server": "node lib/browserServer.js",
28 | 		"clean": "rm -rf lib",
29 | 		"npm-publish": "npm run clean && npm run build && npm publish"
30 | 	},
31 | 	"dependencies": {
32 | 		"@axe-core/playwright": "^4.10.2",
33 | 		"@modelcontextprotocol/sdk": "^1.18.1",
34 | 		"commander": "^14.0.1",
35 | 		"debug": "^4.4.3",
36 | 		"dotenv": "^17.2.2",
37 | 		"mime": "^4.1.0",
38 | 		"playwright": "^1.55.0",
39 | 		"playwright-core": "^1.55.0",
40 | 		"ws": "^8.18.3",
41 | 		"zod-to-json-schema": "^3.24.6"
42 | 	},
43 | 	"devDependencies": {
44 | 		"@eslint/eslintrc": "^3.3.1",
45 | 		"@eslint/js": "^9.36.0",
46 | 		"@playwright/test": "^1.55.0",
47 | 		"@stylistic/eslint-plugin": "^5.3.1",
48 | 		"@types/chrome": "^0.1.12",
49 | 		"@types/debug": "^4.1.12",
50 | 		"@types/node": "^24.5.2",
51 | 		"@types/ws": "^8.18.1",
52 | 		"@typescript-eslint/eslint-plugin": "^8.44.0",
53 | 		"@typescript-eslint/parser": "^8.44.0",
54 | 		"@typescript-eslint/utils": "^8.44.0",
55 | 		"eslint": "^9.35.0",
56 | 		"eslint-plugin-import": "^2.32.0",
57 | 		"eslint-plugin-notice": "^1.0.0",
58 | 		"ts-node": "^10.9.2",
59 | 		"typescript": "^5.9.2"
60 | 	},
61 | 	"keywords": [
62 | 		"mcp",
63 | 		"accessibility",
64 | 		"a11y",
65 | 		"wcag",
66 | 		"axe-core",
67 | 		"playwright",
68 | 		"claude",
69 | 		"model-context-protocol"
70 | 	],
71 | 	"author": "",
72 | 	"license": "MIT",
73 | 	"repository": {
74 | 		"type": "git",
75 | 		"url": "git+https://github.com/JustasMonkev/mcp-accessibility-scanner.git"
76 | 	},
77 | 	"engines": {
78 | 		"node": ">=16.0.0"
79 | 	},
80 | 	"publishConfig": {
81 | 		"access": "public"
82 | 	}
83 | }
84 | 
```

--------------------------------------------------------------------------------
/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 { BrowserServerBackend } from './browserServerBackend.js';
18 | import { resolveConfig } from './config.js';
19 | import { contextFactory } from './browserContextFactory.js';
20 | import * as mcpServer from './mcp/server.js';
21 | import { packageJSON } from './utils/package.js';
22 | 
23 | import type { Config } from '../config.js';
24 | import type { BrowserContext } from 'playwright';
25 | import type { BrowserContextFactory } from './browserContextFactory.js';
26 | import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
27 | 
28 | export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Server> {
29 |   const config = await resolveConfig(userConfig);
30 |   const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
31 |   return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false);
32 | }
33 | 
34 | class SimpleBrowserContextFactory implements BrowserContextFactory {
35 |   name = 'custom';
36 |   description = 'Connect to a browser using a custom context getter';
37 | 
38 |   private readonly _contextGetter: () => Promise<BrowserContext>;
39 | 
40 |   constructor(contextGetter: () => Promise<BrowserContext>) {
41 |     this._contextGetter = contextGetter;
42 |   }
43 | 
44 |   async createContext(): Promise<{ browserContext: BrowserContext, close: () => Promise<void> }> {
45 |     const browserContext = await this._contextGetter();
46 |     return {
47 |       browserContext,
48 |       close: () => browserContext.close()
49 |     };
50 |   }
51 | }
52 | 
```

--------------------------------------------------------------------------------
/src/utils/codegen.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 | 
```

--------------------------------------------------------------------------------
/src/tools/evaluate.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 { defineTabTool } from './tool.js';
20 | import * as javascript from '../utils/codegen.js';
21 | import { generateLocator } from './utils.js';
22 | 
23 | import type * as playwright from 'playwright';
24 | 
25 | const evaluateSchema = z.object({
26 |   function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
27 |   element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
28 |   ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
29 | });
30 | 
31 | const evaluate = defineTabTool({
32 |   capability: 'core',
33 |   schema: {
34 |     name: 'browser_evaluate',
35 |     title: 'Evaluate JavaScript',
36 |     description: 'Evaluate JavaScript expression on page or element',
37 |     inputSchema: evaluateSchema,
38 |     type: 'destructive',
39 |   },
40 | 
41 |   handle: async (tab, params, response) => {
42 |     response.setIncludeSnapshot();
43 | 
44 |     let locator: playwright.Locator | undefined;
45 |     if (params.ref && params.element) {
46 |       locator = await tab.refLocator({ ref: params.ref, element: params.element });
47 |       response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
48 |     } else {
49 |       response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
50 |     }
51 | 
52 |     await tab.waitForCompletion(async () => {
53 |       const receiver = locator ?? tab.page as any;
54 |       const result = await receiver._evaluateFunction(params.function);
55 |       response.addResult(JSON.stringify(result, null, 2) || 'undefined');
56 |     });
57 |   },
58 | });
59 | 
60 | export default [
61 |   evaluate,
62 | ];
63 | 
```

--------------------------------------------------------------------------------
/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 } from './tool.js';
19 | 
20 | const wait = defineTool({
21 |   capability: 'core',
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.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, response) => {
36 |     if (!params.text && !params.textGone && !params.time)
37 |       throw new Error('Either time, text or textGone must be provided');
38 | 
39 |     if (params.time) {
40 |       response.addCode(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`);
41 |       await new Promise(f => setTimeout(f, Math.min(30000, params.time! * 1000)));
42 |     }
43 | 
44 |     const tab = context.currentTabOrDie();
45 |     const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
46 |     const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
47 | 
48 |     if (goneLocator) {
49 |       response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
50 |       await goneLocator.waitFor({ state: 'hidden' });
51 |     }
52 | 
53 |     if (locator) {
54 |       response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
55 |       await locator.waitFor({ state: 'visible' });
56 |     }
57 | 
58 |     response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
59 |     response.setIncludeSnapshot();
60 |   },
61 | });
62 | 
63 | export default [
64 |   wait,
65 | ];
66 | 
```

--------------------------------------------------------------------------------
/src/tools/form.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 { defineTabTool } from './tool.js';
20 | import { generateLocator } from './utils.js';
21 | import * as javascript from '../utils/codegen.js';
22 | 
23 | const fillForm = defineTabTool({
24 |   capability: 'core',
25 | 
26 |   schema: {
27 |     name: 'browser_fill_form',
28 |     title: 'Fill form',
29 |     description: 'Fill multiple form fields',
30 |     inputSchema: z.object({
31 |       fields: z.array(z.object({
32 |         name: z.string().describe('Human-readable field name'),
33 |         type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),
34 |         ref: z.string().describe('Exact target field reference from the page snapshot'),
35 |         value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'),
36 |       })).describe('Fields to fill in'),
37 |     }),
38 |     type: 'destructive',
39 |   },
40 | 
41 |   handle: async (tab, params, response) => {
42 |     for (const field of params.fields) {
43 |       const locator = await tab.refLocator({ element: field.name, ref: field.ref });
44 |       const locatorSource = `await page.${await generateLocator(locator)}`;
45 |       if (field.type === 'textbox' || field.type === 'slider') {
46 |         await locator.fill(field.value);
47 |         response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`);
48 |       } else if (field.type === 'checkbox' || field.type === 'radio') {
49 |         await locator.setChecked(field.value === 'true');
50 |         response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`);
51 |       } else if (field.type === 'combobox') {
52 |         await locator.selectOption({ label: field.value });
53 |         response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`);
54 |       }
55 |     }
56 |   },
57 | });
58 | 
59 | export default [
60 |   fillForm,
61 | ];
62 | 
```

--------------------------------------------------------------------------------
/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 { z } from 'zod';
18 | import type { Context } from '../context.js';
19 | import type * as playwright from 'playwright';
20 | import type { ToolCapability } from '../../config.js';
21 | import type { Tab } from '../tab.js';
22 | import type { Response } from '../response.js';
23 | import type { ToolSchema } from '../mcp/tool.js';
24 | 
25 | export type FileUploadModalState = {
26 |   type: 'fileChooser';
27 |   description: string;
28 |   fileChooser: playwright.FileChooser;
29 | };
30 | 
31 | export type DialogModalState = {
32 |   type: 'dialog';
33 |   description: string;
34 |   dialog: playwright.Dialog;
35 | };
36 | 
37 | export type ModalState = FileUploadModalState | DialogModalState;
38 | 
39 | export type Tool<Input extends z.Schema = z.Schema> = {
40 |   capability: ToolCapability;
41 |   schema: ToolSchema<Input>;
42 |   handle: (context: Context, params: z.output<Input>, response: Response) => Promise<void>;
43 | };
44 | 
45 | export function defineTool<Input extends z.Schema>(tool: Tool<Input>): Tool<Input> {
46 |   return tool;
47 | }
48 | 
49 | export type TabTool<Input extends z.Schema = z.Schema> = {
50 |   capability: ToolCapability;
51 |   schema: ToolSchema<Input>;
52 |   clearsModalState?: ModalState['type'];
53 |   handle: (tab: Tab, params: z.output<Input>, response: Response) => Promise<void>;
54 | };
55 | 
56 | export function defineTabTool<Input extends z.Schema>(tool: TabTool<Input>): Tool<Input> {
57 |   return {
58 |     ...tool,
59 |     handle: async (context, params, response) => {
60 |       const tab = context.currentTabOrDie();
61 |       const modalStates = tab.modalStates().map(state => state.type);
62 |       if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
63 |         response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
64 |       else if (!tool.clearsModalState && modalStates.length)
65 |         response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
66 |       else
67 |         return tool.handle(tab, params, response);
68 |     },
69 |   };
70 | }
71 | 
```

--------------------------------------------------------------------------------
/src/extension/extensionContextFactory.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 debug from 'debug';
18 | import * as playwright from 'playwright';
19 | import { startHttpServer } from '../mcp/http.js';
20 | import { CDPRelayServer } from './cdpRelay.js';
21 | 
22 | import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
23 | 
24 | const debugLogger = debug('pw:mcp:relay');
25 | 
26 | export class ExtensionContextFactory implements BrowserContextFactory {
27 |   private _browserChannel: string;
28 |   private _userDataDir?: string;
29 |   private _executablePath?: string;
30 | 
31 |   constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) {
32 |     this._browserChannel = browserChannel;
33 |     this._userDataDir = userDataDir;
34 |     this._executablePath = executablePath;
35 |   }
36 | 
37 |   async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
38 |     const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName);
39 |     return {
40 |       browserContext: browser.contexts()[0],
41 |       close: async () => {
42 |         debugLogger('close() called for browser context');
43 |         await browser.close();
44 |       }
45 |     };
46 |   }
47 | 
48 |   private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<playwright.Browser> {
49 |     const relay = await this._startRelay(abortSignal);
50 |     await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
51 |     return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
52 |   }
53 | 
54 |   private async _startRelay(abortSignal: AbortSignal) {
55 |     const httpServer = await startHttpServer({});
56 |     if (abortSignal.aborted) {
57 |       httpServer.close();
58 |       throw new Error(abortSignal.reason);
59 |     }
60 |     const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath);
61 |     abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
62 |     debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
63 |     return cdpRelayServer;
64 |   }
65 | }
66 | 
```

--------------------------------------------------------------------------------
/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 | // @ts-ignore
18 | import { asLocator } from 'playwright-core/lib/utils';
19 | 
20 | import type * as playwright from 'playwright';
21 | import type { Tab } from '../tab.js';
22 | 
23 | export async function waitForCompletion<R>(tab: Tab, callback: () => Promise<R>): Promise<R> {
24 |   const requests = new Set<playwright.Request>();
25 |   let frameNavigated = false;
26 |   let waitCallback: () => void = () => {};
27 |   const waitBarrier = new Promise<void>(f => { waitCallback = f; });
28 | 
29 |   const requestListener = (request: playwright.Request) => requests.add(request);
30 |   const requestFinishedListener = (request: playwright.Request) => {
31 |     requests.delete(request);
32 |     if (!requests.size)
33 |       waitCallback();
34 |   };
35 | 
36 |   const frameNavigateListener = (frame: playwright.Frame) => {
37 |     if (frame.parentFrame())
38 |       return;
39 |     frameNavigated = true;
40 |     dispose();
41 |     clearTimeout(timeout);
42 |     void tab.waitForLoadState('load').then(waitCallback);
43 |   };
44 | 
45 |   const onTimeout = () => {
46 |     dispose();
47 |     waitCallback();
48 |   };
49 | 
50 |   tab.page.on('request', requestListener);
51 |   tab.page.on('requestfinished', requestFinishedListener);
52 |   tab.page.on('framenavigated', frameNavigateListener);
53 |   const timeout = setTimeout(onTimeout, 10000);
54 | 
55 |   const dispose = () => {
56 |     tab.page.off('request', requestListener);
57 |     tab.page.off('requestfinished', requestFinishedListener);
58 |     tab.page.off('framenavigated', frameNavigateListener);
59 |     clearTimeout(timeout);
60 |   };
61 | 
62 |   try {
63 |     const result = await callback();
64 |     if (!requests.size && !frameNavigated)
65 |       waitCallback();
66 |     await waitBarrier;
67 |     await tab.waitForTimeout(1000);
68 |     return result;
69 |   } finally {
70 |     dispose();
71 |   }
72 | }
73 | 
74 | export async function generateLocator(locator: playwright.Locator): Promise<string> {
75 |   try {
76 |     const { resolvedSelector } = await (locator as any)._resolveSelector();
77 |     return asLocator('javascript', resolvedSelector);
78 |   } catch (e) {
79 |     throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.');
80 |   }
81 | }
82 | 
83 | export async function callOnPageNoTrace<T>(page: playwright.Page, callback: (page: playwright.Page) => Promise<T>): Promise<T> {
84 |   return await (page as any)._wrapApiCall(() => callback(page), { internal: true });
85 | }
86 | 
```

--------------------------------------------------------------------------------
/src/vscode/main.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 { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18 | import * as mcpServer from '../mcp/server.js';
19 | import { BrowserServerBackend } from '../browserServerBackend.js';
20 | import { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
21 | import type { FullConfig } from '../config.js';
22 | import type { BrowserContext } from 'playwright-core';
23 | 
24 | class VSCodeBrowserContextFactory implements BrowserContextFactory {
25 |   name = 'vscode';
26 |   description = 'Connect to a browser running in the Playwright VS Code extension';
27 | 
28 |   constructor(private _config: FullConfig, private _playwright: typeof import('playwright'), private _connectionString: string) {}
29 | 
30 |   async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: BrowserContext; close: () => Promise<void>; }> {
31 |     let launchOptions: any = this._config.browser.launchOptions;
32 |     if (this._config.browser.userDataDir) {
33 |       launchOptions = {
34 |         ...launchOptions,
35 |         ...this._config.browser.contextOptions,
36 |         userDataDir: this._config.browser.userDataDir,
37 |       };
38 |     }
39 |     const connectionString = new URL(this._connectionString);
40 |     connectionString.searchParams.set('launch-options', JSON.stringify(launchOptions));
41 | 
42 |     const browserType = this._playwright.chromium; // it could also be firefox or webkit, we just need some browser type to call `connect` on
43 |     const browser = await browserType.connect(connectionString.toString());
44 | 
45 |     const context = browser.contexts()[0] ?? await browser.newContext(this._config.browser.contextOptions);
46 | 
47 |     return {
48 |       browserContext: context,
49 |       close: async () => {
50 |         await browser.close();
51 |       }
52 |     };
53 |   }
54 | }
55 | 
56 | async function main(config: FullConfig, connectionString: string, lib: string) {
57 |   const playwright = await import(lib).then(mod => mod.default ?? mod);
58 |   const factory = new VSCodeBrowserContextFactory(config, playwright, connectionString);
59 |   await mcpServer.connect(
60 |       {
61 |         name: 'Playwright MCP',
62 |         nameInConfig: 'playwright-vscode',
63 |         create: () => new BrowserServerBackend(config, factory),
64 |         version: 'unused'
65 |       },
66 |       new StdioServerTransport(),
67 |       false
68 |   );
69 | }
70 | 
71 | await main(
72 |     JSON.parse(process.argv[2]),
73 |     process.argv[3],
74 |     process.argv[4]
75 | );
76 | 
```

--------------------------------------------------------------------------------
/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 | 
19 | import { defineTabTool } from './tool.js';
20 | import { elementSchema } from './snapshot.js';
21 | import { generateLocator } from './utils.js';
22 | import * as javascript from '../utils/codegen.js';
23 | 
24 | const pressKey = defineTabTool({
25 |   capability: 'core',
26 | 
27 |   schema: {
28 |     name: 'browser_press_key',
29 |     title: 'Press a key',
30 |     description: 'Press a key on the keyboard',
31 |     inputSchema: z.object({
32 |       key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
33 |     }),
34 |     type: 'destructive',
35 |   },
36 | 
37 |   handle: async (tab, params, response) => {
38 |     response.setIncludeSnapshot();
39 |     response.addCode(`// Press ${params.key}`);
40 |     response.addCode(`await page.keyboard.press('${params.key}');`);
41 | 
42 |     await tab.waitForCompletion(async () => {
43 |       await tab.page.keyboard.press(params.key);
44 |     });
45 |   },
46 | });
47 | 
48 | const typeSchema = elementSchema.extend({
49 |   text: z.string().describe('Text to type into the element'),
50 |   submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
51 |   slowly: z.boolean().optional().describe('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.'),
52 | });
53 | 
54 | const type = defineTabTool({
55 |   capability: 'core',
56 |   schema: {
57 |     name: 'browser_type',
58 |     title: 'Type text',
59 |     description: 'Type text into editable element',
60 |     inputSchema: typeSchema,
61 |     type: 'destructive',
62 |   },
63 | 
64 |   handle: async (tab, params, response) => {
65 |     const locator = await tab.refLocator(params);
66 | 
67 |     await tab.waitForCompletion(async () => {
68 |       if (params.slowly) {
69 |         response.setIncludeSnapshot();
70 |         response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
71 |         await locator.pressSequentially(params.text);
72 |       } else {
73 |         response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
74 |         await locator.fill(params.text);
75 |       }
76 | 
77 |       if (params.submit) {
78 |         response.setIncludeSnapshot();
79 |         response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
80 |         await locator.press('Enter');
81 |       }
82 |     });
83 |   },
84 | });
85 | 
86 | export default [
87 |   pressKey,
88 |   type,
89 | ];
90 | 
```

--------------------------------------------------------------------------------
/src/mcp/inProcessTransport.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 { Server } from '@modelcontextprotocol/sdk/server/index.js';
18 | import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
19 | import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js';
20 | 
21 | export class InProcessTransport implements Transport {
22 |   private _server: Server;
23 |   private _serverTransport: InProcessServerTransport;
24 |   private _connected: boolean = false;
25 | 
26 |   constructor(server: Server) {
27 |     this._server = server;
28 |     this._serverTransport = new InProcessServerTransport(this);
29 |   }
30 | 
31 |   async start(): Promise<void> {
32 |     if (this._connected)
33 |       throw new Error('InprocessTransport already started!');
34 | 
35 |     await this._server.connect(this._serverTransport);
36 |     this._connected = true;
37 |   }
38 | 
39 |   async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
40 |     if (!this._connected)
41 |       throw new Error('Transport not connected');
42 | 
43 | 
44 |     this._serverTransport._receiveFromClient(message);
45 |   }
46 | 
47 |   async close(): Promise<void> {
48 |     if (this._connected) {
49 |       this._connected = false;
50 |       this.onclose?.();
51 |       this._serverTransport.onclose?.();
52 |     }
53 |   }
54 | 
55 |   onclose?: (() => void) | undefined;
56 |   onerror?: ((error: Error) => void) | undefined;
57 |   onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined;
58 |   sessionId?: string | undefined;
59 |   setProtocolVersion?: ((version: string) => void) | undefined;
60 | 
61 |   _receiveFromServer(message: JSONRPCMessage, extra?: MessageExtraInfo): void {
62 |     this.onmessage?.(message, extra);
63 |   }
64 | }
65 | 
66 | class InProcessServerTransport implements Transport {
67 |   private _clientTransport: InProcessTransport;
68 | 
69 |   constructor(clientTransport: InProcessTransport) {
70 |     this._clientTransport = clientTransport;
71 |   }
72 | 
73 |   async start(): Promise<void> {
74 |   }
75 | 
76 |   async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
77 |     this._clientTransport._receiveFromServer(message);
78 |   }
79 | 
80 |   async close(): Promise<void> {
81 |     this.onclose?.();
82 |   }
83 | 
84 |   onclose?: (() => void) | undefined;
85 |   onerror?: ((error: Error) => void) | undefined;
86 |   onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined;
87 |   sessionId?: string | undefined;
88 |   setProtocolVersion?: ((version: string) => void) | undefined;
89 |   _receiveFromClient(message: JSONRPCMessage): void {
90 |     this.onmessage?.(message);
91 |   }
92 | }
93 | 
```

--------------------------------------------------------------------------------
/src/browserServerBackend.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 { fileURLToPath } from 'url';
18 | import { FullConfig } from './config.js';
19 | import { Context } from './context.js';
20 | import { logUnhandledError } from './utils/log.js';
21 | import { Response } from './response.js';
22 | import { SessionLog } from './sessionLog.js';
23 | import { filteredTools } from './tools.js';
24 | import { toMcpTool } from './mcp/tool.js';
25 | 
26 | import type { Tool } from './tools/tool.js';
27 | import type { BrowserContextFactory } from './browserContextFactory.js';
28 | import type * as mcpServer from './mcp/server.js';
29 | import type { ServerBackend } from './mcp/server.js';
30 | 
31 | export class BrowserServerBackend implements ServerBackend {
32 |   private _tools: Tool[];
33 |   private _context: Context | undefined;
34 |   private _sessionLog: SessionLog | undefined;
35 |   private _config: FullConfig;
36 |   private _browserContextFactory: BrowserContextFactory;
37 | 
38 |   constructor(config: FullConfig, factory: BrowserContextFactory) {
39 |     this._config = config;
40 |     this._browserContextFactory = factory;
41 |     this._tools = filteredTools(config);
42 |   }
43 | 
44 |   async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise<void> {
45 |     let rootPath: string | undefined;
46 |     if (roots.length > 0) {
47 |       const firstRootUri = roots[0]?.uri;
48 |       const url = firstRootUri ? new URL(firstRootUri) : undefined;
49 |       rootPath = url ? fileURLToPath(url) : undefined;
50 |     }
51 |     this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined;
52 |     this._context = new Context({
53 |       tools: this._tools,
54 |       config: this._config,
55 |       browserContextFactory: this._browserContextFactory,
56 |       sessionLog: this._sessionLog,
57 |       clientInfo: { ...clientVersion, rootPath },
58 |     });
59 |   }
60 | 
61 |   async listTools(): Promise<mcpServer.Tool[]> {
62 |     return this._tools.map(tool => toMcpTool(tool.schema));
63 |   }
64 | 
65 |   async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) {
66 |     const tool = this._tools.find(tool => tool.schema.name === name)!;
67 |     if (!tool)
68 |       throw new Error(`Tool "${name}" not found`);
69 |     const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
70 |     const context = this._context!;
71 |     const response = new Response(context, name, parsedArguments);
72 |     context.setRunningTool(name);
73 |     try {
74 |       await tool.handle(context, parsedArguments, response);
75 |       await response.finish();
76 |       this._sessionLog?.logResponse(response);
77 |     } catch (error: any) {
78 |       response.addError(String(error));
79 |     } finally {
80 |       context.setRunningTool(undefined);
81 |     }
82 |     return response.serialize();
83 |   }
84 | 
85 |   serverClosed() {
86 |     void this._context?.dispose().catch(logUnhandledError);
87 |   }
88 | }
89 | 
```

--------------------------------------------------------------------------------
/src/external-modules.d.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Minimal type declarations for optional runtime dependencies.
  2 | declare module 'dotenv' {
  3 |   export interface DotenvConfigOptions {
  4 |     path?: string;
  5 |     encoding?: string;
  6 |     debug?: boolean;
  7 |     override?: boolean;
  8 |   }
  9 | 
 10 |   export interface DotenvConfigOutput {
 11 |     parsed?: Record<string, string>;
 12 |     error?: Error;
 13 |   }
 14 | 
 15 |   export function config(options?: DotenvConfigOptions): DotenvConfigOutput;
 16 | 
 17 |   const dotenv: {
 18 |     config: typeof config;
 19 |   };
 20 | 
 21 |   export default dotenv;
 22 | }
 23 | 
 24 | declare module 'openai' {
 25 |   namespace OpenAI {
 26 |     namespace Chat {
 27 |       namespace Completions {
 28 |         type Role = 'user' | 'assistant' | 'tool';
 29 | 
 30 |         interface ChatCompletionMessageToolCall {
 31 |           id: string;
 32 |           type: 'function';
 33 |           function: {
 34 |             name: string;
 35 |             arguments: string;
 36 |           };
 37 |         }
 38 | 
 39 |         interface ChatCompletionMessageParam {
 40 |           role: Role;
 41 |           content?: string | null;
 42 |           tool_calls?: ChatCompletionMessageToolCall[];
 43 |           tool_call_id?: string;
 44 |         }
 45 | 
 46 |         interface ChatCompletionAssistantMessageParam extends ChatCompletionMessageParam {
 47 |           role: 'assistant';
 48 |         }
 49 | 
 50 |         interface ChatCompletionTool {
 51 |           type: 'function';
 52 |           function: {
 53 |             name: string;
 54 |             description?: string;
 55 |             parameters?: any;
 56 |           };
 57 |         }
 58 |       }
 59 |     }
 60 |   }
 61 | 
 62 |   interface ChatCompletionsApi {
 63 |     create(request: {
 64 |       model: string;
 65 |       messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
 66 |       tools?: OpenAI.Chat.Completions.ChatCompletionTool[];
 67 |       tool_choice?: 'auto' | 'none';
 68 |     }): Promise<{
 69 |       choices: Array<{
 70 |         message: {
 71 |           content?: string | null;
 72 |           tool_calls?: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[];
 73 |         };
 74 |       }>;
 75 |     }>;
 76 |   }
 77 | 
 78 |   class OpenAI {
 79 |     constructor(config?: Record<string, unknown>);
 80 |     chat: {
 81 |       completions: ChatCompletionsApi;
 82 |     };
 83 |   }
 84 | 
 85 |   export { OpenAI };
 86 |   export default OpenAI;
 87 | }
 88 | 
 89 | declare module '@anthropic-ai/sdk' {
 90 |   namespace Anthropic {
 91 |     namespace Messages {
 92 |       type Role = 'user' | 'assistant';
 93 | 
 94 |       interface BaseBlock {
 95 |         type: string;
 96 |       }
 97 | 
 98 |       interface TextBlock extends BaseBlock {
 99 |         type: 'text';
100 |         text: string;
101 |         citations?: any[];
102 |       }
103 | 
104 |       interface ToolUseBlock extends BaseBlock {
105 |         type: 'tool_use';
106 |         id: string;
107 |         name: string;
108 |         input: unknown;
109 |       }
110 | 
111 |       interface ToolResultBlock extends BaseBlock {
112 |         type: 'tool_result';
113 |         tool_use_id: string;
114 |         content: string;
115 |         is_error?: boolean;
116 |       }
117 | 
118 |       type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
119 |       type ToolResultBlockParam = ToolResultBlock;
120 | 
121 |       interface MessageParam {
122 |         role: Role;
123 |         content: string | ContentBlock[];
124 |       }
125 | 
126 |       interface Tool {
127 |         name: string;
128 |         description?: string;
129 |         input_schema: any;
130 |       }
131 |     }
132 |   }
133 | 
134 |   class Anthropic {
135 |     constructor(config?: Record<string, unknown>);
136 |     messages: {
137 |       create(request: {
138 |         model: string;
139 |         max_tokens: number;
140 |         messages: Anthropic.Messages.MessageParam[];
141 |         tools?: Anthropic.Messages.Tool[];
142 |       }): Promise<{
143 |         content: Anthropic.Messages.ContentBlock[];
144 |       }>;
145 |     };
146 |   }
147 | 
148 |   export { Anthropic };
149 |   export default Anthropic;
150 | }
151 | 
```

--------------------------------------------------------------------------------
/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 } from './tool.js';
 19 | 
 20 | const SECOND = 1000;
 21 | const MINUTE = 60 * SECOND;
 22 | 
 23 | const browserTabs = defineTool({
 24 |   capability: 'core-tabs',
 25 |   schema: {
 26 |     name: 'browser_tabs',
 27 |     title: 'Manage tabs',
 28 |     description: 'List, create, close, or select a browser tab.',
 29 |     inputSchema: z.object({
 30 |       action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'),
 31 |       index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'),
 32 |     }),
 33 |     type: 'destructive',
 34 |   },
 35 |   handle: async (context, params, response) => {
 36 |     switch (params.action) {
 37 |       case 'list': {
 38 |         await context.ensureTab();
 39 |         response.setIncludeTabs();
 40 |         return;
 41 |       }
 42 |       case 'new': {
 43 |         await context.newTab();
 44 |         response.setIncludeTabs();
 45 |         return;
 46 |       }
 47 |       case 'close': {
 48 |         await context.closeTab(params.index);
 49 |         response.setIncludeSnapshot();
 50 |         return;
 51 |       }
 52 |       case 'select': {
 53 |         if (params.index === undefined)
 54 |           throw new Error('Tab index is required');
 55 |         await context.selectTab(params.index);
 56 |         response.setIncludeSnapshot();
 57 |         return;
 58 |       }
 59 |     }
 60 |   },
 61 | });
 62 | 
 63 | const navigationTimeout = defineTool({
 64 |   capability: 'core-tabs',
 65 |   schema: {
 66 |     name: 'browser_navigation_timeout',
 67 |     title: 'Navigation timeout',
 68 |     description: 'Sets the timeout for navigation and page load actions. Only affects the current tab and does not persist across browser context recreation.',
 69 |     inputSchema: z.object({
 70 |       timeout: z.number().min(SECOND * 30).max(MINUTE * 20).describe('Timeout in milliseconds for navigation (0-300000ms)'),
 71 |     }),
 72 |     type: 'destructive',
 73 |   },
 74 |   handle: async (context, params, response) => {
 75 |     const tabs = context.tabs();
 76 |     for (const tab of tabs) {
 77 |       tab.page.setDefaultNavigationTimeout(params.timeout);
 78 |     }
 79 |     response.addResult(`Navigation timeout set to ${params.timeout}ms for all tabs.`);
 80 |     response.setIncludeTabs();
 81 |   },
 82 | });
 83 | 
 84 | const defaultTimeout = defineTool({
 85 |   capability: 'core-tabs',
 86 |   schema: {
 87 |     name: 'browser_default_timeout',
 88 |     title: 'Default timeout',
 89 |     description: 'Sets the default timeout for all Playwright operations (clicks, fills, etc). Only affects existing tabs and does not persist across browser context recreation.',
 90 |     inputSchema: z.object({
 91 |       timeout: z.number().min(SECOND * 30).max(MINUTE * 20).describe('Timeout in milliseconds for default operations (0-300000ms)'),
 92 |     }),
 93 |     type: 'destructive',
 94 |   },
 95 |   handle: async (context, params, response) => {
 96 |     const tabs = context.tabs();
 97 |     for (const tab of tabs) {
 98 |       tab.page.setDefaultTimeout(params.timeout);
 99 |     }
100 | 
101 |     response.addResult(`Default timeout set to ${params.timeout}ms for all tabs.`);
102 |     response.setIncludeTabs();
103 |   },
104 | });
105 | 
106 | export default [
107 |   browserTabs,
108 |   navigationTimeout,
109 |   defaultTimeout
110 | ];
111 | 
```

--------------------------------------------------------------------------------
/src/mcp/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/tools/mouse.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 { defineTabTool } from './tool.js';
 19 | 
 20 | const elementSchema = z.object({
 21 |   element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
 22 | });
 23 | 
 24 | const mouseMove = defineTabTool({
 25 |   capability: 'vision',
 26 |   schema: {
 27 |     name: 'browser_mouse_move_xy',
 28 |     title: 'Move mouse',
 29 |     description: 'Move mouse to a given position',
 30 |     inputSchema: elementSchema.extend({
 31 |       x: z.number().describe('X coordinate'),
 32 |       y: z.number().describe('Y coordinate'),
 33 |     }),
 34 |     type: 'readOnly',
 35 |   },
 36 | 
 37 |   handle: async (tab, params, response) => {
 38 |     response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
 39 |     response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);
 40 | 
 41 |     await tab.waitForCompletion(async () => {
 42 |       await tab.page.mouse.move(params.x, params.y);
 43 |     });
 44 |   },
 45 | });
 46 | 
 47 | const mouseClick = defineTabTool({
 48 |   capability: 'vision',
 49 |   schema: {
 50 |     name: 'browser_mouse_click_xy',
 51 |     title: 'Click',
 52 |     description: 'Click left mouse button at a given position',
 53 |     inputSchema: elementSchema.extend({
 54 |       x: z.number().describe('X coordinate'),
 55 |       y: z.number().describe('Y coordinate'),
 56 |     }),
 57 |     type: 'destructive',
 58 |   },
 59 | 
 60 |   handle: async (tab, params, response) => {
 61 |     response.setIncludeSnapshot();
 62 | 
 63 |     response.addCode(`// Click mouse at coordinates (${params.x}, ${params.y})`);
 64 |     response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);
 65 |     response.addCode(`await page.mouse.down();`);
 66 |     response.addCode(`await page.mouse.up();`);
 67 | 
 68 |     await tab.waitForCompletion(async () => {
 69 |       await tab.page.mouse.move(params.x, params.y);
 70 |       await tab.page.mouse.down();
 71 |       await tab.page.mouse.up();
 72 |     });
 73 |   },
 74 | });
 75 | 
 76 | const mouseDrag = defineTabTool({
 77 |   capability: 'vision',
 78 |   schema: {
 79 |     name: 'browser_mouse_drag_xy',
 80 |     title: 'Drag mouse',
 81 |     description: 'Drag left mouse button to a given position',
 82 |     inputSchema: elementSchema.extend({
 83 |       startX: z.number().describe('Start X coordinate'),
 84 |       startY: z.number().describe('Start Y coordinate'),
 85 |       endX: z.number().describe('End X coordinate'),
 86 |       endY: z.number().describe('End Y coordinate'),
 87 |     }),
 88 |     type: 'destructive',
 89 |   },
 90 | 
 91 |   handle: async (tab, params, response) => {
 92 |     response.setIncludeSnapshot();
 93 | 
 94 |     response.addCode(`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`);
 95 |     response.addCode(`await page.mouse.move(${params.startX}, ${params.startY});`);
 96 |     response.addCode(`await page.mouse.down();`);
 97 |     response.addCode(`await page.mouse.move(${params.endX}, ${params.endY});`);
 98 |     response.addCode(`await page.mouse.up();`);
 99 | 
100 |     await tab.waitForCompletion(async () => {
101 |       await tab.page.mouse.move(params.startX, params.startY);
102 |       await tab.page.mouse.down();
103 |       await tab.page.mouse.move(params.endX, params.endY);
104 |       await tab.page.mouse.up();
105 |     });
106 |   },
107 | });
108 | 
109 | export default [
110 |   mouseMove,
111 |   mouseClick,
112 |   mouseDrag,
113 | ];
114 | 
```

--------------------------------------------------------------------------------
/src/actions.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 | type Point = { x: number, y: number };
 18 | 
 19 | export type ActionName =
 20 |   'check' |
 21 |   'click' |
 22 |   'closePage' |
 23 |   'fill' |
 24 |   'navigate' |
 25 |   'openPage' |
 26 |   'press' |
 27 |   'select' |
 28 |   'uncheck' |
 29 |   'setInputFiles' |
 30 |   'assertText' |
 31 |   'assertValue' |
 32 |   'assertChecked' |
 33 |   'assertVisible' |
 34 |   'assertSnapshot';
 35 | 
 36 | export type ActionBase = {
 37 |   name: ActionName,
 38 |   signals: Signal[],
 39 |   ariaSnapshot?: string,
 40 | };
 41 | 
 42 | export type ActionWithSelector = ActionBase & {
 43 |   selector: string,
 44 |   ref?: string,
 45 | };
 46 | 
 47 | export type ClickAction = ActionWithSelector & {
 48 |   name: 'click',
 49 |   button: 'left' | 'middle' | 'right',
 50 |   modifiers: number,
 51 |   clickCount: number,
 52 |   position?: Point,
 53 | };
 54 | 
 55 | export type CheckAction = ActionWithSelector & {
 56 |   name: 'check',
 57 | };
 58 | 
 59 | export type UncheckAction = ActionWithSelector & {
 60 |   name: 'uncheck',
 61 | };
 62 | 
 63 | export type FillAction = ActionWithSelector & {
 64 |   name: 'fill',
 65 |   text: string,
 66 | };
 67 | 
 68 | export type NavigateAction = ActionBase & {
 69 |   name: 'navigate',
 70 |   url: string,
 71 | };
 72 | 
 73 | export type OpenPageAction = ActionBase & {
 74 |   name: 'openPage',
 75 |   url: string,
 76 | };
 77 | 
 78 | export type ClosesPageAction = ActionBase & {
 79 |   name: 'closePage',
 80 | };
 81 | 
 82 | export type PressAction = ActionWithSelector & {
 83 |   name: 'press',
 84 |   key: string,
 85 |   modifiers: number,
 86 | };
 87 | 
 88 | export type SelectAction = ActionWithSelector & {
 89 |   name: 'select',
 90 |   options: string[],
 91 | };
 92 | 
 93 | export type SetInputFilesAction = ActionWithSelector & {
 94 |   name: 'setInputFiles',
 95 |   files: string[],
 96 | };
 97 | 
 98 | export type AssertTextAction = ActionWithSelector & {
 99 |   name: 'assertText',
100 |   text: string,
101 |   substring: boolean,
102 | };
103 | 
104 | export type AssertValueAction = ActionWithSelector & {
105 |   name: 'assertValue',
106 |   value: string,
107 | };
108 | 
109 | export type AssertCheckedAction = ActionWithSelector & {
110 |   name: 'assertChecked',
111 |   checked: boolean,
112 | };
113 | 
114 | export type AssertVisibleAction = ActionWithSelector & {
115 |   name: 'assertVisible',
116 | };
117 | 
118 | export type AssertSnapshotAction = ActionWithSelector & {
119 |   name: 'assertSnapshot',
120 |   ariaSnapshot: string,
121 | };
122 | 
123 | export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction;
124 | export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction;
125 | export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction;
126 | 
127 | // Signals.
128 | 
129 | export type BaseSignal = {
130 | };
131 | 
132 | export type NavigationSignal = BaseSignal & {
133 |   name: 'navigation',
134 |   url: string,
135 | };
136 | 
137 | export type PopupSignal = BaseSignal & {
138 |   name: 'popup',
139 |   popupAlias: string,
140 | };
141 | 
142 | export type DownloadSignal = BaseSignal & {
143 |   name: 'download',
144 |   downloadAlias: string,
145 | };
146 | 
147 | export type DialogSignal = BaseSignal & {
148 |   name: 'dialog',
149 |   dialogAlias: string,
150 | };
151 | 
152 | export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
153 | 
154 | export type FrameDescription = {
155 |   pageGuid: string;
156 |   pageAlias: string;
157 |   framePath: string[];
158 | };
159 | 
160 | export type ActionInContext = {
161 |   frame: FrameDescription;
162 |   description?: string;
163 |   action: Action;
164 |   startTime: number;
165 |   endTime?: number;
166 | };
167 | 
168 | export type SignalInContext = {
169 |   frame: FrameDescription;
170 |   signal: Signal;
171 |   timestamp: number;
172 | };
173 | 
```

--------------------------------------------------------------------------------
/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 { defineTabTool } from './tool.js';
20 | import * as javascript from '../utils/codegen.js';
21 | import { generateLocator } from './utils.js';
22 | 
23 | import type * as playwright from 'playwright';
24 | 
25 | const screenshotSchema = z.object({
26 |   type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
27 |   filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
28 |   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.'),
29 |   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.'),
30 |   fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'),
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 | }).refine(data => {
37 |   return !(data.fullPage && (data.element || data.ref));
38 | }, {
39 |   message: 'fullPage cannot be used with element screenshots.',
40 |   path: ['fullPage']
41 | });
42 | 
43 | const screenshot = defineTabTool({
44 |   capability: 'core',
45 |   schema: {
46 |     name: 'browser_take_screenshot',
47 |     title: 'Take a screenshot',
48 |     description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
49 |     inputSchema: screenshotSchema,
50 |     type: 'readOnly',
51 |   },
52 | 
53 |   handle: async (tab, params, response) => {
54 |     const fileType = params.type || 'png';
55 |     const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
56 |     const options: playwright.PageScreenshotOptions = {
57 |       type: fileType,
58 |       quality: fileType === 'png' ? undefined : 90,
59 |       scale: 'css',
60 |       path: fileName,
61 |       ...(params.fullPage !== undefined && { fullPage: params.fullPage })
62 |     };
63 |     const isElementScreenshot = params.element && params.ref;
64 | 
65 |     const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
66 |     response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
67 | 
68 |     // Only get snapshot when element screenshot is needed
69 |     const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
70 | 
71 |     if (locator)
72 |       response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
73 |     else
74 |       response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
75 | 
76 |     const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
77 |     response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
78 | 
79 |     // https://github.com/microsoft/playwright-mcp/issues/817
80 |     // Never return large images to LLM, saving them to the file system is enough.
81 |     if (!params.fullPage) {
82 |       response.addImage({
83 |         contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
84 |         data: buffer
85 |       });
86 |     }
87 |   }
88 | });
89 | 
90 | export default [
91 |   screenshot,
92 | ];
93 | 
```

--------------------------------------------------------------------------------
/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 =
 20 |   | 'core'
 21 |   | 'tabs'
 22 |   | 'pdf'
 23 |   | 'history'
 24 |   | 'wait'
 25 |   | 'files'
 26 |   | 'install'
 27 |   | 'testing'
 28 |   | 'core-install'
 29 |   | 'core-tabs'
 30 |   | 'vision'
 31 |   | 'verify';
 32 | 
 33 | export type Config = {
 34 |   /**
 35 |    * The browser to use.
 36 |    */
 37 |   browser?: {
 38 |     /**
 39 |      * Use browser agent (experimental).
 40 |      */
 41 |     browserAgent?: string;
 42 | 
 43 |     /**
 44 |      * The type of browser to use.
 45 |      */
 46 |     browserName?: 'chromium' | 'firefox' | 'webkit';
 47 | 
 48 |     /**
 49 |      * Keep the browser profile in memory, do not save it to disk.
 50 |      */
 51 |     isolated?: boolean;
 52 | 
 53 |     /**
 54 |      * Path to a user data directory for browser profile persistence.
 55 |      * Temporary directory is created by default.
 56 |      */
 57 |     userDataDir?: string;
 58 | 
 59 |     /**
 60 |      * Launch options passed to
 61 |      * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
 62 |      *
 63 |      * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
 64 |      */
 65 |     launchOptions?: playwright.LaunchOptions;
 66 | 
 67 |     /**
 68 |      * Context options for the browser context.
 69 |      *
 70 |      * This is useful for settings options like `viewport`.
 71 |      */
 72 |     contextOptions?: playwright.BrowserContextOptions;
 73 | 
 74 |     /**
 75 |      * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
 76 |      */
 77 |     cdpEndpoint?: string;
 78 | 
 79 |     /**
 80 |      * Remote endpoint to connect to an existing Playwright server.
 81 |      */
 82 |     remoteEndpoint?: string;
 83 |   },
 84 | 
 85 |   server?: {
 86 |     /**
 87 |      * The port to listen on for SSE or MCP transport.
 88 |      */
 89 |     port?: number;
 90 | 
 91 |     /**
 92 |      * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
 93 |      */
 94 |     host?: string;
 95 |   },
 96 | 
 97 |   /**
 98 |    * List of enabled tool capabilities. Possible values:
 99 |    *   - 'core': Core browser automation features.
100 |    *   - 'tabs': Tab management features.
101 |    *   - 'pdf': PDF generation and manipulation.
102 |    *   - 'history': Browser history access.
103 |    *   - 'wait': Wait and timing utilities.
104 |    *   - 'files': File upload/download support.
105 |    *   - 'install': Browser installation utilities.
106 |    */
107 |   capabilities?: ToolCapability[];
108 | 
109 |   /**
110 |    * Run server that uses screenshots (Aria snapshots are used by default).
111 |    */
112 |   vision?: boolean;
113 | 
114 |   /**
115 |    * Whether to save the Playwright trace of the session into the output directory.
116 |    */
117 |   saveTrace?: boolean;
118 | 
119 |   /**
120 |    * Whether to persist session logs for the current run.
121 |    */
122 |   saveSession?: boolean;
123 | 
124 |   /**
125 |    * The directory to save output files.
126 |    */
127 |   outputDir?: string;
128 | 
129 |   network?: {
130 |     /**
131 |      * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
132 |      */
133 |     allowedOrigins?: string[];
134 | 
135 |     /**
136 |      * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
137 |      */
138 |     blockedOrigins?: string[];
139 |   };
140 | 
141 |   /**
142 |    * 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.
143 |    */
144 |   imageResponses?: 'allow' | 'omit' | 'auto';
145 | 
146 |   /**
147 |    * Timeout settings for Playwright operations.
148 |    */
149 |   timeouts?: {
150 |     /**
151 |      * Maximum time in milliseconds for page navigation. Defaults to 60000ms (60 seconds).
152 |      */
153 |     navigationTimeout?: number;
154 | 
155 |     /**
156 |      * Default timeout for all Playwright operations (clicks, fills, etc). Defaults to 5000ms (5 seconds).
157 |      */
158 |     defaultTimeout?: number;
159 |   };
160 | };
161 | 
```

--------------------------------------------------------------------------------
/src/mcp/proxyBackend.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 debug from 'debug';
 18 | import { z } from 'zod';
 19 | import { zodToJsonSchema } from 'zod-to-json-schema';
 20 | 
 21 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 22 | import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
 23 | 
 24 | import type { ServerBackend, ClientVersion, Root, Server } from './server.js';
 25 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
 26 | import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
 27 | 
 28 | export type MCPProvider = {
 29 |   name: string;
 30 |   description: string;
 31 |   connect(): Promise<Transport>;
 32 | };
 33 | 
 34 | const errorsDebug = debug('pw:mcp:errors');
 35 | 
 36 | export class ProxyBackend implements ServerBackend {
 37 |   private _mcpProviders: MCPProvider[];
 38 |   private _currentClient: Client | undefined;
 39 |   private _contextSwitchTool: Tool;
 40 |   private _roots: Root[] = [];
 41 | 
 42 |   constructor(mcpProviders: MCPProvider[]) {
 43 |     this._mcpProviders = mcpProviders;
 44 |     this._contextSwitchTool = this._defineContextSwitchTool();
 45 |   }
 46 | 
 47 |   async initialize(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise<void> {
 48 |     this._roots = roots;
 49 |     await this._setCurrentClient(this._mcpProviders[0]);
 50 |   }
 51 | 
 52 |   async listTools(): Promise<Tool[]> {
 53 |     const response = await this._currentClient!.listTools();
 54 |     if (this._mcpProviders.length === 1)
 55 |       return response.tools;
 56 |     return [
 57 |       ...response.tools,
 58 |       this._contextSwitchTool,
 59 |     ];
 60 |   }
 61 | 
 62 |   async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult> {
 63 |     if (name === this._contextSwitchTool.name)
 64 |       return this._callContextSwitchTool(args);
 65 |     return await this._currentClient!.callTool({
 66 |       name,
 67 |       arguments: args,
 68 |     }) as CallToolResult;
 69 |   }
 70 | 
 71 |   serverClosed?(): void {
 72 |     void this._currentClient?.close().catch(errorsDebug);
 73 |   }
 74 | 
 75 |   private async _callContextSwitchTool(params: any): Promise<CallToolResult> {
 76 |     try {
 77 |       const factory = this._mcpProviders.find(factory => factory.name === params.name);
 78 |       if (!factory)
 79 |         throw new Error('Unknown connection method: ' + params.name);
 80 | 
 81 |       await this._setCurrentClient(factory);
 82 |       return {
 83 |         content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
 84 |       };
 85 |     } catch (error) {
 86 |       return {
 87 |         content: [{ type: 'text', text: `### Result\nError: ${error}\n` }],
 88 |         isError: true,
 89 |       };
 90 |     }
 91 |   }
 92 | 
 93 |   private _defineContextSwitchTool(): Tool {
 94 |     return {
 95 |       name: 'browser_connect',
 96 |       description: [
 97 |         'Connect to a browser using one of the available methods:',
 98 |         ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
 99 |       ].join('\n'),
100 |       inputSchema: zodToJsonSchema(z.object({
101 |         name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
102 |       }), { strictUnions: true }) as Tool['inputSchema'],
103 |       annotations: {
104 |         title: 'Connect to a browser context',
105 |         readOnlyHint: true,
106 |         openWorldHint: false,
107 |       },
108 |     };
109 |   }
110 | 
111 |   private async _setCurrentClient(factory: MCPProvider) {
112 |     await this._currentClient?.close();
113 |     this._currentClient = undefined;
114 | 
115 |     const client = new Client({ name: 'Playwright MCP Proxy', version: '0.0.0' });
116 |     client.registerCapabilities({
117 |       roots: {
118 |         listRoots: true,
119 |       },
120 |     });
121 |     client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
122 |     client.setRequestHandler(PingRequestSchema, () => ({}));
123 | 
124 |     const transport = await factory.connect();
125 |     await client.connect(transport);
126 |     this._currentClient = client;
127 |   }
128 | }
129 | 
```

--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------

```
  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 typescriptEslint from "@typescript-eslint/eslint-plugin";
 18 | import tsParser from "@typescript-eslint/parser";
 19 | import notice from "eslint-plugin-notice";
 20 | import path from "path";
 21 | import { fileURLToPath } from "url";
 22 | import stylistic from "@stylistic/eslint-plugin";
 23 | import importRules from "eslint-plugin-import";
 24 | 
 25 | const __filename = fileURLToPath(import.meta.url);
 26 | const __dirname = path.dirname(__filename);
 27 | 
 28 | const plugins = {
 29 |   "@stylistic": stylistic,
 30 |   "@typescript-eslint": typescriptEslint,
 31 |   notice,
 32 |   import: importRules,
 33 | };
 34 | 
 35 | export const baseRules = {
 36 |   "import/extensions": ["error", "ignorePackages", {ts: "always"}],
 37 |   "@typescript-eslint/no-floating-promises": "error",
 38 |   "@typescript-eslint/no-unused-vars": [
 39 |     2,
 40 |     { args: "none", caughtErrors: "none" },
 41 |   ],
 42 | 
 43 |   /**
 44 |    * Enforced rules
 45 |    */
 46 |   // syntax preferences
 47 |   "object-curly-spacing": ["error", "always"],
 48 |   quotes: [
 49 |     2,
 50 |     "single",
 51 |     {
 52 |       avoidEscape: true,
 53 |       allowTemplateLiterals: true,
 54 |     },
 55 |   ],
 56 |   "jsx-quotes": [2, "prefer-single"],
 57 |   "no-extra-semi": 2,
 58 |   "@stylistic/semi": [2],
 59 |   "comma-style": [2, "last"],
 60 |   "wrap-iife": [2, "inside"],
 61 |   "spaced-comment": [
 62 |     2,
 63 |     "always",
 64 |     {
 65 |       markers: ["*"],
 66 |     },
 67 |   ],
 68 |   eqeqeq: [2],
 69 |   "accessor-pairs": [
 70 |     2,
 71 |     {
 72 |       getWithoutSet: false,
 73 |       setWithoutGet: false,
 74 |     },
 75 |   ],
 76 |   "brace-style": [2, "1tbs", { allowSingleLine: true }],
 77 |   curly: [2, "multi-or-nest", "consistent"],
 78 |   "new-parens": 2,
 79 |   "arrow-parens": [2, "as-needed"],
 80 |   "prefer-const": 2,
 81 |   "quote-props": [2, "consistent"],
 82 |   "nonblock-statement-body-position": [2, "below"],
 83 | 
 84 |   // anti-patterns
 85 |   "no-var": 2,
 86 |   "no-with": 2,
 87 |   "no-multi-str": 2,
 88 |   "no-caller": 2,
 89 |   "no-implied-eval": 2,
 90 |   "no-labels": 2,
 91 |   "no-new-object": 2,
 92 |   "no-octal-escape": 2,
 93 |   "no-self-compare": 2,
 94 |   "no-shadow-restricted-names": 2,
 95 |   "no-cond-assign": 2,
 96 |   "no-debugger": 2,
 97 |   "no-dupe-keys": 2,
 98 |   "no-duplicate-case": 2,
 99 |   "no-empty-character-class": 2,
100 |   "no-unreachable": 2,
101 |   "no-unsafe-negation": 2,
102 |   radix: 2,
103 |   "valid-typeof": 2,
104 |   "no-implicit-globals": [2],
105 |   "no-unused-expressions": [
106 |     2,
107 |     { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true },
108 |   ],
109 |   "no-proto": 2,
110 | 
111 |   // es2015 features
112 |   "require-yield": 2,
113 |   "template-curly-spacing": [2, "never"],
114 | 
115 |   // spacing details
116 |   "space-infix-ops": 2,
117 |   "space-in-parens": [2, "never"],
118 |   "array-bracket-spacing": [2, "never"],
119 |   "comma-spacing": [2, { before: false, after: true }],
120 |   "space-before-function-paren": [
121 |     2,
122 |     {
123 |       anonymous: "never",
124 |       named: "never",
125 |       asyncArrow: "always",
126 |     },
127 |   ],
128 |   "no-whitespace-before-property": 2,
129 |   "keyword-spacing": [
130 |     2,
131 |     {
132 |       overrides: {
133 |         if: { after: true },
134 |         else: { after: true },
135 |         for: { after: true },
136 |         while: { after: true },
137 |         do: { after: true },
138 |         switch: { after: true },
139 |         return: { after: true },
140 |       },
141 |     },
142 |   ],
143 |   "arrow-spacing": [
144 |     2,
145 |     {
146 |       after: true,
147 |       before: true,
148 |     },
149 |   ],
150 |   "@stylistic/type-annotation-spacing": 2,
151 | 
152 |   // file whitespace
153 |   "no-multiple-empty-lines": [2, { max: 2, maxEOF: 0 }],
154 |   "no-mixed-spaces-and-tabs": 2,
155 |   "no-trailing-spaces": 2,
156 |   "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"],
157 |   indent: [
158 |     2,
159 |     2,
160 |     { SwitchCase: 1, CallExpression: { arguments: 2 }, MemberExpression: 2 },
161 |   ],
162 |   "key-spacing": [
163 |     2,
164 |     {
165 |       beforeColon: false,
166 |     },
167 |   ],
168 |   "eol-last": 2,
169 | 
170 |   // copyright
171 |   "notice/notice": [
172 |     2,
173 |     {
174 |       mustMatch: "Copyright",
175 |       templateFile: path.join(__dirname, "utils", "copyright.js"),
176 |     },
177 |   ],
178 | 
179 |   // react
180 |   "react/react-in-jsx-scope": 0,
181 |   "no-console": 2,
182 | };
183 | 
184 | const languageOptions = {
185 |   parser: tsParser,
186 |   ecmaVersion: 9,
187 |   sourceType: "module",
188 |   parserOptions: {
189 |     project: path.join(fileURLToPath(import.meta.url), "..", "tsconfig.all.json"),
190 |   }
191 | };
192 | 
193 | export default [
194 |   {
195 |     ignores: ["**/*.js"],
196 |   },
197 |   {
198 |     files: ["**/*.ts", "**/*.tsx"],
199 |     plugins,
200 |     languageOptions,
201 |     rules: baseRules,
202 |   },
203 | ];
204 | 
```

--------------------------------------------------------------------------------
/src/sessionLog.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 { Response } from './response.js';
 21 | import { logUnhandledError } from './utils/log.js';
 22 | import { outputFile  } from './config.js';
 23 | 
 24 | import type { FullConfig } from './config.js';
 25 | import type * as actions from './actions.js';
 26 | import type { Tab, TabSnapshot } from './tab.js';
 27 | 
 28 | type LogEntry = {
 29 |   timestamp: number;
 30 |   toolCall?: {
 31 |     toolName: string;
 32 |     toolArgs: Record<string, any>;
 33 |     result: string;
 34 |     isError?: boolean;
 35 |   };
 36 |   userAction?: actions.Action;
 37 |   code: string;
 38 |   tabSnapshot?: TabSnapshot;
 39 | };
 40 | 
 41 | export class SessionLog {
 42 |   private _folder: string;
 43 |   private _file: string;
 44 |   private _ordinal = 0;
 45 |   private _pendingEntries: LogEntry[] = [];
 46 |   private _sessionFileQueue = Promise.resolve();
 47 |   private _flushEntriesTimeout: NodeJS.Timeout | undefined;
 48 | 
 49 |   constructor(sessionFolder: string) {
 50 |     this._folder = sessionFolder;
 51 |     this._file = path.join(this._folder, 'session.md');
 52 |   }
 53 | 
 54 |   static async create(config: FullConfig, rootPath: string | undefined): Promise<SessionLog> {
 55 |     const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`);
 56 |     await fs.promises.mkdir(sessionFolder, { recursive: true });
 57 |     // eslint-disable-next-line no-console
 58 |     console.error(`Session: ${sessionFolder}`);
 59 |     return new SessionLog(sessionFolder);
 60 |   }
 61 | 
 62 |   logResponse(response: Response) {
 63 |     const entry: LogEntry = {
 64 |       timestamp: performance.now(),
 65 |       toolCall: {
 66 |         toolName: response.toolName,
 67 |         toolArgs: response.toolArgs,
 68 |         result: response.result(),
 69 |         isError: response.isError(),
 70 |       },
 71 |       code: response.code(),
 72 |       tabSnapshot: response.tabSnapshot(),
 73 |     };
 74 |     this._appendEntry(entry);
 75 |   }
 76 | 
 77 |   logUserAction(action: actions.Action, tab: Tab, code: string, isUpdate: boolean) {
 78 |     code = code.trim();
 79 |     if (isUpdate) {
 80 |       const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
 81 |       if (lastEntry.userAction?.name === action.name) {
 82 |         lastEntry.userAction = action;
 83 |         lastEntry.code = code;
 84 |         return;
 85 |       }
 86 |     }
 87 |     if (action.name === 'navigate') {
 88 |       // Already logged at this location.
 89 |       const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
 90 |       if (lastEntry?.tabSnapshot?.url === action.url)
 91 |         return;
 92 |     }
 93 |     const entry: LogEntry = {
 94 |       timestamp: performance.now(),
 95 |       userAction: action,
 96 |       code,
 97 |       tabSnapshot: {
 98 |         url: tab.page.url(),
 99 |         title: '',
100 |         ariaSnapshot: action.ariaSnapshot || '',
101 |         modalStates: [],
102 |         consoleMessages: [],
103 |         downloads: [],
104 |       },
105 |     };
106 |     this._appendEntry(entry);
107 |   }
108 | 
109 |   private _appendEntry(entry: LogEntry) {
110 |     this._pendingEntries.push(entry);
111 |     if (this._flushEntriesTimeout)
112 |       clearTimeout(this._flushEntriesTimeout);
113 |     this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1000);
114 |   }
115 | 
116 |   private async _flushEntries() {
117 |     clearTimeout(this._flushEntriesTimeout);
118 |     const entries = this._pendingEntries;
119 |     this._pendingEntries = [];
120 |     const lines: string[] = [''];
121 | 
122 |     for (const entry of entries) {
123 |       const ordinal = (++this._ordinal).toString().padStart(3, '0');
124 |       if (entry.toolCall) {
125 |         lines.push(
126 |             `### Tool call: ${entry.toolCall.toolName}`,
127 |             `- Args`,
128 |             '```json',
129 |             JSON.stringify(entry.toolCall.toolArgs, null, 2),
130 |             '```',
131 |         );
132 |         if (entry.toolCall.result) {
133 |           lines.push(
134 |               entry.toolCall.isError ? `- Error` : `- Result`,
135 |               '```',
136 |               entry.toolCall.result,
137 |               '```',
138 |           );
139 |         }
140 |       }
141 | 
142 |       if (entry.userAction) {
143 |         const actionData = { ...entry.userAction } as any;
144 |         delete actionData.ariaSnapshot;
145 |         delete actionData.selector;
146 |         delete actionData.signals;
147 | 
148 |         lines.push(
149 |             `### User action: ${entry.userAction.name}`,
150 |             `- Args`,
151 |             '```json',
152 |             JSON.stringify(actionData, null, 2),
153 |             '```',
154 |         );
155 |       }
156 | 
157 |       if (entry.code) {
158 |         lines.push(
159 |             `- Code`,
160 |             '```js',
161 |             entry.code,
162 |             '```');
163 |       }
164 | 
165 |       if (entry.tabSnapshot) {
166 |         const fileName = `${ordinal}.snapshot.yml`;
167 |         fs.promises.writeFile(path.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(logUnhandledError);
168 |         lines.push(`- Snapshot: ${fileName}`);
169 |       }
170 | 
171 |       lines.push('', '');
172 |     }
173 | 
174 |     this._sessionFileQueue = this._sessionFileQueue.then(() => fs.promises.appendFile(this._file, lines.join('\n')));
175 |   }
176 | }
177 | 
```

--------------------------------------------------------------------------------
/src/mcp/http.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 assert from 'assert';
 18 | import net from 'net';
 19 | import http from 'http';
 20 | import crypto from 'crypto';
 21 | 
 22 | import debug from 'debug';
 23 | 
 24 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
 25 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
 26 | import * as mcpServer from './server.js';
 27 | 
 28 | import type { ServerBackendFactory } from './server.js';
 29 | 
 30 | const testDebug = debug('pw:mcp:test');
 31 | 
 32 | export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise<http.Server> {
 33 |   const { host, port } = config;
 34 |   const httpServer = http.createServer();
 35 |   decorateServer(httpServer);
 36 |   await new Promise<void>((resolve, reject) => {
 37 |     httpServer.on('error', reject);
 38 |     abortSignal?.addEventListener('abort', () => {
 39 |       httpServer.close();
 40 |       reject(new Error('Aborted'));
 41 |     });
 42 |     httpServer.listen(port, host, () => {
 43 |       resolve();
 44 |       httpServer.removeListener('error', reject);
 45 |     });
 46 |   });
 47 |   return httpServer;
 48 | }
 49 | 
 50 | export function httpAddressToString(address: string | net.AddressInfo | null): string {
 51 |   assert(address, 'Could not bind server socket');
 52 |   if (typeof address === 'string')
 53 |     return address;
 54 |   const resolvedPort = address.port;
 55 |   let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
 56 |   if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
 57 |     resolvedHost = 'localhost';
 58 |   return `http://${resolvedHost}:${resolvedPort}`;
 59 | }
 60 | 
 61 | export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) {
 62 |   const sseSessions = new Map();
 63 |   const streamableSessions = new Map();
 64 |   httpServer.on('request', async (req, res) => {
 65 |     const url = new URL(`http://localhost${req.url}`);
 66 |     if (url.pathname.startsWith('/sse'))
 67 |       await handleSSE(serverBackendFactory, req, res, url, sseSessions);
 68 |     else
 69 |       await handleStreamable(serverBackendFactory, req, res, streamableSessions);
 70 |   });
 71 | }
 72 | 
 73 | async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map<string, SSEServerTransport>) {
 74 |   if (req.method === 'POST') {
 75 |     const sessionId = url.searchParams.get('sessionId');
 76 |     if (!sessionId) {
 77 |       res.statusCode = 400;
 78 |       return res.end('Missing sessionId');
 79 |     }
 80 | 
 81 |     const transport = sessions.get(sessionId);
 82 |     if (!transport) {
 83 |       res.statusCode = 404;
 84 |       return res.end('Session not found');
 85 |     }
 86 | 
 87 |     return await transport.handlePostMessage(req, res);
 88 |   } else if (req.method === 'GET') {
 89 |     const transport = new SSEServerTransport('/sse', res);
 90 |     sessions.set(transport.sessionId, transport);
 91 |     testDebug(`create SSE session: ${transport.sessionId}`);
 92 |     await mcpServer.connect(serverBackendFactory, transport, false);
 93 |     res.on('close', () => {
 94 |       testDebug(`delete SSE session: ${transport.sessionId}`);
 95 |       sessions.delete(transport.sessionId);
 96 |     });
 97 |     return;
 98 |   }
 99 | 
100 |   res.statusCode = 405;
101 |   res.end('Method not allowed');
102 | }
103 | 
104 | async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, sessions: Map<string, StreamableHTTPServerTransport>) {
105 |   const sessionId = req.headers['mcp-session-id'] as string | undefined;
106 |   if (sessionId) {
107 |     const transport = sessions.get(sessionId);
108 |     if (!transport) {
109 |       res.statusCode = 404;
110 |       res.end('Session not found');
111 |       return;
112 |     }
113 |     return await transport.handleRequest(req, res);
114 |   }
115 | 
116 |   if (req.method === 'POST') {
117 |     const transport = new StreamableHTTPServerTransport({
118 |       sessionIdGenerator: () => crypto.randomUUID(),
119 |       onsessioninitialized: async sessionId => {
120 |         testDebug(`create http session: ${transport.sessionId}`);
121 |         await mcpServer.connect(serverBackendFactory, transport, true);
122 |         sessions.set(sessionId, transport);
123 |       }
124 |     });
125 | 
126 |     transport.onclose = () => {
127 |       if (!transport.sessionId)
128 |         return;
129 |       sessions.delete(transport.sessionId);
130 |       testDebug(`delete http session: ${transport.sessionId}`);
131 |     };
132 | 
133 |     await transport.handleRequest(req, res);
134 |     return;
135 |   }
136 | 
137 |   res.statusCode = 400;
138 |   res.end('Invalid request');
139 | }
140 | 
141 | function decorateServer(server: net.Server) {
142 |   const sockets = new Set<net.Socket>();
143 |   server.on('connection', socket => {
144 |     sockets.add(socket);
145 |     socket.once('close', () => sockets.delete(socket));
146 |   });
147 | 
148 |   const close = server.close;
149 |   server.close = (callback?: (err?: Error) => void) => {
150 |     for (const socket of sockets)
151 |       socket.destroy();
152 |     sockets.clear();
153 |     return close.call(server, callback);
154 |   };
155 | }
156 | 
```

--------------------------------------------------------------------------------
/src/vscode/host.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 { fileURLToPath } from 'url';
 18 | import path from 'path';
 19 | import { z } from 'zod';
 20 | import { zodToJsonSchema } from 'zod-to-json-schema';
 21 | 
 22 | 
 23 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 24 | import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
 25 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 26 | import * as mcpServer from '../mcp/server.js';
 27 | import { logUnhandledError } from '../utils/log.js';
 28 | import { packageJSON } from '../utils/package.js';
 29 | 
 30 | import { FullConfig } from '../config.js';
 31 | import { BrowserServerBackend } from '../browserServerBackend.js';
 32 | import { contextFactory } from '../browserContextFactory.js';
 33 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
 34 | import type { ClientVersion, ServerBackend } from '../mcp/server.js';
 35 | import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
 36 | 
 37 | const contextSwitchOptions = z.object({
 38 |   connectionString: z.string().optional().describe('The connection string to use to connect to the browser'),
 39 |   lib: z.string().optional().describe('The library to use for the connection'),
 40 | });
 41 | 
 42 | class VSCodeProxyBackend implements ServerBackend {
 43 |   name = 'Playwright MCP Client Switcher';
 44 |   version = packageJSON.version;
 45 | 
 46 |   private _currentClient: Client | undefined;
 47 |   private _contextSwitchTool: Tool;
 48 |   private _roots: Root[] = [];
 49 |   private _clientVersion?: ClientVersion;
 50 | 
 51 |   constructor(private readonly _config: FullConfig, private readonly _defaultTransportFactory: () => Promise<Transport>) {
 52 |     this._contextSwitchTool = this._defineContextSwitchTool();
 53 |   }
 54 | 
 55 |   async initialize(server: mcpServer.Server, clientVersion: ClientVersion, roots: Root[]): Promise<void> {
 56 |     this._clientVersion = clientVersion;
 57 |     this._roots = roots;
 58 |     const transport = await this._defaultTransportFactory();
 59 |     await this._setCurrentClient(transport);
 60 |   }
 61 | 
 62 |   async listTools(): Promise<Tool[]> {
 63 |     const response = await this._currentClient!.listTools();
 64 |     return [
 65 |       ...response.tools,
 66 |       this._contextSwitchTool,
 67 |     ];
 68 |   }
 69 | 
 70 |   async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult> {
 71 |     if (name === this._contextSwitchTool.name)
 72 |       return this._callContextSwitchTool(args as any);
 73 |     return await this._currentClient!.callTool({
 74 |       name,
 75 |       arguments: args,
 76 |     }) as CallToolResult;
 77 |   }
 78 | 
 79 |   serverClosed?(server: mcpServer.Server): void {
 80 |     void this._currentClient?.close().catch(logUnhandledError);
 81 |   }
 82 | 
 83 |   private async _callContextSwitchTool(params: z.infer<typeof contextSwitchOptions>): Promise<CallToolResult> {
 84 |     if (!params.connectionString || !params.lib) {
 85 |       const transport = await this._defaultTransportFactory();
 86 |       await this._setCurrentClient(transport);
 87 |       return {
 88 |         content: [{ type: 'text', text: '### Result\nSuccessfully disconnected.\n' }],
 89 |       };
 90 |     }
 91 | 
 92 |     await this._setCurrentClient(
 93 |         new StdioClientTransport({
 94 |           command: process.execPath,
 95 |           cwd: process.cwd(),
 96 |           args: [
 97 |             path.join(fileURLToPath(import.meta.url), '..', 'main.js'),
 98 |             JSON.stringify(this._config),
 99 |             params.connectionString,
100 |             params.lib,
101 |           ],
102 |         })
103 |     );
104 |     return {
105 |       content: [{ type: 'text', text: '### Result\nSuccessfully connected.\n' }],
106 |     };
107 |   }
108 | 
109 |   private _defineContextSwitchTool(): Tool {
110 |     return {
111 |       name: 'browser_connect',
112 |       description: 'Do not call, this tool is used in the integration with the Playwright VS Code Extension and meant for programmatic usage only.',
113 |       inputSchema: zodToJsonSchema(contextSwitchOptions, { strictUnions: true }) as Tool['inputSchema'],
114 |       annotations: {
115 |         title: 'Connect to a browser running in VS Code.',
116 |         readOnlyHint: true,
117 |         openWorldHint: false,
118 |       },
119 |     };
120 |   }
121 | 
122 |   private async _setCurrentClient(transport: Transport) {
123 |     await this._currentClient?.close();
124 |     this._currentClient = undefined;
125 | 
126 |     const client = new Client(this._clientVersion!);
127 |     client.registerCapabilities({
128 |       roots: {
129 |         listRoots: true,
130 |       },
131 |     });
132 |     client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
133 |     client.setRequestHandler(PingRequestSchema, () => ({}));
134 | 
135 |     await client.connect(transport);
136 |     this._currentClient = client;
137 |   }
138 | }
139 | 
140 | export async function runVSCodeTools(config: FullConfig) {
141 |   const serverBackendFactory: mcpServer.ServerBackendFactory = {
142 |     name: 'Playwright w/ vscode',
143 |     nameInConfig: 'playwright-vscode',
144 |     version: packageJSON.version,
145 |     create: () => new VSCodeProxyBackend(config, () => mcpServer.wrapInProcess(new BrowserServerBackend(config, contextFactory(config))))
146 |   };
147 |   await mcpServer.start(serverBackendFactory, config.server);
148 |   return;
149 | }
150 | 
```

--------------------------------------------------------------------------------
/src/response.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 { renderModalStates } from './tab.js';
 18 | 
 19 | import type { Tab, TabSnapshot } from './tab.js';
 20 | import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
 21 | import type { Context } from './context.js';
 22 | 
 23 | export class Response {
 24 |   private _result: string[] = [];
 25 |   private _code: string[] = [];
 26 |   private _images: { contentType: string, data: Buffer }[] = [];
 27 |   private _context: Context;
 28 |   private _includeSnapshot = false;
 29 |   private _includeTabs = false;
 30 |   private _tabSnapshot: TabSnapshot | undefined;
 31 | 
 32 |   readonly toolName: string;
 33 |   readonly toolArgs: Record<string, any>;
 34 |   private _isError: boolean | undefined;
 35 | 
 36 |   constructor(context: Context, toolName: string, toolArgs: Record<string, any>) {
 37 |     this._context = context;
 38 |     this.toolName = toolName;
 39 |     this.toolArgs = toolArgs;
 40 |   }
 41 | 
 42 |   addResult(result: string) {
 43 |     this._result.push(result);
 44 |   }
 45 | 
 46 |   addError(error: string) {
 47 |     this._result.push(error);
 48 |     this._isError = true;
 49 |   }
 50 | 
 51 |   isError() {
 52 |     return this._isError;
 53 |   }
 54 | 
 55 |   result() {
 56 |     return this._result.join('\n');
 57 |   }
 58 | 
 59 |   addCode(code: string) {
 60 |     this._code.push(code);
 61 |   }
 62 | 
 63 |   code() {
 64 |     return this._code.join('\n');
 65 |   }
 66 | 
 67 |   addImage(image: { contentType: string, data: Buffer }) {
 68 |     this._images.push(image);
 69 |   }
 70 | 
 71 |   images() {
 72 |     return this._images;
 73 |   }
 74 | 
 75 |   setIncludeSnapshot() {
 76 |     this._includeSnapshot = true;
 77 |   }
 78 | 
 79 |   setIncludeTabs() {
 80 |     this._includeTabs = true;
 81 |   }
 82 | 
 83 |   async finish() {
 84 |     // All the async snapshotting post-action is happening here.
 85 |     // Everything below should race against modal states.
 86 |     if (this._includeSnapshot && this._context.currentTab())
 87 |       this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
 88 |     for (const tab of this._context.tabs())
 89 |       await tab.updateTitle();
 90 |   }
 91 | 
 92 |   tabSnapshot(): TabSnapshot | undefined {
 93 |     return this._tabSnapshot;
 94 |   }
 95 | 
 96 |   serialize(): { content: (TextContent | ImageContent)[], isError?: boolean } {
 97 |     const response: string[] = [];
 98 | 
 99 |     // Start with command result.
100 |     if (this._result.length) {
101 |       response.push('### Result');
102 |       response.push(this._result.join('\n'));
103 |       response.push('');
104 |     }
105 | 
106 |     // Add code if it exists.
107 |     if (this._code.length) {
108 |       response.push(`### Ran Playwright code
109 | \`\`\`js
110 | ${this._code.join('\n')}
111 | \`\`\``);
112 |       response.push('');
113 |     }
114 | 
115 |     // List browser tabs.
116 |     if (this._includeSnapshot || this._includeTabs)
117 |       response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
118 | 
119 |     // Add snapshot if provided.
120 |     if (this._tabSnapshot?.modalStates.length) {
121 |       response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates));
122 |       response.push('');
123 |     } else if (this._tabSnapshot) {
124 |       response.push(renderTabSnapshot(this._tabSnapshot));
125 |       response.push('');
126 |     }
127 | 
128 |     // Main response part
129 |     const content: (TextContent | ImageContent)[] = [
130 |       { type: 'text', text: response.join('\n') },
131 |     ];
132 | 
133 |     // Image attachments.
134 |     if (this._context.config.imageResponses !== 'omit') {
135 |       for (const image of this._images)
136 |         content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType });
137 |     }
138 | 
139 |     return { content, isError: this._isError };
140 |   }
141 | }
142 | 
143 | function renderTabSnapshot(tabSnapshot: TabSnapshot): string {
144 |   const lines: string[] = [];
145 | 
146 |   if (tabSnapshot.consoleMessages.length) {
147 |     lines.push(`### New console messages`);
148 |     for (const message of tabSnapshot.consoleMessages)
149 |       lines.push(`- ${trim(message.toString(), 100)}`);
150 |     lines.push('');
151 |   }
152 | 
153 |   if (tabSnapshot.downloads.length) {
154 |     lines.push(`### Downloads`);
155 |     for (const entry of tabSnapshot.downloads) {
156 |       if (entry.finished)
157 |         lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
158 |       else
159 |         lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
160 |     }
161 |     lines.push('');
162 |   }
163 | 
164 |   lines.push(`### Page state`);
165 |   lines.push(`- Page URL: ${tabSnapshot.url}`);
166 |   lines.push(`- Page Title: ${tabSnapshot.title}`);
167 |   lines.push(`- Page Snapshot:`);
168 |   lines.push('```yaml');
169 |   lines.push(tabSnapshot.ariaSnapshot);
170 |   lines.push('```');
171 | 
172 |   return lines.join('\n');
173 | }
174 | 
175 | function renderTabsMarkdown(tabs: Tab[], force: boolean = false): string[] {
176 |   if (tabs.length === 1 && !force)
177 |     return [];
178 | 
179 |   if (!tabs.length) {
180 |     return [
181 |       '### Open tabs',
182 |       'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
183 |       '',
184 |     ];
185 |   }
186 | 
187 |   const lines: string[] = ['### Open tabs'];
188 |   for (let i = 0; i < tabs.length; i++) {
189 |     const tab = tabs[i];
190 |     const current = tab.isCurrentTab() ? ' (current)' : '';
191 |     lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
192 |   }
193 |   lines.push('');
194 |   return lines;
195 | }
196 | 
197 | function trim(text: string, maxLength: number) {
198 |   if (text.length <= maxLength)
199 |     return text;
200 |   return text.slice(0, maxLength) + '...';
201 | }
202 | 
```

--------------------------------------------------------------------------------
/src/mcp/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 debug from 'debug';
 18 | 
 19 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
 20 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
 21 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
 22 | import { httpAddressToString, installHttpTransport, startHttpServer } from './http.js';
 23 | import { InProcessTransport } from './inProcessTransport.js';
 24 | 
 25 | import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js';
 26 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
 27 | export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
 28 | export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js';
 29 | 
 30 | const serverDebug = debug('pw:mcp:server');
 31 | const errorsDebug = debug('pw:mcp:errors');
 32 | 
 33 | export type ClientVersion = { name: string, version: string };
 34 | 
 35 | export interface ServerBackend {
 36 |   initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise<void>;
 37 |   listTools(): Promise<Tool[]>;
 38 |   callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult>;
 39 |   serverClosed?(server: Server): void;
 40 | }
 41 | 
 42 | export type ServerBackendFactory = {
 43 |   name: string;
 44 |   nameInConfig: string;
 45 |   version: string;
 46 |   create: () => ServerBackend;
 47 | };
 48 | 
 49 | export async function connect(factory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) {
 50 |   const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat);
 51 |   await server.connect(transport);
 52 | }
 53 | 
 54 | export async function wrapInProcess(backend: ServerBackend): Promise<Transport> {
 55 |   const server = createServer('Internal', '0.0.0', backend, false);
 56 |   return new InProcessTransport(server);
 57 | }
 58 | 
 59 | export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server {
 60 |   let initializedPromiseResolve = () => {};
 61 |   const initializedPromise = new Promise<void>(resolve => initializedPromiseResolve = resolve);
 62 |   const server = new Server({ name, version }, {
 63 |     capabilities: {
 64 |       tools: {},
 65 |     }
 66 |   });
 67 | 
 68 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
 69 |     serverDebug('listTools');
 70 |     await initializedPromise;
 71 |     const tools = await backend.listTools();
 72 |     return { tools };
 73 |   });
 74 | 
 75 |   let heartbeatRunning = false;
 76 |   server.setRequestHandler(CallToolRequestSchema, async request => {
 77 |     serverDebug('callTool', request);
 78 |     await initializedPromise;
 79 | 
 80 |     if (runHeartbeat && !heartbeatRunning) {
 81 |       heartbeatRunning = true;
 82 |       startHeartbeat(server);
 83 |     }
 84 | 
 85 |     try {
 86 |       return await backend.callTool(request.params.name, request.params.arguments || {});
 87 |     } catch (error) {
 88 |       return {
 89 |         content: [{ type: 'text', text: '### Result\n' + String(error) }],
 90 |         isError: true,
 91 |       };
 92 |     }
 93 |   });
 94 |   addServerListener(server, 'initialized', async () => {
 95 |     try {
 96 |       const capabilities = server.getClientCapabilities();
 97 |       let clientRoots: Root[] = [];
 98 |       if (capabilities?.roots) {
 99 |         const { roots } = await server.listRoots(undefined, { timeout: 2_000 }).catch(() => ({ roots: [] }));
100 |         clientRoots = roots;
101 |       }
102 |       const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' };
103 |       await backend.initialize?.(server, clientVersion, clientRoots);
104 |       initializedPromiseResolve();
105 |     } catch (e) {
106 |       errorsDebug(e);
107 |     }
108 |   });
109 |   addServerListener(server, 'close', () => backend.serverClosed?.(server));
110 |   return server;
111 | }
112 | 
113 | const startHeartbeat = (server: Server) => {
114 |   const beat = () => {
115 |     Promise.race([
116 |       server.ping(),
117 |       new Promise((_, reject) => setTimeout(() => reject(new Error('ping timeout')), 5000)),
118 |     ]).then(() => {
119 |       setTimeout(beat, 3000);
120 |     }).catch(() => {
121 |       void server.close();
122 |     });
123 |   };
124 | 
125 |   beat();
126 | };
127 | 
128 | function addServerListener(server: Server, event: 'close' | 'initialized', listener: () => void) {
129 |   const oldListener = server[`on${event}`];
130 |   server[`on${event}`] = () => {
131 |     oldListener?.();
132 |     listener();
133 |   };
134 | }
135 | 
136 | export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) {
137 |   if (options.port === undefined) {
138 |     await connect(serverBackendFactory, new StdioServerTransport(), false);
139 |     return;
140 |   }
141 | 
142 |   const httpServer = await startHttpServer(options);
143 |   await installHttpTransport(httpServer, serverBackendFactory);
144 |   const url = httpAddressToString(httpServer.address());
145 | 
146 |   const mcpConfig: any = { mcpServers: { } };
147 |   mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = {
148 |     url: `${url}/mcp`
149 |   };
150 |   const message = [
151 |     `Listening on ${url}`,
152 |     'Put this in your client config:',
153 |     JSON.stringify(mcpConfig, undefined, 2),
154 |     'For legacy SSE transport support, you can use the /sse endpoint instead.',
155 |   ].join('\n');
156 |     // eslint-disable-next-line no-console
157 |   console.error(message);
158 | }
159 | 
```

--------------------------------------------------------------------------------
/src/tools/verify.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 { defineTabTool } from './tool.js';
 20 | import * as javascript from '../utils/codegen.js';
 21 | import { generateLocator } from './utils.js';
 22 | 
 23 | const verifyElement = defineTabTool({
 24 |   capability: 'verify',
 25 |   schema: {
 26 |     name: 'browser_verify_element_visible',
 27 |     title: 'Verify element visible',
 28 |     description: 'Verify element is visible on the page',
 29 |     inputSchema: z.object({
 30 |       role: z.string().describe('ROLE of the element. Can be found in the snapshot like this: \`- {ROLE} "Accessible Name":\`'),
 31 |       accessibleName: z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: \`- role "{ACCESSIBLE_NAME}"\`'),
 32 |     }),
 33 |     type: 'readOnly',
 34 |   },
 35 | 
 36 |   handle: async (tab, params, response) => {
 37 |     const locator = tab.page.getByRole(params.role as any, { name: params.accessibleName });
 38 |     if (await locator.count() === 0) {
 39 |       response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`);
 40 |       return;
 41 |     }
 42 | 
 43 |     response.addCode(`await expect(page.getByRole(${javascript.escapeWithQuotes(params.role)}, { name: ${javascript.escapeWithQuotes(params.accessibleName)} })).toBeVisible();`);
 44 |     response.addResult('Done');
 45 |   },
 46 | });
 47 | 
 48 | const verifyText = defineTabTool({
 49 |   capability: 'verify',
 50 |   schema: {
 51 |     name: 'browser_verify_text_visible',
 52 |     title: 'Verify text visible',
 53 |     description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`,
 54 |     inputSchema: z.object({
 55 |       text: z.string().describe('TEXT to verify. Can be found in the snapshot like this: \`- role "Accessible Name": {TEXT}\` or like this: \`- text: {TEXT}\`'),
 56 |     }),
 57 |     type: 'readOnly',
 58 |   },
 59 | 
 60 |   handle: async (tab, params, response) => {
 61 |     const locator = tab.page.getByText(params.text).filter({ visible: true });
 62 |     if (await locator.count() === 0) {
 63 |       response.addError('Text not found');
 64 |       return;
 65 |     }
 66 | 
 67 |     response.addCode(`await expect(page.getByText(${javascript.escapeWithQuotes(params.text)})).toBeVisible();`);
 68 |     response.addResult('Done');
 69 |   },
 70 | });
 71 | 
 72 | const verifyList = defineTabTool({
 73 |   capability: 'verify',
 74 |   schema: {
 75 |     name: 'browser_verify_list_visible',
 76 |     title: 'Verify list visible',
 77 |     description: 'Verify list is visible on the page',
 78 |     inputSchema: z.object({
 79 |       element: z.string().describe('Human-readable list description'),
 80 |       ref: z.string().describe('Exact target element reference that points to the list'),
 81 |       items: z.array(z.string()).describe('Items to verify'),
 82 |     }),
 83 |     type: 'readOnly',
 84 |   },
 85 | 
 86 |   handle: async (tab, params, response) => {
 87 |     const locator = await tab.refLocator({ ref: params.ref, element: params.element });
 88 |     const itemTexts: string[] = [];
 89 |     for (const item of params.items) {
 90 |       const itemLocator = locator.getByText(item);
 91 |       if (await itemLocator.count() === 0) {
 92 |         response.addError(`Item "${item}" not found`);
 93 |         return;
 94 |       }
 95 |       itemTexts.push((await itemLocator.textContent())!);
 96 |     }
 97 |     const ariaSnapshot = `\`
 98 | - list:
 99 | ${itemTexts.map(t => `  - listitem: ${javascript.escapeWithQuotes(t, '"')}`).join('\n')}
100 | \``;
101 |     response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`);
102 |     response.addResult('Done');
103 |   },
104 | });
105 | 
106 | const verifyValue = defineTabTool({
107 |   capability: 'verify',
108 |   schema: {
109 |     name: 'browser_verify_value',
110 |     title: 'Verify value',
111 |     description: 'Verify element value',
112 |     inputSchema: z.object({
113 |       type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the element'),
114 |       element: z.string().describe('Human-readable element description'),
115 |       ref: z.string().describe('Exact target element reference that points to the element'),
116 |       value: z.string().describe('Value to verify. For checkbox, use "true" or "false".'),
117 |     }),
118 |     type: 'readOnly',
119 |   },
120 | 
121 |   handle: async (tab, params, response) => {
122 |     const locator = await tab.refLocator({ ref: params.ref, element: params.element });
123 |     const locatorSource = `page.${await generateLocator(locator)}`;
124 |     if (params.type === 'textbox' || params.type === 'slider' || params.type === 'combobox') {
125 |       const value = await locator.inputValue();
126 |       if (value !== params.value) {
127 |         response.addError(`Expected value "${params.value}", but got "${value}"`);
128 |         return;
129 |       }
130 |       response.addCode(`await expect(${locatorSource}).toHaveValue(${javascript.quote(params.value)});`);
131 |     } else if (params.type === 'checkbox' || params.type === 'radio') {
132 |       const value = await locator.isChecked();
133 |       if (value !== (params.value === 'true')) {
134 |         response.addError(`Expected value "${params.value}", but got "${value}"`);
135 |         return;
136 |       }
137 |       const matcher = value ? 'toBeChecked' : 'not.toBeChecked';
138 |       response.addCode(`await expect(${locatorSource}).${matcher}();`);
139 |     }
140 |     response.addResult('Done');
141 |   },
142 | });
143 | 
144 | export default [
145 |   verifyElement,
146 |   verifyText,
147 |   verifyList,
148 |   verifyValue,
149 | ];
150 | 
```

--------------------------------------------------------------------------------
/src/program.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 { program, Option } from 'commander';
 18 | import * as mcpServer from './mcp/server.js';
 19 | import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
 20 | import { packageJSON } from './utils/package.js';
 21 | import { Context } from './context.js';
 22 | import { contextFactory } from './browserContextFactory.js';
 23 | import { ProxyBackend } from './mcp/proxyBackend.js';
 24 | import { BrowserServerBackend } from './browserServerBackend.js';
 25 | import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
 26 | 
 27 | import { runVSCodeTools } from './vscode/host.js';
 28 | import type { MCPProvider } from './mcp/proxyBackend.js';
 29 | 
 30 | program
 31 |     .version('Version ' + packageJSON.version)
 32 |     .name(packageJSON.name)
 33 |     .option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
 34 |     .option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
 35 |     .option('--block-service-workers', 'block service workers')
 36 |     .option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
 37 |     .option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
 38 |     .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
 39 |     .option('--config <path>', 'path to the configuration file.')
 40 |     .option('--device <device>', 'device to emulate, for example: "iPhone 15"')
 41 |     .option('--executable-path <path>', 'path to the browser executable.')
 42 |     .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.')
 43 |     .option('--headless', 'run browser in headless mode, headed by default')
 44 |     .option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
 45 |     .option('--ignore-https-errors', 'ignore https errors')
 46 |     .option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
 47 |     .option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
 48 |     .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
 49 |     .option('--output-dir <path>', 'path to the directory for output files.')
 50 |     .option('--port <port>', 'port to listen on for SSE transport.')
 51 |     .option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
 52 |     .option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
 53 |     .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.')
 54 |     .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
 55 |     .option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
 56 |     .option('--user-agent <ua string>', 'specify user agent string')
 57 |     .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
 58 |     .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
 59 |     .option('--navigation-timeout <ms>', 'maximum time in milliseconds for page navigation. Defaults to 60000ms (60 seconds).', parseInt)
 60 |     .option('--default-timeout <ms>', 'default timeout for all Playwright operations (clicks, fills, etc). Defaults to 5000ms (5 seconds).', parseInt)
 61 |     .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
 62 |     .addOption(new Option('--vscode', 'VS Code tools.').hideHelp())
 63 |     .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
 64 |     .action(async options => {
 65 |       setupExitWatchdog();
 66 | 
 67 |       if (options.vision) {
 68 |         // eslint-disable-next-line no-console
 69 |         console.error('The --vision option is deprecated, use --caps=vision instead');
 70 |         options.caps = 'vision';
 71 |       }
 72 | 
 73 |       const config = await resolveCLIConfig(options);
 74 |       const browserContextFactory = contextFactory(config);
 75 |       const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath);
 76 | 
 77 |       if (options.extension) {
 78 |         const serverBackendFactory: mcpServer.ServerBackendFactory = {
 79 |           name: 'Playwright w/ extension',
 80 |           nameInConfig: 'playwright-extension',
 81 |           version: packageJSON.version,
 82 |           create: () => new BrowserServerBackend(config, extensionContextFactory)
 83 |         };
 84 |         await mcpServer.start(serverBackendFactory, config.server);
 85 |         return;
 86 |       }
 87 | 
 88 |       if (options.vscode) {
 89 |         await runVSCodeTools(config);
 90 |         return;
 91 |       }
 92 | 
 93 |       if (options.connectTool) {
 94 |         const providers: MCPProvider[] = [
 95 |           {
 96 |             name: 'default',
 97 |             description: 'Starts standalone browser',
 98 |             connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)),
 99 |           },
100 |           {
101 |             name: 'extension',
102 |             description: 'Connect to a browser using the Playwright MCP extension',
103 |             connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)),
104 |           },
105 |         ];
106 |         const factory: mcpServer.ServerBackendFactory = {
107 |           name: 'Playwright w/ switch',
108 |           nameInConfig: 'playwright-switch',
109 |           version: packageJSON.version,
110 |           create: () => new ProxyBackend(providers),
111 |         };
112 |         await mcpServer.start(factory, config.server);
113 |         return;
114 |       }
115 | 
116 |       const factory: mcpServer.ServerBackendFactory = {
117 |         name: 'Playwright',
118 |         nameInConfig: 'playwright',
119 |         version: packageJSON.version,
120 |         create: () => new BrowserServerBackend(config, browserContextFactory)
121 |       };
122 |       await mcpServer.start(factory, config.server);
123 |     });
124 | 
125 | function setupExitWatchdog() {
126 |   let isExiting = false;
127 |   const handleExit = async () => {
128 |     if (isExiting)
129 |       return;
130 |     isExiting = true;
131 |     setTimeout(() => process.exit(0), 15000);
132 |     await Context.disposeAll();
133 |     process.exit(0);
134 |   };
135 | 
136 |   process.stdin.on('close', handleExit);
137 |   process.on('SIGINT', handleExit);
138 |   process.on('SIGTERM', handleExit);
139 | }
140 | 
141 | void program.parseAsync(process.argv);
142 | 
```
Page 1/2FirstPrevNextLast