Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/puppeteer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ A Model Context Protocol server that provides browser automation capabilities us

- **puppeteer_navigate**
- Navigate to any URL in the browser
- Input: `url` (string)
- Inputs:
- `url` (string, required): URL to navigate to
- `launchOptions` (object, optional): PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: `{ headless: true, args: ['--user-data-dir="C:/Data"'] }`
- `allowDangerous` (boolean, optional): Allow dangerous LaunchOptions that reduce security. When false, dangerous args like `--no-sandbox`, `--disable-web-security` will throw errors. Default false.

- **puppeteer_screenshot**
- Capture screenshots of the entire page or specific elements
Expand Down Expand Up @@ -61,6 +64,7 @@ The server provides access to two types of resources:
- Screenshot capabilities
- JavaScript execution
- Basic web interaction (navigation, clicking, form filling)
- Customizable Puppeteer launch options

## Configuration to use Puppeteer Server
Here's the Claude Desktop configuration to use the Puppeter server:
Expand Down Expand Up @@ -93,6 +97,39 @@ Here's the Claude Desktop configuration to use the Puppeter server:
}
```

### Launch Options

You can customize Puppeteer's browser behavior in two ways:

1. **Environment Variable**: Set `PUPPETEER_LAUNCH_OPTIONS` with a JSON-encoded string in the MCP configuration's `env` parameter:

```json
{
"mcpServers": {
"mcp-puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
"env": {
"PUPPETEER_LAUNCH_OPTIONS": "{ \"headless\": false, \"executablePath\": \"C:/Program Files/Google/Chrome/Application/chrome.exe\", \"args\": [] }",
"ALLOW_DANGEROUS": "true"
}
}
}
}
```

2. **Tool Call Arguments**: Pass `launchOptions` and `allowDangerous` parameters to the `puppeteer_navigate` tool:

```json
{
"url": "https://example.com",
"launchOptions": {
"headless": false,
"defaultViewport": {"width": 1280, "height": 720}
}
}
```

## Build

Docker build:
Expand All @@ -103,4 +140,4 @@ docker build -t mcp/puppeteer -f src/puppeteer/Dockerfile .

## License

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.
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.
98 changes: 87 additions & 11 deletions src/puppeteer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const TOOLS: Tool[] = [
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
url: { type: "string", description: "URL to navigate to" },
launchOptions: { type: "object", description: "PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: { headless: true, args: ['--no-sandbox'] }" },
allowDangerous: { type: "boolean", description: "Allow dangerous LaunchOptions that reduce security. When false, dangerous args like --no-sandbox will throw errors. Default false." },
},
required: ["url"],
},
Expand Down Expand Up @@ -101,16 +103,65 @@ const TOOLS: Tool[] = [
];

// Global state
let browser: Browser | undefined;
let page: Page | undefined;
let browser: Browser | null;
let page: Page | null;
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
let previousLaunchOptions: any = null;

async function ensureBrowser({ launchOptions, allowDangerous }: any) {

const DANGEROUS_ARGS = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--single-process',
'--disable-web-security',
'--ignore-certificate-errors',
'--disable-features=IsolateOrigins',
'--disable-site-isolation-trials',
'--allow-running-insecure-content'
];

// Parse environment config safely
let envConfig = {};
try {
envConfig = JSON.parse(process.env.PUPPETEER_LAUNCH_OPTIONS || '{}');
} catch (error: any) {
console.warn('Failed to parse PUPPETEER_LAUNCH_OPTIONS:', error?.message || error);
}

// Deep merge environment config with user-provided options
const mergedConfig = deepMerge(envConfig, launchOptions || {});

// Security validation for merged config
if (mergedConfig?.args) {
const dangerousArgs = mergedConfig.args?.filter?.((arg: string) => DANGEROUS_ARGS.some((dangerousArg: string) => arg.startsWith(dangerousArg)));
if (dangerousArgs?.length > 0 && !(allowDangerous || (process.env.ALLOW_DANGEROUS === 'true'))) {
throw new Error(`Dangerous browser arguments detected: ${dangerousArgs.join(', ')}. Fround from environment variable and tool call argument. ` +
'Set allowDangerous: true in the tool call arguments to override.');
}
}

try {
if ((browser && !browser.connected) ||
(launchOptions && (JSON.stringify(launchOptions) != JSON.stringify(previousLaunchOptions)))) {
await browser?.close();
browser = null;
}
}
catch (error) {
browser = null;
}

previousLaunchOptions = launchOptions;

async function ensureBrowser() {
if (!browser) {
const npx_args = { headless: false }
const docker_args = { headless: true, args: ["--no-sandbox", "--single-process", "--no-zygote"] }
browser = await puppeteer.launch(process.env.DOCKER_CONTAINER ? docker_args : npx_args);
browser = await puppeteer.launch(deepMerge(
process.env.DOCKER_CONTAINER ? docker_args : npx_args,
mergedConfig
));
const pages = await browser.pages();
page = pages[0];

Expand All @@ -126,6 +177,31 @@ async function ensureBrowser() {
return page!;
}

// Deep merge utility function
function deepMerge(target: any, source: any): any {
const output = Object.assign({}, target);
if (typeof target !== 'object' || typeof source !== 'object') return source;

for (const key of Object.keys(source)) {
const targetVal = target[key];
const sourceVal = source[key];
if (Array.isArray(targetVal) && Array.isArray(sourceVal)) {
// Deduplicate args/ignoreDefaultArgs, prefer source values
output[key] = [...new Set([
...(key === 'args' || key === 'ignoreDefaultArgs' ?
targetVal.filter((arg: string) => !sourceVal.some((launchArg: string) => arg.startsWith('--') && launchArg.startsWith(arg.split('=')[0]))) :
targetVal),
...sourceVal
])];
} else if (sourceVal instanceof Object && key in target) {
output[key] = deepMerge(targetVal, sourceVal);
} else {
output[key] = sourceVal;
}
}
return output;
}

declare global {
interface Window {
mcpHelper: {
Expand All @@ -136,7 +212,7 @@ declare global {
}

async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
const page = await ensureBrowser();
const page = await ensureBrowser(args);

switch (name) {
case "puppeteer_navigate":
Expand Down Expand Up @@ -285,15 +361,15 @@ async function handleToolCall(name: string, args: any): Promise<CallToolResult>
window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
(window.mcpHelper.originalConsole as any)[method](...args);
};
} );
} );
});
});

const result = await page.evaluate( args.script );
const result = await page.evaluate(args.script);

const logs = await page.evaluate(() => {
Object.assign(console, window.mcpHelper.originalConsole);
const logs = window.mcpHelper.logs;
delete ( window as any).mcpHelper;
delete (window as any).mcpHelper;
return logs;
});

Expand Down Expand Up @@ -405,4 +481,4 @@ runServer().catch(console.error);
process.stdin.on("close", () => {
console.error("Puppeteer MCP Server closed");
server.close();
});
});