Files
hoelee-blog/src/content/posts/why-your-windows-mcp-calls-silently-fail.md
T

4.9 KiB

title, description, pubDate, updatedDate, category, tags, ogImage, banner, draft
title description pubDate updatedDate category tags ogImage banner draft
Why Your Windows Desktop MCP Calls Silently Fail: Taming the 10-Second Idle Gate Why actions in windows-mcp return CONTROL_PREEMPTED or fail silently, and the exact flag needed to disable the desktop control gate. 2026-03-12 2026-10-25 notes
windows
mcp
debugging
ai-agents
troubleshooting
/og/why-your-windows-mcp-calls-silently-fail.jpg /banners/why-your-windows-mcp-calls-silently-fail.jpg false

You install windows-mcp, register it in your agent config, and ask your model to trigger a native Windows notification or click a button in an open desktop application.

The tool call finishes in under 200 milliseconds. The agent happily reports: "I have sent the notification."

Nothing appeared on screen.

When you inspect the raw JSON response returned by the MCP protocol, you don't find a standard Python exception or Win32 error. Instead, you see this:

{
  "code": "CONTROL_PREEMPTED",
  "status": {
    "state": "user",
    "generation": 3,
    "wait_seconds": 9.8639,
    "executed_steps": 0
  }
}

Or, in slightly different timing conditions, your stdio process dumps a stack trace:

RuntimeError: AI control indicator is stopping
AI control indicator failed; releasing physical input

Here is the root cause of this failure mode and the exact configuration setting that fixes it.


The Culprit: The Desktop Control Gate

windows-mcp is designed around a concept called the Desktop Control Policy (implemented in src/windows_mcp/desktop/control_overlay.py and tools/control_policy.py).

The developers wanted to prevent an AI agent from fighting the human user for physical mouse and keyboard ownership. To achieve this, the server installs an idle detection watchdog:

  1. Before executing any state-changing input tool (Click, Type, Move, Shortcut, Notification, etc.), it checks Windows user input timers (GetLastInputInfo).
  2. If the user moved the physical mouse or pressed a keyboard key within the last 10 seconds (WINDOWS_MCP_CONTROL_IDLE_TIMEOUT), the server considers the human to be actively using the machine.
  3. Instead of waiting for you to stop typing, it aborts immediately, returning CONTROL_PREEMPTED and leaving the action unexecuted.

If you are chatting with your agent in a desktop window or typing commands in a terminal, you just touched the keyboard. Your idle timer is zero. Therefore, almost every single immediate tool call the agent issues will be preempted by default.


Why --no-control Fails

If you inspect the repository's README or CLI documentation, you might see references to --control / --no-control.

Naturally, you might try adding --no-control to your server startup command:

uvx windows-mcp serve --no-control

The server instantly crashes on startup with:

Usage: windows-mcp serve [OPTIONS]
Error: No such option '--no-control'.

The CLI parser for the serve subcommand only exposes transport, network, and tool exclusion flags (--transport, --host, --port, --exclude-tools). It does not bind the control flag to the CLI argument group for serve.


The Real Fix: WINDOWS_MCP_CONTROL=false

The configuration loader (src/windows_mcp/config.py) reads desktop control settings directly from environment variables.

To completely bypass the 10-second idle lockout and allow the agent to execute actions immediately upon request, set:

WINDOWS_MCP_CONTROL=false

When injected into your MCP client's environment block:

{
  "command": "uvx",
  "args": ["windows-mcp", "serve", "--exclude-tools", "PowerShell,Registry"],
  "env": {
    "WINDOWS_MCP_CONTROL": "false"
  }
}

The control overlay, idle watchdog, and preemptive preemption logic are completely disabled.


Verification

To confirm the fix, send a toast notification through the raw MCP client while actively moving your mouse:

import asyncio
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp.client.session import ClientSession

async def main():
    params = StdioServerParameters(
        command="uvx",
        args=["windows-mcp", "serve", "--exclude-tools", "PowerShell,Registry"],
        env={"WINDOWS_MCP_CONTROL": "false", "ANONYMIZED_TELEMETRY": "false"}
    )
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            res = await session.call_tool("Notification", arguments={
                "title": "Windows-MCP Ready",
                "message": "Immediate execution verified.",
                "app_id": "Hermes Agent"
            })
            for c in res.content:
                print(c.text)

asyncio.run(main())

Output:

Notification sent: "Windows-MCP Ready" - Immediate execution verified.

No 10-second wait, no CONTROL_PREEMPTED, and no silent drops.