Files
SimpleRemoter/docs/Mcp_Phase2_Design.md
yuanyuanxiang 61089e5fae Feature: Add list_files and get_screenshot MCP tools
Add the two P2c MCP tools on top of the P2b protocol. list_files lists
drives (COMMAND_LIST_DRIVE -> TOKEN_DRIVE_LIST) or a directory (follow-up
COMMAND_LIST_FILES -> TOKEN_FILE_LIST on the same one-shot sub-link);
get_screenshot reuses the screen-preview RPC with a per-host reqId
correlation so stale or MFC-preview responses fall through to the MFC
path untouched. Both share the P2b single-flight pending registry.

Fix file/process name encoding: the client reads process names, paths and
file names via the ANSI (A) APIs, so they are GBK on Windows regardless of
the CLIENT_CAP_UTF8 capability bit (which governs window titles only).
Decode by clientType (LNX/MAC = UTF-8, Windows = 936) rather than
GetClientEncoding, and convert the list_files path to the client ANSI code
page before sending. Sync Mcp_Phase2_Design.md with the P2c design and the
encoding correction.

Co-Authored-By: deepseek-v4-pro
2026-08-19 13:27:32 +02:00

438 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# YAMA MCP 功能开发技术书Phase 2 及后续)
> **状态**设计定稿已按两轮评审修订P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`P2c 已实现、待实机验证(`get_screenshot` / `list_files`)。
> **读者**MCP 后续功能的研发/评审人员。
> **关联文档**[Mcp_Design.md](./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 助手以工具调用方式使用。
本技术书解决三个问题:
1. **做什么**——工具路线图与优先级(只读 → 安全写 → 会话)。
2. **怎么做**——复用现有协议,无头驱动、不弹对话框;**区分「主连接 RPC」与「子链接流式」两种模式**。
3. **怎么稳**——每个阶段独立可交付、可回滚,影响面小、可验收。
---
## 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. 设计原则(贯穿所有后续开发)
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`/`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 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_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 对话框互斥」。
- **编码**:进程名/路径按 clientType 判定Windows=GBK/936、LNX/MAC=UTF-8进程枚举走 A 接口不随 `CLIENT_CAP_UTF8` 转 UTF-8窗口标题为客户端 UTF-8按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP936
- **请求关联**:无请求 id → 每 host 单飞行§4.4)。
- **文件列举也走 A`list_files`**:列盘与进程/窗口同构(`COMMAND_LIST_DRIVE`→一次性子链接→`TOKEN_DRIVE_LIST` 即关);列目录则在此一次性子链接上**多一轮** `COMMAND_LIST_FILES``TOKEN_FILE_LIST` 后再关。故 `list_files` 归入模式 A而非原设计的模式 A详见 §6.3)。
> **模式 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` 字段。
```cpp
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 线程,阻塞一个 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_REQ``TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
| P2c已实现 | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE``TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES``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/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按 clientType 编码转 UTF-8`ParseWindowList`hwnd/title/status/pid标题按能力位解码两者都用 `BoundedStrlen` 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 `LocalSize` 对齐引入的尾部零字节。
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
- `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`
- 中文进程名/窗口标题无乱码(进程名按 clientType 解码、窗口标题按能力位 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 / A✅ 已实现(待实机验证)
> 本节按实际实现修订:`get_screenshot` 是**主连接 RPC**(模式 A有 reqId 关联);`list_files` 是**一次性子链接**(模式 A——列盘 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 即关,列目录则在 `TOKEN_DRIVE_LIST` 到达时就地同子链接再发 `COMMAND_LIST_FILES`→`TOKEN_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`,单帧 JPEG`ScreenPreviewReq`/`ScreenPreviewRspHeader``ctx->Send2Client` 发请求、主连接回响应,**不建子链接、不弹框**。
- 复用 MFC 预览的 RTT/FRP 自适应参数挑选 `ChooseScreenPreviewParams` + `SendScreenPreviewRequest`,不重复实现 `GetTargetQualityLevel`
- **请求关联**`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_DRIVE``TOKEN_DRIVE_LIST``COMMAND_LIST_FILES``TOKEN_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`+`path``BuildListFiles` 已按客户端 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`
- 挂起注册表扩展:`PendingRequest``path``expectedReqId` 字段;`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`(模式 A`list_files`(模式 A注册 schema`get_screenshot` 无 inputSchema、`list_files` 带可选 `path`)。
- `2015RemoteDlg.cpp``MessageHandle`(三处拦截):
- `TOKEN_SCREEN_PREVIEW_RSP`(主 context`TakePreviewResponse(devId, reqId, szBuffer, len)` 命中则 break**不 CancelIO**,主连接不可关)。
- `TOKEN_DRIVE_LIST`(子 context`if (IsPending) { OnDriveList(...) ? CancelIO() : /*继续等*/ break; }`(列盘关链、列目录不关)。
- `TOKEN_FILE_LIST`(子 context**新增 case**`if (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 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 步走:
1. **定协议与模式**:确认复用哪个 `COMMAND_*``TOKEN_*` 链路,判定是模式 A主连接 RPC、A0单向命令还是 B子链接流式写进 §5.1 表格。
2. **定 schema**:在 `BuildToolsListResult` 注册 `name`/`description`/`inputSchema`/`outputSchema`description 用 `u8"..."`,见 §10
3. **加 dispatch**:在 `BuildToolsCall` 里加一个 `toolName == "..."` 分支。
4. **实现数据通路**
- 纯内存/现成字段 → 直接实现,不进 `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` 泵数据。
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_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 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. **P2a**`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
2. **P2b**`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2 的机制跑通并沉淀成 §7 配方。
3. **P2c**`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A
4. **P3a**`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。