win-computer-use
by Regigicas
README.md
# win-computer-use
An MCP server that gives **Claude Code computer use on Windows** — screenshots, mouse, keyboard,
window and display enumeration — which the CLI officially ships on macOS only:
```
createCliExecutor called on win32. Computer control is macOS-only.
```
It does not reimplement anything at a low level. It reuses the native binding that the **Claude
desktop app** installs (`@ant/claude-native`), which loads fine in plain Node, and adds screen
capture on top through GDI (the desktop app captures with Electron's `desktopCapturer`, which does
not exist outside Electron).
> [!WARNING]
> **By default this gives a model unrestricted control of your desktop.** Keystrokes and clicks go
> to whatever window currently has focus, and a screenshot captures the *entire* screen — password
> managers, private messages, anything else you have open.
>
> [An allowlist](#restricting-which-applications-it-can-touch) can restrict input to named
> applications, but it is **opt-in** and it does not restrict what the screenshot sees. Of the four
> safeguards the Claude desktop app has, this implements one: there is no screenshot masking, no
> on-screen indicator while computer use is active, and no panic key. Run it only on a machine where
> that is acceptable to you — and since the install below registers it for *every* session on that
> machine, decide deliberately whether to pair it with an allowlist.
> [!NOTE]
> Unofficial, unsupported, and not affiliated with Anthropic. It depends on internals of the desktop
> app: if the binding or the MSIX package layout changes, this breaks.
## Requirements
- **Windows 10 or 11.** Windows-only by construction; the server exits with an error elsewhere.
- **The Claude desktop app, installed** (the MSIX package from the Microsoft Store or the official
installer). It is where the native binding comes from — the app does not have to be *running*, it
has to be *installed*.
- **Node.js 18+.**
- **Windows PowerShell 5.1** (the built-in `powershell.exe`), used for screen capture.
No npm dependencies.
## Install
Clone it anywhere, then register it with the Claude Code CLI:
```powershell
git clone https://github.com/Regigicas/win-computer-use.git
cd win-computer-use
claude mcp add win-computer-use --scope user -- node "$PWD\server.js"
```
Restart the session for the tools (`mcp__win-computer-use__*`) to appear. Check it with
`claude mcp list`; remove it with `claude mcp remove win-computer-use`.
`--scope user` registers it once for every session on the machine, which is usually what you want
from a tool that drives the desktop rather than a project. It also means *any* session can reach
your desktop, so it is the case where an
[allowlist](#restricting-which-applications-it-can-touch) earns its keep. Use `--scope local`
instead to keep it to the project you are in.
For any other MCP client, the equivalent config entry is:
```json
{
"mcpServers": {
"win-computer-use": {
"command": "node",
"args": ["C:\\path\\to\\win-computer-use\\server.js"]
}
}
}
```
## Tools
| | |
| --- | --- |
| **Screen** | `screenshot`, `zoom`, `list_displays` |
| **Windows** | `list_windows`, `focus_window`, `frontmost_app`, `list_running_apps` |
| **Mouse** | `move_mouse`, `click`, `drag`, `scroll`, `cursor_position` |
| **Keyboard** | `type_text`, `key` |
| **Composite** | `batch` |
| **Policy** | `allowlist_status` |
Coordinates are **screen pixels** (display origin + offset), never pixels of the returned image;
every capture states the scale it used and how to convert. `batch` chains several actions and ends
with a screenshot, so a click-type-check cycle costs one round-trip instead of three.
### Limits and defaults
| | |
| --- | --- |
| `screenshot` scale | Auto-fits the width to ~1400 px; override with `scale` (0.05–1) |
| `zoom` scale | 1 (native pixels); values above 1 are not supported |
| JPEG quality | 80 |
| `type_text` | 2000 characters per call, ~8 ms per character |
| `batch` | 30 actions per call; `wait` takes 0–5000 ms |
| Capture timeout | 15 s |
## Restricting which applications it can touch
With no allowlist configured — the default — input reaches whatever window has focus. Configure one
and the tools that inject input (`click`, `drag`, `scroll`, `type_text`, `key`) refuse to act unless
the foreground window belongs to an allowed application, and `focus_window` refuses to bring a
non-allowed one forward.
Set it with an environment variable, which wins over everything else:
```powershell
claude mcp add win-computer-use --scope user `
-e WIN_COMPUTER_USE_ALLOWLIST="notepad.exe,*brave*" -- node "$PWD\server.js"
```
or in `%LOCALAPPDATA%\win-computer-use\allowlist.json` (point `WIN_COMPUTER_USE_ALLOWLIST_FILE`
elsewhere if you prefer):
```json
{ "allow": ["notepad.exe", "Microsoft.WindowsNotepad_8wekyb3d8bbwe!App", "*brave*"] }
```
The file is re-read whenever it changes on disk, so you can widen or narrow the policy in the middle
of a session without restarting the server or the client.
| Value | Meaning |
| --- | --- |
| unset, and no file | **every application allowed** — the behaviour before the allowlist existed |
| `*` or `all` | every application allowed, said explicitly |
| `none`, or `"allow": []` | nothing allowed: enumeration and screenshots only |
| a list of patterns | only those applications |
Patterns are case-insensitive and take `*` and `?`. Each one is matched against the full `bundleId`
(`c:\windows\system32\notepad.exe`, or an MSIX identifier such as
`Microsoft.WindowsNotepad_8wekyb3d8bbwe!App`), its file name with and without `.exe`, the package
family and package name of an MSIX identifier, and the friendly display name — so `notepad.exe`,
`*notepad*` and the full identifier all work. `list_windows` gains an `allowed` flag while a policy
is in force, and `allowlist_status` reports the policy and whether the app in front right now is on
it.
**No tool can change the allowlist.** A model able to grant itself applications would not be
constrained by one; only the environment variable and the file decide. If the file cannot be parsed
the server fails closed and refuses input until it is fixed.
Each action is checked on its own — including every action inside a `batch` — against:
- the application owning the foreground window;
- the application under the target coordinates of a `click`, `drag` or `scroll`, so a background
window or the desktop behind everything is not a valid target;
- for `type_text`, that focus has not moved elsewhere mid-string; if it does, typing stops and
reports how many characters were delivered.
### What it does not cover
**Captures.** `screenshot` and `zoom` return the whole display whether or not an allowlist is set.
The desktop app masks non-allowlisted windows out of the image (`cuMaskedWindowRects` is right there
in the binding); that is not implemented here. An allowlist limits what can be *done*, not what can
be *seen*.
### How the desktop app differs
Worth knowing if you expect identical behaviour. With an allowlist active the desktop app *hides*
every non-allowed application before acting (`cuHideApps`, then up to five more rounds against
whatever is still frontmost) and masks them out of the screenshot, and it grants each application a
tier: `read` (visible in screenshots, no input), `click` (plain left click only) or `full`. This
server does neither — it refuses the action instead of rearranging your desktop, and every allowed
application is at the equivalent of `full`.
## Files
| File | What it is |
| --- | --- |
| `server.js` | The MCP server (JSON-RPC over stdio, no dependencies). |
| `capture.ps1` | Persistent capture helper (PowerShell 5.1 + System.Drawing). |
| `smoke.js` | Test client: `node smoke.js '[{"name":"screenshot"}]'`. |
## How the binding is resolved
`C:\Program Files\WindowsApps` cannot be listed from an ordinary process, and the `.node` cannot be
loaded in place (`Acceso denegado` / access denied) — but it *can* be read and copied. So the server:
1. Resolves the registered package with `Get-AppxPackage -Name Claude`.
2. Copies `claude-native-binding.node` into `%LOCALAPPDATA%\win-computer-use\` and loads that copy,
re-copying whenever the original's size or mtime changes.
Set `CLAUDE_NATIVE_BINDING=<path>` to skip all of that and load a specific file.
**Nothing from the desktop app is redistributed by this repository.** The binding is copied at
runtime, on your machine, out of your own installation.
## What it writes to disk
- `%LOCALAPPDATA%\win-computer-use\` — the copy of the native binding, plus a `source.json` stamp
recording where it came from. Persistent; safe to delete, it is re-copied on the next run. The
optional `allowlist.json` lives here too, and is only ever *read*.
- `%TEMP%\win-computer-use\` — **each screenshot is written here as a JPEG** before being read back
and sent, then deleted immediately. Deletion is best-effort: if the server is killed mid-capture,
an image of your screen can be left behind in your temp directory.
Nothing is sent anywhere by this server itself; it only answers the MCP client that spawned it.
## Things learned the hard way
- **`typeText` and `typeTextPaced` break on long strings when driven from outside the desktop app**:
the first characters land and the rest come out as the last character repeated, or nothing at all.
That is why `type_text` sends **one character per call**. It costs about a second per 100
characters, but it is exact. Do not "optimise" it back.
- **Shortcuts mean whatever the target app makes them mean, in its own language.** On a Spanish
Windows, `ctrl+a` in Notepad is *Abrir* (open) and select-all is `ctrl+e`. The binding maps keys
correctly; the app on the other end is what differs.
- **Key names are lowercase**: `ctrl`, `alt`, `shift`, `win`, `return`, `escape`, `tab`, `space`,
`backspace`, `delete`, arrows, `home`, `end`, `pageup`, `pagedown`, `f1`–`f12`, and single
characters. The internal enum names (`Num1`, …) throw. Chords go through `keys([...])`, which is
the same API the desktop app uses for `ctrl+v`.
- **GDI capture looks correct in SDR; on an HDR display it can come out washed out.** That is a
`CopyFromScreen` limitation, not the state of your screen.
- The GDI capture does not draw the mouse cursor.
## Troubleshooting
**`the Claude desktop app does not appear to be installed`** — `Get-AppxPackage -Name Claude`
returned nothing. Install the desktop app, or point `CLAUDE_NATIVE_BINDING` at a copy of the binding.
**`binding not found at ...`** — the package layout changed. Look for
`claude-native-binding.node` under the install location and set `CLAUDE_NATIVE_BINDING`.
**Behaviour that does not match the installed version** — `Get-AppxPackage` returns the *registered*
package, which is not necessarily the newest one *staged* on disk. If the app has updated but the
binding behaves like an older build, that mismatch is the first place to look.
**`capture helper could not start`** — `powershell.exe` could not be spawned: check that Windows
PowerShell 5.1 is present and not blocked by policy.
**`blocked by the allowlist: ...`** — an allowlist is configured and the app in front (or under the
click) is not on it. `allowlist_status` prints the policy, where it came from, and what is in front;
[the allowlist section](#restricting-which-applications-it-can-touch) covers how to change it.
**Nothing happens in the target app** — elevated windows (running as administrator) will not accept
injected input from a non-elevated process, and the UAC prompt runs on an isolated desktop that
cannot be automated at all. Fullscreen games generally reject the injection too.
## Limitations and what is untested
See [`docs/limitations.md`](docs/limitations.md) for what has actually been exercised and what has
only been written.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues