Skip to content

UIA returns an empty desktop when running as an MCP server (stdio and HTTP), while the identical code sees the desktop when called in-process — native x64, not #301 #385

Description

@CharlesGuooo

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions