Skip to main content
Glama
README.md
# DesktopBridge 🌉

[![CI](https://github.com/jameymcelveen/desktop-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/jameymcelveen/desktop-bridge/actions/workflows/ci.yml)
[![Home](https://img.shields.io/website?url=https%3A%2F%2Fhome.jameymcelveen.com%2Fhealth&label=home)](https://home.jameymcelveen.com)
[![Node](https://img.shields.io/badge/node-20.19%2B-3c873a)](https://nodejs.org)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

Local **Model Context Protocol** server for macOS. Claude (or any MCP host) talks to it over **stdio** and can:

- Read, write, list, and search files inside **allowlisted directories**
- Read CPU / memory / disk stats, uptime, and a redacted environment
- List running applications
- Run shell commands with **separate stdout/stderr**, timeouts, and optional progress streaming
- Read and write the clipboard
- Capture screenshots and list displays

This process has the same OS rights as the user who launched it. Treat it like giving the model a terminal on your Mac, then shrink that blast radius with `DESKTOP_BRIDGE_ROOTS`.

## Requirements

- macOS (clipboard, screenshots, and application listing use Apple tools)
- Node.js **20.19+** (22 LTS recommended)

## Install

```bash
cd desktop-bridge
npm install
npm run build
npm test
```

The compiled entrypoint is `dist/index.js`.

## Connect to Claude Desktop

1. Build the server (`npm run build`).
2. Open **Claude Desktop → Settings → Developer → Edit Config**.
3. Merge the block from [`claude_desktop_config.example.json`](./claude_desktop_config.example.json), replacing the path and usernames:

```json
{
  "mcpServers": {
    "desktop-bridge": {
      "command": "node",
      "args": ["/Users/YOU/dev/desktop-bridge/dist/index.js"],
      "env": {
        "DESKTOP_BRIDGE_ROOTS": "/Users/YOU/Desktop,/Users/YOU/Documents,/Users/YOU/Downloads"
      }
    }
  }
}
```

4. Fully quit and reopen Claude Desktop.
5. Confirm **desktop-bridge** appears under MCP tools (bridge icon 🌉).

Config file on macOS:

`~/Library/Application Support/Claude/claude_desktop_config.json`

## Connect to Claude Code

```bash
claude mcp add desktop-bridge -- node /Users/YOU/dev/desktop-bridge/dist/index.js
```

Or add the same `command` / `args` / `env` block to `~/.claude.json`.

## Connect to Cursor

Add to `~/.cursor/mcp.json` (or the project `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "desktop-bridge": {
      "command": "node",
      "args": ["/Users/YOU/dev/desktop-bridge/dist/index.js"]
    }
  }
}
```

## Smoke-test without a host

```bash
npm run inspector
```

That launches the MCP Inspector against the built stdio server. Call `list_roots`, then `get_system_info`.

Logs go to **stderr** only. Do not `console.log` in this process — stdout is the JSON-RPC channel.

## Environment

| Variable | Default | Meaning |
| --- | --- | --- |
| `DESKTOP_BRIDGE_ROOTS` | `~/Desktop`, `~/Documents`, `~/Downloads` (if they exist) | Comma-separated directories file tools may touch. The OS temp dir is always added so screenshots have a place to land. |
| `DESKTOP_BRIDGE_MAX_FILE_BYTES` | `10485760` | Max size for a single file read/write (1 KiB–100 MiB). |
| `DESKTOP_BRIDGE_COMMAND_TIMEOUT_MS` | `30000` | Default `run_command` timeout (100–300000). |
| `DESKTOP_BRIDGE_MAX_OUTPUT_BYTES` | `1048576` | Combined stdout+stderr capture cap. Excess output kills the process and sets `truncated`. |
| `DESKTOP_BRIDGE_ALLOW_SHELL` | `true` | Set `false` to disable `run_command`. |
| `DESKTOP_BRIDGE_RESTRICT_SHELL_CWD` | `true` | When true, `run_command` cwd must sit inside an allowed root. |
| `DESKTOP_BRIDGE_STATUS_URL` | unset | Heartbeat POST URL for the status site (`…/api/heartbeat`). |
| `DESKTOP_BRIDGE_STATUS_TOKEN` | unset | Bearer token matching the site’s `HEARTBEAT_TOKEN`. |
| `DESKTOP_BRIDGE_STATUS_INTERVAL_MS` | `15000` | Heartbeat interval (5s–5m). |

Copy [`.env.example`](./.env.example) for a commented template. The server reads **process env** (Claude Desktop `env` block), not a `.env` file.

## Tools

| Tool | What it does |
| --- | --- |
| `list_roots` | Allowed directories and file-size cap |
| `read_file` | Text (optional line window) or base64 |
| `write_file` | Create/overwrite/append; optional `mkdir -p` |
| `list_directory` | Name, type, size, mtime, mode |
| `search_files` | Glob on names and/or regex on file contents |
| `get_system_stats` | CPU %, load, memory, `df` |
| `get_system_info` | Host, uptime, user, redacted env |
| `list_applications` | GUI (or all) processes via System Events |
| `run_command` | Shell with split stdout/stderr; `stream` → progress notifications |
| `read_clipboard` / `write_clipboard` | `pbpaste` / `pbcopy` |
| `get_display_info` | Display name, main flag, scale, frame |
| `take_screenshot` | PNG via `screencapture`; returns an image block when ≤ 5 MiB |

Resources: `desktop://roots`, `desktop://system/info`.  
Prompts: `inspect_desktop`, `find_file`.

## Security model

- **Files:** every path is `realpath`'d. The resolved path must stay inside a configured root. `..`, extra slashes, and symlinks that escape are rejected.
- **Home is not a default root.** That keeps `~/.ssh` and similar out of reach until you add them on purpose.
- **Shell:** still a full user shell. A command can `cd` anywhere even when cwd is restricted. Disable it with `DESKTOP_BRIDGE_ALLOW_SHELL=false` if you only want file/clipboard/screen tools.
- **Env:** keys matching password/token/secret/key/credential/cookie/session are replaced with `[redacted]`.
- **Stdio:** no network listener. The host spawns this process.

## macOS permissions

| Feature | Permission |
| --- | --- |
| Screenshots | **Screen Recording** for the app that spawned Node (Claude Desktop, Cursor, or Terminal) |
| `list_applications` | **Automation → System Events** if macOS prompts |
| Accessibility-heavy apps | may still hide titles; the tool lists process names either way |

If `screencapture` fails, open **System Settings → Privacy & Security → Screen Recording** and enable the host app, then restart it.

## Development

```bash
npm run build    # tsc → dist/
npm start        # node dist/index.js (stdio)
npm test         # compile + node:test
```

Layout: `src/lib/*` (path guard, process runner, glob/search), `src/tools/*` (MCP tools), `src/index.ts` (stdio entry).

## Home

https://home.jameymcelveen.com is the browser start page (`web/`). Widgets are Lit web components under `web/public/components/`, tagged `jm-*`. The isolated catalog is Storybook at https://home.jameymcelveen.com/storybook/ (`cd web && npm run storybook` locally). Lit is vendored into `web/public/vendor/` (`npm run vendor` / postinstall). Sign-in is `@mcelveen.us` plus `STATUS_PASSWORD`.

What is there today, and the dump tray for whatever comes next:

| Piece | Notes |
| --- | --- |
| Widgets | Lit web components, `jm-*` prefix. Isolated catalog: [Storybook](https://home.jameymcelveen.com/storybook/) |
| Search | Autofocus. Google completions as you type (same suggestion feed as google.com). Kagi / DDG. Bangs: `!g` `!k` `!d` `!gh` `!yt` `!w` `!maps` |
| Links | Same tiles as the local landing-page app, plus the properties. Edit as JSON in Settings |
| Weather | Open-Meteo, °F, Florence SC unless you override coords |
| Mac | DesktopBridge heartbeat: online / stale / offline, IPs, load |
| VIN Sweep | Client-side NHTSA decode / recalls / complaints, plus human-only NICB / FL title / iSeeCars taps |
| Scratch | Autosaved notes |
| Word | Daily verse |

Set Chrome/Safari/Firefox homepage to `https://home.jameymcelveen.com` (browsers will not let the page do it for you). Session cookie lasts 30 days. `/` focuses search; `⌘K` too.

Push to `main` runs CI, then deploys Vercel (the site) and Railway (heartbeat + saved config).

On the Mac, add to the MCP server env:

```
DESKTOP_BRIDGE_STATUS_URL=https://home.jameymcelveen.com/api/heartbeat
DESKTOP_BRIDGE_STATUS_TOKEN=<HEARTBEAT_TOKEN>
```

## License

MIT

TDQS

A3.8/5.0

Scored across 14 tools

Disambiguation4/5

Most tools have clearly distinct purposes, but a few pairs like get_system_stats vs get_system_info and list_directory vs search_files could be confused without careful reading. Descriptions clarify the differences, so overall ambiguity is low.

Naming Consistency5/5

All 14 tools follow a consistent verb_noun snake_case naming convention (e.g., list_roots, read_file, get_network_info). This pattern is uniform and predictable, making it easy for agents to infer behavior.

Tool Count5/5

With 14 tools, the server covers a broad range of desktop automation functions—file, system, clipboard, apps, network, command, and display—without being bloated. Each tool has a clear purpose, and the count is well within the ideal range.

Completeness3/5

The toolset covers core file operations (read, write, list, search) but misses common operations like delete or rename. It also lacks process management beyond listing apps, but the provided surface is adequate for many automation tasks, leaving only moderate gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues