# Directory Structure
```
├── .github
│ └── workflows
│ ├── npm-publish.yml
│ └── release.yml
├── .gitignore
├── .nvmrc
├── bun.lockb
├── CHANGELOG.md
├── Dockerfile
├── index.ts
├── LICENSE
├── package.json
├── README.md
├── smithery.yaml
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
```
v20.16.0
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules
dist
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
<h1 align="center">MCP Server Playwright</h1>
<p align="center">
<a href="https://www.automatalabs.io"><img alt="MCP Playwright" src="https://automatalabs.io/icon.svg" height="250"/></a>
</p>
<p align="center">
<b>A Model Context Protocol server that provides browser automation capabilities using Playwright</b></br>
<sub>Enable LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment</sub>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@automatalabs/mcp-server-playwright"><img alt="NPM Version" src="https://img.shields.io/npm/v/@automatalabs/mcp-server-playwright.svg" height="20"/></a>
<a href="https://npmcharts.com/compare/@automatalabs/mcp-server-playwright?minimal=true"><img alt="Downloads per month" src="https://img.shields.io/npm/dm/@automatalabs/mcp-server-playwright.svg" height="20"/></a>
<a href="https://github.com/Automata-Labs-team/MCP-Server-Playwright/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/Automata-Labs-team/MCP-Server-Playwright.svg" height="20"/></a>
<a href="https://smithery.ai/server/@automatalabs/mcp-server-playwright"><img alt="Smithery Installs" src="https://smithery.ai/badge/@automatalabs/mcp-server-playwright" height="20"/></a>
</p>
<a href="https://glama.ai/mcp/servers/9q4zck8po5"><img width="380" height="200" src="https://glama.ai/mcp/servers/9q4zck8po5/badge" alt="MCP-Server-Playwright MCP server" /></a>
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Components](#components)
- [Tools](#tools)
- [Resources](#resources)
- [License](#license)
## Features
- 🌐 Full browser automation capabilities
- 📸 Screenshot capture of entire pages or specific elements
- 🖱️ Comprehensive web interaction (navigation, clicking, form filling)
- 📊 Console log monitoring
- 🔧 JavaScript execution in browser context
## Installation
### Installing via Smithery
To install MCP Server Playwright for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@automatalabs/mcp-server-playwright):
```bash
npx -y @smithery/cli install @automatalabs/mcp-server-playwright --client claude
```
You can install the package using either npx or mcp-get:
Using npx:
```bash
npx @automatalabs/mcp-server-playwright install
```
This command will:
1. Check your operating system compatibility (Windows/macOS)
2. Create or update the Claude configuration file
3. Configure the Playwright server integration
The configuration file will be automatically created/updated at:
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
Using mcp-get:
```bash
npx @michaellatman/mcp-get@latest install @automatalabs/mcp-server-playwright
```
## Configuration
The installation process will automatically add the following configuration to your Claude config file:
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@automatalabs/mcp-server-playwright"]
}
}
}
```
## Using with Cursor
You can also use MCP Server Playwright with [Cursor](https://www.cursor.so/), an AI-powered code editor. To enable browser automation in Cursor via MCP:
1. **Install Playwright browsers** (if not already):
```bash
npx playwright install
```
2. **Install MCP Server Playwright for Cursor** using Smithery:
```bash
npx -y @smithery/cli install @automatalabs/mcp-server-playwright --client cursor
```
3. **Configuration file setup**:
If you do not use Claude, the configuration file (`claude_desktop_config.json`) may not be created automatically.
- On Windows, create a folder named `Claude` in `%APPDATA%` (usually `C:\Users\<YourName>\AppData\Roaming\Claude`).
- Inside that folder, create a file named `claude_desktop_config.json` with the following content:
```json
{
"serverPort": 3456
}
```
4. **Follow the remaining steps in the [Installation](#installation) section above** to complete the setup.
Now, you can use all the browser automation tools provided by MCP Server Playwright directly from Cursor’s AI features, such as web navigation, screenshot capture, and JavaScript execution.
> **Note:** Make sure you have Node.js installed and `npx` available in your system PATH.
## Components
### Tools
#### `browser_navigate`
Navigate to any URL in the browser
```javascript
{
"url": "https://stealthbrowser.cloud"
}
```
#### `browser_screenshot`
Capture screenshots of the entire page or specific elements
```javascript
{
"name": "screenshot-name", // required
"selector": "#element-id", // optional
"fullPage": true // optional, default: false
}
```
#### `browser_click`
Click elements on the page using CSS selector
```javascript
{
"selector": "#button-id"
}
```
#### `browser_click_text`
Click elements on the page by their text content
```javascript
{
"text": "Click me"
}
```
#### `browser_hover`
Hover over elements on the page using CSS selector
```javascript
{
"selector": "#menu-item"
}
```
#### `browser_hover_text`
Hover over elements on the page by their text content
```javascript
{
"text": "Hover me"
}
```
#### `browser_fill`
Fill out input fields
```javascript
{
"selector": "#input-field",
"value": "Hello World"
}
```
#### `browser_select`
Select an option in a SELECT element using CSS selector
```javascript
{
"selector": "#dropdown",
"value": "option-value"
}
```
#### `browser_select_text`
Select an option in a SELECT element by its text content
```javascript
{
"text": "Choose me",
"value": "option-value"
}
```
#### `browser_evaluate`
Execute JavaScript in the browser console
```javascript
{
"script": "document.title"
}
```
### Resources
1. **Console Logs** (`console://logs`)
- Access browser console output in text format
- Includes all console messages from the browser
2. **Screenshots** (`screenshot://<n>`)
- Access PNG images of captured screenshots
- Referenced by the name specified during capture
## License
This project is licensed under the MIT License - see the [LICENSE](https://github.com/Automata-Labs-team/MCP-Server-Playwright/blob/main/LICENSE) file for details.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": ".",
},
"include": [
"./**/*.ts"
],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required: []
properties: {}
commandFunction:
# A function that produces the CLI command to start the MCP on stdio.
|-
(config) => ({command:'node',args:['dist/index.js'],env:{}})
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use a Node.js image as the base for building
FROM node:20 AS builder
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the application
RUN npm run build
# Use a smaller Node.js image for the final output
FROM node:20-slim AS release
# Set the working directory
WORKDIR /app
# Copy the built application from the builder stage
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package-lock.json /app/node_modules ./
# Define the entry point for the Docker container
ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Releases
on:
push:
branches:
- main
jobs:
changelog:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Conventional Changelog Action
id: changelog
uses: TriPSs/[email protected]
with:
github-token: ${{ secrets.GH_PAT }}
version-file: './package.json'
- name: create release
uses: actions/create-release@v1
if: ${{ steps.changelog.outputs.skipped == 'false' }}
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
with:
tag_name: ${{ steps.changelog.outputs.tag }}
release_name: ${{ steps.changelog.outputs.tag }}
body: ${{ steps.changelog.outputs.clean_changelog }}
```
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
```yaml
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run prepare
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@automatalabs/mcp-server-playwright",
"version": "1.2.1",
"description": "MCP server for browser automation using Playwright",
"license": "MIT",
"author": "Automata Labs (https://automatalabs.io)",
"homepage": "https://automatalabs.io",
"repository": {
"type": "git",
"url": "git+https://github.com/Automata-Labs-team/MCP-Server-Playwright.git"
},
"bugs": "https://github.com/Automata-Labs-team/MCP-Server-Playwright/issues",
"type": "module",
"bin": {
"mcp-server-playwright": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.5.0",
"playwright": "^1.48.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/yargs": "^17.0.33",
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
## [1.2.1](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v1.2.0...v1.2.1) (2025-01-24)
### Bug Fixes
* Revert "feat: add xvfb support for linux" ([b910dba](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/b910dbab5de055611e91cceae46ae68b61d177db))
* Revert "feat: use chromium as default browser" ([56934ed](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/56934edaf4a595e0a893457030e735b7ab0d1fc0))
# [1.2.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v1.1.0...v1.2.0) (2025-01-20)
### Features
* add xvfb support for linux ([b7edd79](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/b7edd79ce57c922241d2cc4c8ff4fded5fe4224f))
# [1.1.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v1.0.0...v1.1.0) (2025-01-20)
### Features
* use chromium as default browser ([192d18c](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/192d18c327db6a06481445373ca270de02eb2de6))
# [1.0.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v0.9.0...v1.0.0) (2025-01-20)
# [0.9.0](https://github.com/Automata-Labs-team/MCP-Server-Playwright/compare/v0.8.0...v0.9.0) (2024-12-11)
### Features
* add Claude config file handling for Windows and macOS ([65b3ac3](https://github.com/Automata-Labs-team/MCP-Server-Playwright/commit/65b3ac3ee5f5cc2d5600e1cec920db4b15e6287f))
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import yargs from "yargs/yargs";
import { hideBin } from 'yargs/helpers'
import os from "os";
import path from "path";
import { promises as fs } from "fs";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
CallToolResult,
TextContent,
ImageContent,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import playwright, { Browser, Page } from "playwright";
enum ToolName {
BrowserNavigate = "browser_navigate",
BrowserScreenshot = "browser_screenshot",
BrowserClick = "browser_click",
BrowserClickText = "browser_click_text",
BrowserFill = "browser_fill",
BrowserSelect = "browser_select",
BrowserSelectText = "browser_select_text",
BrowserHover = "browser_hover",
BrowserHoverText = "browser_hover_text",
BrowserEvaluate = "browser_evaluate"
}
// Define the tools once to avoid repetition
const TOOLS: Tool[] = [
{
name: ToolName.BrowserNavigate,
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
required: ["url"],
},
},
{
name: ToolName.BrowserScreenshot,
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name for the screenshot" },
selector: { type: "string", description: "CSS selector for element to screenshot" },
fullPage: { type: "boolean", description: "Take a full page screenshot (default: false)", default: false },
},
required: ["name"],
},
},
{
name: ToolName.BrowserClick,
description: "Click an element on the page using CSS selector",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to click" },
},
required: ["selector"],
},
},
{
name: ToolName.BrowserClickText,
description: "Click an element on the page by its text content",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text content of the element to click" },
},
required: ["text"],
},
},
{
name: ToolName.BrowserFill,
description: "Fill out an input field",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for input field" },
value: { type: "string", description: "Value to fill" },
},
required: ["selector", "value"],
},
},
{
name: ToolName.BrowserSelect,
description: "Select an element on the page with Select tag using CSS selector",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to select" },
value: { type: "string", description: "Value to select" },
},
required: ["selector", "value"],
},
},
{
name: ToolName.BrowserSelectText,
description: "Select an element on the page with Select tag by its text content",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text content of the element to select" },
value: { type: "string", description: "Value to select" },
},
required: ["text", "value"],
},
},
{
name: ToolName.BrowserHover,
description: "Hover an element on the page using CSS selector",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to hover" },
},
required: ["selector"],
},
},
{
name: ToolName.BrowserHoverText,
description: "Hover an element on the page by its text content",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text content of the element to hover" },
},
required: ["text"],
},
},
{
name: ToolName.BrowserEvaluate,
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
];
// Global state
let browser: Browser | undefined;
let page: Page | undefined;
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
async function ensureBrowser() {
if (!browser) {
browser = await playwright.firefox.launch({ headless: false });
}
if (!page) {
page = await browser.newPage();
}
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
server.notification({
method: "notifications/resources/updated",
params: { uri: "console://logs" },
});
});
return page!;
}
async function handleToolCall(name: ToolName, args: any): Promise<CallToolResult> {
const page = await ensureBrowser();
switch (name) {
case ToolName.BrowserNavigate:
await page.goto(args.url);
return {
content: [{
type: "text",
text: `Navigated to ${args.url}`,
}],
isError: false,
};
case ToolName.BrowserScreenshot: {
const fullPage = (args.fullPage === 'true');
const screenshot = await (args.selector ?
page.locator(args.selector).screenshot() :
page.screenshot({ fullPage }));
const base64Screenshot = screenshot.toString('base64');
if (!base64Screenshot) {
return {
content: [{
type: "text",
text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
}],
isError: true,
};
}
screenshots.set(args.name, base64Screenshot);
server.notification({
method: "notifications/resources/list_changed",
});
return {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken`,
} as TextContent,
{
type: "image",
data: base64Screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
};
}
case ToolName.BrowserClick:
try {
await page.locator(args.selector).click();
return {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().click();
return {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserClickText:
try {
await page.getByText(args.text).click();
return {
content: [{
type: "text",
text: `Clicked element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.getByText(args.text).first().click();
return {
content: [{
type: "text",
text: `Clicked element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to click element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to click element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserFill:
try {
await page.locator(args.selector).pressSequentially(args.value, { delay: 100 });
return {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().pressSequentially(args.value, { delay: 100 });
return {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to fill ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserSelect:
try {
await page.locator(args.selector).selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserSelectText:
try {
await page.getByText(args.text).selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected element with text ${args.text} with value: ${args.value}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.getByText(args.text).first().selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected element with text ${args.text} with value: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to select element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to select element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserHover:
try {
await page.locator(args.selector).hover();
return {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().hover();
return {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserHoverText:
try {
await page.getByText(args.text).hover();
return {
content: [{
type: "text",
text: `Hovered element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.getByText(args.text).first().hover();
return {
content: [{
type: "text",
text: `Hovered element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to hover element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to hover element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
case ToolName.BrowserEvaluate:
try {
const result = await page.evaluate((script) => {
const logs: string[] = [];
const originalConsole = { ...console };
['log', 'info', 'warn', 'error'].forEach(method => {
(console as any)[method] = (...args: any[]) => {
logs.push(`[${method}] ${args.join(' ')}`);
(originalConsole as any)[method](...args);
};
});
try {
const result = eval(script);
Object.assign(console, originalConsole);
return { result, logs };
} catch (error) {
Object.assign(console, originalConsole);
throw error;
}
}, args.script);
return {
content: [
{
type: "text",
text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Script execution failed: ${(error as Error).message}`,
}],
isError: true,
};
}
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
const server = new Server(
{
name: "automatalabs/playwright",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
},
);
// Setup request handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(screenshots.keys()).map(name => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
return {
contents: [{
uri,
mimeType: "text/plain",
text: consoleLogs.join("\n"),
}],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = screenshots.get(name);
if (screenshot) {
return {
contents: [{
uri,
mimeType: "image/png",
blob: screenshot,
}],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name as ToolName, request.params.arguments ?? {})
);
}
async function checkPlatformAndInstall() {
const platform = os.platform();
if (platform === "win32") {
console.log("Installing MCP Playwright Server for Windows...");
try {
const configFilePath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
let config: any;
try {
// Try to read existing config file
const fileContent = await fs.readFile(configFilePath, 'utf-8');
config = JSON.parse(fileContent);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// Create new config file with mcpServers object
config = { mcpServers: {} };
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("Created new Claude config file");
} else {
console.error("Error reading Claude config file:", error);
process.exit(1);
}
}
// Ensure mcpServers exists
if (!config.mcpServers) {
config.mcpServers = {};
}
// Update the playwright configuration
config.mcpServers.playwright = {
command: "npx",
args: ["-y", "@automatalabs/mcp-server-playwright"]
};
// Write the updated config back to file
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("✓ Successfully updated Claude configuration");
} catch (error) {
console.error("Error during installation:", error);
process.exit(1);
}
} else if (platform === "darwin") {
console.log("Installing MCP Playwright Server for macOS...");
try {
const configFilePath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
let config: any;
try {
// Try to read existing config file
const fileContent = await fs.readFile(configFilePath, 'utf-8');
config = JSON.parse(fileContent);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// Create new config file with mcpServers object
config = { mcpServers: {} };
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("Created new Claude config file");
} else {
console.error("Error reading Claude config file:", error);
process.exit(1);
}
}
// Ensure mcpServers exists
if (!config.mcpServers) {
config.mcpServers = {};
}
// Update the playwright configuration
config.mcpServers.playwright = {
command: "npx",
args: ["-y", "@automatalabs/mcp-server-playwright"]
};
// Write the updated config back to file
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("✓ Successfully updated Claude configuration");
} catch (error) {
console.error("Error during installation:", error);
process.exit(1);
}
} else {
console.error("Unsupported platform:", platform);
process.exit(1);
}
}
(async () => {
try {
// Parse args but continue with server if no command specified
await yargs(hideBin(process.argv))
.command('install', 'Install MCP-Server-Playwright dependencies', () => {}, async () => {
await checkPlatformAndInstall();
// Exit after successful installation
process.exit(0);
})
.strict()
.help()
.parse();
// If we get here, no command was specified, so run the server
await runServer().catch(console.error);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
})();
```