# Directory Structure
```
├── .gitignore
├── index.ts
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | dist
2 | node_modules
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # BrowserCat MCP Server
2 |
3 | A Model Context Protocol server that provides browser automation capabilities using BrowserCat's cloud browser service. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment without needing to install browsers locally.
4 |
5 | ## Components
6 |
7 | ### Tools
8 |
9 | - **browsercat_navigate**
10 | - Navigate to any URL in the browser
11 | - Input: `url` (string)
12 | - **browsercat_screenshot**
13 | - Capture screenshots of the entire page or specific elements
14 | - Inputs:
15 | - `name` (string, required): Name for the screenshot
16 | - `selector` (string, optional): CSS selector for element to screenshot
17 | - `width` (number, optional, default: 800): Screenshot width
18 | - `height` (number, optional, default: 600): Screenshot height
19 | - **browsercat_click**
20 | - Click elements on the page
21 | - Input: `selector` (string): CSS selector for element to click
22 | - **browsercat_hover**
23 | - Hover elements on the page
24 | - Input: `selector` (string): CSS selector for element to hover
25 | - **browsercat_fill**
26 | - Fill out input fields
27 | - Inputs:
28 | - `selector` (string): CSS selector for input field
29 | - `value` (string): Value to fill
30 | - **browsercat_select**
31 | - Select an option from a dropdown menu
32 | - Inputs:
33 | - `selector` (string): CSS selector for select element
34 | - `value` (string): Value to select
35 | - **browsercat_evaluate**
36 | - Execute JavaScript in the browser console
37 | - Input: `script` (string): JavaScript code to execute
38 |
39 | ### Resources
40 |
41 | The server provides access to two types of resources:
42 |
43 | 1. **Console Logs** (`console://logs`)
44 | - Browser console output in text format
45 | - Includes all console messages from the browser
46 | 2. **Screenshots** (`screenshot://<name>`)
47 | - PNG images of captured screenshots
48 | - Accessible via the screenshot name specified during capture
49 |
50 | ## Key Features
51 |
52 | - Cloud-based browser automation
53 | - No local browser installation required
54 | - Console log monitoring
55 | - Screenshot capabilities
56 | - JavaScript execution
57 | - Basic web interaction (navigation, clicking, form filling)
58 |
59 | ## Configuration to use BrowserCat MCP Server
60 |
61 | ### Environment Variables
62 |
63 | The BrowserCat MCP server requires the following environment variable:
64 |
65 | - `BROWSERCAT_API_KEY`: Your BrowserCat API key (required). You can get one for free at https://browsercat.xyz/mcp.
66 |
67 | ### NPX Configuration
68 |
69 | ```json
70 | {
71 | "mcpServers": {
72 | "browsercat": {
73 | "command": "npx",
74 | "args": ["-y", "@browsercatco/mcp-server"],
75 | "env": {
76 | "BROWSERCAT_API_KEY": "your-api-key-here"
77 | }
78 | }
79 | }
80 | }
81 | ```
82 |
83 | ## License
84 |
85 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
86 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "outDir": "./dist",
12 | "rootDir": "."
13 | },
14 | "include": [
15 | "./**/*.ts"
16 | ],
17 | "exclude": ["node_modules"]
18 | }
19 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@browsercatco/mcp-server",
3 | "version": "0.1.0",
4 | "description": "MCP server for remote browser automation using BrowserCat",
5 | "license": "MIT",
6 | "author": "BrowserCat",
7 | "homepage": "https://www.browsercat.com",
8 | "type": "module",
9 | "bin": {
10 | "mcp-server-puppeteer": "dist/index.js"
11 | },
12 | "files": [
13 | "dist"
14 | ],
15 | "scripts": {
16 | "build": "tsc && shx chmod +x dist/*.js",
17 | "prepare": "npm run build",
18 | "watch": "tsc --watch"
19 | },
20 | "dependencies": {
21 | "@modelcontextprotocol/sdk": "1.0.1",
22 | "puppeteer": "^23.4.0"
23 | },
24 | "devDependencies": {
25 | "shx": "^0.3.4",
26 | "typescript": "^5.6.2"
27 | }
28 | }
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import {
6 | CallToolRequestSchema,
7 | ListResourcesRequestSchema,
8 | ListToolsRequestSchema,
9 | ReadResourceRequestSchema,
10 | CallToolResult,
11 | TextContent,
12 | ImageContent,
13 | Tool,
14 | } from "@modelcontextprotocol/sdk/types.js";
15 | import puppeteer, { Browser, Page } from "puppeteer";
16 |
17 | // Environment variables configuration
18 | const requiredEnvVars = {
19 | BROWSERCAT_API_KEY: process.env.BROWSERCAT_API_KEY,
20 | };
21 |
22 | // Validate required environment variables
23 | Object.entries(requiredEnvVars).forEach(([name, value]) => {
24 | if (!value) throw new Error(`${name} environment variable is required. You can get one for free at https://browsercat.xyz/mcp.`);
25 | });
26 |
27 | // Define the tools available for browser automation
28 | const TOOLS: Tool[] = [
29 | {
30 | name: "browsercat_navigate",
31 | description: "Navigate to a URL",
32 | inputSchema: {
33 | type: "object",
34 | properties: {
35 | url: { type: "string" },
36 | },
37 | required: ["url"],
38 | },
39 | },
40 | {
41 | name: "browsercat_screenshot",
42 | description: "Take a screenshot of the current page or a specific element",
43 | inputSchema: {
44 | type: "object",
45 | properties: {
46 | name: { type: "string", description: "Name for the screenshot" },
47 | selector: { type: "string", description: "CSS selector for element to screenshot" },
48 | width: { type: "number", description: "Width in pixels (default: 800)" },
49 | height: { type: "number", description: "Height in pixels (default: 600)" },
50 | },
51 | required: ["name"],
52 | },
53 | },
54 | {
55 | name: "browsercat_click",
56 | description: "Click an element on the page",
57 | inputSchema: {
58 | type: "object",
59 | properties: {
60 | selector: { type: "string", description: "CSS selector for element to click" },
61 | },
62 | required: ["selector"],
63 | },
64 | },
65 | {
66 | name: "browsercat_fill",
67 | description: "Fill out an input field",
68 | inputSchema: {
69 | type: "object",
70 | properties: {
71 | selector: { type: "string", description: "CSS selector for input field" },
72 | value: { type: "string", description: "Value to fill" },
73 | },
74 | required: ["selector", "value"],
75 | },
76 | },
77 | {
78 | name: "browsercat_select",
79 | description: "Select an option from a dropdown menu",
80 | inputSchema: {
81 | type: "object",
82 | properties: {
83 | selector: { type: "string", description: "CSS selector for select element" },
84 | value: { type: "string", description: "Value to select" },
85 | },
86 | required: ["selector", "value"],
87 | },
88 | },
89 | {
90 | name: "browsercat_hover",
91 | description: "Hover over an element on the page",
92 | inputSchema: {
93 | type: "object",
94 | properties: {
95 | selector: { type: "string", description: "CSS selector for element to hover" },
96 | },
97 | required: ["selector"],
98 | },
99 | },
100 | {
101 | name: "browsercat_evaluate",
102 | description: "Execute JavaScript in the browser console",
103 | inputSchema: {
104 | type: "object",
105 | properties: {
106 | script: { type: "string", description: "JavaScript code to execute" },
107 | },
108 | required: ["script"],
109 | },
110 | },
111 | ];
112 |
113 | // Global state for browser instance and resources
114 | let browser: Browser | undefined;
115 | let page: Page | undefined;
116 | const consoleLogs: string[] = [];
117 | const screenshots = new Map<string, string>();
118 |
119 | /**
120 | * Ensures that a browser instance is running and returns the active page
121 | */
122 | async function ensureBrowser() {
123 | if (!browser) {
124 | // Connect to BrowserCat remote browser using the correct endpoint and header format
125 | const browsercatEndpoint = 'wss://api.browsercat.com/connect';
126 |
127 | browser = await puppeteer.connect({
128 | browserWSEndpoint: browsercatEndpoint,
129 | headers: {
130 | 'Api-Key': requiredEnvVars.BROWSERCAT_API_KEY!
131 | }
132 | });
133 |
134 | const pages = await browser.pages();
135 | if (pages.length === 0) {
136 | page = await browser.newPage();
137 | } else {
138 | page = pages[0];
139 | }
140 |
141 | // Capture console logs from the browser
142 | page.on("console", (msg) => {
143 | const logEntry = `[${msg.type()}] ${msg.text()}`;
144 | consoleLogs.push(logEntry);
145 | server.notification({
146 | method: "notifications/resources/updated",
147 | params: { uri: "console://logs" },
148 | });
149 | });
150 | }
151 | return page!;
152 | }
153 |
154 | // Extend Window interface for our console log capture mechanism
155 | declare global {
156 | interface Window {
157 | mcpHelper: {
158 | logs: string[],
159 | originalConsole: Partial<typeof console>,
160 | }
161 | }
162 | }
163 |
164 | /**
165 | * Handles tool calls from the model and executes corresponding browser actions
166 | */
167 | async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
168 | const page = await ensureBrowser();
169 |
170 | switch (name) {
171 | case "browsercat_navigate":
172 | await page.goto(args.url);
173 | return {
174 | content: [{
175 | type: "text",
176 | text: `Navigated to ${args.url}`,
177 | }],
178 | isError: false,
179 | };
180 |
181 | case "browsercat_screenshot": {
182 | const width = args.width ?? 800;
183 | const height = args.height ?? 600;
184 | await page.setViewport({ width, height });
185 |
186 | const screenshot = await (args.selector ?
187 | (await page.$(args.selector))?.screenshot({ encoding: "base64" }) :
188 | page.screenshot({ encoding: "base64", fullPage: false }));
189 |
190 | if (!screenshot) {
191 | return {
192 | content: [{
193 | type: "text",
194 | text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
195 | }],
196 | isError: true,
197 | };
198 | }
199 |
200 | screenshots.set(args.name, screenshot as string);
201 | server.notification({
202 | method: "notifications/resources/list_changed",
203 | });
204 |
205 | return {
206 | content: [
207 | {
208 | type: "text",
209 | text: `Screenshot '${args.name}' taken at ${width}x${height}`,
210 | } as TextContent,
211 | {
212 | type: "image",
213 | data: screenshot,
214 | mimeType: "image/png",
215 | } as ImageContent,
216 | ],
217 | isError: false,
218 | };
219 | }
220 |
221 | case "browsercat_click":
222 | try {
223 | await page.click(args.selector);
224 | return {
225 | content: [{
226 | type: "text",
227 | text: `Clicked: ${args.selector}`,
228 | }],
229 | isError: false,
230 | };
231 | } catch (error) {
232 | return {
233 | content: [{
234 | type: "text",
235 | text: `Failed to click ${args.selector}: ${(error as Error).message}`,
236 | }],
237 | isError: true,
238 | };
239 | }
240 |
241 | case "browsercat_fill":
242 | try {
243 | await page.waitForSelector(args.selector);
244 | await page.type(args.selector, args.value);
245 | return {
246 | content: [{
247 | type: "text",
248 | text: `Filled ${args.selector} with: ${args.value}`,
249 | }],
250 | isError: false,
251 | };
252 | } catch (error) {
253 | return {
254 | content: [{
255 | type: "text",
256 | text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
257 | }],
258 | isError: true,
259 | };
260 | }
261 |
262 | case "browsercat_select":
263 | try {
264 | await page.waitForSelector(args.selector);
265 | await page.select(args.selector, args.value);
266 | return {
267 | content: [{
268 | type: "text",
269 | text: `Selected ${args.selector} with: ${args.value}`,
270 | }],
271 | isError: false,
272 | };
273 | } catch (error) {
274 | return {
275 | content: [{
276 | type: "text",
277 | text: `Failed to select ${args.selector}: ${(error as Error).message}`,
278 | }],
279 | isError: true,
280 | };
281 | }
282 |
283 | case "browsercat_hover":
284 | try {
285 | await page.waitForSelector(args.selector);
286 | await page.hover(args.selector);
287 | return {
288 | content: [{
289 | type: "text",
290 | text: `Hovered ${args.selector}`,
291 | }],
292 | isError: false,
293 | };
294 | } catch (error) {
295 | return {
296 | content: [{
297 | type: "text",
298 | text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
299 | }],
300 | isError: true,
301 | };
302 | }
303 |
304 | case "browsercat_evaluate":
305 | try {
306 | // Set up console log capture
307 | await page.evaluate(() => {
308 | window.mcpHelper = {
309 | logs: [],
310 | originalConsole: { ...console },
311 | };
312 |
313 | ['log', 'info', 'warn', 'error'].forEach(method => {
314 | (console as any)[method] = (...args: any[]) => {
315 | window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
316 | (window.mcpHelper.originalConsole as any)[method](...args);
317 | };
318 | });
319 | });
320 |
321 | const result = await page.evaluate(args.script);
322 |
323 | // Restore original console and get captured logs
324 | const logs = await page.evaluate(() => {
325 | Object.assign(console, window.mcpHelper.originalConsole);
326 | const logs = window.mcpHelper.logs;
327 | delete (window as any).mcpHelper;
328 | return logs;
329 | });
330 |
331 | return {
332 | content: [
333 | {
334 | type: "text",
335 | text: `Execution result:\n${JSON.stringify(result, null, 2)}\n\nConsole output:\n${logs.join('\n')}`,
336 | },
337 | ],
338 | isError: false,
339 | };
340 | } catch (error) {
341 | return {
342 | content: [{
343 | type: "text",
344 | text: `Script execution failed: ${(error as Error).message}`,
345 | }],
346 | isError: true,
347 | };
348 | }
349 |
350 | default:
351 | return {
352 | content: [{
353 | type: "text",
354 | text: `Unknown tool: ${name}`,
355 | }],
356 | isError: true,
357 | };
358 | }
359 | }
360 |
361 | // Initialize the MCP server
362 | const server = new Server(
363 | {
364 | name: "browsercat-mcp-server",
365 | version: "0.1.0",
366 | },
367 | {
368 | capabilities: {
369 | resources: {},
370 | tools: {},
371 | },
372 | },
373 | );
374 |
375 | // Setup request handlers for MCP protocol
376 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
377 | resources: [
378 | {
379 | uri: "console://logs",
380 | mimeType: "text/plain",
381 | name: "Browser console logs",
382 | },
383 | ...Array.from(screenshots.keys()).map(name => ({
384 | uri: `screenshot://${name}`,
385 | mimeType: "image/png",
386 | name: `Screenshot: ${name}`,
387 | })),
388 | ],
389 | }));
390 |
391 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
392 | const uri = request.params.uri.toString();
393 |
394 | if (uri === "console://logs") {
395 | return {
396 | contents: [{
397 | uri,
398 | mimeType: "text/plain",
399 | text: consoleLogs.join("\n"),
400 | }],
401 | };
402 | }
403 |
404 | if (uri.startsWith("screenshot://")) {
405 | const name = uri.split("://")[1];
406 | const screenshot = screenshots.get(name);
407 | if (screenshot) {
408 | return {
409 | contents: [{
410 | uri,
411 | mimeType: "image/png",
412 | blob: screenshot,
413 | }],
414 | };
415 | }
416 | }
417 |
418 | throw new Error(`Resource not found: ${uri}`);
419 | });
420 |
421 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
422 | tools: TOOLS,
423 | }));
424 |
425 | server.setRequestHandler(CallToolRequestSchema, async (request) =>
426 | handleToolCall(request.params.name, request.params.arguments ?? {})
427 | );
428 |
429 | /**
430 | * Main function to run the MCP server
431 | */
432 | async function runServer() {
433 | const transport = new StdioServerTransport();
434 | await server.connect(transport);
435 | }
436 |
437 | runServer().catch(console.error);
438 |
439 | // Clean up when process ends
440 | process.stdin.on("close", () => {
441 | console.error("BrowserCat MCP Server closed");
442 | server.close();
443 | });
444 |
```