rustdesk-mcp
by Cocos666a
README.md
# rustdesk-mcp
An MCP (Model Context Protocol) server that lets AI agents control remote desktops through the **local RustDesk client** on macOS.
Instead of re-implementing a remote-desktop stack, `rustdesk-mcp` drives the RustDesk app you already have: it launches sessions, captures the session window, and injects keyboard/mouse input — exposing all of it as **21 MCP tools** to any MCP-capable agent (Claude Code, Hermes Agent, …).
```
Agent ──stdio MCP──> rustdesk-mcp ──> local RustDesk.app ──> remote desktop
```
## Features
- **21 tools**: session lifecycle, screenshots, hardware-level input injection, a device book, diagnostics (see [Tools](#tools)).
- **Capture pipeline built for macOS 14+ / 26** — where per-window `CGWindowListCreateImage` was removed: a bundled ScreenCaptureKit helper (`tools/rds_capture.swift`, source included), a cua-driver daemon fallback, and full-screen-capture + crop as last resort.
- **One coordinate contract**: `desktop_*` coordinates are pixels on the most recent `control_screenshot` image, auto-converted to screen points (Retina included).
- **Device book**: persist peers with alias/notes, fuzzy search, discovery from local RustDesk state.
- Passwords are accepted per connection call and **never written to disk**.
## How it works
1. **Connect** — starts the RustDesk GUI if needed and delivers `rustdesk://connect/<peer>` through the `--connect` CLI. The session window is discovered by title and remembered (survives MCP host restarts).
2. **Capture** — grabs the session window image, in order:
- ScreenCaptureKit helper app (`bin/rds-cap2.app`; fast, needs its own Screen Recording grant);
- cua-driver daemon (optional fallback);
- full-screen capture + crop (last resort; slow under LaunchDaemon contexts).
3. **Input** — posts hardware-level CGEvents (needs an Accessibility grant for the Python process) at coordinates mapped from the latest screenshot.
4. **Diagnose** — `rustdesk_status` reports RustDesk install/version, TCC permission state, and capture-route availability with actionable hints.
## Requirements
- macOS 14+ (developed and tested on macOS 26, Apple Silicon).
- [RustDesk](https://rustdesk.com) desktop client installed **and signed in** — this server drives your client, so the IDs it can reach are exactly the ones the app can reach.
- Python 3.11+.
- Xcode Command Line Tools (`swiftc`) — only to build the optional capture helper.
## Install
```bash
git clone https://github.com/Cocos666a/rustdesk-mcp.git rustdesk-mcp
cd rustdesk-mcp
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e .
# Optional but recommended — build the fast capture helper:
bash tools/build_helper.sh
./bin/rds-cap2.app/Contents/MacOS/rds-capture --request # grant Screen Recording
```
### Permissions (macOS, one-time)
Full manual: [docs/MACOS-PERMISSIONS.md](docs/MACOS-PERMISSIONS.md). Summary:
| Permission | Who needs it | Why |
|---|---|---|
| Screen Recording | the Python process running this server; the capture helper | screenshot the session window |
| Accessibility | the Python process running this server | post keyboard/mouse events |
| Accessibility | **RustDesk.app** | forward your keyboard/mouse to the remote |
| Input Monitoring | **RustDesk.app** | capture your local input on modern macOS — without it, video works but remote input is dead |
### Register with your MCP client
Generic stdio config (Claude Desktop, Cursor and other MCP clients):
```json
{
"mcpServers": {
"rustdesk": {
"command": "/absolute/path/to/rustdesk-mcp/bin/rustdesk-mcp"
}
}
}
```
Hermes Agent:
```bash
hermes mcp add rustdesk --command /absolute/path/to/rustdesk-mcp/bin/rustdesk-mcp
```
## Quick start
With the server registered, a typical agent session calls the tools in this order (real names, real arguments):
1. `rustdesk_status()` — RustDesk version, own ID, TCC state, active sessions.
2. `session_connect(peer_id="123456789", password="…")` — opens a session window and returns its `window_id` + bounds.
3. `control_screenshot()` — returns the window image **plus** its pixel size and scale. That image is the coordinate space for every `desktop_*` call.
4. `desktop_click_mouse(x=640, y=320)` — click at pixel (640, 320) of that image.
5. `desktop_typing_text(text="hello")` / `desktop_typing_keys(keys="cmd+shift+t")`.
6. `session_disconnect(peer_id="123456789")` — close the window (the RustDesk process keeps running).
If a screenshot fails, `rustdesk_status()` tells you which capture route is available and which permission is missing.
## Tools
| Tool | Purpose |
|---|---|
| `rustdesk_status` | Environment + permission diagnostics |
| `session_connect` / `session_state` / `session_list` / `session_disconnect` | Session lifecycle (peer id, mode, wait, front, password) |
| `control_portforward` | Open a port-forward session window |
| `control_screenshot` | Capture the session window (image + geometry meta) |
| `desktop_move_mouse` / `desktop_click_mouse` / `desktop_drag_mouse` / `desktop_scroll_mouse` | Mouse control (image-pixel coordinates) |
| `desktop_typing_text` / `desktop_typing_keys` / `desktop_press_keys` / `desktop_paste_text` | Keyboard control (combos like `cmd+shift+t`, key repetition) |
| `desktop_waiting` | Sleep (UI settling) |
| `device_add` / `device_remove` / `device_update` / `device_search` / `device_info` | Local device book |
`peer_id` accepts both RustDesk IDs (`123456789`) and direct IPs (`100.x.y.z`, LAN IPs) — see [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for direct-IP setup on the controlled side.
Runtime state is written to `data/` next to the project and is gitignored; the directory is created on first use. It holds `devices.json` (the device book: peer ids, aliases, notes) and `sessions.json` (window ids and bounds) — no credentials.
## Development
```bash
.venv/bin/pytest -q # unit tests
.venv/bin/python tests/smoke_local.py status # local smoke check
```
## Security
This server is an **input-automation bridge**: anything that can call its tools can move the mouse, type, and read the screen of the remote machine. Run it only for an MCP client you trust, and consider a dedicated RustDesk peer instead of your daily-driver machine.
- **Passwords**: accepted per connection call, forwarded to the RustDesk CLI, and never written to disk. They are redacted (`***`) in the server's stderr log. Note that the `rustdesk://` URL in the tool result echoes the password you passed, so your MCP host's transcript holds it — treat that transcript as sensitive.
- **Clipboard**: `desktop_paste_text` writes to the system clipboard, which overwrites whatever was there, then sends Cmd+V.
- **Device scan**: the device book scanner reads only *file names* under `~/Library/Preferences/com.carriez.RustDesk`; it never parses peer configs or password fields.
- **No config writes**: the CLI wrapper only passes whitelisted arguments, so it cannot change RustDesk settings (`--set-id`, `--option`, `--config`, …).
## Known limitations
- macOS only — implemented against pyobjc/Quartz/CGEvent; no Linux/Windows backend.
- No file transfer / command channel yet (Phase 2).
- Does not modify RustDesk configuration; it only drives the app.
- Public rendezvous reachability depends on your network — some networks (e.g. mainland China) cannot reach RustDesk's public servers; direct IP works fine in that case.
## Alternative: driving a Windows host over RDP (`tools/winexec`)
When macOS refuses to deliver synthetic input (TCC judges the event *sender*, so a grant for `RustDesk.app` does not cover the Python process), the GUI path cannot type or click at all. `tools/winexec/` sidesteps the macOS input layer: a headless FreeRDP client runs in a Linux container, and keystrokes are injected into **its** X11 window with `xdotool`, so they reach the RDP session without touching macOS permissions.
- Needs Docker, a Windows host with RDP enabled, and `WIN_HOST` / `WIN_USER` / `WIN_PASS` in the environment (never written to disk).
- `./winexec.py up` → `./winexec.py type 'dir C:\' --enter` → `./winexec.py shot` → `./winexec.py down`.
- It reconnects **your own** Windows session, so processes started there survive `down`.
- Details: [tools/winexec/README.md](tools/winexec/README.md).
This is a companion channel, not part of the MCP server — the 21 tools above remain the contract.
## Contributing
Issues and pull requests are welcome.
- Tool names and arguments are the public contract (`tests/test_tools.py` enforces it) — renaming or unregistering a tool is a breaking change.
- Run the unit tests before opening a PR: `.venv/bin/pytest -q`. They need no TCC permissions and no running RustDesk.
- Behaviour that only shows up on a real machine (TCC prompts, capture routes, session windows) should be stated with the macOS/RustDesk version you tested on.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues