README
¶
windows-mcp-server
A Model Context Protocol server that bridges AI agents to the Windows desktop — UI Automation, synthetic mouse/keyboard input, screenshots, window and application control, PowerShell, the registry, processes, the filesystem, and web scraping. No computer-vision model is required: the agent perceives the UI through the Windows accessibility tree.
What separates it from a plain automation bridge is that it is the only Windows MCP server that gates every agent action on live device posture and constrains the agent's network egress, and records both in a tamper-evident audit chain the agent cannot switch off. It turns an agent with system access into an agent whose actions are conditional, bounded, and reviewable.
- Device policy engine — every tool call, resource read and prompt fetch is evaluated against live device signals (MDM enrolment, Entra join, Secure Boot, BitLocker, VBS/HVCI, TPM attestation) before it runs, and refused, warned or contained by policy configuration. Rules match by tool, toolset or annotation, so a screenshot is not gated like a shell command. → Policy configuration
- Egress allowlist — a loopback proxy admits only the domains you declare, checking the allowlist before it resolves a name, optionally backed by firewall rules so named applications — or the whole machine — cannot go around it. → Egress setup
- Tamper-evident transparency — a hash-chained audit log (optionally keyed and anchored off-box), heartbeat, rug-pull detection, whole-session recording, and an out-of-band tiered kill switch. None of it is agent-disableable. → Monitoring · Security architecture
Several tools (PowerShell, Registry, FileSystem, Process, App) have full system access with no sandboxing. That is the design, not an oversight. Run untrusted workloads in a VM or Windows Sandbox — see VM isolation — and see SECURITY.md for what is and is not in scope as a vulnerability.
Quick start
Windows 10 or 11 (amd64 or arm64), Go 1.25+ to build.
go build -o windows-mcp-server.exe ./cmd/windows-mcp-server
Point any MCP client at the binary with the stdio subcommand:
{
"mcpServers": {
"windows": {
"command": "C:\\path\\to\\windows-mcp-server.exe",
"args": ["stdio", "--persona", "first-line-support"]
}
}
}
→ Getting started covers Claude Code, Cursor, Codex CLI and Claude Desktop specifically, and what to do before pointing this at a machine you care about.
Not sure this is for you? If you have to approve it on a fleet, read Deciding to deploy this. If you have a job to do, see the walk-throughs for a UI regression suite or a first-line support queue.
Features
| What it does | Guide | |
|---|---|---|
| Desktop automation | 35 tools across 13 toolsets: accessibility-tree perception, UI Automation pattern invocation, synthetic input, screenshots, apps, windows, PowerShell, registry, filesystem, processes, services, event logs, network, scheduled tasks, packages, scraping, plan-and-apply | Toolsets and personas |
| Personas | Presets that select toolsets and inject workflow guidance, so the agent adopts a role rather than just getting a tool list | Toolsets and personas |
| Credentials | The agent signs in to apps and sites without ever being told the secret. The Credentials tool has no read mode and no engine method returns plaintext — but see the note below on toolset exposure |
Credentials |
| Session recording | Once transparency.recording_dir is set, the whole session goes to one video file — automatically, under every persona — with timeline markers |
Session recording |
| Kill switch | Out-of-band, tiered containment. A trip always audits, raises the banner and seals the log; the optional rungs — isolate, kill processes, lock, shut down — run in a fixed order, with the recording finalized before shutdown and the session aborted last | Security architecture |
| MCP conformance | Protocol revision 2026-07-28, measured by the official suite in CI |
MCP compliance |
On the credentials claim: the guarantee holds at the tool boundary — the
Credentialstool cannot read a secret back and no engine method returns one. Another toolset could once route around it: installed generic credentials live in the calling user's Credential Manager, so a persona that also carriesshell(which canCredRead) orfilesystem(which can read a Credential Manager backup) could retrieve them another way. The server now refuses to start when--credentials-fileis combined with either toolset, unless the policy document acknowledges the exposure (credentials.acknowledge_toolset_exposure), and audits the decision — so the exposure is a deliberate, recorded choice rather than a silent hole. See the trust model.
Tools
Tools are grouped into toolsets. screen, interaction, apps and system
are on by default; the rest are opt-in.
| Toolset | Default | Tools |
|---|---|---|
screen |
✓ | Snapshot, Screenshot, DisplayInventory, Recording |
interaction |
✓ | Click, Type, Invoke, GetText, Scroll, Move, Shortcut, Wait, WaitFor, MultiSelect, MultiEdit |
apps |
✓ | App (launch / switch / resize) |
system |
✓ | Clipboard, Process, Notification |
system-admin |
Registry, ScheduledTask (list / get / run / enable / disable / delete / create) |
|
shell |
PowerShell, LaunchExecutable |
|
filesystem |
FileSystem (read / write / copy / move / delete / list / search / info) |
|
web |
Scrape |
|
diagnostics |
SystemInfo (OS/hardware/disk via WMI), Service (list / start / stop / restart), EventLog (Get-WinEvent queries), Network (adapters / dns / config / test) |
|
testing |
Assert (PASS/FAIL UI condition), CaptureEvidence (screenshot + tree) |
|
planning |
Plan, Apply — propose a whole sequence, adjudicate it up front, then run it verbatim — see Plan and apply |
|
packages |
Package (list / search / install / uninstall via winget and MSI) — downloads run outside the egress proxy; in no persona |
|
credentials |
Credentials (list / verify / inject) — enabled automatically by --credentials-file |
Two more are served under every persona and belong to no toolset:
GuardrailStatus (read-only posture) and Kill (stop the session).
The typical loop: call Snapshot for the foreground window and a labeled
tree of interactive elements, then act on a label with Invoke, Click
or Type. Take a fresh Snapshot after the UI changes.
Resources and prompts
| Resource URI | Contents | Toolset |
|---|---|---|
windows://desktop/snapshot |
The most recent Snapshot — reading it does not capture a new one |
screen |
windows://desktop/displays |
Connected displays: bounds, work area, DPI, scale | screen |
windows://session/recording |
Session-recording status, paths, frame count | screen |
windows://system/info |
OS, hardware, memory and disk inventory | diagnostics |
| Prompt | Purpose | Toolset |
|---|---|---|
rpa-journey |
Drive a scripted end-user journey, verifying each step | interaction |
triage-support-issue |
Diagnose a reported problem, gathering state before acting | diagnostics |
capture-evidence |
Record reproducible evidence for a test or support case | testing |
Resources and prompts are filtered by toolset like tools are, so a prompt
whose toolset is disabled is not served. Prompts build their text from the
matching persona's instructions rather than restating it, so --persona and the
prompts cannot drift apart.
Both are decided by the policy engine: resources/read and prompts/get are
written to the audit log, covered by any rule matching toolset: "*" or
annotation: read-only, and fingerprinted for rug-pull detection.
Personas
.\windows-mcp-server.exe personas # list them
.\windows-mcp-server.exe stdio --persona qa-test-engineer
| Persona | Toolsets | Focus |
|---|---|---|
first-line-support |
screen, interaction, apps, system, shell, diagnostics | Diagnose before acting; SystemInfo/Process/Service + PowerShell |
qa-test-engineer |
screen, interaction, apps, system, filesystem, web, testing | Deterministic UI tests; label targeting, Assert, CaptureEvidence |
business-user |
screen, interaction, apps, web, testing | End-user tasks and journey testing through the real UI; no shell, registry or filesystem |
User-journey testing (RPA)
business-user and qa-test-engineer are built for driving scripted journeys —
open apps, sign into sites, click through flows, check and change settings via
the UI — and verifying each step:
perceive → target → act → synchronize → verify
Snapshot # the foreground window + labeled elements
Invoke {name:"Sign in"} # act via the accessibility pattern (reliable)
WaitFor {condition:active_window, window_name:"Inbox"}
Assert {condition:text_present, text:"Welcome"}
CaptureEvidence {label:"logged in"}
Prefer Invoke over Click/Type. Invoke (and set_value, toggle,
select, expand/collapse) acts through a UI Automation control pattern
rather than synthesizing input, so it does not depend on the window being
focused, unoccluded or at a particular DPI. Journeys are far less flaky.
Click/Type remain the fallback for controls exposing no pattern.
Target by name or label. The targeting tools accept a label from the latest
Snapshot, or a name with optional control_type and nth — so a step reads as
Invoke {name:"Submit", control_type:"Button"}. Click, Type and Move also
accept an explicit loc [x,y]; Invoke and GetText resolve through the
accessibility tree and need a label or a name.
What cannot be automated, by design
These sit on the Windows secure desktop, which no user-session process can see or drive — a platform boundary, not a limitation of this server:
- The sign-in / lock screen and UAC elevation prompts
- Driving elevated apps from a non-elevated process (UIPI)
Design around it: have the harness deliver an already-signed-in, unlocked session, and auto-start the server at logon. "Login" inside a journey then means application and website sign-in, which is fully supported. See Deployment.
Security
📐 Architecture with diagrams: docs/security-architecture.md 📄 Schema reference: docs/policy-config.md
A policy engine sits between the MCP caller and the tools. Before a tool runs, a resource is read or a prompt is fetched, it evaluates device signals against rules in a policy document and decides what happens.
MCP client ──▶ audit ──▶ rug-pull ──▶ policy engine ──▶ tool handler
│
device signals
.\windows-mcp-server.exe stdio --policy-config C:\ProgramData\windows-mcp\policy.json
With no --policy-config the built-in default applies: the engine is
present, every declared signal is evaluated and every verdict recorded, and
nothing is refused. Adopting the engine cannot break a working deployment before
its policy is written.
on_fail |
Effect |
|---|---|
allow |
Proceeds; the failure is still recorded |
warn |
Proceeds, and the warning rides back with the result so the model sees it |
hold |
The call is suspended on an out-of-band human authoriser and fails closed — a timeout, an unreachable webhook or an unintelligible reply all deny |
deny |
This call is refused, and re-evaluated next time — a signal that recovers restores service with no restart |
kill |
The kill switch trips and the containment ladder runs |
Rules match on tool, toolset or MCP annotation, so a screenshot is not gated on the posture a shell command is gated on:
{
"rules": [
{ "name": "baseline", "match": { "toolset": "*" }, "require": ["run-context"], "on_fail": "deny" },
{ "name": "destructive", "match": { "annotation": "destructive" }, "require": ["bitlocker"], "on_fail": "deny" },
{ "name": "shell", "match": { "tool": "PowerShell" }, "require": ["mdm-enrolled"], "on_fail": "kill" }
]
}
Requirements are the union across matching rules, so adding a rule never drops
one. Severity is attributed per signal to the most specific rule requiring it:
tool > annotation > named toolset > "*".
.\windows-mcp-server.exe policy validate --policy-config policy.json # document + signal ids; exits 1
.\windows-mcp-server.exe policy check --policy-config policy.json # this device now; exits 2 if not admitted
.\windows-mcp-server.exe policy explain --policy-config policy.json --tool PowerShell
Six starting points ship in policy/examples/: audit.json (adopt first —
refuses nothing), secure.json, enterprise.json, locked-down.json,
egress.json and dual-control.json.
Trust model — read this
The local device signals (dsregcmd, registry, WMI) are auditable
defense-in-depth, not a hard boundary — a local admin can spoof them. The
containment layers raise the cost of, and record, in-session compromise but do
not replace the OS controls you already own: pair them with WDAC/AppLocker,
Conditional Access and code signing. The authoritative remote signals
(Microsoft Graph, an external may-run endpoint) register only when their
credentials are present — see Remote signals.
Threat model
What each actor can do, what constrains them, and what is left over. The honest column is the last one: several controls raise cost and produce evidence rather than prevent, and this is the register a risk function should read it in. The security architecture maps these to mechanisms.
| Actor | Capability | Control | Residual risk |
|---|---|---|---|
| Prompt-injected agent — the model, driven by hostile content | Issue any tool call the served surface allows | Policy engine gates every call on device posture; rate limits break exfil loops; the egress allowlist bounds network reach; the audit chain records every call with argument digests. Plan-and-apply adjudicates a whole proposed sequence up front, and require_plan can force named or destructive tools through that review |
Benign-annotated calls can still compose into a harmful outcome when they run one at a time outside a plan; scope the surface with personas, gate destructive annotations, and use require_plan where the composition risk is real |
| Malicious MCP client — the host process on the other end of stdio | Swap the tool manifest after approval; probe methods | Manifest fingerprinted at startup; tools/list and discover intercepted; rug-pull monitor trips the kill switch on drift; list_changed suppressed |
The stdio transport assumes a trusted host: a client that never drifts is trusted by construction. There is no network listener to attack |
| Local user — non-admin, same session | Read what the agent reads; use the running server | Credentials never returned to the model; the FileSystem tool refuses the credentials file, the audit key, the audit log, the policy document and the kill-switch control directory; GuardrailStatus/Kill cannot be removed |
A user already holds their own privileges; the server does not raise them. The protected-path check binds the FileSystem tool only — PowerShell reaches the same files unchecked, which is why the shell toolset requires an explicit acknowledgement to be served alongside credentials. Registry and ScheduledTask now live in the non-default system-admin toolset |
| Local administrator | Spoof device signals; edit the audit log; disable transparency | Local signals are defense-in-depth, not a boundary; the audit chain is tamper-evident, and keying + off-box anchoring raise the bar; pair with WDAC/AppLocker and Conditional Access | An admin can defeat any on-box control — the value is evidence that survives, not prevention. The audit HMAC key sits on the box unless anchored off it; path protection matches by cleaned path, not 8.3 names or hard links |
| Network attacker | Reach the server; intercept egress | No inbound listener — the transport is stdio only; the egress proxy checks the allowlist before any DNS query and re-checks resolved addresses against loopback/RFC1918/link-local before dialling | Enforce HTTPS and the egress proxy do not intercept navigation inside an already-open browser; the proxy-only enforcement tier is advisory until backed by firewall rules |
Egress: the domains the device may reach
Declare an allowlist and everything else is dropped:
"egress": {
"enabled": true,
"allow": ["*.contoso.com", "login.microsoftonline.com"],
"allow_ports": [443]
}
A loopback CONNECT/HTTP proxy enforces it. *.contoso.com covers the apex and
every depth below it, anchored at a label boundary so fakecontoso.com never
matches. The allowlist is checked before the name is resolved — a refused
host emits no DNS query — and resolved addresses are re-checked against
loopback, RFC1918 and link-local ranges before anything is dialled, so an
allowed name cannot be pointed at something internal. TLS is never intercepted;
there is no CA and nothing is decrypted.
Three enforcement tiers:
| Tier | What forces traffic through the proxy |
|---|---|
proxy-only |
Nothing — it constrains whatever is configured to use it. No elevation |
scoped |
Outbound-block firewall rules on named applications. Loopback stays reachable, so the proxy is their only route out |
global |
The machine's default outbound action becomes block, with a service-scoped exception set for DNS, DHCP, NCSI, time, update and revocation |
Both firewall tiers need elevation and refuse to start without it rather than serving a weaker posture than the document describes.
→ Egress setup — per-tier procedure, browser configuration, verification and recovery.
Flags and environment
Every flag has a WINDOWS_MCP_-prefixed environment variable (--read-only ↔
WINDOWS_MCP_READ_ONLY).
| Flag | Description |
|---|---|
--policy-config |
Path to the device-policy JSON document. Omit for the built-in default |
--toolsets |
Comma-separated toolsets to enable (all, default, or specific ids) |
--tools |
Additionally enable individual tools (bypasses toolset filtering) |
--exclude-tools |
Disable specific tools; applied last |
--read-only |
Expose only read-only tools |
--persona |
Select a persona preset |
--overlay |
Visual feedback overlays (see below) |
--record-fps |
Recording frame rate (default 4) |
--record-codec |
h264/h265 (via ffmpeg; small files) or mjpeg (pure-Go, no dependency) |
--credentials-file |
JSON file of credentials to install at init |
--log-file |
Write debug logs to a file (stdout is reserved for the transport) |
Secrets in the environment
Key material and credentials are environment-only, never flags or the policy
document — argv is world-readable and the policy is meant to be reviewed and
checked in. The suffix says how the value is delivered:
…_KEY/…_TOKEN/…_SECRET/…_HEADERShold the value inline.…_KEY_FILEholds a path to a file whose contents are the material — used where the material is a key file with its own ACLs (the ed25519 evidence signing seed), so it never sits in the process environment.
| Variable | Delivery | Purpose |
|---|---|---|
WINDOWS_MCP_AUDIT_KEY |
value | HMAC key that seals the audit chain (absent → unkeyed) |
WINDOWS_MCP_APPROVAL_KEY |
value | HMAC key signing dual-control webhook requests |
WINDOWS_MCP_EVIDENCE_KEY_FILE |
path | ed25519 seed that signs evidence bundles |
WINDOWS_MCP_OTLP_HEADERS |
value | k=v,k=v auth headers for the OTLP collector |
WINDOWS_MCP_GRAPH_TENANT / _CLIENT_ID / _CLIENT_SECRET |
value | Graph/Intune tier-2 signal credentials |
WINDOWS_MCP_REMOTE_POLICY_TOKEN |
value | bearer token for the remote may-run endpoint |
WINDOWS_MCP_EXPORT_SIGNED_URL (…_MANIFEST, …_SIGNATURE) |
value | pre-signed PUT / SAS / signed URL the sealed evidence bundle is shipped to. A documented exception to the suffix rule: it is a write credential whose only sensible shape is a URL |
whatever egress.auth_token_env names |
value | Proxy-Authorization secret the egress proxy requires |
Everything the security subsystem does — which signals are read and how often, which rules cover which tools, what a failure does, what trips the kill switch and what it actuates, where the audit chain goes, whether the session is recorded, which domains are reachable — is configured in the policy document rather than by flags. The questions are relational, and a flag cannot express a relation: "PowerShell requires MDM enrolment but taking a screenshot does not" has no spelling as a set of booleans. docs/policy-config.md carries a table mapping each removed flag to its field.
Running as SYSTEM
Session 0 has no desktop to drive, so if the server detects it is running as
SYSTEM the desktop-automation toolsets are dropped and the selection is replaced
with system, shell, filesystem, diagnostics, web. This is detected, not
declared. A persona explicitly requested under SYSTEM is refused — the server
exits rather than serving a reshaped version of it.
Visual feedback overlays
--overlay draws click-through, top-most overlays so a viewer can see what the
automation is doing — a green hue around the focused window on each
Snapshot, and an orange flash at each click point. They never intercept
input or take focus.
MCP conformance
The server targets protocol revision 2026-07-28. Conformance is measured by
the official
modelcontextprotocol/conformance
suite, which .github/workflows/mcp-spec-compliance.yml runs and commits the
results of.
2026-07-28 is a stateless protocol: no sessions and no initialize handshake.
Each request carries its protocol version and client capabilities in _meta,
server/discover advertises identity and capabilities, subscriptions/listen
carries server-to-client notifications, every result carries resultType, list
and read results carry ttlMs and cacheScope, POSTs carry Mcp-Method /
Mcp-Name headers, and MCP error codes sit in the reserved -32020..-32099
range.
Three passes run: product (the manifest the server ships), fixtures (the
same server with the suite's named fixture tools registered, behind the
conformance build tag), and a backward-compatibility run at 2025-11-25.
CI gates on the suite's own exit code — a failure absent from the baseline fails
the build, and so does a baseline entry that has started passing.
go build ./... does not compile the conformance host, so the released binary
has no HTTP listener and is stdio-only. Both it and stdio build their MCP
surface through one function, so what the suite measures is what the shipped
binary serves.
→ The report is the verdict; the badge is a summary of the product pass.
Architecture
cmd/windows-mcp-server Cobra/Viper CLI (stdio transport)
internal/winmcp server bootstrap: inventory + MCP server + deps middleware
internal/desktop the Windows engine — one COM STA thread serving UIA
traversal, SendInput, GDI screenshots, overlays,
PowerShell, plus a WMI worker thread
internal/guardrails/ the security stack, split by lifecycle layer:
signals signal vocabulary, probes, registry, checks
audit hash chain, destination, VerifyChain
hostmatch egress allowlist matching + forbidden address ranges
policy document schema, signal cache, engine, verdict
egress the egress proxy + Windows firewall enforcement
enforce the MCP middleware
watch heartbeat, rug-pull, in-flight monitor
contain kill switch, containment ladder, actuator, firewall
status status endpoint, GuardrailStatus + Kill tools
internal/mcpspec vendored-schema loader + offline wire validation
internal/mcpconf official conformance-suite results: ingest + reporting
pkg/windows MCP tool definitions, personas, dependency-injection glue
pkg/inventory domain-agnostic toolset engine (grouping, filtering,
read-only, resources, prompts)
policy/examples starting-point policy documents
schema/ vendored MCP protocol schemas, one dir per revision
conformance/ expected-failure baselines + committed suite results
All Win32/COM work is serialized onto one STA thread; WMI runs on its own thread-affine worker. Tool handlers receive dependencies from the request context via receiving middleware.
Development
go build ./...
go vet ./...
go test ./... -count=1
golangci-lint run --config=./.golangci.yml
pkg/inventory, the parameter helpers, the policy engine and the egress matcher
are cross-platform and tested everywhere; the Windows automation packages build
and test only on Windows. See CONTRIBUTING.md and
CLAUDE.md.
Documentation
docs/ — setup and configuration guides, plus the security architecture and conformance report.
Credits
The tool surface began as a Go port of the Python
Windows-MCP project. It is built on
the go-bindings-win32
and go-bindings-wmi SDKs
and the official MCP Go SDK.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
windows-mcp-server
command
Command windows-mcp-server is an MCP server that bridges AI agents to the Windows desktop: UI Automation, synthetic input, screenshots, window and application control, PowerShell, and system state.
|
Command windows-mcp-server is an MCP server that bridges AI agents to the Windows desktop: UI Automation, synthetic input, screenshots, window and application control, PowerShell, and system state. |
|
internal
|
|
|
desktop
Package desktop is the Windows automation engine underneath the MCP tools.
|
Package desktop is the Windows automation engine underneath the MCP tools. |
|
journeys
Package journeys is the declarative model of a user journey: a named sequence of UI actions, each with assertions about the resulting screen and evidence to capture.
|
Package journeys is the declarative model of a user journey: a named sequence of UI actions, each with assertions about the resulting screen and evidence to capture. |
|
mcpconf
Package mcpconf reads the results of the official Model Context Protocol conformance suite (github.com/modelcontextprotocol/conformance) and renders them as the project's committed compliance report.
|
Package mcpconf reads the results of the official Model Context Protocol conformance suite (github.com/modelcontextprotocol/conformance) and renders them as the project's committed compliance report. |
|
mcpspec
Package mcpspec loads the vendored Model Context Protocol JSON Schemas and validates wire JSON against them.
|
Package mcpspec loads the vendored Model Context Protocol JSON Schemas and validates wire JSON against them. |
|
psdata
Package psdata builds PowerShell scripts in which caller-supplied values are carried as data rather than interpolated into the script as source text.
|
Package psdata builds PowerShell scripts in which caller-supplied values are carried as data rather than interpolated into the script as source text. |
|
runrecord
Package runrecord builds the OpenTelemetry trace that records a journey run, and serialises it as OTLP/JSON.
|
Package runrecord builds the OpenTelemetry trace that records a journey run, and serialises it as OTLP/JSON. |
|
winmcp
Init-time credential provisioning.
|
Init-time credential provisioning. |
|
pkg
|
|
|
inventory
Package inventory is a transport-agnostic, domain-agnostic registry for MCP tools, resources, and prompts.
|
Package inventory is a transport-agnostic, domain-agnostic registry for MCP tools, resources, and prompts. |