OpenDesk by VitalOps — open-source computer-use MCP server that lets AI agents see and control macOS, Windows and Linux desktops, locally or across paired machines.
Screenshots, accessibility-based UI actions, mouse and keyboard, OCR, workflow recording, scheduling, screen memory, and remote machine control, all from natural language.
macOS · Linux · Windows
Not affiliated with the openDesk office suite.
opendesk.single.machine.demo.mp4Single Machine Demo |
Control.Multiple.Machines.Opendesk.compressed.mp4Control Multiple Machines |
opendesk.Ui.testing.final.video.mp4UI Testing |
|
1 · It captures |
2 · You ask |
3 · Any agent recalls it |
🔒 Local SQLite index · 🔐 Encrypted at rest · 🙈 Secrets redacted · 🚫 Per-app deny list · ⏸ Pause hotkey · 🗑 Storage cap
pip install 'opendesk[memory]'
opendesk memory start # then ask your agent about anything you've seenOpenDesk runs as an MCP server for Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Continue or any other MCP client. The commands below need uv (Python) or Node.js 18+; nothing else has to be installed first.
Claude Code, one command:
claude mcp add opendesk -- uvx --from opendesk opendesk-mcpClaude Code plugin (MCP server plus a skill that teaches Claude the workflow):
claude plugin marketplace add vitalops/opendesk
claude plugin install opendesk@opendeskClaude Desktop: download opendesk-<version>.mcpb from Releases and double-click it, or add the JSON below to claude_desktop_config.json.
Cursor (~/.cursor/mcp.json), Claude Desktop, Windsurf, Continue:
{
"mcpServers": {
"opendesk": { "command": "uvx", "args": ["--from", "opendesk", "opendesk-mcp"] }
}
}VS Code (.vscode/mcp.json):
{
"servers": {
"opendesk": { "type": "stdio", "command": "uvx", "args": ["--from", "opendesk", "opendesk-mcp"] }
}
}No Python? Use the Node.js server instead, in any of the configs above:
{ "command": "npx", "args": ["-y", "-p", "@vitalops/opendesk-sdk", "opendesk-js-mcp"] }Remote clients can use streamable HTTP with bearer-token auth: uvx opendesk mcp --transport http --port 8765 (details).
macOS: run
uvx opendesk check --open, then grant Accessibility and Screen Recording to the app that runs OpenDesk (Claude Desktop, your terminal for Claude Code, Cursor or VS Code) and restart that app. See permissions.
| Language | Location | Package | Install |
|---|---|---|---|
| Python 3.10+ | python/ |
opendesk (PyPI) |
pip install opendesk |
| JavaScript / TypeScript | js/ |
@vitalops/opendesk-sdk (npm) |
npm install @vitalops/opendesk-sdk |
pip install opendesk includes the MCP server and local computer use; opendesk install registers it with Claude Code (npx opendesk-js install for the JS server).
Once connected, try:
Take a screenshot of my screen
Click the Chrome icon
Open Spotify and play lo-fi beats
Show me the audit log
Replay everything from this session
What was the error I saw in the terminal on Tuesday?
Use opendesk programmatically in your own agent or app.
from opendesk import create_registry, allow_all_context
registry = create_registry()
ctx = allow_all_context()
result = await registry.get("screenshot").execute(ctx, ...)import { OpenDeskClient } from "@vitalops/opendesk-sdk";
const client = new OpenDeskClient();
await client.screenshot({ marks: true });
await client.ui({ action: "click", app: "Safari", title: "Go" });opendesk is built in independently-importable layers:
┌──────────────────────────────────────────────────────────────┐
│ Integrations MCP · Claude Code · OpenAI · LangChain │
├──────────────────────────────────────────────────────────────┤
│ Tools screenshot · mouse · keyboard · ui · │
│ clipboard · ocr · learn · schedule · audit · │
│ memory · memory_manage │
├──────────────────────────────────────────────────────────────┤
│ Computer LocalComputer · RemoteComputer (ABC) │
├──────────────────────────────────────────────────────────────┤
│ Remote server · client · discovery (mDNS) │
├──────────────────────────────────────────────────────────────┤
│ Protocol frames · codec (msgpack) · peer · transports │
│ auth (X25519 + AEAD, pairing) │
└──────────────────────────────────────────────────────────────┘
| Layer | What it does |
|---|---|
| Computer | The capability surface of a computer (observe / act / subscribe). LocalComputer drives the local machine; RemoteComputer forwards every call over the wire to a paired peer. Tools and integrations target this ABC — they never know whether the machine is local or remote. |
| Tools | One class per capability, agent-friendly Pydantic schemas. Calls into the active Computer on the ToolContext. |
| Integrations | Thin adapters for MCP, Anthropic, OpenAI, LangChain — add one tool, get all four. |
| Remote | opendesk serve / opendesk pair, mDNS discovery, client helper. |
| Protocol | Five-frame wire protocol (msgpack binary, no base64 ever), WebSocket transport, mutual X25519 + ChaCha20-Poly1305 auth and encryption. |
| Automation | learn + schedule backed by pynput recording, JSON storage, APScheduler daemon. |
| Memory | opendesk memory start captures on app/window switches (plus an idle fallback), reads text from the accessibility tree or local OCR, redacts secrets, and indexes text + encrypted thumbnails in SQLite/FTS5. Per-app deny list, pause hotkey, storage cap with rolling deletion. Nothing leaves the machine. |
Full details → docs/architecture
| Tool | What it does |
|---|---|
screenshot |
Capture the screen with numbered boxes on every clickable element (Set-of-Marks) |
ui |
Click and type by element name or [ref] — no coordinates, no cursor movement |
mouse |
Pixel-level mouse control for anything ui can't reach |
keyboard |
Type text, press keys, send hotkeys |
app |
Open, close, and focus applications |
clipboard |
Read and write the system clipboard |
ocr |
Extract text from any region of the screen |
script |
Shell, AppleScript/JXA or PowerShell — skip the GUI when a task doesn't need it |
browser |
Drive Chrome/Edge/Brave over the DevTools protocol: read pages, click, type, eval |
learn |
Record a workflow once, replay it anytime |
schedule |
Run any task or learned procedure on a timer |
audit |
Show the session audit log, replay a session |
memory 🧠 |
Screen memory (read-only): recall anything that was on screen, fully local (opendesk memory start) |
memory_manage |
Pause capture, edit the deny list, change retention, delete history |
fleet |
Run one tool call on several paired machines in parallel |
opendesk_* |
Pick and inspect paired remote machines (opendesk_peers, opendesk_use, …) |
Every tool carries an MCP title and readOnlyHint / destructiveHint annotations, so clients can auto-approve read-only calls and ask before actions.
Full reference → docs/tools
Record a task once, replay it forever, or put it on a schedule.
Record
"Start recording task expense-form"
Perform the workflow yourself. The agent captures every click, keystroke, and screenshot.
Replay
"Stop recording"
"Replay expense-form"
The agent re-executes using the current screen state — no hardcoded coordinates.
Compiled replay. Each recording (or a successful session, via learn(action="save_session")) also compiles into a program that targets UI elements by app, role and title, with a check after each step. learn(action="run") replays it with no LLM calls; if a step fails, it stops and hands the agent the remaining steps to finish and patch. Scheduled replays use the compiled program first, so they run without an API key. → docs/automation/compiled-replay.md
Schedule
"Every morning at 9am, open my email in Chrome, take a screenshot, and summarize what's there"
"Schedule expense-form every friday at 5pm"
opendesk scheduler startSupported timing: every 30m · every 2h · every day at 09:00 · every friday at 17:00 · raw cron
Full guide → docs/automation
Tip
A searchable, fully local history of what was on your screen — like Rewind or Recall, but open source, cross-platform, and callable by any agent.
# Python
pip install 'opendesk[memory]'
opendesk memory start # or: opendesk memory install-service (run at login)
# JavaScript / TypeScript
npm install @vitalops/opendesk-sdk
npx opendesk-js memory start # or: npx opendesk-js memory install-serviceThe daemon captures the screen whenever you switch apps or windows (plus an idle fallback), reads the window's text from the accessibility tree or with local OCR on your machine, redacts secrets, and stores the text plus an encrypted thumbnail in ~/.opendesk/memory. Agents search it with the read-only memory tool; memory_manage pauses capture or deletes history. Then ask:
"What was the error message I saw in the terminal on Tuesday?"
"Find the invoice number I had open last week"
"Show me every time I opened that dashboard this month"
- Fully local index — SQLite + FTS5, no upload, no telemetry.
- Encrypted at rest — thumbnails are AES-GCM encrypted with a key in the OS keychain; text encryption with an exact-word blind index is one flag away (
opendesk memory config --encrypt-text on). - Secret redaction — API keys, tokens, card numbers and
password:pairs are scrubbed before anything is written; private-browsing windows are skipped. - Per-app deny list — password managers excluded by default;
opendesk memory deny add "bank"matches app names and window titles. - Pause hotkey —
Cmd/Ctrl+Shift+Alt+P, oropendesk memory pause 2h. - Storage cap with rolling deletion — default 2 GB / 30 days; oldest frames go first.
- Export —
opendesk memory export --format htmlwrites a decrypted, browsable copy (alsojson/csv) to a folder you choose.
Full reference → docs/tools/memory.md
Every tool call from an agent passes through a local safety guard before it touches the desktop.
- Risk tiers — each tool and action is classified; the
standardpreset allows reads and ordinary input, and asks before high-risk actions such asscriptshell commands orbrowsereval.strictasks before anything that acts. Add your own allow / confirm / block rules per tool, app or peer. - Approvals — asked in the MCP client when it supports elicitation, otherwise in a native OS dialog.
- Kill switch and take-over —
opendesk stoprefuses every agent call immediately, on this machine and from paired controllers;opendesk takeover/handbacklets you drive for a while. - Untrusted screen content — text read off the screen is wrapped as data, scanned for prompt injection, and a hit makes input actions require approval for a while.
- Tamper-evident audit — every call is written to a hash-chained log;
opendesk audit verifychecks it. - Secrets the model never sees — store a password with
opendesk secrets set NAME; the agent types{{secret:NAME}}and OpenDesk fills it in from the OS keychain after you approve, redacting it everywhere.
opendesk policy preset strict # ask before anything that acts
opendesk stop # halt the agent now
opendesk audit verify # check the audit chainFull guide → docs/safety.md
opendesk eval runs task suites against a real desktop and reports success, turns, time and cost: a macOS smoke suite, a prompt-injection suite, and regression checks for compiled workflows. Backends hold the model constant so you can compare OpenDesk's tools with the API's native computer tool. An OSWorld adapter is included.
opendesk eval run macos-smoke --dry-run # show exactly what would run
opendesk eval run injection --repeat 3 # uses Claude Code on your subscriptionFull guide → docs/eval.md
Control another machine from your agent — same tools, same MCP server, the
Computer abstraction just lives on the other end of an encrypted WebSocket.
On the machine being controlled (one time):
pip install 'opendesk[remote]'
opendesk pair # prints a 6-digit code, listensOn the controller (one time):
pip install 'opendesk[remote]'
opendesk discover # list opendesk peers on the LAN
opendesk pair-with <host> <code> --name miniAfter pairing, the controlled machine runs the long-lived server:
opendesk serve # accepts paired peers only…and the controller drives it through the existing MCP server (Claude Code,
Claude Desktop, Cursor — anything that speaks MCP). The agent gets new admin
tools — opendesk_peers, opendesk_use, opendesk_status — and every
existing tool accepts an optional peer: argument:
screenshot → controls the local machine
screenshot peer=mini → controls the paired remote
opendesk_use mini → make mini the default for this session
screenshot → [on mini] ...
With exactly one paired peer the agent doesn't have to specify anything — it becomes the implicit default. With multiple, the agent must pick explicitly (no silent fallback).
One controller at a time. Pair as many machines as you like, but only
one drives the desktop at a time — a second peer trying to connect while
one is active gets a clean BUSY error. Same peer reconnecting bumps
the previous session (no waiting out a stale TCP). Two ways to free the
slot from the controlled machine:
opendesk disconnect— cooperative. Server asks the controller to leave via asession.evictedPUSH; a cooperative client (the in-treeRemoteComputer) suppresses its auto-reconnect and raisesSessionEvicted. Trust is preserved.opendesk unpair <name>— enforced. Revokes trust + closes the session; next reconnect fails authentication.
Security model: pairing exchanges long-lived X25519 keypairs via a 6-digit code-authenticated handshake (PBKDF2-stretched, ~CPU-month to brute force). Subsequent connections use mutual static-key authentication. Every frame is ChaCha20-Poly1305 AEAD-encrypted with per-direction counters. No CA-signed certificates required — the keys ARE the trust.
Full guide → docs/remote
uvx opendesk mcp # run the MCP server without installing anything
pip install opendesk # MCP server + local computer use (screen capture, mouse, keyboard)
pip install 'opendesk[remote]' # + control another machine over LAN
pip install 'opendesk[learn]' # + task recording and replay
pip install 'opendesk[learn,schedule]' # + scheduled tasks
pip install 'opendesk[memory]' # + screen memory (searchable desktop history)
pip install 'opendesk[all]' # everythingThe core and mcp extras from earlier versions still work; they are now part of the base install.
| Feature | macOS | Linux | Windows |
|---|---|---|---|
| Screenshot | ✓ | ✓ | ✓ |
| Mouse & keyboard | ✓ | ✓ | ✓ |
| UI element access | AppleScript | AT-SPI2 | UI Automation |
| Clipboard | pbcopy/pbpaste | xclip/xsel | pyperclip |
| OCR | Vision / tesseract | tesseract | WinRT / tesseract |
| App control | open -a |
xdg-open |
start |
| Task recording | ✓ | ✓ | ✓ |
| Scheduled tasks | ✓ | ✓ | ✓ |
| Remote control (LAN) | ✓ | ✓ | ✓ |
| LAN discovery (mDNS) | ✓ | ✓ | ✓ |
opendesk check --open # or: uvx opendesk check --openIt reports what is missing and opens the right System Settings pane. Grant Accessibility (mouse, keyboard, UI actions) and Screen Recording (screenshots, OCR) to the app that runs OpenDesk:
- Claude Desktop when you use the
.mcpbbundle orclaude_desktop_config.json - your terminal (Terminal, iTerm2, Ghostty, …) for Claude Code
- Cursor or VS Code when they launch the server
Then quit and reopen that app. When a permission is missing, tool errors say so and point back to opendesk check --open.
sudo apt install xclip xdotool python3-atspiNo extra permissions needed — opendesk uses Win32 APIs by default.
See docs/getting-started/permissions.md for the full setup guide.
claude mcp add opendesk -- uvx --from opendesk opendesk-mcp
# or, after `pip install opendesk`:
opendesk install # registers opendesk-mcp globally
opendesk uninstall # removes the registrationAdd to your config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"opendesk": { "command": "uvx", "args": ["--from", "opendesk", "opendesk-mcp"] }
}
}Or install the one-click .mcpb bundle from Releases.
import asyncio
from opendesk import create_registry, allow_all_context
async def main():
registry = create_registry()
ctx = allow_all_context()
result = await registry.get("screenshot").execute(
ctx, registry.get("screenshot").Params(marks=True)
)
print(result.output)
asyncio.run(main())Works with Anthropic SDK, OpenAI, and LangChain — see docs/integrations
Any OpenAI-compatible local server works out of the box:
from openai import OpenAI
from opendesk.integrations.openai_compat import OpenAIAdapter
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
adapter = OpenAIAdapter()
result = await adapter.run_loop(client, model="qwen2.5:72b", messages=messages)If you use OpenDesk in your research or project, please cite it:
@software{opendesk,
author = {Abraham, Abhigith Neil and Rahman, Fariz and Rahman, Fadil},
title = {opendesk: Open Desktop Automation Framework},
year = {2026},
url = {https://github.com/vitalops/opendesk},
version = {0.3.0},
license = {MIT}
}A CITATION.cff is included — GitHub's "Cite this repository" button will pick it up automatically.
- Docs: https://vitalops.ai/opendesk/docs/
- For LLMs and coding agents:
llms.txt,AGENTS.md - MCP Registry entry:
server.json(io.github.vitalops/opendesk) - Privacy:
PRIVACY.md
MIT