@mrtkrcm/mcp-puppeteer
Version:
Model Context Protocol server for browser automation using Puppeteer
675 lines • 31.2 kB
JavaScript
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