Desktop Pilot MCP
by VersoXBT
README.md
<p align="center">
<h1 align="center">Desktop Pilot MCP</h1>
<p align="center">Native macOS automation for Claude. 30-100x faster than screenshots.</p>
</p>
<p align="center">
<a href="https://github.com/VersoXBT/desktop-pilot-mcp"><img alt="Stars" src="https://img.shields.io/github/stars/VersoXBT/desktop-pilot-mcp?style=flat&logo=github&label=Stars" /></a>
<a href="#"><img alt="Version" src="https://img.shields.io/badge/version-1.0.0-blue.svg" /></a>
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green.svg" /></a>
<a href="#"><img alt="Platform" src="https://img.shields.io/badge/platform-macOS%2013%2B-lightgrey.svg" /></a>
<a href="#"><img alt="Swift 6" src="https://img.shields.io/badge/swift-6.0-orange.svg" /></a>
<a href="#"><img alt="Tests" src="https://img.shields.io/badge/tests-22%20passing-brightgreen.svg" /></a>
<a href="#"><img alt="Binary Size" src="https://img.shields.io/badge/binary-427KB-purple.svg" /></a>
<a href="#"><img alt="Dependencies" src="https://img.shields.io/badge/dependencies-0-blue.svg" /></a>
</p>
---
Desktop Pilot is an MCP server that gives Claude direct access to any macOS application through the Accessibility API, AppleScript, and CGEvent -- no screenshots, no pixel coordinates, no vision model overhead. It reads the actual UI tree and acts on semantic element references, the same way Playwright works for browsers.
**One snapshot of Telegram takes 20ms and returns structured data. The same operation with screenshot-based computer-use takes ~3 seconds and returns pixels.**
```
pilot_snapshot { "app": "Telegram" }
[e1] Window "Saved Messages"
[e2] MenuButton "Main menu"
[e3] Button "All chats (111 unread chats)"
[e7] Button "Code (4 unread chats)"
[e18] TextField "Write a message..."
[e20] Button "Record Voice Message"
pilot_click { "ref": "e18" } // focus the text field
pilot_type { "ref": "e18", "text": "Hello from Claude" }
pilot_click { "ref": "e20" } // send
```
No coordinates. No screenshots. No guessing. Just refs.
---
## Quick Start (2 minutes)
```bash
npx desktop-pilot-mcp
```
**Step 1.** Add to your Claude config and restart Claude:
For **Claude Code**, add to `~/.claude.json` under your project's `mcpServers`:
```json
{
"desktop-pilot": {
"command": "npx",
"args": ["-y", "desktop-pilot-mcp"]
}
}
```
For **Claude Desktop**, add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"desktop-pilot": {
"command": "npx",
"args": ["-y", "desktop-pilot-mcp"]
}
}
}
```
**Step 2.** Grant Accessibility permission when macOS prompts you (one-time).
If the prompt doesn't appear: **System Settings > Privacy & Security > Accessibility** -- add your terminal app or Claude Desktop.
**Step 3.** Ask Claude to interact with any app:
> "Take a snapshot of Telegram and show me what's on screen"
That's it. No API keys, no accounts, no configuration files.
<details>
<summary>Alternative: build from source</summary>
Requires Swift 6.0+ (included with Xcode 16+).
```bash
git clone https://github.com/VersoXBT/desktop-pilot-mcp.git
cd desktop-pilot-mcp
swift build -c release
```
Then use the binary path directly in your Claude config:
```json
{
"desktop-pilot": {
"command": "/absolute/path/to/desktop-pilot-mcp/.build/release/desktop-pilot-mcp",
"args": []
}
}
```
</details>
---
## Benchmarks
Real measurements from testing against Telegram, Finder, and other macOS apps:
| Operation | computer-use (screenshots) | Desktop Pilot | Speedup |
|-----------|---------------------------|---------------|---------|
| Snapshot (read full UI tree) | ~3000ms | 20ms | **150x** |
| Snapshot (Finder, 45 elements) | ~3000ms | 78ms | **38x** |
| Click element | ~3000ms | ~50ms | **60x** |
| Read element value | ~3000ms | <1ms | **3000x** |
| Find buttons by role | ~3000ms | 4ms | **750x** |
| Type text | ~4000ms | ~20ms | **200x** |
| Full flow (click + type + send) | ~14s | ~450ms | **30x** |
Screenshot-based approaches (computer-use, etc.) pay the cost of a full screen capture, a vision model call, and coordinate calculation on every single operation. Desktop Pilot reads and acts on the live UI tree directly.
---
## How It Works
Desktop Pilot uses four interaction layers with a smart router that picks the fastest method for each app and action:
```
+-------------------+
| Smart Router |
| (per-app + per- |
| action routing) |
+--------+----------+
|
+-------+-------+-------+--------+
| | | |
+------+--+ +--+------+ +-----+---+ +--+--------+
|AppleScript| | AX | | CGEvent | |Screenshot |
| Layer | | Layer | | Layer | | Layer |
+----------+ +--------+ +---------+ +-----------+
Priority: 20 Pri: 0 Pri: 40 Pri: 50
Scriptable Universal Raw input Fallback
apps only all apps injection (vision)
```
**Layer 1 -- Accessibility API** (priority 0, universal)
Reads the structured UI tree of any macOS app. Every button, text field, menu item, and label is exposed as a node with a semantic ref ID. This is the primary layer for reading state, clicking, and finding elements.
**Layer 2 -- AppleScript / System Events** (priority 20, scriptable apps)
Deep scripting for apps with AppleScript dictionaries (Finder, Safari, Mail, Keynote, Music, etc.). The router detects scriptable apps via `sdef` and routes script-based operations here automatically.
**Layer 3 -- CGEvent** (priority 40, input injection)
Ultra-fast keyboard and mouse input at 1-5ms latency. Used for typing text (more reliable than AXSetValue for most apps), keyboard shortcuts, mouse clicks at coordinates, and drag operations.
**Layer 4 -- Screenshot** (priority 50, last resort)
Captures screen regions or specific element bounds as base64 PNG. Only used when Accessibility can't see the content -- game viewports, canvas elements, custom-rendered UI.
The **Smart Router** classifies each app (scriptable, Electron, native, unknown) and picks the optimal layer per action:
| Action | Scriptable apps | Electron apps | Native apps |
|--------|----------------|---------------|-------------|
| Snapshot / Read / Find | Accessibility | Accessibility | Accessibility |
| Click | Accessibility | Accessibility | Accessibility |
| Type | CGEvent | Accessibility | CGEvent |
| Script | AppleScript | Accessibility | Accessibility |
| Menu | Accessibility | Accessibility | Accessibility |
---
## Comparison
| Feature | Desktop Pilot | computer-use (built-in) | Playwright MCP | adamrdrew/macos-accessibility-mcp | steipete/macos-automator-mcp |
|---------|:------------:|:-----------------------:|:--------------:|:---------------------------------:|:----------------------------:|
| Speed | 20-100ms | 2-5s | 50-200ms | ~200ms | ~500ms |
| Native macOS apps | Yes | Yes | No | Yes | Yes |
| Web apps / browsers | Yes | Yes | Yes | No | No |
| Electron apps | Yes | Yes | Yes | Partial | No |
| Accessibility API | Yes | No | No | Yes | No |
| AppleScript integration | Yes | No | No | No | Yes |
| CGEvent (raw input) | Yes | No | No | No | No |
| Screenshot fallback | Yes | Yes (primary) | Yes | No | No |
| Smart layer routing | Yes | No | No | No | No |
| Semantic element refs | Yes | No | Yes | Basic | No |
| Batch operations | Yes | No | No | No | No |
| Menu bar navigation | Yes | No | No | No | Via script |
| Zero dependencies | Yes | N/A | Node.js | Node.js | Node.js |
| Binary size | 427KB | N/A | ~50MB+ | ~30MB+ | ~30MB+ |
---
## Tool Reference
Desktop Pilot exposes 10 tools through the MCP protocol. All tools use the `pilot_` prefix.
### `pilot_snapshot`
Get a structured snapshot of an app's UI element tree. This is the starting point for any interaction -- it returns every visible element with a ref ID you can pass to other tools.
```json
{ "app": "Telegram" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `app` | string | No | App name or bundle ID. Omit for frontmost app. |
| `maxDepth` | integer | No | Maximum tree depth to traverse (default 10). |
Returns a tree of elements, each with a `ref` (e.g. `e1`, `e2`), role, title, value, enabled/focused state, and bounding rectangle.
---
### `pilot_click`
Click a UI element by its ref ID. Works with buttons, checkboxes, menu items, and any clickable element.
```json
{ "ref": "e5" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ref` | string | Yes | Element reference ID from a snapshot. |
---
### `pilot_type`
Type text into a text field, search box, or any editable element. Focuses the element first, then inserts the text.
```json
{ "ref": "e18", "text": "Hello from Claude" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ref` | string | Yes | Element reference ID from a snapshot. |
| `text` | string | Yes | Text to type into the element. |
---
### `pilot_read`
Read the current value, title, role, and description of a UI element. Use to check text field contents, checkbox state, or label text.
```json
{ "ref": "e3" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ref` | string | Yes | Element reference ID from a snapshot. |
---
### `pilot_find`
Search for UI elements matching criteria across an app's UI tree. Faster than a full snapshot when you know what you're looking for.
```json
{ "role": "AXButton", "title": "Save", "app": "Finder" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `role` | string | No | AX role to match (e.g. `AXButton`, `AXTextField`). |
| `title` | string | No | Title/label substring, case-insensitive. |
| `value` | string | No | Value substring to match. |
| `app` | string | No | Limit search to this app. Omit for frontmost. |
---
### `pilot_menu`
Activate a menu bar item by path. Traverses the app's menu bar hierarchy directly.
```json
{ "path": "File > Save As...", "app": "TextEdit" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | Yes | Menu path with ` > ` separator. |
| `app` | string | No | App name or bundle ID. Omit for frontmost. |
---
### `pilot_script`
Run AppleScript or JXA (JavaScript for Automation) code targeting a specific app.
```json
{
"app": "Finder",
"code": "tell application \"Finder\" to get name of every window",
"language": "applescript"
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `app` | string | Yes | Target app name. |
| `code` | string | Yes | AppleScript or JXA code to execute. |
| `language` | string | No | `applescript` (default) or `jxa`. |
---
### `pilot_screenshot`
Capture a screenshot of a specific element or the full screen. Returns base64 PNG. Use sparingly -- `pilot_snapshot` is usually better for understanding UI state.
```json
{ "ref": "e1" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ref` | string | No | Element ref to screenshot. Omit for full screen. |
---
### `pilot_batch`
Execute multiple tool calls in sequence within a single MCP round-trip. Use to reduce latency when performing multi-step actions.
```json
{
"actions": [
{ "tool": "pilot_click", "params": { "ref": "e18" } },
{ "tool": "pilot_type", "params": { "ref": "e18", "text": "Hello" } },
{ "tool": "pilot_click", "params": { "ref": "e20" } }
]
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `actions` | array | Yes | Array of `{ tool, params }` objects to execute in order. |
---
### `pilot_list_apps`
List all running macOS applications with their names, bundle IDs, PIDs, and window counts. Use to discover available apps before taking a snapshot.
```json
{}
```
No parameters required.
---
## Architecture
```
Sources/
DesktopPilot/
Core/
AppRegistry.swift # App discovery via NSWorkspace
ElementStore.swift # Actor-based ref-to-element mapping
Router.swift # Smart per-app, per-action routing
Snapshot.swift # Batch AX tree traversal
Layers/
LayerProtocol.swift # InteractionLayer protocol
AccessibilityLayer.swift # AXUIElement tree reading + actions
AppleScriptLayer.swift # System Events + sdef scripting
CGEventLayer.swift # Raw keyboard/mouse injection
ScreenshotLayer.swift # Screen capture fallback
MCP/
Server.swift # JSON-RPC 2.0 with Content-Length framing
Tools.swift # 10 tool definitions + dispatch
Types.swift # PilotElement, AppSnapshot, AppInfo
Platform/
PlatformProtocol.swift # Cross-platform bridge interface
macOS/
AXBridge.swift # Low-level AXUIElement C API wrapper
Permissions.swift # Accessibility permission management
SystemEvents.swift # AppleScript/JXA execution helper
DesktopPilotCLI/
main.swift # Entry point: permission check + server start
Tests/
DesktopPilotTests/
DesktopPilotTests.swift # 22 tests: types, router, registry, MCP, tools
```
**Key design decisions:**
- **Zero dependencies.** The entire server is built on Apple frameworks only (ApplicationServices, AppKit, CoreGraphics). No SwiftNIO, no Vapor, no third-party JSON library. This keeps the binary at 427KB.
- **Actor-based element store.** Refs are ephemeral -- they reset on each snapshot. The `ElementStore` actor guarantees thread-safe access to the AXUIElement-to-ref mapping across concurrent tool calls.
- **Content-Length framing.** The MCP server uses the standard JSON-RPC 2.0 protocol with `Content-Length` header framing over stdin/stdout, matching the MCP specification exactly.
- **Batch attribute reading.** Instead of N individual AXUIElementCopyAttributeValue calls per element, the snapshot builder uses `AXUIElementCopyMultipleAttributeValues` to read 6 attributes in a single call. This is why snapshots are fast.
---
## Supported Apps
Desktop Pilot works with any macOS application that exposes an accessibility tree (which is virtually all of them):
| Category | Examples | Primary Layer |
|----------|----------|---------------|
| Apple native | Finder, Safari, Mail, Notes, Calendar, Music | AppleScript + Accessibility |
| Productivity | Microsoft Office, Google Chrome, Firefox | Accessibility |
| Electron | VS Code, Discord, Slack, Spotify, Signal | Accessibility |
| Creative | Final Cut Pro, Logic Pro, Xcode | AppleScript + Accessibility |
| Communication | Telegram, iMessage, WhatsApp | Accessibility |
| System | System Settings, Activity Monitor, Terminal | Accessibility |
---
## Use Cases
- **Automate any macOS workflow** -- file management, app configuration, data entry across apps
- **Build AI agents** that operate native desktop applications Claude can't reach through web APIs
- **Test macOS apps** by driving the UI through structured element refs instead of fragile pixel coordinates
- **Cross-app orchestration** -- copy data from one app, process it, paste into another, all in a single Claude session
- **Accessibility auditing** -- inspect the full UI tree of any app to verify accessibility compliance
---
## Troubleshooting
**"Accessibility permission not granted"**
Open System Settings > Privacy & Security > Accessibility and add the binary or your terminal app. Restart the MCP server after granting.
**"Failed to capture screenshot"**
Grant Screen Recording permission in System Settings > Privacy & Security > Screen Recording. Required only for `pilot_screenshot`.
**Stale refs (`Unknown ref 'e5'`)**
Refs reset on every `pilot_snapshot` call. Always take a fresh snapshot before interacting with elements. If an app's UI has changed since the last snapshot, the old refs are invalid.
**Electron apps not responding to `pilot_type`**
Some Electron apps (VS Code, Discord) swallow raw key events. The router handles this by using Accessibility (AXSetValue) instead of CGEvent for Electron apps. If typing still fails, try `pilot_script` with a System Events keystroke.
**Empty snapshots**
The app may not have any open windows, or it may use a non-standard UI framework (games, OpenGL/Metal renderers). Use `pilot_screenshot` as a fallback for custom-rendered content.
---
## Development
```bash
# Build debug
swift build
# Build release
swift build -c release
# Run tests
swift test
# Run the server directly
swift run desktop-pilot-mcp
```
The project is split into a library target (`DesktopPilot`) and an executable target (`DesktopPilotCLI`) for testability. All core logic lives in the library; the CLI is a thin entry point.
---
## License
MIT
## Contributors
<!-- ALL-CONTRIBUTORS-LIST:START -->
<table>
<tr>
<td align="center"><a href="https://github.com/VersoXBT"><img src="https://avatars.githubusercontent.com/u/202813801?v=4" width="80px;" alt=""/><br /><sub><b>VersoXBT</b></sub></a><br />💻 📖</td>
<td align="center"><a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4" width="80px;" alt=""/><br /><sub><b>Claude</b></sub></a><br />🤖 💡</td>
</tr>
</table>
<!-- ALL-CONTRIBUTORS-LIST:END -->
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues