This is page 1 of 4. Use http://codebase.md/cheffromspace/mcpcontrol?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .claude
│ └── settings.local.json
├── .github
│ ├── dependabot.yml
│ ├── FUNDING.yml
│ ├── pr-webhook-utils.cjs
│ └── workflows
│ ├── ci.yml
│ ├── codeql.yml
│ └── npm-publish.yml
├── .gitignore
├── .husky
│ └── pre-commit
├── .lintstagedrc
├── .prettierignore
├── .prettierrc
├── CLAUDE.md
├── CONTRIBUTING.md
├── docs
│ ├── llms-full.txt
│ ├── providers.md
│ └── sse-transport.md
├── eslint.config.js
├── LICENSE
├── mcpcontrol-wrapper.sh
├── package-lock.json
├── package.json
├── README.md
├── RELEASE_NOTES_v0.2.0.md
├── scripts
│ ├── build.js
│ ├── compare-providers.js
│ ├── generate-test-certs.sh
│ ├── test-provider.js
│ ├── test-screenshot.cjs
│ ├── test-screenshot.mjs
│ ├── test-window.cjs
│ └── test-window.js
├── src
│ ├── config.ts
│ ├── handlers
│ │ ├── tools.test.ts
│ │ ├── tools.ts
│ │ └── tools.zod.ts
│ ├── index.ts
│ ├── interfaces
│ │ ├── automation.ts
│ │ └── provider.ts
│ ├── logger.ts
│ ├── providers
│ │ ├── autohotkey
│ │ │ ├── clipboard.ts
│ │ │ ├── index.test.ts
│ │ │ ├── index.ts
│ │ │ ├── keyboard.ts
│ │ │ ├── mouse.ts
│ │ │ ├── README.md
│ │ │ ├── screen.ts
│ │ │ └── utils.ts
│ │ ├── clipboard
│ │ │ ├── clipboardy
│ │ │ │ └── index.ts
│ │ │ └── powershell
│ │ │ ├── index.test.ts
│ │ │ └── index.ts
│ │ ├── factory.modular.test.ts
│ │ ├── factory.test.ts
│ │ ├── factory.ts
│ │ ├── keysender
│ │ │ ├── clipboard.ts
│ │ │ ├── index.test.ts
│ │ │ ├── index.ts
│ │ │ ├── keyboard.ts
│ │ │ ├── mouse.ts
│ │ │ ├── screen.test.ts
│ │ │ └── screen.ts
│ │ ├── registry.test.ts
│ │ └── registry.ts
│ ├── server.test.ts
│ ├── server.ts
│ ├── tools
│ │ ├── clipboard.ts
│ │ ├── keyboard.test.ts
│ │ ├── keyboard.ts
│ │ ├── mouse.test.ts
│ │ ├── mouse.ts
│ │ ├── screen.test.ts
│ │ ├── screen.ts
│ │ ├── screenshot-file.ts
│ │ ├── screenshot.test.ts
│ │ ├── screenshot.ts
│ │ ├── validation.zod.test.ts
│ │ └── validation.zod.ts
│ └── types
│ ├── common.ts
│ ├── keysender.d.ts
│ ├── responses.ts
│ └── transport.ts
├── test
│ ├── e2e-test.sh
│ ├── server-port.txt
│ ├── test-results.json
│ └── test-server.js
├── test-autohotkey-direct.js
├── test-autohotkey.js
├── test-panel.html
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
```
1 | # Build output
2 | build/
3 | coverage/
4 |
5 | # Dependencies
6 | node_modules/
7 |
8 | # Misc
9 | .DS_Store
10 | *.log
```
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "src/**/*.ts": ["prettier --write", "npm run lint:fix"],
3 | "test/**/*.js": ["prettier --write"]
4 | }
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "jsxSingleQuote": false,
9 | "trailingComma": "all",
10 | "bracketSpacing": true,
11 | "bracketSameLine": false,
12 | "arrowParens": "always",
13 | "endOfLine": "lf"
14 | }
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | libnut-core/
4 |
5 | # Build output
6 | build/
7 | dist/
8 |
9 | # Coverage
10 | coverage/
11 |
12 | # IDE
13 | .vscode/
14 | .idea/
15 | *.sublime-workspace
16 | *.sublime-project
17 |
18 | # Logs
19 | logs
20 | *.log
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # OS
26 | .DS_Store
27 | Thumbs.db
28 |
29 | **/.claude/settings.local.json
30 |
31 | # Test certificates
32 | test/certs/
33 | *.pem
34 | *.crt
35 | *.key
36 |
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # AutoHotkey Provider for MCPControl
2 |
3 | This provider implements the MCPControl automation interfaces using AutoHotkey v2.
4 |
5 | ## Prerequisites
6 |
7 | - AutoHotkey v2.0 or later must be installed on the system
8 | - `AutoHotkey.exe` must be available in the system PATH
9 | - Windows operating system (AutoHotkey is Windows-only)
10 |
11 | ## Installation
12 |
13 | AutoHotkey can be downloaded from: https://www.autohotkey.com/
14 |
15 | Make sure to install version 2.0 or later.
16 |
17 | ## Usage
18 |
19 | ### Using as the primary provider
20 |
21 | ```javascript
22 | const provider = createAutomationProvider({ provider: 'autohotkey' });
23 | ```
24 |
25 | ### Using in modular configuration
26 |
27 | ```javascript
28 | const provider = createAutomationProvider({
29 | providers: {
30 | keyboard: 'autohotkey',
31 | mouse: 'autohotkey',
32 | screen: 'autohotkey',
33 | clipboard: 'autohotkey',
34 | },
35 | });
36 | ```
37 |
38 | ### Environment Variables
39 |
40 | Set the automation provider to AutoHotkey:
41 |
42 | ```bash
43 | export AUTOMATION_PROVIDER=autohotkey
44 | ```
45 |
46 | Configure the AutoHotkey executable path (optional):
47 |
48 | ```bash
49 | export AUTOHOTKEY_PATH="C:\Program Files\AutoHotkey\v2\AutoHotkey.exe"
50 | ```
51 |
52 | Or use modular configuration:
53 |
54 | ```bash
55 | export AUTOMATION_KEYBOARD_PROVIDER=autohotkey
56 | export AUTOMATION_MOUSE_PROVIDER=autohotkey
57 | export AUTOMATION_SCREEN_PROVIDER=autohotkey
58 | export AUTOMATION_CLIPBOARD_PROVIDER=autohotkey
59 | ```
60 |
61 | ## Features
62 |
63 | ### Keyboard Automation
64 | - Type text
65 | - Press individual keys
66 | - Press key combinations
67 | - Hold and release keys
68 |
69 | ### Mouse Automation
70 | - Move mouse to position
71 | - Click mouse buttons
72 | - Double-click
73 | - Scroll
74 | - Drag operations
75 | - Get cursor position
76 |
77 | ### Screen Automation
78 | - Get screen size
79 | - Capture screenshots
80 | - Get pixel colors
81 | - Window management (focus, resize, reposition)
82 | - Get active window information
83 |
84 | ### Clipboard Automation
85 | - Set clipboard content
86 | - Get clipboard content
87 | - Check if clipboard has text
88 | - Clear clipboard
89 |
90 | ## Implementation Notes
91 |
92 | The AutoHotkey provider executes AutoHotkey v2 scripts for each operation. This means:
93 |
94 | 1. Each operation creates a temporary `.ahk` script file
95 | 2. The script is executed via `AutoHotkey.exe`
96 | 3. Results are captured through temporary files or script output
97 | 4. Temporary files are cleaned up after execution
98 |
99 | ## Performance Considerations
100 |
101 | Since each operation requires creating and executing a script, there is some overhead compared to native implementations. For high-frequency operations, consider batching operations or using a different provider.
102 |
103 | ## Error Handling
104 |
105 | If AutoHotkey is not installed or not in the PATH, operations will fail with an error message. Make sure AutoHotkey v2 is properly installed and accessible.
106 |
107 | ## Known Limitations
108 |
109 | 1. Screenshot functionality is basic and uses Windows built-in tools (Paint, Snipping Tool)
110 | 2. Some operations may have timing issues due to the script execution model
111 | 3. Only works on Windows systems
112 | 4. Requires AutoHotkey v2 syntax (not compatible with v1)
113 |
114 | ## Debugging
115 |
116 | To debug AutoHotkey scripts, you can:
117 |
118 | 1. Check the temporary script files generated in the system temp directory
119 | 2. Run the scripts manually with AutoHotkey to see any error messages
120 | 3. Enable AutoHotkey debugging features
121 |
122 | ## Contributing
123 |
124 | When contributing to the AutoHotkey provider:
125 |
126 | 1. Ensure all scripts use AutoHotkey v2 syntax
127 | 2. Test on Windows with AutoHotkey v2 installed
128 | 3. Handle errors gracefully
129 | 4. Clean up temporary files properly
130 | 5. Follow the existing code structure and patterns
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCPControl
2 |
3 | <p align="center">
4 | <img src="https://github.com/user-attachments/assets/1c577e56-7b8d-49e9-aaf5-b8550cc6cfc0" alt="MCPControl Logo" width="250">
5 | </p>
6 |
7 | <p align="center">
8 | <a href="https://github.com/Cheffromspace/MCPControl/releases/tag/v0.2.0">
9 | <img src="https://img.shields.io/badge/release-v0.2.0-blue.svg" alt="Latest Release">
10 | </a>
11 | </p>
12 |
13 | Windows control server for the [Model Context Protocol](https://modelcontextprotocol.io/), providing programmatic control over system operations including mouse, keyboard, window management, and screen capture functionality.
14 |
15 | > **Note**: This project currently supports Windows only.
16 |
17 | ## 🔥 Why MCPControl?
18 |
19 | MCPControl bridges the gap between AI models and your desktop, enabling secure, programmatic control of:
20 |
21 | - 🖱️ **Mouse movements and clicks**
22 | - ⌨️ **Keyboard input and shortcuts**
23 | - 🪟 **Window management**
24 | - 📸 **Screen capture and analysis**
25 | - 📋 **Clipboard operations**
26 |
27 | ## 🔌 Quick Start
28 |
29 | ### Prerequisites
30 |
31 | 1. **Install Build Tools (including VC++ workload)**
32 | ```powershell
33 | # Run as Administrator - may take a few minutes to complete
34 | winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
35 | ```
36 |
37 | 2. **Install Python** (if not already installed)
38 | ```powershell
39 | # Install Python (required for node-gyp)
40 | winget install Python.Python.3.12
41 | ```
42 |
43 | 3. **Install Node.js**
44 | ```powershell
45 | # Install latest LTS version
46 | winget install OpenJS.NodeJS
47 | ```
48 |
49 | ### Installation
50 |
51 | 1. **Install MCPControl Package**
52 | ```powershell
53 | npm install -g mcp-control
54 | ```
55 |
56 | ### Configuration
57 |
58 | MCPControl works best in a **virtual machine at 1280x720 resolution** for optimal click accuracy.
59 |
60 | Configure your Claude client to connect to MCPControl via SSE transport:
61 |
62 | #### Option 1: Direct SSE Connection
63 |
64 | For connecting to an MCPControl server running on a VM or remote machine:
65 |
66 | ```json
67 | {
68 | "mcpServers": {
69 | "MCPControl": {
70 | "transport": "sse",
71 | "url": "http://192.168.1.100:3232/mcp"
72 | }
73 | }
74 | }
75 | ```
76 |
77 | Replace `192.168.1.100:3232` with your server's IP address and port.
78 |
79 | #### Option 2: Local Launch with SSE
80 |
81 | To launch MCPControl locally with SSE transport:
82 |
83 | ```json
84 | {
85 | "mcpServers": {
86 | "MCPControl": {
87 | "command": "mcp-control",
88 | "args": ["--sse"]
89 | }
90 | }
91 | }
92 | ```
93 |
94 | ### Starting the Server
95 |
96 | First, start the MCPControl server on your VM or local machine:
97 |
98 | ```bash
99 | mcp-control --sse
100 | ```
101 |
102 | The server will display:
103 | - Available network interfaces and their IP addresses
104 | - The port number (default: 3232)
105 | - Connection status messages
106 |
107 | ### VM Setup Example
108 |
109 | 1. **Start your Windows VM** with 1280x720 resolution
110 | 2. **Install MCPControl** on the VM:
111 | ```bash
112 | npm install -g mcp-control
113 | ```
114 | 3. **Run the server** with SSE transport:
115 | ```bash
116 | mcp-control --sse
117 | ```
118 | 4. **Note the VM's IP address** (e.g., `192.168.1.100`)
119 | 5. **Configure Claude** with the SSE URL:
120 | ```json
121 | {
122 | "mcpServers": {
123 | "MCPControl": {
124 | "transport": "sse",
125 | "url": "http://192.168.1.100:3232/mcp"
126 | }
127 | }
128 | }
129 | ```
130 | 6. **Restart Claude** and MCPControl will appear in your MCP menu!
131 |
132 | ## 🔧 CLI Options
133 |
134 | MCPControl supports several command-line flags for advanced configurations:
135 |
136 | ```bash
137 | # Run with SSE transport on default port (3232)
138 | mcp-control --sse
139 |
140 | # Run with SSE on custom port
141 | mcp-control --sse --port 3000
142 |
143 | # Run with HTTPS/TLS (required for production deployments)
144 | mcp-control --sse --https --cert /path/to/cert.pem --key /path/to/key.pem
145 |
146 | # Run with HTTPS on custom port
147 | mcp-control --sse --https --port 8443 --cert /path/to/cert.pem --key /path/to/key.pem
148 | ```
149 |
150 | ### Command Line Arguments
151 |
152 | - `--sse` - Enable SSE (Server-Sent Events) transport for network access
153 | - `--port [number]` - Specify custom port (default: 3232)
154 | - `--https` - Enable HTTPS/TLS (required for remote deployments per MCP spec)
155 | - `--cert [path]` - Path to TLS certificate file (required with --https)
156 | - `--key [path]` - Path to TLS private key file (required with --https)
157 |
158 | ### Security Note
159 |
160 | According to the MCP specification, HTTPS is **mandatory** for all HTTP-based transports in production environments. When deploying MCPControl for remote access, always use the `--https` flag with valid TLS certificates.
161 |
162 | ## 🚀 Popular Use Cases
163 |
164 | ### Assisted Automation
165 |
166 | - **Application Testing**: Delegate repetitive UI testing to Claude, allowing AI to navigate through applications and report issues
167 | - **Workflow Automation**: Have Claude operate applications on your behalf, handling repetitive tasks while you focus on creative work
168 | - **Form Filling**: Let Claude handle data entry tasks with your supervision
169 |
170 | ### AI Experimentation
171 |
172 | - **AI Gaming**: Watch Claude learn to play simple games through visual feedback
173 | - **Visual Reasoning**: Test Claude's ability to navigate visual interfaces and solve visual puzzles
174 | - **Human-AI Collaboration**: Explore new interaction paradigms where Claude can see your screen and help with complex tasks
175 |
176 | ### Development and Testing
177 |
178 | - **Cross-Application Integration**: Bridge applications that don't normally communicate
179 | - **UI Testing Framework**: Create robust testing scenarios with visual validation
180 | - **Demo Creation**: Automate the creation of product demonstrations
181 |
182 | ## ⚠️ IMPORTANT DISCLAIMER
183 |
184 | **THIS SOFTWARE IS EXPERIMENTAL AND POTENTIALLY DANGEROUS**
185 |
186 | By using this software, you acknowledge and accept that:
187 |
188 | - Giving AI models direct control over your computer through this tool is inherently risky
189 | - This software can control your mouse, keyboard, and other system functions which could potentially cause unintended consequences
190 | - You are using this software entirely at your own risk
191 | - The creators and contributors of this project accept NO responsibility for any damage, data loss, or other consequences that may arise from using this software
192 | - This tool should only be used in controlled environments with appropriate safety measures in place
193 |
194 | **USE AT YOUR OWN RISK**
195 |
196 | ## 🌟 Features
197 |
198 | <table>
199 | <tr>
200 | <td>
201 | <h3>🪟 Window Management</h3>
202 | <ul>
203 | <li>List all windows</li>
204 | <li>Get active window info</li>
205 | <li>Focus, resize & reposition</li>
206 | </ul>
207 | </td>
208 | <td>
209 | <h3>🖱️ Mouse Control</h3>
210 | <ul>
211 | <li>Precision movement</li>
212 | <li>Click & drag operations</li>
213 | <li>Scrolling & position tracking</li>
214 | </ul>
215 | </td>
216 | </tr>
217 | <tr>
218 | <td>
219 | <h3>⌨️ Keyboard Control</h3>
220 | <ul>
221 | <li>Text input & key combos</li>
222 | <li>Key press/release control</li>
223 | <li>Hold key functionality</li>
224 | </ul>
225 | </td>
226 | <td>
227 | <h3>📸 Screen Operations</h3>
228 | <ul>
229 | <li>High-quality screenshots</li>
230 | <li>Screen size detection</li>
231 | <li>Active window capture</li>
232 | </ul>
233 | </td>
234 | </tr>
235 | </table>
236 |
237 | ## 🔧 Automation Providers
238 |
239 | MCPControl supports multiple automation providers for different use cases:
240 |
241 | - **keysender** (default) - Native Windows automation with high reliability
242 | - **powershell** - Windows PowerShell-based automation for simpler operations
243 | - **autohotkey** - AutoHotkey v2 scripting for advanced automation needs
244 |
245 | ### Provider Configuration
246 |
247 | You can configure the automation provider using environment variables:
248 |
249 | ```bash
250 | # Use a specific provider for all operations
251 | export AUTOMATION_PROVIDER=autohotkey
252 |
253 | # Configure AutoHotkey executable path (if not in PATH)
254 | export AUTOHOTKEY_PATH="C:\Program Files\AutoHotkey\v2\AutoHotkey.exe"
255 | ```
256 |
257 | Or use modular configuration for specific operations:
258 |
259 | ```bash
260 | # Mix and match providers for different operations
261 | export AUTOMATION_KEYBOARD_PROVIDER=autohotkey
262 | export AUTOMATION_MOUSE_PROVIDER=keysender
263 | export AUTOMATION_SCREEN_PROVIDER=keysender
264 | export AUTOMATION_CLIPBOARD_PROVIDER=powershell
265 | ```
266 |
267 | See provider-specific documentation:
268 | - [AutoHotkey Provider](src/providers/autohotkey/README.md)
269 |
270 | ## 🛠️ Development Setup
271 |
272 | If you're interested in contributing or building from source, please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions.
273 |
274 | ### Development Requirements
275 |
276 | To build this project for development, you'll need:
277 |
278 | 1. Windows operating system (required for the keysender dependency)
279 | 2. Node.js 18 or later (install using the official Windows installer which includes build tools)
280 | 3. npm package manager
281 | 4. Native build tools:
282 | - node-gyp: `npm install -g node-gyp`
283 | - cmake-js: `npm install -g cmake-js`
284 |
285 | The keysender dependency relies on Windows-specific native modules that require these build tools.
286 |
287 | ## 📋 Project Structure
288 |
289 | - `/src`
290 | - `/handlers` - Request handlers and tool management
291 | - `/tools` - Core functionality implementations
292 | - `/types` - TypeScript type definitions
293 | - `index.ts` - Main application entry point
294 |
295 | ## 🔖 Repository Branches
296 |
297 | - **`main`** - Main development branch with the latest features and changes
298 | - **`release`** - Stable release branch that mirrors the latest stable tag (currently v0.2.0)
299 |
300 | ### Version Installation
301 |
302 | You can install specific versions of MCPControl using npm:
303 |
304 | ```bash
305 | # Install the latest stable release (from release branch)
306 | npm install mcp-control
307 |
308 | # Install a specific version
309 | npm install [email protected]
310 | ```
311 |
312 | ## 📚 Dependencies
313 |
314 | - [@modelcontextprotocol/sdk](https://www.npmjs.com/package/@modelcontextprotocol/sdk) - MCP SDK for protocol implementation
315 | - [keysender](https://www.npmjs.com/package/keysender) - Windows-only UI automation library
316 | - [clipboardy](https://www.npmjs.com/package/clipboardy) - Clipboard handling
317 | - [sharp](https://www.npmjs.com/package/sharp) - Image processing
318 | - [uuid](https://www.npmjs.com/package/uuid) - UUID generation
319 |
320 | ## 🚧 Known Limitations
321 |
322 | - Window minimize/restore operations are currently unsupported
323 | - Multiple screen functions may not work as expected, depending on setup
324 | - The get_screenshot utility does not work with the VS Code Extension Cline. See [GitHub issue #1865](https://github.com/cline/cline/issues/1865)
325 | - Some operations may require elevated permissions depending on the target application
326 | - Only Windows is supported
327 | - MCPControl works best at 1280x720 resolution, single screen. Click accuracy is optimized for this resolution. We're working on an offset/scaling bug and looking for testers or help creating testing tools
328 |
329 | ## 👥 Contributing
330 |
331 | See [CONTRIBUTING.md](CONTRIBUTING.md)
332 |
333 | ## ⚖️ License
334 |
335 | This project is licensed under the MIT License - see the LICENSE file for details.
336 |
337 | ## 📖 References
338 |
339 | - [Model Context Protocol Documentation](https://modelcontextprotocol.github.io/)
340 |
341 | [](https://mseep.ai/app/cheffromspace-mcpcontrol)
342 |
343 |
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCPControl - Development Guide
2 |
3 | ## Build & Test Commands
4 | - Build: `pwsh.exe -c "npm run build"` - Compiles TypeScript to JavaScript
5 | - Lint: `pwsh.exe -c "npm run lint"` - Runs ESLint to check code quality (TS and JS)
6 | - Format: `pwsh.exe -c "npm run format"` - Runs Prettier to format code
7 | - Format Check: `pwsh.exe -c "npm run format:check"` - Checks if files are properly formatted
8 | - Test: `pwsh.exe -c "npm run test"` - Runs all Vitest tests
9 | - Run single test: `pwsh.exe -c "npm run test -- tools/keyboard.test.ts"` or `pwsh.exe -c "npm run test -- -t \"specific test name\""`
10 | - Watch tests: `pwsh.exe -c "npm run test:watch"` - Runs tests in watch mode
11 | - Coverage: `pwsh.exe -c "npm run test:coverage"` - Generates test coverage report
12 | - E2E Test: `cd test && ./e2e-test.sh [iterations]` - Runs end-to-end tests with Claude and MCPControl
13 |
14 | ## Running with HTTPS/TLS
15 | MCPControl supports HTTPS for secure SSE connections (mandatory per MCP spec for production):
16 | - `node build/index.js --sse --https --cert /path/to/cert.pem --key /path/to/key.pem`
17 | - Default HTTPS port is still 3232 (use --port to change)
18 | - Both --cert and --key are required when using --https
19 |
20 | > Note: MCP Servers are typically launched by the Client as a subprocess.
21 |
22 | ## Code Style Guidelines
23 | - **Imports**: Use ES module syntax with named imports
24 | - **Types**: Define TypeScript interfaces for inputs/outputs in `types/` directory
25 | - **Error Handling**: Use try/catch with standardized response objects
26 | - **Naming**: camelCase for variables/functions, PascalCase for interfaces
27 | - **Functions**: Keep functions small and focused on single responsibility
28 | - **Comments**: Add JSDoc comments for public APIs
29 | - **Testing**:
30 | - Unit tests: Place in same directory as implementation with `.test.ts` suffix
31 | - E2E tests: Added to the `test/` directory
32 | - **Formatting**: Code is formatted using Prettier (pre-commit hooks will run automatically)
33 | - **Error Responses**: Return `{ success: false, message: string }` for errors
34 | - **Success Responses**: Return `{ success: true, data?: any }` for success
35 | - **Linting**: Both TypeScript and JavaScript files are linted with ESLint
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to MCP Control
2 |
3 | Thank you for your interest in contributing to MCP Control! This document provides guidelines and instructions for contributing to the project.
4 |
5 | ## Table of Contents
6 |
7 | - [Code of Conduct](#code-of-conduct)
8 | - [Getting Started](#getting-started)
9 | - [Prerequisites](#prerequisites)
10 | - [Setup](#setup)
11 | - [Development Workflow](#development-workflow)
12 | - [Branching Strategy](#branching-strategy)
13 | - [Commit Guidelines](#commit-guidelines)
14 | - [Pull Requests](#pull-requests)
15 | - [Code Style and Standards](#code-style-and-standards)
16 | - [Testing](#testing)
17 | - [Documentation](#documentation)
18 | - [Project Structure](#project-structure)
19 | - [Issue Tracking](#issue-tracking)
20 | - [Future Roadmap](#future-roadmap)
21 |
22 | ## Code of Conduct
23 |
24 | Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community.
25 |
26 | ## Getting Started
27 |
28 | ### Prerequisites
29 |
30 | - Node.js (latest LTS version recommended)
31 | - npm
32 | - git
33 | - C++ compiler (for building native modules)
34 |
35 | ### Setup
36 |
37 | 1. Fork the repository
38 | 2. Clone your fork:
39 | ```bash
40 | git clone https://github.com/YOUR-USERNAME/MCPControl.git
41 | cd MCPControl
42 | ```
43 |
44 | 3. Build the project:
45 | ```bash
46 | # Install dependencies
47 | npm install
48 |
49 | # Build the project
50 | npm run build
51 | ```
52 |
53 | ## Development Workflow
54 |
55 | ### Branching Strategy
56 |
57 | - `main` branch contains the latest stable code
58 | - Create feature branches from `main` using the naming convention:
59 | - `feature/feature-name` for new features
60 | - `bugfix/issue-description` for bug fixes
61 | - `docs/description` for documentation changes
62 | - `refactor/description` for code refactoring
63 |
64 | ### Commit Guidelines
65 |
66 | - Write clear, descriptive commit messages
67 | - Reference issue numbers in commit messages when applicable
68 | - Keep commits focused on a single logical change
69 |
70 | ### Pull Requests
71 |
72 | 1. Create your feature branch: `git checkout -b feature/amazing-feature`
73 | 2. Commit your changes: `git commit -m 'Add some amazing feature'`
74 | 3. Push to the branch: `git push origin feature/amazing-feature`
75 | 4. Open a Pull Request
76 | 5. Ensure all tests pass and code meets the project standards
77 | 6. Request a review from a maintainer
78 |
79 | ## Code Style and Standards
80 |
81 | - Use ES module syntax with named imports
82 | - Define TypeScript interfaces for inputs/outputs in the `types/` directory
83 | - Use try/catch with standardized response objects for error handling
84 | - Follow naming conventions:
85 | - camelCase for variables/functions
86 | - PascalCase for interfaces
87 | - Keep functions small and focused on single responsibility
88 | - Add JSDoc comments for public APIs
89 | - Use 2-space indentation and semicolons
90 | - For errors, return `{ success: false, message: string }`
91 | - For success, return `{ success: true, data?: any }`
92 |
93 | ## Testing
94 |
95 | - Place tests in the same directory as implementation with `.test.ts` suffix
96 | - Run tests with `npm run test`
97 | - Generate coverage report with `npm run test:coverage`
98 | - Run a single test with `npm run test -- tools/keyboard.test.ts` or `npm run test -- -t "specific test name"`
99 | - Run tests in watch mode with `npm run test:watch`
100 |
101 | All new features should include appropriate test coverage. The project uses Vitest for testing.
102 |
103 | ## Documentation
104 |
105 | - Document public APIs with JSDoc comments
106 | - Update README.md when adding new features or changing functionality
107 | - Keep code comments clear and focused on explaining "why" rather than "what"
108 |
109 | ## Project Structure
110 |
111 | - `/src`
112 | - `/handlers` - Request handlers and tool management
113 | - `/tools` - Core functionality implementations
114 | - `/types` - TypeScript type definitions
115 | - `index.ts` - Main application entry point
116 |
117 | ## Issue Tracking
118 |
119 | Check the [GitHub issues](https://github.com/Cheffromspace/MCPControl/issues) for existing issues you might want to contribute to. Current focus areas include:
120 |
121 | 1. Creating an npm package for easy installation
122 | 2. Adding remote computer control support
123 | 3. Building a dedicated test application
124 |
125 | When creating a new issue:
126 | - Use descriptive titles
127 | - Include steps to reproduce for bugs
128 | - For feature requests, explain the use case and potential implementation approach
129 |
130 | ## Future Roadmap
131 |
132 | Current roadmap and planned features include:
133 |
134 | - Fixing click accuracy issues with different resolutions and multi-screen setups
135 | - Security implementation improvements
136 | - Comprehensive testing
137 | - Error handling enhancements
138 | - Performance optimization
139 | - Automation framework
140 | - Enhanced window management
141 | - Advanced integration features
142 |
143 | ## Publishing
144 |
145 | This project uses GitHub Actions to automatically publish to npm when a version tag is pushed to main:
146 |
147 | 1. Ensure changes are merged to main
148 | 2. Create and push a tag with the version number:
149 | ```bash
150 | git tag v1.2.3
151 | git push origin v1.2.3
152 | ```
153 | 3. The GitHub Action will automatically:
154 | - Build and test the package
155 | - Update the version in package.json
156 | - Publish to npm
157 |
158 | Note: You need to have the `NPM_TOKEN` secret configured in the GitHub repository settings.
159 |
160 | ---
161 |
162 | Thank you for contributing to MCP Control!
163 |
```
--------------------------------------------------------------------------------
/test/server-port.txt:
--------------------------------------------------------------------------------
```
1 | 8080
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
1 | ko_fi: Cheffromspace
2 |
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Utility functions for AutoHotkey provider
3 | */
4 |
5 | /**
6 | * Get the path to AutoHotkey executable
7 | * Can be configured via AUTOHOTKEY_PATH environment variable
8 | */
9 | export function getAutoHotkeyPath(): string {
10 | return process.env.AUTOHOTKEY_PATH || 'AutoHotkey.exe';
11 | }
12 |
```
--------------------------------------------------------------------------------
/.claude/settings.local.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "permissions": {
3 | "allow": [
4 | "Bash(gh repo view:*)",
5 | "Bash(gh repo view:*)",
6 | "Bash(gh label create:*)",
7 | "Bash(gh label create:*)",
8 | "Bash(gh issue create:*)",
9 | "Bash(gh issue create:*)",
10 | "Bash(mkdir:*)"
11 | ],
12 | "deny": []
13 | }
14 | }
```
--------------------------------------------------------------------------------
/src/interfaces/provider.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | KeyboardAutomation,
3 | MouseAutomation,
4 | ScreenAutomation,
5 | ClipboardAutomation,
6 | } from './automation.js';
7 |
8 | export interface AutomationProvider {
9 | keyboard: KeyboardAutomation;
10 | mouse: MouseAutomation;
11 | screen: ScreenAutomation;
12 | clipboard: ClipboardAutomation;
13 | }
14 |
```
--------------------------------------------------------------------------------
/src/types/transport.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Enumeration of supported transport types for MCP server communication
3 | */
4 | export enum TransportType {
5 | /**
6 | * HTTP transport for standard request-response communication
7 | */
8 | HTTP = 'HTTP',
9 |
10 | /**
11 | * Server-Sent Events transport for real-time unidirectional event streaming
12 | */
13 | SSE = 'SSE',
14 | }
15 |
```
--------------------------------------------------------------------------------
/mcpcontrol-wrapper.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # mcpcontrol-wrapper.sh - Bridge script for Claude CLI to run MCPControl from WSL
3 |
4 | # Windows path translation
5 | WIN_PATH=$(echo "$PWD" | sed -E 's|^/mnt/([a-z])(/.*)|\1:\\\2|g' | sed 's|/|\\|g')
6 |
7 | # Run PowerShell.exe with absolute path to ensure it's found
8 | exec /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -c "cd '$WIN_PATH'; npm run build; node build/index.js"
9 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "moduleResolution": "node",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "declaration": true,
13 | "noImplicitAny": true
14 | },
15 | "include": ["src/**/*"],
16 | "exclude": ["node_modules", "build"]
17 | }
18 |
```
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | globals: true,
6 | environment: 'node',
7 | coverage: {
8 | provider: 'v8',
9 | reporter: ['text', 'html'],
10 | exclude: [
11 | 'node_modules/**',
12 | 'build/**',
13 | '**/*.d.ts',
14 | 'vitest.config.ts'
15 | ]
16 | },
17 | include: ['src/**/*.test.ts'],
18 | exclude: ['node_modules', 'build']
19 | }
20 | })
21 |
```
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
```yaml
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | open-pull-requests-limit: 10
8 | versioning-strategy: increase
9 | groups:
10 | dev-dependencies:
11 | patterns:
12 | - "@types/*"
13 | - "vitest"
14 | - "@vitest/*"
15 | production-dependencies:
16 | patterns:
17 | - "@modelcontextprotocol/*"
18 | - "@nut-tree/*"
19 | - "sharp"
20 | - "jimp"
21 | commit-message:
22 | prefix: "chore"
23 | include: "scope"
24 |
```
--------------------------------------------------------------------------------
/test/test-results.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "buttonClicks": [
3 | {
4 | "timestamp": "2025-04-26T21:31:57.589Z",
5 | "buttonId": "A",
6 | "count": 1
7 | },
8 | {
9 | "timestamp": "2025-04-26T23:50:04.745Z",
10 | "buttonId": "8",
11 | "count": 1
12 | }
13 | ],
14 | "sequences": [
15 | {
16 | "timestamp": "2025-04-26T21:31:57.591Z",
17 | "sequence": "A"
18 | },
19 | {
20 | "timestamp": "2025-04-26T23:50:04.747Z",
21 | "sequence": "A8"
22 | }
23 | ],
24 | "finalSequence": "A8",
25 | "startTime": "2025-04-26T21:31:42.896Z",
26 | "endTime": "2025-04-26T23:50:04.747Z"
27 | }
```
--------------------------------------------------------------------------------
/src/handlers/tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2 | import { setupTools as setupToolsWithZod } from './tools.zod.js';
3 | import { AutomationProvider } from '../interfaces/provider.js';
4 |
5 | /**
6 | * Set up automation tools on the MCP server using Zod validation.
7 | * This function provides robust validation with better error messages.
8 | *
9 | * @param server The Model Context Protocol server instance
10 | * @param provider The automation provider implementation
11 | */
12 | export function setupTools(server: Server, provider: AutomationProvider): void {
13 | setupToolsWithZod(server, provider);
14 | }
15 |
```
--------------------------------------------------------------------------------
/src/types/responses.ts:
--------------------------------------------------------------------------------
```typescript
1 | interface ImageContent {
2 | type: 'image';
3 | data: Buffer | string; // Buffer for binary data, string for base64
4 | mimeType: string;
5 | encoding?: 'binary' | 'base64'; // Specify the encoding type
6 | }
7 |
8 | export interface ScreenshotResponse {
9 | screenshot: Buffer | string; // Buffer for binary data, string for base64
10 | timestamp: string;
11 | encoding: 'binary' | 'base64';
12 | }
13 |
14 | export interface WindowsControlResponse {
15 | success: boolean;
16 | message: string;
17 | data?: unknown;
18 | screenshot?: Buffer | string; // Buffer for binary data, string for base64
19 | content?: ImageContent[]; // MCP image content for screenshots
20 | encoding?: 'binary' | 'base64'; // Specify the encoding type
21 | }
22 |
```
--------------------------------------------------------------------------------
/test-autohotkey-direct.js:
--------------------------------------------------------------------------------
```javascript
1 | // Direct test of AutoHotkey provider without factory
2 | import { AutoHotkeyProvider } from './build/providers/autohotkey/index.js';
3 |
4 | // Create the provider directly
5 | const provider = new AutoHotkeyProvider();
6 |
7 | console.log('AutoHotkey provider created successfully');
8 | console.log('Provider has keyboard:', !!provider.keyboard);
9 | console.log('Provider has mouse:', !!provider.mouse);
10 | console.log('Provider has screen:', !!provider.screen);
11 | console.log('Provider has clipboard:', !!provider.clipboard);
12 |
13 | // Test a simple keyboard operation
14 | console.log('\nTesting keyboard.typeText method...');
15 | const result = provider.keyboard.typeText({ text: 'Hello from AutoHotkey!' });
16 | console.log('Result:', result);
17 |
18 | console.log('\nAutoHotkey provider is ready to use!');
```
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 | schedule:
9 | - cron: '30 1 * * 0'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: windows-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ 'typescript' ]
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v4
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v3
31 | with:
32 | languages: ${{ matrix.language }}
33 |
34 | - name: Autobuild
35 | uses: github/codeql-action/autobuild@v3
36 |
37 | - name: Perform CodeQL Analysis
38 | uses: github/codeql-action/analyze@v3
39 | with:
40 | category: "/language:${{matrix.language}}"
41 |
```
--------------------------------------------------------------------------------
/test-autohotkey.js:
--------------------------------------------------------------------------------
```javascript
1 | // Simple test script to verify AutoHotkey provider works
2 | import { createAutomationProvider } from './build/providers/factory.js';
3 |
4 | // Use AutoHotkey as the provider
5 | const provider = createAutomationProvider({ provider: 'autohotkey' });
6 |
7 | console.log('AutoHotkey provider created successfully');
8 | console.log('Provider has keyboard:', !!provider.keyboard);
9 | console.log('Provider has mouse:', !!provider.mouse);
10 | console.log('Provider has screen:', !!provider.screen);
11 | console.log('Provider has clipboard:', !!provider.clipboard);
12 |
13 | // You can also use modular configuration
14 | const modularProvider = createAutomationProvider({
15 | providers: {
16 | keyboard: 'autohotkey',
17 | mouse: 'autohotkey',
18 | screen: 'autohotkey',
19 | clipboard: 'autohotkey',
20 | },
21 | });
22 |
23 | console.log('\nModular provider created successfully');
```
--------------------------------------------------------------------------------
/scripts/generate-test-certs.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Generate self-signed certificates for testing HTTPS support
4 | # NOT FOR PRODUCTION USE
5 |
6 | echo "Generating self-signed certificates for testing..."
7 |
8 | # Create certs directory if it doesn't exist
9 | mkdir -p test/certs
10 |
11 | # Generate private key
12 | openssl genrsa -out test/certs/key.pem 2048
13 |
14 | # Generate certificate signing request
15 | openssl req -new -key test/certs/key.pem -out test/certs/csr.pem \
16 | -subj "/C=US/ST=Test/L=Test/O=MCPControl/OU=Test/CN=localhost"
17 |
18 | # Generate self-signed certificate
19 | openssl x509 -req -days 365 -in test/certs/csr.pem \
20 | -signkey test/certs/key.pem -out test/certs/cert.pem
21 |
22 | # Clean up CSR
23 | rm test/certs/csr.pem
24 |
25 | echo "Test certificates generated successfully!"
26 | echo "Certificate: test/certs/cert.pem"
27 | echo "Private key: test/certs/key.pem"
28 | echo ""
29 | echo "To test HTTPS support, run:"
30 | echo "node build/index.js --sse --https --cert test/certs/cert.pem --key test/certs/key.pem"
```
--------------------------------------------------------------------------------
/src/tools/screenshot.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createAutomationProvider } from '../providers/factory.js';
2 | import { ScreenshotOptions } from '../types/common.js';
3 | import { WindowsControlResponse } from '../types/responses.js';
4 |
5 | /**
6 | * Captures a screenshot with various options for optimization
7 | *
8 | * @param options Optional configuration for the screenshot
9 | * @returns Promise resolving to a WindowsControlResponse with the screenshot data
10 | */
11 | export async function getScreenshot(options?: ScreenshotOptions): Promise<WindowsControlResponse> {
12 | try {
13 | // Create a provider instance to handle the screenshot
14 | const provider = createAutomationProvider();
15 |
16 | // Delegate to the provider's screenshot implementation
17 | const result = await provider.screen.getScreenshot(options);
18 |
19 | // Return the result directly from the provider
20 | return result;
21 | } catch (error) {
22 | return {
23 | success: false,
24 | message: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
25 | };
26 | }
27 | }
28 |
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AutomationProvider } from '../../interfaces/provider.js';
2 | import {
3 | KeyboardAutomation,
4 | MouseAutomation,
5 | ScreenAutomation,
6 | ClipboardAutomation,
7 | } from '../../interfaces/automation.js';
8 | import { AutoHotkeyKeyboardAutomation } from './keyboard.js';
9 | import { AutoHotkeyMouseAutomation } from './mouse.js';
10 | import { AutoHotkeyScreenAutomation } from './screen.js';
11 | import { AutoHotkeyClipboardAutomation } from './clipboard.js';
12 |
13 | /**
14 | * AutoHotkey implementation of the AutomationProvider
15 | *
16 | * NOTE: This provider requires AutoHotkey v2.0+ to be installed on the system.
17 | * It executes AutoHotkey scripts to perform automation tasks.
18 | */
19 | export class AutoHotkeyProvider implements AutomationProvider {
20 | keyboard: KeyboardAutomation;
21 | mouse: MouseAutomation;
22 | screen: ScreenAutomation;
23 | clipboard: ClipboardAutomation;
24 |
25 | constructor() {
26 | this.keyboard = new AutoHotkeyKeyboardAutomation();
27 | this.mouse = new AutoHotkeyMouseAutomation();
28 | this.screen = new AutoHotkeyScreenAutomation();
29 | this.clipboard = new AutoHotkeyClipboardAutomation();
30 | }
31 | }
32 |
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Configuration interface for automation settings
3 | */
4 | export interface AutomationConfig {
5 | /**
6 | * Legacy: The provider to use for all automation
7 | * Currently supported: 'keysender'
8 | */
9 | provider?: string;
10 |
11 | /**
12 | * New: Modular provider configuration
13 | * Allows mixing different providers for different components
14 | */
15 | providers?: {
16 | keyboard?: string;
17 | mouse?: string;
18 | screen?: string;
19 | clipboard?: string;
20 | };
21 | }
22 |
23 | /**
24 | * Load configuration from environment variables
25 | */
26 | export function loadConfig(): AutomationConfig {
27 | // Check for new modular configuration
28 | const keyboard = process.env.AUTOMATION_KEYBOARD_PROVIDER;
29 | const mouse = process.env.AUTOMATION_MOUSE_PROVIDER;
30 | const screen = process.env.AUTOMATION_SCREEN_PROVIDER;
31 | const clipboard = process.env.AUTOMATION_CLIPBOARD_PROVIDER;
32 |
33 | if (keyboard || mouse || screen || clipboard) {
34 | return {
35 | providers: {
36 | keyboard,
37 | mouse,
38 | screen,
39 | clipboard,
40 | },
41 | };
42 | }
43 |
44 | // Fall back to legacy configuration
45 | return {
46 | provider: process.env.AUTOMATION_PROVIDER || 'keysender',
47 | };
48 | }
49 |
```
--------------------------------------------------------------------------------
/src/providers/factory.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { createAutomationProvider } from './factory.js';
3 | import { KeysenderProvider } from './keysender/index.js';
4 |
5 | // Mock the providers
6 | vi.mock('./keysender/index.js', () => {
7 | return {
8 | KeysenderProvider: vi.fn().mockImplementation(() => ({
9 | keyboard: {},
10 | mouse: {},
11 | screen: {},
12 | clipboard: {},
13 | })),
14 | };
15 | });
16 |
17 | describe('createAutomationProvider', () => {
18 | it('should create KeysenderProvider by default', () => {
19 | const provider = createAutomationProvider();
20 | expect(KeysenderProvider).toHaveBeenCalled();
21 | expect(provider).toBeDefined();
22 | });
23 |
24 | it('should create KeysenderProvider when explicitly specified', () => {
25 | const provider = createAutomationProvider({ provider: 'keysender' });
26 | expect(KeysenderProvider).toHaveBeenCalled();
27 | expect(provider).toBeDefined();
28 | });
29 |
30 | it('should be case insensitive for KeysenderProvider', () => {
31 | const provider = createAutomationProvider({ provider: 'KeYsEnDeR' });
32 | expect(KeysenderProvider).toHaveBeenCalled();
33 | expect(provider).toBeDefined();
34 | });
35 |
36 | it('should throw error for unknown provider type', () => {
37 | expect(() => createAutomationProvider({ provider: 'unknown' })).toThrow(
38 | 'Unknown provider type: unknown',
39 | );
40 | });
41 | });
42 |
```
--------------------------------------------------------------------------------
/src/providers/keysender/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AutomationProvider } from '../../interfaces/provider.js';
2 | import {
3 | KeyboardAutomation,
4 | MouseAutomation,
5 | ScreenAutomation,
6 | ClipboardAutomation,
7 | } from '../../interfaces/automation.js';
8 | import { KeysenderKeyboardAutomation } from './keyboard.js';
9 | import { KeysenderMouseAutomation } from './mouse.js';
10 | import { KeysenderScreenAutomation } from './screen.js';
11 | import { KeysenderClipboardAutomation } from './clipboard.js';
12 |
13 | /**
14 | * Keysender implementation of the AutomationProvider
15 | *
16 | * NOTE: This provider requires the Windows operating system to compile native dependencies.
17 | * Building this module on non-Windows platforms will fail.
18 | * Development requires:
19 | * - Node.js installed via the official Windows installer (includes necessary build tools)
20 | * - node-gyp installed globally (npm install -g node-gyp)
21 | * - cmake-js installed globally (npm install -g cmake-js)
22 | */
23 | export class KeysenderProvider implements AutomationProvider {
24 | keyboard: KeyboardAutomation;
25 | mouse: MouseAutomation;
26 | screen: ScreenAutomation;
27 | clipboard: ClipboardAutomation;
28 |
29 | constructor() {
30 | this.keyboard = new KeysenderKeyboardAutomation();
31 | this.mouse = new KeysenderMouseAutomation();
32 | this.screen = new KeysenderScreenAutomation();
33 | this.clipboard = new KeysenderClipboardAutomation();
34 | }
35 | }
36 |
```
--------------------------------------------------------------------------------
/src/tools/mouse.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import { clickAt } from './mouse.js';
3 |
4 | // Mock the provider
5 | vi.mock('../providers/factory.js', () => ({
6 | createAutomationProvider: () => ({
7 | mouse: {
8 | clickAt: vi.fn().mockImplementation((x, y, button) => ({
9 | success: true,
10 | message: `Clicked ${button} button at position (${x}, ${y})`,
11 | })),
12 | getCursorPosition: vi.fn().mockReturnValue({
13 | success: true,
14 | message: 'Current cursor position',
15 | data: { x: 10, y: 20 },
16 | }),
17 | },
18 | }),
19 | }));
20 |
21 | describe('Mouse Tools', () => {
22 | beforeEach(() => {
23 | vi.clearAllMocks();
24 | });
25 |
26 | describe('clickAt', () => {
27 | it('should click at the specified position', () => {
28 | const result = clickAt(100, 200);
29 |
30 | // Verify success response
31 | expect(result.success).toBe(true);
32 | expect(result.message).toContain('Clicked left button at position (100, 200)');
33 | });
34 |
35 | it('should support different mouse buttons', () => {
36 | const result = clickAt(100, 200, 'right');
37 |
38 | expect(result.success).toBe(true);
39 | expect(result.message).toContain('Clicked right button');
40 | });
41 |
42 | it('should handle invalid coordinates', () => {
43 | const result = clickAt(NaN, 200);
44 |
45 | expect(result.success).toBe(false);
46 | expect(result.message).toBe('Invalid coordinates provided');
47 | });
48 | });
49 | });
50 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-control",
3 | "version": "0.2.0",
4 | "description": "Windows control server for the Model Context Protocol",
5 | "license": "MIT",
6 | "type": "module",
7 | "main": "build/index.js",
8 | "bin": "build/index.js",
9 | "scripts": {
10 | "build": "tsc",
11 | "build:all": "node scripts/build.js",
12 | "start": "node build/index.js",
13 | "dev": "tsc -w",
14 | "test": "vitest run",
15 | "test:watch": "vitest",
16 | "test:coverage": "vitest run --coverage",
17 | "lint": "eslint src --ext .ts",
18 | "lint:fix": "eslint src --ext .ts --fix",
19 | "format": "prettier --write \"src/**/*.{ts,js}\" \"test/**/*.js\"",
20 | "format:check": "prettier --check \"src/**/*.{ts,js}\" \"test/**/*.js\"",
21 | "prepare": "husky"
22 | },
23 | "dependencies": {
24 | "@modelcontextprotocol/sdk": "^1.16.0",
25 | "clipboardy": "^4.0.0",
26 | "keysender": "^2.3.0",
27 | "sharp": "^0.34.3",
28 | "ulid": "^3.0.1",
29 | "uuid": "^11.1.0",
30 | "zod": "^3.25.1"
31 | },
32 | "devDependencies": {
33 | "@eslint/js": "^9.31.0",
34 | "@types/express": "^5.0.2",
35 | "@types/node": "^22.15.19",
36 | "@types/uuid": "^10.0.0",
37 | "@typescript-eslint/eslint-plugin": "^8.37.0",
38 | "@typescript-eslint/parser": "^8.36.0",
39 | "@vitest/coverage-v8": "^3.1.3",
40 | "audit-ci": "^7.1.0",
41 | "eslint": "^9.31.0",
42 | "husky": "^9.1.7",
43 | "lint-staged": "^16.1.2",
44 | "prettier": "^3.5.3",
45 | "typescript": "^5.8.3",
46 | "typescript-eslint": "^8.32.1",
47 | "vitest": "^3.0.8"
48 | }
49 | }
50 |
```
--------------------------------------------------------------------------------
/src/tools/clipboard.ts:
--------------------------------------------------------------------------------
```typescript
1 | import clipboardy from 'clipboardy';
2 | import { ClipboardInput } from '../types/common.js';
3 |
4 | export async function getClipboardContent(): Promise<{
5 | success: boolean;
6 | content?: string;
7 | error?: string;
8 | }> {
9 | try {
10 | const content = await clipboardy.read();
11 | return {
12 | success: true,
13 | content,
14 | };
15 | } catch (error) {
16 | return {
17 | success: false,
18 | error: error instanceof Error ? error.message : String(error),
19 | };
20 | }
21 | }
22 |
23 | export async function setClipboardContent(
24 | input: ClipboardInput,
25 | ): Promise<{ success: boolean; error?: string }> {
26 | try {
27 | await clipboardy.write(input.text);
28 | return {
29 | success: true,
30 | };
31 | } catch (error) {
32 | return {
33 | success: false,
34 | error: error instanceof Error ? error.message : String(error),
35 | };
36 | }
37 | }
38 |
39 | export async function hasClipboardText(): Promise<{
40 | success: boolean;
41 | hasText?: boolean;
42 | error?: string;
43 | }> {
44 | try {
45 | const content = await clipboardy.read();
46 | return {
47 | success: true,
48 | hasText: content.length > 0,
49 | };
50 | } catch (error) {
51 | return {
52 | success: false,
53 | error: error instanceof Error ? error.message : String(error),
54 | };
55 | }
56 | }
57 |
58 | export async function clearClipboard(): Promise<{ success: boolean; error?: string }> {
59 | try {
60 | await clipboardy.write('');
61 | return {
62 | success: true,
63 | };
64 | } catch (error) {
65 | return {
66 | success: false,
67 | error: error instanceof Error ? error.message : String(error),
68 | };
69 | }
70 | }
71 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import eslint from '@eslint/js';
2 | import tseslint from 'typescript-eslint';
3 |
4 | // Create a simplified configuration that only lints src TypeScript files
5 | export default tseslint.config(
6 | {
7 | ignores: [
8 | 'build/**',
9 | 'coverage/**',
10 | '*.html',
11 | 'mcpcontrol-wrapper.sh',
12 | 'eslint.config.js',
13 | '.github/**',
14 | 'scripts/**',
15 | 'test/**'
16 | ]
17 | },
18 | {
19 | files: ['src/**/*.ts'],
20 | extends: [
21 | eslint.configs.recommended,
22 | ...tseslint.configs.recommended,
23 | ...tseslint.configs.recommendedTypeChecked
24 | ],
25 | languageOptions: {
26 | parserOptions: {
27 | project: true,
28 | tsconfigRootDir: import.meta.dirname,
29 | },
30 | },
31 | rules: {
32 | 'no-console': 'off',
33 | '@typescript-eslint/no-explicit-any': 'warn',
34 | '@typescript-eslint/explicit-module-boundary-types': 'error',
35 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
36 | }
37 | },
38 | {
39 | // Test files specific configuration
40 | files: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
41 | rules: {
42 | '@typescript-eslint/no-explicit-any': 'off',
43 | '@typescript-eslint/no-non-null-assertion': 'off',
44 | '@typescript-eslint/no-unsafe-assignment': 'off',
45 | '@typescript-eslint/no-unsafe-member-access': 'off',
46 | '@typescript-eslint/no-unsafe-call': 'off',
47 | '@typescript-eslint/no-unsafe-return': 'off',
48 | '@typescript-eslint/no-unsafe-argument': 'off',
49 | '@typescript-eslint/unbound-method': 'off',
50 | },
51 | }
52 | );
```
--------------------------------------------------------------------------------
/src/interfaces/automation.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | MousePosition,
3 | KeyboardInput,
4 | KeyCombination,
5 | KeyHoldOperation,
6 | ScreenshotOptions,
7 | ClipboardInput,
8 | } from '../types/common.js';
9 | import { WindowsControlResponse } from '../types/responses.js';
10 |
11 | export interface KeyboardAutomation {
12 | typeText(input: KeyboardInput): WindowsControlResponse;
13 | pressKey(key: string): WindowsControlResponse;
14 | pressKeyCombination(combination: KeyCombination): Promise<WindowsControlResponse>;
15 | holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse>;
16 | }
17 |
18 | export interface MouseAutomation {
19 | moveMouse(position: MousePosition): WindowsControlResponse;
20 | clickMouse(button?: 'left' | 'right' | 'middle'): WindowsControlResponse;
21 | doubleClick(position?: MousePosition): WindowsControlResponse;
22 | getCursorPosition(): WindowsControlResponse;
23 | scrollMouse(amount: number): WindowsControlResponse;
24 | dragMouse(
25 | from: MousePosition,
26 | to: MousePosition,
27 | button?: 'left' | 'right' | 'middle',
28 | ): WindowsControlResponse;
29 | clickAt(x: number, y: number, button?: 'left' | 'right' | 'middle'): WindowsControlResponse;
30 | }
31 |
32 | export interface ScreenAutomation {
33 | getScreenSize(): WindowsControlResponse;
34 | getActiveWindow(): WindowsControlResponse;
35 | focusWindow(title: string): WindowsControlResponse;
36 | resizeWindow(title: string, width: number, height: number): Promise<WindowsControlResponse>;
37 | repositionWindow(title: string, x: number, y: number): Promise<WindowsControlResponse>;
38 | getScreenshot(options?: ScreenshotOptions): Promise<WindowsControlResponse>;
39 | }
40 |
41 | export interface ClipboardAutomation {
42 | getClipboardContent(): Promise<WindowsControlResponse>;
43 | setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse>;
44 | hasClipboardText(): Promise<WindowsControlResponse>;
45 | clearClipboard(): Promise<WindowsControlResponse>;
46 | }
47 |
```
--------------------------------------------------------------------------------
/src/tools/screen.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { WindowsControlResponse } from '../types/responses.js';
2 | import { createAutomationProvider } from '../providers/factory.js';
3 |
4 | export function getScreenSize(): WindowsControlResponse {
5 | try {
6 | const provider = createAutomationProvider();
7 | return provider.screen.getScreenSize();
8 | } catch (error) {
9 | return {
10 | success: false,
11 | message: `Failed to get screen size: ${error instanceof Error ? error.message : String(error)}`,
12 | };
13 | }
14 | }
15 |
16 | export function getActiveWindow(): WindowsControlResponse {
17 | try {
18 | const provider = createAutomationProvider();
19 | return provider.screen.getActiveWindow();
20 | } catch (error) {
21 | return {
22 | success: false,
23 | message: `Failed to get active window information: ${error instanceof Error ? error.message : String(error)}`,
24 | };
25 | }
26 | }
27 |
28 | export function focusWindow(title: string): WindowsControlResponse {
29 | try {
30 | const provider = createAutomationProvider();
31 | return provider.screen.focusWindow(title);
32 | } catch (error) {
33 | return {
34 | success: false,
35 | message: `Failed to focus window: ${error instanceof Error ? error.message : String(error)}`,
36 | };
37 | }
38 | }
39 |
40 | export async function resizeWindow(
41 | title: string,
42 | width: number,
43 | height: number,
44 | ): Promise<WindowsControlResponse> {
45 | try {
46 | const provider = createAutomationProvider();
47 | return await provider.screen.resizeWindow(title, width, height);
48 | } catch (error) {
49 | return {
50 | success: false,
51 | message: `Failed to resize window: ${error instanceof Error ? error.message : String(error)}`,
52 | };
53 | }
54 | }
55 |
56 | export async function repositionWindow(
57 | title: string,
58 | x: number,
59 | y: number,
60 | ): Promise<WindowsControlResponse> {
61 | try {
62 | const provider = createAutomationProvider();
63 | return await provider.screen.repositionWindow(title, x, y);
64 | } catch (error) {
65 | return {
66 | success: false,
67 | message: `Failed to reposition window: ${error instanceof Error ? error.message : String(error)}`,
68 | };
69 | }
70 | }
71 |
```
--------------------------------------------------------------------------------
/src/types/common.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface MousePosition {
2 | x: number;
3 | y: number;
4 | }
5 |
6 | export interface KeyboardInput {
7 | text: string;
8 | }
9 |
10 | export interface KeyCombination {
11 | keys: string[]; // Array of keys to be pressed together, e.g. ["control", "c"]
12 | }
13 |
14 | export interface KeyHoldOperation {
15 | key: string; // The key to hold
16 | duration?: number; // Duration in milliseconds (optional when state is 'up')
17 | state: 'down' | 'up'; // Whether to press down or release the key
18 | }
19 |
20 | export interface WindowInfo {
21 | title: string;
22 | position: {
23 | x: number;
24 | y: number;
25 | };
26 | size: {
27 | width: number;
28 | height: number;
29 | };
30 | }
31 |
32 | export interface ClipboardInput {
33 | text: string;
34 | }
35 |
36 | // Type for mouse button mapping
37 | export type ButtonMap = {
38 | [key: string]: string;
39 | left: string;
40 | right: string;
41 | middle: string;
42 | };
43 |
44 | // New types for screen search functionality
45 | export interface ImageSearchOptions {
46 | confidence?: number; // Match confidence threshold (0-1)
47 | searchRegion?: {
48 | // Region to search within
49 | x: number;
50 | y: number;
51 | width: number;
52 | height: number;
53 | };
54 | waitTime?: number; // Max time to wait for image in ms
55 | }
56 |
57 | export interface ImageSearchResult {
58 | location: {
59 | x: number;
60 | y: number;
61 | };
62 | confidence: number;
63 | width: number;
64 | height: number;
65 | }
66 |
67 | export interface HighlightOptions {
68 | duration?: number; // Duration to show highlight in ms
69 | color?: string; // Color of highlight (hex format)
70 | }
71 |
72 | // New interface for screenshot options
73 | export interface ScreenshotOptions {
74 | region?: {
75 | x: number;
76 | y: number;
77 | width: number;
78 | height: number;
79 | };
80 | quality?: number; // JPEG quality (1-100), only used if format is 'jpeg'
81 | format?: 'png' | 'jpeg'; // Output format
82 | grayscale?: boolean; // Convert to grayscale
83 | resize?: {
84 | // Resize options
85 | width?: number; // Target width (maintains aspect ratio if only one dimension provided)
86 | height?: number; // Target height
87 | fit?: 'contain' | 'cover' | 'fill' | 'inside' | 'outside'; // Resize fit option
88 | };
89 | compressionLevel?: number; // PNG compression level (0-9), only used if format is 'png'
90 | }
91 |
```
--------------------------------------------------------------------------------
/src/providers/clipboard/clipboardy/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import clipboardy from 'clipboardy';
2 | import { ClipboardInput } from '../../../types/common.js';
3 | import { WindowsControlResponse } from '../../../types/responses.js';
4 | import { ClipboardAutomation } from '../../../interfaces/automation.js';
5 |
6 | /**
7 | * Clipboardy implementation of the ClipboardAutomation interface
8 | *
9 | * Uses the clipboardy library for cross-platform clipboard operations
10 | */
11 | export class ClipboardyProvider implements ClipboardAutomation {
12 | async getClipboardContent(): Promise<WindowsControlResponse> {
13 | try {
14 | const content = await clipboardy.read();
15 | return {
16 | success: true,
17 | message: 'Clipboard content retrieved',
18 | data: content,
19 | };
20 | } catch (error) {
21 | return {
22 | success: false,
23 | message: `Failed to get clipboard content: ${error instanceof Error ? error.message : String(error)}`,
24 | };
25 | }
26 | }
27 |
28 | async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
29 | try {
30 | await clipboardy.write(input.text);
31 | return {
32 | success: true,
33 | message: 'Clipboard content set',
34 | };
35 | } catch (error) {
36 | return {
37 | success: false,
38 | message: `Failed to set clipboard content: ${error instanceof Error ? error.message : String(error)}`,
39 | };
40 | }
41 | }
42 |
43 | async hasClipboardText(): Promise<WindowsControlResponse> {
44 | try {
45 | const content = await clipboardy.read();
46 | const hasText = content.length > 0;
47 | return {
48 | success: true,
49 | message: `Clipboard ${hasText ? 'has' : 'does not have'} text`,
50 | data: hasText,
51 | };
52 | } catch (error) {
53 | return {
54 | success: false,
55 | message: `Failed to check clipboard: ${error instanceof Error ? error.message : String(error)}`,
56 | };
57 | }
58 | }
59 |
60 | async clearClipboard(): Promise<WindowsControlResponse> {
61 | try {
62 | await clipboardy.write('');
63 | return {
64 | success: true,
65 | message: 'Clipboard cleared',
66 | };
67 | } catch (error) {
68 | return {
69 | success: false,
70 | message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
71 | };
72 | }
73 | }
74 | }
75 |
```
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import { execSync } from 'child_process';
4 | import fs from 'fs';
5 | import path from 'path';
6 |
7 | /**
8 | * Build script for MCPControl
9 | * Handles the complete build process
10 | */
11 |
12 | // ANSI color codes for terminal output
13 | const colors = {
14 | reset: '\x1b[0m',
15 | green: '\x1b[32m',
16 | yellow: '\x1b[33m',
17 | blue: '\x1b[34m',
18 | red: '\x1b[31m',
19 | cyan: '\x1b[36m'
20 | };
21 |
22 | /**
23 | * Executes a shell command and pipes output to console
24 | * @param {string} command - Command to execute
25 | * @param {Object} options - Options for child_process.execSync
26 | * @returns {Buffer} Command output
27 | */
28 | function execute(command, options = {}) {
29 | console.log(`${colors.cyan}> ${command}${colors.reset}`);
30 |
31 | const defaultOptions = {
32 | stdio: 'inherit',
33 | ...options
34 | };
35 |
36 | try {
37 | return execSync(command, defaultOptions);
38 | } catch (error) {
39 | console.error(`${colors.red}Command failed: ${command}${colors.reset}`);
40 | process.exit(1);
41 | }
42 | }
43 |
44 | // Main build process
45 | function build() {
46 | console.log(`\n${colors.green}===== MCPControl Build Process =====${colors.reset}\n`);
47 |
48 | // Install dependencies using npm ci for faster and deterministic installs
49 | console.log(`\n${colors.blue}Installing dependencies...${colors.reset}`);
50 |
51 | // Check if package-lock.json exists before running npm ci
52 | if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) {
53 | try {
54 | // Use a different execution method for npm ci to allow falling back to npm install
55 | console.log(`${colors.cyan}> npm ci${colors.reset}`);
56 | execSync('npm ci', { stdio: 'inherit' });
57 | } catch (error) {
58 | console.warn(`\n${colors.yellow}Warning: npm ci failed, falling back to npm install${colors.reset}`);
59 | execute('npm install');
60 | }
61 | } else {
62 | console.log(`\n${colors.yellow}package-lock.json not found, using npm install instead${colors.reset}`);
63 | execute('npm install');
64 | }
65 |
66 | // Build MCPControl
67 | console.log(`\n${colors.blue}Building MCPControl...${colors.reset}`);
68 | execute('npm run build');
69 |
70 | console.log(`\n${colors.green}===== Build Complete =====${colors.reset}`);
71 | console.log(`${colors.green}MCPControl has been successfully built!${colors.reset}\n`);
72 | }
73 |
74 | // Run the build process
75 | build();
76 |
```
--------------------------------------------------------------------------------
/src/providers/keysender/clipboard.ts:
--------------------------------------------------------------------------------
```typescript
1 | import clipboardy from 'clipboardy';
2 | import { ClipboardInput } from '../../types/common.js';
3 | import { WindowsControlResponse } from '../../types/responses.js';
4 | import { ClipboardAutomation } from '../../interfaces/automation.js';
5 |
6 | /**
7 | * Keysender implementation of the ClipboardAutomation interface
8 | *
9 | * Note: Since keysender doesn't provide direct clipboard functionality,
10 | * we use the clipboardy library (same as the NutJS implementation)
11 | */
12 | export class KeysenderClipboardAutomation implements ClipboardAutomation {
13 | async getClipboardContent(): Promise<WindowsControlResponse> {
14 | try {
15 | const content = await clipboardy.read();
16 | return {
17 | success: true,
18 | message: 'Clipboard content retrieved',
19 | data: content,
20 | };
21 | } catch (error) {
22 | return {
23 | success: false,
24 | message: `Failed to get clipboard content: ${error instanceof Error ? error.message : String(error)}`,
25 | };
26 | }
27 | }
28 |
29 | async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
30 | try {
31 | await clipboardy.write(input.text);
32 | return {
33 | success: true,
34 | message: 'Clipboard content set',
35 | };
36 | } catch (error) {
37 | return {
38 | success: false,
39 | message: `Failed to set clipboard content: ${error instanceof Error ? error.message : String(error)}`,
40 | };
41 | }
42 | }
43 |
44 | async hasClipboardText(): Promise<WindowsControlResponse> {
45 | try {
46 | const content = await clipboardy.read();
47 | const hasText = content.length > 0;
48 | return {
49 | success: true,
50 | message: `Clipboard ${hasText ? 'has' : 'does not have'} text`,
51 | data: hasText,
52 | };
53 | } catch (error) {
54 | return {
55 | success: false,
56 | message: `Failed to check clipboard: ${error instanceof Error ? error.message : String(error)}`,
57 | };
58 | }
59 | }
60 |
61 | async clearClipboard(): Promise<WindowsControlResponse> {
62 | try {
63 | await clipboardy.write('');
64 | return {
65 | success: true,
66 | message: 'Clipboard cleared',
67 | };
68 | } catch (error) {
69 | return {
70 | success: false,
71 | message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
72 | };
73 | }
74 | }
75 | }
76 |
```
--------------------------------------------------------------------------------
/src/tools/keyboard.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { KeyboardInput, KeyCombination, KeyHoldOperation } from '../types/common.js';
2 | import { WindowsControlResponse } from '../types/responses.js';
3 | import { createAutomationProvider } from '../providers/factory.js';
4 | import {
5 | MAX_TEXT_LENGTH,
6 | KeySchema,
7 | KeyCombinationSchema,
8 | KeyHoldOperationSchema,
9 | } from './validation.zod.js';
10 |
11 | // Get the automation provider
12 | const provider = createAutomationProvider();
13 |
14 | export function typeText(input: KeyboardInput): WindowsControlResponse {
15 | try {
16 | // Validate text length
17 | if (!input.text) {
18 | throw new Error('Text is required');
19 | }
20 |
21 | if (input.text.length > MAX_TEXT_LENGTH) {
22 | throw new Error(`Text too long: ${input.text.length} characters (max ${MAX_TEXT_LENGTH})`);
23 | }
24 |
25 | return provider.keyboard.typeText(input);
26 | } catch (error) {
27 | return {
28 | success: false,
29 | message: `Failed to type text: ${error instanceof Error ? error.message : String(error)}`,
30 | };
31 | }
32 | }
33 |
34 | export function pressKey(key: string): WindowsControlResponse {
35 | try {
36 | // Validate key using Zod schema
37 | KeySchema.parse(key);
38 |
39 | return provider.keyboard.pressKey(key);
40 | } catch (error) {
41 | return {
42 | success: false,
43 | message: `Failed to press key: ${error instanceof Error ? error.message : String(error)}`,
44 | };
45 | }
46 | }
47 |
48 | export async function pressKeyCombination(
49 | combination: KeyCombination,
50 | ): Promise<WindowsControlResponse> {
51 | try {
52 | // Validate the key combination using Zod schema
53 | KeyCombinationSchema.parse(combination);
54 |
55 | return await provider.keyboard.pressKeyCombination(combination);
56 | } catch (error) {
57 | return {
58 | success: false,
59 | message: `Failed to press key combination: ${error instanceof Error ? error.message : String(error)}`,
60 | };
61 | }
62 | }
63 |
64 | export async function holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse> {
65 | try {
66 | // Validate key hold operation using Zod schema
67 | KeyHoldOperationSchema.parse(operation);
68 |
69 | return await provider.keyboard.holdKey(operation);
70 | } catch (error) {
71 | return {
72 | success: false,
73 | message: `Failed to ${operation.state} key ${operation.key}: ${
74 | error instanceof Error ? error.message : String(error)
75 | }`,
76 | };
77 | }
78 | }
79 |
```
--------------------------------------------------------------------------------
/src/types/keysender.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Type definitions for keysender module
3 | */
4 |
5 | declare module 'keysender' {
6 | interface ScreenSize {
7 | width: number;
8 | height: number;
9 | }
10 |
11 | interface WindowInfo {
12 | title: string;
13 | className: string;
14 | handle: number;
15 | }
16 |
17 | interface ViewInfo {
18 | x: number;
19 | y: number;
20 | width: number;
21 | height: number;
22 | }
23 |
24 | interface CaptureResult {
25 | data: Buffer;
26 | width: number;
27 | height: number;
28 | }
29 |
30 | interface MousePosition {
31 | x: number;
32 | y: number;
33 | }
34 |
35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
36 | class Hardware {
37 | constructor(windowHandle?: number);
38 |
39 | keyboard: {
40 | printText(text: string): Promise<void>;
41 | sendKey(key: string): Promise<void>;
42 | toggleKey(key: string | string[], down: boolean, delay?: Delay): Promise<void>;
43 | };
44 |
45 | mouse: {
46 | moveTo(x: number, y: number): Promise<void>;
47 | click(button?: string): Promise<void>;
48 | toggle(button: string, down: boolean): Promise<void>;
49 | getPos(): MousePosition;
50 | scrollWheel(amount: number): Promise<void>;
51 | };
52 |
53 | workwindow: {
54 | get(): WindowInfo;
55 | set(handle: number): boolean;
56 | getView(): ViewInfo;
57 | setView(view: Partial<ViewInfo>): void;
58 | setForeground(): void;
59 | isForeground(): boolean;
60 | isOpen(): boolean;
61 | capture(
62 | region?: { x: number; y: number; width: number; height: number },
63 | format?: string,
64 | ): CaptureResult;
65 | capture(format?: string): CaptureResult;
66 | };
67 | }
68 |
69 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
70 | function getScreenSize(): ScreenSize;
71 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
72 | function getAllWindows(): WindowInfo[];
73 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
74 | function getWindowChildren(handle: number): WindowInfo[];
75 |
76 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
77 | const KeyboardButton: { [key: string]: string };
78 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
79 | const MouseButton: { [key: string]: string };
80 |
81 | const keysender: {
82 | Hardware: typeof Hardware;
83 | KeyboardButton: typeof KeyboardButton;
84 | MouseButton: typeof MouseButton;
85 | getScreenSize: typeof getScreenSize;
86 | getAllWindows: typeof getAllWindows;
87 | getWindowChildren: typeof getWindowChildren;
88 | };
89 |
90 | export default keysender;
91 | }
92 |
```
--------------------------------------------------------------------------------
/docs/providers.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCPControl Automation Providers
2 |
3 | MCPControl supports multiple automation providers to give users flexibility in how they control their systems. Each provider has its own strengths and may work better in different environments.
4 |
5 | ## Available Providers
6 |
7 | ### Keysender Provider (Default)
8 |
9 | The Keysender provider uses the [keysender](https://github.com/garrettlynch/keysender) library for system automation. It provides comprehensive support for keyboard, mouse, screen, and clipboard operations.
10 |
11 | ## Selecting a Provider
12 |
13 | You can select which provider to use by setting the `AUTOMATION_PROVIDER` environment variable:
14 |
15 | ```bash
16 | # Use the Keysender provider (default)
17 | AUTOMATION_PROVIDER=keysender node build/index.js
18 | ```
19 |
20 | ### Screen Automation Considerations
21 |
22 | The Keysender provider has the following considerations for screen automation:
23 |
24 | - **Window Detection Challenges**: Getting accurate window information can be challenging, especially with:
25 | - Window handles that may not always be valid
26 | - Window titles that may be empty or not match expected values
27 | - Position and size information that may be unavailable or return extreme negative values for minimized windows
28 | - **Window Repositioning and Resizing**: Operations work but may not always report accurate results due to limitations in the underlying API
29 | - **Window Focusing**: May not work reliably for all window types or applications
30 | - **Screenshot Functionality**: May not work consistently in all environments
31 |
32 | We've implemented significant fallbacks and robust error handling for window operations, including:
33 |
34 | - Advanced window selection strategy that prioritizes common applications for better reliability
35 | - Detailed logging to help diagnose window handling issues
36 | - Fallback mechanisms when window operations don't produce the expected results
37 | - Safe property access with type checking to handle edge cases
38 |
39 | ### Recent Improvements
40 |
41 | Recent updates to the provider include:
42 |
43 | - Added a sophisticated window finding algorithm that tries multiple strategies to locate usable windows
44 | - Enhanced window resizing and repositioning with better error handling and result verification
45 | - Improved window information retrieval with multiple fallback layers for missing data
46 | - Better window focusing with proper foreground window management and status reporting
47 | - More robust error handling throughout window operations with detailed logging
48 | - Added support for child window detection and management
49 |
```
--------------------------------------------------------------------------------
/src/providers/registry.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | KeyboardAutomation,
3 | MouseAutomation,
4 | ScreenAutomation,
5 | ClipboardAutomation,
6 | } from '../interfaces/automation.js';
7 |
8 | export interface ProviderRegistry {
9 | registerKeyboard(name: string, provider: KeyboardAutomation): void;
10 | registerMouse(name: string, provider: MouseAutomation): void;
11 | registerScreen(name: string, provider: ScreenAutomation): void;
12 | registerClipboard(name: string, provider: ClipboardAutomation): void;
13 |
14 | getKeyboard(name: string): KeyboardAutomation | undefined;
15 | getMouse(name: string): MouseAutomation | undefined;
16 | getScreen(name: string): ScreenAutomation | undefined;
17 | getClipboard(name: string): ClipboardAutomation | undefined;
18 | }
19 |
20 | /**
21 | * Central registry for automation providers
22 | * Allows registration and retrieval of individual automation components
23 | */
24 | export class DefaultProviderRegistry implements ProviderRegistry {
25 | private keyboards = new Map<string, KeyboardAutomation>();
26 | private mice = new Map<string, MouseAutomation>();
27 | private screens = new Map<string, ScreenAutomation>();
28 | private clipboards = new Map<string, ClipboardAutomation>();
29 |
30 | registerKeyboard(name: string, provider: KeyboardAutomation): void {
31 | this.keyboards.set(name, provider);
32 | }
33 |
34 | registerMouse(name: string, provider: MouseAutomation): void {
35 | this.mice.set(name, provider);
36 | }
37 |
38 | registerScreen(name: string, provider: ScreenAutomation): void {
39 | this.screens.set(name, provider);
40 | }
41 |
42 | registerClipboard(name: string, provider: ClipboardAutomation): void {
43 | this.clipboards.set(name, provider);
44 | }
45 |
46 | getKeyboard(name: string): KeyboardAutomation | undefined {
47 | return this.keyboards.get(name);
48 | }
49 |
50 | getMouse(name: string): MouseAutomation | undefined {
51 | return this.mice.get(name);
52 | }
53 |
54 | getScreen(name: string): ScreenAutomation | undefined {
55 | return this.screens.get(name);
56 | }
57 |
58 | getClipboard(name: string): ClipboardAutomation | undefined {
59 | return this.clipboards.get(name);
60 | }
61 |
62 | /**
63 | * Get a list of all registered provider names for each component type
64 | */
65 | getAvailableProviders(): {
66 | keyboards: string[];
67 | mice: string[];
68 | screens: string[];
69 | clipboards: string[];
70 | } {
71 | return {
72 | keyboards: Array.from(this.keyboards.keys()),
73 | mice: Array.from(this.mice.keys()),
74 | screens: Array.from(this.screens.keys()),
75 | clipboards: Array.from(this.clipboards.keys()),
76 | };
77 | }
78 | }
79 |
80 | // Singleton instance
81 | export const registry = new DefaultProviderRegistry();
82 |
```
--------------------------------------------------------------------------------
/scripts/test-screenshot.cjs:
--------------------------------------------------------------------------------
```
1 | #!/usr/bin/env node
2 | // Test script for the screenshot utility
3 | // This script tests the optimized screenshot functionality to ensure:
4 | // 1. No "Maximum call stack size exceeded" errors
5 | // 2. Reasonable file sizes (not 20MB)
6 |
7 | // Import the built version of the screenshot utility
8 | const { getScreenshot } = require('../build/tools/screenshot.js');
9 |
10 | async function testScreenshot() {
11 | console.log('Testing screenshot utility with various settings...');
12 |
13 | // Test 1: Default settings (should use 1280px width, JPEG format)
14 | console.log('\nTest 1: Default settings (1280px width, JPEG)');
15 | try {
16 | const result1 = await getScreenshot();
17 | if (result1.success) {
18 | console.log('✅ Default screenshot successful');
19 | } else {
20 | console.error('❌ Default screenshot failed:', result1.message);
21 | }
22 | } catch (error) {
23 | console.error('❌ Default screenshot threw exception:', error);
24 | }
25 |
26 | // Test 2: Small size (50x50px)
27 | console.log('\nTest 2: Small size (50x50px)');
28 | try {
29 | const result2 = await getScreenshot({
30 | resize: {
31 | width: 50,
32 | height: 50,
33 | fit: 'fill'
34 | }
35 | });
36 | if (result2.success) {
37 | console.log('✅ Small screenshot successful');
38 | } else {
39 | console.error('❌ Small screenshot failed:', result2.message);
40 | }
41 | } catch (error) {
42 | console.error('❌ Small screenshot threw exception:', error);
43 | }
44 |
45 | // Test 3: PNG format with high compression
46 | console.log('\nTest 3: PNG format with high compression');
47 | try {
48 | const result3 = await getScreenshot({
49 | format: 'png',
50 | compressionLevel: 9,
51 | resize: {
52 | width: 800
53 | }
54 | });
55 | if (result3.success) {
56 | console.log('✅ PNG screenshot successful');
57 | } else {
58 | console.error('❌ PNG screenshot failed:', result3.message);
59 | }
60 | } catch (error) {
61 | console.error('❌ PNG screenshot threw exception:', error);
62 | }
63 |
64 | // Test 4: Grayscale JPEG with low quality
65 | console.log('\nTest 4: Grayscale JPEG with low quality');
66 | try {
67 | const result4 = await getScreenshot({
68 | format: 'jpeg',
69 | quality: 50,
70 | grayscale: true
71 | });
72 | if (result4.success) {
73 | console.log('✅ Grayscale screenshot successful');
74 | } else {
75 | console.error('❌ Grayscale screenshot failed:', result4.message);
76 | }
77 | } catch (error) {
78 | console.error('❌ Grayscale screenshot threw exception:', error);
79 | }
80 |
81 | console.log('\nScreenshot testing complete');
82 | }
83 |
84 | // Run the tests
85 | testScreenshot().catch(console.error);
86 |
```
--------------------------------------------------------------------------------
/scripts/test-screenshot.mjs:
--------------------------------------------------------------------------------
```
1 | #!/usr/bin/env node
2 | // Test script for the screenshot utility
3 | // This script tests the optimized screenshot functionality to ensure:
4 | // 1. No "Maximum call stack size exceeded" errors
5 | // 2. Reasonable file sizes (not 20MB)
6 |
7 | // Use dynamic import for ES modules
8 | async function runTests() {
9 | // Import the built version of the screenshot utility
10 | const { getScreenshot } = await import('../build/tools/screenshot.js');
11 |
12 | console.log('Testing screenshot utility with various settings...');
13 |
14 | // Test 1: Default settings (should use 1280px width, JPEG format)
15 | console.log('\nTest 1: Default settings (1280px width, JPEG)');
16 | try {
17 | const result1 = await getScreenshot();
18 | if (result1.success) {
19 | console.log('✅ Default screenshot successful');
20 | } else {
21 | console.error('❌ Default screenshot failed:', result1.message);
22 | }
23 | } catch (error) {
24 | console.error('❌ Default screenshot threw exception:', error);
25 | }
26 |
27 | // Test 2: Small size (50x50px)
28 | console.log('\nTest 2: Small size (50x50px)');
29 | try {
30 | const result2 = await getScreenshot({
31 | resize: {
32 | width: 50,
33 | height: 50,
34 | fit: 'fill'
35 | }
36 | });
37 | if (result2.success) {
38 | console.log('✅ Small screenshot successful');
39 | } else {
40 | console.error('❌ Small screenshot failed:', result2.message);
41 | }
42 | } catch (error) {
43 | console.error('❌ Small screenshot threw exception:', error);
44 | }
45 |
46 | // Test 3: PNG format with high compression
47 | console.log('\nTest 3: PNG format with high compression');
48 | try {
49 | const result3 = await getScreenshot({
50 | format: 'png',
51 | compressionLevel: 9,
52 | resize: {
53 | width: 800
54 | }
55 | });
56 | if (result3.success) {
57 | console.log('✅ PNG screenshot successful');
58 | } else {
59 | console.error('❌ PNG screenshot failed:', result3.message);
60 | }
61 | } catch (error) {
62 | console.error('❌ PNG screenshot threw exception:', error);
63 | }
64 |
65 | // Test 4: Grayscale JPEG with low quality
66 | console.log('\nTest 4: Grayscale JPEG with low quality');
67 | try {
68 | const result4 = await getScreenshot({
69 | format: 'jpeg',
70 | quality: 50,
71 | grayscale: true
72 | });
73 | if (result4.success) {
74 | console.log('✅ Grayscale screenshot successful');
75 | } else {
76 | console.error('❌ Grayscale screenshot failed:', result4.message);
77 | }
78 | } catch (error) {
79 | console.error('❌ Grayscale screenshot threw exception:', error);
80 | }
81 |
82 | console.log('\nScreenshot testing complete');
83 | }
84 |
85 | // Run the tests
86 | runTests().catch(console.error);
87 |
```
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish to NPM
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | publish:
10 | runs-on: windows-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: '18'
20 | registry-url: 'https://registry.npmjs.org'
21 |
22 | - name: Cache npm dependencies
23 | uses: actions/cache@v3
24 | with:
25 | path: ~/.npm
26 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
27 | restore-keys: |
28 | ${{ runner.os }}-node-
29 |
30 | # Dependencies required for native modules
31 | - name: Install global dependencies
32 | run: |
33 | npm install -g node-gyp
34 | npm install -g cmake-js
35 |
36 | - name: Install dependencies
37 | run: npm ci
38 |
39 | - name: Lint
40 | run: npm run lint
41 |
42 | - name: Run tests
43 | run: npm run test
44 |
45 | - name: Build
46 | run: npm run build:all
47 |
48 | - name: Extract version
49 | id: extract_version
50 | run: echo "VERSION=$($env:GITHUB_REF -replace 'refs/tags/v', '')" >> $env:GITHUB_OUTPUT
51 | shell: pwsh
52 |
53 | - name: Update package version if needed
54 | run: |
55 | $current_version = $(node -p "require('./package.json').version")
56 | $tag_version = "${{ steps.extract_version.outputs.VERSION }}"
57 |
58 | if ($current_version -ne $tag_version) {
59 | npm version $tag_version --no-git-tag-version
60 | Write-Host "Updated version from $current_version to $tag_version"
61 | } else {
62 | Write-Host "Version already set to $current_version, skipping update"
63 | }
64 | shell: pwsh
65 |
66 | - name: Verify package contents
67 | run: |
68 | # Just check what will be included in the package without actually extracting
69 | $package = npm pack --dry-run
70 |
71 | # List the build directory to verify it exists and has content
72 | Write-Host "`nVerifying build directory contents:"
73 | if (Test-Path -Path "./build") {
74 | Get-ChildItem -Path "./build" -Recurse -Depth 1 | Select-Object FullName
75 | } else {
76 | Write-Error "Build directory not found!"
77 | exit 1
78 | }
79 |
80 | - name: Publish to NPM
81 | run: npm publish
82 | env:
83 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
84 |
85 | - name: Verify publish
86 | run: npm view $(node -p "require('./package.json').name") version
87 | if: success()
88 |
```
--------------------------------------------------------------------------------
/scripts/test-window.cjs:
--------------------------------------------------------------------------------
```
1 | // Direct test script for window handling
2 | // Use CommonJS require for keysender
3 | const keysender = require('keysender');
4 | const { Hardware } = keysender;
5 | const getAllWindows = keysender.getAllWindows;
6 |
7 | console.log("Testing keysender window handling directly");
8 |
9 | // Get all windows
10 | const allWindows = getAllWindows();
11 | console.log("\nAll windows:");
12 | allWindows.forEach(window => {
13 | console.log(`- "${window.title}" (handle: ${window.handle}, class: ${window.className})`);
14 | });
15 |
16 | // Try to find Notepad
17 | console.log("\nLooking for Notepad...");
18 | const notepad = allWindows.find(w => w.title && w.title.includes('Notepad'));
19 |
20 | if (notepad) {
21 | console.log(`Found Notepad: "${notepad.title}" (handle: ${notepad.handle})`);
22 |
23 | // Create hardware instance for Notepad
24 | try {
25 | const hw = new Hardware(notepad.handle);
26 | console.log("Created Hardware instance for Notepad");
27 |
28 | // Try to get window view
29 | try {
30 | const view = hw.workwindow.getView();
31 | console.log("Notepad view:", view);
32 | } catch (e) {
33 | console.error("Error getting Notepad view:", e.message);
34 | }
35 |
36 | // Try to set as foreground
37 | try {
38 | hw.workwindow.setForeground();
39 | console.log("Set Notepad as foreground window");
40 | } catch (e) {
41 | console.error("Error setting Notepad as foreground:", e.message);
42 | }
43 |
44 | // Try to resize
45 | try {
46 | hw.workwindow.setView({
47 | x: 200,
48 | y: 200,
49 | width: 800,
50 | height: 600
51 | });
52 | console.log("Resized Notepad to 800x600 at position (200, 200)");
53 |
54 | // Get updated view
55 | const updatedView = hw.workwindow.getView();
56 | console.log("Updated Notepad view:", updatedView);
57 | } catch (e) {
58 | console.error("Error resizing Notepad:", e.message);
59 | }
60 | } catch (e) {
61 | console.error("Error creating Hardware instance for Notepad:", e.message);
62 | }
63 | } else {
64 | console.log("Notepad not found. Please make sure Notepad is running.");
65 | }
66 |
67 | // Try with default Hardware instance
68 | console.log("\nTesting default Hardware instance:");
69 | try {
70 | const defaultHw = new Hardware();
71 | console.log("Created default Hardware instance");
72 |
73 | // Try to get current window
74 | try {
75 | const currentWindow = defaultHw.workwindow.get();
76 | console.log("Current window:", currentWindow);
77 | } catch (e) {
78 | console.error("Error getting current window:", e.message);
79 | }
80 |
81 | // Try to get view
82 | try {
83 | const view = defaultHw.workwindow.getView();
84 | console.log("Current view:", view);
85 | } catch (e) {
86 | console.error("Error getting current view:", e.message);
87 | }
88 | } catch (e) {
89 | console.error("Error creating default Hardware instance:", e.message);
90 | }
91 |
```
--------------------------------------------------------------------------------
/scripts/test-window.js:
--------------------------------------------------------------------------------
```javascript
1 | // Direct test script for window handling
2 | // Use CommonJS require for keysender
3 | const keysender = require('keysender');
4 | const { Hardware } = keysender;
5 | const getAllWindows = keysender.getAllWindows;
6 |
7 | console.log("Testing keysender window handling directly");
8 |
9 | // Get all windows
10 | const allWindows = getAllWindows();
11 | console.log("\nAll windows:");
12 | allWindows.forEach(window => {
13 | console.log(`- "${window.title}" (handle: ${window.handle}, class: ${window.className})`);
14 | });
15 |
16 | // Try to find Notepad
17 | console.log("\nLooking for Notepad...");
18 | const notepad = allWindows.find(w => w.title && w.title.includes('Notepad'));
19 |
20 | if (notepad) {
21 | console.log(`Found Notepad: "${notepad.title}" (handle: ${notepad.handle})`);
22 |
23 | // Create hardware instance for Notepad
24 | try {
25 | const hw = new Hardware(notepad.handle);
26 | console.log("Created Hardware instance for Notepad");
27 |
28 | // Try to get window view
29 | try {
30 | const view = hw.workwindow.getView();
31 | console.log("Notepad view:", view);
32 | } catch (e) {
33 | console.error("Error getting Notepad view:", e.message);
34 | }
35 |
36 | // Try to set as foreground
37 | try {
38 | hw.workwindow.setForeground();
39 | console.log("Set Notepad as foreground window");
40 | } catch (e) {
41 | console.error("Error setting Notepad as foreground:", e.message);
42 | }
43 |
44 | // Try to resize
45 | try {
46 | hw.workwindow.setView({
47 | x: 200,
48 | y: 200,
49 | width: 800,
50 | height: 600
51 | });
52 | console.log("Resized Notepad to 800x600 at position (200, 200)");
53 |
54 | // Get updated view
55 | const updatedView = hw.workwindow.getView();
56 | console.log("Updated Notepad view:", updatedView);
57 | } catch (e) {
58 | console.error("Error resizing Notepad:", e.message);
59 | }
60 | } catch (e) {
61 | console.error("Error creating Hardware instance for Notepad:", e.message);
62 | }
63 | } else {
64 | console.log("Notepad not found. Please make sure Notepad is running.");
65 | }
66 |
67 | // Try with default Hardware instance
68 | console.log("\nTesting default Hardware instance:");
69 | try {
70 | const defaultHw = new Hardware();
71 | console.log("Created default Hardware instance");
72 |
73 | // Try to get current window
74 | try {
75 | const currentWindow = defaultHw.workwindow.get();
76 | console.log("Current window:", currentWindow);
77 | } catch (e) {
78 | console.error("Error getting current window:", e.message);
79 | }
80 |
81 | // Try to get view
82 | try {
83 | const view = defaultHw.workwindow.getView();
84 | console.log("Current view:", view);
85 | } catch (e) {
86 | console.error("Error getting current view:", e.message);
87 | }
88 | } catch (e) {
89 | console.error("Error creating default Hardware instance:", e.message);
90 | }
91 |
```
--------------------------------------------------------------------------------
/scripts/test-provider.js:
--------------------------------------------------------------------------------
```javascript
1 | // Simple script to test provider selection
2 | import { loadConfig } from '../build/config.js';
3 | import { createAutomationProvider } from '../build/providers/factory.js';
4 |
5 | // Override the provider from command line argument if provided
6 | if (process.argv.length > 2) {
7 | process.env.AUTOMATION_PROVIDER = process.argv[2];
8 | }
9 |
10 | // Load configuration
11 | const config = loadConfig();
12 | console.log(`Using provider: ${config.provider}`);
13 |
14 | // Create provider
15 | const provider = createAutomationProvider(config.provider);
16 | console.log(`Provider created: ${provider.constructor.name}`);
17 |
18 | // Print provider details
19 | console.log('\nProvider components:');
20 | console.log(`- Keyboard: ${provider.keyboard.constructor.name}`);
21 | console.log(`- Mouse: ${provider.mouse.constructor.name}`);
22 | console.log(`- Screen: ${provider.screen.constructor.name}`);
23 | console.log(`- Clipboard: ${provider.clipboard.constructor.name}`);
24 |
25 | // Test window operations if requested
26 | const testWindowOps = process.argv.includes('--test-window');
27 | if (testWindowOps) {
28 | console.log('\nTesting window operations:');
29 |
30 | // Get screen size
31 | const screenSizeResult = provider.screen.getScreenSize();
32 | console.log(`\nScreen size: ${JSON.stringify(screenSizeResult.data)}`);
33 |
34 | // Get active window
35 | const activeWindowResult = provider.screen.getActiveWindow();
36 | console.log(`\nActive window: ${JSON.stringify(activeWindowResult.data)}`);
37 |
38 | // Test window focus
39 | if (activeWindowResult.success && activeWindowResult.data?.title) {
40 | const windowTitle = activeWindowResult.data.title;
41 | console.log(`\nFocusing window: "${windowTitle}"`);
42 | const focusResult = provider.screen.focusWindow(windowTitle);
43 | console.log(`Focus result: ${focusResult.success ? 'Success' : 'Failed'} - ${focusResult.message}`);
44 |
45 | // Test window resize
46 | console.log(`\nResizing window: "${windowTitle}" to 800x600`);
47 | const resizeResult = provider.screen.resizeWindow(windowTitle, 800, 600);
48 | console.log(`Resize result: ${resizeResult.success ? 'Success' : 'Failed'} - ${resizeResult.message}`);
49 |
50 | // Wait a bit to see the resize
51 | console.log('Waiting 2 seconds...');
52 | setTimeout(() => {
53 | // Test window reposition
54 | console.log(`\nRepositioning window: "${windowTitle}" to (100, 100)`);
55 | const repositionResult = provider.screen.repositionWindow(windowTitle, 100, 100);
56 | console.log(`Reposition result: ${repositionResult.success ? 'Success' : 'Failed'} - ${repositionResult.message}`);
57 |
58 | // Get active window again to verify changes
59 | setTimeout(() => {
60 | const updatedWindowResult = provider.screen.getActiveWindow();
61 | console.log(`\nUpdated window info: ${JSON.stringify(updatedWindowResult.data)}`);
62 | }, 1000);
63 | }, 2000);
64 | }
65 | }
66 |
```
--------------------------------------------------------------------------------
/src/server.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2 |
3 | // We'll define the mocks before importing the module being tested
4 | const mockMcpServer = {
5 | connect: vi.fn(),
6 | };
7 |
8 | const mockApp = {
9 | get: vi.fn(),
10 | use: vi.fn(),
11 | post: vi.fn(),
12 | };
13 |
14 | const mockHttpServer = {
15 | listen: vi.fn((port, host, callback) => {
16 | if (callback) callback();
17 | return mockHttpServer;
18 | }),
19 | close: vi.fn(),
20 | on: vi.fn(),
21 | };
22 |
23 | const mockSSETransport = {
24 | sessionId: 'test-session-id',
25 | onclose: null,
26 | handlePostMessage: vi.fn(),
27 | };
28 |
29 | // Mock the dependencies before importing the module to test
30 | vi.mock('express', () => {
31 | const jsonMiddlewareMock = vi.fn();
32 | return {
33 | default: vi.fn(() => mockApp),
34 | json: vi.fn().mockReturnValue(jsonMiddlewareMock),
35 | };
36 | });
37 |
38 | vi.mock('http', () => {
39 | return {
40 | createServer: vi.fn(() => mockHttpServer),
41 | };
42 | });
43 |
44 | vi.mock('@modelcontextprotocol/sdk/server/sse.js', () => {
45 | return {
46 | SSEServerTransport: vi.fn(() => mockSSETransport),
47 | };
48 | });
49 |
50 | vi.mock('os', () => {
51 | return {
52 | networkInterfaces: vi.fn(() => ({
53 | eth0: [
54 | {
55 | family: 'IPv4',
56 | internal: false,
57 | address: '192.168.1.100',
58 | },
59 | ],
60 | })),
61 | };
62 | });
63 |
64 | // Now import the module being tested
65 | import { createHttpServer } from './server.js';
66 |
67 | describe('HTTP Server', () => {
68 | beforeEach(() => {
69 | vi.clearAllMocks();
70 | process.env.MAX_SSE_CLIENTS = '100';
71 |
72 | // Mock console.log to prevent test output
73 | vi.spyOn(console, 'log').mockImplementation(() => {});
74 | vi.spyOn(console, 'error').mockImplementation(() => {});
75 | });
76 |
77 | afterEach(() => {
78 | vi.restoreAllMocks();
79 | vi.resetAllMocks();
80 | });
81 |
82 | it('should create an HTTP server with SSE transport', () => {
83 | const result = createHttpServer(mockMcpServer as any);
84 |
85 | // Should have created a server
86 | expect(mockHttpServer).toBeDefined();
87 | expect(result.app).toBe(mockApp);
88 | expect(result.httpServer).toBe(mockHttpServer);
89 | });
90 |
91 | it('should register client limit middleware', () => {
92 | createHttpServer(mockMcpServer as any);
93 |
94 | // Should have registered middleware for client limits
95 | expect(mockApp.use).toHaveBeenCalledWith('/mcp', expect.any(Function));
96 | });
97 |
98 | it('should register metrics endpoint', () => {
99 | createHttpServer(mockMcpServer as any);
100 |
101 | // Should have registered the metrics endpoint
102 | expect(mockApp.get).toHaveBeenCalledWith('/metrics', expect.any(Function));
103 | });
104 |
105 | it('should start listening on the specified port', () => {
106 | const port = 5555;
107 | createHttpServer(mockMcpServer as any, port);
108 |
109 | // Should have started listening on the specified port
110 | expect(mockHttpServer.listen).toHaveBeenCalledWith(port, '0.0.0.0', expect.any(Function));
111 | });
112 | });
113 |
```
--------------------------------------------------------------------------------
/src/providers/clipboard/powershell/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { exec } from 'child_process';
2 | import { promisify } from 'util';
3 | import { ClipboardInput } from '../../../types/common.js';
4 | import { WindowsControlResponse } from '../../../types/responses.js';
5 | import { ClipboardAutomation } from '../../../interfaces/automation.js';
6 |
7 | const execAsync = promisify(exec);
8 |
9 | /**
10 | * PowerShell implementation of the ClipboardAutomation interface
11 | *
12 | * Uses PowerShell commands for clipboard operations on Windows
13 | * NOTE: This provider is Windows-only
14 | */
15 | export class PowerShellClipboardProvider implements ClipboardAutomation {
16 | private async executePowerShell(command: string): Promise<string> {
17 | try {
18 | const { stdout, stderr } = await execAsync(`powershell.exe -Command "${command}"`);
19 | if (stderr) {
20 | throw new Error(stderr);
21 | }
22 | return stdout.trim();
23 | } catch (error) {
24 | throw new Error(
25 | `PowerShell execution failed: ${error instanceof Error ? error.message : String(error)}`,
26 | );
27 | }
28 | }
29 |
30 | async getClipboardContent(): Promise<WindowsControlResponse> {
31 | try {
32 | const content = await this.executePowerShell('Get-Clipboard');
33 | return {
34 | success: true,
35 | message: 'Clipboard content retrieved',
36 | data: content,
37 | };
38 | } catch (error) {
39 | return {
40 | success: false,
41 | message: `Failed to get clipboard content: ${error instanceof Error ? error.message : String(error)}`,
42 | };
43 | }
44 | }
45 |
46 | async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
47 | try {
48 | // Escape quotes in the text for PowerShell
49 | const escapedText = input.text.replace(/"/g, '`"');
50 | await this.executePowerShell(`Set-Clipboard -Value "${escapedText}"`);
51 | return {
52 | success: true,
53 | message: 'Clipboard content set',
54 | };
55 | } catch (error) {
56 | return {
57 | success: false,
58 | message: `Failed to set clipboard content: ${error instanceof Error ? error.message : String(error)}`,
59 | };
60 | }
61 | }
62 |
63 | async hasClipboardText(): Promise<WindowsControlResponse> {
64 | try {
65 | const content = await this.executePowerShell('Get-Clipboard');
66 | const hasText = content.length > 0;
67 | return {
68 | success: true,
69 | message: `Clipboard ${hasText ? 'has' : 'does not have'} text`,
70 | data: hasText,
71 | };
72 | } catch (error) {
73 | return {
74 | success: false,
75 | message: `Failed to check clipboard: ${error instanceof Error ? error.message : String(error)}`,
76 | };
77 | }
78 | }
79 |
80 | async clearClipboard(): Promise<WindowsControlResponse> {
81 | try {
82 | await this.executePowerShell('Set-Clipboard -Value ""');
83 | return {
84 | success: true,
85 | message: 'Clipboard cleared',
86 | };
87 | } catch (error) {
88 | return {
89 | success: false,
90 | message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
91 | };
92 | }
93 | }
94 | }
95 |
```
--------------------------------------------------------------------------------
/src/providers/registry.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach } from 'vitest';
2 | import { DefaultProviderRegistry } from './registry.js';
3 | import { ClipboardAutomation } from '../interfaces/automation.js';
4 | import { ClipboardInput } from '../types/common.js';
5 | import { WindowsControlResponse } from '../types/responses.js';
6 |
7 | class MockClipboardProvider implements ClipboardAutomation {
8 | // eslint-disable-next-line @typescript-eslint/require-await
9 | async getClipboardContent(): Promise<WindowsControlResponse> {
10 | return { success: true, message: 'Mock clipboard content', data: 'test' };
11 | }
12 |
13 | // eslint-disable-next-line @typescript-eslint/require-await
14 | async setClipboardContent(_input: ClipboardInput): Promise<WindowsControlResponse> {
15 | return { success: true, message: 'Mock set clipboard' };
16 | }
17 |
18 | // eslint-disable-next-line @typescript-eslint/require-await
19 | async hasClipboardText(): Promise<WindowsControlResponse> {
20 | return { success: true, message: 'Mock has text', data: true };
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/require-await
24 | async clearClipboard(): Promise<WindowsControlResponse> {
25 | return { success: true, message: 'Mock clear clipboard' };
26 | }
27 | }
28 |
29 | describe('DefaultProviderRegistry', () => {
30 | let registry: DefaultProviderRegistry;
31 |
32 | beforeEach(() => {
33 | registry = new DefaultProviderRegistry();
34 | });
35 |
36 | describe('registration', () => {
37 | it('should register and retrieve clipboard provider', () => {
38 | const provider = new MockClipboardProvider();
39 | registry.registerClipboard('mock', provider);
40 |
41 | const retrieved = registry.getClipboard('mock');
42 | expect(retrieved).toBe(provider);
43 | });
44 |
45 | it('should return undefined for non-existent provider', () => {
46 | const retrieved = registry.getClipboard('non-existent');
47 | expect(retrieved).toBeUndefined();
48 | });
49 |
50 | it('should allow overwriting existing provider', () => {
51 | const provider1 = new MockClipboardProvider();
52 | const provider2 = new MockClipboardProvider();
53 |
54 | registry.registerClipboard('mock', provider1);
55 | registry.registerClipboard('mock', provider2);
56 |
57 | const retrieved = registry.getClipboard('mock');
58 | expect(retrieved).toBe(provider2);
59 | });
60 | });
61 |
62 | describe('getAvailableProviders', () => {
63 | it('should return empty arrays initially', () => {
64 | const available = registry.getAvailableProviders();
65 |
66 | expect(available.keyboards).toEqual([]);
67 | expect(available.mice).toEqual([]);
68 | expect(available.screens).toEqual([]);
69 | expect(available.clipboards).toEqual([]);
70 | });
71 |
72 | it('should return registered provider names', () => {
73 | registry.registerClipboard('provider1', new MockClipboardProvider());
74 | registry.registerClipboard('provider2', new MockClipboardProvider());
75 |
76 | const available = registry.getAvailableProviders();
77 |
78 | expect(available.clipboards).toContain('provider1');
79 | expect(available.clipboards).toContain('provider2');
80 | expect(available.clipboards.length).toBe(2);
81 | });
82 | });
83 | });
84 |
```
--------------------------------------------------------------------------------
/src/tools/screenshot-file.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createAutomationProvider } from '../providers/factory.js';
2 | import { ScreenshotOptions } from '../types/common.js';
3 | import { WindowsControlResponse } from '../types/responses.js';
4 | import * as fs from 'fs';
5 | import * as path from 'path';
6 | import * as os from 'os';
7 | import { v4 as uuidv4 } from 'uuid';
8 |
9 | /**
10 | * Captures a screenshot and saves it to a temporary file
11 | *
12 | * @param options Optional configuration for the screenshot
13 | * @returns Promise resolving to a WindowsControlResponse with the file path
14 | */
15 | export async function getScreenshotToFile(
16 | options?: ScreenshotOptions,
17 | ): Promise<WindowsControlResponse> {
18 | try {
19 | // Create a provider instance to handle the screenshot
20 | const provider = createAutomationProvider();
21 |
22 | // Delegate to the provider's screenshot implementation
23 | const result = await provider.screen.getScreenshot(options);
24 |
25 | // If the screenshot was successful and contains image data
26 | if (result.success && result.content && result.content[0]?.type === 'image') {
27 | // Create a unique filename in the system's temp directory
28 | const tempDir = os.tmpdir();
29 | const fileExt = options?.format === 'png' ? 'png' : 'jpg';
30 | const filename = `screenshot-${uuidv4()}.${fileExt}`;
31 | const filePath = path.join(tempDir, filename);
32 |
33 | // Get the base64 image data and ensure it's a string
34 | let base64Image: string;
35 |
36 | try {
37 | const imageData = result.content[0].data;
38 |
39 | // Validate the data is a string
40 | if (typeof imageData !== 'string') {
41 | return {
42 | success: false,
43 | message: 'Screenshot data is not in expected string format',
44 | };
45 | }
46 |
47 | // Remove the data URL prefix if present
48 | base64Image = imageData.includes('base64,') ? imageData.split('base64,')[1] : imageData;
49 | } catch {
50 | return {
51 | success: false,
52 | message: 'Failed to process screenshot data',
53 | };
54 | }
55 |
56 | // Write the image data to the file
57 | fs.writeFileSync(filePath, Buffer.from(base64Image, 'base64'));
58 |
59 | // Extract dimensions safely
60 | const width =
61 | typeof result.data === 'object' && result.data && 'width' in result.data
62 | ? Number(result.data.width)
63 | : undefined;
64 |
65 | const height =
66 | typeof result.data === 'object' && result.data && 'height' in result.data
67 | ? Number(result.data.height)
68 | : undefined;
69 |
70 | // Return a response with the file path instead of the image data
71 | return {
72 | success: true,
73 | message: 'Screenshot saved to temporary file',
74 | data: {
75 | filePath,
76 | format: options?.format || 'jpeg',
77 | width,
78 | height,
79 | timestamp: new Date().toISOString(),
80 | },
81 | };
82 | }
83 |
84 | // If the screenshot failed or doesn't contain image data, return the original result
85 | return result;
86 | } catch (error) {
87 | const errorMessage =
88 | error instanceof Error ? error.message : 'Unknown error occurred while capturing screenshot';
89 |
90 | return {
91 | success: false,
92 | message: `Failed to capture screenshot to file: ${errorMessage}`,
93 | };
94 | }
95 | }
96 |
```
--------------------------------------------------------------------------------
/scripts/compare-providers.js:
--------------------------------------------------------------------------------
```javascript
1 | /* eslint-disable */
2 | // Script to compare window handling between Keysender and NutJS providers
3 | import { loadConfig } from '../build/config.js';
4 | import { createAutomationProvider } from '../build/providers/factory.js';
5 |
6 | // Test function to try all window operations with a provider
7 | async function testProvider(providerName) {
8 | console.log(`\n=== TESTING ${providerName.toUpperCase()} PROVIDER ===\n`);
9 |
10 | // Configure environment to use the specified provider
11 | process.env.AUTOMATION_PROVIDER = providerName;
12 |
13 | // Load configuration and create provider
14 | const config = loadConfig();
15 | console.log(`Using provider: ${config.provider}`);
16 |
17 | const provider = createAutomationProvider(config.provider);
18 | console.log(`Provider created: ${provider.constructor.name}`);
19 |
20 | // 1. Get screen size
21 | console.log('\n1. Getting screen size:');
22 | const screenSizeResult = provider.screen.getScreenSize();
23 | console.log(`Success: ${screenSizeResult.success}`);
24 | console.log(`Message: ${screenSizeResult.message}`);
25 | console.log(`Data: ${JSON.stringify(screenSizeResult.data)}`);
26 |
27 | // 2. Get active window
28 | console.log('\n2. Getting active window:');
29 | const activeWindowResult = provider.screen.getActiveWindow();
30 | console.log(`Success: ${activeWindowResult.success}`);
31 | console.log(`Message: ${activeWindowResult.message}`);
32 | console.log(`Data: ${JSON.stringify(activeWindowResult.data)}`);
33 |
34 | // Extract window title for later operations
35 | const windowTitle = activeWindowResult.success && activeWindowResult.data?.title
36 | ? activeWindowResult.data.title
37 | : "Unknown";
38 |
39 | // 3. Focus window
40 | console.log(`\n3. Focusing window "${windowTitle}":`);
41 | const focusResult = provider.screen.focusWindow(windowTitle);
42 | console.log(`Success: ${focusResult.success}`);
43 | console.log(`Message: ${focusResult.message}`);
44 | console.log(`Data: ${JSON.stringify(focusResult.data)}`);
45 |
46 | // 4. Resize window
47 | console.log(`\n4. Resizing window "${windowTitle}" to 800x600:`);
48 | const resizeResult = provider.screen.resizeWindow(windowTitle, 800, 600);
49 | console.log(`Success: ${resizeResult.success}`);
50 | console.log(`Message: ${resizeResult.message}`);
51 | console.log(`Data: ${JSON.stringify(resizeResult.data)}`);
52 |
53 | // 5. Reposition window
54 | console.log(`\n5. Repositioning window "${windowTitle}" to position (100, 100):`);
55 | const repositionResult = provider.screen.repositionWindow(windowTitle, 100, 100);
56 | console.log(`Success: ${repositionResult.success}`);
57 | console.log(`Message: ${repositionResult.message}`);
58 | console.log(`Data: ${JSON.stringify(repositionResult.data)}`);
59 |
60 | // 6. Final window check
61 | console.log('\n6. Final window check:');
62 | const finalWindowResult = provider.screen.getActiveWindow();
63 | console.log(`Success: ${finalWindowResult.success}`);
64 | console.log(`Message: ${finalWindowResult.message}`);
65 | console.log(`Data: ${JSON.stringify(finalWindowResult.data)}`);
66 |
67 | console.log(`\n=== COMPLETED ${providerName.toUpperCase()} PROVIDER TESTS ===\n`);
68 | }
69 |
70 | // Main execution
71 | (async () => {
72 | try {
73 | // First test keysender provider
74 | await testProvider('keysender');
75 |
76 | // Only test keysender provider
77 | // await testProvider('other-provider');
78 | } catch (error) {
79 | console.error('Error in testing:', error);
80 | }
81 | })();
82 |
```
--------------------------------------------------------------------------------
/src/tools/screenshot.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2 | import { getScreenshot } from './screenshot';
3 | import { createAutomationProvider } from '../providers/factory';
4 |
5 | // Mock the factory module
6 | vi.mock('../providers/factory', () => ({
7 | createAutomationProvider: vi.fn(),
8 | }));
9 |
10 | describe('Screenshot Functions', () => {
11 | beforeEach(() => {
12 | vi.resetAllMocks();
13 | });
14 |
15 | afterEach(() => {
16 | vi.restoreAllMocks();
17 | vi.clearAllMocks();
18 | });
19 |
20 | describe('getScreenshot', () => {
21 | it('should delegate to provider and return screenshot data on success', async () => {
22 | // Setup mock provider
23 | const mockProvider = {
24 | screen: {
25 | getScreenshot: vi.fn().mockResolvedValue({
26 | success: true,
27 | message: 'Screenshot captured successfully',
28 | content: [
29 | {
30 | type: 'image',
31 | data: 'test-image-data-base64',
32 | mimeType: 'image/png',
33 | },
34 | ],
35 | }),
36 | },
37 | };
38 |
39 | // Make createAutomationProvider return our mock
40 | vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);
41 |
42 | // Execute
43 | const result = await getScreenshot();
44 |
45 | // Verify
46 | expect(createAutomationProvider).toHaveBeenCalledTimes(1);
47 | expect(mockProvider.screen.getScreenshot).toHaveBeenCalledTimes(1);
48 | expect(result).toEqual({
49 | success: true,
50 | message: 'Screenshot captured successfully',
51 | content: [
52 | {
53 | type: 'image',
54 | data: 'test-image-data-base64',
55 | mimeType: 'image/png',
56 | },
57 | ],
58 | });
59 | });
60 |
61 | it('should pass options to provider when specified', async () => {
62 | // Setup mock provider
63 | const mockProvider = {
64 | screen: {
65 | getScreenshot: vi.fn().mockResolvedValue({
66 | success: true,
67 | message: 'Screenshot captured successfully',
68 | content: [
69 | {
70 | type: 'image',
71 | data: 'test-image-data-base64',
72 | mimeType: 'image/jpeg',
73 | },
74 | ],
75 | }),
76 | },
77 | };
78 |
79 | // Make createAutomationProvider return our mock
80 | vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);
81 |
82 | // Options to pass
83 | const options = {
84 | region: { x: 100, y: 100, width: 800, height: 600 },
85 | format: 'jpeg' as const,
86 | };
87 |
88 | // Execute
89 | const result = await getScreenshot(options);
90 |
91 | // Verify
92 | expect(createAutomationProvider).toHaveBeenCalledTimes(1);
93 | expect(mockProvider.screen.getScreenshot).toHaveBeenCalledWith(options);
94 | expect(result).toEqual({
95 | success: true,
96 | message: 'Screenshot captured successfully',
97 | content: [
98 | {
99 | type: 'image',
100 | data: 'test-image-data-base64',
101 | mimeType: 'image/jpeg',
102 | },
103 | ],
104 | });
105 | });
106 |
107 | it('should return error response when provider throws an error', async () => {
108 | // Setup mock provider that throws
109 | const mockProvider = {
110 | screen: {
111 | getScreenshot: vi.fn().mockImplementation(() => {
112 | throw new Error('Capture failed');
113 | }),
114 | },
115 | };
116 |
117 | // Make createAutomationProvider return our mock
118 | vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);
119 |
120 | // Execute
121 | const result = await getScreenshot();
122 |
123 | // Verify
124 | expect(result).toEqual({
125 | success: false,
126 | message: 'Failed to capture screenshot: Capture failed',
127 | });
128 | });
129 | });
130 | });
131 |
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/index.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import { AutoHotkeyProvider } from './index.js';
3 |
4 | // Mock child_process module properly
5 | vi.mock('child_process', () => ({
6 | execSync: vi.fn(),
7 | exec: vi.fn(),
8 | spawn: vi.fn(),
9 | fork: vi.fn(),
10 | execFile: vi.fn(),
11 | }));
12 |
13 | describe('AutoHotkeyProvider', () => {
14 | let provider: AutoHotkeyProvider;
15 |
16 | beforeEach(() => {
17 | provider = new AutoHotkeyProvider();
18 | });
19 |
20 | it('should create an instance with all required automation interfaces', () => {
21 | expect(provider).toBeDefined();
22 | expect(provider.keyboard).toBeDefined();
23 | expect(provider.mouse).toBeDefined();
24 | expect(provider.screen).toBeDefined();
25 | expect(provider.clipboard).toBeDefined();
26 | });
27 |
28 | it('should implement KeyboardAutomation interface', () => {
29 | expect(provider.keyboard).toBeDefined();
30 | expect(provider.keyboard.typeText).toBeDefined();
31 | expect(provider.keyboard.pressKey).toBeDefined();
32 | expect(provider.keyboard.pressKeyCombination).toBeDefined();
33 | expect(provider.keyboard.holdKey).toBeDefined();
34 | });
35 |
36 | it('should implement MouseAutomation interface', () => {
37 | expect(provider.mouse).toBeDefined();
38 | expect(provider.mouse.moveMouse).toBeDefined();
39 | expect(provider.mouse.clickMouse).toBeDefined();
40 | expect(provider.mouse.doubleClick).toBeDefined();
41 | expect(provider.mouse.getCursorPosition).toBeDefined();
42 | expect(provider.mouse.scrollMouse).toBeDefined();
43 | expect(provider.mouse.dragMouse).toBeDefined();
44 | expect(provider.mouse.clickAt).toBeDefined();
45 | });
46 |
47 | it('should implement ScreenAutomation interface', () => {
48 | expect(provider.screen).toBeDefined();
49 | expect(provider.screen.getScreenSize).toBeDefined();
50 | expect(provider.screen.getActiveWindow).toBeDefined();
51 | expect(provider.screen.focusWindow).toBeDefined();
52 | expect(provider.screen.resizeWindow).toBeDefined();
53 | expect(provider.screen.repositionWindow).toBeDefined();
54 | expect(provider.screen.getScreenshot).toBeDefined();
55 | });
56 |
57 | it('should implement ClipboardAutomation interface', () => {
58 | expect(provider.clipboard).toBeDefined();
59 | expect(provider.clipboard.getClipboardContent).toBeDefined();
60 | expect(provider.clipboard.setClipboardContent).toBeDefined();
61 | expect(provider.clipboard.hasClipboardText).toBeDefined();
62 | expect(provider.clipboard.clearClipboard).toBeDefined();
63 | });
64 | });
65 |
66 | describe('AutoHotkeyProvider - Factory Integration', () => {
67 | beforeEach(() => {
68 | // Mock the factory module to avoid keysender ELF header issue
69 | vi.doMock('../factory.js', () => ({
70 | createAutomationProvider: vi.fn().mockImplementation((config: any) => {
71 | if (config?.provider === 'autohotkey' || config?.providers) {
72 | return new AutoHotkeyProvider();
73 | }
74 | return {};
75 | }),
76 | }));
77 | });
78 |
79 | it('should be available through the factory', async () => {
80 | const { createAutomationProvider } = await import('../factory.js');
81 |
82 | const provider = createAutomationProvider({ provider: 'autohotkey' });
83 | expect(provider).toBeDefined();
84 | expect(provider).toBeInstanceOf(AutoHotkeyProvider);
85 | });
86 |
87 | it('should support modular configuration', async () => {
88 | const { createAutomationProvider } = await import('../factory.js');
89 |
90 | const provider = createAutomationProvider({
91 | providers: {
92 | keyboard: 'autohotkey',
93 | mouse: 'autohotkey',
94 | screen: 'autohotkey',
95 | clipboard: 'autohotkey',
96 | },
97 | });
98 |
99 | expect(provider).toBeDefined();
100 | expect(provider.keyboard).toBeDefined();
101 | expect(provider.mouse).toBeDefined();
102 | expect(provider.screen).toBeDefined();
103 | expect(provider.clipboard).toBeDefined();
104 | });
105 | });
106 |
```
--------------------------------------------------------------------------------
/RELEASE_NOTES_v0.2.0.md:
--------------------------------------------------------------------------------
```markdown
1 | # Release Notes v0.2.0
2 |
3 | ## 🎉 Major Features
4 |
5 | ### SSE Transport Now Officially Supported
6 | - Full implementation of Server-Sent Events (SSE) transport for network access
7 | - Built using the MCP SDK transport layer for improved reliability
8 | - HTTP/HTTPS server integration for secure connections
9 | - Configurable port settings (default: 3232)
10 |
11 | ### HTTPS/TLS Support
12 | - Added secure TLS/SSL support for production deployments
13 | - New CLI flags: `--https`, `--cert`, and `--key`
14 | - Certificate validation for enhanced security
15 | - Meets MCP specification requirements for secure remote access
16 |
17 | ### Improved Documentation
18 | - Added comprehensive Quick Start guide with build tools setup
19 | - Enhanced installation instructions for Windows users
20 | - Clear prerequisites including VC++ workload requirements
21 | - Better guidance for Python and Node.js installation
22 |
23 | ## 🚀 Enhancements
24 |
25 | ### Infrastructure Improvements
26 | - Optimized build process with npm ci and caching
27 | - Standardized default port (3232) across entire codebase
28 | - Removed unused dependencies (express, jimp, mcp-control)
29 | - Improved GitHub Actions with better error handling
30 |
31 | ### Testing Framework
32 | - Added end-to-end testing suite for integration testing
33 | - Better test coverage for SSE transport features
34 | - Enhanced CI/CD pipeline reliability
35 |
36 | ### Developer Experience
37 | - Simplified SSE implementation using SDK transport
38 | - Better error handling for client connections
39 | - Buffer management improvements
40 | - Platform-specific path fixes
41 |
42 | ## 🔧 CLI Updates
43 |
44 | New command line options:
45 | ```bash
46 | # Run with SSE transport
47 | mcp-control --sse
48 |
49 | # Run with HTTPS/TLS
50 | mcp-control --sse --https --cert /path/to/cert.pem --key /path/to/key.pem
51 |
52 | # Custom port
53 | mcp-control --sse --port 3000
54 | ```
55 |
56 | ## 📦 Dependency Updates
57 |
58 | - Updated `@modelcontextprotocol/sdk` to latest version
59 | - Bumped TypeScript ESLint packages to v8.32.0+
60 | - Updated `zod` to v3.24.4
61 | - Various dev dependency updates for security
62 |
63 | ## 📚 Documentation
64 |
65 | - Added SSE transport documentation
66 | - Updated README with release badges
67 | - Improved branch structure documentation
68 | - Added build tools setup instructions
69 | - Enhanced security guidelines
70 |
71 | ## 🐛 Bug Fixes
72 |
73 | - Fixed TypeScript errors related to HTTP server usage
74 | - Resolved client error handling in SSE transport
75 | - Corrected platform-specific path issues
76 | - Fixed npm ci error handling in build scripts
77 |
78 | ## ⚠️ Breaking Changes
79 |
80 | - SSE is now the recommended transport method
81 | - HTTPS is required for production deployments per MCP spec
82 | - Some internal API changes for transport handling
83 |
84 | ## 🔐 Security
85 |
86 | - Added proper TLS certificate validation
87 | - Implemented security options for HTTPS connections
88 | - Updated dependencies to address known vulnerabilities
89 |
90 | ## 📈 Migration Guide
91 |
92 | To upgrade from v0.1.x to v0.2.0:
93 |
94 | 1. Update your Claude client configuration to use SSE transport:
95 | ```json
96 | {
97 | "mcpServers": {
98 | "MCPControl": {
99 | "command": "mcp-control",
100 | "args": ["--transport", "sse"]
101 | }
102 | }
103 | }
104 | ```
105 |
106 | 2. For production deployments, use HTTPS:
107 | ```bash
108 | mcp-control --sse --https --cert cert.pem --key key.pem
109 | ```
110 |
111 | 3. Ensure you have the latest build tools installed as per the Quick Start guide
112 |
113 | ## 👥 Contributors
114 |
115 | Special thanks to all contributors who made this release possible, including:
116 | - @Cheffromspace for SSE transport and HTTPS implementation
117 | - @lwsinclair for adding the MseeP.ai security badge
118 | - All the community members who reported issues and provided feedback
119 |
120 | ## 🔮 What's Next
121 |
122 | - Multi-monitor support improvements
123 | - Enhanced click accuracy at different resolutions
124 | - Additional transport options
125 | - Performance optimizations
126 |
127 | ---
128 |
129 | Thank you for using MCPControl! We're excited to bring you these improvements and look forward to your feedback.
```
--------------------------------------------------------------------------------
/src/providers/keysender/index.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { createAutomationProvider } from '../factory.js';
3 | // Imported for type checking but used indirectly through factory
4 | import './index.js';
5 |
6 | // Mock keysender module
7 | vi.mock('keysender', async () => {
8 | await vi.importActual('vitest');
9 |
10 | const mockObject = {
11 | Hardware: vi.fn().mockImplementation(() => ({
12 | workwindow: {
13 | capture: vi.fn(),
14 | get: vi.fn(),
15 | set: vi.fn(),
16 | getView: vi.fn(),
17 | setForeground: vi.fn(),
18 | setView: vi.fn(),
19 | isForeground: vi.fn(),
20 | isOpen: vi.fn(),
21 | },
22 | mouse: {
23 | move: vi.fn(),
24 | leftClick: vi.fn(),
25 | rightClick: vi.fn(),
26 | middleClick: vi.fn(),
27 | doubleClick: vi.fn(),
28 | leftDown: vi.fn(),
29 | leftUp: vi.fn(),
30 | rightDown: vi.fn(),
31 | rightUp: vi.fn(),
32 | scroll: vi.fn(),
33 | },
34 | keyboard: {
35 | pressKey: vi.fn(),
36 | releaseKey: vi.fn(),
37 | typeString: vi.fn(),
38 | },
39 | clipboard: {
40 | getClipboard: vi.fn(),
41 | setClipboard: vi.fn(),
42 | },
43 | })),
44 | getScreenSize: vi.fn().mockReturnValue({ width: 1920, height: 1080 }),
45 | getAllWindows: vi.fn().mockReturnValue([{ title: 'Test Window', handle: 12345 }]),
46 | getWindowChildren: vi.fn().mockReturnValue([]),
47 | };
48 |
49 | return {
50 | default: mockObject,
51 | ...mockObject,
52 | };
53 | });
54 |
55 | // Create a simple mock of KeysenderProvider for use in tests
56 | class MockKeysenderProvider {
57 | keyboard = { keyTap: vi.fn() };
58 | mouse = { moveMouse: vi.fn() };
59 | screen = { getScreenSize: vi.fn() };
60 | clipboard = { readClipboard: vi.fn() };
61 | }
62 |
63 | // Mock the factory to avoid native module loading issues
64 | vi.mock('../factory.js', async () => {
65 | await vi.importActual('vitest');
66 |
67 | return {
68 | createAutomationProvider: vi.fn().mockImplementation((_providerType) => {
69 | return new MockKeysenderProvider();
70 | }),
71 | };
72 | });
73 |
74 | // Mock the automation classes
75 | vi.mock('./keyboard.js', async () => {
76 | await vi.importActual('vitest');
77 | return {
78 | KeysenderKeyboardAutomation: vi.fn().mockImplementation(() => ({
79 | keyTap: vi.fn(),
80 | keyToggle: vi.fn(),
81 | typeString: vi.fn(),
82 | typeStringDelayed: vi.fn(),
83 | setKeyboardDelay: vi.fn(),
84 | })),
85 | };
86 | });
87 |
88 | vi.mock('./mouse.js', async () => {
89 | await vi.importActual('vitest');
90 | return {
91 | KeysenderMouseAutomation: vi.fn().mockImplementation(() => ({
92 | moveMouse: vi.fn(),
93 | moveMouseSmooth: vi.fn(),
94 | mouseClick: vi.fn(),
95 | mouseDoubleClick: vi.fn(),
96 | mouseToggle: vi.fn(),
97 | dragMouse: vi.fn(),
98 | scrollMouse: vi.fn(),
99 | getMousePosition: vi.fn(),
100 | setMousePosition: vi.fn(),
101 | setMouseSpeed: vi.fn(),
102 | })),
103 | };
104 | });
105 |
106 | vi.mock('./screen.js', async () => {
107 | await vi.importActual('vitest');
108 | return {
109 | KeysenderScreenAutomation: vi.fn().mockImplementation(() => ({
110 | getScreenSize: vi.fn(),
111 | getScreenshot: vi.fn(),
112 | getActiveWindow: vi.fn(),
113 | focusWindow: vi.fn(),
114 | resizeWindow: vi.fn(),
115 | repositionWindow: vi.fn(),
116 | })),
117 | };
118 | });
119 |
120 | vi.mock('./clipboard.js', async () => {
121 | await vi.importActual('vitest');
122 | return {
123 | KeysenderClipboardAutomation: vi.fn().mockImplementation(() => ({
124 | readClipboard: vi.fn(),
125 | writeClipboard: vi.fn(),
126 | })),
127 | };
128 | });
129 |
130 | describe('KeysenderProvider', () => {
131 | it('should be created through the factory', () => {
132 | const provider = createAutomationProvider({ provider: 'keysender' });
133 | expect(provider).toBeInstanceOf(MockKeysenderProvider);
134 | });
135 |
136 | it('should have all required automation interfaces', () => {
137 | const provider = new MockKeysenderProvider();
138 |
139 | expect(provider.keyboard).toBeDefined();
140 | expect(provider.mouse).toBeDefined();
141 | expect(provider.screen).toBeDefined();
142 | expect(provider.clipboard).toBeDefined();
143 | });
144 | });
145 |
```
--------------------------------------------------------------------------------
/.github/pr-webhook-utils.cjs:
--------------------------------------------------------------------------------
```
1 | /**
2 | * Utilities for PR webhook data handling and sanitization
3 | */
4 |
5 | /**
6 | * Sanitizes text content to remove truly sensitive information
7 | * @param {string} text - Text content to sanitize
8 | * @returns {string} Sanitized text
9 | */
10 | function sanitizeText(text) {
11 | if (!text) return '';
12 |
13 | try {
14 | return text
15 | // Remove common API tokens with specific patterns
16 | .replace(/(\b)(gh[ps]_[A-Za-z0-9_]{36,})(\b)/g, '[GH_TOKEN_REDACTED]')
17 | .replace(/(\b)(xox[pbar]-[0-9a-zA-Z-]{10,})(\b)/g, '[SLACK_TOKEN_REDACTED]')
18 | .replace(/(\b)(sk-[a-zA-Z0-9]{32,})(\b)/g, '[API_KEY_REDACTED]')
19 | .replace(/(\b)(AKIA[0-9A-Z]{16})(\b)/g, '[AWS_KEY_REDACTED]')
20 | // Remove emails, but only likely real ones (with valid TLDs)
21 | .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/g, '[EMAIL_REDACTED]')
22 | // Remove IP addresses
23 | .replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '[IP_REDACTED]')
24 | // Remove control characters that might break JSON
25 | .replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
26 | } catch (error) {
27 | console.warn(`Error sanitizing text: ${error.message}`);
28 | return '[Content omitted due to sanitization error]';
29 | }
30 | }
31 |
32 | /**
33 | * Checks if a file should be included in webhook data
34 | * @param {string} filename - Filename to check
35 | * @returns {boolean} Whether file should be included
36 | */
37 | function shouldIncludeFile(filename) {
38 | if (!filename) return false;
39 |
40 | const sensitivePatterns = [
41 | // Only exclude actual sensitive files
42 | /\.env($|\.)/i,
43 | /\.key$/i,
44 | /\.pem$/i,
45 | /\.pfx$/i,
46 | /\.p12$/i,
47 | // Binary files that would bloat the payload
48 | /\.(jpg|jpeg|png|gif|ico|pdf|zip|tar|gz|bin|exe)$/i
49 | ];
50 |
51 | return !sensitivePatterns.some(pattern => pattern.test(filename));
52 | }
53 |
54 | /**
55 | * Safely limits patch size to prevent payload issues
56 | * @param {string} patch - Git patch content
57 | * @returns {string|undefined} Limited patch or undefined on error
58 | */
59 | function limitPatch(patch) {
60 | if (!patch) return undefined;
61 |
62 | try {
63 | // Increase reasonable patch size limit to 30KB
64 | const maxPatchSize = 30 * 1024;
65 | if (patch.length > maxPatchSize) {
66 | return patch.substring(0, maxPatchSize) + '\n[... PATCH TRUNCATED DUE TO SIZE ...]';
67 | }
68 | return patch;
69 | } catch (error) {
70 | console.warn(`Error limiting patch: ${error.message}`);
71 | return undefined; // Return undefined on error
72 | }
73 | }
74 |
75 | /**
76 | * Safely stringifies JSON with error handling
77 | * @param {Object} data - Data to stringify
78 | * @returns {Object} Result with success status and data/error
79 | */
80 | function safeStringify(data) {
81 | try {
82 | const jsonData = JSON.stringify(data);
83 | return { success: true, data: jsonData };
84 | } catch (error) {
85 | console.error(`JSON stringify error: ${error.message}`);
86 | return {
87 | success: false,
88 | error: error.message
89 | };
90 | }
91 | }
92 |
93 | /**
94 | * Creates a simplified version of PR data that's less likely to cause parsing issues
95 | * Used as a fallback when the full PR data cannot be stringified
96 | * @param {Object} pr - Full PR data
97 | * @param {Object} context - GitHub context object
98 | * @returns {Object} Simplified PR data with essential information only
99 | */
100 | function createSimplifiedPrData(pr, context) {
101 | return {
102 | id: pr.data.id,
103 | number: pr.data.number,
104 | title: sanitizeText(pr.data.title),
105 | state: pr.data.state,
106 | created_at: pr.data.created_at,
107 | repository: context.repo.repo,
108 | owner: context.repo.owner,
109 | body: sanitizeText(pr.data.body?.substring(0, 1000)),
110 | head: {
111 | ref: pr.data.head.ref,
112 | sha: pr.data.head.sha
113 | },
114 | base: {
115 | ref: pr.data.base.ref,
116 | sha: pr.data.base.sha
117 | },
118 | labels: pr.data.labels?.map(l => l.name),
119 | error: 'Using simplified payload due to JSON serialization issues with full payload'
120 | };
121 | }
122 |
123 | module.exports = {
124 | sanitizeText,
125 | shouldIncludeFile,
126 | limitPatch,
127 | safeStringify,
128 | createSimplifiedPrData
129 | };
130 |
```
--------------------------------------------------------------------------------
/src/providers/factory.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AutomationProvider } from '../interfaces/provider.js';
2 | import { KeysenderProvider } from './keysender/index.js';
3 | import { AutoHotkeyProvider } from './autohotkey/index.js';
4 | import { registry } from './registry.js';
5 | import { AutomationConfig } from '../config.js';
6 | import {
7 | KeyboardAutomation,
8 | MouseAutomation,
9 | ScreenAutomation,
10 | ClipboardAutomation,
11 | } from '../interfaces/automation.js';
12 |
13 | // Import individual providers
14 | import { PowerShellClipboardProvider } from './clipboard/powershell/index.js';
15 | import { ClipboardyProvider } from './clipboard/clipboardy/index.js';
16 |
17 | // Cache to store provider instances
18 | const providerCache: Record<string, AutomationProvider> = {};
19 |
20 | /**
21 | * Initialize the provider registry with available providers
22 | */
23 | export function initializeProviders(): void {
24 | // Register clipboard providers
25 | registry.registerClipboard('powershell', new PowerShellClipboardProvider());
26 | registry.registerClipboard('clipboardy', new ClipboardyProvider());
27 |
28 | // Register AutoHotkey providers
29 | const autohotkeyProvider = new AutoHotkeyProvider();
30 | registry.registerKeyboard('autohotkey', autohotkeyProvider.keyboard);
31 | registry.registerMouse('autohotkey', autohotkeyProvider.mouse);
32 | registry.registerScreen('autohotkey', autohotkeyProvider.screen);
33 | registry.registerClipboard('autohotkey', autohotkeyProvider.clipboard);
34 |
35 | // TODO: Register other providers as they are implemented
36 | }
37 |
38 | /**
39 | * Composite provider that allows mixing different component providers
40 | */
41 | class CompositeProvider implements AutomationProvider {
42 | keyboard: KeyboardAutomation;
43 | mouse: MouseAutomation;
44 | screen: ScreenAutomation;
45 | clipboard: ClipboardAutomation;
46 |
47 | constructor(
48 | keyboard: KeyboardAutomation,
49 | mouse: MouseAutomation,
50 | screen: ScreenAutomation,
51 | clipboard: ClipboardAutomation,
52 | ) {
53 | this.keyboard = keyboard;
54 | this.mouse = mouse;
55 | this.screen = screen;
56 | this.clipboard = clipboard;
57 | }
58 | }
59 |
60 | /**
61 | * Create an automation provider instance based on configuration
62 | * Supports both legacy monolithic providers and new modular providers
63 | */
64 | export function createAutomationProvider(config?: AutomationConfig): AutomationProvider {
65 | // Initialize providers if not already done
66 | if (registry.getAvailableProviders().clipboards.length === 0) {
67 | initializeProviders();
68 | }
69 |
70 | if (!config || !config.providers) {
71 | // Legacy behavior: use monolithic provider
72 | const type = config?.provider || 'keysender';
73 | const providerType = type.toLowerCase();
74 |
75 | // Return cached instance if available
76 | if (providerCache[providerType]) {
77 | return providerCache[providerType];
78 | }
79 |
80 | let provider: AutomationProvider;
81 | switch (providerType) {
82 | case 'keysender':
83 | provider = new KeysenderProvider();
84 | break;
85 | case 'autohotkey':
86 | provider = new AutoHotkeyProvider();
87 | break;
88 | default:
89 | throw new Error(`Unknown provider type: ${providerType}`);
90 | }
91 |
92 | // Cache the instance
93 | providerCache[providerType] = provider;
94 | return provider;
95 | }
96 |
97 | // New modular approach
98 | const cacheKey = JSON.stringify(config.providers);
99 | if (providerCache[cacheKey]) {
100 | return providerCache[cacheKey];
101 | }
102 |
103 | // Get individual components from the registry
104 | const keyboardProvider = config.providers.keyboard
105 | ? registry.getKeyboard(config.providers.keyboard)
106 | : new KeysenderProvider().keyboard;
107 |
108 | const mouseProvider = config.providers.mouse
109 | ? registry.getMouse(config.providers.mouse)
110 | : new KeysenderProvider().mouse;
111 |
112 | const screenProvider = config.providers.screen
113 | ? registry.getScreen(config.providers.screen)
114 | : new KeysenderProvider().screen;
115 |
116 | const clipboardProvider = config.providers.clipboard
117 | ? registry.getClipboard(config.providers.clipboard)
118 | : new KeysenderProvider().clipboard;
119 |
120 | if (!keyboardProvider || !mouseProvider || !screenProvider || !clipboardProvider) {
121 | throw new Error('Failed to resolve all provider components');
122 | }
123 |
124 | const compositeProvider = new CompositeProvider(
125 | keyboardProvider,
126 | mouseProvider,
127 | screenProvider,
128 | clipboardProvider,
129 | );
130 |
131 | providerCache[cacheKey] = compositeProvider;
132 | return compositeProvider;
133 | }
134 |
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Centralized logger using Pino
3 | * Provides consistent logging with configurable log levels
4 | */
5 |
6 | /**
7 | * Log levels in order of verbosity (most verbose to least verbose)
8 | * - trace: Most detailed information for tracing code execution
9 | * - debug: Debugging information useful during development
10 | * - info: General information about normal operation
11 | * - warn: Warning conditions that should be reviewed
12 | * - error: Error conditions that don't interrupt operation
13 | * - fatal: Critical errors that might interrupt operation
14 | * - silent: No logs at all
15 | */
16 | type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
17 |
18 | /**
19 | * Interface for a logger
20 | */
21 | export interface Logger {
22 | trace(msg: string, ...args: unknown[]): void;
23 | debug(msg: string, ...args: unknown[]): void;
24 | info(msg: string, ...args: unknown[]): void;
25 | warn(msg: string, ...args: unknown[]): void;
26 | error(msg: string, ...args: unknown[]): void;
27 | fatal(msg: string, ...args: unknown[]): void;
28 | child(bindings: Record<string, unknown>): Logger;
29 | }
30 |
31 | /**
32 | * Simple console-based logger implementation
33 | * This will be replaced with Pino in the package.json update
34 | */
35 | class ConsoleLogger implements Logger {
36 | private level: number;
37 | private readonly levelMap: Record<LogLevel, number> = {
38 | trace: 10,
39 | debug: 20,
40 | info: 30,
41 | warn: 40,
42 | error: 50,
43 | fatal: 60,
44 | silent: 100,
45 | };
46 | private context: Record<string, unknown> = {};
47 |
48 | constructor(level: LogLevel = 'info', context: Record<string, unknown> = {}) {
49 | this.level = this.levelMap[level];
50 | this.context = context;
51 | }
52 |
53 | private formatMessage(msg: string): string {
54 | if (Object.keys(this.context).length === 0) {
55 | return msg;
56 | }
57 |
58 | const contextStr = Object.entries(this.context)
59 | .map(([key, value]) => `${key}=${String(value)}`)
60 | .join(' ');
61 |
62 | return `[${contextStr}] ${msg}`;
63 | }
64 |
65 | private shouldLog(level: LogLevel): boolean {
66 | return this.levelMap[level] >= this.level;
67 | }
68 |
69 | trace(msg: string, ...args: unknown[]): void {
70 | if (this.shouldLog('trace')) {
71 | console.log(`[TRACE] ${this.formatMessage(msg)}`, ...args);
72 | }
73 | }
74 |
75 | debug(msg: string, ...args: unknown[]): void {
76 | if (this.shouldLog('debug')) {
77 | console.log(`[DEBUG] ${this.formatMessage(msg)}`, ...args);
78 | }
79 | }
80 |
81 | info(msg: string, ...args: unknown[]): void {
82 | if (this.shouldLog('info')) {
83 | console.log(`[INFO] ${this.formatMessage(msg)}`, ...args);
84 | }
85 | }
86 |
87 | warn(msg: string, ...args: unknown[]): void {
88 | if (this.shouldLog('warn')) {
89 | console.warn(`[WARN] ${this.formatMessage(msg)}`, ...args);
90 | }
91 | }
92 |
93 | error(msg: string, ...args: unknown[]): void {
94 | if (this.shouldLog('error')) {
95 | console.error(`[ERROR] ${this.formatMessage(msg)}`, ...args);
96 | }
97 | }
98 |
99 | fatal(msg: string, ...args: unknown[]): void {
100 | if (this.shouldLog('fatal')) {
101 | console.error(`[FATAL] ${this.formatMessage(msg)}`, ...args);
102 | }
103 | }
104 |
105 | child(bindings: Record<string, unknown>): Logger {
106 | return new ConsoleLogger(
107 | Object.keys(this.levelMap).find(key => this.levelMap[key as LogLevel] === this.level) as LogLevel,
108 | { ...this.context, ...bindings }
109 | );
110 | }
111 | }
112 |
113 | /**
114 | * Get log level from environment variable or use default
115 | */
116 | export function getLogLevel(): LogLevel {
117 | const envLevel = process.env.LOG_LEVEL?.toLowerCase() as LogLevel | undefined;
118 |
119 | // Validate that the provided level is valid
120 | const validLevels: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'];
121 |
122 | if (envLevel && validLevels.includes(envLevel)) {
123 | return envLevel;
124 | }
125 |
126 | // Default to info in production, debug in development/test
127 | if (process.env.NODE_ENV === 'production') {
128 | return 'info';
129 | } else if (process.env.NODE_ENV === 'test' || process.env.VITEST) {
130 | return 'silent'; // Silent in tests unless explicitly set
131 | } else {
132 | return 'debug';
133 | }
134 | }
135 |
136 | // Create the default logger instance
137 | export const logger: Logger = new ConsoleLogger(getLogLevel());
138 |
139 | /**
140 | * Create a child logger with component context
141 | * @param component Component name or identifier
142 | * @returns Logger instance with component context
143 | */
144 | export function createLogger(component: string): Logger {
145 | return logger.child({ component });
146 | }
147 |
148 | export default logger;
```
--------------------------------------------------------------------------------
/src/tools/keyboard.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import { typeText, pressKey, pressKeyCombination, holdKey } from './keyboard.js';
3 | import type { KeyboardInput, KeyCombination, KeyHoldOperation } from '../types/common.js';
4 |
5 | // Mock the provider
6 | vi.mock('../providers/factory.js', () => ({
7 | createAutomationProvider: () => ({
8 | keyboard: {
9 | typeText: vi.fn().mockImplementation(() => ({
10 | success: true,
11 | message: 'Typed text successfully',
12 | })),
13 | pressKey: vi.fn().mockImplementation((key) => ({
14 | success: true,
15 | message: `Pressed key: ${key}`,
16 | })),
17 | pressKeyCombination: vi.fn().mockImplementation((combination) => ({
18 | success: true,
19 | message: `Pressed key combination: ${combination.keys.join('+')}`,
20 | })),
21 | holdKey: vi.fn().mockImplementation((operation) =>
22 | operation.state === 'down'
23 | ? {
24 | success: true,
25 | message: `Key ${operation.key} held successfully for ${operation.duration}ms`,
26 | }
27 | : { success: true, message: `Key ${operation.key} released successfully` },
28 | ),
29 | },
30 | }),
31 | }));
32 |
33 | describe('Keyboard Tools', () => {
34 | beforeEach(() => {
35 | vi.clearAllMocks();
36 | });
37 |
38 | describe('typeText', () => {
39 | it('should successfully type text', () => {
40 | const input: KeyboardInput = { text: 'Hello World' };
41 | const result = typeText(input);
42 |
43 | expect(result).toEqual({
44 | success: true,
45 | message: 'Typed text successfully',
46 | });
47 | });
48 |
49 | it('should handle errors when text is missing', () => {
50 | const input: KeyboardInput = { text: '' };
51 | const result = typeText(input);
52 |
53 | expect(result.success).toBe(false);
54 | expect(result.message).toContain('Text is required');
55 | });
56 |
57 | it('should handle errors when text is too long', () => {
58 | // Create a string that's too long
59 | const longText = 'a'.repeat(1001);
60 | const input: KeyboardInput = { text: longText };
61 | const result = typeText(input);
62 |
63 | expect(result.success).toBe(false);
64 | expect(result.message).toContain('Text too long');
65 | });
66 | });
67 |
68 | describe('pressKey', () => {
69 | it('should successfully press a single key', () => {
70 | const result = pressKey('a');
71 |
72 | expect(result).toEqual({
73 | success: true,
74 | message: 'Pressed key: a',
75 | });
76 | });
77 |
78 | it('should handle errors when key is invalid', () => {
79 | const result = pressKey('invalid_key');
80 |
81 | expect(result.success).toBe(false);
82 | expect(result.message).toContain('Invalid key');
83 | });
84 | });
85 |
86 | describe('pressKeyCombination', () => {
87 | it('should successfully press a key combination', async () => {
88 | const combination: KeyCombination = { keys: ['ctrl', 'c'] };
89 | const result = await pressKeyCombination(combination);
90 |
91 | expect(result).toEqual({
92 | success: true,
93 | message: 'Pressed key combination: ctrl+c',
94 | });
95 | });
96 |
97 | it('should handle errors when combination is invalid', async () => {
98 | const result = await pressKeyCombination({ keys: [] });
99 |
100 | expect(result.success).toBe(false);
101 | expect(result.message).toContain('Key combination must contain at least one key');
102 | });
103 | });
104 |
105 | describe('holdKey', () => {
106 | beforeEach(() => {
107 | vi.useFakeTimers();
108 | });
109 |
110 | it('should successfully hold and release a key', async () => {
111 | const operation: KeyHoldOperation = {
112 | key: 'shift',
113 | duration: 1000,
114 | state: 'down',
115 | };
116 |
117 | const holdPromise = holdKey(operation);
118 |
119 | // Fast-forward through the duration
120 | await vi.runAllTimersAsync();
121 | const result = await holdPromise;
122 |
123 | expect(result).toEqual({
124 | success: true,
125 | message: 'Key shift held successfully for 1000ms',
126 | });
127 | });
128 |
129 | it('should handle just releasing a key', async () => {
130 | const operation: KeyHoldOperation = {
131 | key: 'shift',
132 | duration: 0,
133 | state: 'up',
134 | };
135 |
136 | const result = await holdKey(operation);
137 |
138 | expect(result).toEqual({
139 | success: true,
140 | message: 'Key shift released successfully',
141 | });
142 | });
143 |
144 | it('should handle errors when key is invalid', async () => {
145 | const operation: KeyHoldOperation = {
146 | // @ts--error - Testing invalid input
147 | key: 'invalid_key',
148 | duration: 1000,
149 | state: 'down',
150 | };
151 |
152 | const result = await holdKey(operation);
153 |
154 | expect(result.success).toBe(false);
155 | expect(result.message).toContain('Invalid key');
156 | });
157 | });
158 | });
159 |
```
--------------------------------------------------------------------------------
/src/tools/mouse.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { MousePosition } from '../types/common.js';
2 | import { WindowsControlResponse } from '../types/responses.js';
3 | import { createAutomationProvider } from '../providers/factory.js';
4 | import { MousePositionSchema, MouseButtonSchema, ScrollAmountSchema } from './validation.zod.js';
5 | import { createLogger } from '../logger.js';
6 |
7 | // Get the automation provider
8 | const provider = createAutomationProvider();
9 |
10 | // Create a logger for mouse module
11 | const logger = createLogger('mouse');
12 |
13 | // Define button types
14 | type MouseButton = 'left' | 'right' | 'middle';
15 |
16 | export function moveMouse(position: MousePosition): WindowsControlResponse {
17 | try {
18 | // Validate the position
19 | MousePositionSchema.parse(position);
20 |
21 | // Additional screen bounds check if not in test environment
22 | if (!(process.env.NODE_ENV === 'test' || process.env.VITEST)) {
23 | try {
24 | const screenSizeResponse = provider.screen.getScreenSize();
25 | if (screenSizeResponse.success && screenSizeResponse.data) {
26 | const screenSize = screenSizeResponse.data as { width: number; height: number };
27 | if (
28 | position.x < 0 ||
29 | position.x >= screenSize.width ||
30 | position.y < 0 ||
31 | position.y >= screenSize.height
32 | ) {
33 | throw new Error(
34 | `Position (${position.x},${position.y}) is outside screen bounds (0,0)-(${screenSize.width - 1},${screenSize.height - 1})`,
35 | );
36 | }
37 | }
38 | } catch (screenError) {
39 | logger.warn('Error checking screen bounds', screenError);
40 | // Continue without screen bounds check
41 | }
42 | }
43 |
44 | return provider.mouse.moveMouse(position);
45 | } catch (error) {
46 | return {
47 | success: false,
48 | message: `Failed to move mouse: ${error instanceof Error ? error.message : String(error)}`,
49 | };
50 | }
51 | }
52 |
53 | export function clickMouse(button: MouseButton = 'left'): WindowsControlResponse {
54 | try {
55 | // Validate button
56 | MouseButtonSchema.parse(button);
57 | const validatedButton = button;
58 |
59 | return provider.mouse.clickMouse(validatedButton);
60 | } catch (error) {
61 | return {
62 | success: false,
63 | message: `Failed to click mouse: ${error instanceof Error ? error.message : String(error)}`,
64 | };
65 | }
66 | }
67 |
68 | export function doubleClick(position?: MousePosition): WindowsControlResponse {
69 | try {
70 | // Validate position if provided
71 | if (position) {
72 | MousePositionSchema.parse(position);
73 | }
74 |
75 | return provider.mouse.doubleClick(position);
76 | } catch (error) {
77 | return {
78 | success: false,
79 | message: `Failed to double click: ${error instanceof Error ? error.message : String(error)}`,
80 | };
81 | }
82 | }
83 |
84 | export function getCursorPosition(): WindowsControlResponse {
85 | try {
86 | return provider.mouse.getCursorPosition();
87 | } catch (error) {
88 | return {
89 | success: false,
90 | message: `Failed to get cursor position: ${error instanceof Error ? error.message : String(error)}`,
91 | };
92 | }
93 | }
94 |
95 | export function scrollMouse(amount: number): WindowsControlResponse {
96 | try {
97 | // Validate amount
98 | ScrollAmountSchema.parse(amount);
99 |
100 | return provider.mouse.scrollMouse(amount);
101 | } catch (error) {
102 | return {
103 | success: false,
104 | message: `Failed to scroll mouse: ${error instanceof Error ? error.message : String(error)}`,
105 | };
106 | }
107 | }
108 |
109 | export function dragMouse(
110 | from: MousePosition,
111 | to: MousePosition,
112 | button: MouseButton = 'left',
113 | ): WindowsControlResponse {
114 | try {
115 | // Validate positions
116 | MousePositionSchema.parse(from);
117 | MousePositionSchema.parse(to);
118 |
119 | // Validate button
120 | MouseButtonSchema.parse(button);
121 | const validatedButton = button;
122 |
123 | return provider.mouse.dragMouse(from, to, validatedButton);
124 | } catch (error) {
125 | return {
126 | success: false,
127 | message: `Failed to drag mouse: ${error instanceof Error ? error.message : String(error)}`,
128 | };
129 | }
130 | }
131 |
132 | export function clickAt(
133 | x: number,
134 | y: number,
135 | button: MouseButton = 'left',
136 | ): WindowsControlResponse {
137 | // Special case for test compatibility (match original implementation)
138 | if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
139 | return {
140 | success: false,
141 | message: 'Invalid coordinates provided',
142 | };
143 | }
144 |
145 | try {
146 | // Validate position against screen bounds
147 | MousePositionSchema.parse({ x, y });
148 |
149 | // Validate button
150 | MouseButtonSchema.parse(button);
151 | const validatedButton = button;
152 |
153 | return provider.mouse.clickAt(x, y, validatedButton);
154 | } catch (error) {
155 | return {
156 | success: false,
157 | message: `Failed to click at position: ${error instanceof Error ? error.message : String(error)}`,
158 | };
159 | }
160 | }
161 |
```
--------------------------------------------------------------------------------
/src/providers/clipboard/powershell/index.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import type { ClipboardInput } from '../../../types/common.js';
3 |
4 | // Set up mocks at the top level
5 | const execAsyncMock = vi.fn();
6 |
7 | vi.mock('child_process', () => ({
8 | exec: vi.fn(),
9 | }));
10 |
11 | vi.mock('util', () => ({
12 | promisify: vi.fn().mockReturnValue(execAsyncMock),
13 | }));
14 |
15 | // Dynamic import to ensure mocks are setup
16 | describe('PowerShellClipboardProvider', () => {
17 | let PowerShellClipboardProvider: any;
18 | let provider: any;
19 |
20 | beforeEach(async () => {
21 | vi.clearAllMocks();
22 | execAsyncMock.mockReset();
23 |
24 | // Dynamically import after mocks are setup
25 | const module = await import('./index.js');
26 | PowerShellClipboardProvider = module.PowerShellClipboardProvider;
27 | provider = new PowerShellClipboardProvider();
28 | });
29 |
30 | describe('getClipboardContent', () => {
31 | it('should get clipboard content successfully', async () => {
32 | execAsyncMock.mockResolvedValue({ stdout: 'Test content\n', stderr: '' });
33 |
34 | const result = await provider.getClipboardContent();
35 |
36 | expect(result.success).toBe(true);
37 | expect(result.data).toBe('Test content');
38 | expect(execAsyncMock).toHaveBeenCalledWith('powershell.exe -Command "Get-Clipboard"');
39 | });
40 |
41 | it('should handle errors', async () => {
42 | execAsyncMock.mockRejectedValue(new Error('PowerShell error'));
43 |
44 | const result = await provider.getClipboardContent();
45 |
46 | expect(result.success).toBe(false);
47 | expect(result.message).toContain('Failed to get clipboard content');
48 | });
49 |
50 | it('should handle stderr', async () => {
51 | execAsyncMock.mockResolvedValue({ stdout: '', stderr: 'Error output' });
52 |
53 | const result = await provider.getClipboardContent();
54 |
55 | expect(result.success).toBe(false);
56 | expect(result.message).toContain('Error output');
57 | });
58 | });
59 |
60 | describe('setClipboardContent', () => {
61 | it('should set clipboard content successfully', async () => {
62 | execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
63 | const input: ClipboardInput = { text: 'New content' };
64 |
65 | const result = await provider.setClipboardContent(input);
66 |
67 | expect(result.success).toBe(true);
68 | expect(execAsyncMock).toHaveBeenCalledWith(
69 | 'powershell.exe -Command "Set-Clipboard -Value "New content""',
70 | );
71 | });
72 |
73 | it('should escape quotes in text', async () => {
74 | execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
75 | const input: ClipboardInput = { text: 'Text with "quotes"' };
76 |
77 | await provider.setClipboardContent(input);
78 |
79 | expect(execAsyncMock).toHaveBeenCalledWith(
80 | 'powershell.exe -Command "Set-Clipboard -Value "Text with `"quotes`"""',
81 | );
82 | });
83 |
84 | it('should handle errors', async () => {
85 | execAsyncMock.mockRejectedValue(new Error('PowerShell error'));
86 |
87 | const input: ClipboardInput = { text: 'Test' };
88 | const result = await provider.setClipboardContent(input);
89 |
90 | expect(result.success).toBe(false);
91 | expect(result.message).toContain('Failed to set clipboard content');
92 | });
93 | });
94 |
95 | describe('hasClipboardText', () => {
96 | it('should return true when clipboard has text', async () => {
97 | execAsyncMock.mockResolvedValue({ stdout: 'Some text\n', stderr: '' });
98 |
99 | const result = await provider.hasClipboardText();
100 |
101 | expect(result.success).toBe(true);
102 | expect(result.data).toBe(true);
103 | });
104 |
105 | it('should return false when clipboard is empty', async () => {
106 | execAsyncMock.mockResolvedValue({ stdout: '\n', stderr: '' });
107 |
108 | const result = await provider.hasClipboardText();
109 |
110 | expect(result.success).toBe(true);
111 | expect(result.data).toBe(false);
112 | });
113 |
114 | it('should return false when clipboard is empty string', async () => {
115 | execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
116 |
117 | const result = await provider.hasClipboardText();
118 |
119 | expect(result.success).toBe(true);
120 | expect(result.data).toBe(false);
121 | });
122 | });
123 |
124 | describe('clearClipboard', () => {
125 | it('should clear clipboard successfully', async () => {
126 | execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
127 |
128 | const result = await provider.clearClipboard();
129 |
130 | expect(result.success).toBe(true);
131 | expect(execAsyncMock).toHaveBeenCalledWith(
132 | 'powershell.exe -Command "Set-Clipboard -Value """',
133 | );
134 | });
135 |
136 | it('should handle errors in clearClipboard', async () => {
137 | execAsyncMock.mockRejectedValue(new Error('PowerShell error'));
138 |
139 | const result = await provider.clearClipboard();
140 |
141 | expect(result.success).toBe(false);
142 | expect(result.message).toContain('Failed to clear clipboard');
143 | });
144 | });
145 | });
146 |
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/clipboard.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { execSync } from 'child_process';
2 | import { writeFileSync, unlinkSync, readFileSync } from 'fs';
3 | import { tmpdir } from 'os';
4 | import { join } from 'path';
5 | import { WindowsControlResponse } from '../../types/responses.js';
6 | import { ClipboardAutomation } from '../../interfaces/automation.js';
7 | import { ClipboardInput } from '../../types/common.js';
8 | import { getAutoHotkeyPath } from './utils.js';
9 |
10 | /**
11 | * AutoHotkey implementation of the ClipboardAutomation interface
12 | */
13 | export class AutoHotkeyClipboardAutomation implements ClipboardAutomation {
14 | /**
15 | * Execute an AutoHotkey script
16 | */
17 | private executeScript(script: string): void {
18 | const scriptPath = join(tmpdir(), `mcp-ahk-${Date.now()}.ahk`);
19 |
20 | try {
21 | // Write the script to a temporary file
22 | writeFileSync(scriptPath, script, 'utf8');
23 |
24 | // Execute the script with AutoHotkey v2
25 | const autohotkeyPath = getAutoHotkeyPath();
26 | execSync(`"${autohotkeyPath}" "${scriptPath}"`, { stdio: 'pipe' });
27 | } finally {
28 | // Clean up the temporary script file
29 | try {
30 | unlinkSync(scriptPath);
31 | } catch {
32 | // Ignore cleanup errors
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Execute a script and return output from a temporary file
39 | */
40 | private executeScriptWithOutput(script: string, _outputPath: string): void {
41 | const scriptPath = join(tmpdir(), `mcp-ahk-${Date.now()}.ahk`);
42 |
43 | try {
44 | writeFileSync(scriptPath, script, 'utf8');
45 | const autohotkeyPath = getAutoHotkeyPath();
46 | execSync(`"${autohotkeyPath}" "${scriptPath}"`, { stdio: 'pipe' });
47 | } finally {
48 | try {
49 | unlinkSync(scriptPath);
50 | } catch {
51 | // Ignore cleanup errors
52 | }
53 | }
54 | }
55 |
56 | // eslint-disable-next-line @typescript-eslint/require-await
57 | async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
58 | try {
59 | // Escape special characters
60 | const escapedText = input.text
61 | .replace(/\\/g, '\\\\')
62 | .replace(/"/g, '\\"')
63 | .replace(/`/g, '``')
64 | .replace(/{/g, '{{')
65 | .replace(/}/g, '}}');
66 |
67 | const script = `
68 | A_Clipboard := "${escapedText}"
69 | ExitApp
70 | `;
71 |
72 | this.executeScript(script);
73 |
74 | return {
75 | success: true,
76 | message: 'Text copied to clipboard',
77 | };
78 | } catch (error) {
79 | return {
80 | success: false,
81 | message: `Failed to copy to clipboard: ${error instanceof Error ? error.message : String(error)}`,
82 | };
83 | }
84 | }
85 |
86 | // This method is not part of the interface - removing it
87 | /*
88 | paste(): WindowsControlResponse {
89 | try {
90 | const script = `
91 | Send("^v")
92 | ExitApp
93 | `;
94 |
95 | this.executeScript(script);
96 |
97 | return {
98 | success: true,
99 | message: 'Pasted from clipboard',
100 | };
101 | } catch (error) {
102 | return {
103 | success: false,
104 | message: `Failed to paste from clipboard: ${error instanceof Error ? error.message : String(error)}`,
105 | };
106 | }
107 | }
108 | */
109 |
110 | // eslint-disable-next-line @typescript-eslint/require-await
111 | async hasClipboardText(): Promise<WindowsControlResponse> {
112 | try {
113 | const outputPath = join(tmpdir(), `mcp-ahk-output-${Date.now()}.txt`);
114 | const script = `
115 | hasText := A_Clipboard != ""
116 | FileAppend(hasText ? "true" : "false", "${outputPath}")
117 | ExitApp
118 | `;
119 |
120 | this.executeScriptWithOutput(script, outputPath);
121 |
122 | try {
123 | const result = readFileSync(outputPath, 'utf8');
124 | const hasText = result === 'true';
125 |
126 | return {
127 | success: true,
128 | message: hasText ? 'Clipboard contains text' : 'Clipboard is empty',
129 | data: { hasText },
130 | };
131 | } finally {
132 | try {
133 | unlinkSync(outputPath);
134 | } catch {
135 | // Ignore cleanup errors
136 | }
137 | }
138 | } catch (error) {
139 | return {
140 | success: false,
141 | message: `Failed to check clipboard content: ${error instanceof Error ? error.message : String(error)}`,
142 | };
143 | }
144 | }
145 |
146 | // eslint-disable-next-line @typescript-eslint/require-await
147 | async getClipboardContent(): Promise<WindowsControlResponse> {
148 | try {
149 | const outputPath = join(tmpdir(), `mcp-ahk-output-${Date.now()}.txt`);
150 | const script = `
151 | content := A_Clipboard
152 | FileAppend(content, "${outputPath}")
153 | ExitApp
154 | `;
155 |
156 | this.executeScriptWithOutput(script, outputPath);
157 |
158 | try {
159 | const content = readFileSync(outputPath, 'utf8');
160 | return {
161 | success: true,
162 | message: 'Retrieved clipboard content',
163 | data: { text: content },
164 | };
165 | } finally {
166 | try {
167 | unlinkSync(outputPath);
168 | } catch {
169 | // Ignore cleanup errors
170 | }
171 | }
172 | } catch (error) {
173 | return {
174 | success: false,
175 | message: `Failed to read from clipboard: ${error instanceof Error ? error.message : String(error)}`,
176 | };
177 | }
178 | }
179 |
180 | // eslint-disable-next-line @typescript-eslint/require-await
181 | async clearClipboard(): Promise<WindowsControlResponse> {
182 | try {
183 | const script = `
184 | A_Clipboard := ""
185 | ExitApp
186 | `;
187 |
188 | this.executeScript(script);
189 |
190 | return {
191 | success: true,
192 | message: 'Clipboard cleared',
193 | };
194 | } catch (error) {
195 | return {
196 | success: false,
197 | message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
198 | };
199 | }
200 | }
201 | }
202 |
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/keyboard.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { execSync } from 'child_process';
2 | import { writeFileSync, unlinkSync } from 'fs';
3 | import { tmpdir } from 'os';
4 | import { join } from 'path';
5 | import { KeyboardInput, KeyCombination, KeyHoldOperation } from '../../types/common.js';
6 | import { WindowsControlResponse } from '../../types/responses.js';
7 | import { KeyboardAutomation } from '../../interfaces/automation.js';
8 | import {
9 | MAX_TEXT_LENGTH,
10 | KeySchema,
11 | KeyCombinationSchema,
12 | KeyHoldOperationSchema,
13 | } from '../../tools/validation.zod.js';
14 | import { getAutoHotkeyPath } from './utils.js';
15 |
16 | /**
17 | * AutoHotkey implementation of the KeyboardAutomation interface
18 | */
19 | export class AutoHotkeyKeyboardAutomation implements KeyboardAutomation {
20 | /**
21 | * Execute an AutoHotkey script
22 | */
23 | private executeScript(script: string): void {
24 | const scriptPath = join(tmpdir(), `mcp-ahk-${Date.now()}.ahk`);
25 |
26 | try {
27 | // Write the script to a temporary file
28 | writeFileSync(scriptPath, script, 'utf8');
29 |
30 | // Execute the script with AutoHotkey v2
31 | const autohotkeyPath = getAutoHotkeyPath();
32 | execSync(`"${autohotkeyPath}" "${scriptPath}"`, { stdio: 'pipe' });
33 | } finally {
34 | // Clean up the temporary script file
35 | try {
36 | unlinkSync(scriptPath);
37 | } catch {
38 | // Ignore cleanup errors
39 | }
40 | }
41 | }
42 |
43 | /**
44 | * Convert key name to AutoHotkey format
45 | */
46 | private formatKey(key: string): string {
47 | const keyMap: Record<string, string> = {
48 | control: 'Ctrl',
49 | ctrl: 'Ctrl',
50 | shift: 'Shift',
51 | alt: 'Alt',
52 | meta: 'LWin',
53 | windows: 'LWin',
54 | enter: 'Enter',
55 | return: 'Enter',
56 | escape: 'Escape',
57 | esc: 'Escape',
58 | backspace: 'Backspace',
59 | delete: 'Delete',
60 | tab: 'Tab',
61 | space: 'Space',
62 | up: 'Up',
63 | down: 'Down',
64 | left: 'Left',
65 | right: 'Right',
66 | home: 'Home',
67 | end: 'End',
68 | pageup: 'PgUp',
69 | pagedown: 'PgDn',
70 | f1: 'F1',
71 | f2: 'F2',
72 | f3: 'F3',
73 | f4: 'F4',
74 | f5: 'F5',
75 | f6: 'F6',
76 | f7: 'F7',
77 | f8: 'F8',
78 | f9: 'F9',
79 | f10: 'F10',
80 | f11: 'F11',
81 | f12: 'F12',
82 | };
83 |
84 | const lowerKey = key.toLowerCase();
85 | return keyMap[lowerKey] || key;
86 | }
87 |
88 | typeText(input: KeyboardInput): WindowsControlResponse {
89 | try {
90 | // Validate text
91 | if (!input.text) {
92 | throw new Error('Text is required');
93 | }
94 |
95 | if (input.text.length > MAX_TEXT_LENGTH) {
96 | throw new Error(`Text too long: ${input.text.length} characters (max ${MAX_TEXT_LENGTH})`);
97 | }
98 |
99 | // Escape special characters for AutoHotkey
100 | const escapedText = input.text
101 | .replace(/\\/g, '\\\\')
102 | .replace(/"/g, '\\"')
103 | .replace(/`/g, '``')
104 | .replace(/{/g, '{{')
105 | .replace(/}/g, '}}');
106 |
107 | const script = `
108 | SendText("${escapedText}")
109 | ExitApp
110 | `;
111 |
112 | this.executeScript(script);
113 |
114 | return {
115 | success: true,
116 | message: `Typed text successfully`,
117 | };
118 | } catch (error) {
119 | return {
120 | success: false,
121 | message: `Failed to type text: ${error instanceof Error ? error.message : String(error)}`,
122 | };
123 | }
124 | }
125 |
126 | pressKey(key: string): WindowsControlResponse {
127 | try {
128 | // Validate key
129 | KeySchema.parse(key);
130 |
131 | const formattedKey = this.formatKey(key);
132 | const script = `
133 | Send("{${formattedKey}}")
134 | ExitApp
135 | `;
136 |
137 | this.executeScript(script);
138 |
139 | return {
140 | success: true,
141 | message: `Pressed key: ${key}`,
142 | };
143 | } catch (error) {
144 | return {
145 | success: false,
146 | message: `Failed to press key: ${error instanceof Error ? error.message : String(error)}`,
147 | };
148 | }
149 | }
150 |
151 | // eslint-disable-next-line @typescript-eslint/require-await
152 | async pressKeyCombination(combination: KeyCombination): Promise<WindowsControlResponse> {
153 | try {
154 | // Validate combination
155 | KeyCombinationSchema.parse(combination);
156 |
157 | // Build the key combination string
158 | const keys = combination.keys.map((key) => this.formatKey(key));
159 | const comboString = keys.join('+');
160 |
161 | const script = `
162 | Send("{${comboString}}")
163 | ExitApp
164 | `;
165 |
166 | this.executeScript(script);
167 |
168 | return {
169 | success: true,
170 | message: `Pressed key combination: ${combination.keys.join('+')}`,
171 | };
172 | } catch (error) {
173 | return {
174 | success: false,
175 | message: `Failed to press key combination: ${error instanceof Error ? error.message : String(error)}`,
176 | };
177 | }
178 | }
179 |
180 | // eslint-disable-next-line @typescript-eslint/require-await
181 | async holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse> {
182 | try {
183 | // Validate operation
184 | KeyHoldOperationSchema.parse(operation);
185 |
186 | const formattedKey = this.formatKey(operation.key);
187 | const script =
188 | operation.state === 'up'
189 | ? `
190 | Send("{${formattedKey} up}")
191 | ExitApp
192 | `
193 | : `
194 | Send("{${formattedKey} down}")
195 | ExitApp
196 | `;
197 |
198 | this.executeScript(script);
199 |
200 | return {
201 | success: true,
202 | message:
203 | operation.state === 'up'
204 | ? `Released key: ${operation.key}`
205 | : `Holding key: ${operation.key}`,
206 | };
207 | } catch (error) {
208 | return {
209 | success: false,
210 | message: `Failed to ${operation.state === 'up' ? 'release' : 'hold'} key: ${error instanceof Error ? error.message : String(error)}`,
211 | };
212 | }
213 | }
214 | }
215 |
```