# Directory Structure
```
├── .github
│ └── workflows
│ ├── npm-publish.yml
│ └── release.yml
├── .gitignore
├── .nvmrc
├── bun.lockb
├── CHANGELOG.md
├── Dockerfile
├── index.ts
├── LICENSE
├── package.json
├── README.md
├── smithery.yaml
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
```
1 | v20.16.0
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | dist
3 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | <h1 align="center">MCP Server Playwright</h1>
2 | <p align="center">
3 | <a href="https://www.automatalabs.io"><img alt="MCP Playwright" src="https://automatalabs.io/icon.svg" height="250"/></a>
4 | </p>
5 | <p align="center">
6 | <b>A Model Context Protocol server that provides browser automation capabilities using Playwright</b></br>
7 | <sub>Enable LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment</sub>
8 | </p>
9 |
10 | <p align="center">
11 | <a href="https://www.npmjs.com/package/@automatalabs/mcp-server-playwright"><img alt="NPM Version" src="https://img.shields.io/npm/v/@automatalabs/mcp-server-playwright.svg" height="20"/></a>
12 | <a href="https://npmcharts.com/compare/@automatalabs/mcp-server-playwright?minimal=true"><img alt="Downloads per month" src="https://img.shields.io/npm/dm/@automatalabs/mcp-server-playwright.svg" height="20"/></a>
13 | <a href="https://github.com/Automata-Labs-team/MCP-Server-Playwright/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/Automata-Labs-team/MCP-Server-Playwright.svg" height="20"/></a>
14 | <a href="https://smithery.ai/server/@automatalabs/mcp-server-playwright"><img alt="Smithery Installs" src="https://smithery.ai/badge/@automatalabs/mcp-server-playwright" height="20"/></a>
15 | </p>
16 |
17 | <a href="https://glama.ai/mcp/servers/9q4zck8po5"><img width="380" height="200" src="https://glama.ai/mcp/servers/9q4zck8po5/badge" alt="MCP-Server-Playwright MCP server" /></a>
18 |
19 | ## Table of Contents
20 |
21 | - [Features](#features)
22 | - [Installation](#installation)
23 | - [Configuration](#configuration)
24 | - [Components](#components)
25 | - [Tools](#tools)
26 | - [Resources](#resources)
27 | - [License](#license)
28 |
29 | ## Features
30 |
31 | - 🌐 Full browser automation capabilities
32 | - 📸 Screenshot capture of entire pages or specific elements
33 | - 🖱️ Comprehensive web interaction (navigation, clicking, form filling)
34 | - 📊 Console log monitoring
35 | - 🔧 JavaScript execution in browser context
36 |
37 | ## Installation
38 |
39 | ### Installing via Smithery
40 |
41 | To install MCP Server Playwright for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@automatalabs/mcp-server-playwright):
42 |
43 | ```bash
44 | npx -y @smithery/cli install @automatalabs/mcp-server-playwright --client claude
45 | ```
46 |
47 | You can install the package using either npx or mcp-get:
48 |
49 | Using npx:
50 | ```bash
51 | npx @automatalabs/mcp-server-playwright install
52 | ```
53 | This command will:
54 | 1. Check your operating system compatibility (Windows/macOS)
55 | 2. Create or update the Claude configuration file
56 | 3. Configure the Playwright server integration
57 |
58 | The configuration file will be automatically created/updated at:
59 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
60 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
61 |
62 | Using mcp-get:
63 | ```bash
64 | npx @michaellatman/mcp-get@latest install @automatalabs/mcp-server-playwright
65 | ```
66 |
67 | ## Configuration
68 |
69 | The installation process will automatically add the following configuration to your Claude config file:
70 |
71 | ```json
72 | {
73 | "mcpServers": {
74 | "playwright": {
75 | "command": "npx",
76 | "args": ["-y", "@automatalabs/mcp-server-playwright"]
77 | }
78 | }
79 | }
80 | ```
81 | ## Using with Cursor
82 |
83 | You can also use MCP Server Playwright with [Cursor](https://www.cursor.so/), an AI-powered code editor. To enable browser automation in Cursor via MCP:
84 |
85 | 1. **Install Playwright browsers** (if not already):
86 | ```bash
87 | npx playwright install
88 | ```
89 |
90 | 2. **Install MCP Server Playwright for Cursor** using Smithery:
91 | ```bash
92 | npx -y @smithery/cli install @automatalabs/mcp-server-playwright --client cursor
93 | ```
94 |
95 | 3. **Configuration file setup**:
96 | If you do not use Claude, the configuration file (`claude_desktop_config.json`) may not be created automatically.
97 | - On Windows, create a folder named `Claude` in `%APPDATA%` (usually `C:\Users\<YourName>\AppData\Roaming\Claude`).
98 | - Inside that folder, create a file named `claude_desktop_config.json` with the following content:
99 |
100 | ```json
101 | {
102 | "serverPort": 3456
103 | }
104 | ```
105 |
106 | 4. **Follow the remaining steps in the [Installation](#installation) section above** to complete the setup.
107 |
108 | Now, you can use all the browser automation tools provided by MCP Server Playwright directly from Cursor’s AI features, such as web navigation, screenshot capture, and JavaScript execution.
109 |
110 | > **Note:** Make sure you have Node.js installed and `npx` available in your system PATH.
111 |
112 | ## Components
113 |
114 | ### Tools
115 |
116 | #### `browser_navigate`
117 | Navigate to any URL in the browser
118 | ```javascript
119 | {
120 | "url": "https://stealthbrowser.cloud"
121 | }
122 | ```
123 |
124 | #### `browser_screenshot`
125 | Capture screenshots of the entire page or specific elements
126 | ```javascript
127 | {
128 | "name": "screenshot-name", // required
129 | "selector": "#element-id", // optional
130 | "fullPage": true // optional, default: false
131 | }
132 | ```
133 |
134 | #### `browser_click`
135 | Click elements on the page using CSS selector
136 | ```javascript
137 | {
138 | "selector": "#button-id"
139 | }
140 | ```
141 |
142 | #### `browser_click_text`
143 | Click elements on the page by their text content
144 | ```javascript
145 | {
146 | "text": "Click me"
147 | }
148 | ```
149 |
150 | #### `browser_hover`
151 | Hover over elements on the page using CSS selector
152 | ```javascript
153 | {
154 | "selector": "#menu-item"
155 | }
156 | ```
157 |
158 | #### `browser_hover_text`
159 | Hover over elements on the page by their text content
160 | ```javascript
161 | {
162 | "text": "Hover me"
163 | }
164 | ```
165 |
166 | #### `browser_fill`
167 | Fill out input fields
168 | ```javascript
169 | {
170 | "selector": "#input-field",
171 | "value": "Hello World"
172 | }
173 | ```
174 |
175 | #### `browser_select`
176 | Select an option in a SELECT element using CSS selector
177 | ```javascript
178 | {
179 | "selector": "#dropdown",
180 | "value": "option-value"
181 | }
182 | ```
183 |
184 | #### `browser_select_text`
185 | Select an option in a SELECT element by its text content
186 | ```javascript
187 | {
188 | "text": "Choose me",
189 | "value": "option-value"
190 | }
191 | ```
192 |
193 | #### `browser_evaluate`
194 | Execute JavaScript in the browser console
195 | ```javascript
196 | {
197 | "script": "document.title"
198 | }
199 | ```
200 |
201 | ### Resources
202 |
203 | 1. **Console Logs** (`console://logs`)
204 | - Access browser console output in text format
205 | - Includes all console messages from the browser
206 |
207 | 2. **Screenshots** (`screenshot://<n>`)
208 | - Access PNG images of captured screenshots
209 | - Referenced by the name specified during capture
210 |
211 | ## License
212 |
213 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/Automata-Labs-team/MCP-Server-Playwright/blob/main/LICENSE) file for details.
214 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "outDir": "./dist",
12 | "rootDir": ".",
13 | },
14 | "include": [
15 | "./**/*.ts"
16 | ],
17 | "exclude": ["node_modules"]
18 | }
19 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required: []
9 | properties: {}
10 | commandFunction:
11 | # A function that produces the CLI command to start the MCP on stdio.
12 | |-
13 | (config) => ({command:'node',args:['dist/index.js'],env:{}})
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Use a Node.js image as the base for building
3 | FROM node:20 AS builder
4 |
5 | # Set the working directory
6 | WORKDIR /app
7 |
8 | # Copy package.json and package-lock.json
9 | COPY package.json ./
10 |
11 | # Install dependencies
12 | RUN npm install
13 |
14 | # Copy the rest of the application code
15 | COPY . .
16 |
17 | # Build the application
18 | RUN npm run build
19 |
20 | # Use a smaller Node.js image for the final output
21 | FROM node:20-slim AS release
22 |
23 | # Set the working directory
24 | WORKDIR /app
25 |
26 | # Copy the built application from the builder stage
27 | COPY --from=builder /app/dist /app/dist
28 | COPY --from=builder /app/package.json /app/package-lock.json /app/node_modules ./
29 |
30 | # Define the entry point for the Docker container
31 | ENTRYPOINT ["node", "dist/index.js"]
32 |
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Releases
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | changelog:
9 | permissions:
10 | contents: write
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Conventional Changelog Action
15 | id: changelog
16 | uses: TriPSs/[email protected]
17 | with:
18 | github-token: ${{ secrets.GH_PAT }}
19 | version-file: './package.json'
20 | - name: create release
21 | uses: actions/create-release@v1
22 | if: ${{ steps.changelog.outputs.skipped == 'false' }}
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GH_PAT }}
25 | with:
26 | tag_name: ${{ steps.changelog.outputs.tag }}
27 | release_name: ${{ steps.changelog.outputs.tag }}
28 | body: ${{ steps.changelog.outputs.clean_changelog }}
29 |
```
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 | workflow_dispatch:
10 |
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: 20
20 | - run: npm install
21 | - run: npm run prepare
22 |
23 | publish-npm:
24 | needs: build
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v4
28 | - uses: actions/setup-node@v4
29 | with:
30 | node-version: 20
31 | registry-url: https://registry.npmjs.org/
32 | - run: npm install
33 | - run: npm ci
34 | - run: npm publish
35 | env:
36 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
37 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@automatalabs/mcp-server-playwright",
3 | "version": "1.2.1",
4 | "description": "MCP server for browser automation using Playwright",
5 | "license": "MIT",
6 | "author": "Automata Labs (https://automatalabs.io)",
7 | "homepage": "https://automatalabs.io",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/Automata-Labs-team/MCP-Server-Playwright.git"
11 | },
12 | "bugs": "https://github.com/Automata-Labs-team/MCP-Server-Playwright/issues",
13 | "type": "module",
14 | "bin": {
15 | "mcp-server-playwright": "dist/index.js"
16 | },
17 | "files": [
18 | "dist"
19 | ],
20 | "scripts": {
21 | "build": "tsc && shx chmod +x dist/*.js",
22 | "prepare": "npm run build",
23 | "watch": "tsc --watch"
24 | },
25 | "dependencies": {
26 | "@modelcontextprotocol/sdk": "0.5.0",
27 | "playwright": "^1.48.0",
28 | "yargs": "^17.7.2"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^22.10.2",
32 | "@types/yargs": "^17.0.33",
33 | "shx": "^0.3.4",
34 | "typescript": "^5.6.2"
35 | }
36 | }
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | ## [1.2.1](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v1.2.0...v1.2.1) (2025-01-24)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * Revert "feat: add xvfb support for linux" ([b910dba](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/b910dbab5de055611e91cceae46ae68b61d177db))
7 | * Revert "feat: use chromium as default browser" ([56934ed](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/56934edaf4a595e0a893457030e735b7ab0d1fc0))
8 |
9 |
10 |
11 | # [1.2.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v1.1.0...v1.2.0) (2025-01-20)
12 |
13 |
14 | ### Features
15 |
16 | * add xvfb support for linux ([b7edd79](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/b7edd79ce57c922241d2cc4c8ff4fded5fe4224f))
17 |
18 |
19 |
20 | # [1.1.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v1.0.0...v1.1.0) (2025-01-20)
21 |
22 |
23 | ### Features
24 |
25 | * use chromium as default browser ([192d18c](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/192d18c327db6a06481445373ca270de02eb2de6))
26 |
27 |
28 |
29 | # [1.0.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v0.9.0...v1.0.0) (2025-01-20)
30 |
31 |
32 |
33 | # [0.9.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v0.8.0...v0.9.0) (2024-12-11)
34 |
35 |
36 | ### Features
37 |
38 | * add Claude config file handling for Windows and macOS ([65b3ac3](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/65b3ac3ee5f5cc2d5600e1cec920db4b15e6287f))
39 |
40 |
41 |
42 |
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import yargs from "yargs/yargs";
4 | import { hideBin } from 'yargs/helpers'
5 | import os from "os";
6 | import path from "path";
7 | import { promises as fs } from "fs";
8 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10 | import {
11 | CallToolRequestSchema,
12 | ListResourcesRequestSchema,
13 | ListToolsRequestSchema,
14 | ReadResourceRequestSchema,
15 | CallToolResult,
16 | TextContent,
17 | ImageContent,
18 | Tool,
19 | } from "@modelcontextprotocol/sdk/types.js";
20 | import playwright, { Browser, Page } from "playwright";
21 |
22 | enum ToolName {
23 | BrowserNavigate = "browser_navigate",
24 | BrowserScreenshot = "browser_screenshot",
25 | BrowserClick = "browser_click",
26 | BrowserClickText = "browser_click_text",
27 | BrowserFill = "browser_fill",
28 | BrowserSelect = "browser_select",
29 | BrowserSelectText = "browser_select_text",
30 | BrowserHover = "browser_hover",
31 | BrowserHoverText = "browser_hover_text",
32 | BrowserEvaluate = "browser_evaluate"
33 | }
34 |
35 | // Define the tools once to avoid repetition
36 | const TOOLS: Tool[] = [
37 | {
38 | name: ToolName.BrowserNavigate,
39 | description: "Navigate to a URL",
40 | inputSchema: {
41 | type: "object",
42 | properties: {
43 | url: { type: "string" },
44 | },
45 | required: ["url"],
46 | },
47 | },
48 | {
49 | name: ToolName.BrowserScreenshot,
50 | description: "Take a screenshot of the current page or a specific element",
51 | inputSchema: {
52 | type: "object",
53 | properties: {
54 | name: { type: "string", description: "Name for the screenshot" },
55 | selector: { type: "string", description: "CSS selector for element to screenshot" },
56 | fullPage: { type: "boolean", description: "Take a full page screenshot (default: false)", default: false },
57 | },
58 | required: ["name"],
59 | },
60 | },
61 | {
62 | name: ToolName.BrowserClick,
63 | description: "Click an element on the page using CSS selector",
64 | inputSchema: {
65 | type: "object",
66 | properties: {
67 | selector: { type: "string", description: "CSS selector for element to click" },
68 | },
69 | required: ["selector"],
70 | },
71 | },
72 | {
73 | name: ToolName.BrowserClickText,
74 | description: "Click an element on the page by its text content",
75 | inputSchema: {
76 | type: "object",
77 | properties: {
78 | text: { type: "string", description: "Text content of the element to click" },
79 | },
80 | required: ["text"],
81 | },
82 | },
83 | {
84 | name: ToolName.BrowserFill,
85 | description: "Fill out an input field",
86 | inputSchema: {
87 | type: "object",
88 | properties: {
89 | selector: { type: "string", description: "CSS selector for input field" },
90 | value: { type: "string", description: "Value to fill" },
91 | },
92 | required: ["selector", "value"],
93 | },
94 | },
95 | {
96 | name: ToolName.BrowserSelect,
97 | description: "Select an element on the page with Select tag using CSS selector",
98 | inputSchema: {
99 | type: "object",
100 | properties: {
101 | selector: { type: "string", description: "CSS selector for element to select" },
102 | value: { type: "string", description: "Value to select" },
103 | },
104 | required: ["selector", "value"],
105 | },
106 | },
107 | {
108 | name: ToolName.BrowserSelectText,
109 | description: "Select an element on the page with Select tag by its text content",
110 | inputSchema: {
111 | type: "object",
112 | properties: {
113 | text: { type: "string", description: "Text content of the element to select" },
114 | value: { type: "string", description: "Value to select" },
115 | },
116 | required: ["text", "value"],
117 | },
118 | },
119 | {
120 | name: ToolName.BrowserHover,
121 | description: "Hover an element on the page using CSS selector",
122 | inputSchema: {
123 | type: "object",
124 | properties: {
125 | selector: { type: "string", description: "CSS selector for element to hover" },
126 | },
127 | required: ["selector"],
128 | },
129 | },
130 | {
131 | name: ToolName.BrowserHoverText,
132 | description: "Hover an element on the page by its text content",
133 | inputSchema: {
134 | type: "object",
135 | properties: {
136 | text: { type: "string", description: "Text content of the element to hover" },
137 | },
138 | required: ["text"],
139 | },
140 | },
141 | {
142 | name: ToolName.BrowserEvaluate,
143 | description: "Execute JavaScript in the browser console",
144 | inputSchema: {
145 | type: "object",
146 | properties: {
147 | script: { type: "string", description: "JavaScript code to execute" },
148 | },
149 | required: ["script"],
150 | },
151 | },
152 | ];
153 |
154 | // Global state
155 | let browser: Browser | undefined;
156 | let page: Page | undefined;
157 | const consoleLogs: string[] = [];
158 | const screenshots = new Map<string, string>();
159 |
160 | async function ensureBrowser() {
161 | if (!browser) {
162 | browser = await playwright.firefox.launch({ headless: false });
163 | }
164 |
165 | if (!page) {
166 | page = await browser.newPage();
167 | }
168 |
169 | page.on("console", (msg) => {
170 | const logEntry = `[${msg.type()}] ${msg.text()}`;
171 | consoleLogs.push(logEntry);
172 | server.notification({
173 | method: "notifications/resources/updated",
174 | params: { uri: "console://logs" },
175 | });
176 | });
177 | return page!;
178 | }
179 |
180 | async function handleToolCall(name: ToolName, args: any): Promise<CallToolResult> {
181 | const page = await ensureBrowser();
182 |
183 | switch (name) {
184 | case ToolName.BrowserNavigate:
185 | await page.goto(args.url);
186 | return {
187 | content: [{
188 | type: "text",
189 | text: `Navigated to ${args.url}`,
190 | }],
191 | isError: false,
192 | };
193 |
194 | case ToolName.BrowserScreenshot: {
195 | const fullPage = (args.fullPage === 'true');
196 |
197 | const screenshot = await (args.selector ?
198 | page.locator(args.selector).screenshot() :
199 | page.screenshot({ fullPage }));
200 | const base64Screenshot = screenshot.toString('base64');
201 |
202 | if (!base64Screenshot) {
203 | return {
204 | content: [{
205 | type: "text",
206 | text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
207 | }],
208 | isError: true,
209 | };
210 | }
211 |
212 | screenshots.set(args.name, base64Screenshot);
213 | server.notification({
214 | method: "notifications/resources/list_changed",
215 | });
216 |
217 | return {
218 | content: [
219 | {
220 | type: "text",
221 | text: `Screenshot '${args.name}' taken`,
222 | } as TextContent,
223 | {
224 | type: "image",
225 | data: base64Screenshot,
226 | mimeType: "image/png",
227 | } as ImageContent,
228 | ],
229 | isError: false,
230 | };
231 | }
232 |
233 | case ToolName.BrowserClick:
234 | try {
235 | await page.locator(args.selector).click();
236 | return {
237 | content: [{
238 | type: "text",
239 | text: `Clicked: ${args.selector}`,
240 | }],
241 | isError: false,
242 | };
243 | } catch (error) {
244 | if((error as Error).message.includes("strict mode violation")) {
245 | console.log("Strict mode violation, retrying on first element...");
246 | try {
247 | await page.locator(args.selector).first().click();
248 | return {
249 | content: [{
250 | type: "text",
251 | text: `Clicked: ${args.selector}`,
252 | }],
253 | isError: false,
254 | };
255 | } catch (error) {
256 | return {
257 | content: [{
258 | type: "text",
259 | text: `Failed (twice) to click ${args.selector}: ${(error as Error).message}`,
260 | }],
261 | isError: true,
262 | };
263 | }
264 | }
265 |
266 | return {
267 | content: [{
268 | type: "text",
269 | text: `Failed to click ${args.selector}: ${(error as Error).message}`,
270 | }],
271 | isError: true,
272 | };
273 | }
274 |
275 | case ToolName.BrowserClickText:
276 | try {
277 | await page.getByText(args.text).click();
278 | return {
279 | content: [{
280 | type: "text",
281 | text: `Clicked element with text: ${args.text}`,
282 | }],
283 | isError: false,
284 | };
285 | } catch (error) {
286 | if((error as Error).message.includes("strict mode violation")) {
287 | console.log("Strict mode violation, retrying on first element...");
288 | try {
289 | await page.getByText(args.text).first().click();
290 | return {
291 | content: [{
292 | type: "text",
293 | text: `Clicked element with text: ${args.text}`,
294 | }],
295 | isError: false,
296 | };
297 | } catch (error) {
298 | return {
299 | content: [{
300 | type: "text",
301 | text: `Failed (twice) to click element with text ${args.text}: ${(error as Error).message}`,
302 | }],
303 | isError: true,
304 | };
305 | }
306 | }
307 | return {
308 | content: [{
309 | type: "text",
310 | text: `Failed to click element with text ${args.text}: ${(error as Error).message}`,
311 | }],
312 | isError: true,
313 | };
314 | }
315 |
316 | case ToolName.BrowserFill:
317 | try {
318 | await page.locator(args.selector).pressSequentially(args.value, { delay: 100 });
319 | return {
320 | content: [{
321 | type: "text",
322 | text: `Filled ${args.selector} with: ${args.value}`,
323 | }],
324 | isError: false,
325 | };
326 | } catch (error) {
327 | if((error as Error).message.includes("strict mode violation")) {
328 | console.log("Strict mode violation, retrying on first element...");
329 | try {
330 | await page.locator(args.selector).first().pressSequentially(args.value, { delay: 100 });
331 | return {
332 | content: [{
333 | type: "text",
334 | text: `Filled ${args.selector} with: ${args.value}`,
335 | }],
336 | isError: false,
337 | };
338 | } catch (error) {
339 | return {
340 | content: [{
341 | type: "text",
342 | text: `Failed (twice) to fill ${args.selector}: ${(error as Error).message}`,
343 | }],
344 | isError: true,
345 | };
346 | }
347 | }
348 | return {
349 | content: [{
350 | type: "text",
351 | text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
352 | }],
353 | isError: true,
354 | };
355 | }
356 |
357 | case ToolName.BrowserSelect:
358 | try {
359 | await page.locator(args.selector).selectOption(args.value);
360 | return {
361 | content: [{
362 | type: "text",
363 | text: `Selected ${args.selector} with: ${args.value}`,
364 | }],
365 | isError: false,
366 | };
367 | } catch (error) {
368 | if((error as Error).message.includes("strict mode violation")) {
369 | console.log("Strict mode violation, retrying on first element...");
370 | try {
371 | await page.locator(args.selector).first().selectOption(args.value);
372 | return {
373 | content: [{
374 | type: "text",
375 | text: `Selected ${args.selector} with: ${args.value}`,
376 | }],
377 | isError: false,
378 | };
379 | } catch (error) {
380 | return {
381 | content: [{
382 | type: "text",
383 | text: `Failed (twice) to select ${args.selector}: ${(error as Error).message}`,
384 | }],
385 | isError: true,
386 | };
387 | }
388 | }
389 | return {
390 | content: [{
391 | type: "text",
392 | text: `Failed to select ${args.selector}: ${(error as Error).message}`,
393 | }],
394 | isError: true,
395 | };
396 | }
397 |
398 | case ToolName.BrowserSelectText:
399 | try {
400 | await page.getByText(args.text).selectOption(args.value);
401 | return {
402 | content: [{
403 | type: "text",
404 | text: `Selected element with text ${args.text} with value: ${args.value}`,
405 | }],
406 | isError: false,
407 | };
408 | } catch (error) {
409 | if((error as Error).message.includes("strict mode violation")) {
410 | console.log("Strict mode violation, retrying on first element...");
411 | try {
412 | await page.getByText(args.text).first().selectOption(args.value);
413 | return {
414 | content: [{
415 | type: "text",
416 | text: `Selected element with text ${args.text} with value: ${args.value}`,
417 | }],
418 | isError: false,
419 | };
420 | } catch (error) {
421 | return {
422 | content: [{
423 | type: "text",
424 | text: `Failed (twice) to select element with text ${args.text}: ${(error as Error).message}`,
425 | }],
426 | isError: true,
427 | };
428 | }
429 | }
430 | return {
431 | content: [{
432 | type: "text",
433 | text: `Failed to select element with text ${args.text}: ${(error as Error).message}`,
434 | }],
435 | isError: true,
436 | };
437 | }
438 |
439 | case ToolName.BrowserHover:
440 | try {
441 | await page.locator(args.selector).hover();
442 | return {
443 | content: [{
444 | type: "text",
445 | text: `Hovered ${args.selector}`,
446 | }],
447 | isError: false,
448 | };
449 | } catch (error) {
450 | if((error as Error).message.includes("strict mode violation")) {
451 | console.log("Strict mode violation, retrying on first element...");
452 | try {
453 | await page.locator(args.selector).first().hover();
454 | return {
455 | content: [{
456 | type: "text",
457 | text: `Hovered ${args.selector}`,
458 | }],
459 | isError: false,
460 | };
461 | } catch (error) {
462 | return {
463 | content: [{
464 | type: "text",
465 | text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
466 | }],
467 | isError: true,
468 | };
469 | }
470 | }
471 | return {
472 | content: [{
473 | type: "text",
474 | text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
475 | }],
476 | isError: true,
477 | };
478 | }
479 |
480 | case ToolName.BrowserHoverText:
481 | try {
482 | await page.getByText(args.text).hover();
483 | return {
484 | content: [{
485 | type: "text",
486 | text: `Hovered element with text: ${args.text}`,
487 | }],
488 | isError: false,
489 | };
490 | } catch (error) {
491 | if((error as Error).message.includes("strict mode violation")) {
492 | console.log("Strict mode violation, retrying on first element...");
493 | try {
494 | await page.getByText(args.text).first().hover();
495 | return {
496 | content: [{
497 | type: "text",
498 | text: `Hovered element with text: ${args.text}`,
499 | }],
500 | isError: false,
501 | };
502 | } catch (error) {
503 | return {
504 | content: [{
505 | type: "text",
506 | text: `Failed (twice) to hover element with text ${args.text}: ${(error as Error).message}`,
507 | }],
508 | isError: true,
509 | };
510 | }
511 | }
512 | return {
513 | content: [{
514 | type: "text",
515 | text: `Failed to hover element with text ${args.text}: ${(error as Error).message}`,
516 | }],
517 | isError: true,
518 | };
519 | }
520 |
521 | case ToolName.BrowserEvaluate:
522 | try {
523 | const result = await page.evaluate((script) => {
524 | const logs: string[] = [];
525 | const originalConsole = { ...console };
526 |
527 | ['log', 'info', 'warn', 'error'].forEach(method => {
528 | (console as any)[method] = (...args: any[]) => {
529 | logs.push(`[${method}] ${args.join(' ')}`);
530 | (originalConsole as any)[method](...args);
531 | };
532 | });
533 |
534 | try {
535 | const result = eval(script);
536 | Object.assign(console, originalConsole);
537 | return { result, logs };
538 | } catch (error) {
539 | Object.assign(console, originalConsole);
540 | throw error;
541 | }
542 | }, args.script);
543 |
544 | return {
545 | content: [
546 | {
547 | type: "text",
548 | text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
549 | },
550 | ],
551 | isError: false,
552 | };
553 | } catch (error) {
554 | return {
555 | content: [{
556 | type: "text",
557 | text: `Script execution failed: ${(error as Error).message}`,
558 | }],
559 | isError: true,
560 | };
561 | }
562 |
563 | default:
564 | return {
565 | content: [{
566 | type: "text",
567 | text: `Unknown tool: ${name}`,
568 | }],
569 | isError: true,
570 | };
571 | }
572 | }
573 |
574 | const server = new Server(
575 | {
576 | name: "automatalabs/playwright",
577 | version: "0.1.0",
578 | },
579 | {
580 | capabilities: {
581 | resources: {},
582 | tools: {},
583 | },
584 | },
585 | );
586 |
587 |
588 | // Setup request handlers
589 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
590 | resources: [
591 | {
592 | uri: "console://logs",
593 | mimeType: "text/plain",
594 | name: "Browser console logs",
595 | },
596 | ...Array.from(screenshots.keys()).map(name => ({
597 | uri: `screenshot://${name}`,
598 | mimeType: "image/png",
599 | name: `Screenshot: ${name}`,
600 | })),
601 | ],
602 | }));
603 |
604 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
605 | const uri = request.params.uri.toString();
606 |
607 | if (uri === "console://logs") {
608 | return {
609 | contents: [{
610 | uri,
611 | mimeType: "text/plain",
612 | text: consoleLogs.join("\n"),
613 | }],
614 | };
615 | }
616 |
617 | if (uri.startsWith("screenshot://")) {
618 | const name = uri.split("://")[1];
619 | const screenshot = screenshots.get(name);
620 | if (screenshot) {
621 | return {
622 | contents: [{
623 | uri,
624 | mimeType: "image/png",
625 | blob: screenshot,
626 | }],
627 | };
628 | }
629 | }
630 |
631 | throw new Error(`Resource not found: ${uri}`);
632 | });
633 |
634 |
635 |
636 | async function runServer() {
637 | const transport = new StdioServerTransport();
638 | await server.connect(transport);
639 |
640 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
641 | tools: TOOLS,
642 | }));
643 |
644 | server.setRequestHandler(CallToolRequestSchema, async (request) =>
645 | handleToolCall(request.params.name as ToolName, request.params.arguments ?? {})
646 | );
647 | }
648 |
649 | async function checkPlatformAndInstall() {
650 | const platform = os.platform();
651 | if (platform === "win32") {
652 | console.log("Installing MCP Playwright Server for Windows...");
653 | try {
654 | const configFilePath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
655 |
656 | let config: any;
657 | try {
658 | // Try to read existing config file
659 | const fileContent = await fs.readFile(configFilePath, 'utf-8');
660 | config = JSON.parse(fileContent);
661 | } catch (error) {
662 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
663 | // Create new config file with mcpServers object
664 | config = { mcpServers: {} };
665 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
666 | console.log("Created new Claude config file");
667 | } else {
668 | console.error("Error reading Claude config file:", error);
669 | process.exit(1);
670 | }
671 | }
672 |
673 | // Ensure mcpServers exists
674 | if (!config.mcpServers) {
675 | config.mcpServers = {};
676 | }
677 |
678 | // Update the playwright configuration
679 | config.mcpServers.playwright = {
680 | command: "npx",
681 | args: ["-y", "@automatalabs/mcp-server-playwright"]
682 | };
683 |
684 | // Write the updated config back to file
685 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
686 | console.log("✓ Successfully updated Claude configuration");
687 |
688 | } catch (error) {
689 | console.error("Error during installation:", error);
690 | process.exit(1);
691 | }
692 | } else if (platform === "darwin") {
693 | console.log("Installing MCP Playwright Server for macOS...");
694 | try {
695 | const configFilePath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
696 |
697 | let config: any;
698 | try {
699 | // Try to read existing config file
700 | const fileContent = await fs.readFile(configFilePath, 'utf-8');
701 | config = JSON.parse(fileContent);
702 | } catch (error) {
703 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
704 | // Create new config file with mcpServers object
705 | config = { mcpServers: {} };
706 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
707 | console.log("Created new Claude config file");
708 | } else {
709 | console.error("Error reading Claude config file:", error);
710 | process.exit(1);
711 | }
712 | }
713 |
714 | // Ensure mcpServers exists
715 | if (!config.mcpServers) {
716 | config.mcpServers = {};
717 | }
718 |
719 | // Update the playwright configuration
720 | config.mcpServers.playwright = {
721 | command: "npx",
722 | args: ["-y", "@automatalabs/mcp-server-playwright"]
723 | };
724 |
725 | // Write the updated config back to file
726 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
727 | console.log("✓ Successfully updated Claude configuration");
728 |
729 | } catch (error) {
730 | console.error("Error during installation:", error);
731 | process.exit(1);
732 | }
733 | } else {
734 | console.error("Unsupported platform:", platform);
735 | process.exit(1);
736 | }
737 | }
738 |
739 | (async () => {
740 | try {
741 | // Parse args but continue with server if no command specified
742 | await yargs(hideBin(process.argv))
743 | .command('install', 'Install MCP-Server-Playwright dependencies', () => {}, async () => {
744 | await checkPlatformAndInstall();
745 | // Exit after successful installation
746 | process.exit(0);
747 | })
748 | .strict()
749 | .help()
750 | .parse();
751 |
752 | // If we get here, no command was specified, so run the server
753 | await runServer().catch(console.error);
754 | } catch (error) {
755 | console.error('Error:', error);
756 | process.exit(1);
757 | }
758 | })();
759 |
```