Skip to main content
Glama
README.md
# mcp-windows-debug

[![CI](https://github.com/wgm66/mcp-windows-debug/actions/workflows/ci.yml/badge.svg)](https://github.com/wgm66/mcp-windows-debug/actions/workflows/ci.yml)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A TypeScript/Node.js MCP server that plugs into OpenCode over stdio and gives
the model eyes and hands on a Windows machine: it reads project files, captures
screenshots, moves the mouse and types keys, and runs an auto-debug loop against
a target application.

Safety is the point of the whole design. A separate native C++ watchdog process
installs global low-level keyboard and mouse hooks so a human can always click a
protected abort button, even while the model is injecting input. The Node MCP
server and the watchdog are two independent processes, so a stalled Node event
loop cannot freeze your input or silently drop the safety layer. Every action
flows through three gates: a governor, a freshness check, and a window-scoping
guard. Details are in the Security model section below.

This is a Windows-only v1. macOS and Linux backends plug in later behind the
same provider interfaces; they are not implemented yet.

## Quick start

```powershell
git clone https://github.com/wgm66/mcp-windows-debug.git
cd mcp-windows-debug
npm install && npm run build
cd src\watchdog && build.bat   # build the C++ watchdog (MSVC required)
node dist\index.js --validate-config  # verify your OpenCode config
```

## Installation

Prerequisites:

- Node.js 20 or newer, plus npm
- Windows 10 or 11
- Administrator access, needed only to run the watchdog (see below)

Install dependencies and build the TypeScript:

```powershell
npm install
npm run build
```

`npm run build` runs `tsc` and produces `dist/index.js`, which is the entry
point OpenCode launches.

Next, build the watchdog. It is a C++ Win32 console app compiled with MSVC, no
CMake, MSBuild, or MinGW involved:

```bat
cd src\watchdog
build.bat
```

`build.bat` requires the VS2019 Build Tools (MSVC 14.29) and the Windows SDK.
The toolchain paths are hardcoded in the script, so it expects them at their
default install locations. The output is `src\watchdog\watchdog.exe`, which the
Node server locates relative to the project root at runtime.

The watchdog must run elevated. Global low-level hooks refuse to install from a
non-elevated process. Two ways to satisfy this:

1. Start OpenCode from an elevated terminal, so the spawned watchdog inherits
   elevation.
2. Pre-start the watchdog as admin yourself before starting a debug session.

The server cannot request UAC elevation on its own in this build. A debug
session that cannot reach an elevated watchdog fails with
`ELEVATION_REQUIRED`, and a non-elevated watchdog run prints
`ERROR_ACCESS_DENIED` and exits with code 1 rather than silently doing nothing.

## OpenCode configuration

Add a `windows-debug` entry under the `mcp` key in your OpenCode config
(`opencode.json`). Note the key is `mcp`, not `mcpServers`:

```json
{
  "mcp": {
    "windows-debug": {
      "type": "local",
      "command": ["node", "<abs-path>/dist/index.js"],
      "environment": {}
    }
  }
}
```

Replace `<abs-path>` with the absolute path to this project, using forward
slashes so the JSON needs no escaping. For example, if the project lives at
`G:\工程开发\AI全场景图形化调试`, the command becomes:

```json
"command": ["node", "G:/工程开发/AI全场景图形化调试/dist/index.js"]
```

The `command` is an array of argv tokens. The `environment` map is empty by
default; the per-session watchdog token is generated by the server itself and
passed to the watchdog over the process environment, so you do not need to set
anything here.

## Usage

A debug session has a fixed shape: register protected abort buttons, start the
session, let the model work through the auto-debug loop, then end the session.

**Register abort buttons.** A session cannot start with zero protected regions.
Pass one or more screen rectangles to `start_debug_session` as `regions`
(`{ x, y, w, h, id }`, physical pixels). Injected input aimed inside any
registered region is blocked by the watchdog. Human input always passes, so the
region is a guaranteed physical abort area the model cannot reach. Regions are
append-only for the session lifetime; there is deliberately no way to remove or
move one after start.

**Start the session.** `start_debug_session` spawns or attaches the watchdog,
registers every region, and starts the heartbeat. The orchestrator begins
monitoring the current foreground window as the debug target. Pass
`sandbox: 'desktop'` to run injection on a private Win32 desktop
(PostMessage-based, user's real mouse/keyboard untouched) instead of
`SendInput` (which moves the real cursor). `sandbox: 'rdp'` is reserved but
not implemented in v1.

**The auto-debug loop.** While the session is active, the orchestrator polls the
target window for changes: title, rectangle, foreground status, and optionally a
screenshot-signature diff. When a trigger fires, it captures a fresh screenshot
and exposes it as the `debug://context` resource. The client (OpenCode) polls
`debug://context`, decides what to do, and calls `execute_action` with that
decision. The orchestrator never decides actions on its own; it only executes
client decisions, and only after the governor, freshness, and safety gates all
pass.

**End the session.** `end_debug_session` sends SHUTDOWN, kills the watchdog if
it does not respond within one second, releases any held modifier keys, and
returns to IDLE. If the MCP process dies without a clean shutdown, the watchdog's
dead-man switch removes the hooks on its own (see the Security model section).

### Tools

Ten tools are registered.

| Tool | Purpose |
| --- | --- |
| `read_file` | Read a text file from an absolute path; binary files return base64. |
| `list_directory` | List the immediate entries of a directory. |
| `capture_window` | Capture a window by exact title as a PNG; empty title means the frontmost window. |
| `mouse_click` | Click at logical screen coordinates with a given button. |
| `mouse_move` | Move the cursor to logical screen coordinates. |
| `key_press` | Press a key, optionally holding modifiers. |
| `type_text` | Type a text string as keyboard input. |
| `start_debug_session` | Spawn or attach the watchdog and register protected abort regions. Accepts optional `sandbox: 'desktop'` for isolated PostMessage injection. |
| `end_debug_session` | End the active session and shut down the watchdog. |
| `execute_action` | Execute a client-decided action inside the active session. |
| `inspect_element` | Enumerate visible UI elements (name, role, rect, enabled) via UIAutomation tree walker. |

The four input tools (`mouse_click`, `mouse_move`, `key_press`, `type_text`)
all route through the safety layer's `injectGuarded` gate. Calling them with no
active session returns `NO_ACTIVE_SESSION`. Calling them while the cursor or
keyboard focus is outside the target window returns
`WINDOW_SCOPE_VIOLATION`.

### Resources

Three resources are registered.

| URI | Content |
| --- | --- |
| `screenshot://full` | PNG capture of the primary monitor. |
| `screenshot://monitor/{index}` | PNG capture of a specific monitor by 0-based index. |
| `debug://context` | JSON snapshot of the auto-debug loop: status, target, trigger, screenshot, governor state. |

### Governor limits

The orchestrator enforces a fixed throttle on interventions:

- 5 second cooldown between actions
- 6 interventions per minute
- auto-pause after 3 consecutive failures
- hard 30-minute session cap, after which the session auto-ends

Rejections for cooldown, rate limit, or pause are throttling, not failures.
Only a stale-state refusal or an injection error counts toward the 3-failure
pause.

## Security model

What this design guarantees, and what it does not.

**Dual-process isolation.** The Node MCP server and the native watchdog are
separate processes. A stuck Node event loop cannot block the hooks or drop the
safety layer, because the watchdog runs its own message loop.

**Dead-man switch.** The watchdog listens on a named pipe and treats any byte as
a heartbeat. If no heartbeat arrives for more than 2 seconds, it calls
`UnhookWindowsHookEx` on both hooks and exits cleanly. Combined with the removal
grace period, hooks come down within 3 seconds of MCP death, so a crashed or
killed server never leaves input blocked. This is the fail-safe contract; it is
not a sub-second guarantee.

**Window scoping.** Every injection is refused unless a session is active and
the cursor and keyboard focus are inside the session target window.

**Secure-desktop handling.** If the OS switches to the secure desktop (UAC
prompt or lock screen), the orchestrator pauses and refuses injection with zero
input attempted.

**Append-only audit.** Every file read, injected action, screenshot request, and
intervention decision is logged to an append-only audit log. Keystroke content
and file content are never written to it.

**What it does NOT guarantee.** Read this part carefully, because these are the
honest residual risks.

- **Injected-input filtering is not absolute blocking.** The watchdog blocks
  input carrying the `LLKHF_INJECTED` / `LLMHF_INJECTED` flags when the
  destination falls inside a protected region. That stops machine-injected
  input, which is what `SendInput` produces. It does not stop every possible
  input source. Another process could theoretically synthesize non-flagged
  input by other means, and that input would pass the filter. This tool does
  not claim absolute physical blocking. Treat the abort button as a strong,
  best-effort safety net, not a mathematical guarantee.
- **It is a remote-control primitive in the worst case.** The full tool surface
  is file read plus screenshot capture plus keyboard and mouse injection. If an
  attacker or a misbehaving model controls it, that is the capability they get.
  Use it on a machine and against windows you are willing to have that surface
  pointed at.
- **Antivirus and EDR can flag it.** Global low-level hooks and `SendInput`
  injection are exactly the techniques remote-access tools and keyloggers use.
  Expect false positives from AV/EDR products, including the watchdog being
  quarantined or killed mid-session. The dead-man switch makes that safe (hooks
  come down), but it will interrupt sessions. See Troubleshooting.
- **Elevation widens the surface.** The watchdog needs admin to install global
  hooks, so a session runs with an elevated process in the picture. Do not run
  it on a machine where that exposure is unacceptable.

No keystroke or button content is ever read or logged by the watchdog; only the
injected flag and the cursor destination are inspected. Transport is the local
named pipe only. There is no TCP, no network listener, no remote control.

## Troubleshooting

**Antivirus or EDR flags the watchdog.** Add an exclusion for
`src\watchdog\watchdog.exe` (or the project directory) in your AV/EDR console.
The durable fix is code signing: a signed binary is far less likely to be
quarantined. If the watchdog gets killed mid-session, the session transitions to
IDLE and all input tools are refused until a new `start_debug_session`.

**Windows detaches the hook (`LowLevelHooksTimeout`).** Low-level hook
procedures have a hard execution budget, controlled by
`HKCU\Control Panel\Desktop\LowLevelHooksTimeout` (default 300 ms). If the hook
proc runs too long, Windows silently removes it. The watchdog keeps its hook
proc well under 100 ms, so this should not trigger in normal use. If you see
hooks dropping on a heavily loaded machine, the problem is system load or
interference from another low-level hook, not this tool.

**Clicks land at the wrong place on a multi-monitor or mixed-DPI setup.**
Coordinates are mapped between logical and physical pixels using per-monitor
DPI. On mixed-DPI multi-monitor setups there is a known limitation: the logical
to physical conversion passes logical coordinates to a call that expects
physical pixels. It is harmless at 96 DPI but can drift on scaled monitors. If a
click misses, capture a screenshot first, read the target coordinates from it,
and prefer working on the primary monitor.

**A UAC prompt appears, or injection silently fails.** The watchdog runs
elevated, so spawning it can surface a UAC prompt. If you cancel it, the session
fails with `ELEVATION_REQUIRED`. The server cannot re-request elevation on its
own in this build, so pre-start the watchdog as admin before starting the
session, or launch OpenCode from an elevated terminal.

**`ERROR_ACCESS_DENIED` when running the watchdog manually.** This is the
expected behavior for a non-elevated shell. The watchdog refuses to run without
admin and prints `ERROR_ACCESS_DENIED` with exit code 1, so there is no silent
no-op. Run it from an elevated PowerShell instead.

## Playwright MCP compatibility

The server is compatible with the Playwright MCP server out of the box —
no flags or configuration changes are needed. `SetProcessDpiAwarenessContext`
is deferred to the first actual input call (lazy init), so two MCP servers
running side by side do not interfere with each other's DPI settings.

When `windows-debug` first executes an input tool (`mouse_click`, `key_press`,
etc.), it sets per-monitor DPI awareness at that point — which is correct,
because at that moment it is `windows-debug` doing the injection. Before any
input call, the process DPI mode is untouched, so Playwright's coordinate
mapping remains stable.

## Session recording

Sessions can be recorded as JSON transcripts for later replay. The recorder
hooks into the audit log and captures every tool call (name, args, result,
timestamp) without keystroke content (data minimization). Transcripts are
saved to `.omo/recordings/session-<id>.json`.

```powershell
# A session transcript can be replayed programmatically:
node -e "const { SessionRecorder } = require('./dist/recording'); SessionRecorder.replay('.omo/recordings/session-xxx.json', async (call) => { console.log(call.toolName, call.args); })"
```

## UIAutomation (accessibility API)

The `inspect_element` tool enumerates visible UI elements via the
UIAutomation tree walker (competitor parity with terminator-mcp-agent and
Windows MCP Inspector). In v1, this is a stub that returns elements from the
injected deps seam; full COM interop requires a native N-API addon (future
work). The `UIAutomationProvider` class implements `InputProvider` but throws
`UIAutomationError` for injection methods in v1 — use `SendInput` or
`PostMessage` paths for actual injection.