Skip to content

Commit 4f93f82

Browse files
committed
reviewed: fix: detached frame error & feature: Puppeteer launch arguments support
1 parent 9735df6 commit 4f93f82

File tree

2 files changed

+120
-13
lines changed

2 files changed

+120
-13
lines changed

src/puppeteer/README.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ A Model Context Protocol server that provides browser automation capabilities us
88

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

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

6569
## Configuration to use Puppeteer Server
6670
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:
9397
}
9498
```
9599

100+
### Launch Options
101+
102+
You can customize Puppeteer's browser behavior in two ways:
103+
104+
1. **Environment Variable**: Set `PUPPETEER_LAUNCH_OPTIONS` with a JSON-encoded string in the MCP configuration's `env` parameter:
105+
106+
```json
107+
{
108+
"mcpServers": {
109+
"mcp-puppeteer": {
110+
"command": "npx",
111+
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
112+
"env": {
113+
"PUPPETEER_LAUNCH_OPTIONS": "{ \"headless\": false, \"executablePath\": \"C:/Program Files/Google/Chrome/Application/chrome.exe\", \"args\": [] }",
114+
"ALLOW_DANGEROUS": "true"
115+
}
116+
}
117+
}
118+
}
119+
```
120+
121+
2. **Tool Call Arguments**: Pass `launchOptions` and `allowDangerous` parameters to the `puppeteer_navigate` tool:
122+
123+
```json
124+
{
125+
"url": "https://example.com",
126+
"launchOptions": {
127+
"headless": false,
128+
"defaultViewport": {"width": 1280, "height": 720}
129+
}
130+
}
131+
```
132+
96133
## Build
97134

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

104141
## License
105142

106-
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.
143+
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.

src/puppeteer/index.ts

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ const TOOLS: Tool[] = [
2222
inputSchema: {
2323
type: "object",
2424
properties: {
25-
url: { type: "string" },
25+
url: { type: "string", description: "URL to navigate to" },
26+
launchOptions: { type: "object", description: "PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: { headless: true, args: ['--no-sandbox'] }" },
27+
allowDangerous: { type: "boolean", description: "Allow dangerous LaunchOptions that reduce security. When false, dangerous args like --no-sandbox will throw errors. Default false." },
2628
},
2729
required: ["url"],
2830
},
@@ -101,16 +103,65 @@ const TOOLS: Tool[] = [
101103
];
102104

103105
// Global state
104-
let browser: Browser | undefined;
105-
let page: Page | undefined;
106+
let browser: Browser | null;
107+
let page: Page | null;
106108
const consoleLogs: string[] = [];
107109
const screenshots = new Map<string, string>();
110+
let previousLaunchOptions: any = null;
111+
112+
async function ensureBrowser({ launchOptions, allowDangerous }: any) {
113+
114+
const DANGEROUS_ARGS = [
115+
'--no-sandbox',
116+
'--disable-setuid-sandbox',
117+
'--single-process',
118+
'--disable-web-security',
119+
'--ignore-certificate-errors',
120+
'--disable-features=IsolateOrigins',
121+
'--disable-site-isolation-trials',
122+
'--allow-running-insecure-content'
123+
];
124+
125+
// Parse environment config safely
126+
let envConfig = {};
127+
try {
128+
envConfig = JSON.parse(process.env.PUPPETEER_LAUNCH_OPTIONS || '{}');
129+
} catch (error: any) {
130+
console.warn('Failed to parse PUPPETEER_LAUNCH_OPTIONS:', error?.message || error);
131+
}
132+
133+
// Deep merge environment config with user-provided options
134+
const mergedConfig = deepMerge(envConfig, launchOptions || {});
135+
136+
// Security validation for merged config
137+
if (mergedConfig?.args) {
138+
const dangerousArgs = mergedConfig.args?.filter?.((arg: string) => DANGEROUS_ARGS.some((dangerousArg: string) => arg.startsWith(dangerousArg)));
139+
if (dangerousArgs?.length > 0 && !(allowDangerous || (process.env.ALLOW_DANGEROUS === 'true'))) {
140+
throw new Error(`Dangerous browser arguments detected: ${dangerousArgs.join(', ')}. Fround from environment variable and tool call argument. ` +
141+
'Set allowDangerous: true in the tool call arguments to override.');
142+
}
143+
}
144+
145+
try {
146+
if ((browser && !browser.connected) ||
147+
(launchOptions && (JSON.stringify(launchOptions) != JSON.stringify(previousLaunchOptions)))) {
148+
await browser?.close();
149+
browser = null;
150+
}
151+
}
152+
catch (error) {
153+
browser = null;
154+
}
155+
156+
previousLaunchOptions = launchOptions;
108157

109-
async function ensureBrowser() {
110158
if (!browser) {
111159
const npx_args = { headless: false }
112160
const docker_args = { headless: true, args: ["--no-sandbox", "--single-process", "--no-zygote"] }
113-
browser = await puppeteer.launch(process.env.DOCKER_CONTAINER ? docker_args : npx_args);
161+
browser = await puppeteer.launch(deepMerge(
162+
process.env.DOCKER_CONTAINER ? docker_args : npx_args,
163+
mergedConfig
164+
));
114165
const pages = await browser.pages();
115166
page = pages[0];
116167

@@ -126,6 +177,25 @@ async function ensureBrowser() {
126177
return page!;
127178
}
128179

180+
// Deep merge utility function
181+
function deepMerge(target: any, source: any): any {
182+
const output = Object.assign({}, target);
183+
if (typeof target !== 'object' || typeof source !== 'object') return source;
184+
185+
for (const key of Object.keys(source)) {
186+
const targetVal = target[key];
187+
const sourceVal = source[key];
188+
if (Array.isArray(targetVal) && Array.isArray(sourceVal)) {
189+
output[key] = [...targetVal, ...sourceVal];
190+
} else if (sourceVal instanceof Object && key in target) {
191+
output[key] = deepMerge(targetVal, sourceVal);
192+
} else {
193+
output[key] = sourceVal;
194+
}
195+
}
196+
return output;
197+
}
198+
129199
declare global {
130200
interface Window {
131201
mcpHelper: {
@@ -136,7 +206,7 @@ declare global {
136206
}
137207

138208
async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
139-
const page = await ensureBrowser();
209+
const page = await ensureBrowser(args);
140210

141211
switch (name) {
142212
case "puppeteer_navigate":
@@ -285,15 +355,15 @@ async function handleToolCall(name: string, args: any): Promise<CallToolResult>
285355
window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
286356
(window.mcpHelper.originalConsole as any)[method](...args);
287357
};
288-
} );
289-
} );
358+
});
359+
});
290360

291-
const result = await page.evaluate( args.script );
361+
const result = await page.evaluate(args.script);
292362

293363
const logs = await page.evaluate(() => {
294364
Object.assign(console, window.mcpHelper.originalConsole);
295365
const logs = window.mcpHelper.logs;
296-
delete ( window as any).mcpHelper;
366+
delete (window as any).mcpHelper;
297367
return logs;
298368
});
299369

@@ -405,4 +475,4 @@ runServer().catch(console.error);
405475
process.stdin.on("close", () => {
406476
console.error("Puppeteer MCP Server closed");
407477
server.close();
408-
});
478+
});

0 commit comments

Comments
 (0)