feat(posts): publish 4-part windows-mcp series across historical dates with full EN+ZH assets
Deploy / build (push) Successful in 22s
Deploy / build (push) Successful in 22s
This commit is contained in:
1 parent
4cfbc33a6f
commit
d5ab7dec81
36 files changed
+1537
-127
No files matched your search
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "Why Your Windows Desktop MCP Calls Silently Fail: Taming the 10-Second Idle Gate"
|
||||
description: "Why actions in windows-mcp return CONTROL_PREEMPTED or fail silently, and the exact flag needed to disable the desktop control gate."
|
||||
pubDate: 2026-03-12
|
||||
updatedDate: 2026-10-25
|
||||
category: notes
|
||||
tags: ["windows", "mcp", "debugging", "ai-agents", "troubleshooting"]
|
||||
ogImage: /og/why-your-windows-mcp-calls-silently-fail.jpg
|
||||
banner: /banners/why-your-windows-mcp-calls-silently-fail.jpg
|
||||
draft: 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```bash
|
||||
uvx windows-mcp serve --no-control
|
||||
```
|
||||
|
||||
The server instantly crashes on startup with:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```bash
|
||||
WINDOWS_MCP_CONTROL=false
|
||||
```
|
||||
|
||||
When injected into your MCP client's environment block:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```python
|
||||
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:
|
||||
```text
|
||||
Notification sent: "Windows-MCP Ready" - Immediate execution verified.
|
||||
```
|
||||
|
||||
No 10-second wait, no `CONTROL_PREEMPTED`, and no silent drops.
|
||||
Reference in new issue
Block a user