MCP Puppeteer Linux Server
#!/usr/bin/env node
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 { platform } from 'os';
import { execSync } from 'child_process';
// Function to detect X11 environment
function getX11Environment() {
if (platform() !== 'linux') return process.env;
try {
// Try to get the current user
const username = execSync('whoami').toString().trim();
// First, check for KDE's kwin_x11
let psOutput = execSync(`ps aux | grep -i "kwin_x11" | grep "${username}"`).toString();
let lines = psOutput.split('\n').filter(line => line && !line.includes('grep'));
// If no kwin_x11, look for any X session
if (!lines.length) {
psOutput = execSync(`ps aux | grep -i "x.*session" | grep "${username}"`).toString();
lines = psOutput.split('\n').filter(line => line && !line.includes('grep'));
}
// If still nothing, check for Xorg itself
if (!lines.length) {
psOutput = execSync('ps aux | grep "/usr/lib/Xorg"').toString();
lines = psOutput.split('\n').filter(line => line && !line.includes('grep'));
}
// Get the process ID of the first matching line
const pid = lines[0]?.split(/\s+/)[1];
if (!pid) {
// Fallback to defaults if we can't find a process
return {
...process.env,
DISPLAY: ':0',
XAUTHORITY: `/run/user/${process.getuid()}/gdm/Xauthority`
};
}
// Read environment from the process
const envOutput = execSync(`tr '\\0' '\\n' < /proc/${pid}/environ`).toString();
const env = { ...process.env };
// Parse all environment variables
envOutput.split('\n').forEach(line => {
if (line) {
const [key, ...valueParts] = line.split('=');
const value = valueParts.join('=');
env[key] = value;
}
});
// Ensure critical X11 variables exist
if (!env.DISPLAY) {
env.DISPLAY = ':0';
}
// Try to find XAUTHORITY if it's not set
if (!env.XAUTHORITY) {
try {
const xauthLocations = [
`/run/user/${process.getuid()}/gdm/Xauthority`,
`${process.env.HOME}/.Xauthority`,
'/tmp/.docker.xauth',
...execSync('find /tmp -maxdepth 1 -name "xauth*" 2>/dev/null').toString().split('\n')
];
for (const loc of xauthLocations) {
if (loc && execSync(`test -f "${loc}" && echo "exists"`).toString().includes('exists')) {
env.XAUTHORITY = loc;
break;
}
}
} catch (error) {
console.error('Error finding XAUTHORITY:', error);
}
}
return env;
} catch (error) {
console.error('Failed to get X11 environment:', error);
return {
...process.env,
DISPLAY: ':0'
};
}
}
// Get environment once at startup
const processEnvironment = getX11Environment();
console.error('X11 Environment:', {
DISPLAY: processEnvironment.DISPLAY,
XAUTHORITY: processEnvironment.XAUTHORITY
});
// Define the tools once to avoid repetition
const TOOLS = [
{
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
let browser;
let page;
const consoleLogs = [];
const screenshots = new Map();
async function ensureBrowser() {
if (!browser) {
browser = await puppeteer.launch({
headless: false,
env: processEnvironment // Pass the X11 environment variables
});
const pages = await browser.pages();
page = pages[0];
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
server.notification({
method: "notifications/resources/updated",
params: { uri: "console://logs" },
});
});
}
return page;
}
async function handleToolCall(name, args) {
const page = await ensureBrowser();
switch (name) {
case "puppeteer_navigate":
await page.goto(args.url);
return {
content: [{
type: "text",
text: `Navigated to ${args.url}`,
}],
isError: false,
};
case "puppeteer_screenshot": {
const width = args.width ?? 800;
const height = args.height ?? 600;
await page.setViewport({ width, height });
const screenshot = await (args.selector ?
(await page.$(args.selector))?.screenshot({ encoding: "base64" }) :
page.screenshot({ encoding: "base64", fullPage: false }));
if (!screenshot) {
return {
content: [{
type: "text",
text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
}],
isError: true,
};
}
screenshots.set(args.name, screenshot);
server.notification({
method: "notifications/resources/list_changed",
});
return {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken at ${width}x${height}`,
},
{
type: "image",
data: screenshot,
mimeType: "image/png",
},
],
isError: false,
};
}
case "puppeteer_click":
try {
await page.click(args.selector);
return {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "puppeteer_fill":
try {
await page.waitForSelector(args.selector);
await page.type(args.selector, args.value);
return {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to fill ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "puppeteer_select":
try {
await page.waitForSelector(args.selector);
await page.select(args.selector, args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "puppeteer_hover":
try {
await page.waitForSelector(args.selector);
await page.hover(args.selector);
return {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "puppeteer_evaluate":
try {
const result = await page.evaluate((script) => {
const logs = [];
const originalConsole = { ...console };
['log', 'info', 'warn', 'error'].forEach(method => {
console[method] = (...args) => {
logs.push(`[${method}] ${args.join(' ')}`);
originalConsole[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 {
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 {
content: [{
type: "text",
text: `Script execution failed: ${error.message}`,
}],
isError: true,
};
}
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
const server = new Server({
name: "example-servers/puppeteer",
version: "0.1.0",
}, {
capabilities: {
resources: {},
tools: {},
},
});
// Setup request handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(screenshots.keys()).map(name => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
return {
contents: [{
uri,
mimeType: "text/plain",
text: consoleLogs.join("\n"),
}],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = screenshots.get(name);
if (screenshot) {
return {
contents: [{
uri,
mimeType: "image/png",
blob: screenshot,
}],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => handleToolCall(request.params.name, request.params.arguments ?? {}));
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);