This is page 1 of 3. Use http://codebase.md/cheffromspace/mcpcontrol?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:
--------------------------------------------------------------------------------
```
# Build output
build/
coverage/
# Dependencies
node_modules/
# Misc
.DS_Store
*.log
```
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
```
{
"src/**/*.ts": ["prettier --write", "npm run lint:fix"],
"test/**/*.js": ["prettier --write"]
}
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
libnut-core/
# Build output
build/
dist/
# Coverage
coverage/
# IDE
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS
.DS_Store
Thumbs.db
**/.claude/settings.local.json
# Test certificates
test/certs/
*.pem
*.crt
*.key
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/README.md:
--------------------------------------------------------------------------------
```markdown
# AutoHotkey Provider for MCPControl
This provider implements the MCPControl automation interfaces using AutoHotkey v2.
## Prerequisites
- AutoHotkey v2.0 or later must be installed on the system
- `AutoHotkey.exe` must be available in the system PATH
- Windows operating system (AutoHotkey is Windows-only)
## Installation
AutoHotkey can be downloaded from: https://www.autohotkey.com/
Make sure to install version 2.0 or later.
## Usage
### Using as the primary provider
```javascript
const provider = createAutomationProvider({ provider: 'autohotkey' });
```
### Using in modular configuration
```javascript
const provider = createAutomationProvider({
providers: {
keyboard: 'autohotkey',
mouse: 'autohotkey',
screen: 'autohotkey',
clipboard: 'autohotkey',
},
});
```
### Environment Variables
Set the automation provider to AutoHotkey:
```bash
export AUTOMATION_PROVIDER=autohotkey
```
Configure the AutoHotkey executable path (optional):
```bash
export AUTOHOTKEY_PATH="C:\Program Files\AutoHotkey\v2\AutoHotkey.exe"
```
Or use modular configuration:
```bash
export AUTOMATION_KEYBOARD_PROVIDER=autohotkey
export AUTOMATION_MOUSE_PROVIDER=autohotkey
export AUTOMATION_SCREEN_PROVIDER=autohotkey
export AUTOMATION_CLIPBOARD_PROVIDER=autohotkey
```
## Features
### Keyboard Automation
- Type text
- Press individual keys
- Press key combinations
- Hold and release keys
### Mouse Automation
- Move mouse to position
- Click mouse buttons
- Double-click
- Scroll
- Drag operations
- Get cursor position
### Screen Automation
- Get screen size
- Capture screenshots
- Get pixel colors
- Window management (focus, resize, reposition)
- Get active window information
### Clipboard Automation
- Set clipboard content
- Get clipboard content
- Check if clipboard has text
- Clear clipboard
## Implementation Notes
The AutoHotkey provider executes AutoHotkey v2 scripts for each operation. This means:
1. Each operation creates a temporary `.ahk` script file
2. The script is executed via `AutoHotkey.exe`
3. Results are captured through temporary files or script output
4. Temporary files are cleaned up after execution
## Performance Considerations
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.
## Error Handling
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.
## Known Limitations
1. Screenshot functionality is basic and uses Windows built-in tools (Paint, Snipping Tool)
2. Some operations may have timing issues due to the script execution model
3. Only works on Windows systems
4. Requires AutoHotkey v2 syntax (not compatible with v1)
## Debugging
To debug AutoHotkey scripts, you can:
1. Check the temporary script files generated in the system temp directory
2. Run the scripts manually with AutoHotkey to see any error messages
3. Enable AutoHotkey debugging features
## Contributing
When contributing to the AutoHotkey provider:
1. Ensure all scripts use AutoHotkey v2 syntax
2. Test on Windows with AutoHotkey v2 installed
3. Handle errors gracefully
4. Clean up temporary files properly
5. Follow the existing code structure and patterns
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCPControl
<p align="center">
<img src="https://github.com/user-attachments/assets/1c577e56-7b8d-49e9-aaf5-b8550cc6cfc0" alt="MCPControl Logo" width="250">
</p>
<p align="center">
<a href="https://github.com/Cheffromspace/MCPControl/releases/tag/v0.2.0">
<img src="https://img.shields.io/badge/release-v0.2.0-blue.svg" alt="Latest Release">
</a>
</p>
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.
> **Note**: This project currently supports Windows only.
## 🔥 Why MCPControl?
MCPControl bridges the gap between AI models and your desktop, enabling secure, programmatic control of:
- 🖱️ **Mouse movements and clicks**
- ⌨️ **Keyboard input and shortcuts**
- 🪟 **Window management**
- 📸 **Screen capture and analysis**
- 📋 **Clipboard operations**
## 🔌 Quick Start
### Prerequisites
1. **Install Build Tools (including VC++ workload)**
```powershell
# Run as Administrator - may take a few minutes to complete
winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
```
2. **Install Python** (if not already installed)
```powershell
# Install Python (required for node-gyp)
winget install Python.Python.3.12
```
3. **Install Node.js**
```powershell
# Install latest LTS version
winget install OpenJS.NodeJS
```
### Installation
1. **Install MCPControl Package**
```powershell
npm install -g mcp-control
```
### Configuration
MCPControl works best in a **virtual machine at 1280x720 resolution** for optimal click accuracy.
Configure your Claude client to connect to MCPControl via SSE transport:
#### Option 1: Direct SSE Connection
For connecting to an MCPControl server running on a VM or remote machine:
```json
{
"mcpServers": {
"MCPControl": {
"transport": "sse",
"url": "http://192.168.1.100:3232/mcp"
}
}
}
```
Replace `192.168.1.100:3232` with your server's IP address and port.
#### Option 2: Local Launch with SSE
To launch MCPControl locally with SSE transport:
```json
{
"mcpServers": {
"MCPControl": {
"command": "mcp-control",
"args": ["--sse"]
}
}
}
```
### Starting the Server
First, start the MCPControl server on your VM or local machine:
```bash
mcp-control --sse
```
The server will display:
- Available network interfaces and their IP addresses
- The port number (default: 3232)
- Connection status messages
### VM Setup Example
1. **Start your Windows VM** with 1280x720 resolution
2. **Install MCPControl** on the VM:
```bash
npm install -g mcp-control
```
3. **Run the server** with SSE transport:
```bash
mcp-control --sse
```
4. **Note the VM's IP address** (e.g., `192.168.1.100`)
5. **Configure Claude** with the SSE URL:
```json
{
"mcpServers": {
"MCPControl": {
"transport": "sse",
"url": "http://192.168.1.100:3232/mcp"
}
}
}
```
6. **Restart Claude** and MCPControl will appear in your MCP menu!
## 🔧 CLI Options
MCPControl supports several command-line flags for advanced configurations:
```bash
# Run with SSE transport on default port (3232)
mcp-control --sse
# Run with SSE on custom port
mcp-control --sse --port 3000
# Run with HTTPS/TLS (required for production deployments)
mcp-control --sse --https --cert /path/to/cert.pem --key /path/to/key.pem
# Run with HTTPS on custom port
mcp-control --sse --https --port 8443 --cert /path/to/cert.pem --key /path/to/key.pem
```
### Command Line Arguments
- `--sse` - Enable SSE (Server-Sent Events) transport for network access
- `--port [number]` - Specify custom port (default: 3232)
- `--https` - Enable HTTPS/TLS (required for remote deployments per MCP spec)
- `--cert [path]` - Path to TLS certificate file (required with --https)
- `--key [path]` - Path to TLS private key file (required with --https)
### Security Note
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.
## 🚀 Popular Use Cases
### Assisted Automation
- **Application Testing**: Delegate repetitive UI testing to Claude, allowing AI to navigate through applications and report issues
- **Workflow Automation**: Have Claude operate applications on your behalf, handling repetitive tasks while you focus on creative work
- **Form Filling**: Let Claude handle data entry tasks with your supervision
### AI Experimentation
- **AI Gaming**: Watch Claude learn to play simple games through visual feedback
- **Visual Reasoning**: Test Claude's ability to navigate visual interfaces and solve visual puzzles
- **Human-AI Collaboration**: Explore new interaction paradigms where Claude can see your screen and help with complex tasks
### Development and Testing
- **Cross-Application Integration**: Bridge applications that don't normally communicate
- **UI Testing Framework**: Create robust testing scenarios with visual validation
- **Demo Creation**: Automate the creation of product demonstrations
## ⚠️ IMPORTANT DISCLAIMER
**THIS SOFTWARE IS EXPERIMENTAL AND POTENTIALLY DANGEROUS**
By using this software, you acknowledge and accept that:
- Giving AI models direct control over your computer through this tool is inherently risky
- This software can control your mouse, keyboard, and other system functions which could potentially cause unintended consequences
- You are using this software entirely at your own risk
- 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
- This tool should only be used in controlled environments with appropriate safety measures in place
**USE AT YOUR OWN RISK**
## 🌟 Features
<table>
<tr>
<td>
<h3>🪟 Window Management</h3>
<ul>
<li>List all windows</li>
<li>Get active window info</li>
<li>Focus, resize & reposition</li>
</ul>
</td>
<td>
<h3>🖱️ Mouse Control</h3>
<ul>
<li>Precision movement</li>
<li>Click & drag operations</li>
<li>Scrolling & position tracking</li>
</ul>
</td>
</tr>
<tr>
<td>
<h3>⌨️ Keyboard Control</h3>
<ul>
<li>Text input & key combos</li>
<li>Key press/release control</li>
<li>Hold key functionality</li>
</ul>
</td>
<td>
<h3>📸 Screen Operations</h3>
<ul>
<li>High-quality screenshots</li>
<li>Screen size detection</li>
<li>Active window capture</li>
</ul>
</td>
</tr>
</table>
## 🔧 Automation Providers
MCPControl supports multiple automation providers for different use cases:
- **keysender** (default) - Native Windows automation with high reliability
- **powershell** - Windows PowerShell-based automation for simpler operations
- **autohotkey** - AutoHotkey v2 scripting for advanced automation needs
### Provider Configuration
You can configure the automation provider using environment variables:
```bash
# Use a specific provider for all operations
export AUTOMATION_PROVIDER=autohotkey
# Configure AutoHotkey executable path (if not in PATH)
export AUTOHOTKEY_PATH="C:\Program Files\AutoHotkey\v2\AutoHotkey.exe"
```
Or use modular configuration for specific operations:
```bash
# Mix and match providers for different operations
export AUTOMATION_KEYBOARD_PROVIDER=autohotkey
export AUTOMATION_MOUSE_PROVIDER=keysender
export AUTOMATION_SCREEN_PROVIDER=keysender
export AUTOMATION_CLIPBOARD_PROVIDER=powershell
```
See provider-specific documentation:
- [AutoHotkey Provider](src/providers/autohotkey/README.md)
## 🛠️ Development Setup
If you're interested in contributing or building from source, please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions.
### Development Requirements
To build this project for development, you'll need:
1. Windows operating system (required for the keysender dependency)
2. Node.js 18 or later (install using the official Windows installer which includes build tools)
3. npm package manager
4. Native build tools:
- node-gyp: `npm install -g node-gyp`
- cmake-js: `npm install -g cmake-js`
The keysender dependency relies on Windows-specific native modules that require these build tools.
## 📋 Project Structure
- `/src`
- `/handlers` - Request handlers and tool management
- `/tools` - Core functionality implementations
- `/types` - TypeScript type definitions
- `index.ts` - Main application entry point
## 🔖 Repository Branches
- **`main`** - Main development branch with the latest features and changes
- **`release`** - Stable release branch that mirrors the latest stable tag (currently v0.2.0)
### Version Installation
You can install specific versions of MCPControl using npm:
```bash
# Install the latest stable release (from release branch)
npm install mcp-control
# Install a specific version
npm install [email protected]
```
## 📚 Dependencies
- [@modelcontextprotocol/sdk](https://www.npmjs.com/package/@modelcontextprotocol/sdk) - MCP SDK for protocol implementation
- [keysender](https://www.npmjs.com/package/keysender) - Windows-only UI automation library
- [clipboardy](https://www.npmjs.com/package/clipboardy) - Clipboard handling
- [sharp](https://www.npmjs.com/package/sharp) - Image processing
- [uuid](https://www.npmjs.com/package/uuid) - UUID generation
## 🚧 Known Limitations
- Window minimize/restore operations are currently unsupported
- Multiple screen functions may not work as expected, depending on setup
- The get_screenshot utility does not work with the VS Code Extension Cline. See [GitHub issue #1865](https://github.com/cline/cline/issues/1865)
- Some operations may require elevated permissions depending on the target application
- Only Windows is supported
- 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
## 👥 Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
## ⚖️ License
This project is licensed under the MIT License - see the LICENSE file for details.
## 📖 References
- [Model Context Protocol Documentation](https://modelcontextprotocol.github.io/)
[](https://mseep.ai/app/cheffromspace-mcpcontrol)
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
# MCPControl - Development Guide
## Build & Test Commands
- Build: `pwsh.exe -c "npm run build"` - Compiles TypeScript to JavaScript
- Lint: `pwsh.exe -c "npm run lint"` - Runs ESLint to check code quality (TS and JS)
- Format: `pwsh.exe -c "npm run format"` - Runs Prettier to format code
- Format Check: `pwsh.exe -c "npm run format:check"` - Checks if files are properly formatted
- Test: `pwsh.exe -c "npm run test"` - Runs all Vitest tests
- Run single test: `pwsh.exe -c "npm run test -- tools/keyboard.test.ts"` or `pwsh.exe -c "npm run test -- -t \"specific test name\""`
- Watch tests: `pwsh.exe -c "npm run test:watch"` - Runs tests in watch mode
- Coverage: `pwsh.exe -c "npm run test:coverage"` - Generates test coverage report
- E2E Test: `cd test && ./e2e-test.sh [iterations]` - Runs end-to-end tests with Claude and MCPControl
## Running with HTTPS/TLS
MCPControl supports HTTPS for secure SSE connections (mandatory per MCP spec for production):
- `node build/index.js --sse --https --cert /path/to/cert.pem --key /path/to/key.pem`
- Default HTTPS port is still 3232 (use --port to change)
- Both --cert and --key are required when using --https
> Note: MCP Servers are typically launched by the Client as a subprocess.
## Code Style Guidelines
- **Imports**: Use ES module syntax with named imports
- **Types**: Define TypeScript interfaces for inputs/outputs in `types/` directory
- **Error Handling**: Use try/catch with standardized response objects
- **Naming**: camelCase for variables/functions, PascalCase for interfaces
- **Functions**: Keep functions small and focused on single responsibility
- **Comments**: Add JSDoc comments for public APIs
- **Testing**:
- Unit tests: Place in same directory as implementation with `.test.ts` suffix
- E2E tests: Added to the `test/` directory
- **Formatting**: Code is formatted using Prettier (pre-commit hooks will run automatically)
- **Error Responses**: Return `{ success: false, message: string }` for errors
- **Success Responses**: Return `{ success: true, data?: any }` for success
- **Linting**: Both TypeScript and JavaScript files are linted with ESLint
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
# Contributing to MCP Control
Thank you for your interest in contributing to MCP Control! This document provides guidelines and instructions for contributing to the project.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [Development Workflow](#development-workflow)
- [Branching Strategy](#branching-strategy)
- [Commit Guidelines](#commit-guidelines)
- [Pull Requests](#pull-requests)
- [Code Style and Standards](#code-style-and-standards)
- [Testing](#testing)
- [Documentation](#documentation)
- [Project Structure](#project-structure)
- [Issue Tracking](#issue-tracking)
- [Future Roadmap](#future-roadmap)
## Code of Conduct
Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community.
## Getting Started
### Prerequisites
- Node.js (latest LTS version recommended)
- npm
- git
- C++ compiler (for building native modules)
### Setup
1. Fork the repository
2. Clone your fork:
```bash
git clone https://github.com/YOUR-USERNAME/MCPControl.git
cd MCPControl
```
3. Build the project:
```bash
# Install dependencies
npm install
# Build the project
npm run build
```
## Development Workflow
### Branching Strategy
- `main` branch contains the latest stable code
- Create feature branches from `main` using the naming convention:
- `feature/feature-name` for new features
- `bugfix/issue-description` for bug fixes
- `docs/description` for documentation changes
- `refactor/description` for code refactoring
### Commit Guidelines
- Write clear, descriptive commit messages
- Reference issue numbers in commit messages when applicable
- Keep commits focused on a single logical change
### Pull Requests
1. Create your feature branch: `git checkout -b feature/amazing-feature`
2. Commit your changes: `git commit -m 'Add some amazing feature'`
3. Push to the branch: `git push origin feature/amazing-feature`
4. Open a Pull Request
5. Ensure all tests pass and code meets the project standards
6. Request a review from a maintainer
## Code Style and Standards
- Use ES module syntax with named imports
- Define TypeScript interfaces for inputs/outputs in the `types/` directory
- Use try/catch with standardized response objects for error handling
- Follow naming conventions:
- camelCase for variables/functions
- PascalCase for interfaces
- Keep functions small and focused on single responsibility
- Add JSDoc comments for public APIs
- Use 2-space indentation and semicolons
- For errors, return `{ success: false, message: string }`
- For success, return `{ success: true, data?: any }`
## Testing
- Place tests in the same directory as implementation with `.test.ts` suffix
- Run tests with `npm run test`
- Generate coverage report with `npm run test:coverage`
- Run a single test with `npm run test -- tools/keyboard.test.ts` or `npm run test -- -t "specific test name"`
- Run tests in watch mode with `npm run test:watch`
All new features should include appropriate test coverage. The project uses Vitest for testing.
## Documentation
- Document public APIs with JSDoc comments
- Update README.md when adding new features or changing functionality
- Keep code comments clear and focused on explaining "why" rather than "what"
## Project Structure
- `/src`
- `/handlers` - Request handlers and tool management
- `/tools` - Core functionality implementations
- `/types` - TypeScript type definitions
- `index.ts` - Main application entry point
## Issue Tracking
Check the [GitHub issues](https://github.com/Cheffromspace/MCPControl/issues) for existing issues you might want to contribute to. Current focus areas include:
1. Creating an npm package for easy installation
2. Adding remote computer control support
3. Building a dedicated test application
When creating a new issue:
- Use descriptive titles
- Include steps to reproduce for bugs
- For feature requests, explain the use case and potential implementation approach
## Future Roadmap
Current roadmap and planned features include:
- Fixing click accuracy issues with different resolutions and multi-screen setups
- Security implementation improvements
- Comprehensive testing
- Error handling enhancements
- Performance optimization
- Automation framework
- Enhanced window management
- Advanced integration features
## Publishing
This project uses GitHub Actions to automatically publish to npm when a version tag is pushed to main:
1. Ensure changes are merged to main
2. Create and push a tag with the version number:
```bash
git tag v1.2.3
git push origin v1.2.3
```
3. The GitHub Action will automatically:
- Build and test the package
- Update the version in package.json
- Publish to npm
Note: You need to have the `NPM_TOKEN` secret configured in the GitHub repository settings.
---
Thank you for contributing to MCP Control!
```
--------------------------------------------------------------------------------
/test/server-port.txt:
--------------------------------------------------------------------------------
```
8080
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
ko_fi: Cheffromspace
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Utility functions for AutoHotkey provider
*/
/**
* Get the path to AutoHotkey executable
* Can be configured via AUTOHOTKEY_PATH environment variable
*/
export function getAutoHotkeyPath(): string {
return process.env.AUTOHOTKEY_PATH || 'AutoHotkey.exe';
}
```
--------------------------------------------------------------------------------
/.claude/settings.local.json:
--------------------------------------------------------------------------------
```json
{
"permissions": {
"allow": [
"Bash(gh repo view:*)",
"Bash(gh repo view:*)",
"Bash(gh label create:*)",
"Bash(gh label create:*)",
"Bash(gh issue create:*)",
"Bash(gh issue create:*)",
"Bash(mkdir:*)"
],
"deny": []
}
}
```
--------------------------------------------------------------------------------
/src/interfaces/provider.ts:
--------------------------------------------------------------------------------
```typescript
import {
KeyboardAutomation,
MouseAutomation,
ScreenAutomation,
ClipboardAutomation,
} from './automation.js';
export interface AutomationProvider {
keyboard: KeyboardAutomation;
mouse: MouseAutomation;
screen: ScreenAutomation;
clipboard: ClipboardAutomation;
}
```
--------------------------------------------------------------------------------
/src/types/transport.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Enumeration of supported transport types for MCP server communication
*/
export enum TransportType {
/**
* HTTP transport for standard request-response communication
*/
HTTP = 'HTTP',
/**
* Server-Sent Events transport for real-time unidirectional event streaming
*/
SSE = 'SSE',
}
```
--------------------------------------------------------------------------------
/mcpcontrol-wrapper.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# mcpcontrol-wrapper.sh - Bridge script for Claude CLI to run MCPControl from WSL
# Windows path translation
WIN_PATH=$(echo "$PWD" | sed -E 's|^/mnt/([a-z])(/.*)|\1:\\\2|g' | sed 's|/|\\|g')
# Run PowerShell.exe with absolute path to ensure it's found
exec /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -c "cd '$WIN_PATH'; npm run build; node build/index.js"
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"noImplicitAny": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}
```
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: [
'node_modules/**',
'build/**',
'**/*.d.ts',
'vitest.config.ts'
]
},
include: ['src/**/*.test.ts'],
exclude: ['node_modules', 'build']
}
})
```
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
```yaml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
versioning-strategy: increase
groups:
dev-dependencies:
patterns:
- "@types/*"
- "vitest"
- "@vitest/*"
production-dependencies:
patterns:
- "@modelcontextprotocol/*"
- "@nut-tree/*"
- "sharp"
- "jimp"
commit-message:
prefix: "chore"
include: "scope"
```
--------------------------------------------------------------------------------
/test/test-results.json:
--------------------------------------------------------------------------------
```json
{
"buttonClicks": [
{
"timestamp": "2025-04-26T21:31:57.589Z",
"buttonId": "A",
"count": 1
},
{
"timestamp": "2025-04-26T23:50:04.745Z",
"buttonId": "8",
"count": 1
}
],
"sequences": [
{
"timestamp": "2025-04-26T21:31:57.591Z",
"sequence": "A"
},
{
"timestamp": "2025-04-26T23:50:04.747Z",
"sequence": "A8"
}
],
"finalSequence": "A8",
"startTime": "2025-04-26T21:31:42.896Z",
"endTime": "2025-04-26T23:50:04.747Z"
}
```
--------------------------------------------------------------------------------
/src/handlers/tools.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { setupTools as setupToolsWithZod } from './tools.zod.js';
import { AutomationProvider } from '../interfaces/provider.js';
/**
* Set up automation tools on the MCP server using Zod validation.
* This function provides robust validation with better error messages.
*
* @param server The Model Context Protocol server instance
* @param provider The automation provider implementation
*/
export function setupTools(server: Server, provider: AutomationProvider): void {
setupToolsWithZod(server, provider);
}
```
--------------------------------------------------------------------------------
/src/types/responses.ts:
--------------------------------------------------------------------------------
```typescript
interface ImageContent {
type: 'image';
data: Buffer | string; // Buffer for binary data, string for base64
mimeType: string;
encoding?: 'binary' | 'base64'; // Specify the encoding type
}
export interface ScreenshotResponse {
screenshot: Buffer | string; // Buffer for binary data, string for base64
timestamp: string;
encoding: 'binary' | 'base64';
}
export interface WindowsControlResponse {
success: boolean;
message: string;
data?: unknown;
screenshot?: Buffer | string; // Buffer for binary data, string for base64
content?: ImageContent[]; // MCP image content for screenshots
encoding?: 'binary' | 'base64'; // Specify the encoding type
}
```
--------------------------------------------------------------------------------
/test-autohotkey-direct.js:
--------------------------------------------------------------------------------
```javascript
// Direct test of AutoHotkey provider without factory
import { AutoHotkeyProvider } from './build/providers/autohotkey/index.js';
// Create the provider directly
const provider = new AutoHotkeyProvider();
console.log('AutoHotkey provider created successfully');
console.log('Provider has keyboard:', !!provider.keyboard);
console.log('Provider has mouse:', !!provider.mouse);
console.log('Provider has screen:', !!provider.screen);
console.log('Provider has clipboard:', !!provider.clipboard);
// Test a simple keyboard operation
console.log('\nTesting keyboard.typeText method...');
const result = provider.keyboard.typeText({ text: 'Hello from AutoHotkey!' });
console.log('Result:', result);
console.log('\nAutoHotkey provider is ready to use!');
```
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
```yaml
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '30 1 * * 0'
jobs:
analyze:
name: Analyze
runs-on: windows-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'typescript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
```
--------------------------------------------------------------------------------
/test-autohotkey.js:
--------------------------------------------------------------------------------
```javascript
// Simple test script to verify AutoHotkey provider works
import { createAutomationProvider } from './build/providers/factory.js';
// Use AutoHotkey as the provider
const provider = createAutomationProvider({ provider: 'autohotkey' });
console.log('AutoHotkey provider created successfully');
console.log('Provider has keyboard:', !!provider.keyboard);
console.log('Provider has mouse:', !!provider.mouse);
console.log('Provider has screen:', !!provider.screen);
console.log('Provider has clipboard:', !!provider.clipboard);
// You can also use modular configuration
const modularProvider = createAutomationProvider({
providers: {
keyboard: 'autohotkey',
mouse: 'autohotkey',
screen: 'autohotkey',
clipboard: 'autohotkey',
},
});
console.log('\nModular provider created successfully');
```
--------------------------------------------------------------------------------
/scripts/generate-test-certs.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Generate self-signed certificates for testing HTTPS support
# NOT FOR PRODUCTION USE
echo "Generating self-signed certificates for testing..."
# Create certs directory if it doesn't exist
mkdir -p test/certs
# Generate private key
openssl genrsa -out test/certs/key.pem 2048
# Generate certificate signing request
openssl req -new -key test/certs/key.pem -out test/certs/csr.pem \
-subj "/C=US/ST=Test/L=Test/O=MCPControl/OU=Test/CN=localhost"
# Generate self-signed certificate
openssl x509 -req -days 365 -in test/certs/csr.pem \
-signkey test/certs/key.pem -out test/certs/cert.pem
# Clean up CSR
rm test/certs/csr.pem
echo "Test certificates generated successfully!"
echo "Certificate: test/certs/cert.pem"
echo "Private key: test/certs/key.pem"
echo ""
echo "To test HTTPS support, run:"
echo "node build/index.js --sse --https --cert test/certs/cert.pem --key test/certs/key.pem"
```
--------------------------------------------------------------------------------
/src/tools/screenshot.ts:
--------------------------------------------------------------------------------
```typescript
import { createAutomationProvider } from '../providers/factory.js';
import { ScreenshotOptions } from '../types/common.js';
import { WindowsControlResponse } from '../types/responses.js';
/**
* Captures a screenshot with various options for optimization
*
* @param options Optional configuration for the screenshot
* @returns Promise resolving to a WindowsControlResponse with the screenshot data
*/
export async function getScreenshot(options?: ScreenshotOptions): Promise<WindowsControlResponse> {
try {
// Create a provider instance to handle the screenshot
const provider = createAutomationProvider();
// Delegate to the provider's screenshot implementation
const result = await provider.screen.getScreenshot(options);
// Return the result directly from the provider
return result;
} catch (error) {
return {
success: false,
message: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/index.ts:
--------------------------------------------------------------------------------
```typescript
import { AutomationProvider } from '../../interfaces/provider.js';
import {
KeyboardAutomation,
MouseAutomation,
ScreenAutomation,
ClipboardAutomation,
} from '../../interfaces/automation.js';
import { AutoHotkeyKeyboardAutomation } from './keyboard.js';
import { AutoHotkeyMouseAutomation } from './mouse.js';
import { AutoHotkeyScreenAutomation } from './screen.js';
import { AutoHotkeyClipboardAutomation } from './clipboard.js';
/**
* AutoHotkey implementation of the AutomationProvider
*
* NOTE: This provider requires AutoHotkey v2.0+ to be installed on the system.
* It executes AutoHotkey scripts to perform automation tasks.
*/
export class AutoHotkeyProvider implements AutomationProvider {
keyboard: KeyboardAutomation;
mouse: MouseAutomation;
screen: ScreenAutomation;
clipboard: ClipboardAutomation;
constructor() {
this.keyboard = new AutoHotkeyKeyboardAutomation();
this.mouse = new AutoHotkeyMouseAutomation();
this.screen = new AutoHotkeyScreenAutomation();
this.clipboard = new AutoHotkeyClipboardAutomation();
}
}
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Configuration interface for automation settings
*/
export interface AutomationConfig {
/**
* Legacy: The provider to use for all automation
* Currently supported: 'keysender'
*/
provider?: string;
/**
* New: Modular provider configuration
* Allows mixing different providers for different components
*/
providers?: {
keyboard?: string;
mouse?: string;
screen?: string;
clipboard?: string;
};
}
/**
* Load configuration from environment variables
*/
export function loadConfig(): AutomationConfig {
// Check for new modular configuration
const keyboard = process.env.AUTOMATION_KEYBOARD_PROVIDER;
const mouse = process.env.AUTOMATION_MOUSE_PROVIDER;
const screen = process.env.AUTOMATION_SCREEN_PROVIDER;
const clipboard = process.env.AUTOMATION_CLIPBOARD_PROVIDER;
if (keyboard || mouse || screen || clipboard) {
return {
providers: {
keyboard,
mouse,
screen,
clipboard,
},
};
}
// Fall back to legacy configuration
return {
provider: process.env.AUTOMATION_PROVIDER || 'keysender',
};
}
```
--------------------------------------------------------------------------------
/src/providers/factory.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi } from 'vitest';
import { createAutomationProvider } from './factory.js';
import { KeysenderProvider } from './keysender/index.js';
// Mock the providers
vi.mock('./keysender/index.js', () => {
return {
KeysenderProvider: vi.fn().mockImplementation(() => ({
keyboard: {},
mouse: {},
screen: {},
clipboard: {},
})),
};
});
describe('createAutomationProvider', () => {
it('should create KeysenderProvider by default', () => {
const provider = createAutomationProvider();
expect(KeysenderProvider).toHaveBeenCalled();
expect(provider).toBeDefined();
});
it('should create KeysenderProvider when explicitly specified', () => {
const provider = createAutomationProvider({ provider: 'keysender' });
expect(KeysenderProvider).toHaveBeenCalled();
expect(provider).toBeDefined();
});
it('should be case insensitive for KeysenderProvider', () => {
const provider = createAutomationProvider({ provider: 'KeYsEnDeR' });
expect(KeysenderProvider).toHaveBeenCalled();
expect(provider).toBeDefined();
});
it('should throw error for unknown provider type', () => {
expect(() => createAutomationProvider({ provider: 'unknown' })).toThrow(
'Unknown provider type: unknown',
);
});
});
```
--------------------------------------------------------------------------------
/src/providers/keysender/index.ts:
--------------------------------------------------------------------------------
```typescript
import { AutomationProvider } from '../../interfaces/provider.js';
import {
KeyboardAutomation,
MouseAutomation,
ScreenAutomation,
ClipboardAutomation,
} from '../../interfaces/automation.js';
import { KeysenderKeyboardAutomation } from './keyboard.js';
import { KeysenderMouseAutomation } from './mouse.js';
import { KeysenderScreenAutomation } from './screen.js';
import { KeysenderClipboardAutomation } from './clipboard.js';
/**
* Keysender implementation of the AutomationProvider
*
* NOTE: This provider requires the Windows operating system to compile native dependencies.
* Building this module on non-Windows platforms will fail.
* Development requires:
* - Node.js installed via the official Windows installer (includes necessary build tools)
* - node-gyp installed globally (npm install -g node-gyp)
* - cmake-js installed globally (npm install -g cmake-js)
*/
export class KeysenderProvider implements AutomationProvider {
keyboard: KeyboardAutomation;
mouse: MouseAutomation;
screen: ScreenAutomation;
clipboard: ClipboardAutomation;
constructor() {
this.keyboard = new KeysenderKeyboardAutomation();
this.mouse = new KeysenderMouseAutomation();
this.screen = new KeysenderScreenAutomation();
this.clipboard = new KeysenderClipboardAutomation();
}
}
```
--------------------------------------------------------------------------------
/src/tools/mouse.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { clickAt } from './mouse.js';
// Mock the provider
vi.mock('../providers/factory.js', () => ({
createAutomationProvider: () => ({
mouse: {
clickAt: vi.fn().mockImplementation((x, y, button) => ({
success: true,
message: `Clicked ${button} button at position (${x}, ${y})`,
})),
getCursorPosition: vi.fn().mockReturnValue({
success: true,
message: 'Current cursor position',
data: { x: 10, y: 20 },
}),
},
}),
}));
describe('Mouse Tools', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('clickAt', () => {
it('should click at the specified position', () => {
const result = clickAt(100, 200);
// Verify success response
expect(result.success).toBe(true);
expect(result.message).toContain('Clicked left button at position (100, 200)');
});
it('should support different mouse buttons', () => {
const result = clickAt(100, 200, 'right');
expect(result.success).toBe(true);
expect(result.message).toContain('Clicked right button');
});
it('should handle invalid coordinates', () => {
const result = clickAt(NaN, 200);
expect(result.success).toBe(false);
expect(result.message).toBe('Invalid coordinates provided');
});
});
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-control",
"version": "0.2.0",
"description": "Windows control server for the Model Context Protocol",
"license": "MIT",
"type": "module",
"main": "build/index.js",
"bin": "build/index.js",
"scripts": {
"build": "tsc",
"build:all": "node scripts/build.js",
"start": "node build/index.js",
"dev": "tsc -w",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write \"src/**/*.{ts,js}\" \"test/**/*.js\"",
"format:check": "prettier --check \"src/**/*.{ts,js}\" \"test/**/*.js\"",
"prepare": "husky"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"clipboardy": "^4.0.0",
"keysender": "^2.3.0",
"sharp": "^0.34.3",
"ulid": "^3.0.1",
"uuid": "^11.1.0",
"zod": "^3.25.1"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/express": "^5.0.2",
"@types/node": "^22.15.19",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.36.0",
"@vitest/coverage-v8": "^3.1.3",
"audit-ci": "^7.1.0",
"eslint": "^9.31.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vitest": "^3.0.8"
}
}
```
--------------------------------------------------------------------------------
/src/tools/clipboard.ts:
--------------------------------------------------------------------------------
```typescript
import clipboardy from 'clipboardy';
import { ClipboardInput } from '../types/common.js';
export async function getClipboardContent(): Promise<{
success: boolean;
content?: string;
error?: string;
}> {
try {
const content = await clipboardy.read();
return {
success: true,
content,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
export async function setClipboardContent(
input: ClipboardInput,
): Promise<{ success: boolean; error?: string }> {
try {
await clipboardy.write(input.text);
return {
success: true,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
export async function hasClipboardText(): Promise<{
success: boolean;
hasText?: boolean;
error?: string;
}> {
try {
const content = await clipboardy.read();
return {
success: true,
hasText: content.length > 0,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
export async function clearClipboard(): Promise<{ success: boolean; error?: string }> {
try {
await clipboardy.write('');
return {
success: true,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
// Create a simplified configuration that only lints src TypeScript files
export default tseslint.config(
{
ignores: [
'build/**',
'coverage/**',
'*.html',
'mcpcontrol-wrapper.sh',
'eslint.config.js',
'.github/**',
'scripts/**',
'test/**'
]
},
{
files: ['src/**/*.ts'],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked
],
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
}
},
{
// Test files specific configuration
files: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/unbound-method': 'off',
},
}
);
```
--------------------------------------------------------------------------------
/src/interfaces/automation.ts:
--------------------------------------------------------------------------------
```typescript
import {
MousePosition,
KeyboardInput,
KeyCombination,
KeyHoldOperation,
ScreenshotOptions,
ClipboardInput,
} from '../types/common.js';
import { WindowsControlResponse } from '../types/responses.js';
export interface KeyboardAutomation {
typeText(input: KeyboardInput): WindowsControlResponse;
pressKey(key: string): WindowsControlResponse;
pressKeyCombination(combination: KeyCombination): Promise<WindowsControlResponse>;
holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse>;
}
export interface MouseAutomation {
moveMouse(position: MousePosition): WindowsControlResponse;
clickMouse(button?: 'left' | 'right' | 'middle'): WindowsControlResponse;
doubleClick(position?: MousePosition): WindowsControlResponse;
getCursorPosition(): WindowsControlResponse;
scrollMouse(amount: number): WindowsControlResponse;
dragMouse(
from: MousePosition,
to: MousePosition,
button?: 'left' | 'right' | 'middle',
): WindowsControlResponse;
clickAt(x: number, y: number, button?: 'left' | 'right' | 'middle'): WindowsControlResponse;
}
export interface ScreenAutomation {
getScreenSize(): WindowsControlResponse;
getActiveWindow(): WindowsControlResponse;
focusWindow(title: string): WindowsControlResponse;
resizeWindow(title: string, width: number, height: number): Promise<WindowsControlResponse>;
repositionWindow(title: string, x: number, y: number): Promise<WindowsControlResponse>;
getScreenshot(options?: ScreenshotOptions): Promise<WindowsControlResponse>;
}
export interface ClipboardAutomation {
getClipboardContent(): Promise<WindowsControlResponse>;
setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse>;
hasClipboardText(): Promise<WindowsControlResponse>;
clearClipboard(): Promise<WindowsControlResponse>;
}
```
--------------------------------------------------------------------------------
/src/tools/screen.ts:
--------------------------------------------------------------------------------
```typescript
import { WindowsControlResponse } from '../types/responses.js';
import { createAutomationProvider } from '../providers/factory.js';
export function getScreenSize(): WindowsControlResponse {
try {
const provider = createAutomationProvider();
return provider.screen.getScreenSize();
} catch (error) {
return {
success: false,
message: `Failed to get screen size: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function getActiveWindow(): WindowsControlResponse {
try {
const provider = createAutomationProvider();
return provider.screen.getActiveWindow();
} catch (error) {
return {
success: false,
message: `Failed to get active window information: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function focusWindow(title: string): WindowsControlResponse {
try {
const provider = createAutomationProvider();
return provider.screen.focusWindow(title);
} catch (error) {
return {
success: false,
message: `Failed to focus window: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export async function resizeWindow(
title: string,
width: number,
height: number,
): Promise<WindowsControlResponse> {
try {
const provider = createAutomationProvider();
return await provider.screen.resizeWindow(title, width, height);
} catch (error) {
return {
success: false,
message: `Failed to resize window: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export async function repositionWindow(
title: string,
x: number,
y: number,
): Promise<WindowsControlResponse> {
try {
const provider = createAutomationProvider();
return await provider.screen.repositionWindow(title, x, y);
} catch (error) {
return {
success: false,
message: `Failed to reposition window: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
```
--------------------------------------------------------------------------------
/src/types/common.ts:
--------------------------------------------------------------------------------
```typescript
export interface MousePosition {
x: number;
y: number;
}
export interface KeyboardInput {
text: string;
}
export interface KeyCombination {
keys: string[]; // Array of keys to be pressed together, e.g. ["control", "c"]
}
export interface KeyHoldOperation {
key: string; // The key to hold
duration?: number; // Duration in milliseconds (optional when state is 'up')
state: 'down' | 'up'; // Whether to press down or release the key
}
export interface WindowInfo {
title: string;
position: {
x: number;
y: number;
};
size: {
width: number;
height: number;
};
}
export interface ClipboardInput {
text: string;
}
// Type for mouse button mapping
export type ButtonMap = {
[key: string]: string;
left: string;
right: string;
middle: string;
};
// New types for screen search functionality
export interface ImageSearchOptions {
confidence?: number; // Match confidence threshold (0-1)
searchRegion?: {
// Region to search within
x: number;
y: number;
width: number;
height: number;
};
waitTime?: number; // Max time to wait for image in ms
}
export interface ImageSearchResult {
location: {
x: number;
y: number;
};
confidence: number;
width: number;
height: number;
}
export interface HighlightOptions {
duration?: number; // Duration to show highlight in ms
color?: string; // Color of highlight (hex format)
}
// New interface for screenshot options
export interface ScreenshotOptions {
region?: {
x: number;
y: number;
width: number;
height: number;
};
quality?: number; // JPEG quality (1-100), only used if format is 'jpeg'
format?: 'png' | 'jpeg'; // Output format
grayscale?: boolean; // Convert to grayscale
resize?: {
// Resize options
width?: number; // Target width (maintains aspect ratio if only one dimension provided)
height?: number; // Target height
fit?: 'contain' | 'cover' | 'fill' | 'inside' | 'outside'; // Resize fit option
};
compressionLevel?: number; // PNG compression level (0-9), only used if format is 'png'
}
```
--------------------------------------------------------------------------------
/src/providers/clipboard/clipboardy/index.ts:
--------------------------------------------------------------------------------
```typescript
import clipboardy from 'clipboardy';
import { ClipboardInput } from '../../../types/common.js';
import { WindowsControlResponse } from '../../../types/responses.js';
import { ClipboardAutomation } from '../../../interfaces/automation.js';
/**
* Clipboardy implementation of the ClipboardAutomation interface
*
* Uses the clipboardy library for cross-platform clipboard operations
*/
export class ClipboardyProvider implements ClipboardAutomation {
async getClipboardContent(): Promise<WindowsControlResponse> {
try {
const content = await clipboardy.read();
return {
success: true,
message: 'Clipboard content retrieved',
data: content,
};
} catch (error) {
return {
success: false,
message: `Failed to get clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
try {
await clipboardy.write(input.text);
return {
success: true,
message: 'Clipboard content set',
};
} catch (error) {
return {
success: false,
message: `Failed to set clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async hasClipboardText(): Promise<WindowsControlResponse> {
try {
const content = await clipboardy.read();
const hasText = content.length > 0;
return {
success: true,
message: `Clipboard ${hasText ? 'has' : 'does not have'} text`,
data: hasText,
};
} catch (error) {
return {
success: false,
message: `Failed to check clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async clearClipboard(): Promise<WindowsControlResponse> {
try {
await clipboardy.write('');
return {
success: true,
message: 'Clipboard cleared',
};
} catch (error) {
return {
success: false,
message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
}
```
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
/**
* Build script for MCPControl
* Handles the complete build process
*/
// ANSI color codes for terminal output
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m',
cyan: '\x1b[36m'
};
/**
* Executes a shell command and pipes output to console
* @param {string} command - Command to execute
* @param {Object} options - Options for child_process.execSync
* @returns {Buffer} Command output
*/
function execute(command, options = {}) {
console.log(`${colors.cyan}> ${command}${colors.reset}`);
const defaultOptions = {
stdio: 'inherit',
...options
};
try {
return execSync(command, defaultOptions);
} catch (error) {
console.error(`${colors.red}Command failed: ${command}${colors.reset}`);
process.exit(1);
}
}
// Main build process
function build() {
console.log(`\n${colors.green}===== MCPControl Build Process =====${colors.reset}\n`);
// Install dependencies using npm ci for faster and deterministic installs
console.log(`\n${colors.blue}Installing dependencies...${colors.reset}`);
// Check if package-lock.json exists before running npm ci
if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) {
try {
// Use a different execution method for npm ci to allow falling back to npm install
console.log(`${colors.cyan}> npm ci${colors.reset}`);
execSync('npm ci', { stdio: 'inherit' });
} catch (error) {
console.warn(`\n${colors.yellow}Warning: npm ci failed, falling back to npm install${colors.reset}`);
execute('npm install');
}
} else {
console.log(`\n${colors.yellow}package-lock.json not found, using npm install instead${colors.reset}`);
execute('npm install');
}
// Build MCPControl
console.log(`\n${colors.blue}Building MCPControl...${colors.reset}`);
execute('npm run build');
console.log(`\n${colors.green}===== Build Complete =====${colors.reset}`);
console.log(`${colors.green}MCPControl has been successfully built!${colors.reset}\n`);
}
// Run the build process
build();
```
--------------------------------------------------------------------------------
/src/providers/keysender/clipboard.ts:
--------------------------------------------------------------------------------
```typescript
import clipboardy from 'clipboardy';
import { ClipboardInput } from '../../types/common.js';
import { WindowsControlResponse } from '../../types/responses.js';
import { ClipboardAutomation } from '../../interfaces/automation.js';
/**
* Keysender implementation of the ClipboardAutomation interface
*
* Note: Since keysender doesn't provide direct clipboard functionality,
* we use the clipboardy library (same as the NutJS implementation)
*/
export class KeysenderClipboardAutomation implements ClipboardAutomation {
async getClipboardContent(): Promise<WindowsControlResponse> {
try {
const content = await clipboardy.read();
return {
success: true,
message: 'Clipboard content retrieved',
data: content,
};
} catch (error) {
return {
success: false,
message: `Failed to get clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
try {
await clipboardy.write(input.text);
return {
success: true,
message: 'Clipboard content set',
};
} catch (error) {
return {
success: false,
message: `Failed to set clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async hasClipboardText(): Promise<WindowsControlResponse> {
try {
const content = await clipboardy.read();
const hasText = content.length > 0;
return {
success: true,
message: `Clipboard ${hasText ? 'has' : 'does not have'} text`,
data: hasText,
};
} catch (error) {
return {
success: false,
message: `Failed to check clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async clearClipboard(): Promise<WindowsControlResponse> {
try {
await clipboardy.write('');
return {
success: true,
message: 'Clipboard cleared',
};
} catch (error) {
return {
success: false,
message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
}
```
--------------------------------------------------------------------------------
/src/tools/keyboard.ts:
--------------------------------------------------------------------------------
```typescript
import { KeyboardInput, KeyCombination, KeyHoldOperation } from '../types/common.js';
import { WindowsControlResponse } from '../types/responses.js';
import { createAutomationProvider } from '../providers/factory.js';
import {
MAX_TEXT_LENGTH,
KeySchema,
KeyCombinationSchema,
KeyHoldOperationSchema,
} from './validation.zod.js';
// Get the automation provider
const provider = createAutomationProvider();
export function typeText(input: KeyboardInput): WindowsControlResponse {
try {
// Validate text length
if (!input.text) {
throw new Error('Text is required');
}
if (input.text.length > MAX_TEXT_LENGTH) {
throw new Error(`Text too long: ${input.text.length} characters (max ${MAX_TEXT_LENGTH})`);
}
return provider.keyboard.typeText(input);
} catch (error) {
return {
success: false,
message: `Failed to type text: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function pressKey(key: string): WindowsControlResponse {
try {
// Validate key using Zod schema
KeySchema.parse(key);
return provider.keyboard.pressKey(key);
} catch (error) {
return {
success: false,
message: `Failed to press key: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export async function pressKeyCombination(
combination: KeyCombination,
): Promise<WindowsControlResponse> {
try {
// Validate the key combination using Zod schema
KeyCombinationSchema.parse(combination);
return await provider.keyboard.pressKeyCombination(combination);
} catch (error) {
return {
success: false,
message: `Failed to press key combination: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export async function holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse> {
try {
// Validate key hold operation using Zod schema
KeyHoldOperationSchema.parse(operation);
return await provider.keyboard.holdKey(operation);
} catch (error) {
return {
success: false,
message: `Failed to ${operation.state} key ${operation.key}: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
```
--------------------------------------------------------------------------------
/src/types/keysender.d.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Type definitions for keysender module
*/
declare module 'keysender' {
interface ScreenSize {
width: number;
height: number;
}
interface WindowInfo {
title: string;
className: string;
handle: number;
}
interface ViewInfo {
x: number;
y: number;
width: number;
height: number;
}
interface CaptureResult {
data: Buffer;
width: number;
height: number;
}
interface MousePosition {
x: number;
y: number;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class Hardware {
constructor(windowHandle?: number);
keyboard: {
printText(text: string): Promise<void>;
sendKey(key: string): Promise<void>;
toggleKey(key: string | string[], down: boolean, delay?: Delay): Promise<void>;
};
mouse: {
moveTo(x: number, y: number): Promise<void>;
click(button?: string): Promise<void>;
toggle(button: string, down: boolean): Promise<void>;
getPos(): MousePosition;
scrollWheel(amount: number): Promise<void>;
};
workwindow: {
get(): WindowInfo;
set(handle: number): boolean;
getView(): ViewInfo;
setView(view: Partial<ViewInfo>): void;
setForeground(): void;
isForeground(): boolean;
isOpen(): boolean;
capture(
region?: { x: number; y: number; width: number; height: number },
format?: string,
): CaptureResult;
capture(format?: string): CaptureResult;
};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getScreenSize(): ScreenSize;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getAllWindows(): WindowInfo[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getWindowChildren(handle: number): WindowInfo[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const KeyboardButton: { [key: string]: string };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MouseButton: { [key: string]: string };
const keysender: {
Hardware: typeof Hardware;
KeyboardButton: typeof KeyboardButton;
MouseButton: typeof MouseButton;
getScreenSize: typeof getScreenSize;
getAllWindows: typeof getAllWindows;
getWindowChildren: typeof getWindowChildren;
};
export default keysender;
}
```
--------------------------------------------------------------------------------
/docs/providers.md:
--------------------------------------------------------------------------------
```markdown
# MCPControl Automation Providers
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.
## Available Providers
### Keysender Provider (Default)
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.
## Selecting a Provider
You can select which provider to use by setting the `AUTOMATION_PROVIDER` environment variable:
```bash
# Use the Keysender provider (default)
AUTOMATION_PROVIDER=keysender node build/index.js
```
### Screen Automation Considerations
The Keysender provider has the following considerations for screen automation:
- **Window Detection Challenges**: Getting accurate window information can be challenging, especially with:
- Window handles that may not always be valid
- Window titles that may be empty or not match expected values
- Position and size information that may be unavailable or return extreme negative values for minimized windows
- **Window Repositioning and Resizing**: Operations work but may not always report accurate results due to limitations in the underlying API
- **Window Focusing**: May not work reliably for all window types or applications
- **Screenshot Functionality**: May not work consistently in all environments
We've implemented significant fallbacks and robust error handling for window operations, including:
- Advanced window selection strategy that prioritizes common applications for better reliability
- Detailed logging to help diagnose window handling issues
- Fallback mechanisms when window operations don't produce the expected results
- Safe property access with type checking to handle edge cases
### Recent Improvements
Recent updates to the provider include:
- Added a sophisticated window finding algorithm that tries multiple strategies to locate usable windows
- Enhanced window resizing and repositioning with better error handling and result verification
- Improved window information retrieval with multiple fallback layers for missing data
- Better window focusing with proper foreground window management and status reporting
- More robust error handling throughout window operations with detailed logging
- Added support for child window detection and management
```
--------------------------------------------------------------------------------
/src/providers/registry.ts:
--------------------------------------------------------------------------------
```typescript
import {
KeyboardAutomation,
MouseAutomation,
ScreenAutomation,
ClipboardAutomation,
} from '../interfaces/automation.js';
export interface ProviderRegistry {
registerKeyboard(name: string, provider: KeyboardAutomation): void;
registerMouse(name: string, provider: MouseAutomation): void;
registerScreen(name: string, provider: ScreenAutomation): void;
registerClipboard(name: string, provider: ClipboardAutomation): void;
getKeyboard(name: string): KeyboardAutomation | undefined;
getMouse(name: string): MouseAutomation | undefined;
getScreen(name: string): ScreenAutomation | undefined;
getClipboard(name: string): ClipboardAutomation | undefined;
}
/**
* Central registry for automation providers
* Allows registration and retrieval of individual automation components
*/
export class DefaultProviderRegistry implements ProviderRegistry {
private keyboards = new Map<string, KeyboardAutomation>();
private mice = new Map<string, MouseAutomation>();
private screens = new Map<string, ScreenAutomation>();
private clipboards = new Map<string, ClipboardAutomation>();
registerKeyboard(name: string, provider: KeyboardAutomation): void {
this.keyboards.set(name, provider);
}
registerMouse(name: string, provider: MouseAutomation): void {
this.mice.set(name, provider);
}
registerScreen(name: string, provider: ScreenAutomation): void {
this.screens.set(name, provider);
}
registerClipboard(name: string, provider: ClipboardAutomation): void {
this.clipboards.set(name, provider);
}
getKeyboard(name: string): KeyboardAutomation | undefined {
return this.keyboards.get(name);
}
getMouse(name: string): MouseAutomation | undefined {
return this.mice.get(name);
}
getScreen(name: string): ScreenAutomation | undefined {
return this.screens.get(name);
}
getClipboard(name: string): ClipboardAutomation | undefined {
return this.clipboards.get(name);
}
/**
* Get a list of all registered provider names for each component type
*/
getAvailableProviders(): {
keyboards: string[];
mice: string[];
screens: string[];
clipboards: string[];
} {
return {
keyboards: Array.from(this.keyboards.keys()),
mice: Array.from(this.mice.keys()),
screens: Array.from(this.screens.keys()),
clipboards: Array.from(this.clipboards.keys()),
};
}
}
// Singleton instance
export const registry = new DefaultProviderRegistry();
```
--------------------------------------------------------------------------------
/scripts/test-screenshot.cjs:
--------------------------------------------------------------------------------
```
#!/usr/bin/env node
// Test script for the screenshot utility
// This script tests the optimized screenshot functionality to ensure:
// 1. No "Maximum call stack size exceeded" errors
// 2. Reasonable file sizes (not 20MB)
// Import the built version of the screenshot utility
const { getScreenshot } = require('../build/tools/screenshot.js');
async function testScreenshot() {
console.log('Testing screenshot utility with various settings...');
// Test 1: Default settings (should use 1280px width, JPEG format)
console.log('\nTest 1: Default settings (1280px width, JPEG)');
try {
const result1 = await getScreenshot();
if (result1.success) {
console.log('✅ Default screenshot successful');
} else {
console.error('❌ Default screenshot failed:', result1.message);
}
} catch (error) {
console.error('❌ Default screenshot threw exception:', error);
}
// Test 2: Small size (50x50px)
console.log('\nTest 2: Small size (50x50px)');
try {
const result2 = await getScreenshot({
resize: {
width: 50,
height: 50,
fit: 'fill'
}
});
if (result2.success) {
console.log('✅ Small screenshot successful');
} else {
console.error('❌ Small screenshot failed:', result2.message);
}
} catch (error) {
console.error('❌ Small screenshot threw exception:', error);
}
// Test 3: PNG format with high compression
console.log('\nTest 3: PNG format with high compression');
try {
const result3 = await getScreenshot({
format: 'png',
compressionLevel: 9,
resize: {
width: 800
}
});
if (result3.success) {
console.log('✅ PNG screenshot successful');
} else {
console.error('❌ PNG screenshot failed:', result3.message);
}
} catch (error) {
console.error('❌ PNG screenshot threw exception:', error);
}
// Test 4: Grayscale JPEG with low quality
console.log('\nTest 4: Grayscale JPEG with low quality');
try {
const result4 = await getScreenshot({
format: 'jpeg',
quality: 50,
grayscale: true
});
if (result4.success) {
console.log('✅ Grayscale screenshot successful');
} else {
console.error('❌ Grayscale screenshot failed:', result4.message);
}
} catch (error) {
console.error('❌ Grayscale screenshot threw exception:', error);
}
console.log('\nScreenshot testing complete');
}
// Run the tests
testScreenshot().catch(console.error);
```
--------------------------------------------------------------------------------
/scripts/test-screenshot.mjs:
--------------------------------------------------------------------------------
```
#!/usr/bin/env node
// Test script for the screenshot utility
// This script tests the optimized screenshot functionality to ensure:
// 1. No "Maximum call stack size exceeded" errors
// 2. Reasonable file sizes (not 20MB)
// Use dynamic import for ES modules
async function runTests() {
// Import the built version of the screenshot utility
const { getScreenshot } = await import('../build/tools/screenshot.js');
console.log('Testing screenshot utility with various settings...');
// Test 1: Default settings (should use 1280px width, JPEG format)
console.log('\nTest 1: Default settings (1280px width, JPEG)');
try {
const result1 = await getScreenshot();
if (result1.success) {
console.log('✅ Default screenshot successful');
} else {
console.error('❌ Default screenshot failed:', result1.message);
}
} catch (error) {
console.error('❌ Default screenshot threw exception:', error);
}
// Test 2: Small size (50x50px)
console.log('\nTest 2: Small size (50x50px)');
try {
const result2 = await getScreenshot({
resize: {
width: 50,
height: 50,
fit: 'fill'
}
});
if (result2.success) {
console.log('✅ Small screenshot successful');
} else {
console.error('❌ Small screenshot failed:', result2.message);
}
} catch (error) {
console.error('❌ Small screenshot threw exception:', error);
}
// Test 3: PNG format with high compression
console.log('\nTest 3: PNG format with high compression');
try {
const result3 = await getScreenshot({
format: 'png',
compressionLevel: 9,
resize: {
width: 800
}
});
if (result3.success) {
console.log('✅ PNG screenshot successful');
} else {
console.error('❌ PNG screenshot failed:', result3.message);
}
} catch (error) {
console.error('❌ PNG screenshot threw exception:', error);
}
// Test 4: Grayscale JPEG with low quality
console.log('\nTest 4: Grayscale JPEG with low quality');
try {
const result4 = await getScreenshot({
format: 'jpeg',
quality: 50,
grayscale: true
});
if (result4.success) {
console.log('✅ Grayscale screenshot successful');
} else {
console.error('❌ Grayscale screenshot failed:', result4.message);
}
} catch (error) {
console.error('❌ Grayscale screenshot threw exception:', error);
}
console.log('\nScreenshot testing complete');
}
// Run the tests
runTests().catch(console.error);
```
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish to NPM
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Dependencies required for native modules
- name: Install global dependencies
run: |
npm install -g node-gyp
npm install -g cmake-js
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test
- name: Build
run: npm run build:all
- name: Extract version
id: extract_version
run: echo "VERSION=$($env:GITHUB_REF -replace 'refs/tags/v', '')" >> $env:GITHUB_OUTPUT
shell: pwsh
- name: Update package version if needed
run: |
$current_version = $(node -p "require('./package.json').version")
$tag_version = "${{ steps.extract_version.outputs.VERSION }}"
if ($current_version -ne $tag_version) {
npm version $tag_version --no-git-tag-version
Write-Host "Updated version from $current_version to $tag_version"
} else {
Write-Host "Version already set to $current_version, skipping update"
}
shell: pwsh
- name: Verify package contents
run: |
# Just check what will be included in the package without actually extracting
$package = npm pack --dry-run
# List the build directory to verify it exists and has content
Write-Host "`nVerifying build directory contents:"
if (Test-Path -Path "./build") {
Get-ChildItem -Path "./build" -Recurse -Depth 1 | Select-Object FullName
} else {
Write-Error "Build directory not found!"
exit 1
}
- name: Publish to NPM
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Verify publish
run: npm view $(node -p "require('./package.json').name") version
if: success()
```
--------------------------------------------------------------------------------
/scripts/test-window.cjs:
--------------------------------------------------------------------------------
```
// Direct test script for window handling
// Use CommonJS require for keysender
const keysender = require('keysender');
const { Hardware } = keysender;
const getAllWindows = keysender.getAllWindows;
console.log("Testing keysender window handling directly");
// Get all windows
const allWindows = getAllWindows();
console.log("\nAll windows:");
allWindows.forEach(window => {
console.log(`- "${window.title}" (handle: ${window.handle}, class: ${window.className})`);
});
// Try to find Notepad
console.log("\nLooking for Notepad...");
const notepad = allWindows.find(w => w.title && w.title.includes('Notepad'));
if (notepad) {
console.log(`Found Notepad: "${notepad.title}" (handle: ${notepad.handle})`);
// Create hardware instance for Notepad
try {
const hw = new Hardware(notepad.handle);
console.log("Created Hardware instance for Notepad");
// Try to get window view
try {
const view = hw.workwindow.getView();
console.log("Notepad view:", view);
} catch (e) {
console.error("Error getting Notepad view:", e.message);
}
// Try to set as foreground
try {
hw.workwindow.setForeground();
console.log("Set Notepad as foreground window");
} catch (e) {
console.error("Error setting Notepad as foreground:", e.message);
}
// Try to resize
try {
hw.workwindow.setView({
x: 200,
y: 200,
width: 800,
height: 600
});
console.log("Resized Notepad to 800x600 at position (200, 200)");
// Get updated view
const updatedView = hw.workwindow.getView();
console.log("Updated Notepad view:", updatedView);
} catch (e) {
console.error("Error resizing Notepad:", e.message);
}
} catch (e) {
console.error("Error creating Hardware instance for Notepad:", e.message);
}
} else {
console.log("Notepad not found. Please make sure Notepad is running.");
}
// Try with default Hardware instance
console.log("\nTesting default Hardware instance:");
try {
const defaultHw = new Hardware();
console.log("Created default Hardware instance");
// Try to get current window
try {
const currentWindow = defaultHw.workwindow.get();
console.log("Current window:", currentWindow);
} catch (e) {
console.error("Error getting current window:", e.message);
}
// Try to get view
try {
const view = defaultHw.workwindow.getView();
console.log("Current view:", view);
} catch (e) {
console.error("Error getting current view:", e.message);
}
} catch (e) {
console.error("Error creating default Hardware instance:", e.message);
}
```
--------------------------------------------------------------------------------
/scripts/test-window.js:
--------------------------------------------------------------------------------
```javascript
// Direct test script for window handling
// Use CommonJS require for keysender
const keysender = require('keysender');
const { Hardware } = keysender;
const getAllWindows = keysender.getAllWindows;
console.log("Testing keysender window handling directly");
// Get all windows
const allWindows = getAllWindows();
console.log("\nAll windows:");
allWindows.forEach(window => {
console.log(`- "${window.title}" (handle: ${window.handle}, class: ${window.className})`);
});
// Try to find Notepad
console.log("\nLooking for Notepad...");
const notepad = allWindows.find(w => w.title && w.title.includes('Notepad'));
if (notepad) {
console.log(`Found Notepad: "${notepad.title}" (handle: ${notepad.handle})`);
// Create hardware instance for Notepad
try {
const hw = new Hardware(notepad.handle);
console.log("Created Hardware instance for Notepad");
// Try to get window view
try {
const view = hw.workwindow.getView();
console.log("Notepad view:", view);
} catch (e) {
console.error("Error getting Notepad view:", e.message);
}
// Try to set as foreground
try {
hw.workwindow.setForeground();
console.log("Set Notepad as foreground window");
} catch (e) {
console.error("Error setting Notepad as foreground:", e.message);
}
// Try to resize
try {
hw.workwindow.setView({
x: 200,
y: 200,
width: 800,
height: 600
});
console.log("Resized Notepad to 800x600 at position (200, 200)");
// Get updated view
const updatedView = hw.workwindow.getView();
console.log("Updated Notepad view:", updatedView);
} catch (e) {
console.error("Error resizing Notepad:", e.message);
}
} catch (e) {
console.error("Error creating Hardware instance for Notepad:", e.message);
}
} else {
console.log("Notepad not found. Please make sure Notepad is running.");
}
// Try with default Hardware instance
console.log("\nTesting default Hardware instance:");
try {
const defaultHw = new Hardware();
console.log("Created default Hardware instance");
// Try to get current window
try {
const currentWindow = defaultHw.workwindow.get();
console.log("Current window:", currentWindow);
} catch (e) {
console.error("Error getting current window:", e.message);
}
// Try to get view
try {
const view = defaultHw.workwindow.getView();
console.log("Current view:", view);
} catch (e) {
console.error("Error getting current view:", e.message);
}
} catch (e) {
console.error("Error creating default Hardware instance:", e.message);
}
```
--------------------------------------------------------------------------------
/scripts/test-provider.js:
--------------------------------------------------------------------------------
```javascript
// Simple script to test provider selection
import { loadConfig } from '../build/config.js';
import { createAutomationProvider } from '../build/providers/factory.js';
// Override the provider from command line argument if provided
if (process.argv.length > 2) {
process.env.AUTOMATION_PROVIDER = process.argv[2];
}
// Load configuration
const config = loadConfig();
console.log(`Using provider: ${config.provider}`);
// Create provider
const provider = createAutomationProvider(config.provider);
console.log(`Provider created: ${provider.constructor.name}`);
// Print provider details
console.log('\nProvider components:');
console.log(`- Keyboard: ${provider.keyboard.constructor.name}`);
console.log(`- Mouse: ${provider.mouse.constructor.name}`);
console.log(`- Screen: ${provider.screen.constructor.name}`);
console.log(`- Clipboard: ${provider.clipboard.constructor.name}`);
// Test window operations if requested
const testWindowOps = process.argv.includes('--test-window');
if (testWindowOps) {
console.log('\nTesting window operations:');
// Get screen size
const screenSizeResult = provider.screen.getScreenSize();
console.log(`\nScreen size: ${JSON.stringify(screenSizeResult.data)}`);
// Get active window
const activeWindowResult = provider.screen.getActiveWindow();
console.log(`\nActive window: ${JSON.stringify(activeWindowResult.data)}`);
// Test window focus
if (activeWindowResult.success && activeWindowResult.data?.title) {
const windowTitle = activeWindowResult.data.title;
console.log(`\nFocusing window: "${windowTitle}"`);
const focusResult = provider.screen.focusWindow(windowTitle);
console.log(`Focus result: ${focusResult.success ? 'Success' : 'Failed'} - ${focusResult.message}`);
// Test window resize
console.log(`\nResizing window: "${windowTitle}" to 800x600`);
const resizeResult = provider.screen.resizeWindow(windowTitle, 800, 600);
console.log(`Resize result: ${resizeResult.success ? 'Success' : 'Failed'} - ${resizeResult.message}`);
// Wait a bit to see the resize
console.log('Waiting 2 seconds...');
setTimeout(() => {
// Test window reposition
console.log(`\nRepositioning window: "${windowTitle}" to (100, 100)`);
const repositionResult = provider.screen.repositionWindow(windowTitle, 100, 100);
console.log(`Reposition result: ${repositionResult.success ? 'Success' : 'Failed'} - ${repositionResult.message}`);
// Get active window again to verify changes
setTimeout(() => {
const updatedWindowResult = provider.screen.getActiveWindow();
console.log(`\nUpdated window info: ${JSON.stringify(updatedWindowResult.data)}`);
}, 1000);
}, 2000);
}
}
```
--------------------------------------------------------------------------------
/src/server.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// We'll define the mocks before importing the module being tested
const mockMcpServer = {
connect: vi.fn(),
};
const mockApp = {
get: vi.fn(),
use: vi.fn(),
post: vi.fn(),
};
const mockHttpServer = {
listen: vi.fn((port, host, callback) => {
if (callback) callback();
return mockHttpServer;
}),
close: vi.fn(),
on: vi.fn(),
};
const mockSSETransport = {
sessionId: 'test-session-id',
onclose: null,
handlePostMessage: vi.fn(),
};
// Mock the dependencies before importing the module to test
vi.mock('express', () => {
const jsonMiddlewareMock = vi.fn();
return {
default: vi.fn(() => mockApp),
json: vi.fn().mockReturnValue(jsonMiddlewareMock),
};
});
vi.mock('http', () => {
return {
createServer: vi.fn(() => mockHttpServer),
};
});
vi.mock('@modelcontextprotocol/sdk/server/sse.js', () => {
return {
SSEServerTransport: vi.fn(() => mockSSETransport),
};
});
vi.mock('os', () => {
return {
networkInterfaces: vi.fn(() => ({
eth0: [
{
family: 'IPv4',
internal: false,
address: '192.168.1.100',
},
],
})),
};
});
// Now import the module being tested
import { createHttpServer } from './server.js';
describe('HTTP Server', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.MAX_SSE_CLIENTS = '100';
// Mock console.log to prevent test output
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});
it('should create an HTTP server with SSE transport', () => {
const result = createHttpServer(mockMcpServer as any);
// Should have created a server
expect(mockHttpServer).toBeDefined();
expect(result.app).toBe(mockApp);
expect(result.httpServer).toBe(mockHttpServer);
});
it('should register client limit middleware', () => {
createHttpServer(mockMcpServer as any);
// Should have registered middleware for client limits
expect(mockApp.use).toHaveBeenCalledWith('/mcp', expect.any(Function));
});
it('should register metrics endpoint', () => {
createHttpServer(mockMcpServer as any);
// Should have registered the metrics endpoint
expect(mockApp.get).toHaveBeenCalledWith('/metrics', expect.any(Function));
});
it('should start listening on the specified port', () => {
const port = 5555;
createHttpServer(mockMcpServer as any, port);
// Should have started listening on the specified port
expect(mockHttpServer.listen).toHaveBeenCalledWith(port, '0.0.0.0', expect.any(Function));
});
});
```
--------------------------------------------------------------------------------
/src/providers/clipboard/powershell/index.ts:
--------------------------------------------------------------------------------
```typescript
import { exec } from 'child_process';
import { promisify } from 'util';
import { ClipboardInput } from '../../../types/common.js';
import { WindowsControlResponse } from '../../../types/responses.js';
import { ClipboardAutomation } from '../../../interfaces/automation.js';
const execAsync = promisify(exec);
/**
* PowerShell implementation of the ClipboardAutomation interface
*
* Uses PowerShell commands for clipboard operations on Windows
* NOTE: This provider is Windows-only
*/
export class PowerShellClipboardProvider implements ClipboardAutomation {
private async executePowerShell(command: string): Promise<string> {
try {
const { stdout, stderr } = await execAsync(`powershell.exe -Command "${command}"`);
if (stderr) {
throw new Error(stderr);
}
return stdout.trim();
} catch (error) {
throw new Error(
`PowerShell execution failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async getClipboardContent(): Promise<WindowsControlResponse> {
try {
const content = await this.executePowerShell('Get-Clipboard');
return {
success: true,
message: 'Clipboard content retrieved',
data: content,
};
} catch (error) {
return {
success: false,
message: `Failed to get clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
try {
// Escape quotes in the text for PowerShell
const escapedText = input.text.replace(/"/g, '`"');
await this.executePowerShell(`Set-Clipboard -Value "${escapedText}"`);
return {
success: true,
message: 'Clipboard content set',
};
} catch (error) {
return {
success: false,
message: `Failed to set clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async hasClipboardText(): Promise<WindowsControlResponse> {
try {
const content = await this.executePowerShell('Get-Clipboard');
const hasText = content.length > 0;
return {
success: true,
message: `Clipboard ${hasText ? 'has' : 'does not have'} text`,
data: hasText,
};
} catch (error) {
return {
success: false,
message: `Failed to check clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async clearClipboard(): Promise<WindowsControlResponse> {
try {
await this.executePowerShell('Set-Clipboard -Value ""');
return {
success: true,
message: 'Clipboard cleared',
};
} catch (error) {
return {
success: false,
message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
}
```
--------------------------------------------------------------------------------
/src/providers/registry.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { DefaultProviderRegistry } from './registry.js';
import { ClipboardAutomation } from '../interfaces/automation.js';
import { ClipboardInput } from '../types/common.js';
import { WindowsControlResponse } from '../types/responses.js';
class MockClipboardProvider implements ClipboardAutomation {
// eslint-disable-next-line @typescript-eslint/require-await
async getClipboardContent(): Promise<WindowsControlResponse> {
return { success: true, message: 'Mock clipboard content', data: 'test' };
}
// eslint-disable-next-line @typescript-eslint/require-await
async setClipboardContent(_input: ClipboardInput): Promise<WindowsControlResponse> {
return { success: true, message: 'Mock set clipboard' };
}
// eslint-disable-next-line @typescript-eslint/require-await
async hasClipboardText(): Promise<WindowsControlResponse> {
return { success: true, message: 'Mock has text', data: true };
}
// eslint-disable-next-line @typescript-eslint/require-await
async clearClipboard(): Promise<WindowsControlResponse> {
return { success: true, message: 'Mock clear clipboard' };
}
}
describe('DefaultProviderRegistry', () => {
let registry: DefaultProviderRegistry;
beforeEach(() => {
registry = new DefaultProviderRegistry();
});
describe('registration', () => {
it('should register and retrieve clipboard provider', () => {
const provider = new MockClipboardProvider();
registry.registerClipboard('mock', provider);
const retrieved = registry.getClipboard('mock');
expect(retrieved).toBe(provider);
});
it('should return undefined for non-existent provider', () => {
const retrieved = registry.getClipboard('non-existent');
expect(retrieved).toBeUndefined();
});
it('should allow overwriting existing provider', () => {
const provider1 = new MockClipboardProvider();
const provider2 = new MockClipboardProvider();
registry.registerClipboard('mock', provider1);
registry.registerClipboard('mock', provider2);
const retrieved = registry.getClipboard('mock');
expect(retrieved).toBe(provider2);
});
});
describe('getAvailableProviders', () => {
it('should return empty arrays initially', () => {
const available = registry.getAvailableProviders();
expect(available.keyboards).toEqual([]);
expect(available.mice).toEqual([]);
expect(available.screens).toEqual([]);
expect(available.clipboards).toEqual([]);
});
it('should return registered provider names', () => {
registry.registerClipboard('provider1', new MockClipboardProvider());
registry.registerClipboard('provider2', new MockClipboardProvider());
const available = registry.getAvailableProviders();
expect(available.clipboards).toContain('provider1');
expect(available.clipboards).toContain('provider2');
expect(available.clipboards.length).toBe(2);
});
});
});
```
--------------------------------------------------------------------------------
/src/tools/screenshot-file.ts:
--------------------------------------------------------------------------------
```typescript
import { createAutomationProvider } from '../providers/factory.js';
import { ScreenshotOptions } from '../types/common.js';
import { WindowsControlResponse } from '../types/responses.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { v4 as uuidv4 } from 'uuid';
/**
* Captures a screenshot and saves it to a temporary file
*
* @param options Optional configuration for the screenshot
* @returns Promise resolving to a WindowsControlResponse with the file path
*/
export async function getScreenshotToFile(
options?: ScreenshotOptions,
): Promise<WindowsControlResponse> {
try {
// Create a provider instance to handle the screenshot
const provider = createAutomationProvider();
// Delegate to the provider's screenshot implementation
const result = await provider.screen.getScreenshot(options);
// If the screenshot was successful and contains image data
if (result.success && result.content && result.content[0]?.type === 'image') {
// Create a unique filename in the system's temp directory
const tempDir = os.tmpdir();
const fileExt = options?.format === 'png' ? 'png' : 'jpg';
const filename = `screenshot-${uuidv4()}.${fileExt}`;
const filePath = path.join(tempDir, filename);
// Get the base64 image data and ensure it's a string
let base64Image: string;
try {
const imageData = result.content[0].data;
// Validate the data is a string
if (typeof imageData !== 'string') {
return {
success: false,
message: 'Screenshot data is not in expected string format',
};
}
// Remove the data URL prefix if present
base64Image = imageData.includes('base64,') ? imageData.split('base64,')[1] : imageData;
} catch {
return {
success: false,
message: 'Failed to process screenshot data',
};
}
// Write the image data to the file
fs.writeFileSync(filePath, Buffer.from(base64Image, 'base64'));
// Extract dimensions safely
const width =
typeof result.data === 'object' && result.data && 'width' in result.data
? Number(result.data.width)
: undefined;
const height =
typeof result.data === 'object' && result.data && 'height' in result.data
? Number(result.data.height)
: undefined;
// Return a response with the file path instead of the image data
return {
success: true,
message: 'Screenshot saved to temporary file',
data: {
filePath,
format: options?.format || 'jpeg',
width,
height,
timestamp: new Date().toISOString(),
},
};
}
// If the screenshot failed or doesn't contain image data, return the original result
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred while capturing screenshot';
return {
success: false,
message: `Failed to capture screenshot to file: ${errorMessage}`,
};
}
}
```
--------------------------------------------------------------------------------
/scripts/compare-providers.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
// Script to compare window handling between Keysender and NutJS providers
import { loadConfig } from '../build/config.js';
import { createAutomationProvider } from '../build/providers/factory.js';
// Test function to try all window operations with a provider
async function testProvider(providerName) {
console.log(`\n=== TESTING ${providerName.toUpperCase()} PROVIDER ===\n`);
// Configure environment to use the specified provider
process.env.AUTOMATION_PROVIDER = providerName;
// Load configuration and create provider
const config = loadConfig();
console.log(`Using provider: ${config.provider}`);
const provider = createAutomationProvider(config.provider);
console.log(`Provider created: ${provider.constructor.name}`);
// 1. Get screen size
console.log('\n1. Getting screen size:');
const screenSizeResult = provider.screen.getScreenSize();
console.log(`Success: ${screenSizeResult.success}`);
console.log(`Message: ${screenSizeResult.message}`);
console.log(`Data: ${JSON.stringify(screenSizeResult.data)}`);
// 2. Get active window
console.log('\n2. Getting active window:');
const activeWindowResult = provider.screen.getActiveWindow();
console.log(`Success: ${activeWindowResult.success}`);
console.log(`Message: ${activeWindowResult.message}`);
console.log(`Data: ${JSON.stringify(activeWindowResult.data)}`);
// Extract window title for later operations
const windowTitle = activeWindowResult.success && activeWindowResult.data?.title
? activeWindowResult.data.title
: "Unknown";
// 3. Focus window
console.log(`\n3. Focusing window "${windowTitle}":`);
const focusResult = provider.screen.focusWindow(windowTitle);
console.log(`Success: ${focusResult.success}`);
console.log(`Message: ${focusResult.message}`);
console.log(`Data: ${JSON.stringify(focusResult.data)}`);
// 4. Resize window
console.log(`\n4. Resizing window "${windowTitle}" to 800x600:`);
const resizeResult = provider.screen.resizeWindow(windowTitle, 800, 600);
console.log(`Success: ${resizeResult.success}`);
console.log(`Message: ${resizeResult.message}`);
console.log(`Data: ${JSON.stringify(resizeResult.data)}`);
// 5. Reposition window
console.log(`\n5. Repositioning window "${windowTitle}" to position (100, 100):`);
const repositionResult = provider.screen.repositionWindow(windowTitle, 100, 100);
console.log(`Success: ${repositionResult.success}`);
console.log(`Message: ${repositionResult.message}`);
console.log(`Data: ${JSON.stringify(repositionResult.data)}`);
// 6. Final window check
console.log('\n6. Final window check:');
const finalWindowResult = provider.screen.getActiveWindow();
console.log(`Success: ${finalWindowResult.success}`);
console.log(`Message: ${finalWindowResult.message}`);
console.log(`Data: ${JSON.stringify(finalWindowResult.data)}`);
console.log(`\n=== COMPLETED ${providerName.toUpperCase()} PROVIDER TESTS ===\n`);
}
// Main execution
(async () => {
try {
// First test keysender provider
await testProvider('keysender');
// Only test keysender provider
// await testProvider('other-provider');
} catch (error) {
console.error('Error in testing:', error);
}
})();
```
--------------------------------------------------------------------------------
/src/tools/screenshot.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getScreenshot } from './screenshot';
import { createAutomationProvider } from '../providers/factory';
// Mock the factory module
vi.mock('../providers/factory', () => ({
createAutomationProvider: vi.fn(),
}));
describe('Screenshot Functions', () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
describe('getScreenshot', () => {
it('should delegate to provider and return screenshot data on success', async () => {
// Setup mock provider
const mockProvider = {
screen: {
getScreenshot: vi.fn().mockResolvedValue({
success: true,
message: 'Screenshot captured successfully',
content: [
{
type: 'image',
data: 'test-image-data-base64',
mimeType: 'image/png',
},
],
}),
},
};
// Make createAutomationProvider return our mock
vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);
// Execute
const result = await getScreenshot();
// Verify
expect(createAutomationProvider).toHaveBeenCalledTimes(1);
expect(mockProvider.screen.getScreenshot).toHaveBeenCalledTimes(1);
expect(result).toEqual({
success: true,
message: 'Screenshot captured successfully',
content: [
{
type: 'image',
data: 'test-image-data-base64',
mimeType: 'image/png',
},
],
});
});
it('should pass options to provider when specified', async () => {
// Setup mock provider
const mockProvider = {
screen: {
getScreenshot: vi.fn().mockResolvedValue({
success: true,
message: 'Screenshot captured successfully',
content: [
{
type: 'image',
data: 'test-image-data-base64',
mimeType: 'image/jpeg',
},
],
}),
},
};
// Make createAutomationProvider return our mock
vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);
// Options to pass
const options = {
region: { x: 100, y: 100, width: 800, height: 600 },
format: 'jpeg' as const,
};
// Execute
const result = await getScreenshot(options);
// Verify
expect(createAutomationProvider).toHaveBeenCalledTimes(1);
expect(mockProvider.screen.getScreenshot).toHaveBeenCalledWith(options);
expect(result).toEqual({
success: true,
message: 'Screenshot captured successfully',
content: [
{
type: 'image',
data: 'test-image-data-base64',
mimeType: 'image/jpeg',
},
],
});
});
it('should return error response when provider throws an error', async () => {
// Setup mock provider that throws
const mockProvider = {
screen: {
getScreenshot: vi.fn().mockImplementation(() => {
throw new Error('Capture failed');
}),
},
};
// Make createAutomationProvider return our mock
vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);
// Execute
const result = await getScreenshot();
// Verify
expect(result).toEqual({
success: false,
message: 'Failed to capture screenshot: Capture failed',
});
});
});
});
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/index.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AutoHotkeyProvider } from './index.js';
// Mock child_process module properly
vi.mock('child_process', () => ({
execSync: vi.fn(),
exec: vi.fn(),
spawn: vi.fn(),
fork: vi.fn(),
execFile: vi.fn(),
}));
describe('AutoHotkeyProvider', () => {
let provider: AutoHotkeyProvider;
beforeEach(() => {
provider = new AutoHotkeyProvider();
});
it('should create an instance with all required automation interfaces', () => {
expect(provider).toBeDefined();
expect(provider.keyboard).toBeDefined();
expect(provider.mouse).toBeDefined();
expect(provider.screen).toBeDefined();
expect(provider.clipboard).toBeDefined();
});
it('should implement KeyboardAutomation interface', () => {
expect(provider.keyboard).toBeDefined();
expect(provider.keyboard.typeText).toBeDefined();
expect(provider.keyboard.pressKey).toBeDefined();
expect(provider.keyboard.pressKeyCombination).toBeDefined();
expect(provider.keyboard.holdKey).toBeDefined();
});
it('should implement MouseAutomation interface', () => {
expect(provider.mouse).toBeDefined();
expect(provider.mouse.moveMouse).toBeDefined();
expect(provider.mouse.clickMouse).toBeDefined();
expect(provider.mouse.doubleClick).toBeDefined();
expect(provider.mouse.getCursorPosition).toBeDefined();
expect(provider.mouse.scrollMouse).toBeDefined();
expect(provider.mouse.dragMouse).toBeDefined();
expect(provider.mouse.clickAt).toBeDefined();
});
it('should implement ScreenAutomation interface', () => {
expect(provider.screen).toBeDefined();
expect(provider.screen.getScreenSize).toBeDefined();
expect(provider.screen.getActiveWindow).toBeDefined();
expect(provider.screen.focusWindow).toBeDefined();
expect(provider.screen.resizeWindow).toBeDefined();
expect(provider.screen.repositionWindow).toBeDefined();
expect(provider.screen.getScreenshot).toBeDefined();
});
it('should implement ClipboardAutomation interface', () => {
expect(provider.clipboard).toBeDefined();
expect(provider.clipboard.getClipboardContent).toBeDefined();
expect(provider.clipboard.setClipboardContent).toBeDefined();
expect(provider.clipboard.hasClipboardText).toBeDefined();
expect(provider.clipboard.clearClipboard).toBeDefined();
});
});
describe('AutoHotkeyProvider - Factory Integration', () => {
beforeEach(() => {
// Mock the factory module to avoid keysender ELF header issue
vi.doMock('../factory.js', () => ({
createAutomationProvider: vi.fn().mockImplementation((config: any) => {
if (config?.provider === 'autohotkey' || config?.providers) {
return new AutoHotkeyProvider();
}
return {};
}),
}));
});
it('should be available through the factory', async () => {
const { createAutomationProvider } = await import('../factory.js');
const provider = createAutomationProvider({ provider: 'autohotkey' });
expect(provider).toBeDefined();
expect(provider).toBeInstanceOf(AutoHotkeyProvider);
});
it('should support modular configuration', async () => {
const { createAutomationProvider } = await import('../factory.js');
const provider = createAutomationProvider({
providers: {
keyboard: 'autohotkey',
mouse: 'autohotkey',
screen: 'autohotkey',
clipboard: 'autohotkey',
},
});
expect(provider).toBeDefined();
expect(provider.keyboard).toBeDefined();
expect(provider.mouse).toBeDefined();
expect(provider.screen).toBeDefined();
expect(provider.clipboard).toBeDefined();
});
});
```
--------------------------------------------------------------------------------
/RELEASE_NOTES_v0.2.0.md:
--------------------------------------------------------------------------------
```markdown
# Release Notes v0.2.0
## 🎉 Major Features
### SSE Transport Now Officially Supported
- Full implementation of Server-Sent Events (SSE) transport for network access
- Built using the MCP SDK transport layer for improved reliability
- HTTP/HTTPS server integration for secure connections
- Configurable port settings (default: 3232)
### HTTPS/TLS Support
- Added secure TLS/SSL support for production deployments
- New CLI flags: `--https`, `--cert`, and `--key`
- Certificate validation for enhanced security
- Meets MCP specification requirements for secure remote access
### Improved Documentation
- Added comprehensive Quick Start guide with build tools setup
- Enhanced installation instructions for Windows users
- Clear prerequisites including VC++ workload requirements
- Better guidance for Python and Node.js installation
## 🚀 Enhancements
### Infrastructure Improvements
- Optimized build process with npm ci and caching
- Standardized default port (3232) across entire codebase
- Removed unused dependencies (express, jimp, mcp-control)
- Improved GitHub Actions with better error handling
### Testing Framework
- Added end-to-end testing suite for integration testing
- Better test coverage for SSE transport features
- Enhanced CI/CD pipeline reliability
### Developer Experience
- Simplified SSE implementation using SDK transport
- Better error handling for client connections
- Buffer management improvements
- Platform-specific path fixes
## 🔧 CLI Updates
New command line options:
```bash
# Run with SSE transport
mcp-control --sse
# Run with HTTPS/TLS
mcp-control --sse --https --cert /path/to/cert.pem --key /path/to/key.pem
# Custom port
mcp-control --sse --port 3000
```
## 📦 Dependency Updates
- Updated `@modelcontextprotocol/sdk` to latest version
- Bumped TypeScript ESLint packages to v8.32.0+
- Updated `zod` to v3.24.4
- Various dev dependency updates for security
## 📚 Documentation
- Added SSE transport documentation
- Updated README with release badges
- Improved branch structure documentation
- Added build tools setup instructions
- Enhanced security guidelines
## 🐛 Bug Fixes
- Fixed TypeScript errors related to HTTP server usage
- Resolved client error handling in SSE transport
- Corrected platform-specific path issues
- Fixed npm ci error handling in build scripts
## ⚠️ Breaking Changes
- SSE is now the recommended transport method
- HTTPS is required for production deployments per MCP spec
- Some internal API changes for transport handling
## 🔐 Security
- Added proper TLS certificate validation
- Implemented security options for HTTPS connections
- Updated dependencies to address known vulnerabilities
## 📈 Migration Guide
To upgrade from v0.1.x to v0.2.0:
1. Update your Claude client configuration to use SSE transport:
```json
{
"mcpServers": {
"MCPControl": {
"command": "mcp-control",
"args": ["--transport", "sse"]
}
}
}
```
2. For production deployments, use HTTPS:
```bash
mcp-control --sse --https --cert cert.pem --key key.pem
```
3. Ensure you have the latest build tools installed as per the Quick Start guide
## 👥 Contributors
Special thanks to all contributors who made this release possible, including:
- @Cheffromspace for SSE transport and HTTPS implementation
- @lwsinclair for adding the MseeP.ai security badge
- All the community members who reported issues and provided feedback
## 🔮 What's Next
- Multi-monitor support improvements
- Enhanced click accuracy at different resolutions
- Additional transport options
- Performance optimizations
---
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
import { describe, it, expect, vi } from 'vitest';
import { createAutomationProvider } from '../factory.js';
// Imported for type checking but used indirectly through factory
import './index.js';
// Mock keysender module
vi.mock('keysender', async () => {
await vi.importActual('vitest');
const mockObject = {
Hardware: vi.fn().mockImplementation(() => ({
workwindow: {
capture: vi.fn(),
get: vi.fn(),
set: vi.fn(),
getView: vi.fn(),
setForeground: vi.fn(),
setView: vi.fn(),
isForeground: vi.fn(),
isOpen: vi.fn(),
},
mouse: {
move: vi.fn(),
leftClick: vi.fn(),
rightClick: vi.fn(),
middleClick: vi.fn(),
doubleClick: vi.fn(),
leftDown: vi.fn(),
leftUp: vi.fn(),
rightDown: vi.fn(),
rightUp: vi.fn(),
scroll: vi.fn(),
},
keyboard: {
pressKey: vi.fn(),
releaseKey: vi.fn(),
typeString: vi.fn(),
},
clipboard: {
getClipboard: vi.fn(),
setClipboard: vi.fn(),
},
})),
getScreenSize: vi.fn().mockReturnValue({ width: 1920, height: 1080 }),
getAllWindows: vi.fn().mockReturnValue([{ title: 'Test Window', handle: 12345 }]),
getWindowChildren: vi.fn().mockReturnValue([]),
};
return {
default: mockObject,
...mockObject,
};
});
// Create a simple mock of KeysenderProvider for use in tests
class MockKeysenderProvider {
keyboard = { keyTap: vi.fn() };
mouse = { moveMouse: vi.fn() };
screen = { getScreenSize: vi.fn() };
clipboard = { readClipboard: vi.fn() };
}
// Mock the factory to avoid native module loading issues
vi.mock('../factory.js', async () => {
await vi.importActual('vitest');
return {
createAutomationProvider: vi.fn().mockImplementation((_providerType) => {
return new MockKeysenderProvider();
}),
};
});
// Mock the automation classes
vi.mock('./keyboard.js', async () => {
await vi.importActual('vitest');
return {
KeysenderKeyboardAutomation: vi.fn().mockImplementation(() => ({
keyTap: vi.fn(),
keyToggle: vi.fn(),
typeString: vi.fn(),
typeStringDelayed: vi.fn(),
setKeyboardDelay: vi.fn(),
})),
};
});
vi.mock('./mouse.js', async () => {
await vi.importActual('vitest');
return {
KeysenderMouseAutomation: vi.fn().mockImplementation(() => ({
moveMouse: vi.fn(),
moveMouseSmooth: vi.fn(),
mouseClick: vi.fn(),
mouseDoubleClick: vi.fn(),
mouseToggle: vi.fn(),
dragMouse: vi.fn(),
scrollMouse: vi.fn(),
getMousePosition: vi.fn(),
setMousePosition: vi.fn(),
setMouseSpeed: vi.fn(),
})),
};
});
vi.mock('./screen.js', async () => {
await vi.importActual('vitest');
return {
KeysenderScreenAutomation: vi.fn().mockImplementation(() => ({
getScreenSize: vi.fn(),
getScreenshot: vi.fn(),
getActiveWindow: vi.fn(),
focusWindow: vi.fn(),
resizeWindow: vi.fn(),
repositionWindow: vi.fn(),
})),
};
});
vi.mock('./clipboard.js', async () => {
await vi.importActual('vitest');
return {
KeysenderClipboardAutomation: vi.fn().mockImplementation(() => ({
readClipboard: vi.fn(),
writeClipboard: vi.fn(),
})),
};
});
describe('KeysenderProvider', () => {
it('should be created through the factory', () => {
const provider = createAutomationProvider({ provider: 'keysender' });
expect(provider).toBeInstanceOf(MockKeysenderProvider);
});
it('should have all required automation interfaces', () => {
const provider = new MockKeysenderProvider();
expect(provider.keyboard).toBeDefined();
expect(provider.mouse).toBeDefined();
expect(provider.screen).toBeDefined();
expect(provider.clipboard).toBeDefined();
});
});
```
--------------------------------------------------------------------------------
/.github/pr-webhook-utils.cjs:
--------------------------------------------------------------------------------
```
/**
* Utilities for PR webhook data handling and sanitization
*/
/**
* Sanitizes text content to remove truly sensitive information
* @param {string} text - Text content to sanitize
* @returns {string} Sanitized text
*/
function sanitizeText(text) {
if (!text) return '';
try {
return text
// Remove common API tokens with specific patterns
.replace(/(\b)(gh[ps]_[A-Za-z0-9_]{36,})(\b)/g, '[GH_TOKEN_REDACTED]')
.replace(/(\b)(xox[pbar]-[0-9a-zA-Z-]{10,})(\b)/g, '[SLACK_TOKEN_REDACTED]')
.replace(/(\b)(sk-[a-zA-Z0-9]{32,})(\b)/g, '[API_KEY_REDACTED]')
.replace(/(\b)(AKIA[0-9A-Z]{16})(\b)/g, '[AWS_KEY_REDACTED]')
// Remove emails, but only likely real ones (with valid TLDs)
.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/g, '[EMAIL_REDACTED]')
// Remove IP addresses
.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '[IP_REDACTED]')
// Remove control characters that might break JSON
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
} catch (error) {
console.warn(`Error sanitizing text: ${error.message}`);
return '[Content omitted due to sanitization error]';
}
}
/**
* Checks if a file should be included in webhook data
* @param {string} filename - Filename to check
* @returns {boolean} Whether file should be included
*/
function shouldIncludeFile(filename) {
if (!filename) return false;
const sensitivePatterns = [
// Only exclude actual sensitive files
/\.env($|\.)/i,
/\.key$/i,
/\.pem$/i,
/\.pfx$/i,
/\.p12$/i,
// Binary files that would bloat the payload
/\.(jpg|jpeg|png|gif|ico|pdf|zip|tar|gz|bin|exe)$/i
];
return !sensitivePatterns.some(pattern => pattern.test(filename));
}
/**
* Safely limits patch size to prevent payload issues
* @param {string} patch - Git patch content
* @returns {string|undefined} Limited patch or undefined on error
*/
function limitPatch(patch) {
if (!patch) return undefined;
try {
// Increase reasonable patch size limit to 30KB
const maxPatchSize = 30 * 1024;
if (patch.length > maxPatchSize) {
return patch.substring(0, maxPatchSize) + '\n[... PATCH TRUNCATED DUE TO SIZE ...]';
}
return patch;
} catch (error) {
console.warn(`Error limiting patch: ${error.message}`);
return undefined; // Return undefined on error
}
}
/**
* Safely stringifies JSON with error handling
* @param {Object} data - Data to stringify
* @returns {Object} Result with success status and data/error
*/
function safeStringify(data) {
try {
const jsonData = JSON.stringify(data);
return { success: true, data: jsonData };
} catch (error) {
console.error(`JSON stringify error: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
/**
* Creates a simplified version of PR data that's less likely to cause parsing issues
* Used as a fallback when the full PR data cannot be stringified
* @param {Object} pr - Full PR data
* @param {Object} context - GitHub context object
* @returns {Object} Simplified PR data with essential information only
*/
function createSimplifiedPrData(pr, context) {
return {
id: pr.data.id,
number: pr.data.number,
title: sanitizeText(pr.data.title),
state: pr.data.state,
created_at: pr.data.created_at,
repository: context.repo.repo,
owner: context.repo.owner,
body: sanitizeText(pr.data.body?.substring(0, 1000)),
head: {
ref: pr.data.head.ref,
sha: pr.data.head.sha
},
base: {
ref: pr.data.base.ref,
sha: pr.data.base.sha
},
labels: pr.data.labels?.map(l => l.name),
error: 'Using simplified payload due to JSON serialization issues with full payload'
};
}
module.exports = {
sanitizeText,
shouldIncludeFile,
limitPatch,
safeStringify,
createSimplifiedPrData
};
```
--------------------------------------------------------------------------------
/src/providers/factory.ts:
--------------------------------------------------------------------------------
```typescript
import { AutomationProvider } from '../interfaces/provider.js';
import { KeysenderProvider } from './keysender/index.js';
import { AutoHotkeyProvider } from './autohotkey/index.js';
import { registry } from './registry.js';
import { AutomationConfig } from '../config.js';
import {
KeyboardAutomation,
MouseAutomation,
ScreenAutomation,
ClipboardAutomation,
} from '../interfaces/automation.js';
// Import individual providers
import { PowerShellClipboardProvider } from './clipboard/powershell/index.js';
import { ClipboardyProvider } from './clipboard/clipboardy/index.js';
// Cache to store provider instances
const providerCache: Record<string, AutomationProvider> = {};
/**
* Initialize the provider registry with available providers
*/
export function initializeProviders(): void {
// Register clipboard providers
registry.registerClipboard('powershell', new PowerShellClipboardProvider());
registry.registerClipboard('clipboardy', new ClipboardyProvider());
// Register AutoHotkey providers
const autohotkeyProvider = new AutoHotkeyProvider();
registry.registerKeyboard('autohotkey', autohotkeyProvider.keyboard);
registry.registerMouse('autohotkey', autohotkeyProvider.mouse);
registry.registerScreen('autohotkey', autohotkeyProvider.screen);
registry.registerClipboard('autohotkey', autohotkeyProvider.clipboard);
// TODO: Register other providers as they are implemented
}
/**
* Composite provider that allows mixing different component providers
*/
class CompositeProvider implements AutomationProvider {
keyboard: KeyboardAutomation;
mouse: MouseAutomation;
screen: ScreenAutomation;
clipboard: ClipboardAutomation;
constructor(
keyboard: KeyboardAutomation,
mouse: MouseAutomation,
screen: ScreenAutomation,
clipboard: ClipboardAutomation,
) {
this.keyboard = keyboard;
this.mouse = mouse;
this.screen = screen;
this.clipboard = clipboard;
}
}
/**
* Create an automation provider instance based on configuration
* Supports both legacy monolithic providers and new modular providers
*/
export function createAutomationProvider(config?: AutomationConfig): AutomationProvider {
// Initialize providers if not already done
if (registry.getAvailableProviders().clipboards.length === 0) {
initializeProviders();
}
if (!config || !config.providers) {
// Legacy behavior: use monolithic provider
const type = config?.provider || 'keysender';
const providerType = type.toLowerCase();
// Return cached instance if available
if (providerCache[providerType]) {
return providerCache[providerType];
}
let provider: AutomationProvider;
switch (providerType) {
case 'keysender':
provider = new KeysenderProvider();
break;
case 'autohotkey':
provider = new AutoHotkeyProvider();
break;
default:
throw new Error(`Unknown provider type: ${providerType}`);
}
// Cache the instance
providerCache[providerType] = provider;
return provider;
}
// New modular approach
const cacheKey = JSON.stringify(config.providers);
if (providerCache[cacheKey]) {
return providerCache[cacheKey];
}
// Get individual components from the registry
const keyboardProvider = config.providers.keyboard
? registry.getKeyboard(config.providers.keyboard)
: new KeysenderProvider().keyboard;
const mouseProvider = config.providers.mouse
? registry.getMouse(config.providers.mouse)
: new KeysenderProvider().mouse;
const screenProvider = config.providers.screen
? registry.getScreen(config.providers.screen)
: new KeysenderProvider().screen;
const clipboardProvider = config.providers.clipboard
? registry.getClipboard(config.providers.clipboard)
: new KeysenderProvider().clipboard;
if (!keyboardProvider || !mouseProvider || !screenProvider || !clipboardProvider) {
throw new Error('Failed to resolve all provider components');
}
const compositeProvider = new CompositeProvider(
keyboardProvider,
mouseProvider,
screenProvider,
clipboardProvider,
);
providerCache[cacheKey] = compositeProvider;
return compositeProvider;
}
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Centralized logger using Pino
* Provides consistent logging with configurable log levels
*/
/**
* Log levels in order of verbosity (most verbose to least verbose)
* - trace: Most detailed information for tracing code execution
* - debug: Debugging information useful during development
* - info: General information about normal operation
* - warn: Warning conditions that should be reviewed
* - error: Error conditions that don't interrupt operation
* - fatal: Critical errors that might interrupt operation
* - silent: No logs at all
*/
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
/**
* Interface for a logger
*/
export interface Logger {
trace(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
fatal(msg: string, ...args: unknown[]): void;
child(bindings: Record<string, unknown>): Logger;
}
/**
* Simple console-based logger implementation
* This will be replaced with Pino in the package.json update
*/
class ConsoleLogger implements Logger {
private level: number;
private readonly levelMap: Record<LogLevel, number> = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60,
silent: 100,
};
private context: Record<string, unknown> = {};
constructor(level: LogLevel = 'info', context: Record<string, unknown> = {}) {
this.level = this.levelMap[level];
this.context = context;
}
private formatMessage(msg: string): string {
if (Object.keys(this.context).length === 0) {
return msg;
}
const contextStr = Object.entries(this.context)
.map(([key, value]) => `${key}=${String(value)}`)
.join(' ');
return `[${contextStr}] ${msg}`;
}
private shouldLog(level: LogLevel): boolean {
return this.levelMap[level] >= this.level;
}
trace(msg: string, ...args: unknown[]): void {
if (this.shouldLog('trace')) {
console.log(`[TRACE] ${this.formatMessage(msg)}`, ...args);
}
}
debug(msg: string, ...args: unknown[]): void {
if (this.shouldLog('debug')) {
console.log(`[DEBUG] ${this.formatMessage(msg)}`, ...args);
}
}
info(msg: string, ...args: unknown[]): void {
if (this.shouldLog('info')) {
console.log(`[INFO] ${this.formatMessage(msg)}`, ...args);
}
}
warn(msg: string, ...args: unknown[]): void {
if (this.shouldLog('warn')) {
console.warn(`[WARN] ${this.formatMessage(msg)}`, ...args);
}
}
error(msg: string, ...args: unknown[]): void {
if (this.shouldLog('error')) {
console.error(`[ERROR] ${this.formatMessage(msg)}`, ...args);
}
}
fatal(msg: string, ...args: unknown[]): void {
if (this.shouldLog('fatal')) {
console.error(`[FATAL] ${this.formatMessage(msg)}`, ...args);
}
}
child(bindings: Record<string, unknown>): Logger {
return new ConsoleLogger(
Object.keys(this.levelMap).find(key => this.levelMap[key as LogLevel] === this.level) as LogLevel,
{ ...this.context, ...bindings }
);
}
}
/**
* Get log level from environment variable or use default
*/
export function getLogLevel(): LogLevel {
const envLevel = process.env.LOG_LEVEL?.toLowerCase() as LogLevel | undefined;
// Validate that the provided level is valid
const validLevels: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'];
if (envLevel && validLevels.includes(envLevel)) {
return envLevel;
}
// Default to info in production, debug in development/test
if (process.env.NODE_ENV === 'production') {
return 'info';
} else if (process.env.NODE_ENV === 'test' || process.env.VITEST) {
return 'silent'; // Silent in tests unless explicitly set
} else {
return 'debug';
}
}
// Create the default logger instance
export const logger: Logger = new ConsoleLogger(getLogLevel());
/**
* Create a child logger with component context
* @param component Component name or identifier
* @returns Logger instance with component context
*/
export function createLogger(component: string): Logger {
return logger.child({ component });
}
export default logger;
```
--------------------------------------------------------------------------------
/src/tools/keyboard.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { typeText, pressKey, pressKeyCombination, holdKey } from './keyboard.js';
import type { KeyboardInput, KeyCombination, KeyHoldOperation } from '../types/common.js';
// Mock the provider
vi.mock('../providers/factory.js', () => ({
createAutomationProvider: () => ({
keyboard: {
typeText: vi.fn().mockImplementation(() => ({
success: true,
message: 'Typed text successfully',
})),
pressKey: vi.fn().mockImplementation((key) => ({
success: true,
message: `Pressed key: ${key}`,
})),
pressKeyCombination: vi.fn().mockImplementation((combination) => ({
success: true,
message: `Pressed key combination: ${combination.keys.join('+')}`,
})),
holdKey: vi.fn().mockImplementation((operation) =>
operation.state === 'down'
? {
success: true,
message: `Key ${operation.key} held successfully for ${operation.duration}ms`,
}
: { success: true, message: `Key ${operation.key} released successfully` },
),
},
}),
}));
describe('Keyboard Tools', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('typeText', () => {
it('should successfully type text', () => {
const input: KeyboardInput = { text: 'Hello World' };
const result = typeText(input);
expect(result).toEqual({
success: true,
message: 'Typed text successfully',
});
});
it('should handle errors when text is missing', () => {
const input: KeyboardInput = { text: '' };
const result = typeText(input);
expect(result.success).toBe(false);
expect(result.message).toContain('Text is required');
});
it('should handle errors when text is too long', () => {
// Create a string that's too long
const longText = 'a'.repeat(1001);
const input: KeyboardInput = { text: longText };
const result = typeText(input);
expect(result.success).toBe(false);
expect(result.message).toContain('Text too long');
});
});
describe('pressKey', () => {
it('should successfully press a single key', () => {
const result = pressKey('a');
expect(result).toEqual({
success: true,
message: 'Pressed key: a',
});
});
it('should handle errors when key is invalid', () => {
const result = pressKey('invalid_key');
expect(result.success).toBe(false);
expect(result.message).toContain('Invalid key');
});
});
describe('pressKeyCombination', () => {
it('should successfully press a key combination', async () => {
const combination: KeyCombination = { keys: ['ctrl', 'c'] };
const result = await pressKeyCombination(combination);
expect(result).toEqual({
success: true,
message: 'Pressed key combination: ctrl+c',
});
});
it('should handle errors when combination is invalid', async () => {
const result = await pressKeyCombination({ keys: [] });
expect(result.success).toBe(false);
expect(result.message).toContain('Key combination must contain at least one key');
});
});
describe('holdKey', () => {
beforeEach(() => {
vi.useFakeTimers();
});
it('should successfully hold and release a key', async () => {
const operation: KeyHoldOperation = {
key: 'shift',
duration: 1000,
state: 'down',
};
const holdPromise = holdKey(operation);
// Fast-forward through the duration
await vi.runAllTimersAsync();
const result = await holdPromise;
expect(result).toEqual({
success: true,
message: 'Key shift held successfully for 1000ms',
});
});
it('should handle just releasing a key', async () => {
const operation: KeyHoldOperation = {
key: 'shift',
duration: 0,
state: 'up',
};
const result = await holdKey(operation);
expect(result).toEqual({
success: true,
message: 'Key shift released successfully',
});
});
it('should handle errors when key is invalid', async () => {
const operation: KeyHoldOperation = {
// @ts--error - Testing invalid input
key: 'invalid_key',
duration: 1000,
state: 'down',
};
const result = await holdKey(operation);
expect(result.success).toBe(false);
expect(result.message).toContain('Invalid key');
});
});
});
```
--------------------------------------------------------------------------------
/src/tools/mouse.ts:
--------------------------------------------------------------------------------
```typescript
import { MousePosition } from '../types/common.js';
import { WindowsControlResponse } from '../types/responses.js';
import { createAutomationProvider } from '../providers/factory.js';
import { MousePositionSchema, MouseButtonSchema, ScrollAmountSchema } from './validation.zod.js';
import { createLogger } from '../logger.js';
// Get the automation provider
const provider = createAutomationProvider();
// Create a logger for mouse module
const logger = createLogger('mouse');
// Define button types
type MouseButton = 'left' | 'right' | 'middle';
export function moveMouse(position: MousePosition): WindowsControlResponse {
try {
// Validate the position
MousePositionSchema.parse(position);
// Additional screen bounds check if not in test environment
if (!(process.env.NODE_ENV === 'test' || process.env.VITEST)) {
try {
const screenSizeResponse = provider.screen.getScreenSize();
if (screenSizeResponse.success && screenSizeResponse.data) {
const screenSize = screenSizeResponse.data as { width: number; height: number };
if (
position.x < 0 ||
position.x >= screenSize.width ||
position.y < 0 ||
position.y >= screenSize.height
) {
throw new Error(
`Position (${position.x},${position.y}) is outside screen bounds (0,0)-(${screenSize.width - 1},${screenSize.height - 1})`,
);
}
}
} catch (screenError) {
logger.warn('Error checking screen bounds', screenError);
// Continue without screen bounds check
}
}
return provider.mouse.moveMouse(position);
} catch (error) {
return {
success: false,
message: `Failed to move mouse: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function clickMouse(button: MouseButton = 'left'): WindowsControlResponse {
try {
// Validate button
MouseButtonSchema.parse(button);
const validatedButton = button;
return provider.mouse.clickMouse(validatedButton);
} catch (error) {
return {
success: false,
message: `Failed to click mouse: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function doubleClick(position?: MousePosition): WindowsControlResponse {
try {
// Validate position if provided
if (position) {
MousePositionSchema.parse(position);
}
return provider.mouse.doubleClick(position);
} catch (error) {
return {
success: false,
message: `Failed to double click: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function getCursorPosition(): WindowsControlResponse {
try {
return provider.mouse.getCursorPosition();
} catch (error) {
return {
success: false,
message: `Failed to get cursor position: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function scrollMouse(amount: number): WindowsControlResponse {
try {
// Validate amount
ScrollAmountSchema.parse(amount);
return provider.mouse.scrollMouse(amount);
} catch (error) {
return {
success: false,
message: `Failed to scroll mouse: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function dragMouse(
from: MousePosition,
to: MousePosition,
button: MouseButton = 'left',
): WindowsControlResponse {
try {
// Validate positions
MousePositionSchema.parse(from);
MousePositionSchema.parse(to);
// Validate button
MouseButtonSchema.parse(button);
const validatedButton = button;
return provider.mouse.dragMouse(from, to, validatedButton);
} catch (error) {
return {
success: false,
message: `Failed to drag mouse: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
export function clickAt(
x: number,
y: number,
button: MouseButton = 'left',
): WindowsControlResponse {
// Special case for test compatibility (match original implementation)
if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
return {
success: false,
message: 'Invalid coordinates provided',
};
}
try {
// Validate position against screen bounds
MousePositionSchema.parse({ x, y });
// Validate button
MouseButtonSchema.parse(button);
const validatedButton = button;
return provider.mouse.clickAt(x, y, validatedButton);
} catch (error) {
return {
success: false,
message: `Failed to click at position: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
```
--------------------------------------------------------------------------------
/src/providers/clipboard/powershell/index.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { ClipboardInput } from '../../../types/common.js';
// Set up mocks at the top level
const execAsyncMock = vi.fn();
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
vi.mock('util', () => ({
promisify: vi.fn().mockReturnValue(execAsyncMock),
}));
// Dynamic import to ensure mocks are setup
describe('PowerShellClipboardProvider', () => {
let PowerShellClipboardProvider: any;
let provider: any;
beforeEach(async () => {
vi.clearAllMocks();
execAsyncMock.mockReset();
// Dynamically import after mocks are setup
const module = await import('./index.js');
PowerShellClipboardProvider = module.PowerShellClipboardProvider;
provider = new PowerShellClipboardProvider();
});
describe('getClipboardContent', () => {
it('should get clipboard content successfully', async () => {
execAsyncMock.mockResolvedValue({ stdout: 'Test content\n', stderr: '' });
const result = await provider.getClipboardContent();
expect(result.success).toBe(true);
expect(result.data).toBe('Test content');
expect(execAsyncMock).toHaveBeenCalledWith('powershell.exe -Command "Get-Clipboard"');
});
it('should handle errors', async () => {
execAsyncMock.mockRejectedValue(new Error('PowerShell error'));
const result = await provider.getClipboardContent();
expect(result.success).toBe(false);
expect(result.message).toContain('Failed to get clipboard content');
});
it('should handle stderr', async () => {
execAsyncMock.mockResolvedValue({ stdout: '', stderr: 'Error output' });
const result = await provider.getClipboardContent();
expect(result.success).toBe(false);
expect(result.message).toContain('Error output');
});
});
describe('setClipboardContent', () => {
it('should set clipboard content successfully', async () => {
execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
const input: ClipboardInput = { text: 'New content' };
const result = await provider.setClipboardContent(input);
expect(result.success).toBe(true);
expect(execAsyncMock).toHaveBeenCalledWith(
'powershell.exe -Command "Set-Clipboard -Value "New content""',
);
});
it('should escape quotes in text', async () => {
execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
const input: ClipboardInput = { text: 'Text with "quotes"' };
await provider.setClipboardContent(input);
expect(execAsyncMock).toHaveBeenCalledWith(
'powershell.exe -Command "Set-Clipboard -Value "Text with `"quotes`"""',
);
});
it('should handle errors', async () => {
execAsyncMock.mockRejectedValue(new Error('PowerShell error'));
const input: ClipboardInput = { text: 'Test' };
const result = await provider.setClipboardContent(input);
expect(result.success).toBe(false);
expect(result.message).toContain('Failed to set clipboard content');
});
});
describe('hasClipboardText', () => {
it('should return true when clipboard has text', async () => {
execAsyncMock.mockResolvedValue({ stdout: 'Some text\n', stderr: '' });
const result = await provider.hasClipboardText();
expect(result.success).toBe(true);
expect(result.data).toBe(true);
});
it('should return false when clipboard is empty', async () => {
execAsyncMock.mockResolvedValue({ stdout: '\n', stderr: '' });
const result = await provider.hasClipboardText();
expect(result.success).toBe(true);
expect(result.data).toBe(false);
});
it('should return false when clipboard is empty string', async () => {
execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
const result = await provider.hasClipboardText();
expect(result.success).toBe(true);
expect(result.data).toBe(false);
});
});
describe('clearClipboard', () => {
it('should clear clipboard successfully', async () => {
execAsyncMock.mockResolvedValue({ stdout: '', stderr: '' });
const result = await provider.clearClipboard();
expect(result.success).toBe(true);
expect(execAsyncMock).toHaveBeenCalledWith(
'powershell.exe -Command "Set-Clipboard -Value """',
);
});
it('should handle errors in clearClipboard', async () => {
execAsyncMock.mockRejectedValue(new Error('PowerShell error'));
const result = await provider.clearClipboard();
expect(result.success).toBe(false);
expect(result.message).toContain('Failed to clear clipboard');
});
});
});
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/clipboard.ts:
--------------------------------------------------------------------------------
```typescript
import { execSync } from 'child_process';
import { writeFileSync, unlinkSync, readFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { WindowsControlResponse } from '../../types/responses.js';
import { ClipboardAutomation } from '../../interfaces/automation.js';
import { ClipboardInput } from '../../types/common.js';
import { getAutoHotkeyPath } from './utils.js';
/**
* AutoHotkey implementation of the ClipboardAutomation interface
*/
export class AutoHotkeyClipboardAutomation implements ClipboardAutomation {
/**
* Execute an AutoHotkey script
*/
private executeScript(script: string): void {
const scriptPath = join(tmpdir(), `mcp-ahk-${Date.now()}.ahk`);
try {
// Write the script to a temporary file
writeFileSync(scriptPath, script, 'utf8');
// Execute the script with AutoHotkey v2
const autohotkeyPath = getAutoHotkeyPath();
execSync(`"${autohotkeyPath}" "${scriptPath}"`, { stdio: 'pipe' });
} finally {
// Clean up the temporary script file
try {
unlinkSync(scriptPath);
} catch {
// Ignore cleanup errors
}
}
}
/**
* Execute a script and return output from a temporary file
*/
private executeScriptWithOutput(script: string, _outputPath: string): void {
const scriptPath = join(tmpdir(), `mcp-ahk-${Date.now()}.ahk`);
try {
writeFileSync(scriptPath, script, 'utf8');
const autohotkeyPath = getAutoHotkeyPath();
execSync(`"${autohotkeyPath}" "${scriptPath}"`, { stdio: 'pipe' });
} finally {
try {
unlinkSync(scriptPath);
} catch {
// Ignore cleanup errors
}
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async setClipboardContent(input: ClipboardInput): Promise<WindowsControlResponse> {
try {
// Escape special characters
const escapedText = input.text
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/`/g, '``')
.replace(/{/g, '{{')
.replace(/}/g, '}}');
const script = `
A_Clipboard := "${escapedText}"
ExitApp
`;
this.executeScript(script);
return {
success: true,
message: 'Text copied to clipboard',
};
} catch (error) {
return {
success: false,
message: `Failed to copy to clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
// This method is not part of the interface - removing it
/*
paste(): WindowsControlResponse {
try {
const script = `
Send("^v")
ExitApp
`;
this.executeScript(script);
return {
success: true,
message: 'Pasted from clipboard',
};
} catch (error) {
return {
success: false,
message: `Failed to paste from clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
*/
// eslint-disable-next-line @typescript-eslint/require-await
async hasClipboardText(): Promise<WindowsControlResponse> {
try {
const outputPath = join(tmpdir(), `mcp-ahk-output-${Date.now()}.txt`);
const script = `
hasText := A_Clipboard != ""
FileAppend(hasText ? "true" : "false", "${outputPath}")
ExitApp
`;
this.executeScriptWithOutput(script, outputPath);
try {
const result = readFileSync(outputPath, 'utf8');
const hasText = result === 'true';
return {
success: true,
message: hasText ? 'Clipboard contains text' : 'Clipboard is empty',
data: { hasText },
};
} finally {
try {
unlinkSync(outputPath);
} catch {
// Ignore cleanup errors
}
}
} catch (error) {
return {
success: false,
message: `Failed to check clipboard content: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async getClipboardContent(): Promise<WindowsControlResponse> {
try {
const outputPath = join(tmpdir(), `mcp-ahk-output-${Date.now()}.txt`);
const script = `
content := A_Clipboard
FileAppend(content, "${outputPath}")
ExitApp
`;
this.executeScriptWithOutput(script, outputPath);
try {
const content = readFileSync(outputPath, 'utf8');
return {
success: true,
message: 'Retrieved clipboard content',
data: { text: content },
};
} finally {
try {
unlinkSync(outputPath);
} catch {
// Ignore cleanup errors
}
}
} catch (error) {
return {
success: false,
message: `Failed to read from clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async clearClipboard(): Promise<WindowsControlResponse> {
try {
const script = `
A_Clipboard := ""
ExitApp
`;
this.executeScript(script);
return {
success: true,
message: 'Clipboard cleared',
};
} catch (error) {
return {
success: false,
message: `Failed to clear clipboard: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
}
```
--------------------------------------------------------------------------------
/src/providers/autohotkey/keyboard.ts:
--------------------------------------------------------------------------------
```typescript
import { execSync } from 'child_process';
import { writeFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { KeyboardInput, KeyCombination, KeyHoldOperation } from '../../types/common.js';
import { WindowsControlResponse } from '../../types/responses.js';
import { KeyboardAutomation } from '../../interfaces/automation.js';
import {
MAX_TEXT_LENGTH,
KeySchema,
KeyCombinationSchema,
KeyHoldOperationSchema,
} from '../../tools/validation.zod.js';
import { getAutoHotkeyPath } from './utils.js';
/**
* AutoHotkey implementation of the KeyboardAutomation interface
*/
export class AutoHotkeyKeyboardAutomation implements KeyboardAutomation {
/**
* Execute an AutoHotkey script
*/
private executeScript(script: string): void {
const scriptPath = join(tmpdir(), `mcp-ahk-${Date.now()}.ahk`);
try {
// Write the script to a temporary file
writeFileSync(scriptPath, script, 'utf8');
// Execute the script with AutoHotkey v2
const autohotkeyPath = getAutoHotkeyPath();
execSync(`"${autohotkeyPath}" "${scriptPath}"`, { stdio: 'pipe' });
} finally {
// Clean up the temporary script file
try {
unlinkSync(scriptPath);
} catch {
// Ignore cleanup errors
}
}
}
/**
* Convert key name to AutoHotkey format
*/
private formatKey(key: string): string {
const keyMap: Record<string, string> = {
control: 'Ctrl',
ctrl: 'Ctrl',
shift: 'Shift',
alt: 'Alt',
meta: 'LWin',
windows: 'LWin',
enter: 'Enter',
return: 'Enter',
escape: 'Escape',
esc: 'Escape',
backspace: 'Backspace',
delete: 'Delete',
tab: 'Tab',
space: 'Space',
up: 'Up',
down: 'Down',
left: 'Left',
right: 'Right',
home: 'Home',
end: 'End',
pageup: 'PgUp',
pagedown: 'PgDn',
f1: 'F1',
f2: 'F2',
f3: 'F3',
f4: 'F4',
f5: 'F5',
f6: 'F6',
f7: 'F7',
f8: 'F8',
f9: 'F9',
f10: 'F10',
f11: 'F11',
f12: 'F12',
};
const lowerKey = key.toLowerCase();
return keyMap[lowerKey] || key;
}
typeText(input: KeyboardInput): WindowsControlResponse {
try {
// Validate text
if (!input.text) {
throw new Error('Text is required');
}
if (input.text.length > MAX_TEXT_LENGTH) {
throw new Error(`Text too long: ${input.text.length} characters (max ${MAX_TEXT_LENGTH})`);
}
// Escape special characters for AutoHotkey
const escapedText = input.text
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/`/g, '``')
.replace(/{/g, '{{')
.replace(/}/g, '}}');
const script = `
SendText("${escapedText}")
ExitApp
`;
this.executeScript(script);
return {
success: true,
message: `Typed text successfully`,
};
} catch (error) {
return {
success: false,
message: `Failed to type text: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
pressKey(key: string): WindowsControlResponse {
try {
// Validate key
KeySchema.parse(key);
const formattedKey = this.formatKey(key);
const script = `
Send("{${formattedKey}}")
ExitApp
`;
this.executeScript(script);
return {
success: true,
message: `Pressed key: ${key}`,
};
} catch (error) {
return {
success: false,
message: `Failed to press key: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async pressKeyCombination(combination: KeyCombination): Promise<WindowsControlResponse> {
try {
// Validate combination
KeyCombinationSchema.parse(combination);
// Build the key combination string
const keys = combination.keys.map((key) => this.formatKey(key));
const comboString = keys.join('+');
const script = `
Send("{${comboString}}")
ExitApp
`;
this.executeScript(script);
return {
success: true,
message: `Pressed key combination: ${combination.keys.join('+')}`,
};
} catch (error) {
return {
success: false,
message: `Failed to press key combination: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse> {
try {
// Validate operation
KeyHoldOperationSchema.parse(operation);
const formattedKey = this.formatKey(operation.key);
const script =
operation.state === 'up'
? `
Send("{${formattedKey} up}")
ExitApp
`
: `
Send("{${formattedKey} down}")
ExitApp
`;
this.executeScript(script);
return {
success: true,
message:
operation.state === 'up'
? `Released key: ${operation.key}`
: `Holding key: ${operation.key}`,
};
} catch (error) {
return {
success: false,
message: `Failed to ${operation.state === 'up' ? 'release' : 'hold'} key: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { setupTools } from './handlers/tools.js';
import { loadConfig } from './config.js';
import { createAutomationProvider } from './providers/factory.js';
import { AutomationProvider } from './interfaces/provider.js';
import { createHttpServer, DEFAULT_PORT } from './server.js';
class MCPControlServer {
private useSse: boolean;
private port?: number;
private server: Server;
private useHttps: boolean;
private certPath?: string;
private keyPath?: string;
/**
* Automation provider instance used for system interaction
* The provider implements keyboard, mouse, screen, and clipboard functionality
* through a consistent interface allowing for different backend implementations
*/
private provider: AutomationProvider;
/**
* HTTP server instance if SSE mode is enabled
*/
private httpServer?: ReturnType<typeof createHttpServer>;
constructor(
useSse: boolean,
port?: number,
useHttps = false,
certPath?: string,
keyPath?: string,
) {
this.useSse = useSse;
this.port = port;
this.useHttps = useHttps;
this.certPath = certPath;
this.keyPath = keyPath;
try {
// Load configuration
const config = loadConfig();
// Validate configuration
if (!config || typeof config.provider !== 'string') {
throw new Error('Invalid configuration: provider property is missing or invalid');
}
// Validate that the provider is supported
const supportedProviders = ['keysender']; // add others as they become available
if (!supportedProviders.includes(config.provider.toLowerCase())) {
throw new Error(
`Unsupported provider: ${config.provider}. Supported providers: ${supportedProviders.join(', ')}`,
);
}
// Create automation provider based on configuration
this.provider = createAutomationProvider(config);
this.server = new Server(
{
name: 'mcp-control',
version: '0.1.21-alpha.2',
},
{
capabilities: {
tools: {},
},
},
);
this.setupHandlers();
this.setupErrorHandling();
} catch (error) {
// Using process.stderr.write to avoid affecting the JSON-RPC stream
process.stderr.write(
`Failed to initialize MCP Control Server: ${error instanceof Error ? error.message : String(error)}\n`,
);
// Log additional shutdown information
process.stderr.write('Server initialization failed. Application will now exit.\n');
// Exit with non-zero status to indicate error
process.exit(1);
}
}
private setupHandlers(): void {
// Set up tools with Zod validation
setupTools(this.server, this.provider);
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
// Using process.stderr.write to avoid affecting the JSON-RPC stream
process.stderr.write(
`[MCP Error] ${error instanceof Error ? error.message : String(error)}\n`,
);
};
process.on('SIGINT', () => {
if (this.httpServer) {
this.httpServer.httpServer.close();
}
void this.server.close().then(() => process.exit(0));
});
}
async run(): Promise<void> {
// Create the StdioServerTransport for standard MCP communication
const transport = new StdioServerTransport();
await this.server.connect(transport);
// Using process.stderr.write to avoid affecting the JSON-RPC stream
process.stderr.write(
`MCP Control server running on stdio (using ${this.provider.constructor.name})\n`,
);
// Start HTTP server with SSE support if requested
if (this.useSse) {
const port = this.port ?? DEFAULT_PORT;
try {
this.httpServer = createHttpServer(
this.server,
port,
this.useHttps,
this.certPath,
this.keyPath,
);
} catch (error) {
process.stderr.write(
`Failed to create ${this.useHttps ? 'HTTPS' : 'HTTP'} server: ${
error instanceof Error ? error.message : String(error)
}\n`,
);
void this.server.close().then(() => process.exit(1));
return;
}
// Set up error handler for HTTP server
this.httpServer.httpServer.on('error', (err) => {
process.stderr.write(`Failed to start HTTP server: ${err.message}\n`);
});
const protocol = this.useHttps ? 'HTTPS' : 'HTTP';
process.stderr.write(`${protocol}/SSE server enabled on port ${port}\n`);
}
}
}
// Parse CLI flags for SSE mode and port
const args = process.argv.slice(2);
const useSse = args.includes('--sse');
const useHttps = args.includes('--https');
let port: number | undefined;
let certPath: string | undefined;
let keyPath: string | undefined;
// Parse port
const portIndex = args.indexOf('--port');
if (portIndex >= 0 && args[portIndex + 1]) {
const parsed = parseInt(args[portIndex + 1], 10);
if (!Number.isNaN(parsed)) {
port = parsed;
}
}
// Parse certificate and key paths for HTTPS
const certIndex = args.indexOf('--cert');
if (certIndex >= 0 && args[certIndex + 1]) {
certPath = args[certIndex + 1];
}
const keyIndex = args.indexOf('--key');
if (keyIndex >= 0 && args[keyIndex + 1]) {
keyPath = args[keyIndex + 1];
}
// Validate HTTPS configuration
if (useHttps && (!certPath || !keyPath)) {
process.stderr.write('Error: --cert and --key are required when using --https\n');
process.exit(1);
}
const server = new MCPControlServer(useSse, port, useHttps, certPath, keyPath);
server.run().catch((err) => {
// Using process.stderr.write to avoid affecting the JSON-RPC stream
process.stderr.write(
`Error starting server: ${err instanceof Error ? err.message : String(err)}\n`,
);
});
```
--------------------------------------------------------------------------------
/src/providers/keysender/keyboard.ts:
--------------------------------------------------------------------------------
```typescript
import pkg from 'keysender';
const { Hardware } = pkg;
// Define keyboard button type directly
type KeyboardButtonType = string;
import { KeyboardInput, KeyCombination, KeyHoldOperation } from '../../types/common.js';
import { WindowsControlResponse } from '../../types/responses.js';
import { KeyboardAutomation } from '../../interfaces/automation.js';
import {
MAX_TEXT_LENGTH,
KeySchema,
VALID_KEYS,
KeyCombinationSchema,
KeyHoldOperationSchema,
} from '../../tools/validation.zod.js';
import { createLogger } from '../../logger.js';
/**
* Keysender implementation of the KeyboardAutomation interface
*/
export class KeysenderKeyboardAutomation implements KeyboardAutomation {
private keyboard = new Hardware().keyboard;
private logger = createLogger('keysender:keyboard');
typeText(input: KeyboardInput): WindowsControlResponse {
try {
// Validate text
if (!input.text) {
throw new Error('Text is required');
}
if (input.text.length > MAX_TEXT_LENGTH) {
throw new Error(`Text too long: ${input.text.length} characters (max ${MAX_TEXT_LENGTH})`);
}
// Start the asynchronous operation and handle errors properly
this.keyboard.printText(input.text).catch((err) => {
this.logger.error('Error typing text', err);
// We can't update the response after it's returned, but at least log the error
});
return {
success: true,
message: `Typed text successfully`,
};
} catch (error) {
return {
success: false,
message: `Failed to type text: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
pressKey(key: string): WindowsControlResponse {
try {
// Validate the key using Zod schema
KeySchema.parse(key);
const keyboardKey = this._findMatchingString(key, VALID_KEYS);
// Start the asynchronous operation and handle errors properly
this.keyboard.sendKey(keyboardKey).catch((err) => {
this.logger.error(`Error pressing key ${key}`, err);
// We can't update the response after it's returned, but at least log the error
});
return {
success: true,
message: `Pressed key: ${key}`,
};
} catch (error) {
return {
success: false,
message: `Failed to press key: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
_findMatchingString(A: KeyboardButtonType, ButtonList: KeyboardButtonType[]): KeyboardButtonType {
const lowerA = A.toLowerCase();
return ButtonList.filter((item) => lowerA == item.toLowerCase())[0];
}
async pressKeyCombination(combination: KeyCombination): Promise<WindowsControlResponse> {
try {
// Validate the key combination using Zod schema
KeyCombinationSchema.parse(combination);
// Store original keys for the message
const keysForMessage = [...combination.keys];
// Validate each key and collect press
const validatedKeys: KeyboardButtonType[] = [];
for (const key of combination.keys) {
KeySchema.parse(key);
const keyboardKey = this._findMatchingString(key, VALID_KEYS);
validatedKeys.push(keyboardKey);
}
await this.keyboard.toggleKey(validatedKeys, true, 50).catch((err) => {
this.logger.error('Error pressing keys', err);
throw err; // Re-throw to be caught by the outer try/catch
});
await this.keyboard.toggleKey(validatedKeys, false, 50).catch((err) => {
this.logger.error('Error releasing keys', err);
throw err; // Re-throw to be caught by the outer try/catch
});
return {
success: true,
message: `Pressed key combination: ${keysForMessage.join('+')}`,
};
} catch (error) {
// Ensure all keys are released in case of error
try {
const cleanupPromises: Promise<void>[] = [];
for (const key of combination.keys) {
try {
KeySchema.parse(key);
const keyboardKey = this._findMatchingString(key, VALID_KEYS);
cleanupPromises.push(
this.keyboard.toggleKey(keyboardKey, false).catch((err) => {
this.logger.error(`Error releasing key ${key} during cleanup`, err);
// Ignore errors during cleanup
}),
);
} catch (validationError) {
this.logger.error(`Error validating key ${key} during cleanup`, validationError);
// Continue with other keys
}
}
await Promise.all(cleanupPromises);
} catch {
// Ignore errors during cleanup
}
return {
success: false,
message: `Failed to press key combination: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
async holdKey(operation: KeyHoldOperation): Promise<WindowsControlResponse> {
try {
// Validate key hold operation using Zod schema
KeyHoldOperationSchema.parse(operation);
// Toggle the key state (down/up)
await this.keyboard.toggleKey(operation.key, operation.state === 'down');
// If it's a key press (down) with duration, wait for the specified duration then release
if (operation.state === 'down' && operation.duration) {
await new Promise((resolve) => setTimeout(resolve, operation.duration));
await this.keyboard.toggleKey(operation.key, false);
}
return {
success: true,
message: `Key ${operation.key} ${operation.state === 'down' ? 'held' : 'released'} successfully${
operation.state === 'down' && operation.duration ? ` for ${operation.duration}ms` : ''
}`,
};
} catch (error) {
// Ensure key is released in case of error during hold
if (operation.state === 'down') {
try {
await this.keyboard.toggleKey(operation.key, false);
} catch (releaseError) {
this.logger.error(`Error releasing key ${operation.key} during cleanup`, releaseError);
// Ignore errors during cleanup
}
}
return {
success: false,
message: `Failed to ${operation.state} key ${operation.key}: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import express from 'express';
import * as http from 'http';
import * as https from 'https';
import * as fs from 'fs';
import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { networkInterfaces } from 'os';
import { createLogger } from './logger.js';
/**
* Default port for the SSE server
*/
export const DEFAULT_PORT = 3232;
/**
* Maximum number of SSE clients that can connect simultaneously
* Can be overridden with MAX_SSE_CLIENTS environment variable
*/
const MAX_SSE_CLIENTS = parseInt(process.env.MAX_SSE_CLIENTS || '100', 10);
/**
* Creates and configures an HTTP server with SSE support
* @param mcpServer The MCP server instance to connect with
* @param port The port to listen on (default: DEFAULT_PORT)
* @param useHttps Whether to use HTTPS (default: false)
* @param certPath Path to TLS certificate (only used when useHttps is true)
* @param keyPath Path to TLS key (only used when useHttps is true)
* @returns Object containing the express app, http server
*/
// Create a logger for server module
const logger = createLogger('server');
export function createHttpServer(
mcpServer: MCPServer,
port = DEFAULT_PORT,
useHttps = false,
certPath?: string,
keyPath?: string,
): {
app: ReturnType<typeof express>;
httpServer: http.Server | https.Server;
} {
// Create the Express app
const app = express();
// Create server based on protocol
let httpServer: http.Server | https.Server;
if (useHttps) {
if (!certPath || !keyPath) {
throw new Error('Certificate and key paths are required for HTTPS');
}
// Validate certificate files exist
if (!fs.existsSync(certPath)) {
throw new Error(`Certificate file not found: ${certPath}`);
}
if (!fs.existsSync(keyPath)) {
throw new Error(`Key file not found: ${keyPath}`);
}
try {
const httpsOptions = {
cert: fs.readFileSync(certPath),
key: fs.readFileSync(keyPath),
// Security options
minVersion: 'TLSv1.2' as const,
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
httpServer = https.createServer(httpsOptions, app);
} catch (error) {
throw new Error(
`Failed to load TLS certificates: ${error instanceof Error ? error.message : String(error)}`,
);
}
} else {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
httpServer = http.createServer(app);
}
// Track active transports by session ID
const transports: Record<string, SSEServerTransport> = {};
const endpoint = '/mcp';
// Client limit enforcement middleware
app.use(endpoint, (req, res, next) => {
if (req.method === 'GET' && Object.keys(transports).length >= MAX_SSE_CLIENTS) {
res.status(503).json({
success: false,
message: `Maximum number of SSE clients (${MAX_SSE_CLIENTS}) reached`,
});
return;
}
next();
});
// SSE connection endpoint
app.get(endpoint, async (req, res) => {
try {
const transport = new SSEServerTransport(endpoint, res);
const sessionId = transport.sessionId;
// Store transport and set up cleanup
transports[sessionId] = transport;
transport.onclose = () => {
delete transports[sessionId];
};
// Connect to MCP server
await mcpServer.connect(transport);
} catch (error) {
logger.error(
`Error establishing SSE stream: ${
error instanceof Error ? error.message : String(error)
}`
);
if (!res.headersSent) {
res.status(500).send('Error establishing SSE stream');
}
}
});
// HTTP endpoint for bidirectional communication
app.post(endpoint, async (req, res) => {
const sessionId = req.query.sessionId as string;
if (!sessionId) {
res.status(400).send('Missing sessionId parameter');
return;
}
const transport = transports[sessionId];
if (!transport) {
res.status(404).send('Session not found');
return;
}
try {
await transport.handlePostMessage(req, res, req.body);
} catch (error) {
logger.error(
`Error handling request: ${
error instanceof Error ? error.message : String(error)
}`
);
if (!res.headersSent) {
res.status(500).send('Error handling request');
}
}
});
// Simple metrics endpoint
app.get('/metrics', (req, res) => {
try {
const metrics = [
'# HELP mcp_sse_connections_active Current number of active SSE connections',
'# TYPE mcp_sse_connections_active gauge',
`mcp_sse_connections_active ${Object.keys(transports).length}`,
].join('\n');
res.set('Content-Type', 'text/plain; version=0.0.4');
res.send(metrics);
} catch (error) {
logger.error('Error generating metrics', error);
res.status(500).send('Error generating metrics');
}
});
// Start listening
httpServer.listen(port, '0.0.0.0', () => {
// Log that the server is running
const protocol = useHttps ? 'HTTPS' : 'HTTP';
logger.info(`${protocol} server running on port ${port} with SSE support`);
// Display all available network interfaces
try {
const addresses: string[] = [];
// Collect all non-internal IPv4 addresses
const interfaces = networkInterfaces();
Object.keys(interfaces).forEach((ifaceName) => {
const iface = interfaces[ifaceName];
if (iface) {
iface.forEach((details) => {
if (details.family === 'IPv4' && !details.internal) {
addresses.push(details.address);
}
});
}
});
// Display connection URLs
const scheme = useHttps ? 'https' : 'http';
logger.info(`Local URL: ${scheme}://localhost:${port}${endpoint}`);
if (addresses.length > 0) {
logger.info('Available on:');
addresses.forEach((ip) => {
logger.info(` ${scheme}://${ip}:${port}${endpoint}`);
});
}
} catch (err) {
const scheme = useHttps ? 'https' : 'http';
logger.info(`Local URL: ${scheme}://localhost:${port}${endpoint}`);
logger.error('Failed to get network interfaces', err);
}
});
return {
app,
httpServer,
};
}
```
--------------------------------------------------------------------------------
/src/tools/screen.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// Mock the provider
vi.mock('../providers/factory.js', () => ({
createAutomationProvider: () => ({
screen: {
getScreenSize: vi.fn().mockReturnValue({
success: true,
message: 'Screen size retrieved successfully',
data: {
width: 1920,
height: 1080,
},
}),
getActiveWindow: vi.fn().mockReturnValue({
success: true,
message: 'Active window information retrieved successfully',
data: {
title: 'Test Window',
position: { x: 10, y: 20 },
size: { width: 800, height: 600 },
},
}),
focusWindow: vi.fn().mockImplementation((title) => {
if (title === 'Target') {
return {
success: true,
message: 'Successfully focused window: Target',
};
} else if (title === 'Nonexistent') {
return {
success: false,
message: 'Could not find window with title: Nonexistent',
};
} else {
return {
success: false,
message: 'Failed to focus window: Cannot list windows',
};
}
}),
resizeWindow: vi.fn().mockImplementation((title, width, height) => {
if (title === 'Target') {
return {
success: true,
message: `Successfully resized window: Target to ${width}x${height}`,
};
} else if (title === 'Nonexistent') {
return {
success: false,
message: 'Could not find window with title: Nonexistent',
};
} else {
return {
success: false,
message: 'Failed to resize window: Cannot list windows',
};
}
}),
repositionWindow: vi.fn().mockImplementation((title, x, y) => {
if (title === 'Target') {
return {
success: true,
message: `Successfully repositioned window: Target to (${x},${y})`,
};
} else if (title === 'Nonexistent') {
return {
success: false,
message: 'Could not find window with title: Nonexistent',
};
} else {
return {
success: false,
message: 'Failed to reposition window: Cannot list windows',
};
}
}),
},
}),
}));
import {
getScreenSize,
getActiveWindow,
focusWindow,
resizeWindow,
repositionWindow,
} from './screen.js';
describe('Screen Functions', () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
describe('getScreenSize', () => {
it('should return screen dimensions on success', () => {
// Execute
const result = getScreenSize();
// Verify
expect(result).toEqual({
success: true,
message: 'Screen size retrieved successfully',
data: {
width: 1920,
height: 1080,
},
});
});
});
describe('getActiveWindow', () => {
it('should return active window information on success', () => {
// Execute
const result = getActiveWindow();
// Verify
expect(result).toEqual({
success: true,
message: 'Active window information retrieved successfully',
data: {
title: 'Test Window',
position: { x: 10, y: 20 },
size: { width: 800, height: 600 },
},
});
});
});
describe('focusWindow', () => {
it('should focus window with matching title', () => {
// Execute
const result = focusWindow('Target');
// Verify
expect(result).toEqual({
success: true,
message: 'Successfully focused window: Target',
});
});
it('should return error when window with title is not found', () => {
// Execute
const result = focusWindow('Nonexistent');
// Verify
expect(result).toEqual({
success: false,
message: 'Could not find window with title: Nonexistent',
});
});
it('should return error response when focus operation fails', () => {
// Execute
const result = focusWindow('Any');
// Verify
expect(result).toEqual({
success: false,
message: 'Failed to focus window: Cannot list windows',
});
});
});
describe('resizeWindow', () => {
it('should resize window with matching title', async () => {
// Execute
const result = await resizeWindow('Target', 1024, 768);
// Verify
expect(result).toEqual({
success: true,
message: 'Successfully resized window: Target to 1024x768',
});
});
it('should return error when window with title is not found', async () => {
// Execute
const result = await resizeWindow('Nonexistent', 1024, 768);
// Verify
expect(result).toEqual({
success: false,
message: 'Could not find window with title: Nonexistent',
});
});
it('should return error response when resize operation fails', async () => {
// Execute
const result = await resizeWindow('Any', 1024, 768);
// Verify
expect(result).toEqual({
success: false,
message: 'Failed to resize window: Cannot list windows',
});
});
});
describe('repositionWindow', () => {
it('should reposition window with matching title', async () => {
// Execute
const result = await repositionWindow('Target', 100, 200);
// Verify
expect(result).toEqual({
success: true,
message: 'Successfully repositioned window: Target to (100,200)',
});
});
it('should return error when window with title is not found', async () => {
// Execute
const result = await repositionWindow('Nonexistent', 100, 200);
// Verify
expect(result).toEqual({
success: false,
message: 'Could not find window with title: Nonexistent',
});
});
it('should return error response when reposition operation fails', async () => {
// Execute
const result = await repositionWindow('Any', 100, 200);
// Verify
expect(result).toEqual({
success: false,
message: 'Failed to reposition window: Cannot list windows',
});
});
});
});
```
--------------------------------------------------------------------------------
/test/test-server.js:
--------------------------------------------------------------------------------
```javascript
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { parse as parseUrl } from 'url';
// Get the directory name correctly in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Test results that will be collected during testing
let testResults = {
buttonClicks: [],
sequences: [],
finalSequence: null,
startTime: new Date().toISOString(),
};
// Create a simple HTTP server
const server = http.createServer((req, res) => {
const parsedUrl = parseUrl(req.url, true);
const pathname = parsedUrl.pathname;
// Handle API endpoints
if (pathname === '/api/test-data') {
if (req.method === 'POST') {
// Collect test data from the client
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
// If this is a click event
if (data.type === 'click') {
testResults.buttonClicks.push({
timestamp: new Date().toISOString(),
buttonId: data.buttonId,
count: data.count,
});
// Format click event with colored output
console.log(`\x1b[38;2;127;187;255mClicked: ${data.buttonId}\x1b[0m`);
}
// If this is a sequence update
if (data.type === 'sequence') {
testResults.sequences.push({
timestamp: new Date().toISOString(),
sequence: data.sequence,
});
// Removed sequence logging to simplify output
}
// If this is the final result
if (data.type === 'final') {
testResults.finalSequence = data.sequence;
testResults.endTime = new Date().toISOString();
// Write the results to a file for the test script to read
fs.writeFileSync(
path.join(__dirname, 'test-results.json'),
JSON.stringify(testResults, null, 2),
);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
} catch (error) {
console.error('Error processing data:', error);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: error.message }));
}
});
} else if (req.method === 'GET') {
// Return the current test results
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(testResults));
} else {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Method not allowed' }));
}
return;
}
// Reset test results endpoint
if (pathname === '/api/reset') {
testResults = {
buttonClicks: [],
sequences: [],
finalSequence: null,
startTime: new Date().toISOString(),
};
// Remove existing results file if it exists
const resultsPath = path.join(__dirname, 'test-results.json');
if (fs.existsSync(resultsPath)) {
fs.unlinkSync(resultsPath);
}
console.log('Test results reset');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
return;
}
// Serve the test panel HTML
if (pathname === '/' || pathname === '/index.html') {
fs.readFile(path.join(__dirname, '..', 'test-panel.html'), 'utf8', (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal server error');
return;
}
// Insert our API client code
const apiClientCode = `
// API client for test automation
window.testAPI = {
reportClick: function(buttonId, count) {
fetch('/api/test-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'click',
buttonId: buttonId,
count: count
})
}).catch(console.error);
},
reportSequence: function(sequence) {
fetch('/api/test-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'sequence',
sequence: sequence.join ? sequence.join('') : sequence
})
}).catch(console.error);
},
reportFinalResult: function(sequence) {
fetch('/api/test-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'final',
sequence: sequence.join ? sequence.join('') : sequence
})
}).catch(console.error);
}
};
`;
// Insert the API client code before the closing body tag
const modifiedData = data.replace('</body>', `<script>${apiClientCode}</script></body>`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(modifiedData);
});
return;
}
// Handle 404 - Not Found
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
});
// Check if a port is in use
function isPortAvailable(port) {
return new Promise((resolve) => {
import('net')
.then(({ default: net }) => {
const tester = net
.createServer()
.once('error', () => resolve(false))
.once('listening', () => {
tester.close();
resolve(true);
})
.listen(port);
})
.catch(() => resolve(false));
});
}
// Find an available port and start the server
async function startServer() {
const preferredPorts = [3000, 3001, 3002, 3003, 3004, 3005, 8080, 8081, 8082];
let port = process.env.PORT;
// If PORT is not set, try to find an available port
if (!port) {
for (const preferredPort of preferredPorts) {
if (await isPortAvailable(preferredPort)) {
port = preferredPort;
break;
}
}
}
// If we still don't have a port, use a random high port
if (!port) {
port = Math.floor(Math.random() * 16384) + 49152; // Random port between 49152 and 65535
}
server.listen(port, () => {
// Write the port to a file for the test script to read
fs.writeFileSync(path.join(__dirname, 'server-port.txt'), port.toString());
});
}
// Start the server
startServer().catch((err) => {
console.error('Failed to start server:', err);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/tools/validation.zod.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
// Constants for validation
export const MAX_TEXT_LENGTH = 1000;
export const MAX_ALLOWED_COORDINATE = 10000; // Reasonable maximum screen dimension
export const MAX_SCROLL_AMOUNT = 1000;
/**
* List of allowed keyboard keys for validation
*/
export const VALID_KEYS = [
// copied from keysender
'backspace',
'tab',
'enter',
'pause',
'capsLock',
'escape',
'space',
'pageUp',
'pageDown',
'end',
'home',
'left',
'up',
'right',
'down',
'printScreen',
'insert',
'delete',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'num0',
'num1',
'num2',
'num3',
'num4',
'num5',
'num6',
'num7',
'num8',
'num9',
'num*',
'num+',
'num,',
'num-',
'num.',
'num/',
'f1',
'f2',
'f3',
'f4',
'f5',
'f6',
'f7',
'f8',
'f9',
'f10',
'f11',
'f12',
'f13',
'f14',
'f15',
'f16',
'f17',
'f18',
'f19',
'f20',
'f21',
'f22',
'f23',
'f24',
'numLock',
'scrollLock',
';',
'=',
',',
'-',
'.',
'/',
'`',
'[',
'\\',
']',
"'",
'alt',
'ctrl',
'shift',
'lShift',
'rShift',
'lCtrl',
'rCtrl',
'lAlt',
'rAlt',
'lWin',
'rWin',
];
export const VALID_KEYS_lowercase = VALID_KEYS.map((element) => element.toLowerCase());
/**
* Zod schema for mouse position validation
*/
export const MousePositionSchema = z.object({
x: z
.number()
.min(-MAX_ALLOWED_COORDINATE, `X coordinate cannot be less than -${MAX_ALLOWED_COORDINATE}`)
.max(MAX_ALLOWED_COORDINATE, `X coordinate cannot be more than ${MAX_ALLOWED_COORDINATE}`)
.refine((val) => !isNaN(val), 'X coordinate cannot be NaN'),
y: z
.number()
.min(-MAX_ALLOWED_COORDINATE, `Y coordinate cannot be less than -${MAX_ALLOWED_COORDINATE}`)
.max(MAX_ALLOWED_COORDINATE, `Y coordinate cannot be more than ${MAX_ALLOWED_COORDINATE}`)
.refine((val) => !isNaN(val), 'Y coordinate cannot be NaN'),
});
/**
* Zod schema for mouse button validation
*/
export const MouseButtonSchema = z.enum(['left', 'right', 'middle']);
/**
* Zod schema for keyboard key validation
*/
export const KeySchema = z.string().refine(
(key) => VALID_KEYS_lowercase.includes(key.toLowerCase()),
(key) => ({ message: `Invalid key: "${key}". Must be one of the allowed keys.` }),
);
/**
* Helper function to detect dangerous key combinations
*/
function isDangerousKeyCombination(keys: string[]): string | null {
const normalizedKeys = keys.map((k) => k.toLowerCase());
// Explicitly allow common copy/paste shortcuts
if (
normalizedKeys.length === 2 &&
normalizedKeys.includes('ctrl') &&
(normalizedKeys.includes('c') || normalizedKeys.includes('v') || normalizedKeys.includes('x'))
) {
return null;
}
// Check for OS-level dangerous combinations
if (normalizedKeys.includes('command') || normalizedKeys.includes('ctrl')) {
// Control+Alt+Delete or Command+Option+Esc (Force Quit on Mac)
if (
(normalizedKeys.includes('ctrl') &&
normalizedKeys.includes('alt') &&
normalizedKeys.includes('delete')) ||
(normalizedKeys.includes('ctrl') &&
normalizedKeys.includes('shift') &&
normalizedKeys.includes('escape'))
) {
return 'This combination can trigger system functions';
}
}
return null;
}
/**
* Zod schema for key combination validation
*/
export const KeyCombinationSchema = z.object({
keys: z
.array(KeySchema)
.min(1, 'Key combination must contain at least one key')
.max(5, 'Too many keys in combination (max 5)')
.refine(
(keys) => {
const dangerous = isDangerousKeyCombination(keys);
return dangerous === null;
},
(keys) => ({
message: `Potentially dangerous key combination: ${keys.join('+')}. ${isDangerousKeyCombination(keys)}`,
}),
),
});
/**
* Zod schema for keyboard input validation
*/
export const KeyboardInputSchema = z.object({
text: z.string().max(MAX_TEXT_LENGTH, `Text length cannot exceed ${MAX_TEXT_LENGTH} characters`),
});
/**
* Zod schema for key hold operation validation
*/
export const KeyHoldOperationSchema = z
.object({
key: KeySchema,
state: z.enum(['down', 'up']),
duration: z
.number()
.min(0, 'Duration cannot be negative')
.max(10000, 'Duration too long: maximum is 10000ms (10 seconds)')
.optional(),
})
.strict() // Ensure no extra properties
.superRefine((data, ctx) => {
// Check for 'down' state
if (data.state === 'down') {
if (data.duration === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Duration is required for key down operations',
path: ['duration'],
});
} else if (data.duration < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Duration for key down operations must be at least 1ms',
path: ['duration'],
});
}
}
})
// Empty objects should always fail validation
.refine(
(data) => {
return Object.keys(data).length > 0;
},
{
message: 'Missing required properties',
},
);
/**
* Zod schema for scroll amount validation
*/
export const ScrollAmountSchema = z
.number()
.min(-MAX_SCROLL_AMOUNT, `Scroll amount cannot be less than -${MAX_SCROLL_AMOUNT}`)
.max(MAX_SCROLL_AMOUNT, `Scroll amount cannot be more than ${MAX_SCROLL_AMOUNT}`)
.refine((val) => !isNaN(val), 'Scroll amount cannot be NaN');
/**
* Zod schema for clipboard input validation
*/
export const ClipboardInputSchema = z.object({
text: z.string().max(MAX_TEXT_LENGTH, `Text length cannot exceed ${MAX_TEXT_LENGTH} characters`),
});
/**
* Zod schema for screenshot region validation
*/
export const ScreenshotRegionSchema = z.object({
x: z.number().int(),
y: z.number().int(),
width: z.number().int().positive('Width must be positive'),
height: z.number().int().positive('Height must be positive'),
});
/**
* Zod schema for screenshot resize options validation
*/
export const ScreenshotResizeSchema = z.object({
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
fit: z.enum(['contain', 'cover', 'fill', 'inside', 'outside']).optional(),
});
/**
* Zod schema for screenshot options validation
*/
export const ScreenshotOptionsSchema = z.object({
region: ScreenshotRegionSchema.optional(),
quality: z.number().int().min(1).max(100).optional(),
format: z.enum(['png', 'jpeg']).optional(),
grayscale: z.boolean().optional(),
resize: ScreenshotResizeSchema.optional(),
compressionLevel: z.number().int().min(0).max(9).optional(),
});
/**
* Zod schema for window info validation
*/
export const WindowInfoSchema = z.object({
title: z.string(),
position: z.object({
x: z.number().int(),
y: z.number().int(),
}),
size: z.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
}),
});
```
--------------------------------------------------------------------------------
/src/tools/validation.zod.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest';
import {
MousePositionSchema,
MouseButtonSchema,
KeySchema,
KeyCombinationSchema,
KeyHoldOperationSchema,
ScrollAmountSchema,
ClipboardInputSchema,
ScreenshotOptionsSchema,
} from './validation.zod.js';
import { MAX_ALLOWED_COORDINATE, MAX_SCROLL_AMOUNT } from './validation.zod.js';
describe('Zod Validation Schemas', () => {
describe('MousePositionSchema', () => {
it('should validate valid mouse positions', () => {
expect(() => MousePositionSchema.parse({ x: 100, y: 200 })).not.toThrow();
expect(() => MousePositionSchema.parse({ x: -100, y: -200 })).not.toThrow();
expect(() => MousePositionSchema.parse({ x: 0, y: 0 })).not.toThrow();
expect(() =>
MousePositionSchema.parse({ x: MAX_ALLOWED_COORDINATE, y: MAX_ALLOWED_COORDINATE }),
).not.toThrow();
expect(() =>
MousePositionSchema.parse({ x: -MAX_ALLOWED_COORDINATE, y: -MAX_ALLOWED_COORDINATE }),
).not.toThrow();
});
it('should reject invalid mouse positions', () => {
expect(() => MousePositionSchema.parse({ x: 'invalid', y: 200 })).toThrow();
expect(() => MousePositionSchema.parse({ x: 100, y: NaN })).toThrow();
expect(() => MousePositionSchema.parse({ x: MAX_ALLOWED_COORDINATE + 1, y: 200 })).toThrow();
expect(() => MousePositionSchema.parse({ x: 100, y: -MAX_ALLOWED_COORDINATE - 1 })).toThrow();
expect(() => MousePositionSchema.parse({ x: 100 })).toThrow();
expect(() => MousePositionSchema.parse({ y: 200 })).toThrow();
expect(() => MousePositionSchema.parse({})).toThrow();
expect(() => MousePositionSchema.parse(null)).toThrow();
});
});
describe('MouseButtonSchema', () => {
it('should validate valid mouse buttons', () => {
expect(() => MouseButtonSchema.parse('left')).not.toThrow();
expect(() => MouseButtonSchema.parse('right')).not.toThrow();
expect(() => MouseButtonSchema.parse('middle')).not.toThrow();
});
it('should reject invalid mouse buttons', () => {
expect(() => MouseButtonSchema.parse('invalid')).toThrow();
expect(() => MouseButtonSchema.parse('' as any)).toThrow();
expect(() => MouseButtonSchema.parse(null as any)).toThrow();
expect(() => MouseButtonSchema.parse(undefined as any)).toThrow();
});
});
describe('KeySchema', () => {
it('should validate valid keys', () => {
expect(() => KeySchema.parse('a')).not.toThrow();
expect(() => KeySchema.parse('enter')).not.toThrow();
expect(() => KeySchema.parse('space')).not.toThrow();
expect(() => KeySchema.parse('f1')).not.toThrow(); // Function key
});
it('should reject invalid keys', () => {
expect(() => KeySchema.parse('invalid_key')).toThrow();
expect(() => KeySchema.parse('')).toThrow();
expect(() => KeySchema.parse(null as any)).toThrow();
expect(() => KeySchema.parse(undefined as any)).toThrow();
});
});
describe('KeyCombinationSchema', () => {
it('should validate valid key combinations', () => {
expect(() => KeyCombinationSchema.parse({ keys: ['ctrl', 'c'] })).not.toThrow();
expect(() => KeyCombinationSchema.parse({ keys: ['ctrl', 'shift', 'a'] })).not.toThrow();
expect(() => KeyCombinationSchema.parse({ keys: ['a'] })).not.toThrow();
});
it('should reject invalid key combinations', () => {
expect(() => KeyCombinationSchema.parse({ keys: [] })).toThrow();
expect(() => KeyCombinationSchema.parse({ keys: ['ctrl', 'alt', 'delete'] })).toThrow();
expect(() => KeyCombinationSchema.parse({ keys: ['a', 'b', 'c', 'd', 'e', 'f'] })).toThrow();
expect(() => KeyCombinationSchema.parse({ keys: ['invalid_key'] })).toThrow();
expect(() => KeyCombinationSchema.parse({ keys: 'ctrl+c' })).toThrow();
expect(() => KeyCombinationSchema.parse({})).toThrow();
});
});
describe('KeyHoldOperationSchema', () => {
it('should validate valid key hold operations', () => {
expect(() =>
KeyHoldOperationSchema.parse({ key: 'shift', state: 'down', duration: 1000 }),
).not.toThrow();
expect(() => KeyHoldOperationSchema.parse({ key: 'a', state: 'up' })).not.toThrow();
});
it('should reject invalid key hold operations', () => {
expect(() => KeyHoldOperationSchema.parse({ key: 'shift', state: 'down' })).toThrow();
expect(() =>
KeyHoldOperationSchema.parse({ key: 'invalid', state: 'down', duration: 1000 }),
).toThrow();
expect(() =>
KeyHoldOperationSchema.parse({ key: 'shift', state: 'invalid', duration: 1000 }),
).toThrow();
expect(() =>
KeyHoldOperationSchema.parse({ key: 'shift', state: 'down', duration: 0 }),
).toThrow(); // Changed from 5ms to 0ms
expect(() =>
KeyHoldOperationSchema.parse({ key: 'shift', state: 'down', duration: 20000 }),
).toThrow();
expect(() => KeyHoldOperationSchema.parse({})).toThrow();
});
});
describe('ScrollAmountSchema', () => {
it('should validate valid scroll amounts', () => {
expect(() => ScrollAmountSchema.parse(100)).not.toThrow();
expect(() => ScrollAmountSchema.parse(-100)).not.toThrow();
expect(() => ScrollAmountSchema.parse(0)).not.toThrow();
expect(() => ScrollAmountSchema.parse(MAX_SCROLL_AMOUNT)).not.toThrow();
expect(() => ScrollAmountSchema.parse(-MAX_SCROLL_AMOUNT)).not.toThrow();
});
it('should reject invalid scroll amounts', () => {
expect(() => ScrollAmountSchema.parse(MAX_SCROLL_AMOUNT + 1)).toThrow();
expect(() => ScrollAmountSchema.parse(-MAX_SCROLL_AMOUNT - 1)).toThrow();
expect(() => ScrollAmountSchema.parse(NaN)).toThrow();
expect(() => ScrollAmountSchema.parse('100' as any)).toThrow();
expect(() => ScrollAmountSchema.parse(null as any)).toThrow();
expect(() => ScrollAmountSchema.parse(undefined as any)).toThrow();
});
});
describe('ClipboardInputSchema', () => {
it('should validate valid clipboard inputs', () => {
expect(() => ClipboardInputSchema.parse({ text: 'Test clipboard content' })).not.toThrow();
expect(() => ClipboardInputSchema.parse({ text: '' })).not.toThrow();
});
it('should reject invalid clipboard inputs', () => {
const longText = 'a'.repeat(10000);
expect(() => ClipboardInputSchema.parse({ text: longText })).toThrow();
expect(() => ClipboardInputSchema.parse({ text: 123 as any })).toThrow();
expect(() => ClipboardInputSchema.parse({})).toThrow();
expect(() => ClipboardInputSchema.parse({ other: 'value' })).toThrow();
});
});
describe('ScreenshotOptionsSchema', () => {
it('should validate valid screenshot options', () => {
expect(() =>
ScreenshotOptionsSchema.parse({
region: { x: 100, y: 100, width: 500, height: 500 },
format: 'jpeg',
quality: 90,
grayscale: true,
resize: { width: 1280, fit: 'contain' },
}),
).not.toThrow();
expect(() =>
ScreenshotOptionsSchema.parse({
format: 'png',
compressionLevel: 6,
}),
).not.toThrow();
expect(() => ScreenshotOptionsSchema.parse({})).not.toThrow();
});
it('should reject invalid screenshot options', () => {
expect(() =>
ScreenshotOptionsSchema.parse({
region: { x: 100, y: 100, width: -500, height: 500 },
}),
).toThrow();
expect(() =>
ScreenshotOptionsSchema.parse({
quality: 101,
}),
).toThrow();
expect(() =>
ScreenshotOptionsSchema.parse({
format: 'gif',
}),
).toThrow();
expect(() =>
ScreenshotOptionsSchema.parse({
resize: { width: -100, fit: 'invalid' },
}),
).toThrow();
});
});
});
```
--------------------------------------------------------------------------------
/src/providers/factory.modular.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createAutomationProvider, initializeProviders } from './factory.js';
import { registry } from './registry.js';
import { AutomationConfig } from '../config.js';
// These imports are used in test expectations
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ClipboardAutomation } from '../interfaces/automation.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ClipboardInput } from '../types/common.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { WindowsControlResponse } from '../types/responses.js';
// Mock the individual provider imports
vi.mock('./clipboard/powershell/index.js', () => ({
PowerShellClipboardProvider: vi.fn().mockImplementation(() => ({
getClipboardContent: vi.fn().mockResolvedValue({ success: true, data: 'powershell' }),
setClipboardContent: vi.fn().mockResolvedValue({ success: true }),
hasClipboardText: vi.fn().mockResolvedValue({ success: true, data: true }),
clearClipboard: vi.fn().mockResolvedValue({ success: true }),
})),
}));
vi.mock('./clipboard/clipboardy/index.js', () => ({
ClipboardyProvider: vi.fn().mockImplementation(() => ({
getClipboardContent: vi.fn().mockResolvedValue({ success: true, data: 'clipboardy' }),
setClipboardContent: vi.fn().mockResolvedValue({ success: true }),
hasClipboardText: vi.fn().mockResolvedValue({ success: true, data: true }),
clearClipboard: vi.fn().mockResolvedValue({ success: true }),
})),
}));
// Mock keysender provider to avoid ELF header issue
vi.mock('./keysender/index.js', () => ({
KeysenderProvider: vi.fn().mockImplementation(() => ({
keyboard: {
typeText: vi.fn().mockResolvedValue({ success: true }),
pressKey: vi.fn().mockResolvedValue({ success: true }),
pressKeyCombination: vi.fn().mockResolvedValue({ success: true }),
holdKey: vi.fn().mockResolvedValue({ success: true }),
},
mouse: {
moveMouse: vi.fn().mockResolvedValue({ success: true }),
clickMouse: vi.fn().mockResolvedValue({ success: true }),
doubleClick: vi.fn().mockResolvedValue({ success: true }),
getCursorPosition: vi.fn().mockResolvedValue({ success: true, data: { x: 0, y: 0 } }),
scrollMouse: vi.fn().mockResolvedValue({ success: true }),
dragMouse: vi.fn().mockResolvedValue({ success: true }),
clickAt: vi.fn().mockResolvedValue({ success: true }),
},
screen: {
getScreenSize: vi
.fn()
.mockResolvedValue({ success: true, data: { width: 1920, height: 1080 } }),
getActiveWindow: vi.fn().mockResolvedValue({ success: true, data: {} }),
focusWindow: vi.fn().mockResolvedValue({ success: true }),
resizeWindow: vi.fn().mockResolvedValue({ success: true }),
repositionWindow: vi.fn().mockResolvedValue({ success: true }),
getScreenshot: vi.fn().mockResolvedValue({
success: true,
data: { data: '', format: 'png', width: 1920, height: 1080 },
}),
},
clipboard: {
getClipboardContent: vi.fn().mockResolvedValue({ success: true, data: '' }),
setClipboardContent: vi.fn().mockResolvedValue({ success: true }),
hasClipboardText: vi.fn().mockResolvedValue({ success: true, data: true }),
clearClipboard: vi.fn().mockResolvedValue({ success: true }),
},
})),
}));
describe('Factory with Modular Providers', () => {
beforeEach(() => {
// Clear the registry before each test
const available = registry.getAvailableProviders();
available.clipboards.forEach(() => {
// We need to clear the registry, but the interface doesn't provide a clear method
// This is a limitation we should address
});
vi.clearAllMocks();
});
describe('initializeProviders', () => {
it('should register default providers', () => {
initializeProviders();
const available = registry.getAvailableProviders();
expect(available.clipboards).toContain('powershell');
expect(available.clipboards).toContain('clipboardy');
});
});
describe('createAutomationProvider with modular config', () => {
it('should create composite provider with specified components', () => {
const config: AutomationConfig = {
providers: {
clipboard: 'powershell',
},
};
const provider = createAutomationProvider(config);
expect(provider).toBeDefined();
expect(provider.clipboard).toBeDefined();
expect(provider.keyboard).toBeDefined();
expect(provider.mouse).toBeDefined();
expect(provider.screen).toBeDefined();
});
it('should use PowerShell clipboard when specified', async () => {
const config: AutomationConfig = {
providers: {
clipboard: 'powershell',
},
};
const provider = createAutomationProvider(config);
const result = await provider.clipboard.getClipboardContent();
expect(result.data).toBe('powershell');
});
it('should use clipboardy when specified', async () => {
const config: AutomationConfig = {
providers: {
clipboard: 'clipboardy',
},
};
const provider = createAutomationProvider(config);
const result = await provider.clipboard.getClipboardContent();
expect(result.data).toBe('clipboardy');
});
it('should fall back to default providers for unspecified components', () => {
const config: AutomationConfig = {
providers: {
clipboard: 'powershell',
// keyboard, mouse, screen not specified
},
};
const provider = createAutomationProvider(config);
expect(provider.keyboard).toBeDefined();
expect(provider.mouse).toBeDefined();
expect(provider.screen).toBeDefined();
});
it('should cache composite providers based on configuration', () => {
const config: AutomationConfig = {
providers: {
clipboard: 'powershell',
},
};
const provider1 = createAutomationProvider(config);
const provider2 = createAutomationProvider(config);
expect(provider1).toBe(provider2);
});
it('should create different providers for different configurations', () => {
const config1: AutomationConfig = {
providers: {
clipboard: 'powershell',
},
};
const config2: AutomationConfig = {
providers: {
clipboard: 'clipboardy',
},
};
const provider1 = createAutomationProvider(config1);
const provider2 = createAutomationProvider(config2);
expect(provider1).not.toBe(provider2);
});
});
describe('createAutomationProvider with legacy config', () => {
it('should create keysender provider by default', () => {
// Mock keysender provider to avoid ELF header issue
vi.doMock('./keysender/index.js', () => ({
KeysenderProvider: vi.fn().mockImplementation(() => ({
keyboard: {
typeText: vi.fn(),
pressKey: vi.fn(),
},
mouse: {
moveMouse: vi.fn(),
clickMouse: vi.fn(),
},
screen: {
getScreenSize: vi.fn(),
getActiveWindow: vi.fn(),
},
clipboard: {
getClipboardContent: vi.fn(),
setClipboardContent: vi.fn(),
},
})),
}));
const provider = createAutomationProvider();
expect(provider).toBeDefined();
expect(provider.keyboard).toBeDefined();
expect(provider.mouse).toBeDefined();
expect(provider.screen).toBeDefined();
expect(provider.clipboard).toBeDefined();
});
it('should cache legacy providers', () => {
const provider1 = createAutomationProvider();
const provider2 = createAutomationProvider();
expect(provider1).toBe(provider2);
});
});
});
```