Summary
On a native x64 machine, Snapshot consistently reports
Focused Window:
No active window found
Opened Windows:
No windows found
UI Tree:
No elements
while, at the same moment, on the same machine, from the same uv cache archive, constructing Desktop() directly in a normal Python process enumerates the desktop correctly (5 windows, 55–177 interactive nodes).
So the UIA layer itself works. It goes blind specifically when the code runs as an MCP server.
This is not #301 — that one is Windows ARM64 with an x86-emulated interpreter. This machine is native AMD64 with a native AMD64 interpreter (details below).
It is intermittent rather than absolute: the very first session after installing worked perfectly (full window list, full Calculator control tree). Every attempt since has been blind, across four client restarts and two days.
Environment
|
|
| windows-mcp |
0.8.5 (also reproduced on latest and 0.8.0) |
| Windows |
11 Pro for Workstations, 10.0.26200 |
| CPU / arch |
Intel Core Ultra 9 275HX — PROCESSOR_ARCHITECTURE=AMD64, PROCESSOR_ARCHITEW6432 empty |
| Python |
uv-managed CPython 3.12, platform.machine()=AMD64, architecture=('64bit','WindowsPE') |
| uv |
0.11.21 (x86_64-pc-windows-msvc) |
| Display |
single monitor, 2560x1600 @ 150% (effective_dpi 144, scale 1.5) |
| Launch |
uvx windows-mcp@0.8.5 serve |
Notably DisplayInventory keeps working correctly throughout (it reports the right bounds and DPI), so the process is alive, COM is functional enough for that path, and the MCP protocol is healthy. Only the UIA tree/window enumeration comes back empty.
Reproduction
A. In-process — works
from windows_mcp.desktop.service import Desktop
d = Desktop()
st = d.get_state(use_vision=False, use_dom=False, use_ui_tree=True, use_annotation=False)
print(st.active_window.name, len(st.windows), len(st.tree_state.interactive_nodes))
$ uv run --no-project --with windows-mcp==0.8.5 python probe.py
screen size: Size(width=2560, height=1600)
active_window: '<a Chrome window>'
windows found: 5
interactive nodes: 55
Adding the WatchDog exactly as the server lifespan does (WatchDog(), set_focus_callback(d.tree.on_focus_change), start()) also works — 5 windows, 55 → 175 → 177 interactive nodes over three polls.
B. As an MCP server — blind
Spawn it the way a host does (stdio pipes, no console) and drive the protocol directly:
const child = spawn("uvx", ["windows-mcp@0.8.5", "serve"], { stdio: ["pipe","pipe","pipe"], shell: true });
// initialize -> notifications/initialized -> tools/call Snapshot
initialize OK — server: {"name":"windows-mcp","version":"3.4.7"}
tools exposed: App, DisplayInventory, PowerShell, FileSystem, Snapshot, Screenshot, Click, ...
Snapshot -> "No active window found" / "No windows found" / "No elements"
Same result over --transport streamable-http --host 127.0.0.1 --port 8765, driven with a plain fetch client:
initialize: {"name":"windows-mcp","version":"3.4.7"}
session id: 1bad82530345458a82526c6e26beaa69
VERDICT: BLIND over HTTP too
So it is not the client, not the transport, and not stdio buffering.
Hypotheses ruled out (each tested)
| Hypothesis |
Test |
Result |
| Version regression |
latest and 0.8.0 |
both blind |
| WatchDog / tree cache poisoning |
Desktop with and without WatchDog wired |
both fine in-process |
| Stale long-lived process |
freshly spawned process, 0.9 min old |
blind |
| Dependency drift |
blind server and working probe resolve to the same uv cache archive (archive-v0\vUFcMxcuK9JcRgUu) |
same code |
| Leaked/duplicate servers |
killed all, single clean process |
blind |
| Transport-specific |
stdio and streamable-http |
both blind |
| ARM64 emulation (#301) |
native AMD64 CPU and interpreter |
does not apply |
| Locked/blank desktop |
independent process enumerates 11 windows at the same instant; screen renders normally |
desktop is fine |
Suspected cause
The difference between the working and failing paths is which thread touches the UIA objects. In-process the calls happen on the main thread. Under FastMCP, Desktop() is constructed in the lifespan while synchronous tool functions are dispatched to a worker thread. UIA/comtypes objects are apartment-threaded, and cross-apartment use without marshaling can return empty results rather than raising — which matches the symptom exactly: no exception, no log, just an empty desktop.
That would also explain why the first session worked and later ones did not: it depends on which thread happens to service the call.
If that is the cause, one fix is to pin all UIA access to a single dedicated STA thread (or re-CoInitialize per worker and rebuild the automation client there) rather than sharing objects created on the lifespan thread.
Secondary observation: server processes are never reaped
Independent of the above: each client restart spawned a new uvx windows-mcp serve tree while the previous ones kept running. Six full server instances had accumulated (ages 4150 / 1673 / 1282 / 33 / 20 / 0.9 minutes) before I cleaned them up manually. Worth a look — it also makes the blindness harder to diagnose, since it is unclear which instance is answering.
Impact
The failure is silent. Snapshot returning "no windows" is indistinguishable, to a calling agent, from a genuinely empty desktop — so an agent can conclude "there is nothing there" and act on that, instead of surfacing an error. A hard failure would be considerably safer than an empty success.
Happy to run further diagnostics or test a patch — I have both repro harnesses scripted.
Summary
On a native x64 machine,
Snapshotconsistently reportswhile, at the same moment, on the same machine, from the same uv cache archive, constructing
Desktop()directly in a normal Python process enumerates the desktop correctly (5 windows, 55–177 interactive nodes).So the UIA layer itself works. It goes blind specifically when the code runs as an MCP server.
This is not #301 — that one is Windows ARM64 with an x86-emulated interpreter. This machine is native AMD64 with a native AMD64 interpreter (details below).
It is intermittent rather than absolute: the very first session after installing worked perfectly (full window list, full Calculator control tree). Every attempt since has been blind, across four client restarts and two days.
Environment
PROCESSOR_ARCHITECTURE=AMD64,PROCESSOR_ARCHITEW6432emptyplatform.machine()=AMD64,architecture=('64bit','WindowsPE')uvx windows-mcp@0.8.5 serveNotably
DisplayInventorykeeps working correctly throughout (it reports the right bounds and DPI), so the process is alive, COM is functional enough for that path, and the MCP protocol is healthy. Only the UIA tree/window enumeration comes back empty.Reproduction
A. In-process — works
Adding the WatchDog exactly as the server lifespan does (
WatchDog(),set_focus_callback(d.tree.on_focus_change),start()) also works — 5 windows, 55 → 175 → 177 interactive nodes over three polls.B. As an MCP server — blind
Spawn it the way a host does (stdio pipes, no console) and drive the protocol directly:
Same result over
--transport streamable-http --host 127.0.0.1 --port 8765, driven with a plainfetchclient:So it is not the client, not the transport, and not stdio buffering.
Hypotheses ruled out (each tested)
archive-v0\vUFcMxcuK9JcRgUu)Suspected cause
The difference between the working and failing paths is which thread touches the UIA objects. In-process the calls happen on the main thread. Under FastMCP,
Desktop()is constructed in the lifespan while synchronous tool functions are dispatched to a worker thread. UIA/comtypesobjects are apartment-threaded, and cross-apartment use without marshaling can return empty results rather than raising — which matches the symptom exactly: no exception, no log, just an empty desktop.That would also explain why the first session worked and later ones did not: it depends on which thread happens to service the call.
If that is the cause, one fix is to pin all UIA access to a single dedicated STA thread (or re-
CoInitializeper worker and rebuild the automation client there) rather than sharing objects created on the lifespan thread.Secondary observation: server processes are never reaped
Independent of the above: each client restart spawned a new
uvx windows-mcp servetree while the previous ones kept running. Six full server instances had accumulated (ages 4150 / 1673 / 1282 / 33 / 20 / 0.9 minutes) before I cleaned them up manually. Worth a look — it also makes the blindness harder to diagnose, since it is unclear which instance is answering.Impact
The failure is silent.
Snapshotreturning "no windows" is indistinguishable, to a calling agent, from a genuinely empty desktop — so an agent can conclude "there is nothing there" and act on that, instead of surfacing an error. A hard failure would be considerably safer than an empty success.Happy to run further diagnostics or test a patch — I have both repro harnesses scripted.