Add three read-only P2b MCP tools over the existing protocol. list_processes and list_windows trigger the client via COMMAND_SYSTEM / COMMAND_WSLIST on the main connection and receive TOKEN_PSLIST / TOKEN_WSLIST on a one-shot sub-link; get_activity_history uses the main-connection RPC COMMAND_QUERY_ACTIVITY -> TOKEN_REPORT_ACTIVITY. A per-host single-flight pending registry (m_Pending) with a 20s timeout correlates each response to its request and rejects a concurrent request for the same host with -32003. Parsers stop on the first empty record to ignore the client's LocalSize trailing zero padding, and window titles are decoded per the client UTF-8 capability bit. MessageHandle only adds if-guarded branches, so the MFC dialogs are untouched. Sync Mcp_Phase2_Design.md with the mode A'/A architecture, the verification notes, and the registry-backed config location. Co-Authored-By: deepseek-v4-pro
32 KiB
YAMA MCP 功能开发技术书(Phase 2 及后续)
状态:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(
search_hosts/get_host_detail/list_processes/list_windows/get_activity_history)。 读者:MCP 后续功能的研发/评审人员。 关联文档:Mcp_Design.md(Phase 1 的协议、架构、配置与菜单设计)。 核心目标:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,不一次性大改现有功能。
1. 背景与目标
Phase 1 已跑通 MCP 最小闭环:httplib + JSON-RPC 2.0,单工具 list_online_hosts,静态 Bearer token,经「扩展 → MCP设置」对话框开启。目标是把 YAMA 服务端已有的主机管理能力(进程、窗口、终端、截图、文件等)逐步暴露为 MCP 工具,供 Claude Code 等 AI 助手以工具调用方式使用。
本技术书解决三个问题:
- 做什么——工具路线图与优先级(只读 → 安全写 → 会话)。
- 怎么做——复用现有协议,无头驱动、不弹对话框;区分「主连接 RPC」与「子链接流式」两种模式。
- 怎么稳——每个阶段独立可交付、可回滚,影响面小、可验收。
2. 现状回顾(Phase 1 已交付)
| 模块 | 文件 | 职责 |
|---|---|---|
| HTTP/JSON-RPC 分发 | McpServer.h / McpServer.cpp |
POST /mcp;token 校验;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 未引入任何新第三方库——httplib(HTTP)与 jsoncpp(JSON)均为项目既有依赖。
当前工具实现只有一个 dispatch 分支(McpServer.cpp 的 BuildToolsCall),新增工具的边际成本很低:注册 outputSchema + 加一个 tools/call 分支。
3. 设计原则(贯穿所有后续开发)
- 只读优先,写操作单独评审:延续
Mcp_Design.md §1.3。只读工具直接做;任何「改变被控端状态」的工具(命令执行、杀进程、传文件、控制)必须经安全评审 + 独立授权。 - 复用现有协议,不新造命令:一切数据获取/操控都走
common/commands.h里既有的COMMAND_*(服务端→客户端)与TOKEN_*(客户端→服务端),不新增自定义命令。 - 无头接管复用 Web 终端模式:对话框只是 UI 外壳;MCP 复用
WebService的挂起标记/接管模式,把数据接到 MCP 侧,不弹框。 - 每阶段独立可交付、可回滚:一个阶段 = 一批工具 + 对应验收;出错可单独 revert,不连坐。
- 超时与清理是硬约束:MCP 是 request/response,一次
tools/call不能无限阻塞等数据;必须有超时、迟到回收、用完即关。 - 输出 schema 先行:每个工具先在
tools/list里声明完整inputSchema/outputSchema,AI 客户端据此决定何时调用、如何解析。
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_SYSTEM → new IOCPClient 子链接 + LoopProcessManager;SystemManager.cpp:75 GetProcessList() 里 szBuffer[0]=TOKEN_PSLIST,经子链接 Send2Server 回传 |
进程/窗口列表 = 一次性子链接(主连接下 COMMAND_SYSTEM/COMMAND_WSLIST,子连接回 TOKEN_PSLIST/TOKEN_WSLIST) |
2015RemoteDlg.cpp:8527-8536:SendScreenPreviewRequest 用 ctx->Send2Client(...);响应 TOKEN_SCREEN_PREVIEW_RSP 在 MessageHandle:6173 |
屏幕预览 = 主连接 RPC |
WebService.cpp:1836-1900 + 2015RemoteDlg.cpp:6312-6351:COMMAND_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/len(2015RemoteDlg.cpp:6173-6185已在 case 内把szBuffer拷贝到堆消息再转主线程);历史活动的TOKEN_REPORT_ACTIVITY读GetBuffer(0)(=InDeCompressedBuffer)。两者都是每 context 单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在MessageHandle内部(IO 线程),不能异步等 UI 线程。 - 请求关联:
get_screenshot有reqId(ScreenPreviewReq.reqId)可丢弃过期响应;get_activity_history无请求 id,靠「每 host 单飞行」(见 §4.4)保证正确性。
4.2′ 模式 A′:一次性子链接(进程 / 窗口列表)
适用于:list_processes、list_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 IOCPClient,EnableSubConnAuth)
└─► 子链接 ConnectServer 后先发 TOKEN_CONN_AUTH(服务端 SetID 把 clientID 钉在子 context 上)
└─► CSystemManager 构造函数立即 Send2Server(TOKEN_PSLIST / TOKEN_WSLIST)
└─► MessageHandle 在【子 context】上收到 TOKEN_PSLIST / TOKEN_WSLIST(GetClientID()==主 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_AUTH(2015RemoteDlg.cpp:6291的SetID)钉成主连接 clientID,故IsPending(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 对话框互斥」。 - 编码:进程名/路径为客户端 ANSI(CP_ACP);窗口标题为客户端 UTF-8(按能力位
CLIENT_CAP_UTF8判别,老客户端回落 CP_ACP)。 - 请求关联:无请求 id → 每 host 单飞行(§4.4)。
模式 A0(主连接单向命令,无响应):
kill_process(COMMAND_KILLPROCESS)、send_message(COMMAND_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(...) 泵数据
完全复刻 WebService 的 IsTermPending/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 worker:
wait_for会阻塞HandleMcp所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 20–30s 会占住一个 worker。MCP 属低频串行调用(AI 逐条调用),本阶段可接受;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。
5. 工具路线图(分阶段)
5.1 工具总表
「模式」标注实现路径(A=主连接 RPC,A′=一次性子链接,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_REQ→TOKEN_SCREEN_PREVIEW_RSP |
A | 只读 |
| P2c | list_files |
目录列举 | COMMAND_LIST_DRIVE/COMMAND_LIST_FILES→TOKEN_DRIVE_LIST/TOKEN_FILE_LIST |
A | 只读 |
| P3 | exec_command |
执行命令、返回 stdout | COMMAND_SHELL→TOKEN_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 无头接管,但只读、可回滚。
5.3 Phase 3:安全写操作(需单独评审)
进入 Mcp_Design.md §1.3 明确划出的「写操作」区。每个工具交付前需评审三点(见 §8):命令白名单、审计日志、分组授权 + token 分级。
5.4 Phase 4:会话/流式(暂缓)
实时远程桌面、交互终端等需要「持续流」的能力,与当前 request/response 模型不匹配。需先决定传输方案(SSE / 提交任务 + 轮询),本期不做,避免过早引入复杂会话状态。
6. 分阶段实施计划
每期都给出:改动文件、接口、测试点、回滚方式。原则是每期可独立合入、独立验收、独立回滚。
6.1 P2a:search_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 P2b:list_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 单飞行检查。 - 加解析器
ParseProcessList(PID/name/arch/path,CP_ACP 转 UTF-8)、ParseWindowList(hwnd/title/status/pid,标题按能力位解码);两者都用BoundedStrlen有界读 + 「空记录 = 尾部零填充」终止,规避客户端LocalSize对齐引入的尾部零字节。 - 加三个工具:
list_processes(发COMMAND_SYSTEM)、list_windows(发COMMAND_WSLIST)、get_activity_history(发COMMAND_QUERY_ACTIVITY)。
- 加 §4.4 的
2015RemoteDlg.cpp的MessageHandle:TOKEN_PSLIST/TOKEN_WSLIST分支(子 context):if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }(在 UI 线程、IO 线程阻塞期间同步拷贝;用完即关子链接)。TOKEN_REPORT_ACTIVITY分支(主 context):if (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。 - 中文进程名/窗口标题无乱码(进程名 CP_ACP、窗口标题按能力位 UTF-8)。
验证记录(2026-08-19,真实在线主机实测):
list_processes(Windows 11 主机):pid/name/arch/path完整;pid==0仅 1 条(合法[System Process]),无尾部零填充空记录——padding 防护生效。list_windows:hwnd/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 P2c:get_screenshot + list_files(模式 A)
get_screenshot(屏幕预览链路,主连接 RPC):
- 复用现有双击预览机制
COMMAND_SCREEN_PREVIEW_REQ(247)→TOKEN_SCREEN_PREVIEW_RSP(248)(common/commands.h:416-455,单帧 JPEG,ScreenPreviewReq/ScreenPreviewRspHeader),ctx->Send2Client发请求、主连接回响应,不建子链接、不弹框。 ScreenPreviewReq.reqId提供请求关联,可丢弃过期响应。- 客户端能力位
CLIENT_CAP_SCREEN_PREVIEW(0x0004,仅 Windows 客户端声明):非 Windows 主机返回「不支持」错误。 - 若后续需要更高清/控制,再升级为完整屏幕子链接(模式 B,需
RegisterScreenContext接管)。
list_files:复用经典文件链路 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST 与 COMMAND_LIST_FILES→TOKEN_FILE_LIST(模式 A)。明确范围:本阶段只做经典链路;项目另有 COMMAND_GET_FOLDER(66)、插件版 TOKEN_DRIVE_LIST_PLUGIN(150)、V2 文件传输,均列为非目标。文件列表可能较大,需分页/上限(首期限 path 一层、条目上限 500),避免 JSON 响应过大。
验收:get_screenshot 返回 JPEG(base64 或经 MCP 图片内容类型);Windows 主机正常、非 Windows 返回明确「不支持」;list_files 正确列目录且有大列表保护。
6.4 P3a:exec_command(模式 B,写操作,评审后实施)
改动:复刻 Web 终端链路(§4.3),把 m_TermPending 换成 MCP 自己的挂起标记:
- 下
COMMAND_SHELL→TOKEN_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 步走:
- 定协议与模式:确认复用哪个
COMMAND_*→TOKEN_*链路,判定是模式 A(主连接 RPC)、A0(单向命令)还是 B(子链接流式),写进 §5.1 表格。 - 定 schema:在
BuildToolsListResult注册name/description/inputSchema/outputSchema(description 用u8"...",见 §10)。 - 加 dispatch:在
BuildToolsCall里加一个toolName == "..."分支。 - 实现数据通路:
- 纯内存/现成字段 → 直接实现,不进
MessageHandle。 - 模式 A0(单向命令)→ 直接
Send2Client后返回,无需挂起/超时。 - 模式 A → 加
m_Pending挂起 +MessageHandle对应TOKEN_*加if (IsPending) TakeMainResponse(IO 线程同步拷贝,不 CancelIO)。 - 模式 A′ → 同模式 A,但拦截后额外
ContextObject->CancelIO()关一次性子链接。 - 模式 B → 加
m_Pending挂起 +MessageHandle对应TOKEN_*加if (IsPending) TakeSubContext+OnShellData泵数据。
- 纯内存/现成字段 → 直接实现,不进
- 超时与清理:带超时、迟到回收、用完即关(模式 B);每 host 单飞行(§4.5)。
- 验收与回滚:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。
8. 安全与权限模型演进
| 阶段 | 鉴权 | 授权 |
|---|---|---|
| P1/P2 | 单一静态 Bearer token | 全部只读,token 即授权 |
| P3 | 引入 token 分级 + 分组授权 | 只读 token / 完整 token 两档;写操作要求完整 token 且限定可访问分组 |
| P4 | (如需)会话级授权 | 会话内临时授权 |
P3 进入前必须评审的三条边界:
- 命令白名单:
exec_command默认只放行只读命令(systeminfo/tasklist/ipconfig/netstat等);危险命令(del/format/下载执行/reg add)需显式放行或直接拒绝。 - 审计日志:每次写操作记录
host_id + tool + 参数 + token 来源(env/CFG)+ 时间,写Mprintf+ 日志文件。 - 分组授权(新增):写工具必须限定「token 能操作哪些主机/分组」,复用 Web 远程桌面已有的
WebUser.allowed_groups+ admin/viewer 角色模型(WebService.cpp:1852-1877),而非仅区分读/写。否则一个完整 token 就能对所有主机执行命令,越过了 Web 侧已有的分组隔离。
设计底线:MCP 的写能力与 Web 远程桌面同等级敏感,任何写工具都要按「可控、可审、可关、可分组隔离」交付,不允许「开了 MCP 就能无脑执行任意命令」。
9. 配置与开关扩展
延续 Phase 1 的配置风格(settings 段 + env 覆盖 + UIBranding 开关):
配置存储位置(重要):
THIS_CFG在 Release 版恒走注册表HKCU\Software\YAMA\settings(2015Remote.cpp:238的#else分支new iniFile);settings.ini仅在 Debug 版(#ifdef _DEBUG且GetPwdHash()==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_hosts的description中验证,服务端输出正确 UTF-8。 - JSON 转义:jsoncpp 会把非 ASCII 输出为
\uXXXX,这是合法 JSON,客户端解析即还原,无需处理。 - 客户端能力位:依赖客户端能力的工具(如
get_screenshot的CLIENT_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 ini(GBK),与 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.4);MCP 与 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。
附:建议执行顺序
- P2a(
search_hosts+get_host_detail)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。 - P2b(
list_processes+list_windows+get_activity_history)✅ ——首次引入模式 A′ 一次性子链接(进程/窗口)与模式 A 主连接 RPC(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。 - P2c(
get_screenshot走屏幕预览链路 +list_files)——补上「看」的能力,仍是只读、模式 A。 - P3a(
exec_command)——首次引入模式 B 子链接流式,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。