This is page 1 of 2. Use http://codebase.md/microsoft/playwright-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .github
│ └── workflows
│ ├── ci.yml
│ └── publish.yml
├── .gitignore
├── .npmignore
├── cli.js
├── config.d.ts
├── CONTRIBUTING.md
├── Dockerfile
├── extension
│ ├── icons
│ │ ├── icon-128.png
│ │ ├── icon-16.png
│ │ ├── icon-32.png
│ │ └── icon-48.png
│ ├── manifest.json
│ ├── package-lock.json
│ ├── package.json
│ ├── playwright.config.ts
│ ├── README.md
│ ├── src
│ │ ├── background.ts
│ │ ├── relayConnection.ts
│ │ └── ui
│ │ ├── authToken.css
│ │ ├── authToken.tsx
│ │ ├── colors.css
│ │ ├── connect.css
│ │ ├── connect.html
│ │ ├── connect.tsx
│ │ ├── copyToClipboard.css
│ │ ├── copyToClipboard.tsx
│ │ ├── icons.css
│ │ ├── icons.tsx
│ │ ├── status.html
│ │ ├── status.tsx
│ │ ├── tabItem.tsx
│ │ └── tsconfig.json
│ ├── tests
│ │ └── extension.spec.ts
│ ├── tsconfig.json
│ ├── tsconfig.ui.json
│ ├── vite.config.mts
│ └── vite.sw.config.mts
├── index.d.ts
├── index.js
├── LICENSE
├── package-lock.json
├── package.json
├── playwright.config.ts
├── README.md
├── SECURITY.md
├── src
│ └── README.md
├── tests
│ ├── capabilities.spec.ts
│ ├── click.spec.ts
│ ├── core.spec.ts
│ ├── fixtures.ts
│ ├── library.spec.ts
│ └── testserver
│ ├── cert.pem
│ ├── index.ts
│ ├── key.pem
│ └── san.cnf
└── update-readme.js
```
# Files
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
1 | **/*
2 | README.md
3 | LICENSE
4 | !cli.js
5 | !index.*
6 | !config.d.ts
7 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | lib/
2 | dist/
3 | node_modules/
4 | test-results/
5 | playwright-report/
6 | .vscode/mcp.json
7 | .idea
8 | .DS_Store
9 | .env
10 | sessions/
11 |
```
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Where is the source?
2 |
3 | Playwright MCP source code is located in the Playwright monorepo. Please refer to the contributor's guide in [CONTRIBUTING.md](../CONTRIBUTING.md) for more details.
4 |
```
--------------------------------------------------------------------------------
/extension/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Playwright MCP Chrome Extension
2 |
3 | ## Introduction
4 |
5 | The Playwright MCP Chrome Extension allows you to connect to pages in your existing browser and leverage the state of your default user profile. This means the AI assistant can interact with websites where you're already logged in, using your existing cookies, sessions, and browser state, providing a seamless experience without requiring separate authentication or setup.
6 |
7 | ## Prerequisites
8 |
9 | - Chrome/Edge/Chromium browser
10 |
11 | ## Installation Steps
12 |
13 | ### Download the Extension
14 |
15 | Download the latest Chrome extension from GitHub:
16 | - **Download link**: https://github.com/microsoft/playwright-mcp/releases
17 |
18 | ### Load Chrome Extension
19 |
20 | 1. Open Chrome and navigate to `chrome://extensions/`
21 | 2. Enable "Developer mode" (toggle in the top right corner)
22 | 3. Click "Load unpacked" and select the extension directory
23 |
24 | ### Configure Playwright MCP server
25 |
26 | Configure Playwright MCP server to connect to the browser using the extension by passing the `--extension` option when running the MCP server:
27 |
28 | ```json
29 | {
30 | "mcpServers": {
31 | "playwright-extension": {
32 | "command": "npx",
33 | "args": [
34 | "@playwright/mcp@latest",
35 | "--extension"
36 | ]
37 | }
38 | }
39 | }
40 | ```
41 |
42 | ## Usage
43 |
44 | ### Browser Tab Selection
45 |
46 | When the LLM interacts with the browser for the first time, it will load a page where you can select which browser tab the LLM will connect to. This allows you to control which specific page the AI assistant will interact with during the session.
47 |
48 |
49 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Playwright MCP
2 |
3 | A Model Context Protocol (MCP) server that provides browser automation capabilities using [Playwright](https://playwright.dev). This server enables LLMs to interact with web pages through structured accessibility snapshots, bypassing the need for screenshots or visually-tuned models.
4 |
5 | ### Key Features
6 |
7 | - **Fast and lightweight**. Uses Playwright's accessibility tree, not pixel-based input.
8 | - **LLM-friendly**. No vision models needed, operates purely on structured data.
9 | - **Deterministic tool application**. Avoids ambiguity common with screenshot-based approaches.
10 |
11 | ### Requirements
12 | - Node.js 18 or newer
13 | - VS Code, Cursor, Windsurf, Claude Desktop, Goose or any other MCP client
14 |
15 | <!--
16 | // Generate using:
17 | node utils/generate-links.js
18 | -->
19 |
20 | ### Getting started
21 |
22 | First, install the Playwright MCP server with your client.
23 |
24 | **Standard config** works in most of the tools:
25 |
26 | ```js
27 | {
28 | "mcpServers": {
29 | "playwright": {
30 | "command": "npx",
31 | "args": [
32 | "@playwright/mcp@latest"
33 | ]
34 | }
35 | }
36 | }
37 | ```
38 |
39 | [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
40 |
41 | <details>
42 | <summary>Amp</summary>
43 |
44 | Add via the Amp VS Code extension settings screen or by updating your settings.json file:
45 |
46 | ```json
47 | "amp.mcpServers": {
48 | "playwright": {
49 | "command": "npx",
50 | "args": [
51 | "@playwright/mcp@latest"
52 | ]
53 | }
54 | }
55 | ```
56 |
57 | **Amp CLI Setup:**
58 |
59 | Add via the `amp mcp add`command below
60 |
61 | ```bash
62 | amp mcp add playwright -- npx @playwright/mcp@latest
63 | ```
64 |
65 | </details>
66 |
67 | <details>
68 | <summary>Claude Code</summary>
69 |
70 | Use the Claude Code CLI to add the Playwright MCP server:
71 |
72 | ```bash
73 | claude mcp add playwright npx @playwright/mcp@latest
74 | ```
75 | </details>
76 |
77 | <details>
78 | <summary>Claude Desktop</summary>
79 |
80 | Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user), use the standard config above.
81 |
82 | </details>
83 |
84 | <details>
85 | <summary>Codex</summary>
86 |
87 | Use the Codex CLI to add the Playwright MCP server:
88 |
89 | ```bash
90 | codex mcp add playwright npx "@playwright/mcp@latest"
91 | ```
92 |
93 | Alternatively, create or edit the configuration file `~/.codex/config.toml` and add:
94 |
95 | ```toml
96 | [mcp_servers.playwright]
97 | command = "npx"
98 | args = ["@playwright/mcp@latest"]
99 | ```
100 |
101 | For more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers).
102 |
103 | </details>
104 |
105 | <details>
106 | <summary>Cursor</summary>
107 |
108 | #### Click the button to install:
109 |
110 | [<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
111 |
112 | #### Or install manually:
113 |
114 | Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @playwright/mcp@latest`. You can also verify config or add command like arguments via clicking `Edit`.
115 |
116 | </details>
117 |
118 | <details>
119 | <summary>Factory</summary>
120 |
121 | Use the Factory CLI to add the Playwright MCP server:
122 |
123 | ```bash
124 | droid mcp add playwright "npx @playwright/mcp@latest"
125 | ```
126 |
127 | Alternatively, type `/mcp` within Factory droid to open an interactive UI for managing MCP servers.
128 |
129 | For more information, see the [Factory MCP documentation](https://docs.factory.ai/cli/configuration/mcp).
130 |
131 | </details>
132 |
133 | <details>
134 | <summary>Gemini CLI</summary>
135 |
136 | Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#configure-the-mcp-server-in-settingsjson), use the standard config above.
137 |
138 | </details>
139 |
140 | <details>
141 | <summary>Goose</summary>
142 |
143 | #### Click the button to install:
144 |
145 | [](https://block.github.io/goose/extension?cmd=npx&arg=%40playwright%2Fmcp%40latest&id=playwright&name=Playwright&description=Interact%20with%20web%20pages%20through%20structured%20accessibility%20snapshots%20using%20Playwright)
146 |
147 | #### Or install manually:
148 |
149 | Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click "Add Extension".
150 | </details>
151 |
152 | <details>
153 | <summary>Kiro</summary>
154 |
155 | Follow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`:
156 |
157 | ```json
158 | {
159 | "mcpServers": {
160 | "playwright": {
161 | "command": "npx",
162 | "args": [
163 | "@playwright/mcp@latest"
164 | ]
165 | }
166 | }
167 | }
168 | ```
169 | </details>
170 |
171 | <details>
172 | <summary>LM Studio</summary>
173 |
174 | #### Click the button to install:
175 |
176 | [](https://lmstudio.ai/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAcGxheXdyaWdodC9tY3BAbGF0ZXN0Il19)
177 |
178 | #### Or install manually:
179 |
180 | Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.
181 | </details>
182 |
183 | <details>
184 | <summary>opencode</summary>
185 |
186 | Follow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`:
187 |
188 | ```json
189 | {
190 | "$schema": "https://opencode.ai/config.json",
191 | "mcp": {
192 | "playwright": {
193 | "type": "local",
194 | "command": [
195 | "npx",
196 | "@playwright/mcp@latest"
197 | ],
198 | "enabled": true
199 | }
200 | }
201 | }
202 |
203 | ```
204 | </details>
205 |
206 | <details>
207 | <summary>Qodo Gen</summary>
208 |
209 | Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ → Connect more tools → + Add new MCP → Paste the standard config above.
210 |
211 | Click <code>Save</code>.
212 | </details>
213 |
214 | <details>
215 | <summary>VS Code</summary>
216 |
217 | #### Click the button to install:
218 |
219 | [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
220 |
221 | #### Or install manually:
222 |
223 | Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server), use the standard config above. You can also install the Playwright MCP server using the VS Code CLI:
224 |
225 | ```bash
226 | # For VS Code
227 | code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'
228 | ```
229 |
230 | After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
231 | </details>
232 |
233 | <details>
234 | <summary>Warp</summary>
235 |
236 | Go to `Settings` -> `AI` -> `Manage MCP Servers` -> `+ Add` to [add an MCP Server](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server). Use the standard config above.
237 |
238 | Alternatively, use the slash command `/add-mcp` in the Warp prompt and paste the standard config from above:
239 | ```js
240 | {
241 | "mcpServers": {
242 | "playwright": {
243 | "command": "npx",
244 | "args": [
245 | "@playwright/mcp@latest"
246 | ]
247 | }
248 | }
249 | }
250 | ```
251 |
252 | </details>
253 |
254 | <details>
255 | <summary>Windsurf</summary>
256 |
257 | Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use the standard config above.
258 |
259 | </details>
260 |
261 | ### Configuration
262 |
263 | Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list:
264 |
265 | <!--- Options generated by update-readme.js -->
266 |
267 | ```
268 | > npx @playwright/mcp@latest --help
269 | --allowed-hosts <hosts...> comma-separated list of hosts this
270 | server is allowed to serve from.
271 | Defaults to the host the server is bound
272 | to. Pass '*' to disable the host check.
273 | --allowed-origins <origins> semicolon-separated list of origins to
274 | allow the browser to request. Default is
275 | to allow all.
276 | --blocked-origins <origins> semicolon-separated list of origins to
277 | block the browser from requesting.
278 | Blocklist is evaluated before allowlist.
279 | If used without the allowlist, requests
280 | not matching the blocklist are still
281 | allowed.
282 | --block-service-workers block service workers
283 | --browser <browser> browser or chrome channel to use,
284 | possible values: chrome, firefox,
285 | webkit, msedge.
286 | --caps <caps> comma-separated list of additional
287 | capabilities to enable, possible values:
288 | vision, pdf.
289 | --cdp-endpoint <endpoint> CDP endpoint to connect to.
290 | --cdp-header <headers...> CDP headers to send with the connect
291 | request, multiple can be specified.
292 | --config <path> path to the configuration file.
293 | --device <device> device to emulate, for example: "iPhone
294 | 15"
295 | --executable-path <path> path to the browser executable.
296 | --extension Connect to a running browser instance
297 | (Edge/Chrome only). Requires the
298 | "Playwright MCP Bridge" browser
299 | extension to be installed.
300 | --grant-permissions <permissions...> List of permissions to grant to the
301 | browser context, for example
302 | "geolocation", "clipboard-read",
303 | "clipboard-write".
304 | --headless run browser in headless mode, headed by
305 | default
306 | --host <host> host to bind server to. Default is
307 | localhost. Use 0.0.0.0 to bind to all
308 | interfaces.
309 | --ignore-https-errors ignore https errors
310 | --init-script <path...> path to JavaScript file to add as an
311 | initialization script. The script will
312 | be evaluated in every page before any of
313 | the page's scripts. Can be specified
314 | multiple times.
315 | --isolated keep the browser profile in memory, do
316 | not save it to disk.
317 | --image-responses <mode> whether to send image responses to the
318 | client. Can be "allow" or "omit",
319 | Defaults to "allow".
320 | --no-sandbox disable the sandbox for all process
321 | types that are normally sandboxed.
322 | --output-dir <path> path to the directory for output files.
323 | --port <port> port to listen on for SSE transport.
324 | --proxy-bypass <bypass> comma-separated domains to bypass proxy,
325 | for example
326 | ".com,chromium.org,.domain.com"
327 | --proxy-server <proxy> specify proxy server, for example
328 | "http://myproxy:3128" or
329 | "socks5://myproxy:8080"
330 | --save-session Whether to save the Playwright MCP
331 | session into the output directory.
332 | --save-trace Whether to save the Playwright Trace of
333 | the session into the output directory.
334 | --save-video <size> Whether to save the video of the session
335 | into the output directory. For example
336 | "--save-video=800x600"
337 | --secrets <path> path to a file containing secrets in the
338 | dotenv format
339 | --shared-browser-context reuse the same browser context between
340 | all connected HTTP clients.
341 | --storage-state <path> path to the storage state file for
342 | isolated sessions.
343 | --test-id-attribute <attribute> specify the attribute to use for test
344 | ids, defaults to "data-testid"
345 | --timeout-action <timeout> specify action timeout in milliseconds,
346 | defaults to 5000ms
347 | --timeout-navigation <timeout> specify navigation timeout in
348 | milliseconds, defaults to 60000ms
349 | --user-agent <ua string> specify user agent string
350 | --user-data-dir <path> path to the user data directory. If not
351 | specified, a temporary directory will be
352 | created.
353 | --viewport-size <size> specify browser viewport size in pixels,
354 | for example "1280x720"
355 | ```
356 |
357 | <!--- End of options generated section -->
358 |
359 | ### User profile
360 |
361 | You can run Playwright MCP with persistent profile like a regular browser (default), in isolated contexts for testing sessions, or connect to your existing browser using the browser extension.
362 |
363 | **Persistent profile**
364 |
365 | All the logged in information will be stored in the persistent profile, you can delete it between sessions if you'd like to clear the offline state.
366 | Persistent profile is located at the following locations and you can override it with the `--user-data-dir` argument.
367 |
368 | ```bash
369 | # Windows
370 | %USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile
371 |
372 | # macOS
373 | - ~/Library/Caches/ms-playwright/mcp-{channel}-profile
374 |
375 | # Linux
376 | - ~/.cache/ms-playwright/mcp-{channel}-profile
377 | ```
378 |
379 | **Isolated**
380 |
381 | In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser,
382 | the session is closed and all the storage state for this session is lost. You can provide initial storage state
383 | to the browser via the config's `contextOptions` or via the `--storage-state` argument. Learn more about the storage
384 | state [here](https://playwright.dev/docs/auth).
385 |
386 | ```js
387 | {
388 | "mcpServers": {
389 | "playwright": {
390 | "command": "npx",
391 | "args": [
392 | "@playwright/mcp@latest",
393 | "--isolated",
394 | "--storage-state={path/to/storage.json}"
395 | ]
396 | }
397 | }
398 | }
399 | ```
400 |
401 | **Browser Extension**
402 |
403 | The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [extension/README.md](extension/README.md) for installation and setup instructions.
404 |
405 | ### Configuration file
406 |
407 | The Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file
408 | using the `--config` command line option:
409 |
410 | ```bash
411 | npx @playwright/mcp@latest --config path/to/config.json
412 | ```
413 |
414 | <details>
415 | <summary>Configuration file schema</summary>
416 |
417 | ```typescript
418 | {
419 | // Browser configuration
420 | browser?: {
421 | // Browser type to use (chromium, firefox, or webkit)
422 | browserName?: 'chromium' | 'firefox' | 'webkit';
423 |
424 | // Keep the browser profile in memory, do not save it to disk.
425 | isolated?: boolean;
426 |
427 | // Path to user data directory for browser profile persistence
428 | userDataDir?: string;
429 |
430 | // Browser launch options (see Playwright docs)
431 | // @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch
432 | launchOptions?: {
433 | channel?: string; // Browser channel (e.g. 'chrome')
434 | headless?: boolean; // Run in headless mode
435 | executablePath?: string; // Path to browser executable
436 | // ... other Playwright launch options
437 | };
438 |
439 | // Browser context options
440 | // @see https://playwright.dev/docs/api/class-browser#browser-new-context
441 | contextOptions?: {
442 | viewport?: { width: number, height: number };
443 | // ... other Playwright context options
444 | };
445 |
446 | // CDP endpoint for connecting to existing browser
447 | cdpEndpoint?: string;
448 |
449 | // Remote Playwright server endpoint
450 | remoteEndpoint?: string;
451 | },
452 |
453 | // Server configuration
454 | server?: {
455 | port?: number; // Port to listen on
456 | host?: string; // Host to bind to (default: localhost)
457 | },
458 |
459 | // List of additional capabilities
460 | capabilities?: Array<
461 | 'tabs' | // Tab management
462 | 'install' | // Browser installation
463 | 'pdf' | // PDF generation
464 | 'vision' | // Coordinate-based interactions
465 | >;
466 |
467 | // Directory for output files
468 | outputDir?: string;
469 |
470 | // Network configuration
471 | network?: {
472 | // List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
473 | allowedOrigins?: string[];
474 |
475 | // List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
476 | blockedOrigins?: string[];
477 | };
478 |
479 | /**
480 | * Whether to send image responses to the client. Can be "allow" or "omit".
481 | * Defaults to "allow".
482 | */
483 | imageResponses?: 'allow' | 'omit';
484 | }
485 | ```
486 | </details>
487 |
488 | ### Standalone MCP server
489 |
490 | When running headed browser on system w/o display or from worker processes of the IDEs,
491 | run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport.
492 |
493 | ```bash
494 | npx @playwright/mcp@latest --port 8931
495 | ```
496 |
497 | And then in MCP client config, set the `url` to the HTTP endpoint:
498 |
499 | ```js
500 | {
501 | "mcpServers": {
502 | "playwright": {
503 | "url": "http://localhost:8931/mcp"
504 | }
505 | }
506 | }
507 | ```
508 |
509 | <details>
510 | <summary><b>Docker</b></summary>
511 |
512 | **NOTE:** The Docker implementation only supports headless chromium at the moment.
513 |
514 | ```js
515 | {
516 | "mcpServers": {
517 | "playwright": {
518 | "command": "docker",
519 | "args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"]
520 | }
521 | }
522 | }
523 | ```
524 |
525 | Or If you prefer to run the container as a long-lived service instead of letting the MCP client spawn it, use:
526 |
527 | ```
528 | docker run -d -i --rm --init --pull=always \
529 | --entrypoint node \
530 | --name playwright \
531 | -p 8931:8931 \
532 | mcr.microsoft.com/playwright/mcp \
533 | cli.js --headless --browser chromium --no-sandbox --port 8931
534 | ```
535 |
536 | The server will listen on host port **8931** and can be reached by any MCP client.
537 |
538 | You can build the Docker image yourself.
539 |
540 | ```
541 | docker build -t mcr.microsoft.com/playwright/mcp .
542 | ```
543 | </details>
544 |
545 | <details>
546 | <summary><b>Programmatic usage</b></summary>
547 |
548 | ```js
549 | import http from 'http';
550 |
551 | import { createConnection } from '@playwright/mcp';
552 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
553 |
554 | http.createServer(async (req, res) => {
555 | // ...
556 |
557 | // Creates a headless Playwright MCP server with SSE transport
558 | const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
559 | const transport = new SSEServerTransport('/messages', res);
560 | await connection.connect(transport);
561 |
562 | // ...
563 | });
564 | ```
565 | </details>
566 |
567 | ### Tools
568 |
569 | <!--- Tools generated by update-readme.js -->
570 |
571 | <details>
572 | <summary><b>Core automation</b></summary>
573 |
574 | <!-- NOTE: This has been generated via update-readme.js -->
575 |
576 | - **browser_click**
577 | - Title: Click
578 | - Description: Perform click on a web page
579 | - Parameters:
580 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
581 | - `ref` (string): Exact target element reference from the page snapshot
582 | - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
583 | - `button` (string, optional): Button to click, defaults to left
584 | - `modifiers` (array, optional): Modifier keys to press
585 | - Read-only: **false**
586 |
587 | <!-- NOTE: This has been generated via update-readme.js -->
588 |
589 | - **browser_close**
590 | - Title: Close browser
591 | - Description: Close the page
592 | - Parameters: None
593 | - Read-only: **false**
594 |
595 | <!-- NOTE: This has been generated via update-readme.js -->
596 |
597 | - **browser_console_messages**
598 | - Title: Get console messages
599 | - Description: Returns all console messages
600 | - Parameters:
601 | - `onlyErrors` (boolean, optional): Only return error messages
602 | - Read-only: **true**
603 |
604 | <!-- NOTE: This has been generated via update-readme.js -->
605 |
606 | - **browser_drag**
607 | - Title: Drag mouse
608 | - Description: Perform drag and drop between two elements
609 | - Parameters:
610 | - `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element
611 | - `startRef` (string): Exact source element reference from the page snapshot
612 | - `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element
613 | - `endRef` (string): Exact target element reference from the page snapshot
614 | - Read-only: **false**
615 |
616 | <!-- NOTE: This has been generated via update-readme.js -->
617 |
618 | - **browser_evaluate**
619 | - Title: Evaluate JavaScript
620 | - Description: Evaluate JavaScript expression on page or element
621 | - Parameters:
622 | - `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided
623 | - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element
624 | - `ref` (string, optional): Exact target element reference from the page snapshot
625 | - Read-only: **false**
626 |
627 | <!-- NOTE: This has been generated via update-readme.js -->
628 |
629 | - **browser_file_upload**
630 | - Title: Upload files
631 | - Description: Upload one or multiple files
632 | - Parameters:
633 | - `paths` (array, optional): The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.
634 | - Read-only: **false**
635 |
636 | <!-- NOTE: This has been generated via update-readme.js -->
637 |
638 | - **browser_fill_form**
639 | - Title: Fill form
640 | - Description: Fill multiple form fields
641 | - Parameters:
642 | - `fields` (array): Fields to fill in
643 | - Read-only: **false**
644 |
645 | <!-- NOTE: This has been generated via update-readme.js -->
646 |
647 | - **browser_handle_dialog**
648 | - Title: Handle a dialog
649 | - Description: Handle a dialog
650 | - Parameters:
651 | - `accept` (boolean): Whether to accept the dialog.
652 | - `promptText` (string, optional): The text of the prompt in case of a prompt dialog.
653 | - Read-only: **false**
654 |
655 | <!-- NOTE: This has been generated via update-readme.js -->
656 |
657 | - **browser_hover**
658 | - Title: Hover mouse
659 | - Description: Hover over element on page
660 | - Parameters:
661 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
662 | - `ref` (string): Exact target element reference from the page snapshot
663 | - Read-only: **false**
664 |
665 | <!-- NOTE: This has been generated via update-readme.js -->
666 |
667 | - **browser_navigate**
668 | - Title: Navigate to a URL
669 | - Description: Navigate to a URL
670 | - Parameters:
671 | - `url` (string): The URL to navigate to
672 | - Read-only: **false**
673 |
674 | <!-- NOTE: This has been generated via update-readme.js -->
675 |
676 | - **browser_navigate_back**
677 | - Title: Go back
678 | - Description: Go back to the previous page
679 | - Parameters: None
680 | - Read-only: **false**
681 |
682 | <!-- NOTE: This has been generated via update-readme.js -->
683 |
684 | - **browser_network_requests**
685 | - Title: List network requests
686 | - Description: Returns all network requests since loading the page
687 | - Parameters: None
688 | - Read-only: **true**
689 |
690 | <!-- NOTE: This has been generated via update-readme.js -->
691 |
692 | - **browser_press_key**
693 | - Title: Press a key
694 | - Description: Press a key on the keyboard
695 | - Parameters:
696 | - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
697 | - Read-only: **false**
698 |
699 | <!-- NOTE: This has been generated via update-readme.js -->
700 |
701 | - **browser_resize**
702 | - Title: Resize browser window
703 | - Description: Resize the browser window
704 | - Parameters:
705 | - `width` (number): Width of the browser window
706 | - `height` (number): Height of the browser window
707 | - Read-only: **false**
708 |
709 | <!-- NOTE: This has been generated via update-readme.js -->
710 |
711 | - **browser_select_option**
712 | - Title: Select option
713 | - Description: Select an option in a dropdown
714 | - Parameters:
715 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
716 | - `ref` (string): Exact target element reference from the page snapshot
717 | - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
718 | - Read-only: **false**
719 |
720 | <!-- NOTE: This has been generated via update-readme.js -->
721 |
722 | - **browser_snapshot**
723 | - Title: Page snapshot
724 | - Description: Capture accessibility snapshot of the current page, this is better than screenshot
725 | - Parameters: None
726 | - Read-only: **true**
727 |
728 | <!-- NOTE: This has been generated via update-readme.js -->
729 |
730 | - **browser_take_screenshot**
731 | - Title: Take a screenshot
732 | - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
733 | - Parameters:
734 | - `type` (string, optional): Image format for the screenshot. Default is png.
735 | - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.
736 | - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
737 | - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
738 | - `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.
739 | - Read-only: **true**
740 |
741 | <!-- NOTE: This has been generated via update-readme.js -->
742 |
743 | - **browser_type**
744 | - Title: Type text
745 | - Description: Type text into editable element
746 | - Parameters:
747 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
748 | - `ref` (string): Exact target element reference from the page snapshot
749 | - `text` (string): Text to type into the element
750 | - `submit` (boolean, optional): Whether to submit entered text (press Enter after)
751 | - `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.
752 | - Read-only: **false**
753 |
754 | <!-- NOTE: This has been generated via update-readme.js -->
755 |
756 | - **browser_wait_for**
757 | - Title: Wait for
758 | - Description: Wait for text to appear or disappear or a specified time to pass
759 | - Parameters:
760 | - `time` (number, optional): The time to wait in seconds
761 | - `text` (string, optional): The text to wait for
762 | - `textGone` (string, optional): The text to wait for to disappear
763 | - Read-only: **false**
764 |
765 | </details>
766 |
767 | <details>
768 | <summary><b>Tab management</b></summary>
769 |
770 | <!-- NOTE: This has been generated via update-readme.js -->
771 |
772 | - **browser_tabs**
773 | - Title: Manage tabs
774 | - Description: List, create, close, or select a browser tab.
775 | - Parameters:
776 | - `action` (string): Operation to perform
777 | - `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed.
778 | - Read-only: **false**
779 |
780 | </details>
781 |
782 | <details>
783 | <summary><b>Browser installation</b></summary>
784 |
785 | <!-- NOTE: This has been generated via update-readme.js -->
786 |
787 | - **browser_install**
788 | - Title: Install the browser specified in the config
789 | - Description: Install the browser specified in the config. Call this if you get an error about the browser not being installed.
790 | - Parameters: None
791 | - Read-only: **false**
792 |
793 | </details>
794 |
795 | <details>
796 | <summary><b>Coordinate-based (opt-in via --caps=vision)</b></summary>
797 |
798 | <!-- NOTE: This has been generated via update-readme.js -->
799 |
800 | - **browser_mouse_click_xy**
801 | - Title: Click
802 | - Description: Click left mouse button at a given position
803 | - Parameters:
804 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
805 | - `x` (number): X coordinate
806 | - `y` (number): Y coordinate
807 | - Read-only: **false**
808 |
809 | <!-- NOTE: This has been generated via update-readme.js -->
810 |
811 | - **browser_mouse_drag_xy**
812 | - Title: Drag mouse
813 | - Description: Drag left mouse button to a given position
814 | - Parameters:
815 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
816 | - `startX` (number): Start X coordinate
817 | - `startY` (number): Start Y coordinate
818 | - `endX` (number): End X coordinate
819 | - `endY` (number): End Y coordinate
820 | - Read-only: **false**
821 |
822 | <!-- NOTE: This has been generated via update-readme.js -->
823 |
824 | - **browser_mouse_move_xy**
825 | - Title: Move mouse
826 | - Description: Move mouse to a given position
827 | - Parameters:
828 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
829 | - `x` (number): X coordinate
830 | - `y` (number): Y coordinate
831 | - Read-only: **false**
832 |
833 | </details>
834 |
835 | <details>
836 | <summary><b>PDF generation (opt-in via --caps=pdf)</b></summary>
837 |
838 | <!-- NOTE: This has been generated via update-readme.js -->
839 |
840 | - **browser_pdf_save**
841 | - Title: Save as PDF
842 | - Description: Save page as PDF
843 | - Parameters:
844 | - `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.
845 | - Read-only: **true**
846 |
847 | </details>
848 |
849 | <details>
850 | <summary><b>Test assertions (opt-in via --caps=testing)</b></summary>
851 |
852 | <!-- NOTE: This has been generated via update-readme.js -->
853 |
854 | - **browser_generate_locator**
855 | - Title: Create locator for element
856 | - Description: Generate locator for the given element to use in tests
857 | - Parameters:
858 | - `element` (string): Human-readable element description used to obtain permission to interact with the element
859 | - `ref` (string): Exact target element reference from the page snapshot
860 | - Read-only: **true**
861 |
862 | <!-- NOTE: This has been generated via update-readme.js -->
863 |
864 | - **browser_verify_element_visible**
865 | - Title: Verify element visible
866 | - Description: Verify element is visible on the page
867 | - Parameters:
868 | - `role` (string): ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`
869 | - `accessibleName` (string): ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`
870 | - Read-only: **false**
871 |
872 | <!-- NOTE: This has been generated via update-readme.js -->
873 |
874 | - **browser_verify_list_visible**
875 | - Title: Verify list visible
876 | - Description: Verify list is visible on the page
877 | - Parameters:
878 | - `element` (string): Human-readable list description
879 | - `ref` (string): Exact target element reference that points to the list
880 | - `items` (array): Items to verify
881 | - Read-only: **false**
882 |
883 | <!-- NOTE: This has been generated via update-readme.js -->
884 |
885 | - **browser_verify_text_visible**
886 | - Title: Verify text visible
887 | - Description: Verify text is visible on the page. Prefer browser_verify_element_visible if possible.
888 | - Parameters:
889 | - `text` (string): TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`
890 | - Read-only: **false**
891 |
892 | <!-- NOTE: This has been generated via update-readme.js -->
893 |
894 | - **browser_verify_value**
895 | - Title: Verify value
896 | - Description: Verify element value
897 | - Parameters:
898 | - `type` (string): Type of the element
899 | - `element` (string): Human-readable element description
900 | - `ref` (string): Exact target element reference that points to the element
901 | - `value` (string): Value to verify. For checkbox, use "true" or "false".
902 | - Read-only: **false**
903 |
904 | </details>
905 |
906 | <details>
907 | <summary><b>Tracing (opt-in via --caps=tracing)</b></summary>
908 |
909 | <!-- NOTE: This has been generated via update-readme.js -->
910 |
911 | - **browser_start_tracing**
912 | - Title: Start tracing
913 | - Description: Start trace recording
914 | - Parameters: None
915 | - Read-only: **true**
916 |
917 | <!-- NOTE: This has been generated via update-readme.js -->
918 |
919 | - **browser_stop_tracing**
920 | - Title: Stop tracing
921 | - Description: Stop trace recording
922 | - Parameters: None
923 | - Read-only: **true**
924 |
925 | </details>
926 |
927 |
928 | <!--- End of tools generated section -->
929 |
```
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
```markdown
1 | <!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [[email protected]](mailto:[email protected]). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
40 |
41 | <!-- END MICROSOFT SECURITY.MD BLOCK -->
42 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing
2 |
3 | ## Choose an issue
4 |
5 | Playwright MCP **requires an issue** for every contribution, except for minor documentation updates.
6 |
7 | If you are passionate about a bug/feature, but cannot find an issue describing it, **file an issue first**. This will
8 | facilitate the discussion, and you might get some early feedback from project maintainers before spending your time on
9 | creating a pull request.
10 |
11 | ## Make a change
12 |
13 | > [!WARNING]
14 | > The core of the Playwright MCP was moved to the [Playwright monorepo](https://github.com/microsoft/playwright).
15 |
16 | Clone the Playwright repository. If you plan to send a pull request, it might be better to [fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) first.
17 |
18 |
19 | ```bash
20 | git clone https://github.com/microsoft/playwright
21 | cd playwright
22 | ```
23 |
24 | Install dependencies and run the build in watch mode.
25 | ```bash
26 | # install deps and run watch
27 | npm ci
28 | npm run watch
29 | npx playwright install
30 | ```
31 |
32 | Source code for Playwright MCP is located at [packages/playwright/src/mcp](https://github.com/microsoft/playwright/blob/main/packages/playwright/src/mcp).
33 |
34 | ```bash
35 | # list source files
36 | ls -la packages/playwright/src/mcp
37 | ```
38 |
39 | Coding style is fully defined in [eslint.config.mjs](https://github.com/microsoft/playwright/blob/main/eslint.config.mjs). Before creating a pull request, or at any moment during development, run linter to check all kinds of things:
40 | ```bash
41 | # lint the source base before sending PR
42 | npm run flint
43 | ```
44 |
45 | Comments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
46 |
47 | ## Add a test
48 |
49 | Playwright requires a test for the new or modified functionality. An exception would be a pure refactoring, but chances are you are doing more than that.
50 |
51 | There are multiple [test suites](https://github.com/microsoft/playwright/blob/main/tests) in Playwright that will be executed on the CI. Tests for Playwright MCP are located at [tests/mcp](https://github.com/microsoft/playwright/blob/main/tests/mcp).
52 |
53 | ```bash
54 | # list test files
55 | ls -la tests/mcp
56 | ```
57 |
58 | To run the mcp tests, use
59 |
60 | ```bash
61 | # fast path runs all MCP tests in Chromium
62 | npm run mcp-ctest
63 | ```
64 |
65 | ```bash
66 | # slow path runs all tests in three browsers
67 | npm run mcp-test
68 | ```
69 |
70 | Since Playwright tests are using Playwright under the hood, everything from our documentation applies, for example [this guide on running and debugging tests](https://playwright.dev/docs/running-tests#running-tests).
71 |
72 | Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows.
73 |
74 | ## Write a commit message
75 |
76 | Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format:
77 |
78 | ```
79 | label(namespace): title
80 |
81 | description
82 |
83 | footer
84 | ```
85 |
86 | 1. *label* is one of the following:
87 | - `fix` - bug fixes
88 | - `feat` - new features
89 | - `docs` - documentation-only changes
90 | - `test` - test-only changes
91 | - `devops` - changes to the CI or build
92 | - `chore` - everything that doesn't fall under previous categories
93 | 2. *namespace* is put in parentheses after label and is optional. Must be lowercase.
94 | 3. *title* is a brief summary of changes.
95 | 4. *description* is **optional**, new-line separated from title and is in present tense.
96 | 5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to GitHub issues.
97 |
98 | Example:
99 |
100 | ```
101 | feat(trace viewer): network panel filtering
102 |
103 | This patch adds a filtering toolbar to the network panel.
104 | <link to a screenshot>
105 |
106 | Fixes #123, references #234.
107 | ```
108 |
109 | ## Send a pull request
110 |
111 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose.
112 | Make sure to keep your PR (diff) small and readable. If necessary, split your contribution into multiple PRs.
113 | Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
114 |
115 | After a successful code review, one of the maintainers will merge your pull request. Congratulations!
116 |
117 | ## More details
118 |
119 | **No new dependencies**
120 |
121 | There is a very high bar for new dependencies, including updating to a new version of an existing dependency. We recommend to explicitly discuss this in an issue and get a green light from a maintainer, before creating a pull request that updates dependencies.
122 |
123 | ## Contributor License Agreement
124 |
125 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
126 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
127 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
128 |
129 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
130 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
131 | provided by the bot. You will only need to do this once across all repos using our CLA.
132 |
133 | ### Code of Conduct
134 |
135 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
136 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
137 | contact [[email protected]](mailto:[email protected]) with any additional questions or comments.
138 |
```
--------------------------------------------------------------------------------
/extension/src/ui/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | // Help VSCode to find right tsconfig file.
2 | {
3 | "extends": "../../tsconfig.ui.json"
4 | }
5 |
```
--------------------------------------------------------------------------------
/extension/src/ui/status.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html lang="en">
3 | <head>
4 | <meta charset="UTF-8">
5 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 | <title>Playwright MCP Bridge Status</title>
7 | <link rel="stylesheet" href="connect.css">
8 | </head>
9 | <body>
10 | <div id="root"></div>
11 | <script src="status.tsx" type="module"></script>
12 | </body>
13 | </html>
```
--------------------------------------------------------------------------------
/extension/tsconfig.ui.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "esModuleInterop": true,
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "module": "ESNext",
8 | "rootDir": "src",
9 | "outDir": "./lib",
10 | "resolveJsonModule": true,
11 | "types": ["chrome"],
12 | "jsx": "react-jsx",
13 | "jsxImportSource": "react",
14 | "noEmit": true,
15 | },
16 | "include": [
17 | "src/ui",
18 | ],
19 | }
20 |
```
--------------------------------------------------------------------------------
/extension/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "esModuleInterop": true,
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "module": "ESNext",
8 | "rootDir": "src",
9 | "outDir": "./dist/lib",
10 | "resolveJsonModule": true,
11 | "types": ["chrome"],
12 | "jsx": "react-jsx",
13 | "jsxImportSource": "react",
14 | "noEmit": true
15 | },
16 | "include": [
17 | "src",
18 | ],
19 | "exclude": [
20 | "src/ui",
21 | ]
22 | }
23 |
```
--------------------------------------------------------------------------------
/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 | const { createConnection } = require('playwright/lib/mcp/index');
19 | module.exports = { createConnection };
20 |
```
--------------------------------------------------------------------------------
/extension/src/ui/icons.css:
--------------------------------------------------------------------------------
```css
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 | .octicon {
18 | display: inline-block;
19 | overflow: visible !important;
20 | vertical-align: text-bottom;
21 | fill: currentColor;
22 | margin-right: 7px;
23 | flex: none;
24 | }
25 |
26 | .color-icon-success {
27 | color: var(--color-success-fg) !important;
28 | }
29 |
30 | .color-text-danger {
31 | color: var(--color-danger-fg) !important;
32 | }
33 |
```
--------------------------------------------------------------------------------
/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';
20 | import type { BrowserContext } from 'playwright';
21 |
22 | export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Server>;
23 | export {};
24 |
```
--------------------------------------------------------------------------------
/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 | const { program } = require('playwright-core/lib/utilsBundle');
19 | const { decorateCommand } = require('playwright/lib/mcp/program');
20 |
21 | const packageJSON = require('./package.json');
22 | const p = program.version('Version ' + packageJSON.version).name('Playwright MCP');
23 | decorateCommand(p, packageJSON.version)
24 | void program.parseAsync(process.argv);
25 |
```
--------------------------------------------------------------------------------
/extension/src/ui/copyToClipboard.css:
--------------------------------------------------------------------------------
```css
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 | .copy-icon {
18 | flex: none;
19 | height: 24px;
20 | width: 24px;
21 | border: none;
22 | outline: none;
23 | color: var(--color-fg-muted);
24 | background: transparent;
25 | padding: 4px;
26 | cursor: pointer;
27 | display: inline-flex;
28 | align-items: center;
29 | justify-content: center;
30 | border-radius: 4px;
31 | }
32 |
33 | .copy-icon svg {
34 | margin: 0;
35 | }
36 |
37 | .copy-icon:not(:disabled):hover {
38 | background-color: var(--color-btn-selected-bg);
39 | }
40 |
```
--------------------------------------------------------------------------------
/extension/playwright.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { defineConfig } from '@playwright/test';
18 |
19 | import type { TestOptions } from '../tests/fixtures';
20 |
21 | export default defineConfig<TestOptions>({
22 | testDir: './tests',
23 | fullyParallel: true,
24 | forbidOnly: !!process.env.CI,
25 | retries: process.env.CI ? 2 : 0,
26 | workers: process.env.CI ? 1 : undefined,
27 | reporter: 'list',
28 | projects: [
29 | { name: 'chromium', use: { mcpBrowser: 'chromium' } },
30 | ],
31 | });
32 |
```
--------------------------------------------------------------------------------
/tests/core.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { test, expect } from './fixtures';
18 |
19 | test('browser_navigate', async ({ client, server }) => {
20 | expect(await client.callTool({
21 | name: 'browser_navigate',
22 | arguments: { url: server.HELLO_WORLD },
23 | })).toHaveResponse({
24 | code: `await page.goto('${server.HELLO_WORLD}');`,
25 | pageState: `- Page URL: ${server.HELLO_WORLD}
26 | - Page Title: Title
27 | - Page Snapshot:
28 | \`\`\`yaml
29 | - generic [active] [ref=e1]: Hello, world!
30 | \`\`\``,
31 | });
32 | });
33 |
```
--------------------------------------------------------------------------------
/extension/src/ui/connect.html:
--------------------------------------------------------------------------------
```html
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 | <!DOCTYPE html>
17 | <html>
18 | <head>
19 | <title>Playwright MCP extension</title>
20 | <meta name="viewport" content="width=device-width, initial-scale=1">
21 | <link rel="icon" type="image/png" sizes="32x32" href="../../icons/icon-32.png">
22 | <link rel="icon" type="image/png" sizes="16x16" href="../../icons/icon-16.png">
23 | <link rel="stylesheet" href="connect.css">
24 | </head>
25 | <body>
26 | <div id="root"></div>
27 | <script type="module" src="connect.tsx"></script>
28 | </body>
29 | </html>
```
--------------------------------------------------------------------------------
/extension/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@playwright/mcp-extension",
3 | "version": "0.0.45",
4 | "description": "Playwright MCP Browser Extension",
5 | "private": true,
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/microsoft/playwright-mcp.git"
9 | },
10 | "homepage": "https://playwright.dev",
11 | "engines": {
12 | "node": ">=18"
13 | },
14 | "author": {
15 | "name": "Microsoft Corporation"
16 | },
17 | "license": "Apache-2.0",
18 | "scripts": {
19 | "build": "tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts",
20 | "watch": "tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts",
21 | "test": "playwright test",
22 | "clean": "rm -rf dist"
23 | },
24 | "devDependencies": {
25 | "@types/chrome": "^0.0.315",
26 | "@types/react": "^18.2.66",
27 | "@types/react-dom": "^18.2.22",
28 | "@vitejs/plugin-react": "^4.0.0",
29 | "react": "^18.2.0",
30 | "react-dom": "^18.2.0",
31 | "typescript": "^5.8.2",
32 | "vite": "^5.4.21",
33 | "vite-plugin-static-copy": "^3.1.1"
34 | }
35 | }
36 |
```
--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "manifest_version": 3,
3 | "name": "Playwright MCP Bridge",
4 | "version": "0.0.45",
5 | "description": "Share browser tabs with Playwright MCP server",
6 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB",
7 | "permissions": [
8 | "debugger",
9 | "activeTab",
10 | "tabs",
11 | "storage"
12 | ],
13 | "host_permissions": [
14 | "<all_urls>"
15 | ],
16 | "background": {
17 | "service_worker": "lib/background.mjs",
18 | "type": "module"
19 | },
20 | "action": {
21 | "default_title": "Playwright MCP Bridge",
22 | "default_icon": {
23 | "16": "icons/icon-16.png",
24 | "32": "icons/icon-32.png",
25 | "48": "icons/icon-48.png",
26 | "128": "icons/icon-128.png"
27 | }
28 | },
29 | "icons": {
30 | "16": "icons/icon-16.png",
31 | "32": "icons/icon-32.png",
32 | "48": "icons/icon-48.png",
33 | "128": "icons/icon-128.png"
34 | }
35 | }
36 |
```
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { defineConfig } from '@playwright/test';
18 |
19 | import type { TestOptions } from './tests/fixtures';
20 |
21 | export default defineConfig<TestOptions>({
22 | testDir: './tests',
23 | fullyParallel: true,
24 | forbidOnly: !!process.env.CI,
25 | workers: process.env.CI ? 2 : undefined,
26 | reporter: 'list',
27 | projects: [
28 | { name: 'chrome' },
29 | ...process.env.MCP_IN_DOCKER ? [{
30 | name: 'chromium-docker',
31 | grep: /browser_navigate|browser_click/,
32 | use: {
33 | mcpBrowser: 'chromium',
34 | mcpMode: 'docker' as const
35 | }
36 | }] : [],
37 | ],
38 | });
39 |
```
--------------------------------------------------------------------------------
/tests/library.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import child_process from 'child_process';
17 | import fs from 'fs/promises';
18 | import { test, expect } from './fixtures';
19 |
20 | test('library can be used from CommonJS', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/456' } }, async ({}, testInfo) => {
21 | const file = testInfo.outputPath('main.cjs');
22 | await fs.writeFile(file, `
23 | import('@playwright/mcp')
24 | .then(playwrightMCP => playwrightMCP.createConnection())
25 | .then(() => console.log('OK'));
26 | `);
27 | expect(child_process.execSync(`node ${file}`, { encoding: 'utf-8' })).toContain('OK');
28 | });
29 |
```
--------------------------------------------------------------------------------
/tests/click.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { test, expect } from './fixtures';
18 |
19 | test('browser_click', async ({ client, server }) => {
20 | server.setContent('/', `
21 | <title>Title</title>
22 | <button>Submit</button>
23 | <script>
24 | const button = document.querySelector('button');
25 | button.addEventListener('click', () => {
26 | button.focus(); // without manual focus, webkit focuses body
27 | });
28 | </script>
29 | `, 'text/html');
30 |
31 | expect(await client.callTool({
32 | name: 'browser_navigate',
33 | arguments: { url: server.PREFIX },
34 | })).toHaveResponse({
35 | code: `await page.goto('${server.PREFIX}');`,
36 | pageState: expect.stringContaining(`- button \"Submit\" [ref=e2]`),
37 | });
38 |
39 | expect(await client.callTool({
40 | name: 'browser_click',
41 | arguments: {
42 | element: 'Submit button',
43 | ref: 'e2',
44 | },
45 | })).toHaveResponse({
46 | code: `await page.getByRole('button', { name: 'Submit' }).click();`,
47 | pageState: expect.stringContaining(`button "Submit" [active] [ref=e2]`),
48 | });
49 | });
50 |
```
--------------------------------------------------------------------------------
/extension/src/ui/copyToClipboard.tsx:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import * as React from 'react';
18 | import * as icons from './icons';
19 | import './copyToClipboard.css';
20 |
21 | type CopyToClipboardProps = {
22 | value: string;
23 | };
24 |
25 | /**
26 | * A copy to clipboard button.
27 | */
28 | export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value }) => {
29 | type IconType = 'copy' | 'check' | 'cross';
30 | const [icon, setIcon] = React.useState<IconType>('copy');
31 |
32 | React.useEffect(() => {
33 | setIcon('copy');
34 | }, [value]);
35 |
36 | React.useEffect(() => {
37 | if (icon === 'check') {
38 | const timeout = setTimeout(() => {
39 | setIcon('copy');
40 | }, 3000);
41 | return () => clearTimeout(timeout);
42 | }
43 | }, [icon]);
44 |
45 | const handleCopy = React.useCallback(() => {
46 | navigator.clipboard.writeText(value).then(() => {
47 | setIcon('check');
48 | }, () => {
49 | setIcon('cross');
50 | });
51 | }, [value]);
52 | const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
53 | return <button className='copy-icon' title='Copy to clipboard' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
54 | };
55 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@playwright/mcp",
3 | "version": "0.0.45",
4 | "description": "Playwright Tools for MCP",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/microsoft/playwright-mcp.git"
8 | },
9 | "homepage": "https://playwright.dev",
10 | "engines": {
11 | "node": ">=18"
12 | },
13 | "author": {
14 | "name": "Microsoft Corporation"
15 | },
16 | "license": "Apache-2.0",
17 | "scripts": {
18 | "lint": "npm run update-readme",
19 | "update-readme": "node update-readme.js",
20 | "docker-build": "docker build --no-cache -t playwright-mcp-dev:latest .",
21 | "docker-rm": "docker rm playwright-mcp-dev",
22 | "docker-run": "docker run -it -p 8080:8080 --name playwright-mcp-dev playwright-mcp-dev:latest",
23 | "test": "playwright test",
24 | "ctest": "playwright test --project=chrome",
25 | "ftest": "playwright test --project=firefox",
26 | "wtest": "playwright test --project=webkit",
27 | "dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker",
28 | "npm-publish": "npm run clean && npm run test && npm publish",
29 | "copy-config": "cp ../playwright/packages/playwright/src/mcp/config.d.ts . && perl -pi -e \"s|import type \\* as playwright from 'playwright-core';|import type * as playwright from 'playwright';|\" ./config.d.ts",
30 | "roll": "npm run copy-config && npm run lint"
31 | },
32 | "exports": {
33 | "./package.json": "./package.json",
34 | ".": {
35 | "types": "./index.d.ts",
36 | "default": "./index.js"
37 | }
38 | },
39 | "dependencies": {
40 | "playwright": "1.57.0-alpha-1761929702000",
41 | "playwright-core": "1.57.0-alpha-1761929702000"
42 | },
43 | "bin": {
44 | "mcp-server-playwright": "cli.js"
45 | },
46 | "devDependencies": {
47 | "@modelcontextprotocol/sdk": "^1.17.5",
48 | "@playwright/test": "1.57.0-alpha-1761929702000",
49 | "@types/node": "^24.3.0",
50 | "zod-to-json-schema": "^3.24.6"
51 | }
52 | }
53 |
```
--------------------------------------------------------------------------------
/extension/src/ui/tabItem.tsx:
--------------------------------------------------------------------------------
```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 React from 'react';
18 |
19 | export interface TabInfo {
20 | id: number;
21 | windowId: number;
22 | title: string;
23 | url: string;
24 | favIconUrl?: string;
25 | }
26 |
27 | export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({
28 | variant,
29 | onClick,
30 | children
31 | }) => {
32 | return (
33 | <button className={`button ${variant}`} onClick={onClick}>
34 | {children}
35 | </button>
36 | );
37 | };
38 |
39 |
40 | export interface TabItemProps {
41 | tab: TabInfo;
42 | onClick?: () => void;
43 | button?: React.ReactNode;
44 | }
45 |
46 | export const TabItem: React.FC<TabItemProps> = ({
47 | tab,
48 | onClick,
49 | button
50 | }) => {
51 | return (
52 | <div className='tab-item' onClick={onClick} style={onClick ? { cursor: 'pointer' } : undefined}>
53 | <img
54 | src={tab.favIconUrl || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect width="16" height="16" fill="%23f6f8fa"/></svg>'}
55 | alt=''
56 | className='tab-favicon'
57 | />
58 | <div className='tab-content'>
59 | <div className='tab-title'>
60 | {tab.title || 'Untitled'}
61 | </div>
62 | <div className='tab-url'>{tab.url}</div>
63 | </div>
64 | {button}
65 | </div>
66 | );
67 | };
68 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | ARG PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
2 |
3 | # ------------------------------
4 | # Base
5 | # ------------------------------
6 | # Base stage: Contains only the minimal dependencies required for runtime
7 | # (node_modules and Playwright system dependencies)
8 | FROM node:22-bookworm-slim AS base
9 |
10 | ARG PLAYWRIGHT_BROWSERS_PATH
11 | ENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH}
12 |
13 | # Set the working directory
14 | WORKDIR /app
15 |
16 | RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
17 | --mount=type=bind,source=package.json,target=package.json \
18 | --mount=type=bind,source=package-lock.json,target=package-lock.json \
19 | npm ci --omit=dev && \
20 | # Install system dependencies for playwright
21 | npx -y playwright-core install-deps chromium
22 |
23 | # ------------------------------
24 | # Builder
25 | # ------------------------------
26 | FROM base AS builder
27 |
28 | RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
29 | --mount=type=bind,source=package.json,target=package.json \
30 | --mount=type=bind,source=package-lock.json,target=package-lock.json \
31 | npm ci
32 |
33 | # Copy the rest of the app
34 | COPY *.json *.js *.ts .
35 |
36 | # ------------------------------
37 | # Browser
38 | # ------------------------------
39 | # Cache optimization:
40 | # - Browser is downloaded only when node_modules or Playwright system dependencies change
41 | # - Cache is reused when only source code changes
42 | FROM base AS browser
43 |
44 | RUN npx -y playwright-core install --no-shell chromium
45 |
46 | # ------------------------------
47 | # Runtime
48 | # ------------------------------
49 | FROM base
50 |
51 | ARG PLAYWRIGHT_BROWSERS_PATH
52 | ARG USERNAME=node
53 | ENV NODE_ENV=production
54 | ENV PLAYWRIGHT_MCP_OUTPUT_DIR=/tmp/playwright-output
55 |
56 | # Set the correct ownership for the runtime user on production `node_modules`
57 | RUN chown -R ${USERNAME}:${USERNAME} node_modules
58 |
59 | USER ${USERNAME}
60 |
61 | COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}
62 | COPY --chown=${USERNAME}:${USERNAME} cli.js package.json ./
63 |
64 | # Run in headless and only with chromium (other browsers need more dependencies not included in this image)
65 | ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"]
66 |
```
--------------------------------------------------------------------------------
/extension/src/ui/authToken.css:
--------------------------------------------------------------------------------
```css
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 | .auth-token-section {
18 | margin: 16px 0;
19 | padding: 16px;
20 | background-color: #f6f8fa;
21 | border-radius: 6px;
22 | }
23 |
24 | .auth-token-description {
25 | font-size: 12px;
26 | color: #656d76;
27 | margin-bottom: 12px;
28 | }
29 |
30 | .auth-token-container {
31 | display: flex;
32 | align-items: center;
33 | gap: 8px;
34 | background-color: #ffffff;
35 | padding: 8px;
36 | }
37 |
38 | .auth-token-code {
39 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
40 | font-size: 12px;
41 | color: #1f2328;
42 | border: none;
43 | flex: 1;
44 | padding: 0;
45 | word-break: break-all;
46 | }
47 |
48 | .auth-token-refresh {
49 | flex: none;
50 | height: 24px;
51 | width: 24px;
52 | border: none;
53 | outline: none;
54 | color: var(--color-fg-muted);
55 | background: transparent;
56 | padding: 4px;
57 | cursor: pointer;
58 | display: inline-flex;
59 | align-items: center;
60 | justify-content: center;
61 | border-radius: 4px;
62 | }
63 |
64 | .auth-token-refresh svg {
65 | margin: 0;
66 | }
67 |
68 | .auth-token-refresh:not(:disabled):hover {
69 | background-color: var(--color-btn-selected-bg);
70 | }
71 |
72 | .auth-token-example-section {
73 | margin-top: 16px;
74 | }
75 |
76 | .auth-token-example-toggle {
77 | display: flex;
78 | align-items: center;
79 | gap: 8px;
80 | background: none;
81 | border: none;
82 | padding: 8px 0;
83 | font-size: 12px;
84 | color: #656d76;
85 | cursor: pointer;
86 | outline: none;
87 | text-align: left;
88 | width: 100%;
89 | }
90 |
91 | .auth-token-example-toggle:hover {
92 | color: #1f2328;
93 | }
94 |
95 | .auth-token-chevron {
96 | display: inline-flex;
97 | align-items: center;
98 | justify-content: center;
99 | transform: rotate(-90deg);
100 | flex-shrink: 0;
101 | }
102 |
103 | .auth-token-chevron.expanded {
104 | transform: rotate(0deg);
105 | }
106 |
107 | .auth-token-chevron svg {
108 | width: 12px;
109 | height: 12px;
110 | }
111 |
112 | .auth-token-chevron .octicon {
113 | margin: 0px;
114 | }
115 |
116 | .auth-token-example-content {
117 | margin-top: 12px;
118 | padding: 12px 0;
119 | }
120 |
121 | .auth-token-example-description {
122 | font-size: 12px;
123 | color: #656d76;
124 | margin-bottom: 12px;
125 | }
126 |
127 | .auth-token-example-config {
128 | display: flex;
129 | align-items: flex-start;
130 | gap: 8px;
131 | background-color: #ffffff;
132 | padding: 12px;
133 | }
134 |
135 | .auth-token-example-code {
136 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
137 | font-size: 11px;
138 | color: #1f2328;
139 | white-space: pre;
140 | flex: 1;
141 | line-height: 1.4;
142 | }
143 |
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Use Node.js 20
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: '20'
18 | cache: 'npm'
19 | - name: Install dependencies
20 | run: npm ci
21 | - name: Run ESLint
22 | run: npm run lint
23 | - name: Ensure no changes
24 | run: git diff --exit-code
25 |
26 | test:
27 | strategy:
28 | fail-fast: false
29 | matrix:
30 | os: [ubuntu-latest, macos-15, windows-latest]
31 | runs-on: ${{ matrix.os }}
32 | steps:
33 | - uses: actions/checkout@v4
34 | - name: Use Node.js 20
35 | uses: actions/setup-node@v4
36 | with:
37 | node-version: '20'
38 | cache: 'npm'
39 | - name: Install dependencies
40 | run: npm ci
41 | - name: Playwright install
42 | run: npx playwright install --with-deps
43 | - name: Run tests
44 | run: npm run test
45 |
46 | test_docker:
47 | runs-on: ubuntu-latest
48 | steps:
49 | - uses: actions/checkout@v4
50 | - name: Use Node.js 20
51 | uses: actions/setup-node@v4
52 | with:
53 | node-version: '20'
54 | cache: 'npm'
55 | - name: Install dependencies
56 | run: npm ci
57 | - name: Playwright install
58 | run: npx playwright install --with-deps chromium
59 | - name: Set up Docker Buildx
60 | uses: docker/setup-buildx-action@v3
61 | - name: Build and push
62 | uses: docker/build-push-action@v6
63 | with:
64 | tags: playwright-mcp-dev:latest
65 | cache-from: type=gha
66 | cache-to: type=gha,mode=max
67 | load: true
68 | - name: Run tests
69 | shell: bash
70 | run: |
71 | # Used for the Docker tests to share the test-results folder with the container.
72 | umask 0000
73 | npm run test -- --project=chromium-docker
74 | env:
75 | MCP_IN_DOCKER: 1
76 |
77 | test_extension:
78 | runs-on: macos-latest
79 | defaults:
80 | run:
81 | working-directory: ./extension
82 | steps:
83 | - uses: actions/checkout@v4
84 | - name: Use Node.js 20
85 | uses: actions/setup-node@v4
86 | with:
87 | node-version: '20' # crypto.randomUUID(); stalls in v18.20.8
88 | cache: 'npm'
89 | - name: Install dependencies
90 | run: npm ci
91 | - name: Build extension
92 | run: npm run build
93 | - name: Upload artifact
94 | uses: actions/upload-artifact@v4
95 | with:
96 | name: extension
97 | path: ./extension/dist
98 | retention-days: 7
99 | - name: Install MCP server
100 | run: |
101 | cd ..
102 | npm ci
103 | npx playwright install chromium
104 | - name: Run tests
105 | run: |
106 | if [[ "$(uname)" == "Linux" ]]; then
107 | xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test
108 | else
109 | npm run test
110 | fi
111 | shell: bash
112 |
```
--------------------------------------------------------------------------------
/extension/src/ui/icons.tsx:
--------------------------------------------------------------------------------
```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 './icons.css';
18 | import './colors.css';
19 |
20 | export const cross = () => {
21 | return <svg className='octicon color-text-danger' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
22 | <path fillRule='evenodd' d='M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z'></path>
23 | </svg>;
24 | };
25 |
26 | export const check = () => {
27 | return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-icon-success'>
28 | <path fillRule='evenodd' d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'></path>
29 | </svg>;
30 | };
31 |
32 | export const copy = () => {
33 | return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16' aria-hidden='true'>
34 | <path d='M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z'></path>
35 | <path d='M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'></path>
36 | </svg>;
37 | };
38 |
39 | export const refresh = () => {
40 | return <svg className='octicon' viewBox="0 0 16 16" width="16" height="16" aria-hidden='true'>
41 | <path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"></path>
42 | </svg>;
43 | };
44 |
45 | export const chevronDown = () => {
46 | return <svg className='octicon' viewBox="0 0 16 16" width="16" height="16" aria-hidden='true'>
47 | <path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z"></path>
48 | </svg>;
49 | };
50 |
```
--------------------------------------------------------------------------------
/extension/src/ui/status.tsx:
--------------------------------------------------------------------------------
```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 React, { useState, useEffect } from 'react';
18 | import { createRoot } from 'react-dom/client';
19 | import { Button, TabItem } from './tabItem';
20 |
21 | import type { TabInfo } from './tabItem';
22 | import { AuthTokenSection } from './authToken';
23 |
24 | interface ConnectionStatus {
25 | isConnected: boolean;
26 | connectedTabId: number | null;
27 | connectedTab?: TabInfo;
28 | }
29 |
30 | const StatusApp: React.FC = () => {
31 | const [status, setStatus] = useState<ConnectionStatus>({
32 | isConnected: false,
33 | connectedTabId: null
34 | });
35 |
36 | useEffect(() => {
37 | void loadStatus();
38 | }, []);
39 |
40 | const loadStatus = async () => {
41 | // Get current connection status from background script
42 | const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
43 | if (connectedTabId) {
44 | const tab = await chrome.tabs.get(connectedTabId);
45 | setStatus({
46 | isConnected: true,
47 | connectedTabId,
48 | connectedTab: {
49 | id: tab.id!,
50 | windowId: tab.windowId!,
51 | title: tab.title!,
52 | url: tab.url!,
53 | favIconUrl: tab.favIconUrl
54 | }
55 | });
56 | } else {
57 | setStatus({
58 | isConnected: false,
59 | connectedTabId: null
60 | });
61 | }
62 | };
63 |
64 | const openConnectedTab = async () => {
65 | if (!status.connectedTabId)
66 | return;
67 | await chrome.tabs.update(status.connectedTabId, { active: true });
68 | window.close();
69 | };
70 |
71 | const disconnect = async () => {
72 | await chrome.runtime.sendMessage({ type: 'disconnect' });
73 | window.close();
74 | };
75 |
76 | return (
77 | <div className='app-container'>
78 | <div className='content-wrapper'>
79 | {status.isConnected && status.connectedTab ? (
80 | <div>
81 | <div className='tab-section-title'>
82 | Page with connected MCP client:
83 | </div>
84 | <div>
85 | <TabItem
86 | tab={status.connectedTab}
87 | button={
88 | <Button variant='primary' onClick={disconnect}>
89 | Disconnect
90 | </Button>
91 | }
92 | onClick={openConnectedTab}
93 | />
94 | </div>
95 | </div>
96 | ) : (
97 | <div className='status-banner'>
98 | No MCP clients are currently connected.
99 | </div>
100 | )}
101 | <AuthTokenSection />
102 | </div>
103 | </div>
104 | );
105 | };
106 |
107 | // Initialize the React app
108 | const container = document.getElementById('root');
109 | if (container) {
110 | const root = createRoot(container);
111 | root.render(<StatusApp />);
112 | }
113 |
```
--------------------------------------------------------------------------------
/tests/capabilities.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { test, expect } from './fixtures';
18 |
19 | test('test snapshot tool list', async ({ client }) => {
20 | const { tools } = await client.listTools();
21 | expect(new Set(tools.map(t => t.name))).toEqual(new Set([
22 | 'browser_click',
23 | 'browser_console_messages',
24 | 'browser_drag',
25 | 'browser_evaluate',
26 | 'browser_file_upload',
27 | 'browser_fill_form',
28 | 'browser_handle_dialog',
29 | 'browser_hover',
30 | 'browser_select_option',
31 | 'browser_type',
32 | 'browser_close',
33 | 'browser_install',
34 | 'browser_navigate_back',
35 | 'browser_navigate',
36 | 'browser_network_requests',
37 | 'browser_press_key',
38 | 'browser_resize',
39 | 'browser_snapshot',
40 | 'browser_tabs',
41 | 'browser_take_screenshot',
42 | 'browser_wait_for',
43 | ]));
44 | });
45 |
46 | test('test tool list proxy mode', async ({ startClient }) => {
47 | const { client } = await startClient({
48 | args: ['--connect-tool'],
49 | });
50 | const { tools } = await client.listTools();
51 | expect(new Set(tools.map(t => t.name))).toEqual(new Set([
52 | 'browser_click',
53 | 'browser_connect', // the extra tool
54 | 'browser_console_messages',
55 | 'browser_drag',
56 | 'browser_evaluate',
57 | 'browser_file_upload',
58 | 'browser_fill_form',
59 | 'browser_handle_dialog',
60 | 'browser_hover',
61 | 'browser_select_option',
62 | 'browser_type',
63 | 'browser_close',
64 | 'browser_install',
65 | 'browser_navigate_back',
66 | 'browser_navigate',
67 | 'browser_network_requests',
68 | 'browser_press_key',
69 | 'browser_resize',
70 | 'browser_snapshot',
71 | 'browser_tabs',
72 | 'browser_take_screenshot',
73 | 'browser_wait_for',
74 | ]));
75 | });
76 |
77 | test('test capabilities (pdf)', async ({ startClient }) => {
78 | const { client } = await startClient({
79 | args: ['--caps=pdf'],
80 | });
81 | const { tools } = await client.listTools();
82 | const toolNames = tools.map(t => t.name);
83 | expect(toolNames).toContain('browser_pdf_save');
84 | });
85 |
86 | test('test capabilities (vision)', async ({ startClient }) => {
87 | const { client } = await startClient({
88 | args: ['--caps=vision'],
89 | });
90 | const { tools } = await client.listTools();
91 | const toolNames = tools.map(t => t.name);
92 | expect(toolNames).toContain('browser_mouse_move_xy');
93 | expect(toolNames).toContain('browser_mouse_click_xy');
94 | expect(toolNames).toContain('browser_mouse_drag_xy');
95 | });
96 |
97 | test('support for legacy --vision option', async ({ startClient }) => {
98 | const { client } = await startClient({
99 | args: ['--vision'],
100 | });
101 | const { tools } = await client.listTools();
102 | const toolNames = tools.map(t => t.name);
103 | expect(toolNames).toContain('browser_mouse_move_xy');
104 | expect(toolNames).toContain('browser_mouse_click_xy');
105 | expect(toolNames).toContain('browser_mouse_drag_xy');
106 | });
107 |
```
--------------------------------------------------------------------------------
/extension/src/ui/authToken.tsx:
--------------------------------------------------------------------------------
```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 React, { useCallback, useState } from 'react';
18 | import { CopyToClipboard } from './copyToClipboard';
19 | import * as icons from './icons';
20 | import './authToken.css';
21 |
22 | export const AuthTokenSection: React.FC<{}> = ({}) => {
23 | const [authToken, setAuthToken] = useState<string>(getOrCreateAuthToken);
24 | const [isExampleExpanded, setIsExampleExpanded] = useState<boolean>(false);
25 |
26 | const onRegenerateToken = useCallback(() => {
27 | const newToken = generateAuthToken();
28 | localStorage.setItem('auth-token', newToken);
29 | setAuthToken(newToken);
30 | }, []);
31 |
32 | const toggleExample = useCallback(() => {
33 | setIsExampleExpanded(!isExampleExpanded);
34 | }, [isExampleExpanded]);
35 |
36 | return (
37 | <div className='auth-token-section'>
38 | <div className='auth-token-description'>
39 | Set this environment variable to bypass the connection dialog:
40 | </div>
41 | <div className='auth-token-container'>
42 | <code className='auth-token-code'>{authTokenCode(authToken)}</code>
43 | <button className='auth-token-refresh' title='Generate new token' aria-label='Generate new token'onClick={onRegenerateToken}>{icons.refresh()}</button>
44 | <CopyToClipboard value={authTokenCode(authToken)} />
45 | </div>
46 |
47 | <div className='auth-token-example-section'>
48 | <button
49 | className='auth-token-example-toggle'
50 | onClick={toggleExample}
51 | aria-expanded={isExampleExpanded}
52 | title={isExampleExpanded ? 'Hide example config' : 'Show example config'}
53 | >
54 | <span className={`auth-token-chevron ${isExampleExpanded ? 'expanded' : ''}`}>
55 | {icons.chevronDown()}
56 | </span>
57 | Example MCP server configuration
58 | </button>
59 |
60 | {isExampleExpanded && (
61 | <div className='auth-token-example-content'>
62 | <div className='auth-token-example-description'>
63 | Add this configuration to your MCP client (e.g., VS Code) to connect to the Playwright MCP Bridge:
64 | </div>
65 | <div className='auth-token-example-config'>
66 | <code className='auth-token-example-code'>{exampleConfig(authToken)}</code>
67 | <CopyToClipboard value={exampleConfig(authToken)} />
68 | </div>
69 | </div>
70 | )}
71 | </div>
72 | </div>
73 | );
74 | };
75 |
76 | function authTokenCode(authToken: string) {
77 | return `PLAYWRIGHT_MCP_EXTENSION_TOKEN=${authToken}`;
78 | }
79 |
80 | function exampleConfig(authToken: string) {
81 | return `{
82 | "mcpServers": {
83 | "playwright": {
84 | "command": "npx",
85 | "args": ["@playwright/mcp@latest", "--extension"],
86 | "env": {
87 | "PLAYWRIGHT_MCP_EXTENSION_TOKEN":
88 | "${authToken}"
89 | }
90 | }
91 | }
92 | }`;
93 | }
94 |
95 | function generateAuthToken(): string {
96 | // Generate a cryptographically secure random token
97 | const array = new Uint8Array(32);
98 | crypto.getRandomValues(array);
99 | // Convert to base64 and make it URL-safe
100 | return btoa(String.fromCharCode.apply(null, Array.from(array)))
101 | .replace(/[+/=]/g, match => {
102 | switch (match) {
103 | case '+': return '-';
104 | case '/': return '_';
105 | case '=': return '';
106 | default: return match;
107 | }
108 | });
109 | }
110 |
111 | export const getOrCreateAuthToken = (): string => {
112 | let token = localStorage.getItem('auth-token');
113 | if (!token) {
114 | token = generateAuthToken();
115 | localStorage.setItem('auth-token', token);
116 | }
117 | return token;
118 | }
119 |
```
--------------------------------------------------------------------------------
/extension/src/ui/connect.css:
--------------------------------------------------------------------------------
```css
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 | body {
18 | margin: 0;
19 | padding: 0;
20 | }
21 |
22 | /* Base styles */
23 | .app-container {
24 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
25 | background-color: #ffffff;
26 | color: #1f2328;
27 | margin: 0;
28 | padding: 16px;
29 | min-height: 100vh;
30 | font-size: 14px;
31 | }
32 |
33 | .content-wrapper {
34 | max-width: 600px;
35 | margin: 0 auto;
36 | }
37 |
38 | /* Status Banner */
39 | .status-container {
40 | display: flex;
41 | align-items: center;
42 | justify-content: space-between;
43 | margin-bottom: 16px;
44 | padding-right: 12px;
45 | }
46 |
47 | .status-banner {
48 | padding: 12px;
49 | font-size: 14px;
50 | font-weight: 500;
51 | display: flex;
52 | align-items: center;
53 | gap: 8px;
54 | flex: 1;
55 | }
56 |
57 | .status-banner.connected {
58 | color: #1f2328;
59 | }
60 |
61 | .status-banner.connected::before {
62 | content: "\2705";
63 | margin-right: 8px;
64 | }
65 |
66 | .status-banner.error {
67 | color: #1f2328;
68 | }
69 |
70 | .status-banner.error::before {
71 | content: "\274C";
72 | margin-right: 8px;
73 | }
74 |
75 | /* Buttons */
76 | .button-container {
77 | margin-bottom: 16px;
78 | display: flex;
79 | justify-content: flex-end;
80 | padding-right: 12px;
81 | }
82 |
83 | .button {
84 | padding: 8px 16px;
85 | border-radius: 6px;
86 | border: none;
87 | font-size: 14px;
88 | font-weight: 500;
89 | cursor: pointer;
90 | display: inline-flex;
91 | align-items: center;
92 | justify-content: center;
93 | text-decoration: none;
94 | margin-right: 8px;
95 | min-width: 90px;
96 | }
97 |
98 | .button.primary {
99 | background-color: #f8f9fa;
100 | color: #3c4043;
101 | border: 1px solid #dadce0;
102 | }
103 |
104 | .button.primary:hover {
105 | background-color: #f1f3f4;
106 | border-color: #dadce0;
107 | box-shadow: 0 1px 2px 0 rgba(60,64,67,.1);
108 | }
109 |
110 | .button.default {
111 | background-color: #f6f8fa;
112 | color: #24292f;
113 | }
114 |
115 | .button.default:hover {
116 | background-color: #f3f4f6;
117 | }
118 |
119 | .button.reject {
120 | background-color: #da3633;
121 | color: #ffffff;
122 | border: 1px solid #da3633;
123 | }
124 |
125 | .button.reject:hover {
126 | background-color: #c73836;
127 | border-color: #c73836;
128 | }
129 |
130 | /* Tab selection */
131 | .tab-section-title {
132 | padding-left: 12px;
133 | font-size: 12px;
134 | font-weight: 400;
135 | margin-bottom: 12px;
136 | color: #656d76;
137 | }
138 |
139 | .tab-item {
140 | display: flex;
141 | align-items: center;
142 | padding: 12px;
143 | margin-bottom: 8px;
144 | background-color: #ffffff;
145 | cursor: pointer;
146 | border-radius: 6px;
147 | transition: background-color 0.2s ease;
148 | }
149 |
150 | .tab-item:hover {
151 | background-color: #f8f9fa;
152 | }
153 |
154 | .tab-item.selected {
155 | background-color: #f6f8fa;
156 | }
157 |
158 | .tab-item.disabled {
159 | cursor: not-allowed;
160 | opacity: 0.5;
161 | }
162 |
163 | .tab-radio {
164 | margin-right: 12px;
165 | flex-shrink: 0;
166 | }
167 |
168 | .tab-favicon {
169 | width: 16px;
170 | height: 16px;
171 | margin-right: 8px;
172 | flex-shrink: 0;
173 | }
174 |
175 | .tab-content {
176 | flex: 1;
177 | min-width: 0;
178 | }
179 |
180 | .tab-title {
181 | font-weight: 500;
182 | color: #1f2328;
183 | margin-bottom: 2px;
184 | white-space: nowrap;
185 | overflow: hidden;
186 | text-overflow: ellipsis;
187 | }
188 |
189 | .tab-url {
190 | font-size: 12px;
191 | color: #656d76;
192 | white-space: nowrap;
193 | overflow: hidden;
194 | text-overflow: ellipsis;
195 | }
196 |
197 | /* Link-style button */
198 | .link-button {
199 | background: none;
200 | border: none;
201 | color: #0066cc;
202 | text-decoration: underline;
203 | cursor: pointer;
204 | padding: 0;
205 | font: inherit;
206 | }
207 |
208 | /* Auth token section */
209 | .auth-token-section {
210 | margin: 16px 0;
211 | padding: 16px;
212 | background-color: #f6f8fa;
213 | border-radius: 6px;
214 | }
215 |
216 | .auth-token-description {
217 | font-size: 12px;
218 | color: #656d76;
219 | margin-bottom: 12px;
220 | }
221 |
222 | .auth-token-container {
223 | display: flex;
224 | align-items: center;
225 | gap: 8px;
226 | background-color: #ffffff;
227 | padding: 8px;
228 | }
229 |
230 | .auth-token-code {
231 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
232 | font-size: 12px;
233 | color: #1f2328;
234 | border: none;
235 | flex: 1;
236 | padding: 0;
237 | word-break: break-all;
238 | }
239 |
240 | .auth-token-refresh {
241 | flex: none;
242 | height: 24px;
243 | width: 24px;
244 | border: none;
245 | outline: none;
246 | color: var(--color-fg-muted);
247 | background: transparent;
248 | padding: 4px;
249 | cursor: pointer;
250 | display: inline-flex;
251 | align-items: center;
252 | justify-content: center;
253 | border-radius: 4px;
254 | }
255 |
256 | .auth-token-refresh svg {
257 | margin: 0;
258 | }
259 |
260 | .auth-token-refresh:not(:disabled):hover {
261 | background-color: var(--color-btn-selected-bg);
262 | }
263 |
```
--------------------------------------------------------------------------------
/config.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright (c) Microsoft Corporation.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import type * as playwright from 'playwright';
18 |
19 | export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing';
20 |
21 | export type Config = {
22 | /**
23 | * The browser to use.
24 | */
25 | browser?: {
26 | /**
27 | * The type of browser to use.
28 | */
29 | browserName?: 'chromium' | 'firefox' | 'webkit';
30 |
31 | /**
32 | * Keep the browser profile in memory, do not save it to disk.
33 | */
34 | isolated?: boolean;
35 |
36 | /**
37 | * Path to a user data directory for browser profile persistence.
38 | * Temporary directory is created by default.
39 | */
40 | userDataDir?: string;
41 |
42 | /**
43 | * Launch options passed to
44 | * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
45 | *
46 | * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
47 | */
48 | launchOptions?: playwright.LaunchOptions;
49 |
50 | /**
51 | * Context options for the browser context.
52 | *
53 | * This is useful for settings options like `viewport`.
54 | */
55 | contextOptions?: playwright.BrowserContextOptions;
56 |
57 | /**
58 | * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
59 | */
60 | cdpEndpoint?: string;
61 |
62 | /**
63 | * CDP headers to send with the connect request.
64 | */
65 | cdpHeaders?: Record<string, string>;
66 |
67 | /**
68 | * Remote endpoint to connect to an existing Playwright server.
69 | */
70 | remoteEndpoint?: string;
71 |
72 | /**
73 | * Paths to JavaScript files to add as initialization scripts.
74 | * The scripts will be evaluated in every page before any of the page's scripts.
75 | */
76 | initScript?: string[];
77 | },
78 |
79 | server?: {
80 | /**
81 | * The port to listen on for SSE or MCP transport.
82 | */
83 | port?: number;
84 |
85 | /**
86 | * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
87 | */
88 | host?: string;
89 |
90 | /**
91 | * The hosts this server is allowed to serve from. Defaults to the host server is bound to.
92 | * This is not for CORS, but rather for the DNS rebinding protection.
93 | */
94 | allowedHosts?: string[];
95 | },
96 |
97 | /**
98 | * List of enabled tool capabilities. Possible values:
99 | * - 'core': Core browser automation features.
100 | * - 'pdf': PDF generation and manipulation.
101 | * - 'vision': Coordinate-based interactions.
102 | */
103 | capabilities?: ToolCapability[];
104 |
105 | /**
106 | * Whether to save the Playwright session into the output directory.
107 | */
108 | saveSession?: boolean;
109 |
110 | /**
111 | * Whether to save the Playwright trace of the session into the output directory.
112 | */
113 | saveTrace?: boolean;
114 |
115 | /**
116 | * If specified, saves the Playwright video of the session into the output directory.
117 | */
118 | saveVideo?: {
119 | width: number;
120 | height: number;
121 | };
122 |
123 | /**
124 | * Reuse the same browser context between all connected HTTP clients.
125 | */
126 | sharedBrowserContext?: boolean;
127 |
128 | /**
129 | * Secrets are used to prevent LLM from getting sensitive data while
130 | * automating scenarios such as authentication.
131 | * Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative.
132 | */
133 | secrets?: Record<string, string>;
134 |
135 | /**
136 | * The directory to save output files.
137 | */
138 | outputDir?: string;
139 |
140 | network?: {
141 | /**
142 | * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
143 | */
144 | allowedOrigins?: string[];
145 |
146 | /**
147 | * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
148 | */
149 | blockedOrigins?: string[];
150 | };
151 |
152 | /**
153 | * Specify the attribute to use for test ids, defaults to "data-testid".
154 | */
155 | testIdAttribute?: string;
156 |
157 | timeouts?: {
158 | /*
159 | * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms.
160 | */
161 | action?: number;
162 |
163 | /*
164 | * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms.
165 | */
166 | navigation?: number;
167 | };
168 |
169 | /**
170 | * 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.
171 | */
172 | imageResponses?: 'allow' | 'omit';
173 | };
174 |
175 |
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: '0 8 * * *'
6 | release:
7 | types: [published]
8 |
9 | jobs:
10 | publish-canary-npm:
11 | if: github.event.schedule || github.event_name == 'workflow_dispatch'
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: read
15 | id-token: write # Required for OIDC npm publishing
16 | steps:
17 | - uses: actions/checkout@v5
18 | - uses: actions/setup-node@v5
19 | with:
20 | node-version: 20
21 | registry-url: https://registry.npmjs.org/
22 | # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing)
23 | - name: Update npm
24 | run: npm install -g npm@latest
25 |
26 | - name: Get current date
27 | id: date
28 | run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
29 |
30 | - name: Get current version
31 | id: version
32 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
33 |
34 | - name: Set canary version
35 | id: canary-version
36 | run: echo "version=${{ steps.version.outputs.version }}-alpha-${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
37 |
38 | - name: Update package.json version
39 | run: |
40 | npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version
41 |
42 | - run: npm ci
43 | - run: npx playwright install --with-deps
44 | - run: npm run lint
45 | - run: npm run ctest
46 |
47 | - name: Publish to npm with next tag
48 | run: npm publish --tag next
49 |
50 | publish-release-npm:
51 | if: github.event_name == 'release'
52 | runs-on: ubuntu-latest
53 | permissions:
54 | contents: read
55 | id-token: write # Required for OIDC npm publishing
56 | steps:
57 | - uses: actions/checkout@v5
58 | - uses: actions/setup-node@v5
59 | with:
60 | node-version: 20
61 | registry-url: https://registry.npmjs.org/
62 | # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing)
63 | - name: Update npm
64 | run: npm install -g npm@latest
65 | - run: npm ci
66 | - run: npx playwright install --with-deps
67 | - run: npm run lint
68 | - run: npm run ctest
69 | - run: npm publish
70 |
71 | publish-release-docker:
72 | if: github.event_name == 'release'
73 | runs-on: ubuntu-latest
74 | permissions:
75 | contents: read
76 | id-token: write # Needed for OIDC login to Azure
77 | environment: allow-publishing-docker-to-acr
78 | steps:
79 | - uses: actions/checkout@v5
80 | - name: Set up QEMU # Needed for multi-platform builds (e.g., arm64 on amd64 runner)
81 | uses: docker/setup-qemu-action@v3
82 | - name: Set up Docker Buildx # Needed for multi-platform builds
83 | uses: docker/setup-buildx-action@v3
84 | - name: Azure Login via OIDC
85 | uses: azure/login@v2
86 | with:
87 | client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
88 | tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
89 | subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}
90 | - name: Login to ACR
91 | run: az acr login --name playwright
92 | - name: Build and push Docker image
93 | id: build-push
94 | uses: docker/build-push-action@v6
95 | with:
96 | context: .
97 | file: ./Dockerfile # Adjust path if your Dockerfile is elsewhere
98 | platforms: linux/amd64,linux/arm64
99 | push: true
100 | tags: |
101 | playwright.azurecr.io/public/playwright/mcp:${{ github.event.release.tag_name }}
102 | playwright.azurecr.io/public/playwright/mcp:latest
103 | - uses: oras-project/setup-oras@v1
104 | - name: Set oras tags
105 | run: |
106 | attach_eol_manifest() {
107 | local image="$1"
108 | local today=$(date -u +'%Y-%m-%d')
109 | # oras is re-using Docker credentials, so we don't need to login.
110 | # Following the advice in https://portal.microsofticm.com/imp/v3/incidents/incident/476783820/summary
111 | oras attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation "vnd.microsoft.artifact.lifecycle.end-of-life.date=$today" $image
112 | }
113 | # for each tag, attach the eol manifest
114 | for tag in $(echo ${{ steps.build-push.outputs.metadata['image.name'] }} | tr ',' '\n'); do
115 | attach_eol_manifest $tag
116 | done
117 |
118 | package-release-extension:
119 | if: github.event_name == 'release'
120 | runs-on: ubuntu-latest
121 | permissions:
122 | contents: write # Needed to upload release assets
123 | steps:
124 | - uses: actions/checkout@v5
125 | - uses: actions/setup-node@v5
126 | with:
127 | node-version: 20
128 | cache: 'npm'
129 | - name: Install extension dependencies
130 | working-directory: ./extension
131 | run: npm ci
132 | - name: Build extension
133 | working-directory: ./extension
134 | run: npm run build
135 | - name: Get extension version
136 | id: get-version
137 | working-directory: ./extension
138 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
139 | - name: Package extension
140 | working-directory: ./extension
141 | run: |
142 | cd dist
143 | zip -r ../playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip .
144 | cd ..
145 | - name: Upload extension to release
146 | env:
147 | GITHUB_TOKEN: ${{ github.token }}
148 | run: |
149 | gh release upload ${{github.event.release.tag_name}} ./extension/playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip
150 |
```
--------------------------------------------------------------------------------
/update-readme.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 | // @ts-check
18 |
19 | const fs = require('fs')
20 | const path = require('path')
21 | const { zodToJsonSchema } = require('zod-to-json-schema')
22 | const { execSync } = require('child_process');
23 |
24 | const { browserTools } = require('playwright/lib/mcp/browser/tools');
25 |
26 | const capabilities = {
27 | 'core': 'Core automation',
28 | 'core-tabs': 'Tab management',
29 | 'core-install': 'Browser installation',
30 | 'vision': 'Coordinate-based (opt-in via --caps=vision)',
31 | 'pdf': 'PDF generation (opt-in via --caps=pdf)',
32 | 'testing': 'Test assertions (opt-in via --caps=testing)',
33 | 'tracing': 'Tracing (opt-in via --caps=tracing)',
34 | };
35 |
36 | const toolsByCapability = Object.fromEntries(Object.entries(capabilities).map(([capability, title]) => [title, browserTools.filter(tool => tool.capability === capability).sort((a, b) => a.schema.name.localeCompare(b.schema.name))]));
37 |
38 | /**
39 | * @param {any} tool
40 | * @returns {string[]}
41 | */
42 | function formatToolForReadme(tool) {
43 | const lines = /** @type {string[]} */ ([]);
44 | lines.push(`<!-- NOTE: This has been generated via ${path.basename(__filename)} -->`);
45 | lines.push(``);
46 | lines.push(`- **${tool.name}**`);
47 | lines.push(` - Title: ${tool.title}`);
48 | lines.push(` - Description: ${tool.description}`);
49 |
50 | const inputSchema = /** @type {any} */ (zodToJsonSchema(tool.inputSchema || {}));
51 | const requiredParams = inputSchema.required || [];
52 | if (inputSchema.properties && Object.keys(inputSchema.properties).length) {
53 | lines.push(` - Parameters:`);
54 | Object.entries(inputSchema.properties).forEach(([name, param]) => {
55 | const optional = !requiredParams.includes(name);
56 | const meta = /** @type {string[]} */ ([]);
57 | if (param.type)
58 | meta.push(param.type);
59 | if (optional)
60 | meta.push('optional');
61 | lines.push(` - \`${name}\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}`);
62 | });
63 | } else {
64 | lines.push(` - Parameters: None`);
65 | }
66 | lines.push(` - Read-only: **${tool.type === 'readOnly'}**`);
67 | lines.push('');
68 | return lines;
69 | }
70 |
71 | /**
72 | * @param {string} content
73 | * @param {string} startMarker
74 | * @param {string} endMarker
75 | * @param {string[]} generatedLines
76 | * @returns {Promise<string>}
77 | */
78 | async function updateSection(content, startMarker, endMarker, generatedLines) {
79 | const startMarkerIndex = content.indexOf(startMarker);
80 | const endMarkerIndex = content.indexOf(endMarker);
81 | if (startMarkerIndex === -1 || endMarkerIndex === -1)
82 | throw new Error('Markers for generated section not found in README');
83 |
84 | return [
85 | content.slice(0, startMarkerIndex + startMarker.length),
86 | '',
87 | generatedLines.join('\n'),
88 | '',
89 | content.slice(endMarkerIndex),
90 | ].join('\n');
91 | }
92 |
93 | /**
94 | * @param {string} content
95 | * @returns {Promise<string>}
96 | */
97 | async function updateTools(content) {
98 | console.log('Loading tool information from compiled modules...');
99 |
100 | const generatedLines = /** @type {string[]} */ ([]);
101 | for (const [capability, tools] of Object.entries(toolsByCapability)) {
102 | console.log('Updating tools for capability:', capability);
103 | generatedLines.push(`<details>\n<summary><b>${capability}</b></summary>`);
104 | generatedLines.push('');
105 | for (const tool of tools)
106 | generatedLines.push(...formatToolForReadme(tool.schema));
107 | generatedLines.push(`</details>`);
108 | generatedLines.push('');
109 | }
110 |
111 | const startMarker = `<!--- Tools generated by ${path.basename(__filename)} -->`;
112 | const endMarker = `<!--- End of tools generated section -->`;
113 | return updateSection(content, startMarker, endMarker, generatedLines);
114 | }
115 |
116 | /**
117 | * @param {string} content
118 | * @returns {Promise<string>}
119 | */
120 | async function updateOptions(content) {
121 | console.log('Listing options...');
122 | const output = execSync('node cli.js --help');
123 | const lines = output.toString().split('\n');
124 | const firstLine = lines.findIndex(line => line.includes('--version'));
125 | lines.splice(0, firstLine + 1);
126 | const lastLine = lines.findIndex(line => line.includes('--help'));
127 | lines.splice(lastLine);
128 | const startMarker = `<!--- Options generated by ${path.basename(__filename)} -->`;
129 | const endMarker = `<!--- End of options generated section -->`;
130 | return updateSection(content, startMarker, endMarker, [
131 | '```',
132 | '> npx @playwright/mcp@latest --help',
133 | ...lines,
134 | '```',
135 | ]);
136 | }
137 |
138 | async function updateReadme() {
139 | const readmePath = path.join(__dirname, 'README.md');
140 | const readmeContent = await fs.promises.readFile(readmePath, 'utf-8');
141 | const withTools = await updateTools(readmeContent);
142 | const withOptions = await updateOptions(withTools);
143 | await fs.promises.writeFile(readmePath, withOptions, 'utf-8');
144 | console.log('README updated successfully');
145 | }
146 |
147 | updateReadme().catch(err => {
148 | console.error('Error updating README:', err);
149 | process.exit(1);
150 | });
151 |
```
--------------------------------------------------------------------------------
/tests/testserver/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Copyright 2017 Google Inc. All rights reserved.
3 | * Modifications 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 fs from 'fs';
19 | import http from 'http';
20 | import https from 'https';
21 | import path from 'path';
22 | import debug from 'debug';
23 |
24 | const fulfillSymbol = Symbol('fulfil callback');
25 | const rejectSymbol = Symbol('reject callback');
26 |
27 | export class TestServer {
28 | private _server: http.Server;
29 | readonly debugServer: any;
30 | private _routes = new Map<string, (request: http.IncomingMessage, response: http.ServerResponse) => any>();
31 | private _csp = new Map<string, string>();
32 | private _extraHeaders = new Map<string, object>();
33 | private _requestSubscribers = new Map<string, Promise<any>>();
34 | readonly PORT: number;
35 | readonly PREFIX: string;
36 | readonly CROSS_PROCESS_PREFIX: string;
37 | readonly HELLO_WORLD: string;
38 |
39 | static async create(port: number): Promise<TestServer> {
40 | const server = new TestServer(port);
41 | await new Promise(x => server._server.once('listening', x));
42 | return server;
43 | }
44 |
45 | static async createHTTPS(port: number): Promise<TestServer> {
46 | const server = new TestServer(port, {
47 | key: await fs.promises.readFile(path.join(path.dirname(__filename), 'key.pem')),
48 | cert: await fs.promises.readFile(path.join(path.dirname(__filename), 'cert.pem')),
49 | passphrase: 'aaaa',
50 | });
51 | await new Promise(x => server._server.once('listening', x));
52 | return server;
53 | }
54 |
55 | constructor(port: number, sslOptions?: object) {
56 | if (sslOptions)
57 | this._server = https.createServer(sslOptions, this._onRequest.bind(this));
58 | else
59 | this._server = http.createServer(this._onRequest.bind(this));
60 | this._server.listen(port);
61 | this.debugServer = debug('pw:testserver');
62 |
63 | const cross_origin = '127.0.0.1';
64 | const same_origin = 'localhost';
65 | const protocol = sslOptions ? 'https' : 'http';
66 | this.PORT = port;
67 | this.PREFIX = `${protocol}://${same_origin}:${port}/`;
68 | this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}/`;
69 | this.HELLO_WORLD = `${this.PREFIX}hello-world`;
70 | }
71 |
72 | setCSP(path: string, csp: string) {
73 | this._csp.set(path, csp);
74 | }
75 |
76 | setExtraHeaders(path: string, object: Record<string, string>) {
77 | this._extraHeaders.set(path, object);
78 | }
79 |
80 | async stop() {
81 | this.reset();
82 | await new Promise(x => this._server.close(x));
83 | }
84 |
85 | route(path: string, handler: (request: http.IncomingMessage, response: http.ServerResponse) => any) {
86 | this._routes.set(path, handler);
87 | }
88 |
89 | setContent(path: string, content: string, mimeType: string) {
90 | this.route(path, (req, res) => {
91 | res.writeHead(200, { 'Content-Type': mimeType });
92 | res.end(mimeType === 'text/html' ? `<!DOCTYPE html>${content}` : content);
93 | });
94 | }
95 |
96 | redirect(from: string, to: string) {
97 | this.route(from, (req, res) => {
98 | const headers = this._extraHeaders.get(req.url!) || {};
99 | res.writeHead(302, { ...headers, location: to });
100 | res.end();
101 | });
102 | }
103 |
104 | waitForRequest(path: string): Promise<http.IncomingMessage> {
105 | let promise = this._requestSubscribers.get(path);
106 | if (promise)
107 | return promise;
108 | let fulfill, reject;
109 | promise = new Promise((f, r) => {
110 | fulfill = f;
111 | reject = r;
112 | });
113 | promise[fulfillSymbol] = fulfill;
114 | promise[rejectSymbol] = reject;
115 | this._requestSubscribers.set(path, promise);
116 | return promise;
117 | }
118 |
119 | reset() {
120 | this._routes.clear();
121 | this._csp.clear();
122 | this._extraHeaders.clear();
123 | this._server.closeAllConnections();
124 | const error = new Error('Static Server has been reset');
125 | for (const subscriber of this._requestSubscribers.values())
126 | subscriber[rejectSymbol].call(null, error);
127 | this._requestSubscribers.clear();
128 |
129 | this.setContent('/favicon.ico', '', 'image/x-icon');
130 |
131 | this.setContent('/', ``, 'text/html');
132 |
133 | this.setContent('/hello-world', `
134 | <title>Title</title>
135 | <body>Hello, world!</body>
136 | `, 'text/html');
137 | }
138 |
139 | _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
140 | request.on('error', error => {
141 | if ((error as any).code === 'ECONNRESET')
142 | response.end();
143 | else
144 | throw error;
145 | });
146 | (request as any).postBody = new Promise(resolve => {
147 | const chunks: Buffer[] = [];
148 | request.on('data', chunk => {
149 | chunks.push(chunk);
150 | });
151 | request.on('end', () => resolve(Buffer.concat(chunks)));
152 | });
153 | const path = request.url || '/';
154 | this.debugServer(`request ${request.method} ${path}`);
155 | // Notify request subscriber.
156 | if (this._requestSubscribers.has(path)) {
157 | this._requestSubscribers.get(path)![fulfillSymbol].call(null, request);
158 | this._requestSubscribers.delete(path);
159 | }
160 | const handler = this._routes.get(path);
161 | if (handler) {
162 | handler.call(null, request, response);
163 | } else {
164 | response.writeHead(404);
165 | response.end();
166 | }
167 | }
168 | }
169 |
```
--------------------------------------------------------------------------------
/extension/src/relayConnection.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 function debugLog(...args: unknown[]): void {
18 | const enabled = true;
19 | if (enabled) {
20 | // eslint-disable-next-line no-console
21 | console.log('[Extension]', ...args);
22 | }
23 | }
24 |
25 | type ProtocolCommand = {
26 | id: number;
27 | method: string;
28 | params?: any;
29 | };
30 |
31 | type ProtocolResponse = {
32 | id?: number;
33 | method?: string;
34 | params?: any;
35 | result?: any;
36 | error?: string;
37 | };
38 |
39 | export class RelayConnection {
40 | private _debuggee: chrome.debugger.Debuggee;
41 | private _ws: WebSocket;
42 | private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void;
43 | private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void;
44 | private _tabPromise: Promise<void>;
45 | private _tabPromiseResolve!: () => void;
46 | private _closed = false;
47 |
48 | onclose?: () => void;
49 |
50 | constructor(ws: WebSocket) {
51 | this._debuggee = { };
52 | this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve);
53 | this._ws = ws;
54 | this._ws.onmessage = this._onMessage.bind(this);
55 | this._ws.onclose = () => this._onClose();
56 | // Store listeners for cleanup
57 | this._eventListener = this._onDebuggerEvent.bind(this);
58 | this._detachListener = this._onDebuggerDetach.bind(this);
59 | chrome.debugger.onEvent.addListener(this._eventListener);
60 | chrome.debugger.onDetach.addListener(this._detachListener);
61 | }
62 |
63 | // Either setTabId or close is called after creating the connection.
64 | setTabId(tabId: number): void {
65 | this._debuggee = { tabId };
66 | this._tabPromiseResolve();
67 | }
68 |
69 | close(message: string): void {
70 | this._ws.close(1000, message);
71 | // ws.onclose is called asynchronously, so we call it here to avoid forwarding
72 | // CDP events to the closed connection.
73 | this._onClose();
74 | }
75 |
76 | private _onClose() {
77 | if (this._closed)
78 | return;
79 | this._closed = true;
80 | chrome.debugger.onEvent.removeListener(this._eventListener);
81 | chrome.debugger.onDetach.removeListener(this._detachListener);
82 | chrome.debugger.detach(this._debuggee).catch(() => {});
83 | this.onclose?.();
84 | }
85 |
86 | private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void {
87 | if (source.tabId !== this._debuggee.tabId)
88 | return;
89 | debugLog('Forwarding CDP event:', method, params);
90 | const sessionId = source.sessionId;
91 | this._sendMessage({
92 | method: 'forwardCDPEvent',
93 | params: {
94 | sessionId,
95 | method,
96 | params,
97 | },
98 | });
99 | }
100 |
101 | private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void {
102 | if (source.tabId !== this._debuggee.tabId)
103 | return;
104 | this.close(`Debugger detached: ${reason}`);
105 | this._debuggee = { };
106 | }
107 |
108 | private _onMessage(event: MessageEvent): void {
109 | this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e));
110 | }
111 |
112 | private async _onMessageAsync(event: MessageEvent): Promise<void> {
113 | let message: ProtocolCommand;
114 | try {
115 | message = JSON.parse(event.data);
116 | } catch (error: any) {
117 | debugLog('Error parsing message:', error);
118 | this._sendError(-32700, `Error parsing message: ${error.message}`);
119 | return;
120 | }
121 |
122 | debugLog('Received message:', message);
123 |
124 | const response: ProtocolResponse = {
125 | id: message.id,
126 | };
127 | try {
128 | response.result = await this._handleCommand(message);
129 | } catch (error: any) {
130 | debugLog('Error handling command:', error);
131 | response.error = error.message;
132 | }
133 | debugLog('Sending response:', response);
134 | this._sendMessage(response);
135 | }
136 |
137 | private async _handleCommand(message: ProtocolCommand): Promise<any> {
138 | if (message.method === 'attachToTab') {
139 | await this._tabPromise;
140 | debugLog('Attaching debugger to tab:', this._debuggee);
141 | await chrome.debugger.attach(this._debuggee, '1.3');
142 | const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo');
143 | return {
144 | targetInfo: result?.targetInfo,
145 | };
146 | }
147 | if (!this._debuggee.tabId)
148 | throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.');
149 | if (message.method === 'forwardCDPCommand') {
150 | const { sessionId, method, params } = message.params;
151 | debugLog('CDP command:', method, params);
152 | const debuggerSession: chrome.debugger.DebuggerSession = {
153 | ...this._debuggee,
154 | sessionId,
155 | };
156 | // Forward CDP command to chrome.debugger
157 | return await chrome.debugger.sendCommand(
158 | debuggerSession,
159 | method,
160 | params
161 | );
162 | }
163 | }
164 |
165 | private _sendError(code: number, message: string): void {
166 | this._sendMessage({
167 | error: {
168 | code,
169 | message,
170 | },
171 | });
172 | }
173 |
174 | private _sendMessage(message: any): void {
175 | if (this._ws.readyState === WebSocket.OPEN)
176 | this._ws.send(JSON.stringify(message));
177 | }
178 | }
179 |
```
--------------------------------------------------------------------------------
/extension/src/ui/connect.tsx:
--------------------------------------------------------------------------------
```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 React, { useCallback, useEffect, useState } from 'react';
18 | import { createRoot } from 'react-dom/client';
19 | import { Button, TabItem } from './tabItem';
20 | import { AuthTokenSection, getOrCreateAuthToken } from './authToken';
21 |
22 | import type { TabInfo } from './tabItem';
23 |
24 | type Status =
25 | | { type: 'connecting'; message: string }
26 | | { type: 'connected'; message: string }
27 | | { type: 'error'; message: string }
28 | | { type: 'error'; versionMismatch: { extensionVersion: string; } };
29 |
30 | const SUPPORTED_PROTOCOL_VERSION = 1;
31 |
32 | const ConnectApp: React.FC = () => {
33 | const [tabs, setTabs] = useState<TabInfo[]>([]);
34 | const [status, setStatus] = useState<Status | null>(null);
35 | const [showButtons, setShowButtons] = useState(true);
36 | const [showTabList, setShowTabList] = useState(true);
37 | const [clientInfo, setClientInfo] = useState('unknown');
38 | const [mcpRelayUrl, setMcpRelayUrl] = useState('');
39 | const [newTab, setNewTab] = useState<boolean>(false);
40 |
41 | useEffect(() => {
42 | const runAsync = async () => {
43 | const params = new URLSearchParams(window.location.search);
44 | const relayUrl = params.get('mcpRelayUrl');
45 |
46 | if (!relayUrl) {
47 | setShowButtons(false);
48 | setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' });
49 | return;
50 | }
51 |
52 | setMcpRelayUrl(relayUrl);
53 |
54 | try {
55 | const client = JSON.parse(params.get('client') || '{}');
56 | const info = `${client.name}/${client.version}`;
57 | setClientInfo(info);
58 | setStatus({
59 | type: 'connecting',
60 | message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?`
61 | });
62 | } catch (e) {
63 | setStatus({ type: 'error', message: 'Failed to parse client version.' });
64 | return;
65 | }
66 |
67 | const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10);
68 | const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion;
69 | if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) {
70 | const extensionVersion = chrome.runtime.getManifest().version;
71 | setShowButtons(false);
72 | setShowTabList(false);
73 | setStatus({
74 | type: 'error',
75 | versionMismatch: {
76 | extensionVersion,
77 | }
78 | });
79 | return;
80 | }
81 |
82 | const expectedToken = getOrCreateAuthToken();
83 | const token = params.get('token');
84 | if (token === expectedToken) {
85 | await connectToMCPRelay(relayUrl);
86 | await handleConnectToTab();
87 | return;
88 | }
89 | if (token) {
90 | handleReject('Invalid token provided.');
91 | return;
92 | }
93 |
94 | await connectToMCPRelay(relayUrl);
95 |
96 | // If this is a browser_navigate command, hide the tab list and show simple allow/reject
97 | if (params.get('newTab') === 'true') {
98 | setNewTab(true);
99 | setShowTabList(false);
100 | } else {
101 | await loadTabs();
102 | }
103 | };
104 | void runAsync();
105 | }, []);
106 |
107 | const handleReject = useCallback((message: string) => {
108 | setShowButtons(false);
109 | setShowTabList(false);
110 | setStatus({ type: 'error', message });
111 | }, []);
112 |
113 | const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => {
114 | const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl });
115 | if (!response.success)
116 | handleReject(response.error);
117 | }, [handleReject]);
118 |
119 | const loadTabs = useCallback(async () => {
120 | const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
121 | if (response.success)
122 | setTabs(response.tabs);
123 | else
124 | setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error });
125 | }, []);
126 |
127 | const handleConnectToTab = useCallback(async (tab?: TabInfo) => {
128 | setShowButtons(false);
129 | setShowTabList(false);
130 |
131 | try {
132 | const response = await chrome.runtime.sendMessage({
133 | type: 'connectToTab',
134 | mcpRelayUrl,
135 | tabId: tab?.id,
136 | windowId: tab?.windowId,
137 | });
138 |
139 | if (response?.success) {
140 | setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` });
141 | } else {
142 | setStatus({
143 | type: 'error',
144 | message: response?.error || `MCP client "${clientInfo}" failed to connect.`
145 | });
146 | }
147 | } catch (e) {
148 | setStatus({
149 | type: 'error',
150 | message: `MCP client "${clientInfo}" failed to connect: ${e}`
151 | });
152 | }
153 | }, [clientInfo, mcpRelayUrl]);
154 |
155 | useEffect(() => {
156 | const listener = (message: any) => {
157 | if (message.type === 'connectionTimeout')
158 | handleReject('Connection timed out.');
159 | };
160 | chrome.runtime.onMessage.addListener(listener);
161 | return () => {
162 | chrome.runtime.onMessage.removeListener(listener);
163 | };
164 | }, [handleReject]);
165 |
166 | return (
167 | <div className='app-container'>
168 | <div className='content-wrapper'>
169 | {status && (
170 | <div className='status-container'>
171 | <StatusBanner status={status} />
172 | {showButtons && (
173 | <div className='button-container'>
174 | {newTab ? (
175 | <>
176 | <Button variant='primary' onClick={() => handleConnectToTab()}>
177 | Allow
178 | </Button>
179 | <Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
180 | Reject
181 | </Button>
182 | </>
183 | ) : (
184 | <Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
185 | Reject
186 | </Button>
187 | )}
188 | </div>
189 | )}
190 | </div>
191 | )}
192 |
193 | {status?.type === 'connecting' && (
194 | <AuthTokenSection />
195 | )}
196 |
197 | {showTabList && (
198 | <div>
199 | <div className='tab-section-title'>
200 | Select page to expose to MCP server:
201 | </div>
202 | <div>
203 | {tabs.map(tab => (
204 | <TabItem
205 | key={tab.id}
206 | tab={tab}
207 | button={
208 | <Button variant='primary' onClick={() => handleConnectToTab(tab)}>
209 | Connect
210 | </Button>
211 | }
212 | />
213 | ))}
214 | </div>
215 | </div>
216 | )}
217 | </div>
218 | </div>
219 | );
220 | };
221 |
222 | const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => {
223 | const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md';
224 | const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest';
225 | return (
226 | <div>
227 | Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '}
228 | <a href={latestReleaseUrl}>Click here</a> to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '}
229 | See <a href={readmeUrl} target='_blank' rel='noopener noreferrer'>installation instructions</a> for more details.
230 | </div>
231 | );
232 | };
233 |
234 | const StatusBanner: React.FC<{ status: Status }> = ({ status }) => {
235 | return (
236 | <div className={`status-banner ${status.type}`}>
237 | {'versionMismatch' in status ? (
238 | <VersionMismatchError
239 | extensionVersion={status.versionMismatch.extensionVersion}
240 | />
241 | ) : (
242 | status.message
243 | )}
244 | </div>
245 | );
246 | };
247 |
248 | // Initialize the React app
249 | const container = document.getElementById('root');
250 | if (container) {
251 | const root = createRoot(container);
252 | root.render(<ConnectApp />);
253 | }
254 |
```
--------------------------------------------------------------------------------
/extension/src/background.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 { RelayConnection, debugLog } from './relayConnection';
18 |
19 | type PageMessage = {
20 | type: 'connectToMCPRelay';
21 | mcpRelayUrl: string;
22 | } | {
23 | type: 'getTabs';
24 | } | {
25 | type: 'connectToTab';
26 | tabId?: number;
27 | windowId?: number;
28 | mcpRelayUrl: string;
29 | } | {
30 | type: 'getConnectionStatus';
31 | } | {
32 | type: 'disconnect';
33 | };
34 |
35 | class TabShareExtension {
36 | private _activeConnection: RelayConnection | undefined;
37 | private _connectedTabId: number | null = null;
38 | private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();
39 |
40 | constructor() {
41 | chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this));
42 | chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this));
43 | chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this));
44 | chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
45 | chrome.action.onClicked.addListener(this._onActionClicked.bind(this));
46 | }
47 |
48 | // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
49 | private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
50 | switch (message.type) {
51 | case 'connectToMCPRelay':
52 | this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then(
53 | () => sendResponse({ success: true }),
54 | (error: any) => sendResponse({ success: false, error: error.message }));
55 | return true;
56 | case 'getTabs':
57 | this._getTabs().then(
58 | tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),
59 | (error: any) => sendResponse({ success: false, error: error.message }));
60 | return true;
61 | case 'connectToTab':
62 | const tabId = message.tabId || sender.tab?.id!;
63 | const windowId = message.windowId || sender.tab?.windowId!;
64 | this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then(
65 | () => sendResponse({ success: true }),
66 | (error: any) => sendResponse({ success: false, error: error.message }));
67 | return true; // Return true to indicate that the response will be sent asynchronously
68 | case 'getConnectionStatus':
69 | sendResponse({
70 | connectedTabId: this._connectedTabId
71 | });
72 | return false;
73 | case 'disconnect':
74 | this._disconnect().then(
75 | () => sendResponse({ success: true }),
76 | (error: any) => sendResponse({ success: false, error: error.message }));
77 | return true;
78 | }
79 | return false;
80 | }
81 |
82 | private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise<void> {
83 | try {
84 | debugLog(`Connecting to relay at ${mcpRelayUrl}`);
85 | const socket = new WebSocket(mcpRelayUrl);
86 | await new Promise<void>((resolve, reject) => {
87 | socket.onopen = () => resolve();
88 | socket.onerror = () => reject(new Error('WebSocket error'));
89 | setTimeout(() => reject(new Error('Connection timeout')), 5000);
90 | });
91 |
92 | const connection = new RelayConnection(socket);
93 | connection.onclose = () => {
94 | debugLog('Connection closed');
95 | this._pendingTabSelection.delete(selectorTabId);
96 | // TODO: show error in the selector tab?
97 | };
98 | this._pendingTabSelection.set(selectorTabId, { connection });
99 | debugLog(`Connected to MCP relay`);
100 | } catch (error: any) {
101 | const message = `Failed to connect to MCP relay: ${error.message}`;
102 | debugLog(message);
103 | throw new Error(message);
104 | }
105 | }
106 |
107 | private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise<void> {
108 | try {
109 | debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`);
110 | try {
111 | this._activeConnection?.close('Another connection is requested');
112 | } catch (error: any) {
113 | debugLog(`Error closing active connection:`, error);
114 | }
115 | await this._setConnectedTabId(null);
116 |
117 | this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection;
118 | if (!this._activeConnection)
119 | throw new Error('No active MCP relay connection');
120 | this._pendingTabSelection.delete(selectorTabId);
121 |
122 | this._activeConnection.setTabId(tabId);
123 | this._activeConnection.onclose = () => {
124 | debugLog('MCP connection closed');
125 | this._activeConnection = undefined;
126 | void this._setConnectedTabId(null);
127 | };
128 |
129 | await Promise.all([
130 | this._setConnectedTabId(tabId),
131 | chrome.tabs.update(tabId, { active: true }),
132 | chrome.windows.update(windowId, { focused: true }),
133 | ]);
134 | debugLog(`Connected to MCP bridge`);
135 | } catch (error: any) {
136 | await this._setConnectedTabId(null);
137 | debugLog(`Failed to connect tab ${tabId}:`, error.message);
138 | throw error;
139 | }
140 | }
141 |
142 | private async _setConnectedTabId(tabId: number | null): Promise<void> {
143 | const oldTabId = this._connectedTabId;
144 | this._connectedTabId = tabId;
145 | if (oldTabId && oldTabId !== tabId)
146 | await this._updateBadge(oldTabId, { text: '' });
147 | if (tabId)
148 | await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' });
149 | }
150 |
151 | private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise<void> {
152 | try {
153 | await chrome.action.setBadgeText({ tabId, text });
154 | await chrome.action.setTitle({ tabId, title: title || '' });
155 | if (color)
156 | await chrome.action.setBadgeBackgroundColor({ tabId, color });
157 | } catch (error: any) {
158 | // Ignore errors as the tab may be closed already.
159 | }
160 | }
161 |
162 | private async _onTabRemoved(tabId: number): Promise<void> {
163 | const pendingConnection = this._pendingTabSelection.get(tabId)?.connection;
164 | if (pendingConnection) {
165 | this._pendingTabSelection.delete(tabId);
166 | pendingConnection.close('Browser tab closed');
167 | return;
168 | }
169 | if (this._connectedTabId !== tabId)
170 | return;
171 | this._activeConnection?.close('Browser tab closed');
172 | this._activeConnection = undefined;
173 | this._connectedTabId = null;
174 | }
175 |
176 | private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) {
177 | for (const [tabId, pending] of this._pendingTabSelection) {
178 | if (tabId === activeInfo.tabId) {
179 | if (pending.timerId) {
180 | clearTimeout(pending.timerId);
181 | pending.timerId = undefined;
182 | }
183 | continue;
184 | }
185 | if (!pending.timerId) {
186 | pending.timerId = setTimeout(() => {
187 | const existed = this._pendingTabSelection.delete(tabId);
188 | if (existed) {
189 | pending.connection.close('Tab has been inactive for 5 seconds');
190 | chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' });
191 | }
192 | }, 5000);
193 | return;
194 | }
195 | }
196 | }
197 |
198 | private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) {
199 | if (this._connectedTabId === tabId)
200 | void this._setConnectedTabId(tabId);
201 | }
202 |
203 | private async _getTabs(): Promise<chrome.tabs.Tab[]> {
204 | const tabs = await chrome.tabs.query({});
205 | return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));
206 | }
207 |
208 | private async _onActionClicked(): Promise<void> {
209 | await chrome.tabs.create({
210 | url: chrome.runtime.getURL('status.html'),
211 | active: true
212 | });
213 | }
214 |
215 | private async _disconnect(): Promise<void> {
216 | this._activeConnection?.close('User disconnected');
217 | this._activeConnection = undefined;
218 | await this._setConnectedTabId(null);
219 | }
220 | }
221 |
222 | new TabShareExtension();
223 |
```
--------------------------------------------------------------------------------
/tests/fixtures.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 { chromium } from 'playwright';
20 |
21 | import { test as baseTest, expect as baseExpect } from '@playwright/test';
22 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
23 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
24 | import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
25 | import { TestServer } from './testserver/index';
26 |
27 | import type { Config } from '../config';
28 | import type { BrowserContext } from 'playwright';
29 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
30 | import type { Stream } from 'stream';
31 |
32 | export type TestOptions = {
33 | mcpArgs: string[] | undefined;
34 | mcpBrowser: string | undefined;
35 | mcpMode: 'docker' | undefined;
36 | };
37 |
38 | type CDPServer = {
39 | endpoint: string;
40 | start: () => Promise<BrowserContext>;
41 | };
42 |
43 | export type StartClient = (options?: {
44 | clientName?: string,
45 | args?: string[],
46 | config?: Config,
47 | roots?: { name: string, uri: string }[],
48 | rootsResponseDelay?: number,
49 | extensionToken?: string,
50 | }) => Promise<{ client: Client, stderr: () => string }>;
51 |
52 |
53 | type TestFixtures = {
54 | client: Client;
55 | startClient: StartClient;
56 | wsEndpoint: string;
57 | cdpServer: CDPServer;
58 | server: TestServer;
59 | httpsServer: TestServer;
60 | mcpHeadless: boolean;
61 | };
62 |
63 | type WorkerFixtures = {
64 | _workerServers: { server: TestServer, httpsServer: TestServer };
65 | };
66 |
67 | export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>({
68 |
69 | mcpArgs: [undefined, { option: true }],
70 |
71 | client: async ({ startClient }, use) => {
72 | const { client } = await startClient();
73 | await use(client);
74 | },
75 |
76 | startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => {
77 | const configDir = path.dirname(test.info().config.configFile!);
78 | const clients: Client[] = [];
79 |
80 | await use(async options => {
81 | const args: string[] = mcpArgs ?? [];
82 | if (process.env.CI && process.platform === 'linux')
83 | args.push('--no-sandbox');
84 | if (mcpHeadless)
85 | args.push('--headless');
86 | if (mcpBrowser)
87 | args.push(`--browser=${mcpBrowser}`);
88 | if (options?.args)
89 | args.push(...options.args);
90 | if (options?.config) {
91 | const configFile = testInfo.outputPath('config.json');
92 | await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));
93 | args.push(`--config=${path.relative(configDir, configFile)}`);
94 | }
95 |
96 | const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined);
97 | if (options?.roots) {
98 | client.setRequestHandler(ListRootsRequestSchema, async request => {
99 | if (options.rootsResponseDelay)
100 | await new Promise(resolve => setTimeout(resolve, options.rootsResponseDelay));
101 | return {
102 | roots: options.roots,
103 | };
104 | });
105 | }
106 | const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);
107 | let stderrBuffer = '';
108 | stderr?.on('data', data => {
109 | if (process.env.PWMCP_DEBUG)
110 | process.stderr.write(data);
111 | stderrBuffer += data.toString();
112 | });
113 | clients.push(client);
114 | await client.connect(transport);
115 | await client.ping();
116 | return { client, stderr: () => stderrBuffer };
117 | });
118 |
119 | await Promise.all(clients.map(client => client.close()));
120 | },
121 |
122 | wsEndpoint: async ({ }, use) => {
123 | const browserServer = await chromium.launchServer();
124 | await use(browserServer.wsEndpoint());
125 | await browserServer.close();
126 | },
127 |
128 | cdpServer: async ({ mcpBrowser }, use, testInfo) => {
129 | test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers');
130 |
131 | let browserContext: BrowserContext | undefined;
132 | const port = 3200 + test.info().parallelIndex;
133 | await use({
134 | endpoint: `http://localhost:${port}`,
135 | start: async () => {
136 | if (browserContext)
137 | throw new Error('CDP server already exists');
138 | browserContext = await chromium.launchPersistentContext(testInfo.outputPath('cdp-user-data-dir'), {
139 | channel: mcpBrowser,
140 | headless: true,
141 | args: [
142 | `--remote-debugging-port=${port}`,
143 | ],
144 | });
145 | return browserContext;
146 | }
147 | });
148 | await browserContext?.close();
149 | },
150 |
151 | mcpHeadless: async ({ headless }, use) => {
152 | await use(headless);
153 | },
154 |
155 | mcpBrowser: ['chrome', { option: true }],
156 |
157 | mcpMode: [undefined, { option: true }],
158 |
159 | _workerServers: [async ({ }, use, workerInfo) => {
160 | const port = 8907 + workerInfo.workerIndex * 4;
161 | const server = await TestServer.create(port);
162 |
163 | const httpsPort = port + 1;
164 | const httpsServer = await TestServer.createHTTPS(httpsPort);
165 |
166 | await use({ server, httpsServer });
167 |
168 | await Promise.all([
169 | server.stop(),
170 | httpsServer.stop(),
171 | ]);
172 | }, { scope: 'worker' }],
173 |
174 | server: async ({ _workerServers }, use) => {
175 | _workerServers.server.reset();
176 | await use(_workerServers.server);
177 | },
178 |
179 | httpsServer: async ({ _workerServers }, use) => {
180 | _workerServers.httpsServer.reset();
181 | await use(_workerServers.httpsServer);
182 | },
183 | });
184 |
185 | async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{
186 | transport: Transport,
187 | stderr: Stream | null,
188 | }> {
189 | if (mcpMode === 'docker') {
190 | const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`];
191 | const transport = new StdioClientTransport({
192 | command: 'docker',
193 | args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],
194 | });
195 | return {
196 | transport,
197 | stderr: transport.stderr,
198 | };
199 | }
200 |
201 | const transport = new StdioClientTransport({
202 | command: 'node',
203 | args: [path.join(__dirname, '../cli.js'), ...args],
204 | cwd: path.dirname(test.info().config.configFile!),
205 | stderr: 'pipe',
206 | env: {
207 | ...process.env,
208 | DEBUG: 'pw:mcp:test',
209 | DEBUG_COLORS: '0',
210 | DEBUG_HIDE_DATE: '1',
211 | PWMCP_PROFILES_DIR_FOR_TEST: profilesDir,
212 | ...(extensionToken ? { PLAYWRIGHT_MCP_EXTENSION_TOKEN: extensionToken } : {}),
213 | },
214 | });
215 | return {
216 | transport,
217 | stderr: transport.stderr!,
218 | };
219 | }
220 |
221 | type Response = Awaited<ReturnType<Client['callTool']>>;
222 |
223 | export const expect = baseExpect.extend({
224 | toHaveResponse(response: Response, object: any) {
225 | const parsed = parseResponse(response);
226 | const isNot = this.isNot;
227 | try {
228 | if (isNot)
229 | expect(parsed).not.toEqual(expect.objectContaining(object));
230 | else
231 | expect(parsed).toEqual(expect.objectContaining(object));
232 | } catch (e) {
233 | return {
234 | pass: isNot,
235 | message: () => e.message,
236 | };
237 | }
238 | return {
239 | pass: !isNot,
240 | message: () => ``,
241 | };
242 | },
243 | });
244 |
245 | export function formatOutput(output: string): string[] {
246 | return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean);
247 | }
248 |
249 | function parseResponse(response: any) {
250 | const text = response.content[0].text;
251 | const sections = parseSections(text);
252 |
253 | const result = sections.get('Result');
254 | const code = sections.get('Ran Playwright code');
255 | const tabs = sections.get('Open tabs');
256 | const pageState = sections.get('Page state');
257 | const consoleMessages = sections.get('New console messages');
258 | const modalState = sections.get('Modal state');
259 | const downloads = sections.get('Downloads');
260 | const codeNoFrame = code?.replace(/^```js\n/, '').replace(/\n```$/, '');
261 | const isError = response.isError;
262 | const attachments = response.content.slice(1);
263 |
264 | return {
265 | result,
266 | code: codeNoFrame,
267 | tabs,
268 | pageState,
269 | consoleMessages,
270 | modalState,
271 | downloads,
272 | isError,
273 | attachments,
274 | };
275 | }
276 |
277 | function parseSections(text: string): Map<string, string> {
278 | const sections = new Map<string, string>();
279 | const sectionHeaders = text.split(/^### /m).slice(1); // Remove empty first element
280 |
281 | for (const section of sectionHeaders) {
282 | const firstNewlineIndex = section.indexOf('\n');
283 | if (firstNewlineIndex === -1)
284 | continue;
285 |
286 | const sectionName = section.substring(0, firstNewlineIndex);
287 | const sectionContent = section.substring(firstNewlineIndex + 1).trim();
288 | sections.set(sectionName, sectionContent);
289 | }
290 |
291 | return sections;
292 | }
293 |
```