UNPKG

@mrtkrcm/mcp-puppeteer

Version:

Model Context Protocol server for browser automation using Puppeteer

675 lines 31.2 kB
#!/usr/bin/env node var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import puppeteer from "puppeteer"; import { LRUCache } from "./src/utils/LRUCache.js"; import { getBrowserConfig, isAllowedDomain, sanitizeScript, withTimeout } from "./src/utils/browserConfig.js"; import { connectWithRetry, setupPageErrorHandlers, createPage } from "./src/utils/browserConnection.js"; import { generateAccessibilitySnapshot } from "./src/utils/accessibilitySnapshot.js"; // Constants for resource limits var MAX_SCREENSHOTS = 50; var MAX_CONSOLE_LOGS = 1000; var DEFAULT_TIMEOUT = 30000; // Define the tools once to avoid repetition var TOOLS = [ { name: "browser_snapshot", description: "Capture accessibility snapshot of the current page for better element targeting", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "puppeteer_navigate", description: "Navigate to a URL", inputSchema: { type: "object", properties: { url: { type: "string" }, }, required: ["url"], }, }, { name: "puppeteer_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)" }, }, required: ["name"], }, }, { name: "puppeteer_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: "puppeteer_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: "puppeteer_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: "puppeteer_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: "puppeteer_evaluate", description: "Execute JavaScript in the browser console", inputSchema: { type: "object", properties: { script: { type: "string", description: "JavaScript code to execute" }, }, required: ["script"], }, }, ]; // Global state var browser; var page; var consoleLogs = []; var screenshots = new LRUCache(MAX_SCREENSHOTS); function ensureBrowser() { return __awaiter(this, void 0, void 0, function () { var config, endpoint, error_1, errMsg, launchConfig, error_2, errMsg, error_3, errMsg, error_4, errMsg; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!(!browser || !browser.isConnected())) return [3 /*break*/, 14]; console.error('Attempting to establish browser connection...'); browser = undefined; page = undefined; screenshots.clear(); consoleLogs.length = 0; config = getBrowserConfig(); endpoint = process.env.PUPPETEER_BROWSER_WS_ENDPOINT; if (!endpoint) return [3 /*break*/, 4]; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, connectWithRetry(endpoint)]; case 2: browser = _a.sent(); console.error('Connected to existing browser instance.'); browser.on('disconnected', function () { console.error('Browser disconnected unexpectedly.'); browser = undefined; page = undefined; // Attempt reconnection in background setTimeout(function () { return ensureBrowser().catch(console.error); }, 1000); }); return [3 /*break*/, 4]; case 3: error_1 = _a.sent(); errMsg = error_1 instanceof Error ? error_1.message : String(error_1); console.error("Failed to connect to browser: ".concat(errMsg)); if (process.env.FALLBACK_TO_LOCAL_CHROME === 'false') { throw new Error("Could not connect to browser and fallback is disabled."); } console.error('Connect failed, attempting fallback to local launch...'); return [3 /*break*/, 4]; case 4: if (!!browser) return [3 /*break*/, 8]; _a.label = 5; case 5: _a.trys.push([5, 7, , 8]); console.error('Launching new local browser instance...'); launchConfig = getBrowserConfig(); return [4 /*yield*/, puppeteer.launch(launchConfig)]; case 6: browser = _a.sent(); console.error('Local browser launched.'); browser.on('disconnected', function () { console.error('Local browser instance closed unexpectedly.'); browser = undefined; page = undefined; }); return [3 /*break*/, 8]; case 7: error_2 = _a.sent(); errMsg = error_2 instanceof Error ? error_2.message : String(error_2); console.error("Failed to launch local browser instance: ".concat(errMsg)); throw new Error('Failed to initialize browser session.'); case 8: _a.trys.push([8, 10, , 13]); return [4 /*yield*/, createPage(browser)]; case 9: page = _a.sent(); setupPageErrorHandlers(page, consoleLogs); console.error('Browser page ready.'); return [3 /*break*/, 13]; case 10: error_3 = _a.sent(); errMsg = error_3 instanceof Error ? error_3.message : String(error_3); console.error("Failed to get or configure browser page: ".concat(errMsg)); if (!browser) return [3 /*break*/, 12]; return [4 /*yield*/, browser.close().catch(function (e) { return console.error("Error closing browser after page failure:", e); })]; case 11: _a.sent(); _a.label = 12; case 12: browser = undefined; throw new Error("Failed to initialize browser page."); case 13: return [3 /*break*/, 18]; case 14: if (!(!page || page.isClosed())) return [3 /*break*/, 18]; console.error('Page was closed or invalid, opening a new one...'); _a.label = 15; case 15: _a.trys.push([15, 17, , 18]); return [4 /*yield*/, createPage(browser)]; case 16: page = _a.sent(); setupPageErrorHandlers(page, consoleLogs); console.error('New browser page ready.'); return [3 /*break*/, 18]; case 17: error_4 = _a.sent(); errMsg = error_4 instanceof Error ? error_4.message : String(error_4); console.error("Failed to open new page: ".concat(errMsg)); page = undefined; throw new Error("Failed to open new browser page."); case 18: if (!page || page.isClosed()) { throw new Error("Failed to obtain a valid browser page."); } return [2 /*return*/, page]; } }); }); } // Define handleToolCall function properly function handleToolCall(name, args) { return __awaiter(this, void 0, void 0, function () { var page, _a, snapshot, pageUrl, pageTitle, lines, error_5, errMsg, url, error_6, errMsg, width, height, screenshotPromise, screenshot, error_7, errMsg, script, result, error_8, errMsg, error_9, error_10, error_11, error_12; var _this = this; var _b, _c; return __generator(this, function (_d) { switch (_d.label) { case 0: return [4 /*yield*/, ensureBrowser()]; case 1: page = _d.sent(); _a = name; switch (_a) { case "browser_snapshot": return [3 /*break*/, 2]; case "puppeteer_navigate": return [3 /*break*/, 6]; case "puppeteer_screenshot": return [3 /*break*/, 10]; case "puppeteer_evaluate": return [3 /*break*/, 15]; case "puppeteer_click": return [3 /*break*/, 18]; case "puppeteer_fill": return [3 /*break*/, 21]; case "puppeteer_select": return [3 /*break*/, 25]; case "puppeteer_hover": return [3 /*break*/, 29]; } return [3 /*break*/, 33]; case 2: _d.trys.push([2, 5, , 6]); return [4 /*yield*/, generateAccessibilitySnapshot(page)]; case 3: snapshot = _d.sent(); pageUrl = page.url(); return [4 /*yield*/, page.title()]; case 4: pageTitle = _d.sent(); lines = [ "- Page URL: ".concat(pageUrl), "- Page Title: ".concat(pageTitle), "- Page Snapshot", '```yaml', snapshot, '```', ]; return [2 /*return*/, { content: [{ type: "text", text: lines.join('\n') }], isError: false, }]; case 5: error_5 = _d.sent(); errMsg = error_5 instanceof Error ? error_5.message : String(error_5); return [2 /*return*/, { content: [{ type: "text", text: "Failed to capture accessibility snapshot: ".concat(errMsg) }], isError: true, }]; case 6: url = args.url; if (!isAllowedDomain(url)) { return [2 /*return*/, { content: [{ type: "text", text: "Navigation to ".concat(url, " is not allowed") }], isError: true, }]; } _d.label = 7; case 7: _d.trys.push([7, 9, , 10]); return [4 /*yield*/, withTimeout(page.goto(url), DEFAULT_TIMEOUT, "Navigation to ".concat(url))]; case 8: _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Navigated to ".concat(url) }], isError: false, }]; case 9: error_6 = _d.sent(); errMsg = error_6 instanceof Error ? error_6.message : String(error_6); return [2 /*return*/, { content: [{ type: "text", text: "Failed to navigate to ".concat(url, ": ").concat(errMsg) }], isError: true, }]; case 10: width = (_b = args.width) !== null && _b !== void 0 ? _b : 800; height = (_c = args.height) !== null && _c !== void 0 ? _c : 600; return [4 /*yield*/, page.setViewport({ width: width, height: height })]; case 11: _d.sent(); _d.label = 12; case 12: _d.trys.push([12, 14, , 15]); screenshotPromise = args.selector ? (function () { return __awaiter(_this, void 0, void 0, function () { var element, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, page.$(args.selector)]; case 1: element = _b.sent(); if (!element) return [3 /*break*/, 3]; return [4 /*yield*/, element.screenshot({ encoding: "base64" })]; case 2: _a = _b.sent(); return [3 /*break*/, 4]; case 3: _a = undefined; _b.label = 4; case 4: return [2 /*return*/, _a]; } }); }); })() : page.screenshot({ encoding: "base64", fullPage: false }); return [4 /*yield*/, withTimeout(screenshotPromise, DEFAULT_TIMEOUT, "Screenshot capture")]; case 13: screenshot = _d.sent(); if (!screenshot) { return [2 /*return*/, { content: [{ type: "text", text: args.selector ? "Element not found: ".concat(args.selector) : "Screenshot failed", }], isError: true, }]; } screenshots.set(args.name, screenshot); try { setTimeout(function () { server.notification({ method: "notifications/resources/list_changed", }); }, 100); } catch (e) { console.error("Failed to send notification:", e); } return [2 /*return*/, { content: [ { type: "text", text: "Screenshot '".concat(args.name, "' taken at ").concat(width, "x").concat(height), }, { type: "image", data: screenshot, mimeType: "image/png", }, ], isError: false, }]; case 14: error_7 = _d.sent(); errMsg = error_7 instanceof Error ? error_7.message : String(error_7); return [2 /*return*/, { content: [{ type: "text", text: "Screenshot capture failed: ".concat(errMsg) }], isError: true, }]; case 15: _d.trys.push([15, 17, , 18]); script = sanitizeScript(args.script); return [4 /*yield*/, withTimeout(page.evaluate(script), DEFAULT_TIMEOUT, "JavaScript evaluation")]; case 16: result = _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Executed script successfully: ".concat(JSON.stringify(result)), }], isError: false, }]; case 17: error_8 = _d.sent(); errMsg = error_8 instanceof Error ? error_8.message : String(error_8); return [2 /*return*/, { content: [{ type: "text", text: "Failed to execute script: ".concat(errMsg) }], isError: true, }]; case 18: _d.trys.push([18, 20, , 21]); return [4 /*yield*/, page.click(args.selector)]; case 19: _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Clicked: ".concat(args.selector), }], isError: false, }]; case 20: error_9 = _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Failed to click ".concat(args.selector, ": ").concat(error_9.message), }], isError: true, }]; case 21: _d.trys.push([21, 24, , 25]); return [4 /*yield*/, page.waitForSelector(args.selector)]; case 22: _d.sent(); return [4 /*yield*/, page.type(args.selector, args.value)]; case 23: _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Filled ".concat(args.selector, " with: ").concat(args.value), }], isError: false, }]; case 24: error_10 = _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Failed to fill ".concat(args.selector, ": ").concat(error_10.message), }], isError: true, }]; case 25: _d.trys.push([25, 28, , 29]); return [4 /*yield*/, page.waitForSelector(args.selector)]; case 26: _d.sent(); return [4 /*yield*/, page.select(args.selector, args.value)]; case 27: _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Selected ".concat(args.selector, " with: ").concat(args.value), }], isError: false, }]; case 28: error_11 = _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Failed to select ".concat(args.selector, ": ").concat(error_11.message), }], isError: true, }]; case 29: _d.trys.push([29, 32, , 33]); return [4 /*yield*/, page.waitForSelector(args.selector)]; case 30: _d.sent(); return [4 /*yield*/, page.hover(args.selector)]; case 31: _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Hovered ".concat(args.selector), }], isError: false, }]; case 32: error_12 = _d.sent(); return [2 /*return*/, { content: [{ type: "text", text: "Failed to hover ".concat(args.selector, ": ").concat(error_12.message), }], isError: true, }]; case 33: throw new Error("Unknown tool: ".concat(name)); } }); }); } // Create server instance var server = new Server({ name: "puppeteer", version: "1.0.0", }, { capabilities: { tools: {}, resources: {}, }, }); // Set up request handlers server.setRequestHandler(ListToolsRequestSchema, function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, ({ tools: TOOLS, })]; }); }); }); server.setRequestHandler(ListResourcesRequestSchema, function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, ({ resources: __spreadArray([ { name: "console://logs", type: "text/plain", } ], Array.from(screenshots.keys()).map(function (name) { return ({ name: "screenshot://".concat(name), type: "image/png", }); }), true), })]; }); }); }); server.setRequestHandler(ReadResourceRequestSchema, function (request) { return __awaiter(void 0, void 0, void 0, function () { var match, screenshot; return __generator(this, function (_a) { if (typeof request.params.name === 'string' && request.params.name === "console://logs") { return [2 /*return*/, { content: [ { type: "text", text: consoleLogs.join("\n"), }, ], }]; } if (typeof request.params.name === 'string') { match = request.params.name.match(/^screenshot:\/\/(.+)$/); if (match) { screenshot = screenshots.get(match[1]); if (screenshot) { return [2 /*return*/, { content: [ { type: "image", data: screenshot, mimeType: "image/png", }, ], }]; } } } throw new Error("Resource not found: ".concat(request.params.name)); }); }); }); server.setRequestHandler(CallToolRequestSchema, function (request) { return __awaiter(void 0, void 0, void 0, function () { var _a; return __generator(this, function (_b) { return [2 /*return*/, handleToolCall(request.params.name, (_a = request.params.arguments) !== null && _a !== void 0 ? _a : {})]; }); }); }); // Graceful shutdown handler function gracefulShutdown() { return __awaiter(this, void 0, void 0, function () { var error_13; return __generator(this, function (_a) { switch (_a.label) { case 0: console.error('\nShutting down server...'); _a.label = 1; case 1: _a.trys.push([1, 4, 5, 7]); if (!(browser === null || browser === void 0 ? void 0 : browser.isConnected())) return [3 /*break*/, 3]; console.error('Closing browser...'); return [4 /*yield*/, browser.close()]; case 2: _a.sent(); console.error('Browser closed.'); _a.label = 3; case 3: return [3 /*break*/, 7]; case 4: error_13 = _a.sent(); console.error('Error closing browser during shutdown:', error_13); return [3 /*break*/, 7]; case 5: return [4 /*yield*/, server.close()]; case 6: _a.sent(); console.error('MCP server closed.'); process.exit(0); return [7 /*endfinally*/]; case 7: return [2 /*return*/]; } }); }); } // Handle signals for graceful shutdown process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); // Catch stdin closing for stdio mode process.stdin.on("close", function () { var isStdioMode = !process.argv.includes('--sse'); if (isStdioMode) { console.error("STDIN closed, initiating shutdown for stdio server."); gracefulShutdown(); } else { console.error("STDIN closed, but not shutting down (SSE mode active)."); } }); // Start the server function runServer() { return __awaiter(this, void 0, void 0, function () { var transport; return __generator(this, function (_a) { switch (_a.label) { case 0: transport = new StdioServerTransport(); return [4 /*yield*/, server.connect(transport)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); } runServer().catch(function (error) { console.error("Server failed to start or encountered a fatal error:", error); process.exit(1); }); //# sourceMappingURL=index.js.map