playwright-stealth-mcp-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -0
- package/build/index.integration-with-mock.js +21 -0
- package/build/index.js +78 -0
- package/package.json +67 -0
- package/shared/index.d.ts +4 -0
- package/shared/index.js +3 -0
- package/shared/logging.d.ts +20 -0
- package/shared/logging.js +34 -0
- package/shared/playwright-client/playwright-client.integration-mock.d.ts +20 -0
- package/shared/playwright-client/playwright-client.integration-mock.js +71 -0
- package/shared/server.d.ts +93 -0
- package/shared/server.js +159 -0
- package/shared/tools.d.ts +4 -0
- package/shared/tools.js +287 -0
- package/shared/types.d.ts +20 -0
- package/shared/types.js +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Playwright Stealth MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for browser automation using Playwright with optional stealth mode to bypass anti-bot protection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Simplified API**: Single `browser_execute` tool that exposes the full Playwright API instead of many specialized tools
|
|
8
|
+
- **Stealth Mode**: Optional anti-detection measures using `playwright-extra` and `puppeteer-extra-plugin-stealth`
|
|
9
|
+
- **Persistent Sessions**: Browser session persists across tool calls for multi-step automation
|
|
10
|
+
- **Screenshot Support**: Capture page screenshots for visual verification
|
|
11
|
+
- **Code Execution**: Run arbitrary Playwright code with access to the `page` object
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx playwright-stealth-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install globally:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g playwright-stealth-mcp-server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
### Claude Desktop Configuration
|
|
28
|
+
|
|
29
|
+
Add to your Claude Desktop config file:
|
|
30
|
+
|
|
31
|
+
**Non-Stealth Mode** (Standard Playwright):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"playwright": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "playwright-stealth-mcp-server"],
|
|
39
|
+
"env": {
|
|
40
|
+
"STEALTH_MODE": "false"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Stealth Mode** (Anti-bot bypass):
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"playwright-stealth": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "playwright-stealth-mcp-server"],
|
|
55
|
+
"env": {
|
|
56
|
+
"STEALTH_MODE": "true"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Environment Variables
|
|
64
|
+
|
|
65
|
+
| Variable | Description | Default |
|
|
66
|
+
| -------------- | ------------------------------------------------ | ------- |
|
|
67
|
+
| `STEALTH_MODE` | Enable stealth mode with anti-detection measures | `false` |
|
|
68
|
+
| `HEADLESS` | Run browser in headless mode | `true` |
|
|
69
|
+
| `TIMEOUT` | Default execution timeout in milliseconds | `30000` |
|
|
70
|
+
|
|
71
|
+
## Available Tools
|
|
72
|
+
|
|
73
|
+
### `browser_execute`
|
|
74
|
+
|
|
75
|
+
Execute Playwright code with access to the `page` object.
|
|
76
|
+
|
|
77
|
+
**Parameters:**
|
|
78
|
+
|
|
79
|
+
- `code` (required): JavaScript code to execute. The `page` object is available in scope.
|
|
80
|
+
- `timeout` (optional): Execution timeout in milliseconds.
|
|
81
|
+
|
|
82
|
+
**Example:**
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
await page.goto('https://example.com');
|
|
86
|
+
const title = await page.title();
|
|
87
|
+
return title;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `browser_screenshot`
|
|
91
|
+
|
|
92
|
+
Take a screenshot of the current page.
|
|
93
|
+
|
|
94
|
+
**Parameters:**
|
|
95
|
+
|
|
96
|
+
- `fullPage` (optional): Capture full scrollable page. Default: `false`
|
|
97
|
+
|
|
98
|
+
### `browser_get_state`
|
|
99
|
+
|
|
100
|
+
Get the current browser state including URL, title, and configuration.
|
|
101
|
+
|
|
102
|
+
### `browser_close`
|
|
103
|
+
|
|
104
|
+
Close the browser session. A new browser will be launched on the next `browser_execute` call.
|
|
105
|
+
|
|
106
|
+
## Usage Examples
|
|
107
|
+
|
|
108
|
+
### Navigate and Extract Data
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// Navigate to a page
|
|
112
|
+
await page.goto('https://news.ycombinator.com');
|
|
113
|
+
|
|
114
|
+
// Extract headlines
|
|
115
|
+
const headlines = await page.$$eval('.titleline > a', (links) =>
|
|
116
|
+
links.slice(0, 5).map((a) => a.textContent)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return headlines;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Fill and Submit a Form
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
await page.goto('https://example.com/login');
|
|
126
|
+
|
|
127
|
+
// Fill credentials
|
|
128
|
+
await page.fill('input[name="email"]', 'user@example.com');
|
|
129
|
+
await page.fill('input[name="password"]', 'password123');
|
|
130
|
+
|
|
131
|
+
// Submit form
|
|
132
|
+
await page.click('button[type="submit"]');
|
|
133
|
+
|
|
134
|
+
// Wait for navigation
|
|
135
|
+
await page.waitForNavigation();
|
|
136
|
+
|
|
137
|
+
return await page.title();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Handle Dynamic Content
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
await page.goto('https://spa-example.com');
|
|
144
|
+
|
|
145
|
+
// Wait for element to appear
|
|
146
|
+
await page.waitForSelector('.loaded-content');
|
|
147
|
+
|
|
148
|
+
// Click to load more
|
|
149
|
+
await page.click('.load-more-button');
|
|
150
|
+
|
|
151
|
+
// Wait for new content
|
|
152
|
+
await page.waitForSelector('.new-items');
|
|
153
|
+
|
|
154
|
+
const items = await page.$$eval('.item', (els) => els.map((el) => el.textContent));
|
|
155
|
+
return items;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Security Considerations
|
|
159
|
+
|
|
160
|
+
**Important:** The `browser_execute` tool executes arbitrary JavaScript code. This design provides full Playwright API access but has security implications:
|
|
161
|
+
|
|
162
|
+
- Only use this server with trusted input (e.g., from an LLM in a controlled environment)
|
|
163
|
+
- The code runs in a Node.js context with access to the `page` object
|
|
164
|
+
- Do not expose this server to untrusted users or public networks
|
|
165
|
+
|
|
166
|
+
This is intentional for maximum flexibility - it allows LLMs to leverage their existing Playwright knowledge. For production use, ensure proper access controls are in place.
|
|
167
|
+
|
|
168
|
+
## When to Use Stealth Mode
|
|
169
|
+
|
|
170
|
+
Enable stealth mode (`STEALTH_MODE=true`) when:
|
|
171
|
+
|
|
172
|
+
- Accessing sites with Cloudflare protection
|
|
173
|
+
- Sites that block automation tools
|
|
174
|
+
- Pages that detect headless browsers
|
|
175
|
+
- OAuth flows that trigger CAPTCHA challenges
|
|
176
|
+
|
|
177
|
+
Stealth mode includes:
|
|
178
|
+
|
|
179
|
+
- WebDriver property masking
|
|
180
|
+
- Chrome automation flag removal
|
|
181
|
+
- User-Agent normalization
|
|
182
|
+
- Plugin/mime type spoofing
|
|
183
|
+
- Navigator property patching
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Install dependencies
|
|
189
|
+
npm run install-all
|
|
190
|
+
|
|
191
|
+
# Build the project
|
|
192
|
+
npm run build
|
|
193
|
+
|
|
194
|
+
# Run in development mode
|
|
195
|
+
npm run dev
|
|
196
|
+
|
|
197
|
+
# Run tests
|
|
198
|
+
npm test
|
|
199
|
+
|
|
200
|
+
# Run integration tests
|
|
201
|
+
npm run test:integration
|
|
202
|
+
|
|
203
|
+
# Run manual tests (requires Playwright browsers)
|
|
204
|
+
npm run test:manual:setup
|
|
205
|
+
npm run test:manual
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Architecture
|
|
209
|
+
|
|
210
|
+
This MCP server uses a simplified design inspired by [playwriter](https://github.com/remorses/playwriter):
|
|
211
|
+
|
|
212
|
+
- Single `browser_execute` tool instead of many specialized tools
|
|
213
|
+
- Reduces context window usage for LLMs
|
|
214
|
+
- Leverages existing Playwright knowledge in LLM training data
|
|
215
|
+
- Full API access without artificial constraints
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Integration test entry point with mock client
|
|
4
|
+
* Used for testing the MCP server without launching a real browser
|
|
5
|
+
*/
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { createMCPServer } from '../shared/index.js';
|
|
8
|
+
import { logServerStart, logError } from '../shared/logging.js';
|
|
9
|
+
import { createMockPlaywrightClient } from '../shared/playwright-client/playwright-client.integration-mock.js';
|
|
10
|
+
async function main() {
|
|
11
|
+
const { server, registerHandlers } = createMCPServer();
|
|
12
|
+
// Use mock client factory for integration tests
|
|
13
|
+
await registerHandlers(server, createMockPlaywrightClient);
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
15
|
+
await server.connect(transport);
|
|
16
|
+
logServerStart('Playwright (Integration Mock)');
|
|
17
|
+
}
|
|
18
|
+
main().catch((error) => {
|
|
19
|
+
logError('main', error);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
package/build/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createMCPServer } from '../shared/index.js';
|
|
4
|
+
import { logServerStart, logError, logWarning } from '../shared/logging.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// ENVIRONMENT VALIDATION
|
|
7
|
+
// =============================================================================
|
|
8
|
+
function validateEnvironment() {
|
|
9
|
+
const optional = [
|
|
10
|
+
{
|
|
11
|
+
name: 'STEALTH_MODE',
|
|
12
|
+
description: 'Enable stealth mode to bypass anti-bot protection (true/false)',
|
|
13
|
+
defaultValue: 'false',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'HEADLESS',
|
|
17
|
+
description: 'Run browser in headless mode (true/false)',
|
|
18
|
+
defaultValue: 'true',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'TIMEOUT',
|
|
22
|
+
description: 'Default execution timeout in milliseconds',
|
|
23
|
+
defaultValue: '30000',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
// Log configuration
|
|
27
|
+
const stealthMode = process.env.STEALTH_MODE === 'true';
|
|
28
|
+
const headless = process.env.HEADLESS !== 'false';
|
|
29
|
+
const timeout = process.env.TIMEOUT || '30000';
|
|
30
|
+
if (stealthMode) {
|
|
31
|
+
logWarning('config', 'Stealth mode enabled - using anti-detection measures');
|
|
32
|
+
}
|
|
33
|
+
if (!headless) {
|
|
34
|
+
logWarning('config', 'Running in non-headless mode - browser window will be visible');
|
|
35
|
+
}
|
|
36
|
+
if (process.env.TIMEOUT) {
|
|
37
|
+
logWarning('config', `Custom timeout configured: ${timeout}ms`);
|
|
38
|
+
}
|
|
39
|
+
// Show optional configuration if DEBUG is set
|
|
40
|
+
if (process.env.DEBUG) {
|
|
41
|
+
console.error('\nOptional environment variables:');
|
|
42
|
+
optional.forEach(({ name, description, defaultValue }) => {
|
|
43
|
+
const current = process.env[name] || defaultValue;
|
|
44
|
+
console.error(` - ${name}: ${description}`);
|
|
45
|
+
console.error(` Current: ${current}`);
|
|
46
|
+
});
|
|
47
|
+
console.error('');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// MAIN ENTRY POINT
|
|
52
|
+
// =============================================================================
|
|
53
|
+
async function main() {
|
|
54
|
+
// Step 1: Validate environment variables
|
|
55
|
+
validateEnvironment();
|
|
56
|
+
// Step 2: Create server using factory
|
|
57
|
+
const { server, registerHandlers, cleanup } = createMCPServer();
|
|
58
|
+
// Step 3: Register all handlers (tools)
|
|
59
|
+
await registerHandlers(server);
|
|
60
|
+
// Step 4: Set up graceful shutdown
|
|
61
|
+
const handleShutdown = async () => {
|
|
62
|
+
logWarning('shutdown', 'Received shutdown signal, closing browser...');
|
|
63
|
+
await cleanup();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
};
|
|
66
|
+
process.on('SIGINT', handleShutdown);
|
|
67
|
+
process.on('SIGTERM', handleShutdown);
|
|
68
|
+
// Step 5: Start server with stdio transport
|
|
69
|
+
const transport = new StdioServerTransport();
|
|
70
|
+
await server.connect(transport);
|
|
71
|
+
const stealthMode = process.env.STEALTH_MODE === 'true';
|
|
72
|
+
logServerStart(`Playwright${stealthMode ? ' (Stealth)' : ''}`);
|
|
73
|
+
}
|
|
74
|
+
// Run the server
|
|
75
|
+
main().catch((error) => {
|
|
76
|
+
logError('main', error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playwright-stealth-mcp-server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Local implementation of Playwright Stealth MCP server",
|
|
5
|
+
"mcpName": "com.pulsemcp.servers/playwright-stealth",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"playwright-stealth-mcp-server": "./build/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && npm run build:integration",
|
|
13
|
+
"build:integration": "tsc -p tsconfig.integration.json",
|
|
14
|
+
"start": "node build/index.js",
|
|
15
|
+
"start:integration": "node build/index.integration-with-mock.js",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"dev:integration": "tsx src/index.integration-with-mock.ts",
|
|
18
|
+
"predev": "cd ../shared && npm run build && cd ../local && node setup-dev.js",
|
|
19
|
+
"prebuild": "cd ../shared && npm run build && cd ../local && node setup-dev.js",
|
|
20
|
+
"prepublishOnly": "node prepare-publish.js && node ../scripts/prepare-npm-readme.js",
|
|
21
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
22
|
+
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"stage-publish": "npm version"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
29
|
+
"playwright": "^1.49.1",
|
|
30
|
+
"playwright-extra": "^4.3.6",
|
|
31
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
32
|
+
"zod": "^3.24.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.15.31",
|
|
36
|
+
"tsx": "^4.19.4",
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"mcp",
|
|
41
|
+
"modelcontextprotocol",
|
|
42
|
+
"playwright",
|
|
43
|
+
"stealth",
|
|
44
|
+
"browser",
|
|
45
|
+
"automation"
|
|
46
|
+
],
|
|
47
|
+
"author": "PulseMCP",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"build/**/*.js",
|
|
54
|
+
"shared/**/*.js",
|
|
55
|
+
"shared/**/*.d.ts",
|
|
56
|
+
"README.md"
|
|
57
|
+
],
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/pulsemcp/mcp-servers.git",
|
|
61
|
+
"directory": "experimental/playwright-stealth/local"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/pulsemcp/mcp-servers/issues"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://github.com/pulsemcp/mcp-servers/tree/main/experimental/playwright-stealth"
|
|
67
|
+
}
|
package/shared/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utilities for consistent output across MCP servers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Log server startup message
|
|
6
|
+
*/
|
|
7
|
+
export declare function logServerStart(serverName: string, transport?: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Log an error with context
|
|
10
|
+
*/
|
|
11
|
+
export declare function logError(context: string, error: unknown): void;
|
|
12
|
+
/**
|
|
13
|
+
* Log a warning
|
|
14
|
+
*/
|
|
15
|
+
export declare function logWarning(context: string, message: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Log debug information (only in development)
|
|
18
|
+
*/
|
|
19
|
+
export declare function logDebug(context: string, message: string): void;
|
|
20
|
+
//# sourceMappingURL=logging.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utilities for consistent output across MCP servers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Log server startup message
|
|
6
|
+
*/
|
|
7
|
+
export function logServerStart(serverName, transport = 'stdio') {
|
|
8
|
+
console.error(`MCP server ${serverName} running on ${transport}`);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Log an error with context
|
|
12
|
+
*/
|
|
13
|
+
export function logError(context, error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
16
|
+
console.error(`[ERROR] ${context}: ${message}`);
|
|
17
|
+
if (stack) {
|
|
18
|
+
console.error(stack);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Log a warning
|
|
23
|
+
*/
|
|
24
|
+
export function logWarning(context, message) {
|
|
25
|
+
console.error(`[WARN] ${context}: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Log debug information (only in development)
|
|
29
|
+
*/
|
|
30
|
+
export function logDebug(context, message) {
|
|
31
|
+
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
|
32
|
+
console.error(`[DEBUG] ${context}: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Playwright client for integration tests
|
|
3
|
+
* Simulates browser behavior without launching a real browser
|
|
4
|
+
*/
|
|
5
|
+
import type { IPlaywrightClient } from '../server.js';
|
|
6
|
+
import type { ExecuteResult, BrowserState, PlaywrightConfig } from '../types.js';
|
|
7
|
+
export declare class MockPlaywrightClient implements IPlaywrightClient {
|
|
8
|
+
private state;
|
|
9
|
+
private config;
|
|
10
|
+
constructor(config: PlaywrightConfig);
|
|
11
|
+
execute(code: string, options?: {
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}): Promise<ExecuteResult>;
|
|
14
|
+
screenshot(): Promise<string>;
|
|
15
|
+
getState(): Promise<BrowserState>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
getConfig(): PlaywrightConfig;
|
|
18
|
+
}
|
|
19
|
+
export declare function createMockPlaywrightClient(): IPlaywrightClient;
|
|
20
|
+
//# sourceMappingURL=playwright-client.integration-mock.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class MockPlaywrightClient {
|
|
2
|
+
state = { isOpen: false };
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async execute(code, options) {
|
|
8
|
+
// Simulate browser being opened
|
|
9
|
+
this.state.isOpen = true;
|
|
10
|
+
// Simple mock responses based on code content
|
|
11
|
+
if (code.includes('page.goto')) {
|
|
12
|
+
const urlMatch = code.match(/goto\(['"`]([^'"`]+)['"`]\)/);
|
|
13
|
+
if (urlMatch) {
|
|
14
|
+
this.state.currentUrl = urlMatch[1];
|
|
15
|
+
this.state.title = `Mock Page - ${urlMatch[1]}`;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
result: undefined,
|
|
20
|
+
consoleOutput: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (code.includes('page.title')) {
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
result: JSON.stringify(this.state.title || 'Mock Title'),
|
|
27
|
+
consoleOutput: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (code.includes('error') || code.includes('throw')) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: 'Mock error for testing',
|
|
34
|
+
consoleOutput: ['[error] Mock error occurred'],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Check for timeout
|
|
38
|
+
if (options?.timeout && options.timeout < 100) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
error: `Execution timed out after ${options.timeout}ms`,
|
|
42
|
+
consoleOutput: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
result: JSON.stringify({ mock: true, code: code.substring(0, 50) }),
|
|
48
|
+
consoleOutput: ['[log] Mock execution completed'],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async screenshot() {
|
|
52
|
+
// Return a minimal valid PNG as base64 (1x1 transparent pixel)
|
|
53
|
+
return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
|
54
|
+
}
|
|
55
|
+
async getState() {
|
|
56
|
+
return { ...this.state };
|
|
57
|
+
}
|
|
58
|
+
async close() {
|
|
59
|
+
this.state = { isOpen: false };
|
|
60
|
+
}
|
|
61
|
+
getConfig() {
|
|
62
|
+
return this.config;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function createMockPlaywrightClient() {
|
|
66
|
+
return new MockPlaywrightClient({
|
|
67
|
+
stealthMode: false,
|
|
68
|
+
headless: true,
|
|
69
|
+
timeout: 30000,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ExecuteResult, BrowserState, PlaywrightConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Playwright client interface
|
|
5
|
+
* Defines all methods for browser automation
|
|
6
|
+
*/
|
|
7
|
+
export interface IPlaywrightClient {
|
|
8
|
+
/**
|
|
9
|
+
* Execute Playwright code in the browser context
|
|
10
|
+
*/
|
|
11
|
+
execute(code: string, options?: {
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}): Promise<ExecuteResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Take a screenshot of the current page
|
|
16
|
+
*/
|
|
17
|
+
screenshot(options?: {
|
|
18
|
+
fullPage?: boolean;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Get the current browser state
|
|
22
|
+
*/
|
|
23
|
+
getState(): Promise<BrowserState>;
|
|
24
|
+
/**
|
|
25
|
+
* Close the browser
|
|
26
|
+
*/
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Get the configuration
|
|
30
|
+
*/
|
|
31
|
+
getConfig(): PlaywrightConfig;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Playwright client implementation with optional stealth mode
|
|
35
|
+
*/
|
|
36
|
+
export declare class PlaywrightClient implements IPlaywrightClient {
|
|
37
|
+
private browser;
|
|
38
|
+
private context;
|
|
39
|
+
private page;
|
|
40
|
+
private consoleMessages;
|
|
41
|
+
private config;
|
|
42
|
+
constructor(config: PlaywrightConfig);
|
|
43
|
+
private ensureBrowser;
|
|
44
|
+
execute(code: string, options?: {
|
|
45
|
+
timeout?: number;
|
|
46
|
+
}): Promise<ExecuteResult>;
|
|
47
|
+
screenshot(options?: {
|
|
48
|
+
fullPage?: boolean;
|
|
49
|
+
}): Promise<string>;
|
|
50
|
+
getState(): Promise<BrowserState>;
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
getConfig(): PlaywrightConfig;
|
|
53
|
+
}
|
|
54
|
+
export type ClientFactory = () => IPlaywrightClient;
|
|
55
|
+
export declare function createMCPServer(): {
|
|
56
|
+
server: Server<{
|
|
57
|
+
method: string;
|
|
58
|
+
params?: {
|
|
59
|
+
[x: string]: unknown;
|
|
60
|
+
_meta?: {
|
|
61
|
+
[x: string]: unknown;
|
|
62
|
+
progressToken?: string | number | undefined;
|
|
63
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
64
|
+
taskId: string;
|
|
65
|
+
} | undefined;
|
|
66
|
+
} | undefined;
|
|
67
|
+
} | undefined;
|
|
68
|
+
}, {
|
|
69
|
+
method: string;
|
|
70
|
+
params?: {
|
|
71
|
+
[x: string]: unknown;
|
|
72
|
+
_meta?: {
|
|
73
|
+
[x: string]: unknown;
|
|
74
|
+
progressToken?: string | number | undefined;
|
|
75
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
76
|
+
taskId: string;
|
|
77
|
+
} | undefined;
|
|
78
|
+
} | undefined;
|
|
79
|
+
} | undefined;
|
|
80
|
+
}, {
|
|
81
|
+
[x: string]: unknown;
|
|
82
|
+
_meta?: {
|
|
83
|
+
[x: string]: unknown;
|
|
84
|
+
progressToken?: string | number | undefined;
|
|
85
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
86
|
+
taskId: string;
|
|
87
|
+
} | undefined;
|
|
88
|
+
} | undefined;
|
|
89
|
+
}>;
|
|
90
|
+
registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
|
|
91
|
+
cleanup: () => Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=server.d.ts.map
|
package/shared/server.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { createRegisterTools } from './tools.js';
|
|
3
|
+
/**
|
|
4
|
+
* Playwright client implementation with optional stealth mode
|
|
5
|
+
*/
|
|
6
|
+
export class PlaywrightClient {
|
|
7
|
+
browser = null;
|
|
8
|
+
context = null;
|
|
9
|
+
page = null;
|
|
10
|
+
consoleMessages = [];
|
|
11
|
+
config;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
async ensureBrowser() {
|
|
16
|
+
if (this.page) {
|
|
17
|
+
return this.page;
|
|
18
|
+
}
|
|
19
|
+
if (this.config.stealthMode) {
|
|
20
|
+
// Use playwright-extra with stealth plugin
|
|
21
|
+
const { chromium } = await import('playwright-extra');
|
|
22
|
+
const StealthPlugin = (await import('puppeteer-extra-plugin-stealth')).default;
|
|
23
|
+
chromium.use(StealthPlugin());
|
|
24
|
+
this.browser = await chromium.launch({
|
|
25
|
+
headless: this.config.headless,
|
|
26
|
+
args: [
|
|
27
|
+
'--disable-blink-features=AutomationControlled',
|
|
28
|
+
'--disable-dev-shm-usage',
|
|
29
|
+
'--no-sandbox',
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Use standard playwright
|
|
35
|
+
const { chromium } = await import('playwright');
|
|
36
|
+
this.browser = await chromium.launch({
|
|
37
|
+
headless: this.config.headless,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
this.context = await this.browser.newContext({
|
|
41
|
+
viewport: { width: 1920, height: 1080 },
|
|
42
|
+
userAgent: this.config.stealthMode
|
|
43
|
+
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
44
|
+
: undefined,
|
|
45
|
+
});
|
|
46
|
+
this.page = await this.context.newPage();
|
|
47
|
+
// Capture console messages
|
|
48
|
+
this.page.on('console', (msg) => {
|
|
49
|
+
this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
|
|
50
|
+
// Keep only last 100 messages
|
|
51
|
+
if (this.consoleMessages.length > 100) {
|
|
52
|
+
this.consoleMessages.shift();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return this.page;
|
|
56
|
+
}
|
|
57
|
+
async execute(code, options) {
|
|
58
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
59
|
+
try {
|
|
60
|
+
const page = await this.ensureBrowser();
|
|
61
|
+
// Clear console messages for this execution
|
|
62
|
+
const startIndex = this.consoleMessages.length;
|
|
63
|
+
// Create the execution context with page available
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
65
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
66
|
+
const fn = new AsyncFunction('page', code);
|
|
67
|
+
// Execute with timeout
|
|
68
|
+
const result = await Promise.race([
|
|
69
|
+
fn(page),
|
|
70
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Execution timed out after ${timeout}ms`)), timeout)),
|
|
71
|
+
]);
|
|
72
|
+
// Get console output from this execution
|
|
73
|
+
const consoleOutput = this.consoleMessages.slice(startIndex);
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
result: result !== undefined ? JSON.stringify(result, null, 2) : undefined,
|
|
77
|
+
consoleOutput,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
consoleOutput: this.consoleMessages.slice(-10),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async screenshot(options) {
|
|
89
|
+
const page = await this.ensureBrowser();
|
|
90
|
+
const buffer = await page.screenshot({
|
|
91
|
+
fullPage: options?.fullPage ?? false,
|
|
92
|
+
type: 'png',
|
|
93
|
+
});
|
|
94
|
+
return buffer.toString('base64');
|
|
95
|
+
}
|
|
96
|
+
async getState() {
|
|
97
|
+
if (!this.page) {
|
|
98
|
+
return { isOpen: false };
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return {
|
|
102
|
+
currentUrl: this.page.url(),
|
|
103
|
+
title: await this.page.title(),
|
|
104
|
+
isOpen: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { isOpen: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async close() {
|
|
112
|
+
if (this.browser) {
|
|
113
|
+
await this.browser.close();
|
|
114
|
+
this.browser = null;
|
|
115
|
+
this.context = null;
|
|
116
|
+
this.page = null;
|
|
117
|
+
this.consoleMessages = [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
getConfig() {
|
|
121
|
+
return this.config;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function createMCPServer() {
|
|
125
|
+
const stealthMode = process.env.STEALTH_MODE === 'true';
|
|
126
|
+
const server = new Server({
|
|
127
|
+
name: 'playwright-stealth-mcp-server',
|
|
128
|
+
version: '0.0.1',
|
|
129
|
+
}, {
|
|
130
|
+
capabilities: {
|
|
131
|
+
tools: {},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Track active client for cleanup
|
|
135
|
+
let activeClient = null;
|
|
136
|
+
const registerHandlers = async (server, clientFactory) => {
|
|
137
|
+
// Use provided factory or create default client
|
|
138
|
+
const factory = clientFactory ||
|
|
139
|
+
(() => {
|
|
140
|
+
const headless = process.env.HEADLESS !== 'false';
|
|
141
|
+
const timeout = parseInt(process.env.TIMEOUT || '30000', 10);
|
|
142
|
+
activeClient = new PlaywrightClient({
|
|
143
|
+
stealthMode,
|
|
144
|
+
headless,
|
|
145
|
+
timeout,
|
|
146
|
+
});
|
|
147
|
+
return activeClient;
|
|
148
|
+
});
|
|
149
|
+
const registerTools = createRegisterTools(factory);
|
|
150
|
+
registerTools(server);
|
|
151
|
+
};
|
|
152
|
+
const cleanup = async () => {
|
|
153
|
+
if (activeClient) {
|
|
154
|
+
await activeClient.close();
|
|
155
|
+
activeClient = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return { server, registerHandlers, cleanup };
|
|
159
|
+
}
|
package/shared/tools.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// TOOL SCHEMAS
|
|
5
|
+
// =============================================================================
|
|
6
|
+
const ExecuteSchema = z.object({
|
|
7
|
+
code: z.string().describe('Playwright code to execute. The `page` object is available in scope.'),
|
|
8
|
+
timeout: z.number().optional().describe('Execution timeout in milliseconds. Default: 30000'),
|
|
9
|
+
});
|
|
10
|
+
const ScreenshotSchema = z.object({
|
|
11
|
+
fullPage: z.boolean().optional().describe('Capture the full scrollable page. Default: false'),
|
|
12
|
+
});
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// TOOL DESCRIPTIONS
|
|
15
|
+
// =============================================================================
|
|
16
|
+
const EXECUTE_DESCRIPTION = `Execute Playwright code in the browser.
|
|
17
|
+
|
|
18
|
+
This tool runs JavaScript code with access to a Playwright \`page\` object. The browser session persists across calls, so you can navigate, interact with elements, and extract data across multiple tool invocations.
|
|
19
|
+
|
|
20
|
+
**Available in scope:**
|
|
21
|
+
- \`page\` - The Playwright Page object with full API access
|
|
22
|
+
|
|
23
|
+
**Example usage:**
|
|
24
|
+
|
|
25
|
+
Navigate to a page:
|
|
26
|
+
\`\`\`javascript
|
|
27
|
+
await page.goto('https://example.com');
|
|
28
|
+
return await page.title();
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
Click an element and wait:
|
|
32
|
+
\`\`\`javascript
|
|
33
|
+
await page.click('button.submit');
|
|
34
|
+
await page.waitForSelector('.success-message');
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
Extract text content:
|
|
38
|
+
\`\`\`javascript
|
|
39
|
+
const items = await page.$$eval('.item', els => els.map(el => el.textContent));
|
|
40
|
+
return items;
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
Fill a form:
|
|
44
|
+
\`\`\`javascript
|
|
45
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
46
|
+
await page.fill('input[name="password"]', 'secret');
|
|
47
|
+
await page.click('button[type="submit"]');
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
**Returns:**
|
|
51
|
+
- \`success\`: boolean indicating if execution succeeded
|
|
52
|
+
- \`result\`: JSON stringified return value (if any)
|
|
53
|
+
- \`error\`: error message (if failed)
|
|
54
|
+
- \`consoleOutput\`: array of console messages from the page
|
|
55
|
+
|
|
56
|
+
**Note:** When STEALTH_MODE=true, the browser includes anti-detection measures to help bypass bot protection.`;
|
|
57
|
+
const SCREENSHOT_DESCRIPTION = `Take a screenshot of the current page.
|
|
58
|
+
|
|
59
|
+
Captures the visible viewport or full page as a PNG image encoded in base64.
|
|
60
|
+
|
|
61
|
+
**Returns:**
|
|
62
|
+
- Base64-encoded PNG image data
|
|
63
|
+
|
|
64
|
+
**Use cases:**
|
|
65
|
+
- Verify page state after navigation
|
|
66
|
+
- Debug automation issues
|
|
67
|
+
- Capture visual content for analysis`;
|
|
68
|
+
const GET_STATE_DESCRIPTION = `Get the current browser state.
|
|
69
|
+
|
|
70
|
+
Returns information about the current browser session including the URL, page title, and whether a browser is open.
|
|
71
|
+
|
|
72
|
+
**Returns:**
|
|
73
|
+
- \`currentUrl\`: Current page URL
|
|
74
|
+
- \`title\`: Current page title
|
|
75
|
+
- \`isOpen\`: Whether a browser session is active`;
|
|
76
|
+
const CLOSE_DESCRIPTION = `Close the browser session.
|
|
77
|
+
|
|
78
|
+
Shuts down the browser and clears all state. A new browser will be launched on the next execute call.`;
|
|
79
|
+
export function createRegisterTools(clientFactory) {
|
|
80
|
+
// Create a single client instance that persists across calls
|
|
81
|
+
let client = null;
|
|
82
|
+
const getClient = () => {
|
|
83
|
+
if (!client) {
|
|
84
|
+
client = clientFactory();
|
|
85
|
+
}
|
|
86
|
+
return client;
|
|
87
|
+
};
|
|
88
|
+
const tools = [
|
|
89
|
+
{
|
|
90
|
+
name: 'browser_execute',
|
|
91
|
+
description: EXECUTE_DESCRIPTION,
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
code: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Playwright code to execute. The `page` object is available in scope.',
|
|
98
|
+
},
|
|
99
|
+
timeout: {
|
|
100
|
+
type: 'number',
|
|
101
|
+
description: 'Execution timeout in milliseconds. Default: 30000',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ['code'],
|
|
105
|
+
},
|
|
106
|
+
handler: async (args) => {
|
|
107
|
+
try {
|
|
108
|
+
const validated = ExecuteSchema.parse(args);
|
|
109
|
+
const result = await getClient().execute(validated.code, {
|
|
110
|
+
timeout: validated.timeout,
|
|
111
|
+
});
|
|
112
|
+
if (result.success) {
|
|
113
|
+
const parts = [];
|
|
114
|
+
if (result.result) {
|
|
115
|
+
parts.push(`Result:\n${result.result}`);
|
|
116
|
+
}
|
|
117
|
+
if (result.consoleOutput && result.consoleOutput.length > 0) {
|
|
118
|
+
parts.push(`Console output:\n${result.consoleOutput.join('\n')}`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: parts.length > 0 ? parts.join('\n\n') : 'Execution completed successfully.',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: `Error: ${result.error}`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'browser_screenshot',
|
|
156
|
+
description: SCREENSHOT_DESCRIPTION,
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
fullPage: {
|
|
161
|
+
type: 'boolean',
|
|
162
|
+
description: 'Capture the full scrollable page. Default: false',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
handler: async (args) => {
|
|
167
|
+
try {
|
|
168
|
+
const validated = ScreenshotSchema.parse(args);
|
|
169
|
+
const base64 = await getClient().screenshot({
|
|
170
|
+
fullPage: validated.fullPage,
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: 'image',
|
|
176
|
+
data: base64,
|
|
177
|
+
mimeType: 'image/png',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: `Error taking screenshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'browser_get_state',
|
|
197
|
+
description: GET_STATE_DESCRIPTION,
|
|
198
|
+
inputSchema: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {},
|
|
201
|
+
},
|
|
202
|
+
handler: async () => {
|
|
203
|
+
try {
|
|
204
|
+
const state = await getClient().getState();
|
|
205
|
+
const config = getClient().getConfig();
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: 'text',
|
|
210
|
+
text: JSON.stringify({
|
|
211
|
+
...state,
|
|
212
|
+
stealthMode: config.stealthMode,
|
|
213
|
+
headless: config.headless,
|
|
214
|
+
}, null, 2),
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return {
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: 'text',
|
|
224
|
+
text: `Error getting state: ${error instanceof Error ? error.message : String(error)}`,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
isError: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'browser_close',
|
|
234
|
+
description: CLOSE_DESCRIPTION,
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {},
|
|
238
|
+
},
|
|
239
|
+
handler: async () => {
|
|
240
|
+
try {
|
|
241
|
+
await getClient().close();
|
|
242
|
+
client = null; // Clear the reference so a new browser is created on next call
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: 'text',
|
|
247
|
+
text: 'Browser closed successfully.',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: `Error closing browser: ${error instanceof Error ? error.message : String(error)}`,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
return (server) => {
|
|
267
|
+
// List available tools
|
|
268
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
269
|
+
return {
|
|
270
|
+
tools: tools.map((tool) => ({
|
|
271
|
+
name: tool.name,
|
|
272
|
+
description: tool.description,
|
|
273
|
+
inputSchema: tool.inputSchema,
|
|
274
|
+
})),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// Handle tool calls
|
|
278
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
279
|
+
const { name, arguments: args } = request.params;
|
|
280
|
+
const tool = tools.find((t) => t.name === name);
|
|
281
|
+
if (!tool) {
|
|
282
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
283
|
+
}
|
|
284
|
+
return await tool.handler(args);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Playwright Stealth MCP server
|
|
3
|
+
*/
|
|
4
|
+
export interface PlaywrightConfig {
|
|
5
|
+
stealthMode: boolean;
|
|
6
|
+
headless: boolean;
|
|
7
|
+
timeout: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ExecuteResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
result?: unknown;
|
|
12
|
+
error?: string;
|
|
13
|
+
consoleOutput?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface BrowserState {
|
|
16
|
+
currentUrl?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=types.d.ts.map
|
package/shared/types.js
ADDED