# Directory Structure
```
├── .gitignore
├── image
│ ├── AvailableMCPTools.png
│ └── UsingMCPServer.png
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── requestHandler.ts
│ ├── tools.ts
│ └── toolsHandler.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules
dist
.DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP Browser Automation
This is demo project to practice Model Context Protocol based server implemenation for automating browsing with Playwright. It interacts with a Claude Desktop client to accept user prompts and use server to control browser.
<a href="https://glama.ai/mcp/servers/hokppvk1dy"><img width="380" height="200" src="https://glama.ai/mcp/servers/hokppvk1dy/badge" alt="Browser Automation Server MCP server" /></a>
## Pre-requisites
- [Playwright](https://playwright.dev/)
- [Claude Desktop](https://claude.ai/download)
- [Node.js](https://nodejs.org/en/download/)
## Building
1. Clone the repository: `git clone https://github.com/hrmeetsingh/mcp-browser-automation.git`
2. Install dependencies: `npm install`
3. Verify the output executables are present in `dist` folder
## Integration
1. Create a configuration file in `~/Application\ Support/Claude/claude_desktop_config.json` (This is for macOS)
2. Copy the following to the file:
```json
{
"mcpServers": {
"mcp-browser-automation": {
"command": "node",
"args": ["/path/to/mcp-browser-automation/dist/index.js"]
}
}
}
```
3. Start Claude Desktop
## Usage
1. Open Claude Desktop
2. Start a new conversation to open a browser and navigate to a URL
## Example
- Added MCP Server options

- Navigating to a URL and doing actions with playwright

```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createToolDefinitions } from "./tools.js";
import { setupRequestHandlers } from "./requestHandler.js";
async function runServer() {
const server = new Server(
{
name: "executeautomation/mcp-browser-automation",
version: "0.2.6",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Create tool definitions
const TOOLS = createToolDefinitions();
// Setup request handlers
setupRequestHandlers(server, TOOLS);
// Create transport and connect
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-browser-automation",
"version": "1.0.0",
"description": "This is demo project to practice Model Context Protocol implemenation for automating browsing",
"author": "Harmeet Singh",
"types": "dist/index.d.ts",
"type": "module",
"bin": {
"mcp-browser-automation": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.3",
"playwright": "1.49.1",
"@playwright/browser-chromium": "1.49.1"
},
"keywords": ["playwright", "automation", "AI", "Claude", "Model Context Protocol"],
"devDependencies": {
"@types/node": "^20.10.5",
"shx": "^0.3.4",
"typescript": "^5.6.2"
},
"license": "MIT"
}
```
--------------------------------------------------------------------------------
/src/requestHandler.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
Tool
} from "@modelcontextprotocol/sdk/types.js";
import { handleToolCall, getConsoleLogs, getScreenshots } from "./toolsHandler.js";
export function setupRequestHandlers(server: Server, tools: Tool[]) {
// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(getScreenshots().keys()).map(name => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
return {
contents: [{
uri,
mimeType: "text/plain",
text: getConsoleLogs().join("\n"),
}],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = getScreenshots().get(name);
if (screenshot) {
return {
contents: [{
uri,
mimeType: "image/png",
blob: screenshot,
}],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: tools,
}));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments ?? {}, server)
);
}
```
--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------
```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
export function createToolDefinitions(): Tool[] {
return [
{
name: "playwright_navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
required: ["url"],
},
},
{
name: "playwright_screenshot",
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" },
width: { type: "number", description: "Width in pixels (default: 800)" },
height: { type: "number", description: "Height in pixels (default: 600)" },
storeBase64: { type: "boolean", description: "Store screenshot in base64 format (default: true)" },
savePng: { type: "boolean", description: "Save screenshot as PNG file (default: false)" },
downloadsDir: { type: "string", description: "Custom downloads directory path (default: user's Downloads folder)" },
},
required: ["name"],
},
},
{
name: "playwright_click",
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to click" },
},
required: ["selector"],
},
},
{
name: "playwright_fill",
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: "playwright_select",
description: "Select an element on the page with Select tag",
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: "playwright_hover",
description: "Hover an element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to hover" },
},
required: ["selector"],
},
},
{
name: "playwright_evaluate",
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
{
name: "playwright_get",
description: "Perform an HTTP GET request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform GET operation" }
},
required: ["url"],
},
},
{
name: "playwright_post",
description: "Perform an HTTP POST request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform POST operation" },
value: { type: "string", description: "Data to post in the body" },
},
required: ["url", "value"],
},
},
{
name: "playwright_put",
description: "Perform an HTTP PUT request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform PUT operation" },
value: { type: "string", description: "Data to PUT in the body" },
},
required: ["url", "value"],
},
},
{
name: "playwright_patch",
description: "Perform an HTTP PATCH request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform PUT operation" },
value: { type: "string", description: "Data to PATCH in the body" },
},
required: ["url", "value"],
},
},
{
name: "playwright_delete",
description: "Perform an HTTP DELETE request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform DELETE operation" }
},
required: ["url"],
},
},
];
}
// Browser-requiring tools for conditional browser launch
export const BROWSER_TOOLS = [
"playwright_navigate",
"playwright_screenshot",
"playwright_click",
"playwright_fill",
"playwright_select",
"playwright_hover",
"playwright_evaluate"
];
// API Request tools for conditional launch
export const API_TOOLS = [
"playwright_get",
"playwright_post",
"playwright_put",
"playwright_delete",
"playwright_patch"
];
```
--------------------------------------------------------------------------------
/src/toolsHandler.ts:
--------------------------------------------------------------------------------
```typescript
import { chromium, Browser, Page, request, APIRequest, APIRequestContext } from "playwright";
import { CallToolResult, TextContent, ImageContent } from "@modelcontextprotocol/sdk/types.js";
import { BROWSER_TOOLS, API_TOOLS } from "./tools.js";
import fs from 'node:fs';
import * as os from 'os';
import * as path from 'path';
// Global state
let browser: Browser | undefined;
let page: Page | undefined;
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
const defaultDownloadsPath = path.join(os.homedir(), 'Downloads');
async function ensureBrowser() {
if (!browser) {
browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
deviceScaleFactor: 1,
});
page = await context.newPage();
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
// Note: server.notification is assumed to be passed in from the main server
});
}
return page!;
}
async function ensureApiContext(url: string) {
return await request.newContext({
baseURL: url,
});
}
export async function handleToolCall(
name: string,
args: any,
server: any
): Promise<{ toolResult: CallToolResult }> {
// Check if the tool requires browser interaction
const requiresBrowser = BROWSER_TOOLS.includes(name);
// Check if the tool requires api interaction
const requiresApi = API_TOOLS.includes(name);
let page: Page | undefined;
let apiContext: APIRequestContext;
// Only launch browser if the tool requires browser interaction
if (requiresBrowser) {
page = await ensureBrowser();
}
// Set up API context for API-related operations
if (requiresApi) {
apiContext = await ensureApiContext(args.url);
}
switch (name) {
case "playwright_navigate":
try {
await page!.goto(args.url, {
timeout: args.timeout || 30000,
waitUntil: args.waitUntil || "load"
});
return {
toolResult: {
content: [{
type: "text",
text: `Navigated to ${args.url} with ${args.waitUntil || "load"} wait`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Navigation failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_screenshot": {
try {
const screenshotOptions: any = {
type: args.type || "png",
fullPage: !!args.fullPage
};
if (args.selector) {
const element = await page!.$(args.selector);
if (!element) {
return {
toolResult: {
content: [{
type: "text",
text: `Element not found: ${args.selector}`,
}],
isError: true,
},
};
}
screenshotOptions.element = element;
}
if (args.mask) {
screenshotOptions.mask = await Promise.all(
args.mask.map(async (selector: string) => await page!.$(selector))
);
}
const screenshot = await page!.screenshot(screenshotOptions);
const base64Screenshot = screenshot.toString('base64');
const responseContent: (TextContent | ImageContent)[] = [];
// Handle PNG file saving
if (args.savePng !== false) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${args.name}-${timestamp}.png`;
const downloadsDir = args.downloadsDir || defaultDownloadsPath;
// Create downloads directory if it doesn't exist
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const filePath = path.join(downloadsDir, filename);
await fs.promises.writeFile(filePath, screenshot);
responseContent.push({
type: "text",
text: `Screenshot saved to: ${filePath}`,
} as TextContent);
}
// Handle base64 storage
if (args.storeBase64 !== false) {
screenshots.set(args.name, base64Screenshot);
server.notification({
method: "notifications/resources/list_changed",
});
responseContent.push({
type: "image",
data: base64Screenshot,
mimeType: "image/png",
} as ImageContent);
}
return {
toolResult: {
content: responseContent,
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Screenshot failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
}
case "playwright_click":
try {
await page!.click(args.selector);
return {
toolResult: {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_fill":
try {
await page!.waitForSelector(args.selector);
await page!.fill(args.selector, args.value);
return {
toolResult: {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to type ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_select":
try {
await page!.waitForSelector(args.selector);
await page!.selectOption(args.selector, args.value);
return {
toolResult: {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_hover":
try {
await page!.waitForSelector(args.selector);
await page!.hover(args.selector);
return {
toolResult: {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_evaluate":
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 {
toolResult: {
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 {
toolResult: {
content: [{
type: "text",
text: `Script execution failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_get":
try {
var response = await apiContext!.get(args.url);
return {
toolResult: {
content: [{
type: "text",
text: `Performed GET Operation ${args.url}`,
},
{
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}
],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to perform GET operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_post":
try {
var data = {
data: args.value,
headers: {
'Content-Type': 'application/json'
}
};
var response = await apiContext!.post(args.url, data);
return {
toolResult: {
content: [{
type: "text",
text: `Performed POST Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
},
{
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to perform POST operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_put":
try {
var data = {
data: args.value,
headers: {
'Content-Type': 'application/json'
}
};
var response = await apiContext!.put(args.url, data);
return {
toolResult: {
content: [{
type: "text",
text: `Performed PUT Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
}, {
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to perform PUT operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_delete":
try {
var response = await apiContext!.delete(args.url);
return {
toolResult: {
content: [{
type: "text",
text: `Performed delete Operation ${args.url}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to perform delete operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_patch":
try {
var data = {
data: args.value,
headers: {
'Content-Type': 'application/json'
}
};
var response = await apiContext!.patch(args.url, data);
return {
toolResult: {
content: [{
type: "text",
text: `Performed PATCH Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
}, {
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
}, {
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to perform PATCH operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
default:
return {
toolResult: {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
},
};
}
}
// Expose utility functions for resource management
export function getConsoleLogs(): string[] {
return consoleLogs;
}
export function getScreenshots(): Map<string, string> {
return screenshots;
}
```