dsh-unified-desktop
两个 Windows 桌面自动化 MCP,44 个工具,其中 15 个是重复或危险的。
这个插件把它们合成一个:29 个工具,一个命名空间,内建危险调用拦截。
不是简单的代理转发。它知道 click 该用哪个后端(wincu 走 UIA Invoke,不抢焦点),
ocr 只有一边有,read_webpage 的 DOM 模式只有另一边有,
而 PowerShell / Registry / FileSystem / Process 两边都不该暴露。
合并的是 dsh-computer-use-win(UI Automation + OCR + 元素级语义操作) 与 Windows-MCP(浏览器 DOM + 区域截图 + 应用启动)。 每个取舍的理由见 能力归属矩阵。
它解决的问题
单独装这两个插件,模型会看到 44 个工具、两份互相打架的说明:
| 问题 | 具体表现 |
|---|---|
| 能力重复 | click / Click、type_text / Type、scroll / Scroll… 共 10 组,模型不知道该选哪个 |
| 上下文吃紧 | 两套 schema 合计 61,599 tokens(实测),光工具定义就吃掉大半上下文 |
| 危险工具裸露 | windows-mcp 默认暴露 PowerShell(任意命令)、Registry(改注册表)、FileSystem、Process(kill) |
| 无统一入口 | 两个 MCP server 各自启动,工具命名空间分裂成两个 |
合并后的变化
| 分开装 | 本插件 | |
|---|---|---|
| MCP server 数 | 2 | 1 |
| 工具数 | 44 | 29 |
| 重复能力 | 10 组 | 已去重 |
| 危险工具 | 全部裸露 | 内建守卫拦截 |
| 工具命名空间 | 分裂成两个 | 统一 mcp__desk__* |
工具数与去重结果由 lib/catalog.mjs 的 CATALOG / EXCLUDED 定义,
node test/catalog-test.mjs 会校验一致性(43 项断言)。
关于上下文占用的说明:44 个工具的实测基线是 61,599 tokens,
数据在 docs/tool-footprint.json(2026-10-22 从两个后端实时拉 tools/list 后统计)。
合并后只保留 29 个工具的 schema,但具体数值随上游 schema 体积变化,本项目未持续测量——
本插件只做去重与重命名,不裁剪单个工具的 schema。
想在自己的环境里量,可以跑 node test/aggregate-e2e.mjs,它会打印当前工具面的字符数与估算 token。
和通用 MCP 网关有什么不同
GitHub 上已有成熟的多 MCP 聚合方案(1MCP 525★、 IBM mcp-context-forge 4.7k★、 MCP-Gateway 155★)。它们是横向的:接任意 N 个 server,靠配置里的 filter 规则裁剪工具面。
本项目是纵向的:只为两个特定后端服务,靠一张能力归属表做语义级去重——
它知道 Snapshot 该拆成「树模式用 wincu、DOM 模式并入 read_webpage」,
而通用网关只能按名字过滤,不知道这两个工具其实是同一件事的不同实现。
如果你要接的是别的 MCP server,用上面那些通用网关更合适。 如果你就在 Windows 上用这两个后端,这张归属表是为你写的。
这张图由
node scripts/gen-dedup-svg.mjs从lib/catalog.mjs生成, 数字与代码永远一致,不会随文档改动而失同步。
架构
DSH ──stdio──> [aggregate.mjs] ──stdio──> wincu server.mjs
└─stdio──> windows-mcp.exe
聚合层负责三件事:
- 合并工具面 —— 从两个后端拉
tools/list,按目录去重合并 - 路由调用 ——
tools/call按目录映射到对应后端 - 内建守卫 —— 危险参数在转发前拦截
能力分工
一句话概括:交互类全给 wincu,系统与浏览器类全给 windows-mcp。
选 wincu 的(它更强):
| 能力 | 为什么用 wincu |
|---|---|
snapshot / read_tree / find |
UIA 树 + RuntimeId 精确元素定位 |
ocr |
只有 wincu 有,Windows.Media.Ocr 字级坐标 |
click / double_click |
优先走 UIA Invoke 模式,不抢焦点 |
invoke / set_value |
只有 wincu 有,UIA 语义操作 |
type_text |
剪贴板路径支持中文,且恢复原剪贴板 |
| 窗口管理 | activate / close / move |
选 windows-mcp 的(wincu 没有):
| 能力 | 用途 |
|---|---|
read_webpage |
浏览器 DOM 模式,读当前标签页,绕开反爬 |
screenshot |
区域截图 region + 多屏 display |
launch_app |
按开始菜单名启动应用 |
notify |
Windows toast 通知 |
clipboard |
读写剪贴板 |
display_info / control_status |
显示器信息、桌面控制权 |
不暴露的工具
风险类(内建守卫,且 DSH 原生工具已覆盖):
| 工具 | 排除原因 |
|---|---|
PowerShell |
任意命令执行;DSH 原生 pwsh 已覆盖且受策略管辖 |
Registry |
直接改注册表,风险过高 |
FileSystem |
DSH 原生 read/write/edit/glob/grep 已覆盖 |
Process |
kill 风险高 |
重复类(已由 wincu 的更强实现替代):
Click、Type、Move、Scroll、Shortcut、Wait、WaitFor、Snapshot、MultiSelect、MultiEdit
完整清单见 lib/catalog.mjs 的 EXCLUDED。
每一项「为什么留这个、为什么砍那个」的完整推导,见 docs/capability-matrix.md。
工具清单(29 个)
观察
snapshot screenshot read_tree element_info find ocr list_windows read_webpage display_info control_status health
鼠标键盘
click double_click type_text keypress scroll move_mouse drag set_value invoke focus
窗口
activate_window close_window move_window launch_app
等待
wait wait_for
系统
notify clipboard
安装
前置:
# 1. wincu(提供 UIA + OCR)
dsh plugin --profile web add dsh-computer-use-win
# 2. windows-mcp(提供 DOM + 应用启动)
uv tool install windows-mcp --python 3.14
安装本插件:
dsh plugin --profile web add <本目录路径>
打后端补丁(重要):
node scripts/patch-backend.mjs
不打补丁的话,windows-mcp 会要求你连续空闲 10 秒才允许操作,
否则 screenshot / read_webpage / launch_app 等全部返回 USER_CONTROL。
详见 docs/backend-patch.md。
⚠️ 这个补丁会直接修改你本机的 windows-mcp 安装
scripts/patch-backend.mjs不是插件,它改写uv tool安装目录下的windows_mcp/desktop/control.py与control_hooks.py源文件。执行前请知悉:
重要:本插件已聚合 wincu 的能力,需禁用独立挂载的 wincu避免工具重复。
在 profile 的 cordis.patch.yml 加:
- id: mcp-dsh-computer-use-win
disabled: true
重启 DSH,工具以 mcp__desk__* 出现。
设置项
| 项 | 默认 | 说明 |
|---|---|---|
enabledTools |
全部 29 个 | 暴露哪些工具 |
guardEnabled |
true |
策略守卫开关 |
auditLog |
true |
写审计日志 |
idleThresholdSeconds |
0.5 |
windows-mcp 桌面控制门的空闲阈值(秒) |
wcuServerPath |
自动探测 | wincu server.mjs 路径 |
wmExePath |
自动探测 | windows-mcp.exe 路径 |
toolCallTimeoutMs |
180000 |
工具超时 |
一个关键实现细节
wincu 必须用 .generations/live/ 下的真实路径启动,不能用 profiles/web/node_modules 的软链接。
因为它的入口判断是:
import.meta.url === pathToFileURL(process.argv[1]).href
软链接会让 import.meta.url 解析成真实路径,而 argv[1] 是链接路径,两者不等,
server 会静默退出(exit 0,无任何输出)。
本插件的 resolveWcuServer() 会自动优先选 .generations 路径。
测试
npm test # 全部 6 个套件
单独跑:
node test/catalog-test.mjs # 目录一致性、去重、危险工具排除(43 断言)
node test/config-test.mjs # 配置校验与路径解析(21 断言)
node test/backend-patch-test.mjs # 后端补丁状态与行为(14 断言,无后端时跳过)
node test/aggregate-e2e.mjs # 工具面合并、双后端路由(13 断言)
node test/concurrency-test.mjs # 并发不串号(6 断言)
node test/electron-verify.mjs # DSH 真实 Electron 运行时(7 断言)
catalog-test、config-test、backend-patch-test不需要运行中的后端,任何环境都能跑 (CI 默认跑这三个)。其余三个需要本机已安装 wincu 与 windows-mcp。
诊断
遇到 wm 工具被拒(USER_CONTROL)时:
node scripts/diagnose-gate.mjs
输出补丁状态、5 秒内的钩子触发统计、以及 MCP 层实测结果。 完整排查过程见 docs/diagnosis-user-control.md。
排错
工具没出现(mcp__desk__* 不存在)
- 看日志
%APPDATA%\dsh-desktop\logs\harness.log,搜[aggregate] - 正常情况下应有:
[aggregate] unified-desktop 0.1.0 ready (wcu=true wm=true guard=true) [wcu] windows-computer-use MCP server 0.5.0 ready [wm] Starting MCP server 'windows-mcp' - 如果
wcu=false:检查 wincu 是否安装。路径必须是profiles\.generations\live\dsh-computer-use-win+*\node_modules\dsh-computer-use-win\mcp\server.mjs - 如果
wm=false:uv tool install windows-mcp --python 3.14
loader 一直显示 still waiting for 1 plugin entries
这是正常的。MCP 初始化约需 30-40 秒(两个后端依次启动),loader 只是计时提示, 插件就绪后不再刷新该行。
工具重复出现(mcp__wincu__* 和 mcp__desk__* 都有)
说明独立的 wincu 没有被禁用。在 profile 的 cordis.patch.yml 加:
- id: mcp-dsh-computer-use-win
disabled: true
调用返回 USER_CONTROL
windows-mcp 的桌面控制门。两种情况:
- 没打后端补丁 → 执行
node scripts/patch-backend.mjs后重启 - 打了补丁但你正在操作 → 停下鼠标键盘约
idleThresholdSeconds秒再试
用 control_status 查看当前状态:state: "ready" 表示可操作,"user" 表示正在等你空闲。
如果一直卡在 user 且 wait_seconds 固定在某个值附近不动,说明有设备在高频上报
无位移的鼠标移动(常见于无线接收器)。补丁 2 已处理这种情况,确认它是否应用:
node scripts/patch-backend.mjs --check
DOM 模式读不到网页
read_webpage 的 use_dom=true 需要浏览器(Chrome/Edge/Firefox)有活动标签页,
且该页面已加载完成。
已知限制
- 不是沙箱。守卫是正则黑名单,挡得住直白危险命令,挡不住刻意构造的绕过。
- wincu 升级可能破坏路径。
resolveWcuServer按目录名前缀匹配,升级后版本号变化会自动重新探测。 - 两个后端独立启动。一个挂了另一个仍可用,但相应工具会缺失。
- DOM 模式依赖浏览器。
read_webpage的use_dom需要 Chrome/Edge/Firefox 活动标签页。 - 参数未做归一化。两个后端的参数名保持原样(如 wincu 用
elementId,wm 用loc/label), 模型需要按各工具自己的 schema 传参。归一化在Unreleased计划中。 - 依赖本地后端补丁。windows-mcp 0.8.8 的桌面控制门在部分硬件上不可用,
本项目通过
scripts/patch-backend.mjs修复。已向上游报告 (CursorTouch/Windows-MCP#498); 若上游合并,可移除本地补丁。详见 docs/upstream-tracking.md。
设计取舍
为什么用「聚合」而不是「改源码合并」
两个后端分别是 Node(wincu)和 Python(windows-mcp)。真合并意味着用同一种语言重写另一方的核心能力:
- 把 wincu 的 OCR + UIA RuntimeId 机制移植到 Python —— 工作量大且易出错
- 把 windows-mcp 的 DOM 提取移植到 Node —— 需要重写 CDP/IAccessible2 逻辑
聚合层用 MCP 协议做边界,两个后端各自演进,互不干扰。代价是多一个进程,收益是
升级任一方都不需要改聚合层(除非工具名变了,那会在 buildToolList 里报 missing backend tool)。
为什么排除 PowerShell / Registry / FileSystem / Process
不是能力不够,而是重复且危险:
- DSH 原生已有
pwsh、read/write/edit/glob/grep,且受 DSH 沙箱与审批管辖 - MCP server 是独立进程,DSH 的沙箱管不到它(
dsh-fs-sandbox的策略只作用于 DSH 自己的工具) - 保留它们等于开一条绕过沙箱的通道,收益为零
贡献
git clone <repo>
cd dsh-unified-desktop
npm test # 需要本机已装 wincu 与 windows-mcp
添加新工具:
- 在
lib/catalog.mjs的CATALOG加条目(name/backend/target/description) - 跑
node test/catalog-test.mjs—— 它会校验命名规范、去重、能力覆盖 - 跑
node test/aggregate-e2e.mjs—— 确认后端真的提供了该工具
修改守卫规则:编辑 lib/guard.mjs 的 PS_RULES / REG_DENY_PREFIX,然后跑 npm test。
License
MIT。聚合层原创;能力来自两个上游项目(均为 MIT)。
上游的版权声明见 THIRD-PARTY-NOTICES.md。
No comments yet. Be the first to write one.