#
tokens: 5640/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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
![Added MCP Server options](./image/AvailableMCPTools.png)

- Navigating to a URL and doing actions with playwright
![Navigating to a URL and entering text](./image/UsingMCPServer.png)

```

--------------------------------------------------------------------------------
/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;
}
```