Skip to main content
Glama
Cocos666a

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).