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
438 lines
37 KiB
Markdown
438 lines
37 KiB
Markdown
# 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 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 对话框互斥」。
|
||
- **编码**:进程名/路径按 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 线程,阻塞一个 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`→`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` 返回 JPEG(base64 + 元数据),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 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。
|
||
|
||
---
|
||
|
||
## 附:建议执行顺序
|
||
|
||
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 的挂起机制成熟后,走完安全评审再落地写操作。
|
||
|
||
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|