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).
- Capture pipeline built for macOS 14+ / 26 — where per-window
CGWindowListCreateImagewas 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 recentcontrol_screenshotimage, 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
- Connect — starts the RustDesk GUI if needed and delivers
rustdesk://connect/<peer>through the--connectCLI. The session window is discovered by title and remembered (survives MCP host restarts). - 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).
- ScreenCaptureKit helper app (
- Input — posts hardware-level CGEvents (needs an Accessibility grant for the Python process) at coordinates mapped from the latest screenshot.
- Diagnose —
rustdesk_statusreports 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 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
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. 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):
{
"mcpServers": {
"rustdesk": {
"command": "/absolute/path/to/rustdesk-mcp/bin/rustdesk-mcp"
}
}
}
Hermes Agent:
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):
rustdesk_status()— RustDesk version, own ID, TCC state, active sessions.session_connect(peer_id="123456789", password="…")— opens a session window and returns itswindow_id+ bounds.control_screenshot()— returns the window image plus its pixel size and scale. That image is the coordinate space for everydesktop_*call.desktop_click_mouse(x=640, y=320)— click at pixel (640, 320) of that image.desktop_typing_text(text="hello")/desktop_typing_keys(keys="cmd+shift+t").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 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
.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 therustdesk://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_textwrites 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_PASSin 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.
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.pyenforces 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.