# Directory Structure
```
├── .gitignore
├── .npmignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── controllers
│ │ └── playwright.ts
│ ├── mcp
│ │ ├── server.ts
│ │ └── types.ts
│ ├── server.ts
│ └── types
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | dist/
3 | *.log
4 | .env
5 | .DS_Store
6 | Thumbs.db
7 | *.tmp
8 | *.temp
9 | screenshots/
10 | .vscode/settings.json
11 |
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
1 | # Source files
2 | src/
3 |
4 | # Test files
5 | test*
6 | *.test.*
7 | coverage/
8 |
9 | # Development config
10 | tsconfig.json
11 | .gitignore
12 | .git/
13 | .vscode/
14 |
15 | # Documentation
16 | mcp-plan.md
17 |
18 | # Temporary files
19 | *.log
20 | .DS_Store
21 |
22 | # Scripts
23 | *.bat
24 | *.sh
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # PlayMCP Browser Automation Server
2 |
3 | A comprehensive MCP (Model Context Protocol) server for browser automation using Playwright. This server provides **38 powerful tools** for web scraping, testing, and automation.
4 |
5 | <a href="https://glama.ai/mcp/servers/@jomon003/PlayMCP">
6 | <img width="380" height="200" src="https://glama.ai/mcp/servers/@jomon003/PlayMCP/badge" alt="PlayBrowser Automation Server MCP server" />
7 | </a>
8 |
9 | ## Features
10 |
11 | ### 🚀 **Core Browser Automation** (21 tools)
12 | - **Navigation**: `navigate`, `goForward`, `goBack` (via scroll)
13 | - **Interaction**: `click`, `type`, `hover`, `dragAndDrop`, `selectOption`
14 | - **Mouse Control**: `moveMouse`, `mouseMove`, `mouseClick`, `mouseDrag`
15 | - **Keyboard**: `pressKey`
16 | - **Waiting**: `waitForText`, `waitForSelector`
17 | - **Screenshots**: `screenshot`, `takeScreenshot` (enhanced)
18 | - **Page Info**: `getPageSource`, `getPageText`, `getPageTitle`, `getPageUrl`
19 | - **Element Analysis**: `getElementContent`, `getElementHierarchy`
20 | - **Scripts & Styles**: `getScripts`, `getStylesheets`, `getMetaTags`
21 |
22 | ### 🔍 **Advanced Data Extraction** (7 tools)
23 | - **Links & Images**: `getLinks`, `getImages`
24 | - **Forms**: `getForms`
25 | - **Console Monitoring**: `getConsoleMessages`
26 | - **Network Monitoring**: `getNetworkRequests`
27 | - **JavaScript Execution**: `executeJavaScript`, `evaluateWithReturn`
28 |
29 | ### 📁 **File Operations** (2 tools)
30 | - **File Upload**: `uploadFiles`
31 | - **Dialog Handling**: `handleDialog`
32 |
33 | ### ⚙️ **Browser Management** (8 tools)
34 | - **Browser Control**: `openBrowser`, `closeBrowser`
35 | - **Viewport Management**: `resize`
36 | - **Page Manipulation**: `scroll` (enhanced with feedback)
37 | - **Element Hierarchy**: Deep DOM analysis with configurable depth
38 | - **Enhanced Screenshots**: Full page, element-specific, custom paths
39 | - **Mouse Coordinates**: Pixel-perfect mouse control
40 | - **Wait Conditions**: Smart waiting for elements and text
41 |
42 | ## Quick Start
43 |
44 | ### Installation
45 | ```bash
46 | # Clone the repository
47 | git clone https://github.com/jomon003/PlayMCP.git
48 | cd PlayMCP
49 |
50 | # Install dependencies
51 | npm install
52 |
53 | # Build the project
54 | npm run build
55 |
56 | # Test the server
57 | npm test
58 | ```
59 |
60 | ### Basic Usage
61 | ```javascript
62 | // Start the server
63 | node ./dist/server.js
64 |
65 | // Send MCP commands via JSON-RPC
66 | {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
67 | ```
68 |
69 | ## Tool Categories
70 |
71 | ### 🎯 Navigation & Interaction
72 | - **navigate**: Go to any URL
73 | - **goForward**: Navigate forward in browser history
74 | - **click**: Click elements with smart selector resolution
75 | - **type**: Type text with realistic keyboard simulation
76 | - **hover**: Hover over elements for tooltips and interactions
77 | - **dragAndDrop**: Drag elements between locations
78 | - **selectOption**: Choose options from dropdowns and multi-selects
79 | - **pressKey**: Send specific keyboard keys (Enter, Escape, etc.)
80 |
81 | ### ⏱️ Smart Waiting
82 | - **waitForText**: Wait for specific text to appear
83 | - **waitForSelector**: Wait for elements to load
84 | - Built-in timeouts and error handling
85 |
86 | ### 🖱️ Precise Mouse Control
87 | - **mouseMove**: Move to exact coordinates
88 | - **mouseClick**: Click at specific pixels
89 | - **mouseDrag**: Drag between coordinate points
90 | - **moveMouse**: Enhanced mouse positioning
91 |
92 | ### 📊 Data Extraction
93 | - **getElementHierarchy**: Deep DOM structure analysis
94 | - **getConsoleMessages**: Monitor browser console output
95 | - **getNetworkRequests**: Track HTTP requests and responses
96 | - **getLinks**: Extract all page links with metadata
97 | - **getImages**: Get all images with attributes
98 | - **getForms**: Analyze form structures and fields
99 |
100 | ### 🎬 Visual & Media
101 | - **screenshot**: Basic screenshot capture
102 | - **takeScreenshot**: Advanced screenshots (full page, elements, custom paths)
103 | - **resize**: Control viewport dimensions
104 |
105 | ### 📁 File & Dialog Operations
106 | - **uploadFiles**: Handle file input uploads
107 | - **handleDialog**: Manage alerts, confirms, and prompts
108 |
109 | ### ⚙️ JavaScript Execution
110 | - **executeJavaScript**: Run JavaScript code
111 | - **evaluateWithReturn**: Execute JS with return values
112 |
113 | ### Core Browser Controls
114 | - **openBrowser** - Launch a new browser instance with optional headless mode
115 | - **navigate** - Navigate to any URL
116 | - **click** - Click elements using CSS selectors
117 | - **type** - Type text into input fields
118 | - **moveMouse** - Move mouse to specific coordinates
119 | - **scroll** - Scroll the page by specified amounts with enhanced feedback and smooth scrolling support
120 | - **screenshot** - Take screenshots of the page, viewport, or specific elements
121 | - **closeBrowser** - Close the browser instance
122 |
123 | ### Page Content Extraction
124 | - **getPageSource** - Get the complete HTML source code
125 | - **getPageText** - Get the text content (stripped of HTML)
126 | - **getPageTitle** - Get the page title
127 | - **getPageUrl** - Get the current URL
128 | - **getScripts** - Extract all JavaScript code from the page
129 | - **getStylesheets** - Extract all CSS stylesheets
130 | - **getMetaTags** - Get all meta tags with their attributes
131 | - **getLinks** - Get all links with href, text, and title
132 | - **getImages** - Get all images with src, alt, and dimensions
133 | - **getForms** - Get all forms with their fields and attributes
134 | - **getElementContent** - Get HTML and text content of specific elements
135 | - **getElementHierarchy** - Get the hierarchical DOM structure with parent-child relationships
136 |
137 | ### Advanced Capabilities
138 | - **executeJavaScript** - Execute arbitrary JavaScript code on the page and return results
139 |
140 | ## Available Tools Reference
141 |
142 | | Tool | Description | Required Parameters |
143 | |------|-------------|-------------------|
144 | | `openBrowser` | Launch browser instance | `headless?: boolean, debug?: boolean` |
145 | | `navigate` | Navigate to URL | `url: string` |
146 | | `click` | Click element | `selector: string` |
147 | | `type` | Type text into element | `selector: string, text: string` |
148 | | `moveMouse` | Move mouse to coordinates | `x: number, y: number` |
149 | | `scroll` | Scroll page with feedback | `x: number, y: number, smooth?: boolean` |
150 | | `screenshot` | Take screenshot | `path: string, type?: string, selector?: string` |
151 | | `getPageSource` | Get HTML source | None |
152 | | `getPageText` | Get text content | None |
153 | | `getPageTitle` | Get page title | None |
154 | | `getPageUrl` | Get current URL | None |
155 | | `getScripts` | Get JavaScript code | None |
156 | | `getStylesheets` | Get CSS stylesheets | None |
157 | | `getMetaTags` | Get meta tags | None |
158 | | `getLinks` | Get all links | None |
159 | | `getImages` | Get all images | None |
160 | | `getForms` | Get all forms | None |
161 | | `getElementContent` | Get element content | `selector: string` |
162 | | `getElementHierarchy` | Get DOM hierarchy | `selector?: string, maxDepth?: number, includeText?: boolean, includeAttributes?: boolean` |
163 | | `executeJavaScript` | Run JavaScript | `script: string` |
164 | | `closeBrowser` | Close browser | None |
165 |
166 | ## Installation
167 |
168 | ### Complete Installation Steps
169 |
170 | 1. **Prerequisites**
171 | - Node.js 16+ (download from [nodejs.org](https://nodejs.org/))
172 | - Git (for cloning the repository)
173 |
174 | 2. **Clone and Setup**
175 | ```bash
176 | git clone <repository-url>
177 | cd PlayMCP
178 | npm install
179 | npm run build
180 | ```
181 |
182 | 3. **Install Playwright Browsers**
183 | ```bash
184 | npx playwright install
185 | ```
186 | This downloads the necessary browser binaries (Chromium, Firefox, Safari).
187 |
188 | 4. **Verify Installation**
189 | ```bash
190 | npm run start
191 | ```
192 | You should see "Browser Automation MCP Server starting..." if everything is working.
193 |
194 | ### Quick Installation
195 | ```bash
196 | git clone <repository-url>
197 | cd PlayMCP
198 | npm install && npm run build && npx playwright install
199 | ```
200 |
201 | ## Usage
202 |
203 | ### As MCP Server
204 |
205 | Add to your MCP configuration file:
206 |
207 | **Standard MCP Configuration:**
208 | ```json
209 | {
210 | "servers": {
211 | "playmcp-browser": {
212 | "type": "stdio",
213 | "command": "node",
214 | "args": ["./dist/server.js"],
215 | "cwd": "/path/to/PlayMCP",
216 | "description": "Browser automation server using Playwright"
217 | }
218 | }
219 | }
220 | ```
221 |
222 | **Alternative Configuration (works with VS Code GitHub Copilot):**
223 | ```json
224 | {
225 | "servers": {
226 | "playmcp-browser": {
227 | "type": "stdio",
228 | "command": "node",
229 | "args": ["/absolute/path/to/PlayMCP/dist/server.js"]
230 | }
231 | }
232 | }
233 | ```
234 |
235 | **For Windows users:**
236 | ```json
237 | {
238 | "servers": {
239 | "playmcp-browser": {
240 | "type": "stdio",
241 | "command": "node",
242 | "args": ["C:\\path\\to\\PlayMCP\\dist\\server.js"]
243 | }
244 | }
245 | }
246 | ```
247 |
248 | ### VS Code GitHub Copilot Integration
249 |
250 | This MCP server is fully compatible with VS Code GitHub Copilot. After adding the configuration above to your MCP settings, you can use all browser automation tools directly within VS Code.
251 |
252 | ### Configuration Examples
253 |
254 | **Claude Desktop (config.json location):**
255 | - Windows: `%APPDATA%\Claude\config.json`
256 | - macOS: `~/Library/Application Support/Claude/config.json`
257 | - Linux: `~/.config/Claude/config.json`
258 |
259 | **VS Code MCP Extension:**
260 | Add to your VS Code settings.json or MCP configuration file.
261 |
262 | **Example Full Configuration:**
263 | ```json
264 | {
265 | "mcpServers": {
266 | "playmcp-browser": {
267 | "type": "stdio",
268 | "command": "node",
269 | "args": ["/Users/username/PlayMCP/dist/server.js"],
270 | "description": "Browser automation with Playwright"
271 | }
272 | }
273 | }
274 | ```
275 |
276 | ### Tool Examples
277 |
278 | **Basic Web Scraping:**
279 | ```javascript
280 | // Open browser and navigate
281 | await openBrowser({ headless: false, debug: true })
282 | await navigate({ url: "https://example.com" })
283 |
284 | // Extract content
285 | const title = await getPageTitle()
286 | const links = await getLinks()
287 | const forms = await getForms()
288 | ```
289 |
290 | **Form Automation:**
291 | ```javascript
292 | // Fill out a form
293 | await click({ selector: "#login-button" })
294 | await type({ selector: "#username", text: "[email protected]" })
295 | await type({ selector: "#password", text: "password123" })
296 | await click({ selector: "#submit" })
297 | ```
298 |
299 | **Page Interaction:**
300 | ```javascript
301 | // Enhanced scrolling with feedback
302 | await scroll({ x: 0, y: 500, smooth: false })
303 | // Returns: { before: {x: 0, y: 0}, after: {x: 0, y: 500}, scrolled: {x: 0, y: 500} }
304 |
305 | // Smooth scrolling
306 | await scroll({ x: 0, y: 300, smooth: true })
307 |
308 | // Mouse interaction
309 | await moveMouse({ x: 100, y: 200 })
310 | await click({ selector: ".dropdown-menu" })
311 | ```
312 |
313 | **DOM Structure Analysis:**
314 | ```javascript
315 | // Get page hierarchy (3 levels deep)
316 | await getElementHierarchy({ maxDepth: 3 })
317 |
318 | // Get detailed hierarchy with text and attributes
319 | await getElementHierarchy({
320 | selector: "#main-content",
321 | maxDepth: -1,
322 | includeText: true,
323 | includeAttributes: true
324 | })
325 |
326 | // Get basic structure of a specific section
327 | await getElementHierarchy({ selector: ".sidebar", maxDepth: 2 })
328 | ```
329 |
330 | **Advanced JavaScript Execution:**
331 | ```javascript
332 | // Run custom JavaScript
333 | await executeJavaScript({
334 | script: "document.querySelectorAll('h1').length"
335 | })
336 |
337 | // Modify page content
338 | await executeJavaScript({
339 | script: "document.body.style.backgroundColor = 'lightblue'"
340 | })
341 |
342 | // Extract complex data
343 | await executeJavaScript({
344 | script: `
345 | Array.from(document.querySelectorAll('article')).map(article => ({
346 | title: article.querySelector('h2')?.textContent,
347 | summary: article.querySelector('p')?.textContent
348 | }))
349 | `
350 | })
351 | ```
352 |
353 | **Screenshot and Documentation:**
354 | ```javascript
355 | // Take screenshots
356 | await screenshot({ path: "./full-page.png", type: "page" })
357 | await screenshot({ path: "./element.png", type: "element", selector: "#main-content" })
358 | ```
359 |
360 | ## Quick Start
361 |
362 | 1. **Install and setup:**
363 | ```bash
364 | git clone <repo-url> && cd PlayMCP
365 | npm install && npm run build && npx playwright install
366 | ```
367 |
368 | 2. **Add to your MCP client configuration**
369 |
370 | 3. **Start automating:**
371 | ```javascript
372 | await openBrowser({ debug: true })
373 | await navigate({ url: "https://news.ycombinator.com" })
374 | const links = await getLinks()
375 | console.log(`Found ${links.length} links`)
376 |
377 | // Analyze page structure
378 | const hierarchy = await getElementHierarchy({ maxDepth: 2 })
379 | console.log('Page structure:', hierarchy)
380 | ```
381 |
382 | ## Development
383 |
384 | - **src/server.ts** - Main MCP server implementation
385 | - **src/controllers/playwright.ts** - Playwright browser controller
386 | - **src/mcp/** - MCP protocol implementation
387 | - **src/types/** - TypeScript type definitions
388 |
389 | ## Requirements
390 |
391 | ### System Requirements
392 | - **Node.js 16+** (LTS version recommended)
393 | - **Operating System:** Windows, macOS, or Linux
394 | - **Memory:** At least 2GB RAM (4GB+ recommended for heavy usage)
395 | - **Disk Space:** ~500MB for browser binaries and dependencies
396 |
397 | ### Dependencies
398 | - **Playwright:** Handles browser automation (automatically installed)
399 | - **TypeScript:** For compilation (dev dependency)
400 | - **Browser Binaries:** Downloaded via `npx playwright install`
401 |
402 | ## Troubleshooting
403 |
404 | ### Common Issues
405 |
406 | 1. **"Browser not initialized" error**
407 | - Make sure to call `openBrowser` before other browser operations
408 | - Check if Node.js version is 16 or higher
409 |
410 | 2. **Playwright installation fails**
411 | ```bash
412 | # Try manual browser installation
413 | npx playwright install chromium
414 | # Or install all browsers
415 | npx playwright install
416 | ```
417 |
418 | 3. **Permission errors on Linux/macOS**
419 | ```bash
420 | # Make sure the script is executable
421 | chmod +x dist/server.js
422 | ```
423 |
424 | 4. **Path issues in MCP configuration**
425 | - Use absolute paths in the configuration
426 | - On Windows, use double backslashes: `C:\\path\\to\\PlayMCP\\dist\\server.js`
427 | - Verify the path exists: `node /path/to/PlayMCP/dist/server.js`
428 |
429 | 5. **Browser crashes or timeouts**
430 | - Try running with `headless: false` for debugging
431 | - Increase system memory if running multiple browser instances
432 | - Check if antivirus software is blocking browser processes
433 |
434 | ### Testing Your Installation
435 |
436 | ```bash
437 | # Test the server directly
438 | echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node ./dist/server.js
439 | ```
440 |
441 | You should see a JSON response listing all available tools.
442 |
443 | ## License
444 |
445 | MIT License
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "moduleResolution": "node",
6 | "lib": ["ES2020", "DOM"],
7 | "outDir": "./dist",
8 | "rootDir": "./src",
9 | "strict": true,
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "resolveJsonModule": true,
14 | "declaration": true,
15 | "sourceMap": true,
16 | "allowJs": true
17 | },
18 | "include": [
19 | "src/**/*"
20 | ],
21 | "exclude": [
22 | "node_modules",
23 | "dist",
24 | "test-*.js",
25 | "test-*.ts"
26 | ]
27 | }
28 |
```
--------------------------------------------------------------------------------
/src/mcp/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface Tool {
2 | name: string;
3 | description: string;
4 | inputSchema: {
5 | type: string;
6 | properties: Record<string, any>;
7 | required: string[];
8 | };
9 | }
10 |
11 | export interface ServerConfig {
12 | name: string;
13 | version: string;
14 | }
15 |
16 | export interface ServerCapabilities {
17 | capabilities: {
18 | tools: Record<string, Tool>;
19 | };
20 | }
21 |
22 | export interface CallToolRequest {
23 | params: {
24 | name: string;
25 | arguments?: Record<string, any>;
26 | };
27 | }
28 |
29 | export interface ToolResponse {
30 | content: Array<{
31 | type: string;
32 | text: string;
33 | }>;
34 | isError?: boolean;
35 | }
36 |
37 | export interface ListToolsResponse {
38 | tools: Tool[];
39 | }
```
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Page, Browser, BrowserContext } from 'playwright';
2 |
3 | export interface BrowserState {
4 | browser: Browser | null;
5 | context: BrowserContext | null;
6 | page: Page | null;
7 | debug: boolean;
8 | }
9 |
10 | export class BrowserError extends Error {
11 | suggestion?: string;
12 |
13 | constructor(message: string, suggestion?: string) {
14 | super(message);
15 | this.name = 'BrowserError';
16 | this.suggestion = suggestion;
17 | }
18 | }
19 |
20 | export interface ScreenshotOptions {
21 | path: string;
22 | type?: 'element' | 'page' | 'viewport';
23 | selector?: string;
24 | }
25 |
26 | export interface ElementInfo {
27 | tagName: string;
28 | className: string;
29 | id: string;
30 | attributes: Array<{
31 | name: string;
32 | value: string;
33 | }>;
34 | innerText: string | null;
35 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "playmcp",
3 | "version": "2.0.0",
4 | "description": "Comprehensive MCP server for browser automation with 38 powerful Playwright tools",
5 | "main": "dist/server.js",
6 | "type": "module",
7 | "scripts": {
8 | "build": "tsc",
9 | "start": "node dist/server.js",
10 | "dev": "tsc && node dist/server.js"
11 | },
12 | "keywords": [
13 | "browser",
14 | "automation",
15 | "playwright",
16 | "mcp",
17 | "browser-automation"
18 | ],
19 | "files": [
20 | "dist/**/*",
21 | "README.md",
22 | "package.json"
23 | ],
24 | "engines": {
25 | "node": ">=16.0.0"
26 | },
27 | "author": "",
28 | "license": "MIT",
29 | "dependencies": {
30 | "playwright": "^1.40.0"
31 | },
32 | "devDependencies": {
33 | "@types/node": "^20.0.0",
34 | "typescript": "^5.0.0"
35 | }
36 | }
37 |
```
--------------------------------------------------------------------------------
/src/mcp/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Tool, ServerConfig, ServerCapabilities, CallToolRequest, ToolResponse, ListToolsResponse } from './types.js';
2 |
3 | export class Server {
4 | private config: ServerConfig;
5 | private capabilities: ServerCapabilities;
6 | private listToolsHandler?: () => Promise<ListToolsResponse>;
7 | private callToolHandler?: (request: CallToolRequest) => Promise<ToolResponse>;
8 |
9 | constructor(config: ServerConfig, capabilities: ServerCapabilities) {
10 | this.config = config;
11 | this.capabilities = capabilities;
12 | }
13 |
14 | setRequestHandler<T extends 'listTools' | 'callTool'>(
15 | type: T,
16 | handler: T extends 'listTools'
17 | ? () => Promise<ListToolsResponse>
18 | : (request: CallToolRequest) => Promise<ToolResponse>
19 | ) {
20 | if (type === 'listTools') {
21 | this.listToolsHandler = handler as () => Promise<ListToolsResponse>;
22 | } else {
23 | this.callToolHandler = handler as (request: CallToolRequest) => Promise<ToolResponse>;
24 | }
25 | } private async handleInput(input: string) {
26 | try {
27 | const message = JSON.parse(input);
28 |
29 | // Handle MCP initialize request
30 | if (message.method === 'initialize') {
31 | console.log(JSON.stringify({
32 | jsonrpc: "2.0",
33 | id: message.id, result: {
34 | protocolVersion: "2024-11-05",
35 | capabilities: this.capabilities.capabilities,
36 | serverInfo: {
37 | name: this.config.name,
38 | version: this.config.version
39 | }
40 | }
41 | }));
42 | return;
43 | }
44 |
45 | // Handle MCP notifications/initialized
46 | if (message.method === 'notifications/initialized') {
47 | // Acknowledge initialization complete
48 | return;
49 | }
50 |
51 | // Handle MCP tools/list request
52 | if (message.method === 'tools/list') {
53 | if (!this.listToolsHandler) {
54 | throw new Error('No list tools handler registered');
55 | }
56 | const response = await this.listToolsHandler();
57 | console.log(JSON.stringify({
58 | jsonrpc: "2.0",
59 | id: message.id,
60 | result: response
61 | }));
62 | return;
63 | }
64 |
65 | // Handle MCP tools/call request
66 | if (message.method === 'tools/call') {
67 | if (!this.callToolHandler) {
68 | throw new Error('No call tool handler registered');
69 | }
70 | const response = await this.callToolHandler({
71 | params: {
72 | name: message.params.name,
73 | arguments: message.params.arguments
74 | }
75 | });
76 | console.log(JSON.stringify({
77 | jsonrpc: "2.0",
78 | id: message.id,
79 | result: response
80 | }));
81 | return;
82 | }
83 |
84 | // Legacy command handling (keep for backward compatibility)
85 | if (message.command) {
86 | if (!this.callToolHandler) {
87 | throw new Error('No call tool handler registered');
88 | }
89 | const response = await this.callToolHandler({
90 | params: {
91 | name: message.command,
92 | arguments: message.arguments
93 | }
94 | });
95 | console.log(JSON.stringify({
96 | type: "response",
97 | result: {
98 | success: !response.isError,
99 | ...(response.isError ? {
100 | error: {
101 | message: response.content[0].text,
102 | suggestion: response.content[1]?.text
103 | }
104 | } : {
105 | message: response.content[0].text
106 | })
107 | }
108 | }));
109 | }
110 | } catch (error) {
111 | console.log(JSON.stringify({
112 | jsonrpc: "2.0",
113 | id: input.includes('"id"') ? JSON.parse(input).id : null,
114 | error: {
115 | code: -32603,
116 | message: error instanceof Error ? error.message : 'Unknown error',
117 | data: {
118 | suggestion: 'Check input format'
119 | }
120 | }
121 | }));
122 | }
123 | } async connect() {
124 | // Set up stdin handling - wait for initialize request
125 | process.stdin.setEncoding('utf8');
126 | let buffer = '';
127 |
128 | process.stdin.on('data', (chunk: string) => {
129 | buffer += chunk;
130 | if (buffer.includes('\n')) {
131 | const lines = buffer.split('\n');
132 | buffer = lines.pop() || '';
133 |
134 | for (const line of lines) {
135 | if (line.trim()) {
136 | this.handleInput(line);
137 | }
138 | }
139 | }
140 | });
141 |
142 | return new Promise((resolve) => {
143 | process.on('SIGINT', () => {
144 | resolve(undefined);
145 | });
146 | });
147 | }
148 | }
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { Server } from './mcp/server.js';
3 | import { Tool } from './mcp/types.js';
4 | import { playwrightController } from './controllers/playwright.js';
5 |
6 | const OPEN_BROWSER_TOOL: Tool = {
7 | name: "openBrowser",
8 | description: "Launch a new browser instance",
9 | inputSchema: {
10 | type: "object",
11 | properties: {
12 | headless: { type: "boolean" },
13 | debug: { type: "boolean" }
14 | },
15 | required: []
16 | }
17 | };
18 |
19 | const NAVIGATE_TOOL: Tool = {
20 | name: "navigate",
21 | description: "Navigate to a URL",
22 | inputSchema: {
23 | type: "object",
24 | properties: {
25 | url: { type: "string" }
26 | },
27 | required: ["url"]
28 | }
29 | };
30 |
31 | const TYPE_TOOL: Tool = {
32 | name: "type",
33 | description: "Type text into an element",
34 | inputSchema: {
35 | type: "object",
36 | properties: {
37 | selector: { type: "string" },
38 | text: { type: "string" }
39 | },
40 | required: ["selector", "text"]
41 | }
42 | };
43 |
44 | const CLICK_TOOL: Tool = {
45 | name: "click",
46 | description: "Click an element",
47 | inputSchema: {
48 | type: "object",
49 | properties: {
50 | selector: { type: "string" }
51 | },
52 | required: ["selector"]
53 | }
54 | };
55 |
56 | const MOVE_MOUSE_TOOL: Tool = {
57 | name: "moveMouse",
58 | description: "Move mouse to coordinates",
59 | inputSchema: {
60 | type: "object",
61 | properties: {
62 | x: { type: "number" },
63 | y: { type: "number" }
64 | },
65 | required: ["x", "y"]
66 | }
67 | };
68 |
69 | const SCREENSHOT_TOOL: Tool = {
70 | name: "screenshot",
71 | description: "Take a screenshot",
72 | inputSchema: {
73 | type: "object",
74 | properties: {
75 | path: { type: "string" },
76 | type: { type: "string", enum: ["viewport", "element", "page"] },
77 | selector: { type: "string" }
78 | },
79 | required: ["path"]
80 | }
81 | };
82 |
83 | const GET_PAGE_SOURCE_TOOL: Tool = {
84 | name: "getPageSource",
85 | description: "Get the HTML source code of the current page",
86 | inputSchema: {
87 | type: "object",
88 | properties: {},
89 | required: []
90 | }
91 | };
92 |
93 | const GET_PAGE_TEXT_TOOL: Tool = {
94 | name: "getPageText",
95 | description: "Get the text content of the current page",
96 | inputSchema: {
97 | type: "object",
98 | properties: {},
99 | required: []
100 | }
101 | };
102 |
103 | const GET_PAGE_TITLE_TOOL: Tool = {
104 | name: "getPageTitle",
105 | description: "Get the title of the current page",
106 | inputSchema: {
107 | type: "object",
108 | properties: {},
109 | required: []
110 | }
111 | };
112 |
113 | const GET_PAGE_URL_TOOL: Tool = {
114 | name: "getPageUrl",
115 | description: "Get the URL of the current page",
116 | inputSchema: {
117 | type: "object",
118 | properties: {},
119 | required: []
120 | }
121 | };
122 |
123 | const GET_SCRIPTS_TOOL: Tool = {
124 | name: "getScripts",
125 | description: "Get all JavaScript code from the current page",
126 | inputSchema: {
127 | type: "object",
128 | properties: {},
129 | required: []
130 | }
131 | };
132 |
133 | const GET_STYLESHEETS_TOOL: Tool = {
134 | name: "getStylesheets",
135 | description: "Get all CSS stylesheets from the current page",
136 | inputSchema: {
137 | type: "object",
138 | properties: {},
139 | required: []
140 | }
141 | };
142 |
143 | const GET_META_TAGS_TOOL: Tool = {
144 | name: "getMetaTags",
145 | description: "Get all meta tags from the current page",
146 | inputSchema: {
147 | type: "object",
148 | properties: {},
149 | required: []
150 | }
151 | };
152 |
153 | const GET_LINKS_TOOL: Tool = {
154 | name: "getLinks",
155 | description: "Get all links from the current page",
156 | inputSchema: {
157 | type: "object",
158 | properties: {},
159 | required: []
160 | }
161 | };
162 |
163 | const GET_IMAGES_TOOL: Tool = {
164 | name: "getImages",
165 | description: "Get all images from the current page",
166 | inputSchema: {
167 | type: "object",
168 | properties: {},
169 | required: []
170 | }
171 | };
172 |
173 | const GET_FORMS_TOOL: Tool = {
174 | name: "getForms",
175 | description: "Get all forms from the current page",
176 | inputSchema: {
177 | type: "object",
178 | properties: {},
179 | required: []
180 | }
181 | };
182 |
183 | const GET_ELEMENT_CONTENT_TOOL: Tool = {
184 | name: "getElementContent",
185 | description: "Get the HTML and text content of a specific element",
186 | inputSchema: {
187 | type: "object",
188 | properties: {
189 | selector: { type: "string" }
190 | },
191 | required: ["selector"]
192 | }
193 | };
194 |
195 | const EXECUTE_JAVASCRIPT_TOOL: Tool = {
196 | name: "executeJavaScript",
197 | description: "Execute arbitrary JavaScript code on the current page and return the result",
198 | inputSchema: {
199 | type: "object",
200 | properties: {
201 | script: {
202 | type: "string",
203 | description: "The JavaScript code to execute on the page. Can be expressions or statements."
204 | }
205 | },
206 | required: ["script"]
207 | }
208 | };
209 |
210 | const SCROLL_TOOL: Tool = {
211 | name: "scroll",
212 | description: "Scroll the page by specified amounts with enhanced feedback",
213 | inputSchema: {
214 | type: "object",
215 | properties: {
216 | x: {
217 | type: "number",
218 | description: "Horizontal scroll amount in pixels (positive = right, negative = left)"
219 | },
220 | y: {
221 | type: "number",
222 | description: "Vertical scroll amount in pixels (positive = down, negative = up)"
223 | },
224 | smooth: {
225 | type: "boolean",
226 | description: "Whether to use smooth scrolling animation (default: false)"
227 | }
228 | },
229 | required: ["x", "y"]
230 | }
231 | };
232 |
233 | const GET_ELEMENT_HIERARCHY_TOOL: Tool = {
234 | name: "getElementHierarchy",
235 | description: "Get the hierarchical structure of page elements with parent-child relationships",
236 | inputSchema: {
237 | type: "object",
238 | properties: {
239 | selector: {
240 | type: "string",
241 | description: "CSS selector for root element (default: 'body')"
242 | },
243 | maxDepth: {
244 | type: "number",
245 | description: "Maximum depth to traverse (-1 for unlimited, default: 3)"
246 | },
247 | includeText: {
248 | type: "boolean",
249 | description: "Include text content of elements (default: false)"
250 | },
251 | includeAttributes: {
252 | type: "boolean",
253 | description: "Include element attributes (default: false)"
254 | }
255 | },
256 | required: []
257 | }
258 | };
259 |
260 | const GO_FORWARD_TOOL: Tool = {
261 | name: "goForward",
262 | description: "Navigate forward to the next page in history",
263 | inputSchema: {
264 | type: "object",
265 | properties: {},
266 | required: []
267 | }
268 | };
269 |
270 | const HOVER_TOOL: Tool = {
271 | name: "hover",
272 | description: "Hover over an element on the page",
273 | inputSchema: {
274 | type: "object",
275 | properties: {
276 | selector: { type: "string" }
277 | },
278 | required: ["selector"]
279 | }
280 | };
281 |
282 | const DRAG_AND_DROP_TOOL: Tool = {
283 | name: "dragAndDrop",
284 | description: "Drag and drop from one element to another",
285 | inputSchema: {
286 | type: "object",
287 | properties: {
288 | sourceSelector: { type: "string" },
289 | targetSelector: { type: "string" }
290 | },
291 | required: ["sourceSelector", "targetSelector"]
292 | }
293 | };
294 |
295 | const SELECT_OPTION_TOOL: Tool = {
296 | name: "selectOption",
297 | description: "Select option(s) in a dropdown or select element",
298 | inputSchema: {
299 | type: "object",
300 | properties: {
301 | selector: { type: "string" },
302 | values: {
303 | type: "array",
304 | items: { type: "string" },
305 | description: "Array of values to select"
306 | }
307 | },
308 | required: ["selector", "values"]
309 | }
310 | };
311 |
312 | const PRESS_KEY_TOOL: Tool = {
313 | name: "pressKey",
314 | description: "Press a key on the keyboard",
315 | inputSchema: {
316 | type: "object",
317 | properties: {
318 | key: {
319 | type: "string",
320 | description: "Key to press (e.g., 'Enter', 'Escape', 'ArrowDown', etc.)"
321 | }
322 | },
323 | required: ["key"]
324 | }
325 | };
326 |
327 | const WAIT_FOR_TEXT_TOOL: Tool = {
328 | name: "waitForText",
329 | description: "Wait for specific text to appear on the page",
330 | inputSchema: {
331 | type: "object",
332 | properties: {
333 | text: { type: "string" },
334 | timeout: {
335 | type: "number",
336 | description: "Timeout in milliseconds (default: 30000)"
337 | }
338 | },
339 | required: ["text"]
340 | }
341 | };
342 |
343 | const WAIT_FOR_SELECTOR_TOOL: Tool = {
344 | name: "waitForSelector",
345 | description: "Wait for a specific selector to appear on the page",
346 | inputSchema: {
347 | type: "object",
348 | properties: {
349 | selector: { type: "string" },
350 | timeout: {
351 | type: "number",
352 | description: "Timeout in milliseconds (default: 30000)"
353 | }
354 | },
355 | required: ["selector"]
356 | }
357 | };
358 |
359 | const RESIZE_TOOL: Tool = {
360 | name: "resize",
361 | description: "Resize the browser viewport",
362 | inputSchema: {
363 | type: "object",
364 | properties: {
365 | width: { type: "number" },
366 | height: { type: "number" }
367 | },
368 | required: ["width", "height"]
369 | }
370 | };
371 |
372 | const HANDLE_DIALOG_TOOL: Tool = {
373 | name: "handleDialog",
374 | description: "Handle browser dialogs (alerts, confirms, prompts)",
375 | inputSchema: {
376 | type: "object",
377 | properties: {
378 | accept: {
379 | type: "boolean",
380 | description: "Whether to accept (true) or dismiss (false) the dialog"
381 | },
382 | promptText: {
383 | type: "string",
384 | description: "Text to enter in prompt dialogs (optional)"
385 | }
386 | },
387 | required: ["accept"]
388 | }
389 | };
390 |
391 | const GET_CONSOLE_MESSAGES_TOOL: Tool = {
392 | name: "getConsoleMessages",
393 | description: "Get console messages from the browser",
394 | inputSchema: {
395 | type: "object",
396 | properties: {},
397 | required: []
398 | }
399 | };
400 |
401 | const GET_NETWORK_REQUESTS_TOOL: Tool = {
402 | name: "getNetworkRequests",
403 | description: "Get network requests made by the page",
404 | inputSchema: {
405 | type: "object",
406 | properties: {},
407 | required: []
408 | }
409 | };
410 |
411 | const UPLOAD_FILES_TOOL: Tool = {
412 | name: "uploadFiles",
413 | description: "Upload files through a file input element",
414 | inputSchema: {
415 | type: "object",
416 | properties: {
417 | selector: { type: "string" },
418 | filePaths: {
419 | type: "array",
420 | items: { type: "string" },
421 | description: "Array of absolute file paths to upload"
422 | }
423 | },
424 | required: ["selector", "filePaths"]
425 | }
426 | };
427 |
428 | const EVALUATE_WITH_RETURN_TOOL: Tool = {
429 | name: "evaluateWithReturn",
430 | description: "Execute JavaScript code and return the result",
431 | inputSchema: {
432 | type: "object",
433 | properties: {
434 | script: {
435 | type: "string",
436 | description: "JavaScript code to execute"
437 | }
438 | },
439 | required: ["script"]
440 | }
441 | };
442 |
443 | const TAKE_SCREENSHOT_TOOL: Tool = {
444 | name: "takeScreenshot",
445 | description: "Take a screenshot of the page or specific element",
446 | inputSchema: {
447 | type: "object",
448 | properties: {
449 | path: { type: "string" },
450 | fullPage: {
451 | type: "boolean",
452 | description: "Whether to capture the full scrollable page"
453 | },
454 | element: {
455 | type: "string",
456 | description: "CSS selector for element screenshot"
457 | }
458 | },
459 | required: ["path"]
460 | }
461 | };
462 |
463 | const MOUSE_MOVE_TOOL: Tool = {
464 | name: "mouseMove",
465 | description: "Move mouse to specific coordinates",
466 | inputSchema: {
467 | type: "object",
468 | properties: {
469 | x: { type: "number" },
470 | y: { type: "number" }
471 | },
472 | required: ["x", "y"]
473 | }
474 | };
475 |
476 | const MOUSE_CLICK_TOOL: Tool = {
477 | name: "mouseClick",
478 | description: "Click at specific coordinates",
479 | inputSchema: {
480 | type: "object",
481 | properties: {
482 | x: { type: "number" },
483 | y: { type: "number" }
484 | },
485 | required: ["x", "y"]
486 | }
487 | };
488 |
489 | const MOUSE_DRAG_TOOL: Tool = {
490 | name: "mouseDrag",
491 | description: "Drag from one coordinate to another",
492 | inputSchema: {
493 | type: "object",
494 | properties: {
495 | startX: { type: "number" },
496 | startY: { type: "number" },
497 | endX: { type: "number" },
498 | endY: { type: "number" }
499 | },
500 | required: ["startX", "startY", "endX", "endY"]
501 | }
502 | };
503 |
504 | const CLOSE_BROWSER_TOOL: Tool = {
505 | name: "closeBrowser",
506 | description: "Close the browser",
507 | inputSchema: {
508 | type: "object",
509 | properties: {},
510 | required: []
511 | }
512 | };
513 |
514 | const tools = {
515 | openBrowser: OPEN_BROWSER_TOOL,
516 | navigate: NAVIGATE_TOOL,
517 | type: TYPE_TOOL,
518 | click: CLICK_TOOL,
519 | moveMouse: MOVE_MOUSE_TOOL,
520 | scroll: SCROLL_TOOL,
521 | screenshot: SCREENSHOT_TOOL,
522 | getPageSource: GET_PAGE_SOURCE_TOOL,
523 | getPageText: GET_PAGE_TEXT_TOOL,
524 | getPageTitle: GET_PAGE_TITLE_TOOL,
525 | getPageUrl: GET_PAGE_URL_TOOL,
526 | getScripts: GET_SCRIPTS_TOOL,
527 | getStylesheets: GET_STYLESHEETS_TOOL,
528 | getMetaTags: GET_META_TAGS_TOOL,
529 | getLinks: GET_LINKS_TOOL,
530 | getImages: GET_IMAGES_TOOL,
531 | getForms: GET_FORMS_TOOL,
532 | getElementContent: GET_ELEMENT_CONTENT_TOOL,
533 | getElementHierarchy: GET_ELEMENT_HIERARCHY_TOOL,
534 | executeJavaScript: EXECUTE_JAVASCRIPT_TOOL,
535 | goForward: GO_FORWARD_TOOL,
536 | hover: HOVER_TOOL,
537 | dragAndDrop: DRAG_AND_DROP_TOOL,
538 | selectOption: SELECT_OPTION_TOOL,
539 | pressKey: PRESS_KEY_TOOL,
540 | waitForText: WAIT_FOR_TEXT_TOOL,
541 | waitForSelector: WAIT_FOR_SELECTOR_TOOL,
542 | resize: RESIZE_TOOL,
543 | handleDialog: HANDLE_DIALOG_TOOL,
544 | getConsoleMessages: GET_CONSOLE_MESSAGES_TOOL,
545 | getNetworkRequests: GET_NETWORK_REQUESTS_TOOL,
546 | uploadFiles: UPLOAD_FILES_TOOL,
547 | evaluateWithReturn: EVALUATE_WITH_RETURN_TOOL,
548 | takeScreenshot: TAKE_SCREENSHOT_TOOL,
549 | mouseMove: MOUSE_MOVE_TOOL,
550 | mouseClick: MOUSE_CLICK_TOOL,
551 | mouseDrag: MOUSE_DRAG_TOOL,
552 | closeBrowser: CLOSE_BROWSER_TOOL
553 | };
554 |
555 | const server = new Server(
556 | {
557 | name: "playmcp-browser",
558 | version: "1.0.0",
559 | },
560 | {
561 | capabilities: {
562 | tools,
563 | },
564 | }
565 | );
566 |
567 | server.setRequestHandler('listTools', async () => ({
568 | tools: Object.values(tools)
569 | }));
570 |
571 | server.setRequestHandler('callTool', async (request) => {
572 | const { name, arguments: args = {} } = request.params;
573 |
574 | try {
575 | switch (name) {
576 | case 'openBrowser': {
577 | await playwrightController.openBrowser(
578 | args.headless as boolean,
579 | args.debug as boolean
580 | );
581 | return {
582 | content: [{ type: "text", text: "Browser opened successfully" }]
583 | };
584 | }
585 |
586 | case 'navigate': {
587 | if (!args.url || typeof args.url !== 'string') {
588 | return {
589 | content: [{ type: "text", text: "URL is required" }],
590 | isError: true
591 | };
592 | }
593 | await playwrightController.navigate(args.url);
594 | return {
595 | content: [{ type: "text", text: "Navigation successful" }]
596 | };
597 | }
598 |
599 | case 'type': {
600 | if (!args.selector || !args.text) {
601 | return {
602 | content: [{ type: "text", text: "Selector and text are required" }],
603 | isError: true
604 | };
605 | }
606 | await playwrightController.type(args.selector as string, args.text as string);
607 | return {
608 | content: [{ type: "text", text: "Text entered successfully" }]
609 | };
610 | }
611 |
612 | case 'click': {
613 | if (!args.selector) {
614 | return {
615 | content: [{ type: "text", text: "Selector is required" }],
616 | isError: true
617 | };
618 | }
619 | await playwrightController.click(args.selector as string);
620 | return {
621 | content: [{ type: "text", text: "Click successful" }]
622 | };
623 | }
624 |
625 | case 'moveMouse': {
626 | if (typeof args.x !== 'number' || typeof args.y !== 'number') {
627 | return {
628 | content: [{ type: "text", text: "X and Y coordinates are required" }],
629 | isError: true
630 | };
631 | }
632 | await playwrightController.moveMouse(args.x, args.y);
633 | return {
634 | content: [{ type: "text", text: "Mouse moved successfully" }]
635 | };
636 | }
637 |
638 | case 'scroll': {
639 | if (typeof args.x !== 'number' || typeof args.y !== 'number') {
640 | return {
641 | content: [{ type: "text", text: "X and Y scroll amounts are required" }],
642 | isError: true
643 | };
644 | }
645 | const scrollResult = await playwrightController.scroll(
646 | args.x,
647 | args.y,
648 | args.smooth as boolean || false
649 | );
650 | return {
651 | content: [{
652 | type: "text",
653 | text: JSON.stringify({
654 | message: "Page scrolled successfully",
655 | before: scrollResult.before,
656 | after: scrollResult.after,
657 | scrolled: {
658 | x: scrollResult.after.x - scrollResult.before.x,
659 | y: scrollResult.after.y - scrollResult.before.y
660 | }
661 | }, null, 2)
662 | }]
663 | };
664 | }
665 |
666 | case 'screenshot': {
667 | if (!args.path) {
668 | return {
669 | content: [{ type: "text", text: "Path is required" }],
670 | isError: true
671 | };
672 | }
673 | await playwrightController.screenshot(args as any);
674 | return {
675 | content: [{ type: "text", text: "Screenshot taken successfully" }]
676 | };
677 | }
678 |
679 | case 'getPageSource': {
680 | const source = await playwrightController.getPageSource();
681 | return {
682 | content: [{ type: "text", text: source }]
683 | };
684 | }
685 |
686 | case 'getPageText': {
687 | const text = await playwrightController.getPageText();
688 | return {
689 | content: [{ type: "text", text }]
690 | };
691 | }
692 |
693 | case 'getPageTitle': {
694 | const title = await playwrightController.getPageTitle();
695 | return {
696 | content: [{ type: "text", text: title }]
697 | };
698 | }
699 |
700 | case 'getPageUrl': {
701 | const url = await playwrightController.getPageUrl();
702 | return {
703 | content: [{ type: "text", text: url }]
704 | };
705 | }
706 |
707 | case 'getScripts': {
708 | const scripts = await playwrightController.getScripts();
709 | return {
710 | content: [{ type: "text", text: scripts.join('\n') }]
711 | };
712 | }
713 |
714 | case 'getStylesheets': {
715 | const stylesheets = await playwrightController.getStylesheets();
716 | return {
717 | content: [{ type: "text", text: stylesheets.join('\n') }]
718 | };
719 | } case 'getMetaTags': {
720 | const metaTags = await playwrightController.getMetaTags();
721 | return {
722 | content: [{ type: "text", text: JSON.stringify(metaTags, null, 2) }]
723 | };
724 | }
725 |
726 | case 'getLinks': {
727 | const links = await playwrightController.getLinks();
728 | return {
729 | content: [{ type: "text", text: JSON.stringify(links, null, 2) }]
730 | };
731 | }
732 |
733 | case 'getImages': {
734 | const images = await playwrightController.getImages();
735 | return {
736 | content: [{ type: "text", text: JSON.stringify(images, null, 2) }]
737 | };
738 | }
739 |
740 | case 'getForms': {
741 | const forms = await playwrightController.getForms();
742 | return {
743 | content: [{ type: "text", text: JSON.stringify(forms, null, 2) }]
744 | };
745 | }
746 |
747 | case 'getElementContent': {
748 | if (!args.selector) {
749 | return {
750 | content: [{ type: "text", text: "Selector is required" }],
751 | isError: true
752 | };
753 | }
754 | const content = await playwrightController.getElementContent(args.selector as string);
755 | return {
756 | content: [{ type: "text", text: JSON.stringify(content, null, 2) }]
757 | };
758 | }
759 |
760 | case 'getElementHierarchy': {
761 | const hierarchy = await playwrightController.getElementHierarchy(
762 | args.selector as string || 'body',
763 | args.maxDepth as number || 3,
764 | args.includeText as boolean || false,
765 | args.includeAttributes as boolean || false
766 | );
767 | return {
768 | content: [{ type: "text", text: JSON.stringify(hierarchy, null, 2) }]
769 | };
770 | }
771 |
772 | case 'executeJavaScript': {
773 | if (!args.script || typeof args.script !== 'string') {
774 | return {
775 | content: [{ type: "text", text: "JavaScript script is required" }],
776 | isError: true
777 | };
778 | }
779 | const result = await playwrightController.executeJavaScript(args.script);
780 | return {
781 | content: [{
782 | type: "text",
783 | text: result !== undefined ? JSON.stringify(result, null, 2) : "Script executed successfully (no return value)"
784 | }]
785 | };
786 | }
787 |
788 | case 'goForward': {
789 | await playwrightController.goForward();
790 | return {
791 | content: [{ type: "text", text: "Navigated forward successfully" }]
792 | };
793 | }
794 |
795 | case 'hover': {
796 | if (!args.selector) {
797 | return {
798 | content: [{ type: "text", text: "Selector is required" }],
799 | isError: true
800 | };
801 | }
802 | await playwrightController.hover(args.selector as string);
803 | return {
804 | content: [{ type: "text", text: "Hover completed successfully" }]
805 | };
806 | }
807 |
808 | case 'dragAndDrop': {
809 | if (!args.sourceSelector || !args.targetSelector) {
810 | return {
811 | content: [{ type: "text", text: "Source and target selectors are required" }],
812 | isError: true
813 | };
814 | }
815 | await playwrightController.dragAndDrop(args.sourceSelector as string, args.targetSelector as string);
816 | return {
817 | content: [{ type: "text", text: "Drag and drop completed successfully" }]
818 | };
819 | }
820 |
821 | case 'selectOption': {
822 | if (!args.selector || !args.values) {
823 | return {
824 | content: [{ type: "text", text: "Selector and values are required" }],
825 | isError: true
826 | };
827 | }
828 | await playwrightController.selectOption(args.selector as string, args.values as string[]);
829 | return {
830 | content: [{ type: "text", text: "Option selected successfully" }]
831 | };
832 | }
833 |
834 | case 'pressKey': {
835 | if (!args.key) {
836 | return {
837 | content: [{ type: "text", text: "Key is required" }],
838 | isError: true
839 | };
840 | }
841 | await playwrightController.pressKey(args.key as string);
842 | return {
843 | content: [{ type: "text", text: "Key pressed successfully" }]
844 | };
845 | }
846 |
847 | case 'waitForText': {
848 | if (!args.text) {
849 | return {
850 | content: [{ type: "text", text: "Text is required" }],
851 | isError: true
852 | };
853 | }
854 | await playwrightController.waitForText(args.text as string, args.timeout as number);
855 | return {
856 | content: [{ type: "text", text: "Text found successfully" }]
857 | };
858 | }
859 |
860 | case 'waitForSelector': {
861 | if (!args.selector) {
862 | return {
863 | content: [{ type: "text", text: "Selector is required" }],
864 | isError: true
865 | };
866 | }
867 | await playwrightController.waitForSelector(args.selector as string, args.timeout as number);
868 | return {
869 | content: [{ type: "text", text: "Selector found successfully" }]
870 | };
871 | }
872 |
873 | case 'resize': {
874 | if (typeof args.width !== 'number' || typeof args.height !== 'number') {
875 | return {
876 | content: [{ type: "text", text: "Width and height are required" }],
877 | isError: true
878 | };
879 | }
880 | await playwrightController.resize(args.width, args.height);
881 | return {
882 | content: [{ type: "text", text: "Browser resized successfully" }]
883 | };
884 | }
885 |
886 | case 'handleDialog': {
887 | if (typeof args.accept !== 'boolean') {
888 | return {
889 | content: [{ type: "text", text: "Accept parameter is required" }],
890 | isError: true
891 | };
892 | }
893 | await playwrightController.handleDialog(args.accept, args.promptText as string);
894 | return {
895 | content: [{ type: "text", text: "Dialog handler set successfully" }]
896 | };
897 | }
898 |
899 | case 'getConsoleMessages': {
900 | const messages = await playwrightController.getConsoleMessages();
901 | return {
902 | content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
903 | };
904 | }
905 |
906 | case 'getNetworkRequests': {
907 | const requests = await playwrightController.getNetworkRequests();
908 | return {
909 | content: [{ type: "text", text: JSON.stringify(requests, null, 2) }]
910 | };
911 | }
912 |
913 | case 'uploadFiles': {
914 | if (!args.selector || !args.filePaths) {
915 | return {
916 | content: [{ type: "text", text: "Selector and file paths are required" }],
917 | isError: true
918 | };
919 | }
920 | await playwrightController.uploadFiles(args.selector as string, args.filePaths as string[]);
921 | return {
922 | content: [{ type: "text", text: "Files uploaded successfully" }]
923 | };
924 | }
925 |
926 | case 'evaluateWithReturn': {
927 | if (!args.script || typeof args.script !== 'string') {
928 | return {
929 | content: [{ type: "text", text: "JavaScript script is required" }],
930 | isError: true
931 | };
932 | }
933 | const result = await playwrightController.evaluateWithReturn(args.script);
934 | return {
935 | content: [{
936 | type: "text",
937 | text: result !== undefined ? JSON.stringify(result, null, 2) : "null"
938 | }]
939 | };
940 | }
941 |
942 | case 'takeScreenshot': {
943 | if (!args.path) {
944 | return {
945 | content: [{ type: "text", text: "Path is required" }],
946 | isError: true
947 | };
948 | }
949 | await playwrightController.takeScreenshot(args.path as string, {
950 | fullPage: args.fullPage as boolean,
951 | element: args.element as string
952 | });
953 | return {
954 | content: [{ type: "text", text: "Screenshot taken successfully" }]
955 | };
956 | }
957 |
958 | case 'mouseMove': {
959 | if (typeof args.x !== 'number' || typeof args.y !== 'number') {
960 | return {
961 | content: [{ type: "text", text: "X and Y coordinates are required" }],
962 | isError: true
963 | };
964 | }
965 | await playwrightController.mouseMove(args.x, args.y);
966 | return {
967 | content: [{ type: "text", text: "Mouse moved successfully" }]
968 | };
969 | }
970 |
971 | case 'mouseClick': {
972 | if (typeof args.x !== 'number' || typeof args.y !== 'number') {
973 | return {
974 | content: [{ type: "text", text: "X and Y coordinates are required" }],
975 | isError: true
976 | };
977 | }
978 | await playwrightController.mouseClick(args.x, args.y);
979 | return {
980 | content: [{ type: "text", text: "Mouse clicked successfully" }]
981 | };
982 | }
983 |
984 | case 'mouseDrag': {
985 | if (typeof args.startX !== 'number' || typeof args.startY !== 'number' ||
986 | typeof args.endX !== 'number' || typeof args.endY !== 'number') {
987 | return {
988 | content: [{ type: "text", text: "Start and end coordinates are required" }],
989 | isError: true
990 | };
991 | }
992 | await playwrightController.mouseDrag(args.startX, args.startY, args.endX, args.endY);
993 | return {
994 | content: [{ type: "text", text: "Mouse drag completed successfully" }]
995 | };
996 | }
997 |
998 | case 'closeBrowser': {
999 | await playwrightController.closeBrowser();
1000 | return {
1001 | content: [{ type: "text", text: "Browser closed successfully" }]
1002 | };
1003 | }
1004 |
1005 | default:
1006 | return {
1007 | content: [{ type: "text", text: `Unknown tool: ${name}` }],
1008 | isError: true
1009 | };
1010 | }
1011 | } catch (error: any) {
1012 | return {
1013 | content: [{
1014 | type: "text",
1015 | text: `Error: ${error.message}${error.suggestion ? `\nSuggestion: ${error.suggestion}` : ''}`
1016 | }],
1017 | isError: true
1018 | };
1019 | }
1020 | });
1021 |
1022 | async function runServer() {
1023 | console.error("Browser Automation MCP Server starting...");
1024 | await server.connect();
1025 | }
1026 |
1027 | // Handle process exit
1028 | process.on('SIGINT', async () => {
1029 | try {
1030 | await playwrightController.closeBrowser();
1031 | } catch (error) {
1032 | // Ignore errors during cleanup
1033 | }
1034 | process.exit(0);
1035 | });
1036 |
1037 | runServer().catch((error) => {
1038 | console.error("Fatal error running server:", error);
1039 | process.exit(1);
1040 | });
```
--------------------------------------------------------------------------------
/src/controllers/playwright.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { chromium } from 'playwright';
2 | import { BrowserError, BrowserState, ScreenshotOptions, ElementInfo } from '../types/index.js';
3 |
4 | class PlaywrightController {
5 | private state: BrowserState = {
6 | browser: null,
7 | context: null,
8 | page: null,
9 | debug: false
10 | };
11 |
12 | private currentMousePosition = { x: 0, y: 0 };
13 |
14 | private log(...args: any[]) {
15 | if (this.state.debug) {
16 | console.log(JSON.stringify({
17 | type: "debug",
18 | message: args.map(arg =>
19 | typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
20 | ).join(' ')
21 | }));
22 | }
23 | }
24 |
25 | async openBrowser(headless: boolean = false, debug: boolean = false): Promise<void> {
26 | try {
27 | this.state.debug = debug;
28 | this.log('Attempting to launch browser');
29 |
30 | if (this.state.browser?.isConnected()) {
31 | this.log('Browser already running');
32 | return;
33 | }
34 |
35 | this.log('Launching new browser instance', { headless });
36 | this.state.browser = await chromium.launch({
37 | headless,
38 | args: ['--no-sandbox']
39 | });
40 |
41 | this.log('Creating browser context');
42 | this.state.context = await this.state.browser.newContext({
43 | viewport: { width: 1280, height: 720 }
44 | });
45 |
46 | this.log('Creating new page');
47 | this.state.page = await this.state.context.newPage();
48 |
49 | this.log('Browser successfully launched');
50 | } catch (error: any) {
51 | console.error('Browser launch error:', error);
52 | throw new BrowserError(
53 | 'Failed to launch browser',
54 | `Technical details: ${error?.message || 'Unknown error'}`
55 | );
56 | }
57 | }
58 |
59 | async closeBrowser(): Promise<void> {
60 | try {
61 | this.log('Closing browser');
62 | await this.state.page?.close();
63 | await this.state.context?.close();
64 | await this.state.browser?.close();
65 | this.state = { browser: null, context: null, page: null, debug: false };
66 | this.log('Browser closed');
67 | } catch (error: any) {
68 | console.error('Browser close error:', error);
69 | throw new BrowserError('Failed to close browser', 'The browser might have already been closed');
70 | }
71 | }
72 |
73 | async navigate(url: string): Promise<void> {
74 | try {
75 | if (!this.isInitialized()) {
76 | throw new Error('Browser not initialized');
77 | }
78 | this.log('Navigating to', url);
79 | await this.state.page?.goto(url);
80 | this.log('Navigation complete');
81 | } catch (error: any) {
82 | console.error('Navigation error:', error);
83 | throw new BrowserError('Failed to navigate', 'Check if the URL is valid and accessible');
84 | }
85 | }
86 |
87 | async goBack(): Promise<void> {
88 | try {
89 | if (!this.isInitialized()) {
90 | throw new Error('Browser not initialized');
91 | }
92 | this.log('Going back');
93 | await this.state.page?.goBack();
94 | this.log('Navigation back complete');
95 | } catch (error: any) {
96 | console.error('Go back error:', error);
97 | throw new BrowserError('Failed to go back', 'Check if there is a previous page in history');
98 | }
99 | }
100 |
101 | async refresh(): Promise<void> {
102 | try {
103 | if (!this.isInitialized()) {
104 | throw new Error('Browser not initialized');
105 | }
106 | this.log('Refreshing page');
107 | await this.state.page?.reload();
108 | this.log('Page refresh complete');
109 | } catch (error: any) {
110 | console.error('Refresh error:', error);
111 | throw new BrowserError('Failed to refresh page', 'Check if the page is still accessible');
112 | }
113 | }
114 |
115 | async click(selector?: string): Promise<void> {
116 | try {
117 | if (!this.isInitialized() || !this.state.page) {
118 | throw new Error('Browser not initialized');
119 | }
120 | if (selector) {
121 | this.log('Clicking element', selector);
122 | await this.state.page.click(selector);
123 | } else {
124 | this.log('Clicking at position', this.currentMousePosition);
125 | await this.state.page.mouse.click(this.currentMousePosition.x, this.currentMousePosition.y);
126 | }
127 | this.log('Click complete');
128 | } catch (error: any) {
129 | console.error('Click error:', error);
130 | throw new BrowserError(
131 | 'Failed to click',
132 | selector ? 'Check if the element exists and is visible' : 'Check if mouse position is valid'
133 | );
134 | }
135 | }
136 |
137 | async type(selector: string, text: string): Promise<void> {
138 | try {
139 | if (!this.isInitialized()) {
140 | throw new Error('Browser not initialized');
141 | }
142 | this.log('Typing into element', { selector, text });
143 | await this.state.page?.type(selector, text);
144 | this.log('Type complete');
145 | } catch (error: any) {
146 | console.error('Type error:', error);
147 | throw new BrowserError('Failed to type text', 'Check if the input element exists and is editable');
148 | }
149 | }
150 |
151 | async moveMouse(x: number, y: number): Promise<void> {
152 | try {
153 | if (!this.isInitialized()) {
154 | throw new Error('Browser not initialized');
155 | }
156 | this.log('Moving mouse to', { x, y });
157 | await this.state.page?.mouse.move(x, y);
158 | this.currentMousePosition = { x, y };
159 | this.log('Mouse move complete');
160 | } catch (error: any) {
161 | console.error('Mouse move error:', error);
162 | throw new BrowserError('Failed to move mouse', 'Check if coordinates are within viewport');
163 | }
164 | }
165 |
166 | async scroll(x: number, y: number, smooth: boolean = false): Promise<{before: {x: number, y: number}, after: {x: number, y: number}}> {
167 | try {
168 | if (!this.isInitialized() || !this.state.page) {
169 | throw new Error('Browser not initialized');
170 | }
171 |
172 | this.log('Scrolling', { x, y, smooth });
173 |
174 | // Get scroll position before scrolling
175 | const beforeScroll = await this.state.page.evaluate(() => ({
176 | x: window.scrollX,
177 | y: window.scrollY
178 | }));
179 |
180 | // Perform scroll with optional smooth behavior
181 | await this.state.page.evaluate((args: {x: number, y: number, smooth: boolean}) => {
182 | window.scrollBy({
183 | left: args.x,
184 | top: args.y,
185 | behavior: args.smooth ? 'smooth' : 'auto'
186 | });
187 | }, { x, y, smooth });
188 |
189 | // Wait for scroll to complete
190 | await this.state.page.waitForTimeout(smooth ? 500 : 100);
191 |
192 | // Get scroll position after scrolling
193 | const afterScroll = await this.state.page.evaluate(() => ({
194 | x: window.scrollX,
195 | y: window.scrollY
196 | }));
197 |
198 | this.log('Scroll complete', { before: beforeScroll, after: afterScroll });
199 |
200 | return {
201 | before: beforeScroll,
202 | after: afterScroll
203 | };
204 | } catch (error: any) {
205 | console.error('Scroll error:', error);
206 | throw new BrowserError('Failed to scroll', 'Check if scroll values are valid');
207 | }
208 | }
209 |
210 | async screenshot(options: ScreenshotOptions): Promise<void> {
211 | try {
212 | if (!this.isInitialized() || !this.state.page) {
213 | throw new Error('Browser not initialized');
214 | }
215 | this.log('Taking screenshot', options);
216 |
217 | if (options.type === 'element' && options.selector) {
218 | const element = await this.state.page.$(options.selector);
219 | if (!element) {
220 | throw new Error('Element not found');
221 | }
222 | await element.screenshot({ path: options.path });
223 | } else if (options.type === 'viewport') {
224 | await this.state.page.screenshot({ path: options.path });
225 | } else {
226 | await this.state.page.screenshot({ path: options.path, fullPage: true });
227 | }
228 |
229 | this.log('Screenshot saved to', options.path);
230 | } catch (error: any) {
231 | console.error('Screenshot error:', error);
232 | throw new BrowserError(
233 | 'Failed to take screenshot',
234 | 'Check if the path is writable and element exists (if capturing element)'
235 | );
236 | }
237 | }
238 |
239 | async inspectElement(selector: string): Promise<ElementInfo> {
240 | try {
241 | if (!this.isInitialized() || !this.state.page) {
242 | throw new Error('Browser not initialized');
243 | }
244 | this.log('Inspecting element', selector);
245 | const info = await this.state.page.$eval(selector, (el: Element) => ({
246 | tagName: el.tagName,
247 | className: el.className,
248 | id: el.id,
249 | attributes: Array.from(el.attributes).map(attr => ({
250 | name: attr.name,
251 | value: attr.value
252 | })),
253 | innerText: el.textContent
254 | }));
255 | this.log('Element inspection complete');
256 | return info;
257 | } catch (error: any) {
258 | console.error('Inspect element error:', error);
259 | throw new BrowserError('Failed to inspect element', 'Check if the element exists');
260 | }
261 | }
262 |
263 | async getPageSource(): Promise<string> {
264 | try {
265 | if (!this.isInitialized()) {
266 | throw new Error('Browser not initialized');
267 | }
268 | this.log('Getting page source');
269 | const content = await this.state.page?.content();
270 | this.log('Page source retrieved');
271 | return content || '';
272 | } catch (error: any) {
273 | console.error('Get page source error:', error);
274 | throw new BrowserError('Failed to get page source', 'Check if the page is loaded');
275 | }
276 | }
277 |
278 | async getPageText(): Promise<string> {
279 | try {
280 | if (!this.isInitialized()) {
281 | throw new Error('Browser not initialized');
282 | }
283 | this.log('Getting page text content');
284 | const text = await this.state.page?.innerText('body');
285 | this.log('Page text retrieved');
286 | return text || '';
287 | } catch (error: any) {
288 | console.error('Get page text error:', error);
289 | throw new BrowserError('Failed to get page text', 'Check if the page is loaded');
290 | }
291 | }
292 |
293 | async getPageTitle(): Promise<string> {
294 | try {
295 | if (!this.isInitialized()) {
296 | throw new Error('Browser not initialized');
297 | }
298 | this.log('Getting page title');
299 | const title = await this.state.page?.title();
300 | this.log('Page title retrieved:', title);
301 | return title || '';
302 | } catch (error: any) {
303 | console.error('Get page title error:', error);
304 | throw new BrowserError('Failed to get page title', 'Check if the page is loaded');
305 | }
306 | }
307 |
308 | async getPageUrl(): Promise<string> {
309 | try {
310 | if (!this.isInitialized()) {
311 | throw new Error('Browser not initialized');
312 | }
313 | this.log('Getting page URL');
314 | const url = this.state.page?.url();
315 | this.log('Page URL retrieved:', url);
316 | return url || '';
317 | } catch (error: any) {
318 | console.error('Get page URL error:', error);
319 | throw new BrowserError('Failed to get page URL', 'Check if the page is loaded');
320 | }
321 | }
322 |
323 | async getScripts(): Promise<string[]> {
324 | try {
325 | if (!this.isInitialized()) {
326 | throw new Error('Browser not initialized');
327 | }
328 | this.log('Getting page scripts');
329 | const scripts = await this.state.page?.evaluate(() => {
330 | const scriptElements = Array.from(document.querySelectorAll('script'));
331 | return scriptElements.map(script => {
332 | if (script.src) {
333 | return `// External script: ${script.src}`;
334 | }
335 | return script.textContent || script.innerHTML;
336 | }).filter(content => content.trim().length > 0);
337 | });
338 | this.log('Scripts retrieved:', scripts?.length);
339 | return scripts || [];
340 | } catch (error: any) {
341 | console.error('Get scripts error:', error);
342 | throw new BrowserError('Failed to get scripts', 'Check if the page is loaded');
343 | }
344 | }
345 |
346 | async getStylesheets(): Promise<string[]> {
347 | try {
348 | if (!this.isInitialized()) {
349 | throw new Error('Browser not initialized');
350 | }
351 | this.log('Getting page stylesheets');
352 | const stylesheets = await this.state.page?.evaluate(() => {
353 | const styleElements = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'));
354 | return styleElements.map(element => {
355 | if (element.tagName === 'LINK') {
356 | const link = element as HTMLLinkElement;
357 | return `/* External stylesheet: ${link.href} */`;
358 | }
359 | return element.textContent || element.innerHTML;
360 | }).filter(content => content.trim().length > 0);
361 | });
362 | this.log('Stylesheets retrieved:', stylesheets?.length);
363 | return stylesheets || [];
364 | } catch (error: any) {
365 | console.error('Get stylesheets error:', error);
366 | throw new BrowserError('Failed to get stylesheets', 'Check if the page is loaded');
367 | }
368 | }
369 |
370 | async getMetaTags(): Promise<Array<{name?: string, property?: string, content?: string, httpEquiv?: string}>> {
371 | try {
372 | if (!this.isInitialized()) {
373 | throw new Error('Browser not initialized');
374 | }
375 | this.log('Getting meta tags');
376 | const metaTags = await this.state.page?.evaluate(() => {
377 | const metaElements = Array.from(document.querySelectorAll('meta'));
378 | return metaElements.map(meta => ({
379 | name: meta.getAttribute('name') || undefined,
380 | property: meta.getAttribute('property') || undefined,
381 | content: meta.getAttribute('content') || undefined,
382 | httpEquiv: meta.getAttribute('http-equiv') || undefined
383 | }));
384 | });
385 | this.log('Meta tags retrieved:', metaTags?.length);
386 | return metaTags || [];
387 | } catch (error: any) {
388 | console.error('Get meta tags error:', error);
389 | throw new BrowserError('Failed to get meta tags', 'Check if the page is loaded');
390 | }
391 | }
392 |
393 | async getLinks(): Promise<Array<{href: string, text: string, title?: string}>> {
394 | try {
395 | if (!this.isInitialized()) {
396 | throw new Error('Browser not initialized');
397 | }
398 | this.log('Getting page links');
399 | const links = await this.state.page?.evaluate(() => {
400 | const linkElements = Array.from(document.querySelectorAll('a[href]'));
401 | return linkElements.map(link => ({
402 | href: (link as HTMLAnchorElement).href,
403 | text: link.textContent?.trim() || '',
404 | title: link.getAttribute('title') || undefined
405 | }));
406 | });
407 | this.log('Links retrieved:', links?.length);
408 | return links || [];
409 | } catch (error: any) {
410 | console.error('Get links error:', error);
411 | throw new BrowserError('Failed to get links', 'Check if the page is loaded');
412 | }
413 | }
414 |
415 | async getImages(): Promise<Array<{src: string, alt?: string, title?: string, width?: number, height?: number}>> {
416 | try {
417 | if (!this.isInitialized()) {
418 | throw new Error('Browser not initialized');
419 | }
420 | this.log('Getting page images');
421 | const images = await this.state.page?.evaluate(() => {
422 | const imgElements = Array.from(document.querySelectorAll('img'));
423 | return imgElements.map(img => ({
424 | src: (img as HTMLImageElement).src,
425 | alt: img.getAttribute('alt') || undefined,
426 | title: img.getAttribute('title') || undefined,
427 | width: (img as HTMLImageElement).naturalWidth || undefined,
428 | height: (img as HTMLImageElement).naturalHeight || undefined
429 | }));
430 | });
431 | this.log('Images retrieved:', images?.length);
432 | return images || [];
433 | } catch (error: any) {
434 | console.error('Get images error:', error);
435 | throw new BrowserError('Failed to get images', 'Check if the page is loaded');
436 | }
437 | }
438 |
439 | async getForms(): Promise<Array<{action?: string, method?: string, fields: Array<{name?: string, type?: string, value?: string}>}>> {
440 | try {
441 | if (!this.isInitialized()) {
442 | throw new Error('Browser not initialized');
443 | }
444 | this.log('Getting page forms');
445 | const forms = await this.state.page?.evaluate(() => {
446 | const formElements = Array.from(document.querySelectorAll('form'));
447 | return formElements.map(form => ({
448 | action: form.getAttribute('action') || undefined,
449 | method: form.getAttribute('method') || undefined,
450 | fields: Array.from(form.querySelectorAll('input, select, textarea')).map(field => ({
451 | name: field.getAttribute('name') || undefined,
452 | type: field.getAttribute('type') || field.tagName.toLowerCase(),
453 | value: (field as HTMLInputElement).value || undefined
454 | }))
455 | }));
456 | });
457 | this.log('Forms retrieved:', forms?.length);
458 | return forms || [];
459 | } catch (error: any) {
460 | console.error('Get forms error:', error);
461 | throw new BrowserError('Failed to get forms', 'Check if the page is loaded');
462 | }
463 | }
464 |
465 | async getElementContent(selector: string): Promise<{html: string, text: string}> {
466 | try {
467 | if (!this.isInitialized()) {
468 | throw new Error('Browser not initialized');
469 | }
470 | this.log('Getting element content for selector:', selector);
471 | const content = await this.state.page?.evaluate((sel) => {
472 | const element = document.querySelector(sel);
473 | if (!element) {
474 | throw new Error(`Element not found: ${sel}`);
475 | }
476 | return {
477 | html: element.innerHTML,
478 | text: element.textContent || ''
479 | };
480 | }, selector);
481 | this.log('Element content retrieved');
482 | return content || {html: '', text: ''};
483 | } catch (error: any) {
484 | console.error('Get element content error:', error);
485 | throw new BrowserError('Failed to get element content', 'Check if the element exists');
486 | }
487 | }
488 |
489 | async executeJavaScript(script: string): Promise<any> {
490 | try {
491 | if (!this.isInitialized()) {
492 | throw new Error('Browser not initialized');
493 | }
494 | this.log('Executing JavaScript:', script);
495 | const result = await this.state.page?.evaluate((scriptToExecute) => {
496 | // Create a function wrapper to handle different types of JavaScript code
497 | try {
498 | // If the script is an expression, return its value
499 | // If the script is statements, execute them and return undefined
500 | const wrappedScript = `
501 | (function() {
502 | ${scriptToExecute}
503 | })()
504 | `;
505 | return eval(wrappedScript);
506 | } catch (error) {
507 | // If wrapping fails, try executing directly
508 | return eval(scriptToExecute);
509 | }
510 | }, script);
511 | this.log('JavaScript execution completed:', result);
512 | return result;
513 | } catch (error: any) {
514 | console.error('Execute JavaScript error:', error);
515 | throw new BrowserError('Failed to execute JavaScript', 'Check if the JavaScript syntax is valid');
516 | }
517 | }
518 |
519 | async getElementHierarchy(
520 | selector: string = 'body',
521 | maxDepth: number = 3,
522 | includeText: boolean = false,
523 | includeAttributes: boolean = false
524 | ): Promise<any> {
525 | try {
526 | if (!this.isInitialized()) {
527 | throw new Error('Browser not initialized');
528 | }
529 |
530 | this.log('Getting element hierarchy', { selector, maxDepth, includeText, includeAttributes });
531 |
532 | const hierarchy = await this.state.page?.evaluate((args: {
533 | selector: string,
534 | maxDepth: number,
535 | includeText: boolean,
536 | includeAttributes: boolean
537 | }) => {
538 | const { selector, maxDepth, includeText, includeAttributes } = args;
539 |
540 | function getElementInfo(element: Element) {
541 | const info: any = {
542 | tagName: element.tagName.toLowerCase(),
543 | id: element.id || undefined,
544 | className: element.className || undefined,
545 | children: []
546 | };
547 |
548 | if (includeText && element.textContent) {
549 | // Get only direct text content, not from children
550 | const directText = Array.from(element.childNodes)
551 | .filter(node => node.nodeType === Node.TEXT_NODE)
552 | .map(node => node.textContent?.trim())
553 | .filter(text => text)
554 | .join(' ');
555 | if (directText) {
556 | info.text = directText;
557 | }
558 | }
559 |
560 | if (includeAttributes && element.attributes.length > 0) {
561 | info.attributes = {};
562 | for (let i = 0; i < element.attributes.length; i++) {
563 | const attr = element.attributes[i];
564 | if (attr.name !== 'id' && attr.name !== 'class') {
565 | info.attributes[attr.name] = attr.value;
566 | }
567 | }
568 | }
569 |
570 | return info;
571 | }
572 |
573 | function traverseElement(element: Element, currentDepth: number): any {
574 | const elementInfo = getElementInfo(element);
575 |
576 | if (currentDepth < maxDepth || maxDepth === -1) {
577 | const children = Array.from(element.children);
578 | elementInfo.children = children.map(child =>
579 | traverseElement(child, currentDepth + 1)
580 | );
581 | } else if (element.children.length > 0) {
582 | elementInfo.childrenCount = element.children.length;
583 | }
584 |
585 | return elementInfo;
586 | }
587 |
588 | const rootElement = document.querySelector(selector);
589 | if (!rootElement) {
590 | throw new Error(`Element not found: ${selector}`);
591 | }
592 |
593 | return traverseElement(rootElement, 0);
594 | }, { selector, maxDepth, includeText, includeAttributes });
595 |
596 | this.log('Element hierarchy retrieved');
597 | return hierarchy;
598 | } catch (error: any) {
599 | console.error('Get element hierarchy error:', error);
600 | throw new BrowserError('Failed to get element hierarchy', 'Check if the selector exists');
601 | }
602 | }
603 |
604 | // Additional navigation methods
605 | async goForward(): Promise<void> {
606 | try {
607 | if (!this.isInitialized() || !this.state.page) {
608 | throw new Error('Browser not initialized');
609 | }
610 | this.log('Going forward');
611 | await this.state.page.goForward();
612 | this.log('Forward navigation complete');
613 | } catch (error: any) {
614 | console.error('Go forward error:', error);
615 | throw new BrowserError('Failed to go forward', 'Check if there is a next page in history');
616 | }
617 | }
618 |
619 | // Enhanced interaction methods
620 | async hover(selector: string): Promise<void> {
621 | try {
622 | if (!this.isInitialized() || !this.state.page) {
623 | throw new Error('Browser not initialized');
624 | }
625 | this.log('Hovering over element', { selector });
626 | const locator = this.state.page.locator(selector);
627 | await locator.hover();
628 | this.log('Hover complete');
629 | } catch (error: any) {
630 | console.error('Hover error:', error);
631 | throw new BrowserError('Failed to hover over element', 'Check if the selector exists and is visible');
632 | }
633 | }
634 |
635 | async dragAndDrop(sourceSelector: string, targetSelector: string): Promise<void> {
636 | try {
637 | if (!this.isInitialized() || !this.state.page) {
638 | throw new Error('Browser not initialized');
639 | }
640 | this.log('Performing drag and drop', { sourceSelector, targetSelector });
641 | const sourceLocator = this.state.page.locator(sourceSelector);
642 | const targetLocator = this.state.page.locator(targetSelector);
643 | await sourceLocator.dragTo(targetLocator);
644 | this.log('Drag and drop complete');
645 | } catch (error: any) {
646 | console.error('Drag and drop error:', error);
647 | throw new BrowserError('Failed to drag and drop', 'Check if both selectors exist and are interactable');
648 | }
649 | }
650 |
651 | async selectOption(selector: string, values: string[]): Promise<void> {
652 | try {
653 | if (!this.isInitialized() || !this.state.page) {
654 | throw new Error('Browser not initialized');
655 | }
656 | this.log('Selecting options', { selector, values });
657 | const locator = this.state.page.locator(selector);
658 | await locator.selectOption(values);
659 | this.log('Select option complete');
660 | } catch (error: any) {
661 | console.error('Select option error:', error);
662 | throw new BrowserError('Failed to select option', 'Check if the selector exists and values are valid');
663 | }
664 | }
665 |
666 | async pressKey(key: string): Promise<void> {
667 | try {
668 | if (!this.isInitialized() || !this.state.page) {
669 | throw new Error('Browser not initialized');
670 | }
671 | this.log('Pressing key', { key });
672 | await this.state.page.keyboard.press(key);
673 | this.log('Key press complete');
674 | } catch (error: any) {
675 | console.error('Press key error:', error);
676 | throw new BrowserError('Failed to press key', 'Check if the key name is valid');
677 | }
678 | }
679 |
680 | async waitForText(text: string, timeout: number = 30000): Promise<void> {
681 | try {
682 | if (!this.isInitialized() || !this.state.page) {
683 | throw new Error('Browser not initialized');
684 | }
685 | this.log('Waiting for text', { text, timeout });
686 | await this.state.page.waitForSelector(`text=${text}`, { timeout });
687 | this.log('Text found');
688 | } catch (error: any) {
689 | console.error('Wait for text error:', error);
690 | throw new BrowserError('Text not found within timeout', 'Check if the text appears on the page');
691 | }
692 | }
693 |
694 | async waitForSelector(selector: string, timeout: number = 30000): Promise<void> {
695 | try {
696 | if (!this.isInitialized() || !this.state.page) {
697 | throw new Error('Browser not initialized');
698 | }
699 | this.log('Waiting for selector', { selector, timeout });
700 | await this.state.page.waitForSelector(selector, { timeout });
701 | this.log('Selector found');
702 | } catch (error: any) {
703 | console.error('Wait for selector error:', error);
704 | throw new BrowserError('Selector not found within timeout', 'Check if the selector appears on the page');
705 | }
706 | }
707 |
708 | async resize(width: number, height: number): Promise<void> {
709 | try {
710 | if (!this.isInitialized() || !this.state.page) {
711 | throw new Error('Browser not initialized');
712 | }
713 | this.log('Resizing viewport', { width, height });
714 | await this.state.page.setViewportSize({ width, height });
715 | this.log('Resize complete');
716 | } catch (error: any) {
717 | console.error('Resize error:', error);
718 | throw new BrowserError('Failed to resize viewport', 'Check if width and height are positive numbers');
719 | }
720 | }
721 |
722 | // Dialog handling
723 | async handleDialog(accept: boolean, promptText?: string): Promise<void> {
724 | try {
725 | if (!this.isInitialized() || !this.state.page) {
726 | throw new Error('Browser not initialized');
727 | }
728 | this.log('Setting up dialog handler', { accept, promptText });
729 |
730 | this.state.page.once('dialog', async dialog => {
731 | this.log('Dialog detected', { type: dialog.type(), message: dialog.message() });
732 | if (accept) {
733 | await dialog.accept(promptText);
734 | } else {
735 | await dialog.dismiss();
736 | }
737 | this.log('Dialog handled');
738 | });
739 | } catch (error: any) {
740 | console.error('Handle dialog error:', error);
741 | throw new BrowserError('Failed to handle dialog', 'Check if there is a dialog to handle');
742 | }
743 | }
744 |
745 | // Console and network methods
746 | async getConsoleMessages(): Promise<string[]> {
747 | try {
748 | if (!this.isInitialized() || !this.state.page) {
749 | throw new Error('Browser not initialized');
750 | }
751 | this.log('Getting console messages');
752 |
753 | const messages: string[] = [];
754 |
755 | // Listen to console events
756 | this.state.page.on('console', msg => {
757 | messages.push(`[${msg.type().toUpperCase()}] ${msg.text()}`);
758 | });
759 |
760 | // Return collected messages
761 | this.log('Console messages retrieved');
762 | return messages;
763 | } catch (error: any) {
764 | console.error('Get console messages error:', error);
765 | throw new BrowserError('Failed to get console messages', 'Browser console monitoring error');
766 | }
767 | }
768 |
769 | async getNetworkRequests(): Promise<Array<{url: string, method: string, status?: number}>> {
770 | try {
771 | if (!this.isInitialized() || !this.state.page) {
772 | throw new Error('Browser not initialized');
773 | }
774 | this.log('Getting network requests');
775 |
776 | const requests: Array<{url: string, method: string, status?: number}> = [];
777 |
778 | // Listen to request events
779 | this.state.page.on('request', request => {
780 | requests.push({
781 | url: request.url(),
782 | method: request.method()
783 | });
784 | });
785 |
786 | this.state.page.on('response', response => {
787 | const request = requests.find(req => req.url === response.url());
788 | if (request) {
789 | request.status = response.status();
790 | }
791 | });
792 |
793 | this.log('Network requests retrieved');
794 | return requests;
795 | } catch (error: any) {
796 | console.error('Get network requests error:', error);
797 | throw new BrowserError('Failed to get network requests', 'Network monitoring error');
798 | }
799 | }
800 |
801 | async uploadFiles(selector: string, filePaths: string[]): Promise<void> {
802 | try {
803 | if (!this.isInitialized() || !this.state.page) {
804 | throw new Error('Browser not initialized');
805 | }
806 | this.log('Uploading files', { selector, filePaths });
807 | const locator = this.state.page.locator(selector);
808 | await locator.setInputFiles(filePaths);
809 | this.log('File upload complete');
810 | } catch (error: any) {
811 | console.error('File upload error:', error);
812 | throw new BrowserError('Failed to upload files', 'Check if selector is a file input and files exist');
813 | }
814 | }
815 |
816 | async evaluateWithReturn(script: string): Promise<any> {
817 | try {
818 | if (!this.isInitialized() || !this.state.page) {
819 | throw new Error('Browser not initialized');
820 | }
821 | this.log('Evaluating JavaScript with return', { script });
822 | const result = await this.state.page.evaluate(script);
823 | this.log('JavaScript evaluation complete');
824 | return result;
825 | } catch (error: any) {
826 | console.error('JavaScript evaluation error:', error);
827 | throw new BrowserError('Failed to evaluate JavaScript', 'Check if the script is valid JavaScript');
828 | }
829 | }
830 |
831 | // Enhanced screenshot functionality
832 | async takeScreenshot(path: string, options?: {fullPage?: boolean, element?: string}): Promise<void> {
833 | try {
834 | if (!this.isInitialized() || !this.state.page) {
835 | throw new Error('Browser not initialized');
836 | }
837 | this.log('Taking screenshot', { path, options });
838 |
839 | if (options?.element) {
840 | const locator = this.state.page.locator(options.element);
841 | await locator.screenshot({ path });
842 | } else {
843 | await this.state.page.screenshot({ path, fullPage: options?.fullPage });
844 | }
845 |
846 | this.log('Screenshot saved');
847 | } catch (error: any) {
848 | console.error('Screenshot error:', error);
849 | throw new BrowserError('Failed to take screenshot', 'Check if the path is writable');
850 | }
851 | }
852 |
853 | // Mouse coordinate methods
854 | async mouseMove(x: number, y: number): Promise<void> {
855 | try {
856 | if (!this.isInitialized() || !this.state.page) {
857 | throw new Error('Browser not initialized');
858 | }
859 | this.log('Moving mouse', { x, y });
860 | await this.state.page.mouse.move(x, y);
861 | this.currentMousePosition = { x, y };
862 | this.log('Mouse move complete');
863 | } catch (error: any) {
864 | console.error('Mouse move error:', error);
865 | throw new BrowserError('Failed to move mouse', 'Check if coordinates are valid');
866 | }
867 | }
868 |
869 | async mouseClick(x: number, y: number): Promise<void> {
870 | try {
871 | if (!this.isInitialized() || !this.state.page) {
872 | throw new Error('Browser not initialized');
873 | }
874 | this.log('Clicking at coordinates', { x, y });
875 | await this.state.page.mouse.click(x, y);
876 | this.currentMousePosition = { x, y };
877 | this.log('Mouse click complete');
878 | } catch (error: any) {
879 | console.error('Mouse click error:', error);
880 | throw new BrowserError('Failed to click at coordinates', 'Check if coordinates are valid');
881 | }
882 | }
883 |
884 | async mouseDrag(startX: number, startY: number, endX: number, endY: number): Promise<void> {
885 | try {
886 | if (!this.isInitialized() || !this.state.page) {
887 | throw new Error('Browser not initialized');
888 | }
889 | this.log('Mouse drag', { startX, startY, endX, endY });
890 | await this.state.page.mouse.move(startX, startY);
891 | await this.state.page.mouse.down();
892 | await this.state.page.mouse.move(endX, endY);
893 | await this.state.page.mouse.up();
894 | this.currentMousePosition = { x: endX, y: endY };
895 | this.log('Mouse drag complete');
896 | } catch (error: any) {
897 | console.error('Mouse drag error:', error);
898 | throw new BrowserError('Failed to drag mouse', 'Check if coordinates are valid');
899 | }
900 | }
901 |
902 | isInitialized(): boolean {
903 | return !!(this.state.browser?.isConnected() && this.state.context && this.state.page);
904 | }
905 | }
906 |
907 | export const playwrightController = new PlaywrightController();
```