# 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 | 
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 | 
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 |
```