winmcp
The server was validated end to end against a live Notepad++ window (part of its real-world test suites), addressing it by hwnd to click controls, write text into editor fields via WM_SETTEXT with read-back verification, send keystrokes, and capture the window with PrintWindow without ever taking the foreground.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
winmcp
A window-addressed desktop control MCP server for Windows that never takes the foreground.
The core promise: it addresses one window by handle, acts through Win32
messages and UIA patterns, and never moves your cursor or steals your focus.
When an action genuinely needs the foreground, it refuses and returns a
needs_permission result instead of doing it — the caller must ask you first.
Why not patch window-mcp
The model of the usual desktop MCP is "snapshot the whole desktop (every window's UIA tree), hand back coordinates, move the real cursor, click." That has three structural costs: a full-desktop traversal takes 11–15 s, a real cursor cannot coexist with a human, and coordinates only hold for the snapshot that produced them.
This tool changes the model: address a single window by hwnd and act with
window messages and PrintWindow.
Related MCP server: desktop-automation
Measurements (on a normal Windows desktop)
Operation | Time | Takes foreground |
| ~1 ms | no |
| 16–31 ms | no |
UIA tree read | 5 ms – 14 s | no |
Win32 | 0.0 ms | no |
Win32 | 0.3 ms | no |
UIA | 504–518 ms | yes |
UIA | 514 ms | yes |
The conclusion that shaped the design: UIA pattern invocation is what steals the foreground, and it is three orders of magnitude slower than a window message. So window messages are the primary route, and UIA is only used after the user agrees.
Tools
windows()— visible top-level windows: hwnd, title, class, pid, rect, foreground flagsee(hwnd, with_tree=false)—PrintWindowcapture plus an optional UIA tree with a hard timeoutclick(hwnd, target)— window message first,needs_permissionotherwisetype_text(hwnd, target, text)—WM_SETTEXTfirst, verified by read-backkey(hwnd, keys)— keystrokes; always needs the foregroundscroll(hwnd, target, direction)— background scrolling is unreliable, expect a refusalopen_app(command)— starts a program without letting it pop to the frontreveal(hwnd)— shows a hidden window; this one does take the foreground
The permission gate
When an action cannot be done without the foreground:
{
"ok": false,
"needs_permission": true,
"reason": "no native control matching 'X' that accepts a window message",
"detail": "'Window title' can only be reached through UIA or the real cursor...",
"hint": "Show this to the user. Retry with allow_foreground=true only after they agree."
}Install
git clone <this repo> $env:LOCALAPPDATA\winmcp
cd $env:LOCALAPPDATA\winmcp
uv venv .venv --python 3.14
uv pip install --python .venv\Scripts\python.exe "windows-mcp==0.8.8"
uv pip install --python .venv\Scripts\python.exe -e .
pwsh -File .\install.ps1 # registers the logon task, starts the serverThe server listens on http://127.0.0.1:8765/mcp.
Register it in a client, e.g. Codex ~/.codex/config.toml:
[mcp_servers.winmcp]
url = "http://127.0.0.1:8765/mcp"
required = falseDo not install under a directory with capability SIDs
PrintWindow returns ERROR_ACCESS_DENIED for every window when the
interpreter lives under a directory whose ACL carries S-1-4-* application
package capability SIDs. Some user profile folders inherit them. If capture
works from one venv and fails from another with identical code and Python
build, check the ACL before anything else:
(Get-Acl $path).Access | Where-Object { $_.IdentityReference -like 'S-1-4-*' }Installing under %LOCALAPPDATA% avoids the issue.
Self-check
cd $env:LOCALAPPDATA\winmcp
.venv\Scripts\python.exe -m winmcp.check36 assertions. It creates its own window, so it never touches yours.
Known limits
Chromium / Electron / Flutter / games have no Win32 child controls.
clickalways goes through the permission gate there.Electron apps hide their UI from accessibility unless launched with it on. See below — this is the difference between a readable app and a black box.
Background UIA reads are slow for browsers (10+ s). The timeout stops it and returns
timed_out: truerather than hanging.PrintWindowreturns blank frames for GPU overlays, protected content and minimised windows. Reported asimage_unavailable, never as a blank picture.UWP shell windows read as zero elements. An
ApplicationFrameWindowreally does contain only empty title-bar and input-sink children; the content lives in a separateCoreWindow.Win32 multiline edits expose no
ScrollPattern. Only the mouse wheel moves them.Chromium inputs return
NonefromValuePattern.Valueeven when the text is there, so read-back verification is impossible; pixel comparison is the only check.
Electron apps are black boxes until accessibility is switched on
Chromium builds its accessibility tree lazily, and an outside UIA client asking for it is not enough to switch it on. Measured on QQ (Chromium 9.9.22):
Launch | Elements read | Named |
plain | 9 | 0 |
| 128 | 126 |
Those 9 are empty PaneControl shells wrapping the D3D surface — no names, no
addressable controls, nothing to click by name.
And Chromium throws the tree away again as soon as another window comes forward. Same window, same session, only the foreground changed:
Foreground | Elements |
132 | |
your editor | 0 |
QQ again | 132 |
That is why an agent can read a window one moment and find nothing the next. Two flags stop it, and either one alone is enough:
--disable-backgrounding-occluded-windows--disable-features=CalculateNativeWinOcclusion
open_app(..., accessibility=True) passes all three flags. With them a
backgrounded QQ reads 132 elements on every poll while your editor keeps the
foreground.
Clicks that never steal the foreground
UIA InvokePattern on Chromium goes through IA2, which focuses the widget and
takes the foreground. LegacyIAccessible.DoDefaultAction does the same work
through MSAA and leaves focus alone. Measured on the same backgrounded window:
Route | Effect landed | Cursor moved | Foreground moved |
| yes | no | no |
| no | no | yes |
posted | no | no | no |
So click() tries the legacy route first, before touching the window's
activation state at all. Only if that finds no target does it bring the window
forward and fall back to the modern patterns.
False successes this tool had to fix
Three separate times an API reported success while nothing happened. They are now guarded, and the lesson is in the code:
SetForegroundWindowon a hidden window returns success andGetForegroundWindowpoints at it, but the screen does not change. Guarded by also requiringIsWindowVisibleandnot IsIconic.ValuePattern.SetValueon a Chromium input returns silently and stores nothing. Guarded by reading the value back and falling back to click-then-type.PostMessage(BM_CLICK)to a render host returns 1 and does nothing. Guarded by only messaging real button classes.
Real-world testing
128 assertions across seven suites, against real applications — Notepad, Paint, VS Code, Explorer, Calculator, Notepad++, and a live browser session.
Suite | Covers | Result |
T1 |
| 9/9 |
T2 |
| 5/5 |
T3 |
| 10/10 |
T4 |
| 9/9 |
T5 |
| 10/10 |
T6 |
| 1/1 |
T7 |
| 30/30 |
T8 | hostile input, concurrency, leaks | 31/31 |
T9 | MCP protocol, schemas, error shapes | 23/23 |
Verified end to end: drove a real browser to a search page, read the results from the UIA tree, and clicked through to a repository page; filled and submitted an HTML form and confirmed every field arrived by reading the server's JSON response.
Design notes worth keeping
Key notation is translated, not passed through. SendKeys wants {Ctrl}s;
a caller writing ^s would get those characters typed literally into the
document. _normalise_keys accepts ^s, Ctrl+s, ^{END} and emits the right
form. A chord is only a chord when every part before the last is a modifier name
— otherwise 1+1 becomes {1}{1} and the plus disappears.
Pattern getters are not universal. GroupControl has no GetInvokePattern,
WindowControl has no GetScrollPattern. Every pattern access goes through one
guarded helper; a bare attribute access crashes on real UIs.
Silent launch is a fight, not a race. A launched app calls
SetForegroundWindow about 200 ms in, sooner than any hook can hide its window.
So the tool hides new windows every pass and takes the foreground back every pass
until the situation is stable. foreground_contests reports how often the app
tried.
Verify screen changes with a screenshot. Every failure mode above reported success. No API return value is trusted for anything visible.
Credential dialogs stall git commands. On Windows, git push / git remote add can trigger Git Credential Manager's "Select an account" window (an
Avalonia top-level window, class Avalonia-*). It blocks the command until an
account is chosen. Its title has no "git" keyword, so a keyword-based window
scan misses it and the agent waits forever with no output. The rule is shipped
to every connected client in the server's MCP instructions: if a git command
stalls, call windows(), find the new window, then see(hwnd, with_tree=True)
to click the account ListItem and Continue. Match by title/class, not by pid
— the dialog belongs to a different process.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Eyes and hands on real Windows PCs — observe, click, type via Glasswarp API.
Securely control computers you explicitly pair through files, terminals, processes, screenshots, desktop UI/input, clipboard, browser automation, diagnostics, and document tools.
- DNS-DATA RemoteDeprecatedcom.dns-data
Connect AI assistants to authorized Windows and macOS devices for files, commands, and management.
Connect ChatGPT, Claude, Cursor or VS Code to your Windows PC: files, terminal, browser, apps.
Related MCP Servers
- AlicenseCqualityCmaintenanceEnables agent-native desktop control by driving mouse, keyboard, and interface elements through accessibility-first semantic operations, with fallback to screen coordinates when accessibility cannot reach a target.30MIT
- AlicenseNot gradedqualityBmaintenanceEnables secure, deny-by-default automation of Windows windows with per-action permissions, allowing observation, clicking, typing, and dragging on approved applications while verifying target identity and focus at runtime.Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables AI clients to inspect and control allowlisted Windows applications using screenshots, mouse actions, Unicode keyboard input, UI Automation, and an Esc/F12 emergency stop.1Apache 2.0
- AlicenseAqualityFmaintenanceLets Claude Desktop and Claude Code inspect and drive Windows desktop applications in the background, including clicking, typing, launching, and verifying results, while the user keeps their mouse, keyboard, and foreground window.7176 PyPI4MIT