Content
# WinReach MCP
[](https://github.com/GhouI/winreach-mcp/actions/workflows/ci.yml)
[](LICENSE)
[](https://modelcontextprotocol.io)
[](https://learn.microsoft.com/powershell/)
**Give your AI agent its own Windows machine.** Remote PowerShell, file transfer, screenshots, and full desktop control (computer use — mouse & keyboard) over HTTP — secured with per-user keys, roles, command policies, mTLS, and an audit log.
WinReach is a TypeScript [Model Context Protocol](https://modelcontextprotocol.io) server that runs on a Windows host and exposes it to AI agents over Streamable HTTP. Where most Windows MCP servers run locally over stdio on the user's own machine, **WinReach is remote-first and secured**: Codex, Claude Code, and other MCP-capable agents can run PowerShell, move files, capture the screen, and drive the mouse and keyboard on a Windows box from anywhere — each with its own bearer key, role, and command policy, every action written to an audit log, and a one-command Cloudflare tunnel to publish it with no inbound firewall hole.
```text
Agent or MCP client -> WinReach MCP over HTTP -> Windows (PowerShell + desktop)
```
## Demo
[](assets/winreach-demo.mp4)
Connect an agent over HTTP, run remote PowerShell, drive the Windows desktop, and keep every action behind per-user keys — in about 30 seconds.
▶ **[Watch the full-quality MP4](assets/winreach-demo.mp4)** · built with [Remotion](https://www.remotion.dev/) (source and re-render steps in [video/](video/README.md)).
There's also a vertical **4:5 LinkedIn cut** with burned-in captions — [assets/winreach-linkedin.mp4](assets/winreach-linkedin.mp4) (cover: [assets/winreach-linkedin-cover.png](assets/winreach-linkedin-cover.png), captions: [assets/winreach-linkedin.srt](assets/winreach-linkedin.srt)).
## Why WinReach?
Most coding agents are comfortable in terminals, but Windows RDP is a GUI-first environment. WinReach gives agents a clean command surface instead:
- Run PowerShell commands on a Windows host from any MCP client.
- Keep persistent PowerShell sessions when variables, cwd, or imported modules matter.
- Publish the server to your agent in one command with a built-in Cloudflare tunnel — no fixed public IP, no inbound firewall hole.
- Use bearer-token auth and provider firewalls instead of exposing raw RDP workflows.
- Test locally with a diagnostic client before connecting a real agent.
- Avoid IIS: WinReach is a standalone Node HTTP server.
## Install & Connect (npx)
No clone, no build. WinReach ships as an npm package with a `winreach-mcp` binary.
**1. Generate a token:**
```powershell
npx winreach-mcp gen-token
# -> a fresh random bearer token, e.g. 9V4P3Am6EjKsMJVetyrl-s60NVcP_MmDvsP_gFgcq3U
```
**2a. Local, zero-config (stdio) — fastest path.** Any stdio MCP client can launch WinReach directly on the Windows machine. No HTTP, no tunnel, no token wrangling (an ephemeral admin token is minted if you don't set one):
```jsonc
{
"mcpServers": {
"winreach": {
"command": "npx",
"args": ["-y", "winreach-mcp", "--stdio"]
}
}
}
```
**2b. Remote (HTTP + bearer token).** Run the server on the Windows host and connect agents over the network (optionally via the built-in Cloudflare tunnel):
```powershell
$env:WINREACH_TOKEN = "PASTE_TOKEN_FROM_STEP_1"
npx winreach-mcp # add --tunnel to publish a public Cloudflare URL
```
Copy-paste snippets for **Claude Code, Claude Desktop, Codex, and Cursor** (both local stdio and remote HTTP forms), plus the Claude Desktop one-click extension, are in **[docs/CONNECT.md](docs/CONNECT.md)**.
The `winreach-mcp` CLI:
| Command | What it does |
| --- | --- |
| `npx winreach-mcp` (or `start`) | Start the Streamable-HTTP MCP server (default; add `--tunnel` for a public URL). |
| `npx winreach-mcp --stdio` | Run over the local stdio transport for an MCP client that launches it. |
| `npx winreach-mcp gen-token` | Print a fresh random bearer token. |
## Tooling
WinReach exposes up to eight MCP tools (the last three are opt-in):
| Tool | Purpose |
| --- | --- |
| `powershell_execute` | Run one isolated PowerShell command. |
| `powershell_open_session` | Start a persistent PowerShell session. |
| `powershell_send` | Send a command to a persistent session. |
| `powershell_close_session` | Close a persistent session. |
| `powershell_list_sessions` | List active sessions. |
| `take_screenshot` | Capture the current screen of the Windows host as an image. Off by default (see below). |
| `computer_use` | Control the desktop like a human — move/click the mouse, type text, press key chords, and scroll — via Win32 `SendInput`. Coordinates match `take_screenshot`. Off by default; admin/operator roles recommended (see below). |
| `file_upload` | Write a file to the host, inside the configured file root. Off by default (see below). |
| `file_download` | Read a file from the host (optionally moving it) as base64. Off by default (see below). |
Command results include:
```json
{
"commandId": "uuid",
"stdout": "hello\r\n",
"stderr": "",
"exitCode": 0,
"durationMs": 143,
"truncated": false
}
```
`take_screenshot` captures the full virtual desktop (all monitors) and returns an
MCP image block (base64 PNG by default, `jpeg` optional) alongside a metadata
block:
```json
{
"commandId": "uuid",
"success": true,
"format": "png",
"mimeType": "image/png",
"width": 1920,
"height": 1080,
"bytes": 82344,
"durationMs": 96
}
```
Screen capture needs an active interactive desktop session. If WinReach runs in
a non-interactive service context (Windows session 0), the capture fails and the
tool returns `success: false` with the PowerShell error instead of an image.
Because screen capture reads whatever is on the desktop, it is a
read/exfiltration capability the command policy cannot express, so it is
**disabled by default**. Enable it with `WINREACH_ALLOW_SCREENSHOT=1`, and
optionally restrict it to specific principal roles with `WINREACH_SCREENSHOT_ROLES`.
When disabled (or when the caller's role is not permitted) the tool is not
exposed to that principal at all. Captures are written to a **server-owned**
directory (`WINREACH_SCREENSHOT_DIR`) — callers cannot choose the path — and are
automatically deleted after `WINREACH_SCREENSHOT_RETENTION_HOURS` (default `8`)
so they do not accumulate on the host.
### File transfer
`file_upload` and `file_download` move files between the agent and the Windows
host over MCP (base64-encoded, SHA-256 returned for integrity). They are **off by
default** and only registered once you set a sandbox root:
```powershell
$env:WINREACH_FILE_ROOT = "C:\winreach-files"
```
- **Every path is relative to `WINREACH_FILE_ROOT`.** Absolute paths, `..`
traversal, and symlinks that escape the root are rejected, so a transfer can
never touch files outside the sandbox. Setting the root is what enables the
tools.
- `file_upload` refuses to overwrite unless `overwrite: true`, and creates
parent directories inside the root as needed.
- `file_download` returns the file as base64; set `deleteSource: true` to **move**
it (the server copy is deleted after a successful read).
- Files larger than `WINREACH_MAX_FILE_BYTES` (default 75 MB) are rejected in
either direction.
- Every transfer is written to the audit log (tool, principal, server path, size).
## Quickstart
Requirements:
- Node.js 24 or newer
- npm
- Windows PowerShell (`powershell.exe`) or PowerShell 7 (`pwsh`)
Run WinReach locally:
```powershell
git clone https://github.com/GhouI/winreach-mcp.git
cd winreach-mcp
npm install
$env:WINREACH_TOKEN = "dev-token"
npm run dev
```
The server defaults to:
```text
http://127.0.0.1:7573/mcp
```
Use the diagnostic client from another terminal:
```powershell
$env:WINREACH_TOKEN = "dev-token"
npm run client -- list-tools
npm run client -- exec Write-Output hello
```
Target more than one WinReach server with `WINREACH_URLS`:
```powershell
$env:WINREACH_TOKEN = "shared-token"
$env:WINREACH_URLS = "http://win-1:7573/mcp,http://win-2:7573/mcp"
npm run client -- exec hostname
```
## Public Access With One Command
When you set up WinReach on a server and want an agent to reach it from anywhere, enable the built-in [Cloudflare quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/). WinReach downloads `cloudflared` on first use (no Cloudflare account needed), opens a tunnel, and prints a ready-to-paste agent config.
```powershell
$env:WINREACH_TOKEN = "replace-with-a-long-random-token"
$env:WINREACH_TUNNEL = "cloudflare"
npm run dev
```
Output:
```text
WinReach MCP listening at http://127.0.0.1:7573/mcp
Cloudflare tunnel ready: https://random-words.trycloudflare.com
Public MCP endpoint: https://random-words.trycloudflare.com/mcp
Connect an agent with this public endpoint (the bearer token is still required):
Claude Code:
claude mcp add --transport http winreach https://random-words.trycloudflare.com/mcp --header "Authorization: Bearer <WINREACH_TOKEN>"
...
```
You can also enable it ad hoc with a flag:
```powershell
npm run dev -- --tunnel
```
Notes:
- `cloudflared` connects to WinReach over loopback, so tunnel mode needs **no** `0.0.0.0` bind and **no** inbound firewall rule. WinReach tells `cloudflared` to rewrite the forwarded `Host` header to `127.0.0.1` so the server's built-in DNS-rebinding protection keeps working.
- Tunnel mode publishes a remote-command server to the public internet, protected **only** by the bearer token. Use a long random `WINREACH_TOKEN` (32+ characters); WinReach warns at startup if the token looks weak.
- Quick-tunnel hostnames are random and **change every restart**. Re-paste the printed URL into your agent, or move to a [named Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) for a stable hostname.
- The bearer token is still enforced over the tunnel. Treat the public URL as sensitive and rotate the token after demos.
- Auto-install downloads and runs the official `cloudflared` binary from GitHub releases over HTTPS. If you need a verified/pinned binary, set `WINREACH_TUNNEL_AUTOINSTALL=0` and point `WINREACH_CLOUDFLARED_PATH` at a `cloudflared` you installed and checked yourself.
## Connect Agents
Copy-paste snippets for Claude Code, Claude Desktop, Codex, and Cursor (local
stdio and remote HTTP forms) plus the one-click Claude Desktop extension:
**[docs/CONNECT.md](docs/CONNECT.md)**.
Full setup guide: [Install WinReach and Connect Agents](docs/INSTALL_AND_AGENT_USAGE.md)
Feature overview: [WinReach Features](docs/FEATURES.md)
Codex `~/.codex/config.toml`:
```toml
[mcp_servers.winreach]
url = "http://WINDOWS_SERVER_IP:7573/mcp"
bearer_token_env_var = "WINREACH_TOKEN"
tool_timeout_sec = 120
default_tools_approval_mode = "prompt"
enabled = true
```
Claude Code:
```powershell
claude mcp add --transport http winreach http://WINDOWS_SERVER_IP:7573/mcp `
--header "Authorization: Bearer YOUR_TOKEN"
```
Also see:
- [Codex config example](examples/codex-config.toml)
- [Claude Code MCP JSON example](examples/claude.mcp.json)
## Remote Windows Deployment
On the Windows host:
```powershell
git clone https://github.com/GhouI/winreach-mcp.git
cd winreach-mcp
npm install
$env:WINREACH_TOKEN = "replace-with-a-long-random-token"
$env:WINREACH_HOST = "0.0.0.0"
$env:WINREACH_PORT = "7573"
npm run dev
```
Open the Windows firewall for the MCP port:
```powershell
New-NetFirewallRule `
-DisplayName "WinReach MCP 7573" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 7573 `
-Action Allow
```
Restrict your cloud firewall so TCP `7573` is reachable only from trusted IP addresses. If you only need agent access (not raw port access), prefer [tunnel mode](#public-access-with-one-command) and keep the bind on `127.0.0.1`.
## Configuration
Prefer a UI? The [`setup-web`](setup-web/) app (Next.js) generates the env vars,
a `start.ps1`, a Windows firewall rule for your allowed IPs, and agent-connect
snippets from a form — see [setup-web/README.md](setup-web/README.md).
WinReach reads `WINREACH_*` variables.
| Variable | Default | Description |
| --- | --- | --- |
| `WINREACH_TOKEN` | required* | Bearer token for a single full-access admin. *Required unless `WINREACH_PRINCIPALS` is set. |
| `WINREACH_PRINCIPALS` | empty | JSON array of per-user principals: `[{"name","role","token"\|"tokenEnv"\|"tokenHash","allow":[],"deny":[],"tools":[]}]`. Enables per-user authorization. A key may be plaintext (`token`/`tokenEnv`) or a SHA-256 hex `tokenHash` (a presented token authenticates when its SHA-256 matches — so an external store never needs the plaintext). `tools` optionally limits a principal to specific tool names (omit for all). |
| `WINREACH_COMMAND_ALLOWLIST` | empty | Regex allowlist (comma-separated or JSON array). If non-empty, only matching commands run. |
| `WINREACH_COMMAND_DENYLIST` | empty | Regex denylist (comma-separated or JSON array). Matching commands are blocked; deny wins over allow. |
| `WINREACH_AUDIT_LOG` | empty | Path to an append-only JSONL audit log of every tool call. |
| `WINREACH_TLS_CERT` | empty | PEM certificate path. Set with `WINREACH_TLS_KEY` to serve HTTPS in-app. |
| `WINREACH_TLS_KEY` | empty | PEM private key path for TLS. |
| `WINREACH_TLS_KEY_PASSPHRASE` | empty | Passphrase for an encrypted TLS key. |
| `WINREACH_TLS_CLIENT_CA` | empty | PEM CA bundle to verify client certificates. Enables mutual TLS (requires TLS). |
| `WINREACH_ALLOW_SCREENSHOT` | `0` | Set to `1` to enable the `take_screenshot` tool. Disabled by default. |
| `WINREACH_SCREENSHOT_ROLES` | empty | Comma-separated principal roles allowed to capture. Empty means any authenticated principal (when enabled). |
| `WINREACH_SCREENSHOT_DIR` | temp subdir | Server-owned directory captures are written to. Callers cannot override it. |
| `WINREACH_SCREENSHOT_RETENTION_HOURS` | `8` | Captures older than this are deleted (on startup and before each capture). Set `0` to keep them indefinitely. |
| `WINREACH_ALLOW_COMPUTER_USE` | `0` | Set to `1` to enable the `computer_use` desktop-input tool. Disabled by default. GUI control bypasses the command policy, so grant it to trusted roles only. |
| `WINREACH_COMPUTER_USE_ROLES` | empty | Comma-separated roles allowed to drive the desktop. Empty means any authenticated principal (when enabled). |
| `WINREACH_COMPUTER_USE_MAX_ACTIONS_PER_SEC` | `10` | Per-principal rate cap on input actions. |
| `WINREACH_COMPUTER_USE_KEY_DENYLIST` | empty | Comma-separated regexes for key chords to block (e.g. `win\+r`). A speed bump, not a boundary. |
| `WINREACH_COMPUTER_USE_HALT_FILE` | empty | If this file exists, all `computer_use` actions are refused — an operator kill switch. |
| `WINREACH_COMPUTER_USE_AUDIT_TEXT` | `0` | Set to `1` to record raw typed text in the audit log. Off by default (length + truncated hash only). |
| `WINREACH_FILE_ROOT` | empty | Sandbox root for `file_upload`/`file_download`. Setting it enables the tools; all transfer paths resolve within it. |
| `WINREACH_MAX_FILE_BYTES` | `78643200` | Maximum bytes per transferred file (both directions). Default 75 MB. |
| `WINREACH_URL` | `http://127.0.0.1:7573/mcp` | Diagnostic client URL for one WinReach server. |
| `WINREACH_URLS` | empty | Diagnostic client comma-separated URLs for multiple servers using `WINREACH_TOKEN`. |
| `WINREACH_TARGETS` | empty | Diagnostic client JSON array for named servers and per-target token env vars. |
| `WINREACH_HOST` | `127.0.0.1` | Bind host. Use `0.0.0.0` only behind a firewall or tunnel. |
| `WINREACH_PORT` | `7573` | Bind port. |
| `WINREACH_ENDPOINT_PATH` | `/mcp` | MCP endpoint path. |
| `WINREACH_ALLOWED_ORIGINS` | empty | Comma-separated allowed `Origin` values. |
| `WINREACH_SHELL_PATH` | auto | Explicit `pwsh` or `powershell.exe` path. |
| `WINREACH_CWD` | process cwd | Default working directory. |
| `WINREACH_TIMEOUT_MS` | `30000` | Default command timeout. |
| `WINREACH_MAX_OUTPUT_BYTES` | `1048576` | Max captured bytes per output stream. |
| `WINREACH_TUNNEL` | empty | Set to `cloudflare` to publish the server through a Cloudflare quick tunnel. |
| `WINREACH_TUNNEL_AUTOINSTALL` | `1` | Auto-download `cloudflared` when missing. Set to `0` to require a preinstalled binary. |
| `WINREACH_CLOUDFLARED_PATH` | auto | Explicit path to the `cloudflared` binary. |
## Security
WinReach is a remote command-execution server. Treat it as sensitive infrastructure.
- Use a long random `WINREACH_TOKEN`.
- Keep `WINREACH_HOST=127.0.0.1` unless you have a trusted network path or are using tunnel mode.
- Do not expose WinReach directly to the public internet on a raw port.
- Restrict TCP `7573` at your provider firewall.
- Tunnel URLs are still protected by the bearer token, but treat them as secrets and rotate after demos.
- Run as a dedicated low-privilege Windows user when possible.
- Rotate tokens after demos, testing sessions, and shared access.
### Hardening
WinReach can now enforce transport security, per-user authorization, command policy, and auditing without an external proxy.
**Serve HTTPS in-app (optionally with mutual TLS):**
```powershell
$env:WINREACH_TOKEN = "replace-with-a-long-random-token"
$env:WINREACH_TLS_CERT = "C:\certs\winreach-cert.pem"
$env:WINREACH_TLS_KEY = "C:\certs\winreach-key.pem"
# Require client certificates (mTLS): connections without a cert signed by this CA
# are dropped during the TLS handshake, before the bearer-token check.
$env:WINREACH_TLS_CLIENT_CA = "C:\certs\client-ca.pem"
npm run dev
```
**Give each client its own identity, role, and command policy:**
```powershell
$env:WINREACH_PRINCIPALS = @'
[
{ "name": "ci", "role": "admin", "tokenEnv": "CI_TOKEN" },
{ "name": "agent", "role": "readonly", "tokenEnv": "AGENT_TOKEN",
"allow": ["^Get-", "^Test-"], "deny": ["Remove-Item", "Stop-Service"],
"tools": ["powershell_execute"] }
]
'@
```
**Apply a deployment-wide guardrail and record everything:**
```powershell
$env:WINREACH_COMMAND_DENYLIST = "Remove-Item -Recurse,Format-Volume,Stop-Computer"
$env:WINREACH_AUDIT_LOG = "C:\logs\winreach-audit.jsonl"
```
The global policy plus the caller's principal policy must both allow a command; deny always wins, and a non-empty allowlist blocks anything it does not match. Blocked calls return an MCP error and are written to the audit log.
**Install as a Windows service under a dedicated account:**
```powershell
npm run build
$pw = Read-Host -AsSecureString "Service account password"
./scripts/install-service.ps1 -EnvFile .env -ServiceAccount ".\winreach" -ServiceAccountPassword $pw
# Remove later with: ./scripts/uninstall-service.ps1
```
See [SECURITY.md](SECURITY.md) for the full control matrix and what remains out of scope.
## Roadmap
Shipped: in-app HTTPS/mTLS, per-client authorization, command allow/deny policy, audit logging, and a Windows service installer (see [Hardening](#hardening)).
Still planned:
- Named Cloudflare tunnels for stable hostnames
- Request-level Windows Integrated Authentication (Negotiate/NTLM)
- Rate limiting and per-principal quotas
- Git Bash support
- Packaged releases
## Contributing
Issues, ideas, and pull requests are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md).
## License
MIT. See [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available