# 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:
--------------------------------------------------------------------------------
```
node_modules/
dist/
*.log
.env
.DS_Store
Thumbs.db
*.tmp
*.temp
screenshots/
.vscode/settings.json
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
# Source files
src/
# Test files
test*
*.test.*
coverage/
# Development config
tsconfig.json
.gitignore
.git/
.vscode/
# Documentation
mcp-plan.md
# Temporary files
*.log
.DS_Store
# Scripts
*.bat
*.sh
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# PlayMCP Browser Automation Server
A comprehensive MCP (Model Context Protocol) server for browser automation using Playwright. This server provides **38 powerful tools** for web scraping, testing, and automation.
<a href="https://glama.ai/mcp/servers/@jomon003/PlayMCP">
<img width="380" height="200" src="https://glama.ai/mcp/servers/@jomon003/PlayMCP/badge" alt="PlayBrowser Automation Server MCP server" />
</a>
## Features
### 🚀 **Core Browser Automation** (21 tools)
- **Navigation**: `navigate`, `goForward`, `goBack` (via scroll)
- **Interaction**: `click`, `type`, `hover`, `dragAndDrop`, `selectOption`
- **Mouse Control**: `moveMouse`, `mouseMove`, `mouseClick`, `mouseDrag`
- **Keyboard**: `pressKey`
- **Waiting**: `waitForText`, `waitForSelector`
- **Screenshots**: `screenshot`, `takeScreenshot` (enhanced)
- **Page Info**: `getPageSource`, `getPageText`, `getPageTitle`, `getPageUrl`
- **Element Analysis**: `getElementContent`, `getElementHierarchy`
- **Scripts & Styles**: `getScripts`, `getStylesheets`, `getMetaTags`
### 🔍 **Advanced Data Extraction** (7 tools)
- **Links & Images**: `getLinks`, `getImages`
- **Forms**: `getForms`
- **Console Monitoring**: `getConsoleMessages`
- **Network Monitoring**: `getNetworkRequests`
- **JavaScript Execution**: `executeJavaScript`, `evaluateWithReturn`
### 📁 **File Operations** (2 tools)
- **File Upload**: `uploadFiles`
- **Dialog Handling**: `handleDialog`
### ⚙️ **Browser Management** (8 tools)
- **Browser Control**: `openBrowser`, `closeBrowser`
- **Viewport Management**: `resize`
- **Page Manipulation**: `scroll` (enhanced with feedback)
- **Element Hierarchy**: Deep DOM analysis with configurable depth
- **Enhanced Screenshots**: Full page, element-specific, custom paths
- **Mouse Coordinates**: Pixel-perfect mouse control
- **Wait Conditions**: Smart waiting for elements and text
## Quick Start
### Installation
```bash
# Clone the repository
git clone https://github.com/jomon003/PlayMCP.git
cd PlayMCP
# Install dependencies
npm install
# Build the project
npm run build
# Test the server
npm test
```
### Basic Usage
```javascript
// Start the server
node ./dist/server.js
// Send MCP commands via JSON-RPC
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
```
## Tool Categories
### 🎯 Navigation & Interaction
- **navigate**: Go to any URL
- **goForward**: Navigate forward in browser history
- **click**: Click elements with smart selector resolution
- **type**: Type text with realistic keyboard simulation
- **hover**: Hover over elements for tooltips and interactions
- **dragAndDrop**: Drag elements between locations
- **selectOption**: Choose options from dropdowns and multi-selects
- **pressKey**: Send specific keyboard keys (Enter, Escape, etc.)
### ⏱️ Smart Waiting
- **waitForText**: Wait for specific text to appear
- **waitForSelector**: Wait for elements to load
- Built-in timeouts and error handling
### 🖱️ Precise Mouse Control
- **mouseMove**: Move to exact coordinates
- **mouseClick**: Click at specific pixels
- **mouseDrag**: Drag between coordinate points
- **moveMouse**: Enhanced mouse positioning
### 📊 Data Extraction
- **getElementHierarchy**: Deep DOM structure analysis
- **getConsoleMessages**: Monitor browser console output
- **getNetworkRequests**: Track HTTP requests and responses
- **getLinks**: Extract all page links with metadata
- **getImages**: Get all images with attributes
- **getForms**: Analyze form structures and fields
### 🎬 Visual & Media
- **screenshot**: Basic screenshot capture
- **takeScreenshot**: Advanced screenshots (full page, elements, custom paths)
- **resize**: Control viewport dimensions
### 📁 File & Dialog Operations
- **uploadFiles**: Handle file input uploads
- **handleDialog**: Manage alerts, confirms, and prompts
### ⚙️ JavaScript Execution
- **executeJavaScript**: Run JavaScript code
- **evaluateWithReturn**: Execute JS with return values
### Core Browser Controls
- **openBrowser** - Launch a new browser instance with optional headless mode
- **navigate** - Navigate to any URL
- **click** - Click elements using CSS selectors
- **type** - Type text into input fields
- **moveMouse** - Move mouse to specific coordinates
- **scroll** - Scroll the page by specified amounts with enhanced feedback and smooth scrolling support
- **screenshot** - Take screenshots of the page, viewport, or specific elements
- **closeBrowser** - Close the browser instance
### Page Content Extraction
- **getPageSource** - Get the complete HTML source code
- **getPageText** - Get the text content (stripped of HTML)
- **getPageTitle** - Get the page title
- **getPageUrl** - Get the current URL
- **getScripts** - Extract all JavaScript code from the page
- **getStylesheets** - Extract all CSS stylesheets
- **getMetaTags** - Get all meta tags with their attributes
- **getLinks** - Get all links with href, text, and title
- **getImages** - Get all images with src, alt, and dimensions
- **getForms** - Get all forms with their fields and attributes
- **getElementContent** - Get HTML and text content of specific elements
- **getElementHierarchy** - Get the hierarchical DOM structure with parent-child relationships
### Advanced Capabilities
- **executeJavaScript** - Execute arbitrary JavaScript code on the page and return results
## Available Tools Reference
| Tool | Description | Required Parameters |
|------|-------------|-------------------|
| `openBrowser` | Launch browser instance | `headless?: boolean, debug?: boolean` |
| `navigate` | Navigate to URL | `url: string` |
| `click` | Click element | `selector: string` |
| `type` | Type text into element | `selector: string, text: string` |
| `moveMouse` | Move mouse to coordinates | `x: number, y: number` |
| `scroll` | Scroll page with feedback | `x: number, y: number, smooth?: boolean` |
| `screenshot` | Take screenshot | `path: string, type?: string, selector?: string` |
| `getPageSource` | Get HTML source | None |
| `getPageText` | Get text content | None |
| `getPageTitle` | Get page title | None |
| `getPageUrl` | Get current URL | None |
| `getScripts` | Get JavaScript code | None |
| `getStylesheets` | Get CSS stylesheets | None |
| `getMetaTags` | Get meta tags | None |
| `getLinks` | Get all links | None |
| `getImages` | Get all images | None |
| `getForms` | Get all forms | None |
| `getElementContent` | Get element content | `selector: string` |
| `getElementHierarchy` | Get DOM hierarchy | `selector?: string, maxDepth?: number, includeText?: boolean, includeAttributes?: boolean` |
| `executeJavaScript` | Run JavaScript | `script: string` |
| `closeBrowser` | Close browser | None |
## Installation
### Complete Installation Steps
1. **Prerequisites**
- Node.js 16+ (download from [nodejs.org](https://nodejs.org/))
- Git (for cloning the repository)
2. **Clone and Setup**
```bash
git clone <repository-url>
cd PlayMCP
npm install
npm run build
```
3. **Install Playwright Browsers**
```bash
npx playwright install
```
This downloads the necessary browser binaries (Chromium, Firefox, Safari).
4. **Verify Installation**
```bash
npm run start
```
You should see "Browser Automation MCP Server starting..." if everything is working.
### Quick Installation
```bash
git clone <repository-url>
cd PlayMCP
npm install && npm run build && npx playwright install
```
## Usage
### As MCP Server
Add to your MCP configuration file:
**Standard MCP Configuration:**
```json
{
"servers": {
"playmcp-browser": {
"type": "stdio",
"command": "node",
"args": ["./dist/server.js"],
"cwd": "/path/to/PlayMCP",
"description": "Browser automation server using Playwright"
}
}
}
```
**Alternative Configuration (works with VS Code GitHub Copilot):**
```json
{
"servers": {
"playmcp-browser": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/PlayMCP/dist/server.js"]
}
}
}
```
**For Windows users:**
```json
{
"servers": {
"playmcp-browser": {
"type": "stdio",
"command": "node",
"args": ["C:\\path\\to\\PlayMCP\\dist\\server.js"]
}
}
}
```
### VS Code GitHub Copilot Integration
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.
### Configuration Examples
**Claude Desktop (config.json location):**
- Windows: `%APPDATA%\Claude\config.json`
- macOS: `~/Library/Application Support/Claude/config.json`
- Linux: `~/.config/Claude/config.json`
**VS Code MCP Extension:**
Add to your VS Code settings.json or MCP configuration file.
**Example Full Configuration:**
```json
{
"mcpServers": {
"playmcp-browser": {
"type": "stdio",
"command": "node",
"args": ["/Users/username/PlayMCP/dist/server.js"],
"description": "Browser automation with Playwright"
}
}
}
```
### Tool Examples
**Basic Web Scraping:**
```javascript
// Open browser and navigate
await openBrowser({ headless: false, debug: true })
await navigate({ url: "https://example.com" })
// Extract content
const title = await getPageTitle()
const links = await getLinks()
const forms = await getForms()
```
**Form Automation:**
```javascript
// Fill out a form
await click({ selector: "#login-button" })
await type({ selector: "#username", text: "[email protected]" })
await type({ selector: "#password", text: "password123" })
await click({ selector: "#submit" })
```
**Page Interaction:**
```javascript
// Enhanced scrolling with feedback
await scroll({ x: 0, y: 500, smooth: false })
// Returns: { before: {x: 0, y: 0}, after: {x: 0, y: 500}, scrolled: {x: 0, y: 500} }
// Smooth scrolling
await scroll({ x: 0, y: 300, smooth: true })
// Mouse interaction
await moveMouse({ x: 100, y: 200 })
await click({ selector: ".dropdown-menu" })
```
**DOM Structure Analysis:**
```javascript
// Get page hierarchy (3 levels deep)
await getElementHierarchy({ maxDepth: 3 })
// Get detailed hierarchy with text and attributes
await getElementHierarchy({
selector: "#main-content",
maxDepth: -1,
includeText: true,
includeAttributes: true
})
// Get basic structure of a specific section
await getElementHierarchy({ selector: ".sidebar", maxDepth: 2 })
```
**Advanced JavaScript Execution:**
```javascript
// Run custom JavaScript
await executeJavaScript({
script: "document.querySelectorAll('h1').length"
})
// Modify page content
await executeJavaScript({
script: "document.body.style.backgroundColor = 'lightblue'"
})
// Extract complex data
await executeJavaScript({
script: `
Array.from(document.querySelectorAll('article')).map(article => ({
title: article.querySelector('h2')?.textContent,
summary: article.querySelector('p')?.textContent
}))
`
})
```
**Screenshot and Documentation:**
```javascript
// Take screenshots
await screenshot({ path: "./full-page.png", type: "page" })
await screenshot({ path: "./element.png", type: "element", selector: "#main-content" })
```
## Quick Start
1. **Install and setup:**
```bash
git clone <repo-url> && cd PlayMCP
npm install && npm run build && npx playwright install
```
2. **Add to your MCP client configuration**
3. **Start automating:**
```javascript
await openBrowser({ debug: true })
await navigate({ url: "https://news.ycombinator.com" })
const links = await getLinks()
console.log(`Found ${links.length} links`)
// Analyze page structure
const hierarchy = await getElementHierarchy({ maxDepth: 2 })
console.log('Page structure:', hierarchy)
```
## Development
- **src/server.ts** - Main MCP server implementation
- **src/controllers/playwright.ts** - Playwright browser controller
- **src/mcp/** - MCP protocol implementation
- **src/types/** - TypeScript type definitions
## Requirements
### System Requirements
- **Node.js 16+** (LTS version recommended)
- **Operating System:** Windows, macOS, or Linux
- **Memory:** At least 2GB RAM (4GB+ recommended for heavy usage)
- **Disk Space:** ~500MB for browser binaries and dependencies
### Dependencies
- **Playwright:** Handles browser automation (automatically installed)
- **TypeScript:** For compilation (dev dependency)
- **Browser Binaries:** Downloaded via `npx playwright install`
## Troubleshooting
### Common Issues
1. **"Browser not initialized" error**
- Make sure to call `openBrowser` before other browser operations
- Check if Node.js version is 16 or higher
2. **Playwright installation fails**
```bash
# Try manual browser installation
npx playwright install chromium
# Or install all browsers
npx playwright install
```
3. **Permission errors on Linux/macOS**
```bash
# Make sure the script is executable
chmod +x dist/server.js
```
4. **Path issues in MCP configuration**
- Use absolute paths in the configuration
- On Windows, use double backslashes: `C:\\path\\to\\PlayMCP\\dist\\server.js`
- Verify the path exists: `node /path/to/PlayMCP/dist/server.js`
5. **Browser crashes or timeouts**
- Try running with `headless: false` for debugging
- Increase system memory if running multiple browser instances
- Check if antivirus software is blocking browser processes
### Testing Your Installation
```bash
# Test the server directly
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node ./dist/server.js
```
You should see a JSON response listing all available tools.
## License
MIT License
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true,
"allowJs": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"test-*.js",
"test-*.ts"
]
}
```
--------------------------------------------------------------------------------
/src/mcp/types.ts:
--------------------------------------------------------------------------------
```typescript
export interface Tool {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required: string[];
};
}
export interface ServerConfig {
name: string;
version: string;
}
export interface ServerCapabilities {
capabilities: {
tools: Record<string, Tool>;
};
}
export interface CallToolRequest {
params: {
name: string;
arguments?: Record<string, any>;
};
}
export interface ToolResponse {
content: Array<{
type: string;
text: string;
}>;
isError?: boolean;
}
export interface ListToolsResponse {
tools: Tool[];
}
```
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Page, Browser, BrowserContext } from 'playwright';
export interface BrowserState {
browser: Browser | null;
context: BrowserContext | null;
page: Page | null;
debug: boolean;
}
export class BrowserError extends Error {
suggestion?: string;
constructor(message: string, suggestion?: string) {
super(message);
this.name = 'BrowserError';
this.suggestion = suggestion;
}
}
export interface ScreenshotOptions {
path: string;
type?: 'element' | 'page' | 'viewport';
selector?: string;
}
export interface ElementInfo {
tagName: string;
className: string;
id: string;
attributes: Array<{
name: string;
value: string;
}>;
innerText: string | null;
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "playmcp",
"version": "2.0.0",
"description": "Comprehensive MCP server for browser automation with 38 powerful Playwright tools",
"main": "dist/server.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "tsc && node dist/server.js"
},
"keywords": [
"browser",
"automation",
"playwright",
"mcp",
"browser-automation"
],
"files": [
"dist/**/*",
"README.md",
"package.json"
],
"engines": {
"node": ">=16.0.0"
},
"author": "",
"license": "MIT",
"dependencies": {
"playwright": "^1.40.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
```
--------------------------------------------------------------------------------
/src/mcp/server.ts:
--------------------------------------------------------------------------------
```typescript
import { Tool, ServerConfig, ServerCapabilities, CallToolRequest, ToolResponse, ListToolsResponse } from './types.js';
export class Server {
private config: ServerConfig;
private capabilities: ServerCapabilities;
private listToolsHandler?: () => Promise<ListToolsResponse>;
private callToolHandler?: (request: CallToolRequest) => Promise<ToolResponse>;
constructor(config: ServerConfig, capabilities: ServerCapabilities) {
this.config = config;
this.capabilities = capabilities;
}
setRequestHandler<T extends 'listTools' | 'callTool'>(
type: T,
handler: T extends 'listTools'
? () => Promise<ListToolsResponse>
: (request: CallToolRequest) => Promise<ToolResponse>
) {
if (type === 'listTools') {
this.listToolsHandler = handler as () => Promise<ListToolsResponse>;
} else {
this.callToolHandler = handler as (request: CallToolRequest) => Promise<ToolResponse>;
}
} private async handleInput(input: string) {
try {
const message = JSON.parse(input);
// Handle MCP initialize request
if (message.method === 'initialize') {
console.log(JSON.stringify({
jsonrpc: "2.0",
id: message.id, result: {
protocolVersion: "2024-11-05",
capabilities: this.capabilities.capabilities,
serverInfo: {
name: this.config.name,
version: this.config.version
}
}
}));
return;
}
// Handle MCP notifications/initialized
if (message.method === 'notifications/initialized') {
// Acknowledge initialization complete
return;
}
// Handle MCP tools/list request
if (message.method === 'tools/list') {
if (!this.listToolsHandler) {
throw new Error('No list tools handler registered');
}
const response = await this.listToolsHandler();
console.log(JSON.stringify({
jsonrpc: "2.0",
id: message.id,
result: response
}));
return;
}
// Handle MCP tools/call request
if (message.method === 'tools/call') {
if (!this.callToolHandler) {
throw new Error('No call tool handler registered');
}
const response = await this.callToolHandler({
params: {
name: message.params.name,
arguments: message.params.arguments
}
});
console.log(JSON.stringify({
jsonrpc: "2.0",
id: message.id,
result: response
}));
return;
}
// Legacy command handling (keep for backward compatibility)
if (message.command) {
if (!this.callToolHandler) {
throw new Error('No call tool handler registered');
}
const response = await this.callToolHandler({
params: {
name: message.command,
arguments: message.arguments
}
});
console.log(JSON.stringify({
type: "response",
result: {
success: !response.isError,
...(response.isError ? {
error: {
message: response.content[0].text,
suggestion: response.content[1]?.text
}
} : {
message: response.content[0].text
})
}
}));
}
} catch (error) {
console.log(JSON.stringify({
jsonrpc: "2.0",
id: input.includes('"id"') ? JSON.parse(input).id : null,
error: {
code: -32603,
message: error instanceof Error ? error.message : 'Unknown error',
data: {
suggestion: 'Check input format'
}
}
}));
}
} async connect() {
// Set up stdin handling - wait for initialize request
process.stdin.setEncoding('utf8');
let buffer = '';
process.stdin.on('data', (chunk: string) => {
buffer += chunk;
if (buffer.includes('\n')) {
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
this.handleInput(line);
}
}
}
});
return new Promise((resolve) => {
process.on('SIGINT', () => {
resolve(undefined);
});
});
}
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from './mcp/server.js';
import { Tool } from './mcp/types.js';
import { playwrightController } from './controllers/playwright.js';
const OPEN_BROWSER_TOOL: Tool = {
name: "openBrowser",
description: "Launch a new browser instance",
inputSchema: {
type: "object",
properties: {
headless: { type: "boolean" },
debug: { type: "boolean" }
},
required: []
}
};
const NAVIGATE_TOOL: Tool = {
name: "navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" }
},
required: ["url"]
}
};
const TYPE_TOOL: Tool = {
name: "type",
description: "Type text into an element",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" },
text: { type: "string" }
},
required: ["selector", "text"]
}
};
const CLICK_TOOL: Tool = {
name: "click",
description: "Click an element",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" }
},
required: ["selector"]
}
};
const MOVE_MOUSE_TOOL: Tool = {
name: "moveMouse",
description: "Move mouse to coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
};
const SCREENSHOT_TOOL: Tool = {
name: "screenshot",
description: "Take a screenshot",
inputSchema: {
type: "object",
properties: {
path: { type: "string" },
type: { type: "string", enum: ["viewport", "element", "page"] },
selector: { type: "string" }
},
required: ["path"]
}
};
const GET_PAGE_SOURCE_TOOL: Tool = {
name: "getPageSource",
description: "Get the HTML source code of the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_PAGE_TEXT_TOOL: Tool = {
name: "getPageText",
description: "Get the text content of the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_PAGE_TITLE_TOOL: Tool = {
name: "getPageTitle",
description: "Get the title of the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_PAGE_URL_TOOL: Tool = {
name: "getPageUrl",
description: "Get the URL of the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_SCRIPTS_TOOL: Tool = {
name: "getScripts",
description: "Get all JavaScript code from the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_STYLESHEETS_TOOL: Tool = {
name: "getStylesheets",
description: "Get all CSS stylesheets from the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_META_TAGS_TOOL: Tool = {
name: "getMetaTags",
description: "Get all meta tags from the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_LINKS_TOOL: Tool = {
name: "getLinks",
description: "Get all links from the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_IMAGES_TOOL: Tool = {
name: "getImages",
description: "Get all images from the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_FORMS_TOOL: Tool = {
name: "getForms",
description: "Get all forms from the current page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_ELEMENT_CONTENT_TOOL: Tool = {
name: "getElementContent",
description: "Get the HTML and text content of a specific element",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" }
},
required: ["selector"]
}
};
const EXECUTE_JAVASCRIPT_TOOL: Tool = {
name: "executeJavaScript",
description: "Execute arbitrary JavaScript code on the current page and return the result",
inputSchema: {
type: "object",
properties: {
script: {
type: "string",
description: "The JavaScript code to execute on the page. Can be expressions or statements."
}
},
required: ["script"]
}
};
const SCROLL_TOOL: Tool = {
name: "scroll",
description: "Scroll the page by specified amounts with enhanced feedback",
inputSchema: {
type: "object",
properties: {
x: {
type: "number",
description: "Horizontal scroll amount in pixels (positive = right, negative = left)"
},
y: {
type: "number",
description: "Vertical scroll amount in pixels (positive = down, negative = up)"
},
smooth: {
type: "boolean",
description: "Whether to use smooth scrolling animation (default: false)"
}
},
required: ["x", "y"]
}
};
const GET_ELEMENT_HIERARCHY_TOOL: Tool = {
name: "getElementHierarchy",
description: "Get the hierarchical structure of page elements with parent-child relationships",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for root element (default: 'body')"
},
maxDepth: {
type: "number",
description: "Maximum depth to traverse (-1 for unlimited, default: 3)"
},
includeText: {
type: "boolean",
description: "Include text content of elements (default: false)"
},
includeAttributes: {
type: "boolean",
description: "Include element attributes (default: false)"
}
},
required: []
}
};
const GO_FORWARD_TOOL: Tool = {
name: "goForward",
description: "Navigate forward to the next page in history",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const HOVER_TOOL: Tool = {
name: "hover",
description: "Hover over an element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" }
},
required: ["selector"]
}
};
const DRAG_AND_DROP_TOOL: Tool = {
name: "dragAndDrop",
description: "Drag and drop from one element to another",
inputSchema: {
type: "object",
properties: {
sourceSelector: { type: "string" },
targetSelector: { type: "string" }
},
required: ["sourceSelector", "targetSelector"]
}
};
const SELECT_OPTION_TOOL: Tool = {
name: "selectOption",
description: "Select option(s) in a dropdown or select element",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" },
values: {
type: "array",
items: { type: "string" },
description: "Array of values to select"
}
},
required: ["selector", "values"]
}
};
const PRESS_KEY_TOOL: Tool = {
name: "pressKey",
description: "Press a key on the keyboard",
inputSchema: {
type: "object",
properties: {
key: {
type: "string",
description: "Key to press (e.g., 'Enter', 'Escape', 'ArrowDown', etc.)"
}
},
required: ["key"]
}
};
const WAIT_FOR_TEXT_TOOL: Tool = {
name: "waitForText",
description: "Wait for specific text to appear on the page",
inputSchema: {
type: "object",
properties: {
text: { type: "string" },
timeout: {
type: "number",
description: "Timeout in milliseconds (default: 30000)"
}
},
required: ["text"]
}
};
const WAIT_FOR_SELECTOR_TOOL: Tool = {
name: "waitForSelector",
description: "Wait for a specific selector to appear on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" },
timeout: {
type: "number",
description: "Timeout in milliseconds (default: 30000)"
}
},
required: ["selector"]
}
};
const RESIZE_TOOL: Tool = {
name: "resize",
description: "Resize the browser viewport",
inputSchema: {
type: "object",
properties: {
width: { type: "number" },
height: { type: "number" }
},
required: ["width", "height"]
}
};
const HANDLE_DIALOG_TOOL: Tool = {
name: "handleDialog",
description: "Handle browser dialogs (alerts, confirms, prompts)",
inputSchema: {
type: "object",
properties: {
accept: {
type: "boolean",
description: "Whether to accept (true) or dismiss (false) the dialog"
},
promptText: {
type: "string",
description: "Text to enter in prompt dialogs (optional)"
}
},
required: ["accept"]
}
};
const GET_CONSOLE_MESSAGES_TOOL: Tool = {
name: "getConsoleMessages",
description: "Get console messages from the browser",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const GET_NETWORK_REQUESTS_TOOL: Tool = {
name: "getNetworkRequests",
description: "Get network requests made by the page",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const UPLOAD_FILES_TOOL: Tool = {
name: "uploadFiles",
description: "Upload files through a file input element",
inputSchema: {
type: "object",
properties: {
selector: { type: "string" },
filePaths: {
type: "array",
items: { type: "string" },
description: "Array of absolute file paths to upload"
}
},
required: ["selector", "filePaths"]
}
};
const EVALUATE_WITH_RETURN_TOOL: Tool = {
name: "evaluateWithReturn",
description: "Execute JavaScript code and return the result",
inputSchema: {
type: "object",
properties: {
script: {
type: "string",
description: "JavaScript code to execute"
}
},
required: ["script"]
}
};
const TAKE_SCREENSHOT_TOOL: Tool = {
name: "takeScreenshot",
description: "Take a screenshot of the page or specific element",
inputSchema: {
type: "object",
properties: {
path: { type: "string" },
fullPage: {
type: "boolean",
description: "Whether to capture the full scrollable page"
},
element: {
type: "string",
description: "CSS selector for element screenshot"
}
},
required: ["path"]
}
};
const MOUSE_MOVE_TOOL: Tool = {
name: "mouseMove",
description: "Move mouse to specific coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
};
const MOUSE_CLICK_TOOL: Tool = {
name: "mouseClick",
description: "Click at specific coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
};
const MOUSE_DRAG_TOOL: Tool = {
name: "mouseDrag",
description: "Drag from one coordinate to another",
inputSchema: {
type: "object",
properties: {
startX: { type: "number" },
startY: { type: "number" },
endX: { type: "number" },
endY: { type: "number" }
},
required: ["startX", "startY", "endX", "endY"]
}
};
const CLOSE_BROWSER_TOOL: Tool = {
name: "closeBrowser",
description: "Close the browser",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const tools = {
openBrowser: OPEN_BROWSER_TOOL,
navigate: NAVIGATE_TOOL,
type: TYPE_TOOL,
click: CLICK_TOOL,
moveMouse: MOVE_MOUSE_TOOL,
scroll: SCROLL_TOOL,
screenshot: SCREENSHOT_TOOL,
getPageSource: GET_PAGE_SOURCE_TOOL,
getPageText: GET_PAGE_TEXT_TOOL,
getPageTitle: GET_PAGE_TITLE_TOOL,
getPageUrl: GET_PAGE_URL_TOOL,
getScripts: GET_SCRIPTS_TOOL,
getStylesheets: GET_STYLESHEETS_TOOL,
getMetaTags: GET_META_TAGS_TOOL,
getLinks: GET_LINKS_TOOL,
getImages: GET_IMAGES_TOOL,
getForms: GET_FORMS_TOOL,
getElementContent: GET_ELEMENT_CONTENT_TOOL,
getElementHierarchy: GET_ELEMENT_HIERARCHY_TOOL,
executeJavaScript: EXECUTE_JAVASCRIPT_TOOL,
goForward: GO_FORWARD_TOOL,
hover: HOVER_TOOL,
dragAndDrop: DRAG_AND_DROP_TOOL,
selectOption: SELECT_OPTION_TOOL,
pressKey: PRESS_KEY_TOOL,
waitForText: WAIT_FOR_TEXT_TOOL,
waitForSelector: WAIT_FOR_SELECTOR_TOOL,
resize: RESIZE_TOOL,
handleDialog: HANDLE_DIALOG_TOOL,
getConsoleMessages: GET_CONSOLE_MESSAGES_TOOL,
getNetworkRequests: GET_NETWORK_REQUESTS_TOOL,
uploadFiles: UPLOAD_FILES_TOOL,
evaluateWithReturn: EVALUATE_WITH_RETURN_TOOL,
takeScreenshot: TAKE_SCREENSHOT_TOOL,
mouseMove: MOUSE_MOVE_TOOL,
mouseClick: MOUSE_CLICK_TOOL,
mouseDrag: MOUSE_DRAG_TOOL,
closeBrowser: CLOSE_BROWSER_TOOL
};
const server = new Server(
{
name: "playmcp-browser",
version: "1.0.0",
},
{
capabilities: {
tools,
},
}
);
server.setRequestHandler('listTools', async () => ({
tools: Object.values(tools)
}));
server.setRequestHandler('callTool', async (request) => {
const { name, arguments: args = {} } = request.params;
try {
switch (name) {
case 'openBrowser': {
await playwrightController.openBrowser(
args.headless as boolean,
args.debug as boolean
);
return {
content: [{ type: "text", text: "Browser opened successfully" }]
};
}
case 'navigate': {
if (!args.url || typeof args.url !== 'string') {
return {
content: [{ type: "text", text: "URL is required" }],
isError: true
};
}
await playwrightController.navigate(args.url);
return {
content: [{ type: "text", text: "Navigation successful" }]
};
}
case 'type': {
if (!args.selector || !args.text) {
return {
content: [{ type: "text", text: "Selector and text are required" }],
isError: true
};
}
await playwrightController.type(args.selector as string, args.text as string);
return {
content: [{ type: "text", text: "Text entered successfully" }]
};
}
case 'click': {
if (!args.selector) {
return {
content: [{ type: "text", text: "Selector is required" }],
isError: true
};
}
await playwrightController.click(args.selector as string);
return {
content: [{ type: "text", text: "Click successful" }]
};
}
case 'moveMouse': {
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
return {
content: [{ type: "text", text: "X and Y coordinates are required" }],
isError: true
};
}
await playwrightController.moveMouse(args.x, args.y);
return {
content: [{ type: "text", text: "Mouse moved successfully" }]
};
}
case 'scroll': {
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
return {
content: [{ type: "text", text: "X and Y scroll amounts are required" }],
isError: true
};
}
const scrollResult = await playwrightController.scroll(
args.x,
args.y,
args.smooth as boolean || false
);
return {
content: [{
type: "text",
text: JSON.stringify({
message: "Page scrolled successfully",
before: scrollResult.before,
after: scrollResult.after,
scrolled: {
x: scrollResult.after.x - scrollResult.before.x,
y: scrollResult.after.y - scrollResult.before.y
}
}, null, 2)
}]
};
}
case 'screenshot': {
if (!args.path) {
return {
content: [{ type: "text", text: "Path is required" }],
isError: true
};
}
await playwrightController.screenshot(args as any);
return {
content: [{ type: "text", text: "Screenshot taken successfully" }]
};
}
case 'getPageSource': {
const source = await playwrightController.getPageSource();
return {
content: [{ type: "text", text: source }]
};
}
case 'getPageText': {
const text = await playwrightController.getPageText();
return {
content: [{ type: "text", text }]
};
}
case 'getPageTitle': {
const title = await playwrightController.getPageTitle();
return {
content: [{ type: "text", text: title }]
};
}
case 'getPageUrl': {
const url = await playwrightController.getPageUrl();
return {
content: [{ type: "text", text: url }]
};
}
case 'getScripts': {
const scripts = await playwrightController.getScripts();
return {
content: [{ type: "text", text: scripts.join('\n') }]
};
}
case 'getStylesheets': {
const stylesheets = await playwrightController.getStylesheets();
return {
content: [{ type: "text", text: stylesheets.join('\n') }]
};
} case 'getMetaTags': {
const metaTags = await playwrightController.getMetaTags();
return {
content: [{ type: "text", text: JSON.stringify(metaTags, null, 2) }]
};
}
case 'getLinks': {
const links = await playwrightController.getLinks();
return {
content: [{ type: "text", text: JSON.stringify(links, null, 2) }]
};
}
case 'getImages': {
const images = await playwrightController.getImages();
return {
content: [{ type: "text", text: JSON.stringify(images, null, 2) }]
};
}
case 'getForms': {
const forms = await playwrightController.getForms();
return {
content: [{ type: "text", text: JSON.stringify(forms, null, 2) }]
};
}
case 'getElementContent': {
if (!args.selector) {
return {
content: [{ type: "text", text: "Selector is required" }],
isError: true
};
}
const content = await playwrightController.getElementContent(args.selector as string);
return {
content: [{ type: "text", text: JSON.stringify(content, null, 2) }]
};
}
case 'getElementHierarchy': {
const hierarchy = await playwrightController.getElementHierarchy(
args.selector as string || 'body',
args.maxDepth as number || 3,
args.includeText as boolean || false,
args.includeAttributes as boolean || false
);
return {
content: [{ type: "text", text: JSON.stringify(hierarchy, null, 2) }]
};
}
case 'executeJavaScript': {
if (!args.script || typeof args.script !== 'string') {
return {
content: [{ type: "text", text: "JavaScript script is required" }],
isError: true
};
}
const result = await playwrightController.executeJavaScript(args.script);
return {
content: [{
type: "text",
text: result !== undefined ? JSON.stringify(result, null, 2) : "Script executed successfully (no return value)"
}]
};
}
case 'goForward': {
await playwrightController.goForward();
return {
content: [{ type: "text", text: "Navigated forward successfully" }]
};
}
case 'hover': {
if (!args.selector) {
return {
content: [{ type: "text", text: "Selector is required" }],
isError: true
};
}
await playwrightController.hover(args.selector as string);
return {
content: [{ type: "text", text: "Hover completed successfully" }]
};
}
case 'dragAndDrop': {
if (!args.sourceSelector || !args.targetSelector) {
return {
content: [{ type: "text", text: "Source and target selectors are required" }],
isError: true
};
}
await playwrightController.dragAndDrop(args.sourceSelector as string, args.targetSelector as string);
return {
content: [{ type: "text", text: "Drag and drop completed successfully" }]
};
}
case 'selectOption': {
if (!args.selector || !args.values) {
return {
content: [{ type: "text", text: "Selector and values are required" }],
isError: true
};
}
await playwrightController.selectOption(args.selector as string, args.values as string[]);
return {
content: [{ type: "text", text: "Option selected successfully" }]
};
}
case 'pressKey': {
if (!args.key) {
return {
content: [{ type: "text", text: "Key is required" }],
isError: true
};
}
await playwrightController.pressKey(args.key as string);
return {
content: [{ type: "text", text: "Key pressed successfully" }]
};
}
case 'waitForText': {
if (!args.text) {
return {
content: [{ type: "text", text: "Text is required" }],
isError: true
};
}
await playwrightController.waitForText(args.text as string, args.timeout as number);
return {
content: [{ type: "text", text: "Text found successfully" }]
};
}
case 'waitForSelector': {
if (!args.selector) {
return {
content: [{ type: "text", text: "Selector is required" }],
isError: true
};
}
await playwrightController.waitForSelector(args.selector as string, args.timeout as number);
return {
content: [{ type: "text", text: "Selector found successfully" }]
};
}
case 'resize': {
if (typeof args.width !== 'number' || typeof args.height !== 'number') {
return {
content: [{ type: "text", text: "Width and height are required" }],
isError: true
};
}
await playwrightController.resize(args.width, args.height);
return {
content: [{ type: "text", text: "Browser resized successfully" }]
};
}
case 'handleDialog': {
if (typeof args.accept !== 'boolean') {
return {
content: [{ type: "text", text: "Accept parameter is required" }],
isError: true
};
}
await playwrightController.handleDialog(args.accept, args.promptText as string);
return {
content: [{ type: "text", text: "Dialog handler set successfully" }]
};
}
case 'getConsoleMessages': {
const messages = await playwrightController.getConsoleMessages();
return {
content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
};
}
case 'getNetworkRequests': {
const requests = await playwrightController.getNetworkRequests();
return {
content: [{ type: "text", text: JSON.stringify(requests, null, 2) }]
};
}
case 'uploadFiles': {
if (!args.selector || !args.filePaths) {
return {
content: [{ type: "text", text: "Selector and file paths are required" }],
isError: true
};
}
await playwrightController.uploadFiles(args.selector as string, args.filePaths as string[]);
return {
content: [{ type: "text", text: "Files uploaded successfully" }]
};
}
case 'evaluateWithReturn': {
if (!args.script || typeof args.script !== 'string') {
return {
content: [{ type: "text", text: "JavaScript script is required" }],
isError: true
};
}
const result = await playwrightController.evaluateWithReturn(args.script);
return {
content: [{
type: "text",
text: result !== undefined ? JSON.stringify(result, null, 2) : "null"
}]
};
}
case 'takeScreenshot': {
if (!args.path) {
return {
content: [{ type: "text", text: "Path is required" }],
isError: true
};
}
await playwrightController.takeScreenshot(args.path as string, {
fullPage: args.fullPage as boolean,
element: args.element as string
});
return {
content: [{ type: "text", text: "Screenshot taken successfully" }]
};
}
case 'mouseMove': {
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
return {
content: [{ type: "text", text: "X and Y coordinates are required" }],
isError: true
};
}
await playwrightController.mouseMove(args.x, args.y);
return {
content: [{ type: "text", text: "Mouse moved successfully" }]
};
}
case 'mouseClick': {
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
return {
content: [{ type: "text", text: "X and Y coordinates are required" }],
isError: true
};
}
await playwrightController.mouseClick(args.x, args.y);
return {
content: [{ type: "text", text: "Mouse clicked successfully" }]
};
}
case 'mouseDrag': {
if (typeof args.startX !== 'number' || typeof args.startY !== 'number' ||
typeof args.endX !== 'number' || typeof args.endY !== 'number') {
return {
content: [{ type: "text", text: "Start and end coordinates are required" }],
isError: true
};
}
await playwrightController.mouseDrag(args.startX, args.startY, args.endX, args.endY);
return {
content: [{ type: "text", text: "Mouse drag completed successfully" }]
};
}
case 'closeBrowser': {
await playwrightController.closeBrowser();
return {
content: [{ type: "text", text: "Browser closed successfully" }]
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true
};
}
} catch (error: any) {
return {
content: [{
type: "text",
text: `Error: ${error.message}${error.suggestion ? `\nSuggestion: ${error.suggestion}` : ''}`
}],
isError: true
};
}
});
async function runServer() {
console.error("Browser Automation MCP Server starting...");
await server.connect();
}
// Handle process exit
process.on('SIGINT', async () => {
try {
await playwrightController.closeBrowser();
} catch (error) {
// Ignore errors during cleanup
}
process.exit(0);
});
runServer().catch((error) => {
console.error("Fatal error running server:", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/controllers/playwright.ts:
--------------------------------------------------------------------------------
```typescript
import { chromium } from 'playwright';
import { BrowserError, BrowserState, ScreenshotOptions, ElementInfo } from '../types/index.js';
class PlaywrightController {
private state: BrowserState = {
browser: null,
context: null,
page: null,
debug: false
};
private currentMousePosition = { x: 0, y: 0 };
private log(...args: any[]) {
if (this.state.debug) {
console.log(JSON.stringify({
type: "debug",
message: args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ')
}));
}
}
async openBrowser(headless: boolean = false, debug: boolean = false): Promise<void> {
try {
this.state.debug = debug;
this.log('Attempting to launch browser');
if (this.state.browser?.isConnected()) {
this.log('Browser already running');
return;
}
this.log('Launching new browser instance', { headless });
this.state.browser = await chromium.launch({
headless,
args: ['--no-sandbox']
});
this.log('Creating browser context');
this.state.context = await this.state.browser.newContext({
viewport: { width: 1280, height: 720 }
});
this.log('Creating new page');
this.state.page = await this.state.context.newPage();
this.log('Browser successfully launched');
} catch (error: any) {
console.error('Browser launch error:', error);
throw new BrowserError(
'Failed to launch browser',
`Technical details: ${error?.message || 'Unknown error'}`
);
}
}
async closeBrowser(): Promise<void> {
try {
this.log('Closing browser');
await this.state.page?.close();
await this.state.context?.close();
await this.state.browser?.close();
this.state = { browser: null, context: null, page: null, debug: false };
this.log('Browser closed');
} catch (error: any) {
console.error('Browser close error:', error);
throw new BrowserError('Failed to close browser', 'The browser might have already been closed');
}
}
async navigate(url: string): Promise<void> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Navigating to', url);
await this.state.page?.goto(url);
this.log('Navigation complete');
} catch (error: any) {
console.error('Navigation error:', error);
throw new BrowserError('Failed to navigate', 'Check if the URL is valid and accessible');
}
}
async goBack(): Promise<void> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Going back');
await this.state.page?.goBack();
this.log('Navigation back complete');
} catch (error: any) {
console.error('Go back error:', error);
throw new BrowserError('Failed to go back', 'Check if there is a previous page in history');
}
}
async refresh(): Promise<void> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Refreshing page');
await this.state.page?.reload();
this.log('Page refresh complete');
} catch (error: any) {
console.error('Refresh error:', error);
throw new BrowserError('Failed to refresh page', 'Check if the page is still accessible');
}
}
async click(selector?: string): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
if (selector) {
this.log('Clicking element', selector);
await this.state.page.click(selector);
} else {
this.log('Clicking at position', this.currentMousePosition);
await this.state.page.mouse.click(this.currentMousePosition.x, this.currentMousePosition.y);
}
this.log('Click complete');
} catch (error: any) {
console.error('Click error:', error);
throw new BrowserError(
'Failed to click',
selector ? 'Check if the element exists and is visible' : 'Check if mouse position is valid'
);
}
}
async type(selector: string, text: string): Promise<void> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Typing into element', { selector, text });
await this.state.page?.type(selector, text);
this.log('Type complete');
} catch (error: any) {
console.error('Type error:', error);
throw new BrowserError('Failed to type text', 'Check if the input element exists and is editable');
}
}
async moveMouse(x: number, y: number): Promise<void> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Moving mouse to', { x, y });
await this.state.page?.mouse.move(x, y);
this.currentMousePosition = { x, y };
this.log('Mouse move complete');
} catch (error: any) {
console.error('Mouse move error:', error);
throw new BrowserError('Failed to move mouse', 'Check if coordinates are within viewport');
}
}
async scroll(x: number, y: number, smooth: boolean = false): Promise<{before: {x: number, y: number}, after: {x: number, y: number}}> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Scrolling', { x, y, smooth });
// Get scroll position before scrolling
const beforeScroll = await this.state.page.evaluate(() => ({
x: window.scrollX,
y: window.scrollY
}));
// Perform scroll with optional smooth behavior
await this.state.page.evaluate((args: {x: number, y: number, smooth: boolean}) => {
window.scrollBy({
left: args.x,
top: args.y,
behavior: args.smooth ? 'smooth' : 'auto'
});
}, { x, y, smooth });
// Wait for scroll to complete
await this.state.page.waitForTimeout(smooth ? 500 : 100);
// Get scroll position after scrolling
const afterScroll = await this.state.page.evaluate(() => ({
x: window.scrollX,
y: window.scrollY
}));
this.log('Scroll complete', { before: beforeScroll, after: afterScroll });
return {
before: beforeScroll,
after: afterScroll
};
} catch (error: any) {
console.error('Scroll error:', error);
throw new BrowserError('Failed to scroll', 'Check if scroll values are valid');
}
}
async screenshot(options: ScreenshotOptions): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Taking screenshot', options);
if (options.type === 'element' && options.selector) {
const element = await this.state.page.$(options.selector);
if (!element) {
throw new Error('Element not found');
}
await element.screenshot({ path: options.path });
} else if (options.type === 'viewport') {
await this.state.page.screenshot({ path: options.path });
} else {
await this.state.page.screenshot({ path: options.path, fullPage: true });
}
this.log('Screenshot saved to', options.path);
} catch (error: any) {
console.error('Screenshot error:', error);
throw new BrowserError(
'Failed to take screenshot',
'Check if the path is writable and element exists (if capturing element)'
);
}
}
async inspectElement(selector: string): Promise<ElementInfo> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Inspecting element', selector);
const info = await this.state.page.$eval(selector, (el: Element) => ({
tagName: el.tagName,
className: el.className,
id: el.id,
attributes: Array.from(el.attributes).map(attr => ({
name: attr.name,
value: attr.value
})),
innerText: el.textContent
}));
this.log('Element inspection complete');
return info;
} catch (error: any) {
console.error('Inspect element error:', error);
throw new BrowserError('Failed to inspect element', 'Check if the element exists');
}
}
async getPageSource(): Promise<string> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page source');
const content = await this.state.page?.content();
this.log('Page source retrieved');
return content || '';
} catch (error: any) {
console.error('Get page source error:', error);
throw new BrowserError('Failed to get page source', 'Check if the page is loaded');
}
}
async getPageText(): Promise<string> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page text content');
const text = await this.state.page?.innerText('body');
this.log('Page text retrieved');
return text || '';
} catch (error: any) {
console.error('Get page text error:', error);
throw new BrowserError('Failed to get page text', 'Check if the page is loaded');
}
}
async getPageTitle(): Promise<string> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page title');
const title = await this.state.page?.title();
this.log('Page title retrieved:', title);
return title || '';
} catch (error: any) {
console.error('Get page title error:', error);
throw new BrowserError('Failed to get page title', 'Check if the page is loaded');
}
}
async getPageUrl(): Promise<string> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page URL');
const url = this.state.page?.url();
this.log('Page URL retrieved:', url);
return url || '';
} catch (error: any) {
console.error('Get page URL error:', error);
throw new BrowserError('Failed to get page URL', 'Check if the page is loaded');
}
}
async getScripts(): Promise<string[]> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page scripts');
const scripts = await this.state.page?.evaluate(() => {
const scriptElements = Array.from(document.querySelectorAll('script'));
return scriptElements.map(script => {
if (script.src) {
return `// External script: ${script.src}`;
}
return script.textContent || script.innerHTML;
}).filter(content => content.trim().length > 0);
});
this.log('Scripts retrieved:', scripts?.length);
return scripts || [];
} catch (error: any) {
console.error('Get scripts error:', error);
throw new BrowserError('Failed to get scripts', 'Check if the page is loaded');
}
}
async getStylesheets(): Promise<string[]> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page stylesheets');
const stylesheets = await this.state.page?.evaluate(() => {
const styleElements = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'));
return styleElements.map(element => {
if (element.tagName === 'LINK') {
const link = element as HTMLLinkElement;
return `/* External stylesheet: ${link.href} */`;
}
return element.textContent || element.innerHTML;
}).filter(content => content.trim().length > 0);
});
this.log('Stylesheets retrieved:', stylesheets?.length);
return stylesheets || [];
} catch (error: any) {
console.error('Get stylesheets error:', error);
throw new BrowserError('Failed to get stylesheets', 'Check if the page is loaded');
}
}
async getMetaTags(): Promise<Array<{name?: string, property?: string, content?: string, httpEquiv?: string}>> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting meta tags');
const metaTags = await this.state.page?.evaluate(() => {
const metaElements = Array.from(document.querySelectorAll('meta'));
return metaElements.map(meta => ({
name: meta.getAttribute('name') || undefined,
property: meta.getAttribute('property') || undefined,
content: meta.getAttribute('content') || undefined,
httpEquiv: meta.getAttribute('http-equiv') || undefined
}));
});
this.log('Meta tags retrieved:', metaTags?.length);
return metaTags || [];
} catch (error: any) {
console.error('Get meta tags error:', error);
throw new BrowserError('Failed to get meta tags', 'Check if the page is loaded');
}
}
async getLinks(): Promise<Array<{href: string, text: string, title?: string}>> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page links');
const links = await this.state.page?.evaluate(() => {
const linkElements = Array.from(document.querySelectorAll('a[href]'));
return linkElements.map(link => ({
href: (link as HTMLAnchorElement).href,
text: link.textContent?.trim() || '',
title: link.getAttribute('title') || undefined
}));
});
this.log('Links retrieved:', links?.length);
return links || [];
} catch (error: any) {
console.error('Get links error:', error);
throw new BrowserError('Failed to get links', 'Check if the page is loaded');
}
}
async getImages(): Promise<Array<{src: string, alt?: string, title?: string, width?: number, height?: number}>> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page images');
const images = await this.state.page?.evaluate(() => {
const imgElements = Array.from(document.querySelectorAll('img'));
return imgElements.map(img => ({
src: (img as HTMLImageElement).src,
alt: img.getAttribute('alt') || undefined,
title: img.getAttribute('title') || undefined,
width: (img as HTMLImageElement).naturalWidth || undefined,
height: (img as HTMLImageElement).naturalHeight || undefined
}));
});
this.log('Images retrieved:', images?.length);
return images || [];
} catch (error: any) {
console.error('Get images error:', error);
throw new BrowserError('Failed to get images', 'Check if the page is loaded');
}
}
async getForms(): Promise<Array<{action?: string, method?: string, fields: Array<{name?: string, type?: string, value?: string}>}>> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting page forms');
const forms = await this.state.page?.evaluate(() => {
const formElements = Array.from(document.querySelectorAll('form'));
return formElements.map(form => ({
action: form.getAttribute('action') || undefined,
method: form.getAttribute('method') || undefined,
fields: Array.from(form.querySelectorAll('input, select, textarea')).map(field => ({
name: field.getAttribute('name') || undefined,
type: field.getAttribute('type') || field.tagName.toLowerCase(),
value: (field as HTMLInputElement).value || undefined
}))
}));
});
this.log('Forms retrieved:', forms?.length);
return forms || [];
} catch (error: any) {
console.error('Get forms error:', error);
throw new BrowserError('Failed to get forms', 'Check if the page is loaded');
}
}
async getElementContent(selector: string): Promise<{html: string, text: string}> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting element content for selector:', selector);
const content = await this.state.page?.evaluate((sel) => {
const element = document.querySelector(sel);
if (!element) {
throw new Error(`Element not found: ${sel}`);
}
return {
html: element.innerHTML,
text: element.textContent || ''
};
}, selector);
this.log('Element content retrieved');
return content || {html: '', text: ''};
} catch (error: any) {
console.error('Get element content error:', error);
throw new BrowserError('Failed to get element content', 'Check if the element exists');
}
}
async executeJavaScript(script: string): Promise<any> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Executing JavaScript:', script);
const result = await this.state.page?.evaluate((scriptToExecute) => {
// Create a function wrapper to handle different types of JavaScript code
try {
// If the script is an expression, return its value
// If the script is statements, execute them and return undefined
const wrappedScript = `
(function() {
${scriptToExecute}
})()
`;
return eval(wrappedScript);
} catch (error) {
// If wrapping fails, try executing directly
return eval(scriptToExecute);
}
}, script);
this.log('JavaScript execution completed:', result);
return result;
} catch (error: any) {
console.error('Execute JavaScript error:', error);
throw new BrowserError('Failed to execute JavaScript', 'Check if the JavaScript syntax is valid');
}
}
async getElementHierarchy(
selector: string = 'body',
maxDepth: number = 3,
includeText: boolean = false,
includeAttributes: boolean = false
): Promise<any> {
try {
if (!this.isInitialized()) {
throw new Error('Browser not initialized');
}
this.log('Getting element hierarchy', { selector, maxDepth, includeText, includeAttributes });
const hierarchy = await this.state.page?.evaluate((args: {
selector: string,
maxDepth: number,
includeText: boolean,
includeAttributes: boolean
}) => {
const { selector, maxDepth, includeText, includeAttributes } = args;
function getElementInfo(element: Element) {
const info: any = {
tagName: element.tagName.toLowerCase(),
id: element.id || undefined,
className: element.className || undefined,
children: []
};
if (includeText && element.textContent) {
// Get only direct text content, not from children
const directText = Array.from(element.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent?.trim())
.filter(text => text)
.join(' ');
if (directText) {
info.text = directText;
}
}
if (includeAttributes && element.attributes.length > 0) {
info.attributes = {};
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
if (attr.name !== 'id' && attr.name !== 'class') {
info.attributes[attr.name] = attr.value;
}
}
}
return info;
}
function traverseElement(element: Element, currentDepth: number): any {
const elementInfo = getElementInfo(element);
if (currentDepth < maxDepth || maxDepth === -1) {
const children = Array.from(element.children);
elementInfo.children = children.map(child =>
traverseElement(child, currentDepth + 1)
);
} else if (element.children.length > 0) {
elementInfo.childrenCount = element.children.length;
}
return elementInfo;
}
const rootElement = document.querySelector(selector);
if (!rootElement) {
throw new Error(`Element not found: ${selector}`);
}
return traverseElement(rootElement, 0);
}, { selector, maxDepth, includeText, includeAttributes });
this.log('Element hierarchy retrieved');
return hierarchy;
} catch (error: any) {
console.error('Get element hierarchy error:', error);
throw new BrowserError('Failed to get element hierarchy', 'Check if the selector exists');
}
}
// Additional navigation methods
async goForward(): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Going forward');
await this.state.page.goForward();
this.log('Forward navigation complete');
} catch (error: any) {
console.error('Go forward error:', error);
throw new BrowserError('Failed to go forward', 'Check if there is a next page in history');
}
}
// Enhanced interaction methods
async hover(selector: string): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Hovering over element', { selector });
const locator = this.state.page.locator(selector);
await locator.hover();
this.log('Hover complete');
} catch (error: any) {
console.error('Hover error:', error);
throw new BrowserError('Failed to hover over element', 'Check if the selector exists and is visible');
}
}
async dragAndDrop(sourceSelector: string, targetSelector: string): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Performing drag and drop', { sourceSelector, targetSelector });
const sourceLocator = this.state.page.locator(sourceSelector);
const targetLocator = this.state.page.locator(targetSelector);
await sourceLocator.dragTo(targetLocator);
this.log('Drag and drop complete');
} catch (error: any) {
console.error('Drag and drop error:', error);
throw new BrowserError('Failed to drag and drop', 'Check if both selectors exist and are interactable');
}
}
async selectOption(selector: string, values: string[]): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Selecting options', { selector, values });
const locator = this.state.page.locator(selector);
await locator.selectOption(values);
this.log('Select option complete');
} catch (error: any) {
console.error('Select option error:', error);
throw new BrowserError('Failed to select option', 'Check if the selector exists and values are valid');
}
}
async pressKey(key: string): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Pressing key', { key });
await this.state.page.keyboard.press(key);
this.log('Key press complete');
} catch (error: any) {
console.error('Press key error:', error);
throw new BrowserError('Failed to press key', 'Check if the key name is valid');
}
}
async waitForText(text: string, timeout: number = 30000): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Waiting for text', { text, timeout });
await this.state.page.waitForSelector(`text=${text}`, { timeout });
this.log('Text found');
} catch (error: any) {
console.error('Wait for text error:', error);
throw new BrowserError('Text not found within timeout', 'Check if the text appears on the page');
}
}
async waitForSelector(selector: string, timeout: number = 30000): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Waiting for selector', { selector, timeout });
await this.state.page.waitForSelector(selector, { timeout });
this.log('Selector found');
} catch (error: any) {
console.error('Wait for selector error:', error);
throw new BrowserError('Selector not found within timeout', 'Check if the selector appears on the page');
}
}
async resize(width: number, height: number): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Resizing viewport', { width, height });
await this.state.page.setViewportSize({ width, height });
this.log('Resize complete');
} catch (error: any) {
console.error('Resize error:', error);
throw new BrowserError('Failed to resize viewport', 'Check if width and height are positive numbers');
}
}
// Dialog handling
async handleDialog(accept: boolean, promptText?: string): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Setting up dialog handler', { accept, promptText });
this.state.page.once('dialog', async dialog => {
this.log('Dialog detected', { type: dialog.type(), message: dialog.message() });
if (accept) {
await dialog.accept(promptText);
} else {
await dialog.dismiss();
}
this.log('Dialog handled');
});
} catch (error: any) {
console.error('Handle dialog error:', error);
throw new BrowserError('Failed to handle dialog', 'Check if there is a dialog to handle');
}
}
// Console and network methods
async getConsoleMessages(): Promise<string[]> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Getting console messages');
const messages: string[] = [];
// Listen to console events
this.state.page.on('console', msg => {
messages.push(`[${msg.type().toUpperCase()}] ${msg.text()}`);
});
// Return collected messages
this.log('Console messages retrieved');
return messages;
} catch (error: any) {
console.error('Get console messages error:', error);
throw new BrowserError('Failed to get console messages', 'Browser console monitoring error');
}
}
async getNetworkRequests(): Promise<Array<{url: string, method: string, status?: number}>> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Getting network requests');
const requests: Array<{url: string, method: string, status?: number}> = [];
// Listen to request events
this.state.page.on('request', request => {
requests.push({
url: request.url(),
method: request.method()
});
});
this.state.page.on('response', response => {
const request = requests.find(req => req.url === response.url());
if (request) {
request.status = response.status();
}
});
this.log('Network requests retrieved');
return requests;
} catch (error: any) {
console.error('Get network requests error:', error);
throw new BrowserError('Failed to get network requests', 'Network monitoring error');
}
}
async uploadFiles(selector: string, filePaths: string[]): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Uploading files', { selector, filePaths });
const locator = this.state.page.locator(selector);
await locator.setInputFiles(filePaths);
this.log('File upload complete');
} catch (error: any) {
console.error('File upload error:', error);
throw new BrowserError('Failed to upload files', 'Check if selector is a file input and files exist');
}
}
async evaluateWithReturn(script: string): Promise<any> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Evaluating JavaScript with return', { script });
const result = await this.state.page.evaluate(script);
this.log('JavaScript evaluation complete');
return result;
} catch (error: any) {
console.error('JavaScript evaluation error:', error);
throw new BrowserError('Failed to evaluate JavaScript', 'Check if the script is valid JavaScript');
}
}
// Enhanced screenshot functionality
async takeScreenshot(path: string, options?: {fullPage?: boolean, element?: string}): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Taking screenshot', { path, options });
if (options?.element) {
const locator = this.state.page.locator(options.element);
await locator.screenshot({ path });
} else {
await this.state.page.screenshot({ path, fullPage: options?.fullPage });
}
this.log('Screenshot saved');
} catch (error: any) {
console.error('Screenshot error:', error);
throw new BrowserError('Failed to take screenshot', 'Check if the path is writable');
}
}
// Mouse coordinate methods
async mouseMove(x: number, y: number): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Moving mouse', { x, y });
await this.state.page.mouse.move(x, y);
this.currentMousePosition = { x, y };
this.log('Mouse move complete');
} catch (error: any) {
console.error('Mouse move error:', error);
throw new BrowserError('Failed to move mouse', 'Check if coordinates are valid');
}
}
async mouseClick(x: number, y: number): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Clicking at coordinates', { x, y });
await this.state.page.mouse.click(x, y);
this.currentMousePosition = { x, y };
this.log('Mouse click complete');
} catch (error: any) {
console.error('Mouse click error:', error);
throw new BrowserError('Failed to click at coordinates', 'Check if coordinates are valid');
}
}
async mouseDrag(startX: number, startY: number, endX: number, endY: number): Promise<void> {
try {
if (!this.isInitialized() || !this.state.page) {
throw new Error('Browser not initialized');
}
this.log('Mouse drag', { startX, startY, endX, endY });
await this.state.page.mouse.move(startX, startY);
await this.state.page.mouse.down();
await this.state.page.mouse.move(endX, endY);
await this.state.page.mouse.up();
this.currentMousePosition = { x: endX, y: endY };
this.log('Mouse drag complete');
} catch (error: any) {
console.error('Mouse drag error:', error);
throw new BrowserError('Failed to drag mouse', 'Check if coordinates are valid');
}
}
isInitialized(): boolean {
return !!(this.state.browser?.isConnected() && this.state.context && this.state.page);
}
}
export const playwrightController = new PlaywrightController();
```