#
tokens: 11152/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```