Files
SimpleRemoter/docs/Mcp_Phase2_Design.md
yuanyuanxiang c6c6e1d5ef Improve: Add optional max_width to get_screenshot
The get_screenshot tool inherited the MFC thumbnail-preview profile, capping
the frame at 1024px wide (1280 on 4K source), which is too small for AI
vision/OCR of dense UI. Add an optional max_width argument that overrides the
width (clamped to the client's 64..1920 limit) while keeping the RTT-adaptive
jpegQuality. Omitted or 0 keeps the existing thumbnail profile; 1920 yields
near-native resolution (full 1080p on a 1080p source, 1920 wide on 4K). Sync
the design doc.

Co-Authored-By: deepseek-v4-pro
2026-08-19 13:41:38 +02:00

37 KiB
Raw Blame History

YAMA MCP 功能开发技术书Phase 2 及后续)

状态设计定稿已按两轮评审修订P2a、P2b 已实现并经真实主机验证(search_hosts / get_host_detail / list_processes / list_windows / get_activity_historyP2c 已实现、待实机验证(get_screenshot / list_files)。 读者MCP 后续功能的研发/评审人员。 关联文档Mcp_Design.mdPhase 1 的协议、架构、配置与菜单设计)。 核心目标:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,不一次性大改现有功能


1. 背景与目标

Phase 1 已跑通 MCP 最小闭环:httplib + JSON-RPC 2.0,单工具 list_online_hosts,静态 Bearer token经「扩展 → MCP设置」对话框开启。目标是把 YAMA 服务端已有的主机管理能力(进程、窗口、终端、截图、文件等)逐步暴露为 MCP 工具,供 Claude Code 等 AI 助手以工具调用方式使用。

本技术书解决三个问题:

  1. 做什么——工具路线图与优先级(只读 → 安全写 → 会话)。
  2. 怎么做——复用现有协议,无头驱动、不弹对话框;区分「主连接 RPC」与「子链接流式」两种模式
  3. 怎么稳——每个阶段独立可交付、可回滚,影响面小、可验收。

2. 现状回顾Phase 1 已交付)

模块 文件 职责
HTTP/JSON-RPC 分发 McpServer.h / McpServer.cpp POST /mcptoken 校验;initialize/ping/tools/list/tools/call
工具序列化复用 HostJson.h / HostJson.cpp BuildHostJson(ctx, clientMap),单主机 JSON与 WebService 共用)
配置对话框 McpSettingsDlg.h / .cpp 运行时构造对话框enable/port/bind/token
生命周期接入 2015RemoteDlg.cpp:2136-2162(启动)、3900-3906(停止) 随主对话框启停
FRP 暴露 2015RemoteDlg.cpp:3269-3304 MCP 端口仅在「启用 + 绑 0.0.0.0」时纳入 FRP

关键结论:MCP 未引入任何新第三方库——httplibHTTPjsoncppJSON均为项目既有依赖。

当前工具实现只有一个 dispatch 分支(McpServer.cppBuildToolsCall),新增工具的边际成本很低:注册 outputSchema + 加一个 tools/call 分支


3. 设计原则(贯穿所有后续开发)

  1. 只读优先,写操作单独评审:延续 Mcp_Design.md §1.3。只读工具直接做;任何「改变被控端状态」的工具(命令执行、杀进程、传文件、控制)必须经安全评审 + 独立授权。
  2. 复用现有协议,不新造命令:一切数据获取/操控都走 common/commands.h 里既有的 COMMAND_*(服务端→客户端)与 TOKEN_*(客户端→服务端),不新增自定义命令。
  3. 无头接管复用 Web 终端模式:对话框只是 UI 外壳MCP 复用 WebService 的挂起标记/接管模式,把数据接到 MCP 侧,不弹框
  4. 每阶段独立可交付、可回滚:一个阶段 = 一批工具 + 对应验收;出错可单独 revert不连坐。
  5. 超时与清理是硬约束MCP 是 request/response一次 tools/call 不能无限阻塞等数据;必须有超时、迟到回收、用完即关。
  6. 输出 schema 先行:每个工具先在 tools/list 里声明完整 inputSchema/outputSchemaAI 客户端据此决定何时调用、如何解析。

4. 核心机制:无头驱动(双模式)

本节是本技术书的关键。此前设计把「进程/窗口/截图」误当作「子链接」,评审后纠正为:这些是主连接 request/response;真正走子链接的只有终端、完整屏幕、文件传输等大流量/持续流能力。两者实现机制不同,必须分开建模。

4.1 连接模型:主连接 vs 子链接

common/commands.h 定义了完整协议,分两层:

  • 主连接:客户端上线后与主控保持的长连接。承载心跳(TOKEN_HEARTBEAT)、命令下发,以及轻量 request/response——屏幕预览缩略图(TOKEN_SCREEN_PREVIEW_RSP)、历史活动(TOKEN_REPORT_ACTIVITY)等。响应以 TOKEN_* 回到主连接(捕获点见 §4.2)。
  • 子链接:屏幕、终端、文件传输、进程/窗口列表等功能,服务端下 COMMAND_* 后客户端新建一条 TCP 子链接回连,数据以 TOKEN_* 开头,作为一个新的 CONTEXT_OBJECT 进入分派。其中进程/窗口列表是一次性子链接(回一条 TOKEN_PSLIST/TOKEN_WSLIST 即关,见 §4.2 模式 A终端/屏幕/文件传输则是持续流子链接

两者都由同一个 MessageHandle(CONTEXT_OBJECT*) 分派,靠 ContextObject->GetClientID() 关联到同一台主机。判断一个 TOKEN_* 属于哪层,看它的数据落在哪个 context 上

证据 结论
客户端 KernelManager.cpp:1156 COMMAND_SYSTEMnew IOCPClient 子链接 + LoopProcessManagerSystemManager.cpp:75 GetProcessList()szBuffer[0]=TOKEN_PSLIST,经子链接 Send2Server 回传 进程/窗口列表 = 一次性子链接(主连接下 COMMAND_SYSTEM/COMMAND_WSLIST,子连接回 TOKEN_PSLIST/TOKEN_WSLIST
2015RemoteDlg.cpp:8527-8536SendScreenPreviewRequestctx->Send2Client(...);响应 TOKEN_SCREEN_PREVIEW_RSPMessageHandle:6173 屏幕预览 = 主连接 RPC
WebService.cpp:1836-1900 + 2015RemoteDlg.cpp:6312-6351COMMAND_SHELL → 客户端建 shell 子上下文 → TOKEN_SHELL_START/TOKEN_TERMINAL_START 终端 = 子链接

4.2 模式 A主连接 RPC截图 / 历史活动)

适用于:get_screenshot(屏幕预览)、get_activity_history(历史活动)。

MCP 工具 tools/call
  └─► 主 context ctx = FindHost(device_id)
        └─► ctx->Send2Client(COMMAND_*)                  // 主连接发 1 字节命令
客户端处理后在主连接回 TOKEN_*
  └─► MessageHandle 收到 TOKEN_*(数据已在 ctx->InDeCompressedBuffer
        ├─ if (McpServer().IsPending(devId))
        │      McpServer().TakeMainResponse(devId, ctx->GetBuffer(0), ctx->GetBufferLength()); // IO 线程同步拷贝
        │      break;                                    // 跳过 MFC 弹框(不 CancelIO主连接不可关
        └─ 否则走原 MFC 路径(如打开历史活动对话框)

要点:

  • 无子链接、无 subCtx:数据就在主 context 缓冲里MCP 只需在 MessageHandle 的对应 TOKEN_* 分支里同步拷贝到结果缓冲,再唤醒等待者。
  • 不 CancelIO:响应落在主连接上,关闭会断开整个主机会话;故拦截后仅 TakeMainResponse + break,主连接保持。
  • 捕获点因工具而异(必须在 IO 线程同步拷贝):屏幕预览的 TOKEN_SCREEN_PREVIEW_RSP 直接读本消息的 szBuffer/len2015RemoteDlg.cpp:6173-6185 已在 case 内把 szBuffer 拷贝到堆消息再转主线程);历史活动的 TOKEN_REPORT_ACTIVITYGetBuffer(0)= InDeCompressedBuffer)。两者都是每 context 单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在 MessageHandle 内部IO 线程),不能异步等 UI 线程。
  • 请求关联get_screenshotreqIdScreenPreviewReq.reqId)可丢弃过期响应;get_activity_history 无请求 id,靠「每 host 单飞行」(见 §4.4)保证正确性。

4.2 模式 A一次性子链接进程 / 窗口列表)

适用于:list_processeslist_windows这是本技术书相对原设计的重要纠正:进程/窗口列表不是主连接 RPC而是一次性子链接——COMMAND_PSLIST 在主连接上是空操作(客户端 KernelManager::OnReceive 无此 case正确的触发命令是 COMMAND_SYSTEM(进程)/COMMAND_WSLIST(窗口)。

MCP 工具 tools/call
  └─► 主 context ctx = FindHost(device_id)
        └─► ctx->Send2Client(COMMAND_SYSTEM / COMMAND_WSLIST)   // 主连接发 1 字节命令
客户端 KernelManager::OnReceive 收到后新建一条子链接new IOCPClientEnableSubConnAuth
  └─► 子链接 ConnectServer 后先发 TOKEN_CONN_AUTH服务端 SetID 把 clientID 钉在子 context 上)
        └─► CSystemManager 构造函数立即 Send2Server(TOKEN_PSLIST / TOKEN_WSLIST)
  └─► MessageHandle 在【子 context】上收到 TOKEN_PSLIST / TOKEN_WSLISTGetClientID()==主 host id
        ├─ if (McpServer().IsPending(devId))
        │      McpServer().TakeMainResponse(devId, GetBuffer(0), GetBufferLength());
        │      ContextObject->CancelIO();       // 用完即关一次性子链接(不关会泄漏)
        │      break;                           // 跳过 WM_OPENSYSTEMDIALOG
        └─ 否则 g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, ...)  // MFC 路径

要点:

  • 有子链接、有 subCtx:数据落在子 context 上,非主 context。子 context 的 GetClientID() 已由 TOKEN_CONN_AUTH2015RemoteDlg.cpp:6291SetID)钉成主连接 clientIDIsPending(devId) 能与主 context 的 device_id 对上。

  • 用完即关MCP 拦截后 CancelIO() 关闭子链接(客户端 IOCPClient 以 exit_while_disconnect=true 构造,会随断开退出);不关会泄漏子链接。

  • 与 MFC 对话框可并存MCP 与 MFC 对话框各自使用独立的子链接(客户端每次 COMMAND_SYSTEM/COMMAND_WSLIST 都新建一条因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」§4.4)规避,而非「与 MFC 对话框互斥」。

  • 编码:进程名/路径按 clientType 判定Windows=GBK/936、LNX/MAC=UTF-8进程枚举走 A 接口不随 CLIENT_CAP_UTF8 转 UTF-8窗口标题为客户端 UTF-8按能力位 CLIENT_CAP_UTF8 判别,老客户端回落 CP936

  • 请求关联:无请求 id → 每 host 单飞行§4.4)。

  • 文件列举也走 Alist_files:列盘与进程/窗口同构(COMMAND_LIST_DRIVE→一次性子链接→TOKEN_DRIVE_LIST 即关);列目录则在此一次性子链接上多一轮 COMMAND_LIST_FILESTOKEN_FILE_LIST 后再关。故 list_files 归入模式 A而非原设计的模式 A详见 §6.3)。

模式 A0主连接单向命令无响应kill_processCOMMAND_KILLPROCESS)、send_messageCOMMAND_TALK)这类「发完即走」的命令没有 MCP 需要的响应数据,不需要 §4.4 的挂起注册表/超时/单飞行——Send2Client 后直接返回成功。它是模式 A 的退化情形,实现最简单。

4.3 模式 B子链接流式终端 / 完整屏幕 / 完整文件)

适用于:exec_command(终端),以及未来的实时远程桌面、完整文件传输。

MCP 工具 tools/call
  └─► 登记挂起请求 m_Pending[device_id](标记 mode=B
  └─► ctx = FindHost(device_id); ctx->Send2Client(COMMAND_SHELL)   // 主连接下命令
客户端建 shell 子链接
  └─► MessageHandle 收到 TOKEN_SHELL_START / TOKEN_TERMINAL_START
        ├─ if (McpServer().IsPending(devId))
        │      McpServer().TakeSubContext(devId, ContextObject);   // 接管子上下文
        │      break;                                              // 跳过弹框
        └─ 否则弹 WM_OPENSHELLDIALOG
后续 shell 输出
  └─► MessageHandle 顶部 IsTerminalContext 分支 → McpServer().OnShellData(...) 泵数据

完全复刻 WebServiceIsTermPending/RegisterTerminalContext/OnTerminalData 模式(WebService.h:283-287),但不复用其长会话状态——MCP 是「一次性:建链 → 收发 → 关链」。

4.4 统一挂起请求注册表

模式 A主连接 RPC与模式 A一次性子链接共用一个按 device_id= context::GetClientID())索引的注册表。当前实现P2b只覆盖 A/A 两种「一次性响应」,故结构里只有 data + done,无需 mode/subCtx/timedOut;未来 P3a 的 exec_command(模式 B 流式)再扩展 subCtx 字段。

struct PendingRequest {
    std::string       tool;
    std::vector<BYTE> data;   // 结果缓冲MessageHandle 内同步拷贝的字节)
    bool              done = false;
};
std::mutex              m_PendingMutex;
std::condition_variable m_PendingCv;
std::map<uint64_t, PendingRequest> m_Pending;   // 受 m_PendingMutex 保护

暴露给 MessageHandle 的钩子:

  • bool IsPending(uint64_t device_id) —— 该 TOKEN_* 是否为 MCP 触发。
  • void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) —— 模式 A/A拷贝缓冲、done=true、唤醒 cv(在 IO 线程阻塞期间同步调用,拷贝安全)。

工具线程侧的方法:

  • bool BeginPending(uint64_t device_id, const std::string& tool) —— 登记挂起(false = 该 host 已有挂起,返回「设备忙」)。
  • bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) —— 等待响应(超时/失败自动清理并返回 false)。
  • void ClearPending(uint64_t device_id) —— 提前退出路径的清理。

每 host 单飞行:同一 device_id 同一时刻只允许一个挂起请求。这是「无请求 id」工具的正确性前提——进程/窗口/历史活动响应都不带请求 id若同 host 并发两个请求,无法区分响应归属,故并发第二个请求返回「设备忙」(-32003)而非串扰。

说明单飞行只约束「MCP 侧同 host 并发」,约束「MCP 与 MFC 对话框并存」——二者各用独立子链接(模式 A或各自触发模式 A互不占位。详见 §4.2′。

4.5 超时与清理MCP 与 Web 的关键差异)

Web 是长连接子链接可长驻MCP 是一次性 request/response。因此:

  • 超时wait_for(timeout) 等数据;当前 P2b 用编译期常量 kMcpToolTimeoutMs = 20000(暂未接配置,见 §9。超时返回 JSON-RPC 错误(-32001),并清理注册表。
  • 迟到回收(已知边界):数据在超时后才到达(IsPending 已清)时,拦截分支不成立,响应会回落到原 MFC 路径(进程/窗口弹 WM_OPENSYSTEMDIALOG,历史活动弹对话框)——属「客户端在 20s 内未响应」这一病态场景的罕见副作用,不会串扰或崩溃。正常路径(<1s 内响应)不受影响。若未来要彻底消除,可给注册表加「超时墓碑 + 宽限期丢弃」。
  • 用完即关:模式 A进程/窗口)拦截后 CancelIO() 关一次性子链接;模式 A主连接 RPC不关主连接。未来模式 B终端收集完 stdout 后主动关子上下文。
  • 阻塞 httplib workerwait_for 会阻塞 HandleMcp 所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 2030s 会占住一个 worker。MCP 属低频串行调用AI 逐条调用),本阶段可接受;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。

5. 工具路线图(分阶段)

5.1 工具总表

「模式」标注实现路径A=主连接 RPCA=一次性子链接A0=主连接单向命令[无响应]B=子链接流式,—=纯内存)。

阶段 工具 功能 复用协议/函数 模式 风险
P1已交付 list_online_hosts 在线主机列表 BuildHostJson 只读
P2a已实现并验证 search_hosts 按名称/IP/分组/OS/在线态过滤 list_online_hosts 内存过滤 只读
P2a已实现并验证 get_host_detail 单机详情(不含活动历史) 现成 device 字段(BuildHostJson 只读
P2b已实现并验证 list_processes 主机进程列表 COMMAND_SYSTEM→(子链接)TOKEN_PSLIST A 只读
P2b已实现并验证 list_windows 主机窗口列表 COMMAND_WSLIST→(子链接)TOKEN_WSLIST A 只读
P2b已实现并验证 get_activity_history 单机历史活动(文本快照) COMMAND_QUERY_ACTIVITY(159)→TOKEN_REPORT_ACTIVITY(160) A 只读
P2c已实现 get_screenshot 单帧屏幕截图JPEG COMMAND_SCREEN_PREVIEW_REQTOKEN_SCREEN_PREVIEW_RSP A 只读
P2c已实现 list_files 目录列举 COMMAND_LIST_DRIVETOKEN_DRIVE_LIST(列盘)/ 同子链接再 COMMAND_LIST_FILESTOKEN_FILE_LIST(列目录) A 只读
P3 exec_command 执行命令、返回 stdout COMMAND_SHELLTOKEN_SHELL_START/TOKEN_SHELL_DATA B
P3 send_message 给被控端弹消息 COMMAND_TALK(220) A0 写(低危)
P3 kill_process 结束进程 COMMAND_KILLPROCESS A0
P4暂缓 实时远程桌面 / 交互终端 持续流 屏幕/终端子链接 + SSE/轮询 B 需会话模型

5.2 Phase 2只读观测低风险无写操作

全部只读不改变被控端任何状态token 模型不变(仍单一静态 token。P2a 纯内存、零改动P2b/P2c 引入模式 A/A 无头接管,但只读、可回滚。

5.3 Phase 3安全写操作需单独评审

进入 Mcp_Design.md §1.3 明确划出的「写操作」区。每个工具交付前需评审三点(见 §8命令白名单、审计日志、分组授权 + token 分级

5.4 Phase 4会话/流式(暂缓)

实时远程桌面、交互终端等需要「持续流」的能力,与当前 request/response 模型不匹配。需先决定传输方案SSE / 提交任务 + 轮询),本期不做,避免过早引入复杂会话状态。


6. 分阶段实施计划

每期都给出:改动文件、接口、测试点、回滚方式。原则是每期可独立合入、独立验收、独立回滚

6.1 P2asearch_hosts + get_host_detail(纯内存,零子链接) 已实现并验证

改动:仅 McpServer.cpp

  • BuildToolsListResult 增两个工具描述 + schema。
  • BuildToolsCall 增两个分支,复用 list_online_hosts 已遍历出的主机数据做内存过滤/单机抽取(抽出 CollectOnlineHosts 收集 + BuildHostItemSchema 复用)。
  • 无任何 MessageHandle/WebService/context 改动。

影响面:极小,只加不改。回滚revert 单文件即可。

验收tools/list 返回 3 个工具;tools/call search_hosts 按 filter 正确过滤;get_host_detail 对在线主机返回正确字段、对离线/非法 id 返回明确错误。

范围修正:原计划 get_host_detail 附带「活动历史」,但活动历史是 COMMAND_QUERY_ACTIVITY(159)→TOKEN_REPORT_ACTIVITY(160) 的主连接往返(客户端上报、服务端不落库),非纯内存字段,违反 P2a「纯内存」约束。故 P2a 的 get_host_detail 只返回主机实时字段,活动历史单列为 P2b 的 get_activity_history(模式 A

6.2 P2blist_processes + list_windows + get_activity_history(模式 A / 模式 A首次无头接管 已实现并验证

本节已按实际实现修订:进程/窗口是一次性子链接(模式 A历史活动是主连接 RPC(模式 A见 §4.2/§4.2′。

改动

  • McpServer.h/.cpp
    • 加 §4.4 的 m_Pending 注册表 + IsPending/TakeMainResponse/BeginPending/WaitPending/ClearPending,每 host 单飞行检查。
    • 加解析器 ParseProcessListPID/name/arch/path按 clientType 编码转 UTF-8ParseWindowListhwnd/title/status/pid标题按能力位解码两者都用 BoundedStrlen 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 LocalSize 对齐引入的尾部零字节。
    • 加三个工具:list_processes(发 COMMAND_SYSTEM)、list_windows(发 COMMAND_WSLIST)、get_activity_history(发 COMMAND_QUERY_ACTIVITY)。
  • 2015RemoteDlg.cppMessageHandle
    • TOKEN_PSLIST/TOKEN_WSLIST 分支(子 contextif (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }在 UI 线程、IO 线程阻塞期间同步拷贝;用完即关子链接)。
    • TOKEN_REPORT_ACTIVITY 分支(主 contextif (McpServer().IsPending(devId)) { TakeMainResponse(...); break; }不 CancelIO,主连接不可关)。

影响面MessageHandle 只新增 if 分支,不影响现有 MFC 弹框路径(非 MCP 触发时 IsPending 为假,行为不变)。

验收

  • MFC 双击进程/窗口管理弹框仍正常回归点MFC 历史活动对话框仍正常(回归点)。
  • MCP list_processes 返回 [{pid,name,arch,path}]list_windows 返回 [{hwnd,title,status,pid}]get_activity_history 返回 {records:[...], activityHistory}UTF-8 文本,逐行拆记录)。
  • 超时(如对离线主机)返回 -32001 而非挂死;同 host 并发第二次调用返回 -32003「设备忙」;发送失败返回 -32004
  • 中文进程名/窗口标题无乱码(进程名按 clientType 解码、窗口标题按能力位 UTF-8

验证记录2026-08-19真实在线主机实测

  • list_processesWindows 11 主机):pid/name/arch/path 完整;pid==0 仅 1 条(合法 [System Process]无尾部零填充空记录——padding 防护生效。
  • list_windowshwnd/title/status/pid 完整,无空标题记录;中文标题(「搭建 MCP 架构…」「微信」UTF-8 正确。
  • get_activity_history:返回 [时间] [标题] 时长 记录UTF-8 正确。
  • 错误路径实测:缺 id-32602;非数字 id-32602;未知/离线主机 → -32002;错误 token → HTTP 401。
  • 客户端版本门槛get_activity_history 依赖客户端 2026-08-15(提交 29929e4)加入的 COMMAND_QUERY_ACTIVITY 处理;更早的客户端(如 Jul 14 2026)不识别该命令、无响应,服务端 20s 超时返回 -32001——属预期行为,非 bug。测试须选 Aug 15 2026 及之后的客户端。

回滚revert McpServer.h + McpServer.cpp + MessageHandle 两处 if 分支。

6.3 P2cget_screenshot + list_files(模式 A / A 已实现(待实机验证)

本节按实际实现修订:get_screenshot主连接 RPC(模式 A有 reqId 关联);list_files一次性子链接(模式 A——列盘 COMMAND_LIST_DRIVETOKEN_DRIVE_LIST 即关,列目录则在 TOKEN_DRIVE_LIST 到达时就地同子链接再发 COMMAND_LIST_FILESTOKEN_FILE_LIST 后关链。原设计把 list_files 误标为模式 A已纠正与 §4.2 对进程/窗口的同类纠正一致)。

get_screenshot(屏幕预览链路,主连接 RPC模式 A

  • 复用现有双击预览机制 COMMAND_SCREEN_PREVIEW_REQ(247)→TOKEN_SCREEN_PREVIEW_RSP(248)common/commands.h:416-455,单帧 JPEGScreenPreviewReq/ScreenPreviewRspHeaderctx->Send2Client 发请求、主连接回响应,不建子链接、不弹框
  • 复用 MFC 预览的 RTT/FRP 自适应参数挑选 ChooseScreenPreviewParams + SendScreenPreviewRequest,不重复实现 GetTargetQualityLevel
  • 可选 max_width 参数:缺省/0 沿用缩略图档位(最大 10244K 源屏高档 1280max_width>0 覆盖宽度并钳制到客户端上限 [64,1920](客户端 CaptureAndEncodePreview 硬钳制),供 AI 视觉/OCR 场景请求接近原分辨率1080p 源屏传 1920 即原分辨率4K 源屏最多 1920jpegQuality 仍沿用档位自适应值。
  • 请求关联ScreenPreviewReq.reqId 提供关联MCP 用独立 16 位 reqId 发生器(NextPreviewReqId,跳过 0+ SetPendingReqId 登记期望值,TakePreviewResponse 只接受「挂起命中 + reqId 一致」的响应,过期/他途MFC 预览并发)响应原样回落到 MFC 路径。
  • 客户端能力位 CLIENT_CAP_SCREEN_PREVIEW(0x0004仅 Windows 客户端声明):非 Windows 主机返回 -32005「不支持」。
  • 返回结构:content[0] = {type:"image", data: base64(JPEG), mimeType:"image/jpeg"} + structuredContent.image = {mimeType,width,height,bytes}(元数据,不含 base64避免二次膨胀

list_files(文件链路,一次性子链接,模式 A

  • 复用经典文件链路 COMMAND_LIST_DRIVETOKEN_DRIVE_LISTCOMMAND_LIST_FILESTOKEN_FILE_LIST明确范围:本阶段只做经典链路;项目另有 COMMAND_GET_FOLDER(66)、插件版 TOKEN_DRIVE_LIST_PLUGIN(150)、V2 文件传输,均列为非目标。
  • 两种调用形态
    • path 缺省/空(或 .///\)→ 只列盘:COMMAND_LIST_DRIVE 开子链接,TOKEN_DRIVE_LIST 到达即取走盘列表并关链(与进程/窗口同构的 A
    • path 指定目录 → 列目录:TOKEN_DRIVE_LIST 到达时 OnDriveList 就地同子链接下发 COMMAND_LIST_FILES+pathBuildListFiles 已按客户端 ANSI 转好,OnDriveList 不再转),等 TOKEN_FILE_LIST 取走并关链。
  • 编码:盘符类型名/文件系统、文件名、path 均按 clientType 判定(与进程名/路径一致Windows 走 A 接口(FindFirstFileA/GetLogicalDriveStringsA/SHGetFileInfoA 等)= 客户端 ANSI(GBK/936)LNX/MAC 文件系统天然 UTF-8。不能用 GetClientEncoding——它按能力位返回 CP_UTF8会误解声明 CLIENT_CAP_UTF8 的 Windows 客户端(其文件名实为 GBK。解析用 ToUtf8(buf, cp)、下发用 ToAnsi(path, cp)cp = (clientType=="LNX"||"MAC") ? CP_UTF8 : 936
  • 大列表保护ParseFileList(data, 500, cp) 条目上限 500避免 JSON 响应过大。
  • 返回结构:列盘 → structuredContent.drives[];列目录 → structuredContent.files[]{name,isDir,size,mtime}mtime 为 Unix 秒)。

改动

  • McpServer.h/.cpp
    • 挂起注册表扩展:PendingRequestpathexpectedReqId 字段;BeginPending(devId, tool, path) 重载,新增 NextPreviewReqId/SetPendingReqId/TakePreviewResponse/OnDriveList 四方法。
    • 解析器:ParseDriveList[letter][type][totalMB:4][freeMB:4][typeName\0][fileSystem\0]letter=='\0' 终止)、ParseFileList[attr][filename\0][sizeHigh:4][sizeLow:4][ft:8],空记录/上限 500 终止)。
    • 工具:get_screenshot(模式 Alist_files(模式 A注册 schemaget_screenshot 无 inputSchema、list_files 带可选 path)。
  • 2015RemoteDlg.cppMessageHandle(三处拦截):
    • TOKEN_SCREEN_PREVIEW_RSP(主 contextTakePreviewResponse(devId, reqId, szBuffer, len) 命中则 break不 CancelIO,主连接不可关)。
    • TOKEN_DRIVE_LIST(子 contextif (IsPending) { OnDriveList(...) ? CancelIO() : /*继续等*/ break; }(列盘关链、列目录不关)。
    • TOKEN_FILE_LIST(子 context新增 caseif (IsPending) TakeMainResponse(...); CancelIO();(无论命中都关孤儿子链接,防泄漏)。

影响面MessageHandle 仅新增 if 分支与一个 TOKEN_FILE_LIST case非 MCP 触发时 IsPending 为假、TOKEN_FILE_LIST case 在 MFC 路径下因 hDlg 接管根本不进 MessageHandle,现有 MFC 文件管理/屏幕预览弹框路径不变。

验收 / 实机验证点(待构建后执行)

  • MFC 双击文件管理弹框仍正常回归点MFC 双击预览缩略图仍正常(回归点)。
  • MCP get_screenshot 返回 JPEGbase64 + 元数据Windows 主机正常;非 Windows 主机(或未声明 CLIENT_CAP_SCREEN_PREVIEW)返回 -32005
  • MCP list_files(无 path)返回 drives[]list_files(path="C:\\") 返回 files[](含目录/文件/大小/mtime中文文件名无乱码条目 >500 时截断为 500。
  • 错误路径:缺 id/非数字 id-32602;未知/离线主机 → -32002;同 host 并发 → -32003;发送失败 → -32004;超时 → -32001

回滚revert McpServer.h + McpServer.cpp + MessageHandle 三处拦截。

6.4 P3aexec_command(模式 B写操作评审后实施

改动:复刻 Web 终端链路§4.3),把 m_TermPending 换成 MCP 自己的挂起标记:

  • COMMAND_SHELLTOKEN_SHELL_START/TOKEN_SHELL_DATA 接管 → 发送命令文本 → 收集 stdout → 关子链接 → 返回。

完成判据(本工具最难的半截,必须定死):交互 shell 没有「命令结束」信号,采用哨兵方案

实际下发: cmd ; echo __MCP_DONE_<nonce>__ ; echo __MCP_EXIT_$?__
收集端:   持续累积 TOKEN_SHELL_DATA直到同时匹配 __MCP_DONE_<nonce>__ 与 __MCP_EXIT_<code>__
          取出退出码,剥离哨兵行,返回 stdout

<nonce> 随机,避免命令自身输出误命中哨兵;整体受 McpToolTimeoutMs 兜底(超时强制关链 + 返回错误)。

前置安全评审(见 §8通过后才实施。

验收exec_command(id, "tasklist") 返回进程列表文本 + 退出码;白名单外命令被拒绝并审计;超时/离线返回明确错误MFC 终端弹框不受影响。


7. 「新增工具」标准配方(可复用流程)

后续任何人新增一个工具,按以下 6 步走:

  1. 定协议与模式:确认复用哪个 COMMAND_*TOKEN_* 链路,判定是模式 A主连接 RPC、A0单向命令还是 B子链接流式写进 §5.1 表格。
  2. 定 schema:在 BuildToolsListResult 注册 name/description/inputSchema/outputSchemadescription 用 u8"...",见 §10
  3. 加 dispatch:在 BuildToolsCall 里加一个 toolName == "..." 分支。
  4. 实现数据通路
    • 纯内存/现成字段 → 直接实现,不进 MessageHandle
    • 模式 A0单向命令→ 直接 Send2Client 后返回,无需挂起/超时。
    • 模式 A → 加 m_Pending 挂起 + MessageHandle 对应 TOKEN_*if (IsPending) TakeMainResponseIO 线程同步拷贝,不 CancelIO)。
    • 模式 A → 同模式 A但拦截后额外 ContextObject->CancelIO() 关一次性子链接。
    • 模式 B → 加 m_Pending 挂起 + MessageHandle 对应 TOKEN_*if (IsPending) TakeSubContext + OnShellData 泵数据。
  5. 超时与清理:带超时、迟到回收、用完即关(模式 B每 host 单飞行§4.5)。
  6. 验收与回滚:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。

8. 安全与权限模型演进

阶段 鉴权 授权
P1/P2 单一静态 Bearer token 全部只读token 即授权
P3 引入 token 分级 + 分组授权 只读 token / 完整 token 两档;写操作要求完整 token 且限定可访问分组
P4 (如需)会话级授权 会话内临时授权

P3 进入前必须评审的三条边界:

  1. 命令白名单exec_command 默认只放行只读命令(systeminfo/tasklist/ipconfig/netstat 等);危险命令(del/format/下载执行/reg add)需显式放行或直接拒绝。
  2. 审计日志:每次写操作记录 host_id + tool + 参数 + token 来源env/CFG+ 时间,写 Mprintf + 日志文件。
  3. 分组授权(新增)写工具必须限定「token 能操作哪些主机/分组」,复用 Web 远程桌面已有的 WebUser.allowed_groups + admin/viewer 角色模型WebService.cpp:1852-1877),而非仅区分读/写。否则一个完整 token 就能对所有主机执行命令,越过了 Web 侧已有的分组隔离。

设计底线MCP 的写能力与 Web 远程桌面同等级敏感,任何写工具都要按「可控、可审、可关、可分组隔离」交付,不允许「开了 MCP 就能无脑执行任意命令」。


9. 配置与开关扩展

延续 Phase 1 的配置风格(settings 段 + env 覆盖 + UIBranding 开关):

配置存储位置(重要)THIS_CFGRelease 版恒走注册表 HKCU\Software\YAMA\settings2015Remote.cpp:238#else 分支 new iniFilesettings.ini 仅在 Debug 版#ifdef _DEBUGGetPwdHash()==masterHash)才读,否则同样走注册表。故 Release 版改 MCP 配置应走「扩展 → MCP设置」对话框写注册表或直接改注册表手改 settings.ini 对 Release 无效。

配置键 默认 说明
McpEnabled 0 总开关(已有)
McpPort 6544 端口(已有)
McpBind 127.0.0.1 绑定(已有)
McpToken 静态 token已有
McpReadonly 1 P3 新增1=仅只读工具0=开放写工具(配合完整 token
McpCmdWhitelist 内置只读命令集 P3 新增exec_command 白名单(分号分隔)
McpToolTimeoutMs 20000 P2b 新增:数据等待超时(当前为 McpServer.cpp 内编译期常量 kMcpToolTimeoutMs,未接配置)

新增写能力时,UIBranding.h/FeatureFlags.h 加对应隐藏/特性位,保持「品牌定制可裁剪」的既有约定。


10. 编码 / 兼容性规范

  • UTF-8 输出:所有进入 JSON 的字符串用 u8"..." 前缀(项目 /execution-charset:.936 会把普通窄字面量编成 GBK。已在 list_online_hostsdescription 中验证,服务端输出正确 UTF-8。
  • JSON 转义jsoncpp 会把非 ASCII 输出为 \uXXXX,这是合法 JSON客户端解析即还原无需处理。
  • 客户端能力位:依赖客户端能力的工具(如 get_screenshotCLIENT_CAP_SCREEN_PREVIEW、UTF-8 字段的 CLIENT_CAP_UTF8)必须先查能力位,不支持则返回明确错误而非空结果。
  • id 一致性:工具入参 id 必须与 list_online_hosts 返回的 id 同源(context::GetClientID() 的 uint64可直接回填 FindHost(uint64_t);仅接受在线主机的 id离线返回明确错误。
  • 版本兼容:子链接命令/结构体(common/commands.h禁止改动既有字段MCP 只消费既有协议,不新增命令(原则 2
  • MBCS 构建:新增 UI 文案走 _TR() + lang iniGBK与 Phase 1 一致。

11. 验收标准Definition of Done

每个工具交付时,以下全部满足才算完成:

  • tools/list 返回该工具及完整 schema。
  • 正常输入返回正确结构化结果。
  • 非法输入(错 id / 缺参数 / 设备离线 / 不支持能力)返回明确的 JSON-RPC 错误码,不崩溃、不挂死。
  • 数据获取有超时、迟到回收;模式 B 用完即关(无会话泄漏)。
  • 每 host 单飞行:同 host 并发请求被拒绝而非串扰。
  • 回归点:对应 MFC 弹框/Web 路径行为不变(非 MCP 触发时不受影响)。
  • 编码正确UTF-8中文无乱码
  • 写操作工具附带白名单/审计/分组授权(仅 P3
  • 无新增第三方库。

12. 风险与回滚

风险 缓解
无头接管串扰 MFC/Web 路径 挂起标记按 device_id 唯一;if (IsPending) 仅在 MCP 触发时成立,其余路径原样
共享 InDeCompressedBuffer 被覆盖 模式 A 在 IO 线程 MessageHandle 内同步拷贝,不等异步
无请求 id 的工具响应串扰 每 host 单飞行§4.4MCP 与 MFC 各用独立子链接互不占位§4.2
阻塞 httplib worker 饿死后续请求 低频串行可接受;文档显式声明,未来并发改「任务+轮询」§4.5
超时/挂死 所有工具强制 wait_for(timeout) + 迟到回收
写操作越权 P3 前三道评审边界(白名单/审计/分组授权)+ McpReadonly 默认 1
一次性大改影响现有功能 分 P2a→P2b→P2c→P3a 四期,每期独立合入/验收/回滚
输出过大拖垮 JSON list_files/进程列表等大结果设上限/分页

回滚策略:每期改动集中在一两个文件(McpServer.* + 可选 MessageHandle 若干 if 分支),出错 revert 当期 commit 即可,不影响已交付的 P1。


附:建议执行顺序

  1. P2asearch_hosts + get_host_detail ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
  2. P2blist_processes + list_windows + get_activity_history ——首次引入模式 A 一次性子链接(进程/窗口)与模式 A 主连接 RPC(历史活动)无头接管,把 §4.2/§4.2 的机制跑通并沉淀成 §7 配方。
  3. P2cget_screenshot 走屏幕预览链路 + list_files ——补上「看」的能力,仍是只读(get_screenshot 模式 A、list_files 模式 A
  4. P3aexec_command)——首次引入模式 B 子链接流式,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。

每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。