为什么你的 Windows 桌面 MCP 调用会静默失败:攻破 10 秒空闲门禁与避坑实录

你刚装好 windows-mcp,将其加到 Agent 的配置中,然后让它发送一条 Windows 系统通知,或者点击某个打开的应用窗口按钮。

工具调用在不到 200 毫秒内就返回了,大模型语气轻松地向你汇报:“通知已成功发送。”

然而屏幕上什么都没有发生。

当你把 MCP 协议传输的底层原始 JSON 结果打印出来时,既没有看到标准的 Python 异常,也没有看到 Win32 系统错误,而是看到这样一段内容:

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

或者在不同的时序下,后台进程直接抛出一串异常堆栈:

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

这就是导致大部分桌面 MCP 调用静默失败的隐蔽真凶。


根因剖析:桌面控制门禁(Desktop Control Policy)

windows-mcp 内部设计了一套桌面所有权控制机制(位于 src/windows_mcp/desktop/control_overlay.py 与 tools/control_policy.py)。

开发者为了避免 AI 在物理桌面上与正在操作鼠标键盘的真人“抢夺输入焦点”,实现了一个空闲检测看门狗:

  1. 在执行任何具有副作用的输入工具(Click、Type、Move、Shortcut、Notification 等)前,看门狗会通过 Win32 API GetLastInputInfo 读取最后一次物理输入的时间戳。
  2. 如果人类在过去 10 秒内(WINDOWS_MCP_CONTROL_IDLE_TIMEOUT)移动过鼠标或敲击过键盘,服务端判定此时“人类正在使用电脑”。
  3. 此时它既不排队也不等待,而是立刻主动放弃执行,直接返回 CONTROL_PREEMPTED(控制权被抢占),工具实际执行步骤数为 0。

但现实使用场景是:你在终端敲完回车下达指令,或者在聊天框点下发送——这意味着你刚刚操作过键盘鼠标。你的系统空闲时间必然小于 1 秒,因此 Agent 随后的第一次工具调用几乎 100% 触发抢占拦截,导致操作直接被吞。


为什么 --no-control 参数不管用

翻看该项目的 README 或文档,你可能会看到关于 --control / --no-control 的说明。

很多人第一反应是给命令加上 --no-control:

uvx windows-mcp serve --no-control

但服务会在启动瞬间直接崩溃报错:

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

这是因为该项目基于 Click 库构建 CLI,--no-control 只绑在顶层或其他模式上,serve 子命令的参数解析器中根本没有注册这个 CLI 标志。


真正有效的解法:WINDOWS_MCP_CONTROL=false

通过审查代码可以发现,配置加载模块(src/windows_mcp/config.py)会优先读取系统环境变量。

要彻底绕过这道 10 秒空闲限制,允许 Agent 在下达指令后立即获得执行权限,只需注入环境变量:

WINDOWS_MCP_CONTROL=false

将其填入 Agent 的配置文件 env 字段:

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

此时控制层覆盖(Overlay)、空闲看门狗以及拦截抢占逻辑将被完全旁路关闭。


验证与验收

为了验证问题已彻底根除,在鼠标持续移动的状态下通过原生 Python MCP 客户端发起 Toast 通知测试:

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())

终端实时输出:

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

无需苦等 10 秒倒数,再无 CONTROL_PREEMPTED,所有桌面动作即时执行。关于什么时候该用系统 API、什么时候该让 Agent 操控物理键鼠,可以进一步参考:什么时候该让 AI 操控物理鼠标(以及什么时候绝对不要用)。


需要实现复杂的桌面端或企业级自动化?

如果你正在跨越遗留软件缺乏 API 的鸿沟,需要构建结合 MCP、无头浏览器与可靠键鼠模拟的桌面自动化链路:

Lee Teong Hoe

Lee Teong Hoe · Mr Hoelee

Full-stack developer & DevOps engineer based in Malaysia. I run my own production infrastructure and build web systems for small businesses — this blog is the living record.

Hiring? Profile & stack · Malaysian SME? Website design & development ↗ · All posts