reviewed: fix: detached frame error & feature: Puppeteer launch arguments support

This commit is contained in:
AB498
2025-03-30 04:13:59 +06:00
parent 9735df6da7
commit 4f93f82009
2 changed files with 120 additions and 13 deletions

View File

@@ -8,7 +8,10 @@ A Model Context Protocol server that provides browser automation capabilities us
- **puppeteer_navigate** - **puppeteer_navigate**
- Navigate to any URL in the browser - 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** - **puppeteer_screenshot**
- Capture screenshots of the entire page or specific elements - Capture screenshots of the entire page or specific elements
@@ -61,6 +64,7 @@ The server provides access to two types of resources:
- Screenshot capabilities - Screenshot capabilities
- JavaScript execution - JavaScript execution
- Basic web interaction (navigation, clicking, form filling) - Basic web interaction (navigation, clicking, form filling)
- Customizable Puppeteer launch options
## Configuration to use Puppeteer Server ## Configuration to use Puppeteer Server
Here's the Claude Desktop configuration to use the Puppeter server: Here's the Claude Desktop configuration to use the Puppeter server:
@@ -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 ## Build
Docker build: Docker build:

View File

@@ -22,7 +22,9 @@ const TOOLS: Tool[] = [
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { 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"], required: ["url"],
}, },
@@ -101,16 +103,65 @@ const TOOLS: Tool[] = [
]; ];
// Global state // Global state
let browser: Browser | undefined; let browser: Browser | null;
let page: Page | undefined; let page: Page | null;
const consoleLogs: string[] = []; const consoleLogs: string[] = [];
const screenshots = new Map<string, 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) { if (!browser) {
const npx_args = { headless: false } const npx_args = { headless: false }
const docker_args = { headless: true, args: ["--no-sandbox", "--single-process", "--no-zygote"] } 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(); const pages = await browser.pages();
page = pages[0]; page = pages[0];
@@ -126,6 +177,25 @@ async function ensureBrowser() {
return page!; 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)) {
output[key] = [...targetVal, ...sourceVal];
} else if (sourceVal instanceof Object && key in target) {
output[key] = deepMerge(targetVal, sourceVal);
} else {
output[key] = sourceVal;
}
}
return output;
}
declare global { declare global {
interface Window { interface Window {
mcpHelper: { mcpHelper: {
@@ -136,7 +206,7 @@ declare global {
} }
async function handleToolCall(name: string, args: any): Promise<CallToolResult> { async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
const page = await ensureBrowser(); const page = await ensureBrowser(args);
switch (name) { switch (name) {
case "puppeteer_navigate": case "puppeteer_navigate":
@@ -285,15 +355,15 @@ async function handleToolCall(name: string, args: any): Promise<CallToolResult>
window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`); window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
(window.mcpHelper.originalConsole as any)[method](...args); (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(() => { const logs = await page.evaluate(() => {
Object.assign(console, window.mcpHelper.originalConsole); Object.assign(console, window.mcpHelper.originalConsole);
const logs = window.mcpHelper.logs; const logs = window.mcpHelper.logs;
delete ( window as any).mcpHelper; delete (window as any).mcpHelper;
return logs; return logs;
}); });