desktop-control-mcp
# desktop-control-mcp
An MCP server that lets an MCP client (Claude Code, Claude Desktop, or claude.ai via a
custom connector) open and close applications, files, and folders on this Windows machine.
Tools exposed:
| Tool | Does |
|---|---|
| `open_app` | Launch an app by friendly name (`chrome`, `word`, `notepad`, ...) or `.exe` name, optionally with an argument (URL/file) |
| `close_app` | Force-close a running app by name |
| `list_running_apps` | List processes that currently have a visible window |
| `open_path` | Open a file/folder path, or a friendly folder name (`documents`, `desktop`, `downloads`, `pictures`, `music`, `videos`, `home`) |
| `close_folder_window` | Close any open File Explorer window(s) showing a given folder |
| `list_folder_contents` | List the files/subfolders directly inside a folder (name, type, path, size) |
| `open_path_with` | Open a file/folder with a SPECIFIC app (e.g. a folder in VS Code) - `app` is required |
| `find_duplicate_files` | Recursively find duplicate files by content (name, all locations, count, wasted space) |
| `list_open_folders` | List every open File Explorer folder window (path, title, hwnd) |
| `close_all_folder_windows` | Close every open File Explorer folder window |
| `focus_folder_window` | Bring an open folder window to front (un-minimizes if needed) |
| `minimize_folder_window` | Minimize an open folder window |
| `list_chrome_tabs` | List every open Chrome tab (index + title) |
| `open_chrome_tab` | Open a new Chrome tab, optionally to a URL |
| `close_chrome_tab` | Close the open tab matching a title search |
| `focus_chrome_tab` | Switch to the open tab matching a title search |
| `scroll_chrome_page` | Maximize Chrome and scroll the active page up/down (simulated PageUp/PageDown) |
| `list_chrome_profiles` | List configured Chrome profiles (name + directory) |
| `open_chrome_profile` | Open a new Chrome window under a specific profile (most reliable when Chrome isn't already running) |
| `focus_app` | Bring an app's window to front / switch to it (un-minimizes if needed) |
| `maximize_app` | Maximize an app's window |
| `minimize_app` | Minimize an app's window |
| `system_info` | Report OS, CPU, RAM (total/used/free/%), and disk usage per drive |
| `restart_graphics_driver` | Send Ctrl+Shift+Win+B to restart the graphics driver - fixes a frozen/stuck/black/glitched screen without closing apps or losing work |
| `shutdown_pc` | Schedule a full shutdown after a delay. Requires `confirm: true`. |
| `restart_pc` | Schedule a restart after a delay. Requires `confirm: true`. |
| `cancel_shutdown` | Abort a pending `shutdown_pc`/`restart_pc` before it fires |
Known app aliases live in [src/appMap.js](src/appMap.js) — add more there as needed. Any
other name is tried as a literal `.exe`.
`open_path_with`'s `app` field is required on purpose: if a request doesn't say which app
to use, the calling model should ask you rather than guess — making the field required
enforces that at the schema level instead of relying on prompt wording. Use plain
`open_path` for "just open it with whatever's default" requests.
`focus_app` / `maximize_app` / `minimize_app` take an optional `pid`. If more than one
window of the requested app is open, they refuse to guess — the error lists each
candidate's PID and window title so you (or Claude) can retry with the right `pid`.
`find_duplicate_files` matches by content (SHA-256), not filename — two files named
differently but byte-identical count as duplicates; two files with the same name but
different content don't. It groups by file size first and only hashes within a group
that has more than one file, so a folder with mostly unique-sized files is fast. It stops
after 20000 files on a very large tree and sets `truncated: true` rather than hanging.
### Chrome tabs
`close_chrome_tab` / `focus_chrome_tab` match by a case-insensitive substring against tab
titles (not URLs) and take an optional `index` the same way the window tools take `pid` -
if the query matches more than one tab, they list the candidates instead of guessing.
Built on Chrome's own UI Automation tree (the same mechanism screen readers use), not
`taskkill`/Win32 windows, since individual tabs aren't OS-level windows at all.
Two things worth knowing:
- Only the **first** Chrome window is used if you have more than one open — tabs in a
second Chrome window aren't visible to these tools.
- The **first** Chrome tab call after Chrome starts (or after it's been idle a while) can
return an empty list while Chrome's accessibility tree activates. If you get `[]` from
`list_chrome_tabs` when you know tabs are open, just call it again.
## ⚠️ Security model — read before exposing this to the internet
This server can launch and kill processes on your laptop. **Anyone who can send it a
request has meaningful control of your machine.** It is not a toy:
- The HTTP transport (`src/http.js`) **refuses to start without `MCP_AUTH_TOKEN` set**
and requires `Authorization: Bearer <token>` on every request. There is no bypass for
this — don't add one.
- It binds to `127.0.0.1` only, never `0.0.0.0`. The only way it becomes reachable from
outside this machine is if you deliberately point ngrok (or something else) at it.
- Every request is logged to stdout with timestamp and source IP, including rejected
(unauthenticated) ones — keep an eye on that console while a tunnel is open.
- A small denylist (`PROCESS_DENYLIST` in [src/appMap.js](src/appMap.js)) blocks closing
core OS processes (`lsass.exe`, `winlogon.exe`, `services.exe`, ...) so a bad request
can't easily crash the whole session.
- All dynamic input (app names, paths) is passed to child processes via environment
variables, never string-concatenated into a shell/PowerShell command line — this is
what prevents a crafted tool argument from smuggling in extra commands.
### `shutdown_pc` / `restart_pc` — the two tools that can strand you
These are the most destructive tools in this server, so they have three independent
safety nets:
1. **`confirm: true` is required.** Omit it and the tool refuses before touching
`shutdown.exe` — a guard against an accidental or ambiguous call.
2. **A countdown delay** (default 60s, min 15s, max 3600s) before anything happens.
3. **`cancel_shutdown`** aborts a pending shutdown/restart during that countdown.
Two things worth knowing that aren't obvious from the tool names:
- **Unsaved work is not protected.** Windows' `shutdown.exe` automatically implies
force-close (`/f`) whenever the delay is greater than 0 seconds — every default call
here has a delay, so once the countdown reaches zero, open apps are closed without a
save prompt. The delay is your only window to react, not a "Windows will ask first"
safety net.
- **`restart_pc` currently ends remote control, same as `shutdown_pc`.** This server
doesn't auto-start on boot, so after either one fires you'll need to be physically at
the machine to get remote control back (auto-start-on-login is a possible follow-up,
not set up here).
If you ever need to cancel by hand instead of through an MCP tool call, run `shutdown /a`
from a normal `cmd.exe` or PowerShell window — **not Git Bash**. Git Bash's path
auto-conversion rewrites leading-slash flags like `/a` into a bogus Windows path before
`shutdown.exe` ever sees it, so the command silently does nothing (prints usage/help,
exit code 1) instead of cancelling. This bit us during testing of this feature.
What this does **not** do: there is no arbitrary shell-command tool here on purpose.
"Open/close apps and files" is a large enough attack surface already; a generic
`run_command` tool would turn this into an unrestricted remote shell. If you extend this
server, keep that boundary in mind.
Practical rules for running this safely:
1. **Treat `MCP_AUTH_TOKEN` like a root password.** Generate a long random one (see
`.env.example`), never commit it, never post the ngrok URL anywhere public.
2. **Only run the tunnel while you're actively using it.** Stop `ngrok` (and the server,
if you want to be extra safe) when you're done for the day.
3. **Prefer ngrok's own auth on top of yours.** `ngrok http 3939 --oauth=google
--oauth-allow-email=you@example.com` (requires a paid ngrok plan) or at least
`ngrok http 3939 --basic-auth "user:longpassword"` adds a second lock on the door.
4. **Watch the console.** Every open/close call this server executes is printed. If you
see activity you didn't trigger, kill the ngrok tunnel immediately (Ctrl+C) and
rotate `MCP_AUTH_TOKEN`.
5. A free ngrok URL changes every time you restart the tunnel — that's a feature, not a
bug, since a stale forgotten URL still needs your (rotatable) token to do anything.
6. **Try `shutdown_pc`/`restart_pc` locally over stdio first**, with a generous
`delaySeconds` and `cancel_shutdown` ready to go, before ever calling them through the
ngrok tunnel where a dropped connection could keep you from cancelling in time.
## Setup
```bash
npm install
copy .env.example .env
```
Edit `.env` and set `MCP_AUTH_TOKEN` to a long random value:
```bash
node -e "console.log(require('crypto').randomBytes(24).toString('hex'))"
```
## Running locally (stdio) — for Claude Code / Claude Desktop on this machine
No token needed for stdio; the client launches the process directly, so there's no
network exposure.
Claude Code:
```bash
claude mcp add desktop-control -- node "C:\Users\Al-Fateh\Documents\agentic-system\desktop-control-mcp\src\stdio.js"
```
Claude Desktop (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"desktop-control": {
"command": "node",
"args": ["C:\\Users\\Al-Fateh\\Documents\\agentic-system\\desktop-control-mcp\\src\\stdio.js"]
}
}
}
```
## Running remotely (HTTP + ngrok) — for claude.ai custom connectors
1. Start the server:
```bash
npm run start:http
```
It listens on `http://127.0.0.1:3939/mcp` and will refuse to start if
`MCP_AUTH_TOKEN` isn't set.
2. In a separate terminal, tunnel it:
```bash
ngrok http 3939
```
Note the `https://<random>.ngrok-free.app` URL ngrok prints.
3. Connect a client to `https://<random>.ngrok-free.app/mcp` with header
`Authorization: Bearer <your MCP_AUTH_TOKEN>`:
- **Claude Code**: `claude mcp add --transport http desktop-control https://<random>.ngrok-free.app/mcp --header "Authorization: Bearer <token>"`
- **claude.ai** (Settings → Connectors → Add custom connector): paste the URL. If the
UI doesn't offer a header field for your account type, you'll need OAuth-based auth
instead of a static bearer token to use claude.ai's hosted connector flow — the
bearer-token approach above is guaranteed to work with Claude Code today.
4. When you're done, `Ctrl+C` both the ngrok tunnel and the server.
## Files
```
src/appMap.js friendly-name -> exe/process map, and the process denylist
src/winControl.js the actual open/close logic (PowerShell + taskkill, injection-safe)
src/server.js MCP tool definitions, shared by both transports
src/stdio.js entry point for local stdio transport
src/http.js entry point for remote HTTP transport (auth required)
```
TDQS
Scored across 27 tools
Each tool targets a distinct resource-action pair: app launching/windowing, folder paths/windows, Chrome profiles/tabs, and system control are clearly separated. Even the three 'open' tools differ by target type (app name, default handler, specific app), and close/focus/minimize variants are unambiguous.
Nearly all tools follow a verb_noun snake_case pattern (list_*, open_*, close_*, focus_*, minimize_*), with only system_info and open_path_with as minor deviations. The naming is highly predictable and makes the tool set easy to navigate.
At 27 tools, this is on the heavy side, but the broad desktop-control scope justifies the count: app management, folder windows, Chrome control, and system power actions are all represented. A few niche tools (scroll_chrome_page, restart_graphics_driver) could be considered optional, making it feel slightly bloated.
The tool set covers the major desktop workflows: app launch/focus/window-state, folder browsing and window management, Chrome tab/profile control, and system shutdown/restart. Minor gaps exist—no maximize folder window, no way to pass command-line args to open_app—but nothing blocks the core use cases.