download_file pulls a remote file or directory back to the controller over the existing V2 file-transfer protocol. It reuses COMMAND_LIST_DRIVE to open the file-manager sub-link, then sends CMD_DOWN_FILES_V2 so the Windows client streams COMMAND_SEND_FILE_V2 chunks and a per-file COMMAND_FILE_COMPLETE_V2 SHA-256 checksum over its own authenticated sub-connection. The server routes those two packet types into a new per-device FileTransferSession whenever a download is pending, so the headless path never opens the GUI progress dialog; the C2C and existing GUI branches are left untouched. Files are written under a normalized local_dir, and every chunk filename is resolved and verified to stay inside local_dir (directories included) to block .. traversal; overwrite=false skips existing files and counts them as skipped. One transfer per host (mutually exclusive with the one-shot pending registry), a dedicated McpFileTransfer=0-by-default gate surfaced in the settings dialog, and audit logging complete the change. upload_file remains future work. Co-Authored-By: deepseek-v4-pro
21 KiB
YAMA MCP 文件传输功能设计(download_file / upload_file)
状态:设计定稿(经专家审查修订,结论见 §13)。
download_file采用 V2 文件传输协议(CMD_DOWN_FILES_V2+ 流式子连接 + SHA-256 校验),upload_file复用服务端既有FileBatchTransferWorkerV2。两者共享同一套「流式文件会话」注册表。 读者:MCP 后续功能研发/评审人员。 关联文档:Mcp_Phase2_Design.md(双模式无头驱动机制)、FILE_TRANSFER_V2.md(V2 协议清单)、Mcp_Design.md(Phase 1 协议/架构/配置)。
1. 背景与目标
MCP 目前已暴露 list_files(只读列目录,McpServer.cpp:1741),但没有文件传输工具:把远程主机的文件/目录拉回主控本机(下载),或把本机文件推到远程(上传),只能通过 GUI 文件管理器手工操作。
本技术书解决:
download_file(本期 P1)——远程文件/目录 → 主控本机指定目录,复用 V2 推送协议,带 SHA-256 完整性校验、支持大文件。upload_file(后续 P2)——主控本机文件/目录 → 远程指定目录,复用服务端 V2 发送器。- 一套共享的「流式文件会话」基础设施——一次投入,下载/上传两处受益,镜像现有终端(
TermSession)与远程控制(ScreenCtrlSession)会话模式。
2. 现状回顾
| 能力 | 现有协议/命令 | 服务端现有实现 | 是否暴露给 MCP |
|---|---|---|---|
| 远程列目录 | COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST;COMMAND_LIST_FILES→TOKEN_FILE_LIST |
McpServer.cpp:1741(list_files) |
✅ |
| 远程→本地下载(V1) | COMMAND_DOWN_FILES(commands.h:166) + TOKEN_FILE_SIZE/DATA/FINISH |
FileManagerDlg.cpp:2164 / file/CFileManagerDlg.cpp:1136 |
❌ 仅 GUI |
| 远程→本地下载(V2) | CMD_DOWN_FILES_V2(94, commands.h:260) + COMMAND_SEND_FILE_V2(85) + COMMAND_FILE_COMPLETE_V2(91) |
FileManagerDlg.cpp:3337-3384(下发)、2756(RecvFileChunkV2 落盘) |
❌ 仅 GUI |
| 本地→远程上传(V2) | FileBatchTransferWorkerV2(服务端作 sender) |
2015RemoteDlg.cpp:7572 |
❌ 仅 GUI |
2.1 关键事实
-
V2 下载的客户端会另开一条新子连接:
client/FileManager.cpp:1101-1190UploadToRemoteV2()解析完文件列表后,new IOCPClient(...)→EnableSubConnAuth()→ConnectServer(...)(FileManager.cpp:1170-1172),在独立鉴权子连接上推COMMAND_SEND_FILE_V2分块 +COMMAND_FILE_COMPLETE_V2校验包。即下载 ≠ 复用文件管理器那条子链接,而是一条持续流子链接——正属Mcp_Phase2_Design.md §4.3当初判定「本期不做」的流式能力。 -
接收端
RecvFileChunkV2无 UI 可调:CDlgFileSend.cpp:128已RecvFileChunkV2(buf, len, nullptr, nullptr, hash, hmac, 0)无头落盘;落盘状态在SimplePlugins/file_upload.cpp内部按transferID自维护,天然适合 MCP 无头复用。 -
V2 落盘路径由客户端回填:
CMD_DOWN_FILES_V2的targetDir是主控本机保存目录(FileManagerDlg.cpp:3370m_Local_Path);客户端把它拼进每个 chunk 的filename发回,服务端RecvFileChunkV2直接按该完整路径写盘。因此「下载保存到哪」由服务端在命令里指定、客户端原样回填。
3. 协议选型(已定:V2)
| 维度 | V2 推送(选定) | V1 拉取(回退) |
|---|---|---|
| 命令 | CMD_DOWN_FILES_V2 + COMMAND_SEND_FILE_V2 + COMMAND_FILE_COMPLETE_V2 |
COMMAND_DOWN_FILES + TOKEN_FILE_SIZE/DATA/FINISH |
| 连接 | 客户端新开一条鉴权流式子连接 | 复用文件管理器子链接 |
| 完整性 | ✅ SHA-256(COMMAND_FILE_COMPLETE_V2) |
❌ 无 |
| 大文件 | ✅ 64 位偏移,>4GB | 64 位偏移但无校验 |
| 客户端代码 | ✅ 已存在,是当前 GUI 正规路径 | ⚠️ 遗留路径 |
| MCP 侧复杂度 | 需路由一条新流式子连接(新增会话注册表) | 需在 MessageHandle 维护「发 CONTINUE/收 DATA」状态机,逐块 ACK 慢 |
结论:采用 V2。 代价是服务端要新增「流式文件会话」注册与路由;但这是 upload_file 同样要复用的基础设施,一次投入两处受益。V1 仅作「不想引入新子连接路由」时的降级备案,不在本期实现。
4. 设计原则
- 复用现有协议,不新造命令:
CMD_DOWN_FILES_V2、COMMAND_SEND_FILE_V2、COMMAND_FILE_COMPLETE_V2、COMMAND_LIST_DRIVE全部为common/commands.h既有;客户端零改动。 - 无头接管复用会话模式:镜像
McpServer.h的TermSession/ScreenCtrlSession,新增FileTransferSession;MessageHandle用IsFileTransferContext(context*)判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。 - 单设备单传输会话:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回
-32003 Device busy。 - 超时与清理是硬约束:大文件传输是长任务,超时需独立于
kMcpToolTimeoutMs(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。 - 输出 schema 先行:两个工具先在
tools/list声明完整inputSchema/outputSchema。
5. 核心机制:流式文件会话
5.1 会话注册表(McpServer.h/.cpp)
struct FileTransferSession {
std::string tool; // "download_file" / "upload_file"
std::string localDir; // download: 本机落盘目录(规范化后绝对路径,用于穿越校验)
std::string remotePath; // 审计用原始远程路径(UTF-8)
uint64_t transferID = 0; // 绑定后从首包填充(download);upload 由本端 GenerateTransferID()
uint32_t totalFiles = 0; // 完成判定:首个 COMMAND_SEND_FILE_V2 chunk 的 totalFiles(§13 F4)
uint32_t filesDone = 0; // 完成判定:累计收到的 COMMAND_FILE_COMPLETE_V2 数
bool done = false;
int error = 0; // FEV2_* / 自定义
std::vector<FileEntry> files; // {path, size} 已落盘/已发送文件
std::vector<FileEntry> skipped; // overwrite=false 时跳过的同名文件
time_t startAt = 0;
};
std::mutex m_FileXferMutex;
std::map<uint64_t, FileTransferSession> m_FileXferSessions; // device_id → 会话
取消
m_FileXferContextToDevice映射与IsFileTransferContext(context*):流式子连接经TOKEN_CONN_AUTH已把clientID钉在ContextObject上(2015RemoteDlg.cpp:6411 SetID),分派时直接用ContextObject->GetClientID()定位device_id,无需 context 路由表(§13 F1)。
配套接口(与 BeginTermPending / OnTerminalData / WaitTerminalDone 同构):
bool BeginFileTransferPending(uint64_t device_id, const std::string& tool,
const std::string& localDir, const std::string& remotePath);
bool IsFileTransferPending(uint64_t device_id); // 分支顶部守卫
void OnFileChunkV2(uint64_t device_id, const BYTE* buf, ULONG len); // 路径校验 + 无头 RecvFileChunkV2
void OnFileCompleteV2(uint64_t device_id, const BYTE* buf, ULONG len);// SHA-256 + filesDone 计数
bool WaitFileTransferDone(uint64_t device_id, int timeoutMs, std::vector<FileEntry>& out);
void ClearFileTransfer(uint64_t device_id); // 失败/超时/断线收尾(删半成品)
5.2 流式子连接的路由(device_id 键,镜像 TOKEN_DRIVE_LIST 守卫)
V2 下载的数据包 COMMAND_SEND_FILE_V2(85) / COMMAND_FILE_COMPLETE_V2(91) 本就由 MessageHandle 按 ContextObject->GetClientID() 分派(2015RemoteDlg.cpp:5842 / 6101)。MCP 只需在这两个 case 的 dstClientID==0(M2C)分支顶部加守卫:
case COMMAND_SEND_FILE_V2: {
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
if (pkt->dstClientID == 0) {
uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsFileTransferPending(devId)) { // ← 新增守卫:无头接管
McpServer().OnFileChunkV2(devId, szBuffer, len);
break; // 不建 CDlgFileSend、不设 hDlg
}
// ... 原 GUI 逻辑(建 CDlgFileSend + OnReceiveComplete)不变
}
// ... C2C 分支(dstClientID != 0)完全不动
}
TOKEN_CONN_AUTH(6378) 完全不动——它只负责把clientID钉在 ctx 上,下载复用既有行为。TOKEN_DRIVE_LIST(6430) 既有IsPending(devId)守卫扩展复用:OnDriveList识别当前挂起工具为download_file时,改发CMD_DOWN_FILES_V2而非COMMAND_LIST_FILES,发完即CancelIO文件管理器子链接(数据走新流式子连接,§13 F3)。- 守卫为假时全部回落原 GUI/C2C 路径——对既有功能零影响(原则 #1,§13 F7)。
6. download_file 设计(P1)
6.1 工具 Schema
input: {
id: string (必填, 主机 id)
remote_path: string (必填, 远程文件或目录绝对路径, 如 C:\Users\shaun\Pictures)
local_dir: string (必填, 主控本机保存目录; 不存在会自动创建)
overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 则跳过)
timeout_ms: integer (可选, 默认 600000, 上限 3600000)
}
output: {
files: [{ path: string, size: integer, sha256: string }] // 实际落盘文件
total_bytes: integer
skipped: integer
}
6.2 时序
MCP工具线程 服务端 MessageHandle 客户端
│ 1. BeginFileTransferPending(id,"download_file",localDir,remotePath) │
│ 2. 主连接发 COMMAND_LIST_DRIVE ────────────────────────────────────────► 开文件管理器子链接
│ 3. ◄── TOKEN_DRIVE_LIST ─────────────────────┤
│ OnDriveList → 锁外下发 CMD_DOWN_FILES_V2[targetDir\0][remote_path\0]\0 │
│ 4. ────────────────────────────────────────► UploadToRemoteV2()
│ (客户端另开一条鉴权流式子连接 ConnectServer)
│ 5. 新子连接 TOKEN_CONN_AUTH 钉 clientID → 后续分块按 device_id 守卫接管 │
│ 6. ◄── COMMAND_SEND_FILE_V2 分块流 ──────────┤
│ → OnFileChunkV2 → RecvFileChunkV2() 无头落盘
│ 7. ◄── COMMAND_FILE_COMPLETE_V2(SHA-256) ────┤
│ 8. 校验通过 → done → 唤醒工具线程 → CancelIO 两条子连接 → 返回 │
6.3 服务端改动
McpServer.h:新增FileTransferSession结构 + 注册表 + 接口声明(§5.1)。McpServer.cpp:BuildDownloadFileInputSchema/OutputSchema+BuildDownloadFile(...)(tools/call分派);OnFileChunkV2:先路径校验(chunkfilename规范化后仍在local_dir内,防..穿越,§13 F5)→ 调RecvFileChunkV2(buf,len,nullptr,nullptr,hash,hmac,0)无头落盘,记totalFiles/进度;OnFileCompleteV2:HandleFileCompleteV2校验 SHA-256 →filesDone++;filesDone==totalFiles时置done+notify(§13 F4);hash/hmac镜像FileManagerDlg.cpp:2755的GetPwdHash()/GetHMAC(100),兼容客户端 M2Chmac为空(client/FileManager.cpp:1167,§13 F6);ClearFileTransfer(发送失败/超时/断线):CancelIO子链接 + 删除半成品。
2015RemoteDlg.cpp(MessageHandle):COMMAND_SEND_FILE_V2(85)、COMMAND_FILE_COMPLETE_V2(91) 的dstClientID==0分支顶部加IsFileTransferPending(devId)守卫;TOKEN_DRIVE_LIST处扩展OnDriveList识别download_file改发CMD_DOWN_FILES_V2。TOKEN_CONN_AUTH与 C2C 分支完全不动。McpSettingsDlg.cpp/.h:新增McpFileTransfer配置项(默认 0)。
6.4 落盘与路径安全
- 发
CMD_DOWN_FILES_V2前把local_dir规范化为绝对路径并CreateDirectory;targetDir用 ANSI(MBCS 构建本机路径)。 - 每收到一个 chunk,校验其
filename规范化后前缀必须在local_dir内(防..穿越,呼应FILE_TRANSFER_V2.md §8.3已识别的风险);逃逸则置error并CancelIO。 - 编码:
remote_path走 UTF-8→ANSI(936)(Windows 客户端,复用McpServer.cpp既有ToAnsi,与list_files一致)。
6.5 超时与并发
- P1 范围:同步 + 单文件/已打包目录。一次
tools/call阻塞到底,不引入 job/task 异步模型(决策见 §12)。核心场景「先terminal_exec压缩成单 zip,再download_file拉回」天然是单文件,同步足够。 - 默认
timeout_ms=600000、上限 3600000,因大文件远超kMcpToolTimeoutMs(20s)。 - 单飞互斥(§13 F2):
BeginFileTransferPending与既有一次性m_Pending双向互斥——下载登记时占用m_Pending,反之BeginPending也检查m_FileXferSessions,保证同 host 任意时刻只一个 MCP 工具在飞;并发返回-32003 Device busy。 - 断线(
OfflineProc擦会话)+ 超时(删半成品)双收尾。
7. upload_file 设计(P2,未来)
- 复用服务端 sender:
2015RemoteDlg.cpp:7572已有FileBatchTransferWorkerV2(files, targetDir, ..., SendFileChunkToClientV2, ...),服务端读本机文件分块COMMAND_SEND_FILE_V2推给客户端;客户端RecvFileChunkV2落盘并回COMMAND_FILE_COMPLETE_V2。 - 比 download 更简单:服务端是发送方,工具线程直接驱动
FileBatchTransferWorkerV2,无需等待外来流;只需等客户端回COMMAND_FILE_COMPLETE_V2(复用同一文件会话注册表)。 - Schema(§3.2 已列):
id+local_path+remote_dir+overwrite+timeout_ms。 - 安全:写远程盘,独立评审;
remote_dir路径规范化,可选系统目录黑名单。
8. 安全门
| 工具 | 定性 | 门槛 |
|---|---|---|
download_file |
不改远程状态,但数据外带 | McpFileTransfer=1(仅此一开关;不依赖 McpReadonly,与 list_files/get_screenshot 同权) |
upload_file |
写远程盘 | McpFileTransfer=1 且 McpReadonly=0(复用既有「允许写」主开关) |
- 只新增一个开关
McpFileTransfer(默认 0):download 只看它;upload 额外要求McpReadonly=0。决策依据见 §12。 tools/list据此隐藏两个工具(与exec_command/terminal_*/remote_*同款开关判断)。- 审计:
host_id + tool + remote_path + local_dir + 结果,写Mprintf+ 审计日志。
9. 改动文件清单
| 文件 | 改动 |
|---|---|
server/2015Remote/McpServer.h |
FileTransferSession + 注册表 + 8 个接口声明 |
server/2015Remote/McpServer.cpp |
download_file schema/实现 + 会话状态机 + 路由分支 |
server/2015Remote/2015RemoteDlg.cpp |
MessageHandle:COMMAND_SEND_FILE_V2(85) / COMMAND_FILE_COMPLETE_V2(91) 两处守卫分支 + TOKEN_DRIVE_LIST 处 OnDriveList 扩展;TOKEN_CONN_AUTH 不动 |
server/2015Remote/McpSettingsDlg.cpp/.h |
McpFileTransfer 配置项 |
(P2)McpServer.cpp |
upload_file 实现(复用 FileBatchTransferWorkerV2) |
客户端零改动(UploadToRemoteV2 / RecvFileChunkV2 / FileBatchTransferWorkerV2 均已存在)。
10. 分阶段实施与回滚
- P1:
download_file+McpFileTransfer开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与certutil -hashfile比对一致)。 - P2:
upload_file。 - P3(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现
enableResume=false,client/FileManager.cpp:1164)、大文件进度流式上报。 - 回滚:改动集中在
McpServer.*+MessageHandle三个if分支,revert 当期 commit 即可,不影响既有 GUI 文件管理器。
11. 测试点
- 单文件 / 目录(含中文名、深层嵌套)下载;SHA-256 与
certutil -hashfile比对一致。 overwrite=false同名跳过;local_dir不存在自动创建。- 路径穿越:
remote_path含..或 chunk 文件名逃逸local_dir时被拒。 - 大文件(>2GB)超时与断线;并发下载同主机返回
-32003。 - 编码:GBK 中文文件名往返无乱码(与
list_files同规则)。 - 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。
12. 决策记录(以「简单易用」为原则)
原则:最少开关、最少认知负担、复用既有机制、不为低频场景预留复杂度;只有威胁模型确实需要时才加复杂度。
12.1 download 数据外带 → 不加目录白名单,信任 McpFileTransfer + 审计
- 威胁模型:MCP 默认
127.0.0.1+ token;「数据外带」的实际顾虑是 AI 误操作或提示注入。但一旦开启terminal_exec(全 shell、无白名单),外带通道远大于 download,目录白名单对已开终端者是冗余防御。 - 一致性:
terminal_*/remote_*均只有开关 + 审计、无路径白名单;给 download 单独加白名单是特例,增加解释负担。 - 简单:工具契约保持
id + remote_path + local_dir,不引入「允许根目录」概念。 - 逃生舱:若未来确有需要,复用
McpCmdWhitelist那种单字符串配置作可选收紧项,进 P3,不进 P1。
12.2 大文件模型 → P1 同步 + 大超时,收窄到单文件/已打包目录
- MCP 是 request/response;为一次性传输引入「提交任务 + 轮询进度 + 任务清理」违背「简单 + 每阶段独立可交付」。
- 核心场景(先压缩成单 zip 再拉回)天然是单文件,同步足够;
get_screenshot已先例式地内联返回字节。 - 异步任务模型进 P3,且只在确有超大流式需求时才做,不预建。
12.3 upload 开关 → 单一 McpFileTransfer,upload 复用 McpReadonly=0
download_file=McpFileTransfer=1(不依赖McpReadonly):它只读远程,与list_files/get_screenshot同权,readonly 语义本就只拦「改远程」。upload_file=McpFileTransfer=1 && McpReadonly=0:upload 改远程,复用既有「允许写」主开关,与terminal_*/remote_*的X && !Readonly模式一致。- 只新增 1 个开关;代价是「不能只开 upload 不开 download」——这是可接受的简化(无人有此诉求)。
13. 专家审查记录(实施前)
原则优先级:① 对既有功能影响最小 → ② 简单易用 → ③ 优先 V2。逐条核对了 2015RemoteDlg.cpp 的 MessageHandle 分派、client/FileManager.cpp 与 SimplePlugins/file_upload.cpp 的实现,结论如下:
| # | 审查发现 | 结论 |
|---|---|---|
| F1 | 路由机制比草案更简单:COMMAND_SEND_FILE_V2(5842)/COMMAND_FILE_COMPLETE_V2(6101) 本就按 ContextObject->GetClientID() 分派,流式子连接经 TOKEN_CONN_AUTH(6411 SetID) 钉住 clientID |
无需 context 路由表。改为 device_id 键 + 两个 case 顶部的 IsFileTransferPending(devId) 守卫,TOKEN_CONN_AUTH 完全不动 |
| F2 | 一次性 m_Pending 与新增文件会话可能并发冲突(同 host 双工具) |
BeginFileTransferPending 与 m_Pending 双向互斥,同 host 全 MCP 工具单飞 |
| F3 | 文件管理器子链接发完 CMD_DOWN_FILES_V2 后即无用(数据走新流式子连接) |
OnDriveList 发完命令即 CancelIO 该子链接;客户端 UploadToRemoteV2 用独立 IOCPClient,不依赖它 |
| F4 | 完成信号是每文件一个 COMMAND_FILE_COMPLETE_V2(FileCompletePacketV2.fileIndex) |
会话记 totalFiles(首 chunk)/filesDone(complete 计数),filesDone==totalFiles 才算 done |
| F5 | RecvFileChunkV2 内部直接按 chunk filename 落盘 |
MCP 在调用前校验 filename 规范化仍在 local_dir 内,逃逸即取消传输 |
| F6 | RecvFileChunkV2 需正确 hash/hmac |
镜像 FileManagerDlg.cpp:2755 的 GetPwdHash()/GetHMAC(100);客户端 M2C hmac 为空(FileManager.cpp:1167) |
| F7 | 三处改动均为「加 if 守卫 + 现有逻辑作 else」,C2C 分支不动 | 结构性满足原则 #1,对既有 GUI/C2C 零影响 |
判定:7 项问题均已在正文对应章节修正,无阻塞项,定稿。