#include "stdafx.h" #include "McpServer.h" #include "jsoncpp/json.h" #include "HostJson.h" // BuildHostJson(单台主机序列化公共函数) #include "context.h" // context 接口 #include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs/m_ClientMap)+ VERSION_STR #include "LangManager.h" // _TR(审计日志标题语言映射) #include #ifndef _WIN64 #ifdef _DEBUG #pragma comment(lib, "jsoncpp/jsoncppd.lib") #else #pragma comment(lib, "jsoncpp/jsoncpp.lib") #endif #else #ifdef _DEBUG #pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib") #else #pragma comment(lib, "jsoncpp/jsoncpp_x64.lib") #endif #endif namespace { // P2b 工具等待响应的超时(ms)。MCP 一次性请求:等待子连接回传进程/窗口列表。 static const int kMcpToolTimeoutMs = 20000; // Json::Value → 紧凑 JSON 字符串 std::string JsonToString(const Json::Value& v) { Json::StreamWriterBuilder b; b["indentation"] = ""; return Json::writeString(b, v); } // JSON-RPC 2.0 成功响应 std::string BuildResult(const Json::Value& id, const Json::Value& result) { Json::Value resp(Json::objectValue); resp["jsonrpc"] = "2.0"; resp["id"] = id; resp["result"] = result; return JsonToString(resp); } // JSON-RPC 2.0 错误响应 std::string BuildError(const Json::Value& id, int code, const std::string& msg) { Json::Value resp(Json::objectValue); resp["jsonrpc"] = "2.0"; resp["id"] = id; Json::Value err(Json::objectValue); err["code"] = code; err["message"] = msg; resp["error"] = err; return JsonToString(resp); } // initialize 握手(MCP 规范:protocolVersion + capabilities + serverInfo) std::string BuildInitializeResult(const Json::Value& id) { Json::Value result(Json::objectValue); result["protocolVersion"] = "2025-06-18"; Json::Value caps(Json::objectValue); caps["tools"] = Json::Value(Json::objectValue); result["capabilities"] = caps; Json::Value serverInfo(Json::objectValue); serverInfo["name"] = "yama"; serverInfo["version"] = VERSION_STR; result["serverInfo"] = serverInfo; return BuildResult(id, result); } // ping 健康检查:返回空 result std::string BuildPingResult(const Json::Value& id) { return BuildResult(id, Json::Value(Json::objectValue)); } // ========== P2a 通用辅助 ========== // 小写化(仅 ASCII,UTF-8 多字节原样保留):用于不区分大小写的子串匹配 std::string ToLowerAscii(const std::string& s) { std::string r = s; for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); return r; } // 不区分大小写的子串匹配 bool ContainsCI(const std::string& haystack, const std::string& needle) { if (needle.empty()) return true; return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos; } // 取对象的字符串字段,缺失/非字符串返回 "" std::string JsonStrField(const Json::Value& v, const char* key) { if (v.isObject() && v.isMember(key) && v[key].isString()) return v[key].asString(); return ""; } // 是否纯数字(host id 为 uint64 十进制字符串) bool IsDigits(const std::string& s) { if (s.empty()) return false; for (char c : s) if (c < '0' || c > '9') return false; return true; } // 读取 tools/call 的入参(MCP 规范:params.arguments 为工具入参对象) Json::Value GetCallArguments(const Json::Value& params) { if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject()) return params["arguments"]; return Json::Value(Json::objectValue); } // 读取可选字符串入参,缺失返回 "" std::string GetStringArg(const Json::Value& args, const char* key) { return JsonStrField(args, key); } // ========== P2b 辅助 ========== // 解析 id 入参(必填、纯数字)为 uint64;非法返回 false。 bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) { std::string sid = GetStringArg(args, "id"); if (sid.empty()) { err = "Missing required parameter: id"; return false; } if (!IsDigits(sid)) { err = "Invalid id: expected a decimal host id string"; return false; } out = strtoull(sid.c_str(), nullptr, 10); return true; } // 在 m_HostList 中按 clientID 找在线主 context(复刻 CollectOnlineHosts 的锁内遍历)。 context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) { if (!parent) return nullptr; context* found = nullptr; EnterCriticalSection(&parent->m_cs); for (context* ctx : parent->m_HostList) { if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) { found = ctx; break; } } LeaveCriticalSection(&parent->m_cs); return found; } // 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。 size_t BoundedStrlen(const char* p, size_t avail) { size_t n = 0; while (n < avail && p[n]) ++n; return n; } // 从源编码 cp 转为 UTF-8(输出到 JSON)。空/失败返回 ""。 std::string ToUtf8(const char* s, UINT cp) { if (!s || !*s) return ""; int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0); if (wlen <= 0) return ""; std::wstring w(wlen - 1, L'\0'); MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen); int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL); if (u8len <= 0) return ""; std::string out(u8len - 1, '\0'); WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL); return out; } // 从 UTF-8 转为目标编码 cp(文件链路的目录路径在 Windows 走 ANSI,与 FileManagerDlg 的 // CString 一致;cp 按 clientType 判定:Windows=936、LNX/MAC=CP_UTF8,见 BuildListFiles)。 // 非 UTF-8(如纯 ASCII)失败时原样返回。 std::string ToAnsi(const std::string& utf8, UINT cp) { if (utf8.empty()) return ""; int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0); if (wlen <= 0) return utf8; std::wstring w(wlen, L'\0'); MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen); int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL); if (alen <= 0) return utf8; std::string out(alen, '\0'); WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL); return out; } // 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。 // 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。 void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) { title.clear(); status = "normal"; pid = 0; if (!s || !*s) return; std::string t(s); int len = (int)t.size(); int pipePos[4] = { -1, -1, -1, -1 }; int cnt = 0; for (int i = len - 1; i >= 0 && cnt < 4; --i) { if (t[i] == '|') pipePos[cnt++] = i; } if (cnt < 4) { // 老格式:只有标题 title = t; return; } pid = strtoull(t.c_str() + pipePos[2] + 1, nullptr, 10); status = t.substr(pipePos[3] + 1, pipePos[2] - pipePos[3] - 1); title = t.substr(0, pipePos[3]); } // 解析 TOKEN_PSLIST 缓冲(data[0]=token,其后为 [pid:4][name:arch\0][path\0] 记录)。 // 进程名/路径为客户端 ANSI(编码由 cp 指定),转 UTF-8 输出。 Json::Value ParseProcessList(const std::vector& data, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 2) return arr; const char* p = (const char*)data.data(); size_t len = data.size(); size_t off = 1; // 跳过 TOKEN 字节 while (off + sizeof(DWORD) <= len) { DWORD pid = *(const DWORD*)(p + off); off += sizeof(DWORD); const char* exeFile = p + off; size_t exeLen = BoundedStrlen(exeFile, len - off); if (exeLen >= len - off) break; // 未以 '\0' 结尾,异常数据 if (exeLen == 0) break; // 空进程名 = 尾部零填充(LocalSize 对齐),停止解析 off += exeLen + 1; const char* fullPath = p + off; size_t pathLen = BoundedStrlen(fullPath, len - off); if (pathLen >= len - off) break; off += pathLen + 1; std::string name(exeFile, exeLen); std::string arch; size_t colon = name.find(':'); if (colon != std::string::npos) { arch = name.substr(colon + 1); name = name.substr(0, colon); } Json::Value item(Json::objectValue); item["pid"] = (Json::UInt64)pid; item["name"] = ToUtf8(name.c_str(), cp); item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), cp); item["path"] = ToUtf8(fullPath, cp); arr.append(item); } return arr; } // 解析 TOKEN_WSLIST 缓冲(data[0]=token,其后为 [hwnd:4][title|status|pid|r1|r2\0] 记录)。 // 窗口标题为客户端 UTF-8(老客户端为 CP_ACP),用 cp 解码后转 UTF-8 输出。 Json::Value ParseWindowList(const std::vector& data, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 2) return arr; const char* p = (const char*)data.data(); size_t len = data.size(); size_t off = 1; while (off + sizeof(DWORD) <= len) { DWORD hwnd = *(const DWORD*)(p + off); off += sizeof(DWORD); const char* titleWithAttrs = p + off; size_t tlen = BoundedStrlen(titleWithAttrs, len - off); if (tlen >= len - off) break; if (tlen == 0) break; // 空记录 = 尾部零填充(LocalSize 对齐),停止解析 off += tlen + 1; std::string title, status; uint64_t pid = 0; ParseWindowAttrsStr(titleWithAttrs, title, status, pid); Json::Value item(Json::objectValue); item["hwnd"] = (Json::UInt64)hwnd; item["title"] = ToUtf8(title.c_str(), cp); item["status"] = status; item["pid"] = (Json::UInt64)pid; arr.append(item); } return arr; } // 解析 TOKEN_DRIVE_LIST 缓冲(data[0]=token,其后为 // [letter:1][GetDriveType:1][totalMB:4][freeMB:4][typeName\0][fileSystem\0] 记录, // 以 letter=='\0' 终止)。typeName/fileSystem 为客户端 ANSI(编码由 cp 指定)。 Json::Value ParseDriveList(const std::vector& data, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 2) return arr; const char* p = (const char*)data.data(); size_t len = data.size(); size_t off = 1; // 跳过 TOKEN 字节 while (off + 10 <= len && p[off] != '\0') { char letter = p[off]; unsigned int type = (unsigned int)(unsigned char)p[off + 1]; DWORD totalMB = *(const DWORD*)(p + off + 2); DWORD freeMB = *(const DWORD*)(p + off + 6); off += 10; const char* typeName = p + off; size_t tlen = BoundedStrlen(typeName, len - off); if (tlen >= len - off) break; off += tlen + 1; const char* fileSystem = p + off; size_t flen = BoundedStrlen(fileSystem, len - off); if (flen >= len - off) break; off += flen + 1; std::string drive; drive += letter; drive += ":\\"; Json::Value item(Json::objectValue); item["drive"] = drive; item["type"] = (Json::UInt64)type; item["typeName"] = ToUtf8(typeName, cp); item["fileSystem"] = ToUtf8(fileSystem, cp); item["totalMB"] = (Json::UInt64)totalMB; item["freeMB"] = (Json::UInt64)freeMB; arr.append(item); } return arr; } // 解析 TOKEN_FILE_LIST 缓冲(data[0]=token,其后为 // [attr:1][filename\0][sizeHigh:4][sizeLow:4][ftLastWriteTime:8] 记录)。 // 文件名为客户端 ANSI(编码由 cp 指定);attr 非 0 表示目录(FILE_ATTRIBUTE_DIRECTORY)。 Json::Value ParseFileList(const std::vector& data, int maxEntries, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 2) return arr; const char* p = (const char*)data.data(); size_t len = data.size(); size_t off = 1; int count = 0; while (off + 1 <= len && count < maxEntries) { bool isDir = (p[off] & FILE_ATTRIBUTE_DIRECTORY) != 0; off += 1; const char* name = p + off; size_t nlen = BoundedStrlen(name, len - off); if (nlen >= len - off) break; if (nlen == 0) break; // 空记录 = 尾部零填充(LocalAlloc 对齐),停止解析 off += nlen + 1; if (off + 16 > len) break; DWORD sizeHigh = *(const DWORD*)(p + off); DWORD sizeLow = *(const DWORD*)(p + off + 4); ULONGLONG size = ((ULONGLONG)sizeHigh << 32) | sizeLow; // 修改时间(FILETIME,100ns since 1601-01-01)→ Unix 秒 // FILETIME 在线上是 dwLowDateTime(低 4 字节) 在前、dwHighDateTime(高 4 字节) 在后 // (客户端 memcpy(&ftLastWriteTime, sizeof(FILETIME)),见 FileManager.cpp::SendFilesList)。 DWORD ftLow = *(const DWORD*)(p + off + 8); DWORD ftHigh = *(const DWORD*)(p + off + 12); off += 16; ULONGLONG ft = ((ULONGLONG)ftHigh << 32) | ftLow; Json::Int64 mtime = (Json::Int64)(ft / 10000000ULL) - 11644473600LL; Json::Value item(Json::objectValue); item["name"] = ToUtf8(name, cp); item["isDir"] = isDir; item["size"] = (Json::UInt64)size; item["mtime"] = mtime; arr.append(item); ++count; } return arr; } // 解析 TOKEN_SERVERLIST 缓冲(data[0]=token,其后为 // [displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0] 记录)。 // 字段全部来自 Windows A 接口(EnumServicesStatus / QueryServiceConfig),为客户端 // ANSI(编码由 cp 指定)。以 displayName 与 serviceName 同时为空作为尾部零填充 // (LocalAlloc/LocalReAlloc LMEM_ZEROINIT 对齐)的终止条件。 Json::Value ParseServiceList(const std::vector& data, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 2) return arr; const char* p = (const char*)data.data(); size_t len = data.size(); size_t off = 1; // 跳过 TOKEN 字节 while (off < len) { const char* f[5]; bool ok = true; for (int i = 0; i < 5; ++i) { size_t n = BoundedStrlen(p + off, len - off); if (n >= len - off) { ok = false; break; } // 未以 '\0' 结尾,异常数据 f[i] = p + off; off += n + 1; } if (!ok) break; // displayName 与 serviceName 都为空 → 尾部零填充,停止解析 if (f[0][0] == '\0' && f[1][0] == '\0') break; Json::Value item(Json::objectValue); item["display_name"] = ToUtf8(f[0], cp); item["service_name"] = ToUtf8(f[1], cp); item["binary_path"] = ToUtf8(f[2], cp); item["status"] = ToUtf8(f[3], cp); // Stopped/Running/Paused/... 英文 item["start_type"] = ToUtf8(f[4], cp); // Boot-Start/Auto-Start/Demand-Start/... 英文 arr.append(item); } return arr; } // ========== list_registry:注册表解析 ========== // 注册表 REGMSG 头(client RegisterOperation.cpp / server RegisterDlg.cpp 各自本地定义,此处同构)。 struct RegMsgHeader { int count; // 名字个数 DWORD size; // 名字大小(定宽) DWORD valsize; // 值大小(定宽) }; // client KEYVALUE 枚举(RegisterOperation.cpp):MREG_SZ=0 … MREG_NONE=6。 enum RegValueType { REG_T_SZ = 0, REG_T_DWORD = 1, REG_T_BINARY = 2, REG_T_EXPAND_SZ = 3, REG_T_MULTI_SZ = 4, REG_T_QWORD = 5, REG_T_NONE = 6, }; // client MYKEY 枚举:根键 token。 enum RegRootToken { REG_ROOT_CLASSES_ROOT = 0, REG_ROOT_CURRENT_USER = 1, REG_ROOT_LOCAL_MACHINE = 2, REG_ROOT_USERS = 3, REG_ROOT_CURRENT_CONFIG = 4, }; // ASCII 不区分大小写前缀匹配(注册表根键名大小写不敏感)。 bool StrPrefixCI(const std::string& s, const char* prefix, size_t len) { if (s.size() < len) return false; for (size_t i = 0; i < len; ++i) { char a = s[i], b = prefix[i]; if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) return false; } return true; } // 值类型名(可读字符串) const char* RegTypeName(BYTE type) { switch (type) { case REG_T_SZ: return "REG_SZ"; case REG_T_DWORD: return "REG_DWORD"; case REG_T_BINARY: return "REG_BINARY"; case REG_T_EXPAND_SZ: return "REG_EXPAND_SZ"; case REG_T_MULTI_SZ: return "REG_MULTI_SZ"; case REG_T_QWORD: return "REG_QWORD"; case REG_T_NONE: return "REG_NONE"; default: return "REG_UNKNOWN"; } } // 将注册表值数据格式化为字符串。valSize 为定宽(client 按 MaxDataLen+1 填充), // 字符串类取到 '\0' 为止、数值类取定长前缀;REG_BINARY 长度无法从协议还原,按 valSize 输出 hex。 std::string FormatRegData(BYTE type, const BYTE* val, size_t valSize, UINT cp) { switch (type) { case REG_T_SZ: case REG_T_EXPAND_SZ: return ToUtf8((const char*)val, cp); case REG_T_MULTI_SZ: { // 多个 '\0' 结尾串、双 '\0' 结束 std::string out; size_t i = 0; while (i < valSize && val[i] != '\0') { const char* s = (const char*)(val + i); size_t n = BoundedStrlen(s, valSize - i); if (!out.empty()) out += "\n"; out += ToUtf8(s, cp); i += n + 1; } return out; } case REG_T_DWORD: { // 4 字节小端 if (valSize < 4) return ""; DWORD v = 0; memcpy(&v, val, 4); char buf[64]; sprintf(buf, "0x%08lX (%lu)", (unsigned long)v, (unsigned long)v); return buf; } case REG_T_QWORD: { // 8 字节小端 if (valSize < 8) return ""; uint64_t v = 0; memcpy(&v, val, 8); char buf[96]; sprintf(buf, "0x%016I64X (%I64u)", (unsigned __int64)v, (unsigned __int64)v); return buf; } case REG_T_BINARY: { // hex,尾部可能含 0 填充 std::string out; char buf[4]; for (size_t i = 0; i < valSize; ++i) { sprintf(buf, "%02X", val[i]); out += buf; } return out; } case REG_T_NONE: default: return ""; } } // 解析 TOKEN_REG_PATH 缓冲:[token:1][RegMsgHeader:12][count * size 定宽子键名]。cp 为客户端 ANSI。 Json::Value ParseRegPath(const std::vector& data, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 1 + sizeof(RegMsgHeader)) return arr; const char* p = (const char*)data.data(); size_t off = 1; // 跳过 TOKEN 字节 RegMsgHeader hdr; memcpy(&hdr, p + off, sizeof(hdr)); off += sizeof(hdr); int count = hdr.count; size_t nameSize = hdr.size; for (int i = 0; i < count; ++i) { if (off + nameSize > data.size()) break; const char* name = p + off; off += nameSize; if (name[0] == '\0') break; // 空名 = 尾部零填充,停止 arr.append(ToUtf8(name, cp)); } return arr; } // 解析 TOKEN_REG_KEY 缓冲:[token:1][RegMsgHeader:12][count * {type:1,name:size,data:valsize}]。 Json::Value ParseRegKey(const std::vector& data, UINT cp) { Json::Value arr(Json::arrayValue); if (data.size() < 1 + sizeof(RegMsgHeader)) return arr; const char* p = (const char*)data.data(); size_t off = 1; // 跳过 TOKEN 字节 RegMsgHeader hdr; memcpy(&hdr, p + off, sizeof(hdr)); off += sizeof(hdr); int count = hdr.count; size_t nameSize = hdr.size; size_t valSize = hdr.valsize; for (int i = 0; i < count; ++i) { if (off + 1 + nameSize + valSize > data.size()) break; BYTE type = (BYTE)p[off]; off += 1; const char* name = p + off; // 定宽 nameSize 内以 '\0' 结尾 off += nameSize; const BYTE* val = (const BYTE*)(p + off); off += valSize; Json::Value item(Json::objectValue); item["name"] = ToUtf8(name, cp); item["type"] = RegTypeName(type); item["data"] = FormatRegData(type, val, valSize, cp); arr.append(item); } return arr; } // 解析 "HKEY_LOCAL_MACHINE\Software\..." → rootToken + 相对子键路径(去掉根键名与首个 '\')。 // 返回 false 表示 path 为空(调用方列根键)或未匹配任何根键(调用方报错)。 bool ParseRegistryPath(const std::string& path, BYTE& rootToken, std::string& relPath) { struct RootMap { const char* name; BYTE token; }; static const RootMap roots[] = { { "HKEY_CLASSES_ROOT", REG_ROOT_CLASSES_ROOT }, { "HKEY_CURRENT_USER", REG_ROOT_CURRENT_USER }, { "HKEY_LOCAL_MACHINE", REG_ROOT_LOCAL_MACHINE }, { "HKEY_USERS", REG_ROOT_USERS }, { "HKEY_CURRENT_CONFIG", REG_ROOT_CURRENT_CONFIG }, }; if (path.empty()) return false; for (const RootMap& r : roots) { size_t n = strlen(r.name); if (StrPrefixCI(path, r.name, n)) { rootToken = r.token; relPath = path.substr(n); if (!relPath.empty() && relPath[0] == '\\') relPath.erase(0, 1); return true; } } return false; } // 列根键(path 为空):返回 5 个固定根键,无需查询客户端。 std::string BuildRegistryRoots(const Json::Value& id) { Json::Value keys(Json::arrayValue); const char* roots[] = { "HKEY_CLASSES_ROOT", "HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE", "HKEY_USERS", "HKEY_CURRENT_CONFIG" }; for (const char* r : roots) keys.append(r); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["keys"] = keys; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string((int)keys.size()) + std::string(u8" 个根键。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // 收集所有在线主机 JSON 数组(m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C) void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) { if (!parent) return; EnterCriticalSection(&parent->m_cs); for (context* ctx : parent->m_HostList) { if (!ctx || !ctx->IsLogin()) continue; hosts.append(BuildHostJson(ctx, parent->m_ClientMap)); } LeaveCriticalSection(&parent->m_cs); } // ========== 工具 schema ========== // 单台主机字段 schema(hosts 数组元素 / 单机详情共用的形状) Json::Value BuildHostItemSchema() { Json::Value itemProps(Json::objectValue); const char* strFields[] = { "id", "name", "remark", "ip", "os", "location", "rtt", "version", "activeWindow", "group", "screen", "clientType" }; for (const char* f : strFields) { Json::Value p(Json::objectValue); p["type"] = "string"; itemProps[f] = p; } Json::Value onlineProp(Json::objectValue); onlineProp["type"] = "boolean"; itemProps["online"] = onlineProp; return itemProps; } // list_online_hosts / search_hosts 的 outputSchema(hosts 数组) Json::Value BuildHostOutputSchema() { Json::Value props(Json::objectValue); Json::Value hostsProp(Json::objectValue); hostsProp["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "object"; items["properties"] = BuildHostItemSchema(); hostsProp["items"] = items; props["hosts"] = hostsProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("hosts"); schema["required"] = required; return schema; } // get_host_detail 的 outputSchema(单台主机) Json::Value BuildHostDetailOutputSchema() { Json::Value props(Json::objectValue); Json::Value hostProp(Json::objectValue); hostProp["type"] = "object"; hostProp["properties"] = BuildHostItemSchema(); props["host"] = hostProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("host"); schema["required"] = required; return schema; } // search_hosts 的 inputSchema(全部可选) Json::Value BuildSearchHostsInputSchema() { Json::Value props(Json::objectValue); const char* strParams[] = { "name", "ip", "group", "os" }; for (const char* p : strParams) { Json::Value s(Json::objectValue); s["type"] = "string"; props[p] = s; } Json::Value onlineProp(Json::objectValue); onlineProp["type"] = "boolean"; props["online"] = onlineProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; return schema; } // get_host_detail 的 inputSchema(id 必填) Json::Value BuildGetHostDetailInputSchema() { Json::Value props(Json::objectValue); Json::Value idProp(Json::objectValue); idProp["type"] = "string"; idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; props["id"] = idProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("id"); schema["required"] = required; return schema; } // list_processes 的 outputSchema(processes 数组) Json::Value BuildProcessListOutputSchema() { Json::Value props(Json::objectValue); Json::Value procsProp(Json::objectValue); procsProp["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "object"; Json::Value itemProps(Json::objectValue); Json::Value pidProp(Json::objectValue); pidProp["type"] = "integer"; itemProps["pid"] = pidProp; const char* strFields[] = { "name", "arch", "path" }; for (const char* f : strFields) { Json::Value s(Json::objectValue); s["type"] = "string"; itemProps[f] = s; } items["properties"] = itemProps; procsProp["items"] = items; props["processes"] = procsProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("processes"); schema["required"] = required; return schema; } // list_windows 的 outputSchema(windows 数组) Json::Value BuildWindowListOutputSchema() { Json::Value props(Json::objectValue); Json::Value winsProp(Json::objectValue); winsProp["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "object"; Json::Value itemProps(Json::objectValue); Json::Value hwndProp(Json::objectValue); hwndProp["type"] = "integer"; itemProps["hwnd"] = hwndProp; Json::Value pidProp(Json::objectValue); pidProp["type"] = "integer"; itemProps["pid"] = pidProp; const char* strFields[] = { "title", "status" }; for (const char* f : strFields) { Json::Value s(Json::objectValue); s["type"] = "string"; itemProps[f] = s; } items["properties"] = itemProps; winsProp["items"] = items; props["windows"] = winsProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("windows"); schema["required"] = required; return schema; } // get_activity_history 的 outputSchema(records 数组 + 原始文本) Json::Value BuildActivityHistoryOutputSchema() { Json::Value props(Json::objectValue); Json::Value recordsProp(Json::objectValue); recordsProp["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "string"; recordsProp["items"] = items; props["records"] = recordsProp; Json::Value rawProp(Json::objectValue); rawProp["type"] = "string"; props["activityHistory"] = rawProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; return schema; } // list_services 的 outputSchema(services 数组) Json::Value BuildServiceListOutputSchema() { Json::Value props(Json::objectValue); Json::Value svcsProp(Json::objectValue); svcsProp["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "object"; Json::Value itemProps(Json::objectValue); const char* strFields[] = { "display_name", "service_name", "binary_path", "status", "start_type" }; for (const char* f : strFields) { Json::Value s(Json::objectValue); s["type"] = "string"; itemProps[f] = s; } items["properties"] = itemProps; svcsProp["items"] = items; props["services"] = svcsProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("services"); schema["required"] = required; return schema; } // list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键) Json::Value BuildListRegistryInputSchema() { Json::Value props(Json::objectValue); Json::Value idProp(Json::objectValue); idProp["type"] = "string"; idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; props["id"] = idProp; Json::Value pathProp(Json::objectValue); pathProp["type"] = "string"; pathProp["description"] = u8"注册表键路径(如 HKEY_LOCAL_MACHINE\\Software);省略或传空则返回 5 个根键"; props["path"] = pathProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("id"); schema["required"] = required; return schema; } // list_registry 的 outputSchema(keys 字符串数组 / values 对象数组) Json::Value BuildListRegistryOutputSchema() { Json::Value props(Json::objectValue); Json::Value pathProp(Json::objectValue); pathProp["type"] = "string"; props["path"] = pathProp; Json::Value keysProp(Json::objectValue); keysProp["type"] = "array"; Json::Value keyItems(Json::objectValue); keyItems["type"] = "string"; keysProp["items"] = keyItems; props["keys"] = keysProp; Json::Value valsProp(Json::objectValue); valsProp["type"] = "array"; Json::Value valItems(Json::objectValue); valItems["type"] = "object"; Json::Value valProps(Json::objectValue); const char* strFields[] = { "name", "type", "data" }; for (const char* f : strFields) { Json::Value s(Json::objectValue); s["type"] = "string"; valProps[f] = s; } valItems["properties"] = valProps; valsProp["items"] = valItems; props["values"] = valsProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("keys"); required.append("values"); schema["required"] = required; return schema; } // exec_command 的 inputSchema(id 必填、command 必填、timeout_ms 可选) Json::Value BuildExecCommandInputSchema() { Json::Value props(Json::objectValue); Json::Value idProp(Json::objectValue); idProp["type"] = "string"; idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; props["id"] = idProp; Json::Value cmdProp(Json::objectValue); cmdProp["type"] = "string"; cmdProp["description"] = u8"要执行的命令(受只读模式与命令白名单约束,仅允许只读命令前缀)"; props["command"] = cmdProp; Json::Value timeoutProp(Json::objectValue); timeoutProp["type"] = "integer"; timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000,上限 600000)"; props["timeout_ms"] = timeoutProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("id"); required.append("command"); schema["required"] = required; return schema; } // exec_command 的 outputSchema(stdout 文本 / exit_code) Json::Value BuildExecCommandOutputSchema() { Json::Value props(Json::objectValue); Json::Value stdoutProp(Json::objectValue); stdoutProp["type"] = "string"; stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)"; props["stdout"] = stdoutProp; Json::Value exitProp(Json::objectValue); exitProp["type"] = "integer"; exitProp["description"] = u8"退出码:0=成功、1=非零退出、-1=未知(进程异常退出)"; props["exit_code"] = exitProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("stdout"); required.append("exit_code"); schema["required"] = required; return schema; } // get_client_log 的 outputSchema(原始日志文本) Json::Value BuildClientLogOutputSchema() { Json::Value props(Json::objectValue); Json::Value logProp(Json::objectValue); logProp["type"] = "string"; props["log"] = logProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("log"); schema["required"] = required; return schema; } // get_audit_log 的 outputSchema(服务端消息/审计日志条目数组) Json::Value BuildAuditLogOutputSchema() { Json::Value props(Json::objectValue); Json::Value entriesProp(Json::objectValue); entriesProp["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "object"; Json::Value itemProps(Json::objectValue); const char* strFields[] = { "type", "time", "msg" }; for (const char* f : strFields) { Json::Value s(Json::objectValue); s["type"] = "string"; itemProps[f] = s; } items["properties"] = itemProps; entriesProp["items"] = items; props["entries"] = entriesProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("entries"); schema["required"] = required; return schema; } // get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中) Json::Value BuildScreenshotOutputSchema() { Json::Value props(Json::objectValue); Json::Value imageProp(Json::objectValue); imageProp["type"] = "object"; Json::Value imageProps(Json::objectValue); Json::Value mimeProp(Json::objectValue); mimeProp["type"] = "string"; imageProps["mimeType"] = mimeProp; const char* intFields[] = { "width", "height", "bytes" }; for (const char* f : intFields) { Json::Value s(Json::objectValue); s["type"] = "integer"; imageProps[f] = s; } imageProp["properties"] = imageProps; props["image"] = imageProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("image"); schema["required"] = required; return schema; } // get_screenshot 的 inputSchema(id 必填,max_width 可选) Json::Value BuildGetScreenshotInputSchema() { Json::Value props(Json::objectValue); Json::Value idProp(Json::objectValue); idProp["type"] = "string"; idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; props["id"] = idProp; Json::Value mwProp(Json::objectValue); mwProp["type"] = "integer"; mwProp["description"] = u8"期望图片最大宽度(像素,钳制到 64~1920)。省略或传 0 时沿用 RTT 自适应缩略图档位(最大 1024);传 1920 可拿到接近原分辨率(1080p 源屏即原分辨率,4K 源屏最多 1920),供 AI 视觉/OCR 场景提升清晰度。"; props["max_width"] = mwProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("id"); schema["required"] = required; return schema; } // list_files 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列盘) Json::Value BuildListFilesInputSchema() { Json::Value props(Json::objectValue); Json::Value idProp(Json::objectValue); idProp["type"] = "string"; idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; props["id"] = idProp; Json::Value pathProp(Json::objectValue); pathProp["type"] = "string"; pathProp["description"] = u8"要列举的目录路径(如 C:\\Windows);省略或传空则返回驱动器列表"; props["path"] = pathProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value required(Json::arrayValue); required.append("id"); schema["required"] = required; return schema; } // list_files 的 outputSchema(drives 数组 / files 数组,二者其一非空) Json::Value BuildListFilesOutputSchema() { Json::Value props(Json::objectValue); Json::Value drivesProp(Json::objectValue); drivesProp["type"] = "array"; Json::Value driveItems(Json::objectValue); driveItems["type"] = "object"; Json::Value driveProps(Json::objectValue); const char* driveStr[] = { "drive", "typeName", "fileSystem" }; for (const char* f : driveStr) { Json::Value s(Json::objectValue); s["type"] = "string"; driveProps[f] = s; } const char* driveInt[] = { "type", "totalMB", "freeMB" }; for (const char* f : driveInt) { Json::Value s(Json::objectValue); s["type"] = "integer"; driveProps[f] = s; } driveItems["properties"] = driveProps; drivesProp["items"] = driveItems; props["drives"] = drivesProp; Json::Value filesProp(Json::objectValue); filesProp["type"] = "array"; Json::Value fileItems(Json::objectValue); fileItems["type"] = "object"; Json::Value fileProps(Json::objectValue); Json::Value nameProp(Json::objectValue); nameProp["type"] = "string"; fileProps["name"] = nameProp; Json::Value isDirProp(Json::objectValue); isDirProp["type"] = "boolean"; fileProps["isDir"] = isDirProp; const char* fileInt[] = { "size", "mtime" }; for (const char* f : fileInt) { Json::Value s(Json::objectValue); s["type"] = "integer"; fileProps[f] = s; } fileItems["properties"] = fileProps; filesProp["items"] = fileItems; props["files"] = filesProp; Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; return schema; } // tools/list std::string BuildToolsListResult(const Json::Value& id) { Json::Value result(Json::objectValue); Json::Value tools(Json::arrayValue); // 1) list_online_hosts { Json::Value tool(Json::objectValue); tool["name"] = "list_online_hosts"; // 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK, // 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。 tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。"; Json::Value inputSchema(Json::objectValue); inputSchema["type"] = "object"; inputSchema["properties"] = Json::Value(Json::objectValue); inputSchema["required"] = Json::Value(Json::arrayValue); tool["inputSchema"] = inputSchema; tool["outputSchema"] = BuildHostOutputSchema(); tools.append(tool); } // 2) search_hosts(P2a:纯内存过滤,无子链接) { Json::Value tool(Json::objectValue); tool["name"] = "search_hosts"; tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。"; tool["inputSchema"] = BuildSearchHostsInputSchema(); tool["outputSchema"] = BuildHostOutputSchema(); tools.append(tool); } // 3) get_host_detail(P2a:单机详情,纯内存) { Json::Value tool(Json::objectValue); tool["name"] = "get_host_detail"; tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); tool["outputSchema"] = BuildHostDetailOutputSchema(); tools.append(tool); } // 4) list_processes(P2b:主连接 RPC,进程列表) { Json::Value tool(Json::objectValue); tool["name"] = "list_processes"; tool["description"] = u8"获取指定在线主机的进程列表(PID、映像名称、架构、程序完整路径)。通过主连接下发命令、子连接回传,一次性返回。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema tool["outputSchema"] = BuildProcessListOutputSchema(); tools.append(tool); } // 5) list_windows(P2b:主连接 RPC,窗口列表) { Json::Value tool(Json::objectValue); tool["name"] = "list_windows"; tool["description"] = u8"获取指定在线主机的顶层窗口列表(句柄、窗口标题、窗口状态、所属进程 PID)。一次性返回。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema tool["outputSchema"] = BuildWindowListOutputSchema(); tools.append(tool); } // 6) get_activity_history(P2b:主连接 RPC,历史活动记录) { Json::Value tool(Json::objectValue); tool["name"] = "get_activity_history"; tool["description"] = u8"获取指定在线主机的历史活动记录(前台窗口驻留时长,每行一条「[时间] [标题] 时长」)。主连接 RPC、一次性返回。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema tool["outputSchema"] = BuildActivityHistoryOutputSchema(); tools.append(tool); } // 7) get_screenshot(P2c:屏幕预览链路,主连接 RPC) { Json::Value tool(Json::objectValue); tool["name"] = "get_screenshot"; tool["description"] = u8"截取指定在线主机的一帧屏幕,返回 base64 编码的 JPEG 图片(image 对象含 mimeType/data/width/height)。仅 Windows 客户端支持(能力位 CLIENT_CAP_SCREEN_PREVIEW),主连接 RPC、一次性返回。"; tool["inputSchema"] = BuildGetScreenshotInputSchema(); // { id } 必填 + max_width 可选 tool["outputSchema"] = BuildScreenshotOutputSchema(); tools.append(tool); } // 8) list_files(P2c:文件链路,列盘/列目录) { Json::Value tool(Json::objectValue); tool["name"] = "list_files"; tool["description"] = u8"列举指定在线主机的目录。省略或空 path 返回驱动器列表(drives);给定 path 返回该目录一层内的文件/子目录(files,最多 500 条,含 name/isDir/size/mtime)。只读。"; tool["inputSchema"] = BuildListFilesInputSchema(); tool["outputSchema"] = BuildListFilesOutputSchema(); tools.append(tool); } // 9) list_services(P3:服务链路,仅 Windows,一次性子链接) { Json::Value tool(Json::objectValue); tool["name"] = "list_services"; tool["description"] = u8"获取指定在线 Windows 主机的服务列表(显示名、服务名、可执行文件路径、运行状态、启动类型)。仅 Windows 客户端支持;一次性返回。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema tool["outputSchema"] = BuildServiceListOutputSchema(); tools.append(tool); } // 10) get_client_log(P3:客户端运行日志,一次性子链接取首条全量) { Json::Value tool(Json::objectValue); tool["name"] = "get_client_log"; tool["description"] = u8"获取指定在线主机 YAMA 客户端的内存运行日志(最近最多 1000 条,含时间戳/源文件/行号)。客户端持续增量上报,本工具取当前时刻的全量快照后即断开。只读。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema tool["outputSchema"] = BuildClientLogOutputSchema(); tools.append(tool); } // 11) get_audit_log(服务端本地:主界面消息/审计日志,纯内存,无子链接) { Json::Value tool(Json::objectValue); tool["name"] = "get_audit_log"; tool["description"] = u8"获取 YAMA 服务端主界面的消息/审计日志(主机上线/下线、操作结果、告警等)。返回列表内当前全部条目(最多 1000 条,新在前)。只读、纯内存。"; Json::Value inputSchema(Json::objectValue); inputSchema["type"] = "object"; inputSchema["properties"] = Json::Value(Json::objectValue); inputSchema["required"] = Json::Value(Json::arrayValue); tool["inputSchema"] = inputSchema; tool["outputSchema"] = BuildAuditLogOutputSchema(); tools.append(tool); } // 12) list_registry(P3:注册表查询,列根键/列子键+值,仅 Windows,一次性子链接) { Json::Value tool(Json::objectValue); tool["name"] = "list_registry"; tool["description"] = u8"查询指定在线 Windows 主机的注册表。省略或空 path 返回 5 个根键;给定 path(如 HKEY_LOCAL_MACHINE\\Software)返回该键一层内的子键(keys)与值(values,含 name/type/data)。只读。"; tool["inputSchema"] = BuildListRegistryInputSchema(); tool["outputSchema"] = BuildListRegistryOutputSchema(); tools.append(tool); } // 13) exec_command(P3:一次性远程命令,仅 Windows,安全门:只读默认 + 白名单 + 审计) // 只读模式下 tools/list 直接隐藏该工具;即便被绕过,BuildExecCommand 也会再次校验只读。 if (!CMcpServer::Instance().IsReadonly()) { Json::Value tool(Json::objectValue); tool["name"] = "exec_command"; tool["description"] = u8"在指定在线 Windows 主机上执行一条命令并返回 stdout 与退出码(受只读模式与命令白名单约束,默认仅允许只读命令前缀,见 McpReadonly / McpCmdWhitelist)。"; tool["inputSchema"] = BuildExecCommandInputSchema(); tool["outputSchema"] = BuildExecCommandOutputSchema(); tools.append(tool); } result["tools"] = tools; return BuildResult(id, result); } // tools/call:list_online_hosts std::string BuildListOnlineHosts(const Json::Value& id, CMy2015RemoteDlg* parent) { Json::Value hosts(Json::arrayValue); CollectOnlineHosts(parent, hosts); int count = (int)hosts.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["hosts"] = hosts; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 台主机在线。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:search_hosts std::string BuildSearchHosts(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { Json::Value all(Json::arrayValue); CollectOnlineHosts(parent, all); std::string fName = GetStringArg(args, "name"); std::string fIp = GetStringArg(args, "ip"); std::string fGroup = GetStringArg(args, "group"); std::string fOs = GetStringArg(args, "os"); bool hasOnline = args.isObject() && args.isMember("online") && args["online"].isBool(); bool wantOnline = hasOnline ? args["online"].asBool() : true; Json::Value hosts(Json::arrayValue); // 列表只含在线主机:显式 online=false 时直接空结果 if (!hasOnline || wantOnline) { for (unsigned int i = 0; i < all.size(); ++i) { const Json::Value& h = all[i]; if (!fName.empty()) { std::string name = JsonStrField(h, "name"); std::string remark = JsonStrField(h, "remark"); if (!ContainsCI(name, fName) && !ContainsCI(remark, fName)) continue; } if (!fIp.empty() && !ContainsCI(JsonStrField(h, "ip"), fIp)) continue; if (!fGroup.empty() && !ContainsCI(JsonStrField(h, "group"), fGroup)) continue; if (!fOs.empty() && !ContainsCI(JsonStrField(h, "os"), fOs)) continue; hosts.append(h); } } int count = (int)hosts.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["hosts"] = hosts; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 台主机匹配。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:get_host_detail std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { std::string sid = GetStringArg(args, "id"); if (sid.empty()) { return BuildError(id, -32602, "Missing required parameter: id"); } if (!IsDigits(sid)) { return BuildError(id, -32602, "Invalid id: expected a decimal host id string"); } Json::Value all(Json::arrayValue); CollectOnlineHosts(parent, all); for (unsigned int i = 0; i < all.size(); ++i) { const Json::Value& h = all[i]; if (JsonStrField(h, "id") == sid) { Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["host"] = h; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; std::string name = JsonStrField(h, "name"); std::string ip = JsonStrField(h, "ip"); item["text"] = std::string(u8"主机 ") + name + " (" + ip + ")" + u8" 的详情。"; content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } } return BuildError(id, -32002, "Host not found or offline: " + sid); } // tools/call:list_processes(主连接 RPC,子连接一次性回传) std::string BuildListProcesses(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "list_processes")) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_SYSTEM; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for process list"); // 进程名/路径编码按 clientType 判定:Windows 走 A 接口(QueryFullProcessImageNameA 等) // =客户端 ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 的 /proc 天然 UTF-8。 // 不能用 GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 进程名。 CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936; Json::Value procs = ParseProcessList(data, cp); int count = (int)procs.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["processes"] = procs; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个进程。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:list_windows(主连接 RPC,子连接一次性回传) std::string BuildListWindows(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "list_windows")) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_WSLIST; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for window list"); // 窗口标题编码由客户端能力位决定(新客户端 UTF-8,老客户端 CP_ACP)。 UINT cp = GetClientEncoding(ctx); Json::Value wins = ParseWindowList(data, cp); int count = (int)wins.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["windows"] = wins; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个窗口。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:get_activity_history(主连接 RPC,无子链接、不弹框、不 CancelIO) std::string BuildGetActivityHistory(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "get_activity_history")) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_QUERY_ACTIVITY; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for activity history"); // data[0]=token,其后为客户端 ActivityHistory::Dump() 的 UTF-8 纯文本 // (标题在客户端已由 GetActiveWindowTitle 转 UTF-8,服务端无需再转码)。 std::string text; if (data.size() > 1) text.assign((const char*)data.data() + 1, data.size() - 1); // 拆行:每行一条记录,过滤空行(含 Dump 末尾的换行)。 Json::Value records(Json::arrayValue); size_t start = 0; while (start <= text.size()) { size_t nl = text.find('\n', start); std::string line = text.substr(start, (nl == std::string::npos ? text.size() : nl) - start); if (!line.empty()) records.append(line); if (nl == std::string::npos) break; start = nl + 1; } int count = (int)records.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["records"] = records; structuredContent["activityHistory"] = text; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 条历史活动记录。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:get_screenshot(屏幕预览链路,主连接 RPC,不建子链接、不弹框) std::string BuildGetScreenshot(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); // 能力位门槛:仅 Windows 客户端声明 CLIENT_CAP_SCREEN_PREVIEW(非 Windows 返回明确错误) if (!ctx->SupportsScreenPreview()) return BuildError(id, -32005, "Host does not support screen preview"); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "get_screenshot")) return BuildError(id, -32003, "Device busy: another request is pending for this host"); uint16_t reqId = mcp.NextPreviewReqId(); mcp.SetPendingReqId(devId, reqId); // 复用 MFC 预览的 RTT/FRP 自适应参数挑选,避免重复实现 GetTargetQualityLevel 逻辑 WORD maxWidth = 0; BYTE quality = 0; parent->ChooseScreenPreviewParams(ctx, maxWidth, quality); // 可选 max_width:缺省/0 沿用缩略图档位;>0 覆盖宽度(钳制到客户端上限 [64,1920]), // 供 AI 视觉/OCR 场景请求接近原分辨率的大图。jpegQuality 仍沿用档位自适应值。 if (args.isMember("max_width") && args["max_width"].isInt()) { int mw = args["max_width"].asInt(); if (mw > 0) { if (mw < 64) mw = 64; if (mw > 1920) mw = 1920; maxWidth = (WORD)mw; } } parent->SendScreenPreviewRequest(ctx, reqId, maxWidth, quality); std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for screenshot"); // data = [ScreenPreviewRspHeader][JPEG] if (data.size() < sizeof(ScreenPreviewRspHeader)) return BuildError(id, -32000, "Invalid screenshot response"); const ScreenPreviewRspHeader* hdr = reinterpret_cast(data.data()); if (hdr->status != SCREEN_PREVIEW_OK || hdr->format != SCREEN_PREVIEW_FMT_JPEG || hdr->bytes == 0 || data.size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes) { return BuildError(id, -32000, "Screenshot capture failed (status " + std::to_string((int)hdr->status) + ")"); } std::string b64 = httplib::detail::base64_encode( std::string((const char*)data.data() + sizeof(ScreenPreviewRspHeader), hdr->bytes)); Json::Value image(Json::objectValue); image["mimeType"] = "image/jpeg"; image["width"] = (Json::UInt64)hdr->width; image["height"] = (Json::UInt64)hdr->height; image["bytes"] = (Json::UInt64)hdr->bytes; Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["image"] = image; // 元数据(不含 base64,避免在 content 之外重复大 payload) result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "image"; item["data"] = b64; item["mimeType"] = "image/jpeg"; content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:list_files(列盘走 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST; // 列目录走 COMMAND_LIST_DRIVE 开子链接后,再下发 COMMAND_LIST_FILES→TOKEN_FILE_LIST) std::string BuildListFiles(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); std::string path = GetStringArg(args, "path"); bool listDrives = path.empty() || path == "." || path == "/" || path == "\\"; // 目录路径/文件名编码按 clientType 判定:Windows 走 A 接口(FindFirstFileA 等)= 客户端 // ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 文件系统天然 UTF-8。不能用 // GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 文件名。 CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936; // 下发前把 UTF-8 path 转成客户端 ANSI(空/./\/\\ 为 ASCII,转换后不变),OnDriveList 直接用。 std::string ansiPath = ToAnsi(path, cp); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "list_files", ansiPath)) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_LIST_DRIVE; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for file list"); Json::Value drives(Json::arrayValue); Json::Value files(Json::arrayValue); int count = 0; if (listDrives) { drives = ParseDriveList(data, cp); count = (int)drives.size(); } else { files = ParseFileList(data, 500, cp); // 上限 500,避免 JSON 响应过大 count = (int)files.size(); } Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); if (listDrives) structuredContent["drives"] = drives; else structuredContent["files"] = files; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = listDrives ? (std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个驱动器。")) : (std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个条目。")); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:list_services(主连接下发 COMMAND_SERVICES,子连接一次性回传 TOKEN_SERVERLIST) std::string BuildListServices(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); // 服务管理仅 Windows 客户端实现(EnumServicesStatus 等 A 接口);LNX/MAC 无 Windows // 服务概念,提前返回避免 20s 超时等待。 CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); if (clientType == "LNX" || clientType == "MAC") return BuildError(id, -32005, "list_services is only supported on Windows hosts"); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "list_services")) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_SERVICES; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for service list"); // 服务名/路径/显示名来自 Windows A 接口 = 客户端 ANSI(GBK/936),按 clientType 判定。 UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936; Json::Value services = ParseServiceList(data, cp); int count = (int)services.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["services"] = services; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个服务。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:list_registry(主连接下发 COMMAND_REGEDIT → 子连接 TOKEN_REGEDIT → 下发 // COMMAND_REG_FIND → TOKEN_REG_PATH + TOKEN_REG_KEY 两包;path 空 = 列根键,无需查询客户端) std::string BuildListRegistry(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); // 注册表仅 Windows 客户端实现(RegOpenKeyEx 等 A 接口);LNX/MAC 无注册表。 CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); if (clientType == "LNX" || clientType == "MAC") return BuildError(id, -32005, "list_registry is only supported on Windows hosts"); std::string path = GetStringArg(args, "path"); BYTE rootToken = 0; std::string relPath; if (!ParseRegistryPath(path, rootToken, relPath)) { if (path.empty()) return BuildRegistryRoots(id); // 空 path → 列根键 return BuildError(id, -32602, "Invalid path: expected a root key like HKEY_LOCAL_MACHINE\\Software"); } // 客户端 RegisterOperation 以 char KeyPath[MAX_PATH] 承载相对子键路径(SetPath→strcpy 无界), // 超长会栈溢出。按 UTF-8 字节数做保守上限(GBK/936 字节数 ≤ UTF-8,故只查 UTF-8 即可)。 if (relPath.size() >= MAX_PATH) return BuildError(id, -32602, "Registry path too long (exceeds MAX_PATH)"); // 注册表键路径为客户端 ANSI(GBK/936),下发前转好;rootToken 编码进 path[0] 供 OnRegeditReady 拆分。 UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936; std::string rootAndPath = std::string(1, (char)rootToken) + ToAnsi(relPath, cp); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "list_registry", rootAndPath)) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_REGEDIT; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector subkeys, values; if (!mcp.WaitPendingRegistry(devId, subkeys, values, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for registry data"); Json::Value keysArr = ParseRegPath(subkeys, cp); Json::Value valsArr = ParseRegKey(values, cp); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["path"] = path; structuredContent["keys"] = keysArr; structuredContent["values"] = valsArr; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = std::string(u8"共 ") + std::to_string((int)keysArr.size()) + std::string(u8" 个子键、") + std::to_string((int)valsArr.size()) + std::string(u8" 个值。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // ========== P3:exec_command ========== // exec_command 默认白名单:只读命令前缀(逗号分隔,前缀匹配,大小写不敏感)。 // 只放查询/只读类命令;写命令(del/copy/move/reg add/sc stop/...)一律不在列。 static const char* kDefaultCmdWhitelist = "dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat," "path,set,find,findstr,reg query,sc query," "ping,tracert,nslookup,getmac,driverquery,query,gpresult," "arp -a,route print,schtasks /query"; // 去首尾空白(空格/制表符/回车/换行)。 static std::string Trim(const std::string& s) { size_t b = s.find_first_not_of(" \t\r\n"); if (b == std::string::npos) return ""; return s.substr(b, s.find_last_not_of(" \t\r\n") - b + 1); } // 去尾部空白(含尾部 CR/LF)。 static std::string TrimRight(const std::string& s) { size_t e = s.find_last_not_of(" \t\r\n"); return (e == std::string::npos) ? "" : s.substr(0, e + 1); } // 命令白名单校验:命令(trim + ASCII 小写)以某白名单项为前缀,且其后字符为空/空格/制表符, // 防止 "dirx" 前缀误配 "dir"。多词前缀(sc query / reg query)也按完整前缀匹配。 static bool IsCommandAllowed(const std::string& command, const std::string& whitelist) { std::string c = ToLowerAscii(Trim(command)); if (c.empty()) return false; size_t start = 0; while (start < whitelist.size()) { size_t comma = whitelist.find(',', start); std::string item = Trim(whitelist.substr( start, comma == std::string::npos ? std::string::npos : comma - start)); if (!item.empty()) { std::string p = ToLowerAscii(item); if (c.size() >= p.size() && c.compare(0, p.size(), p) == 0 && (c.size() == p.size() || c[p.size()] == ' ' || c[p.size()] == '\t')) { return true; } } if (comma == std::string::npos) break; start = comma + 1; } return false; } // 剥 ANSI 转义序列(CSI/OSC/单字节 ESC),避免终端彩色码污染返回文本。 static std::string StripAnsi(const std::string& s) { std::string out; out.reserve(s.size()); for (size_t i = 0; i < s.size(); ++i) { if (s[i] != '\x1b') { out += s[i]; continue; } ++i; // 吞 ESC if (i < s.size() && s[i] == '[') { // CSI ... 结尾 @–~ while (++i < s.size() && !(s[i] >= '@' && s[i] <= '~')) {} } else if (i < s.size() && s[i] == ']') { // OSC ... 结尾 BEL/ESC while (++i < s.size() && s[i] != '\x07' && s[i] != '\x1b') {} } // 其它单字节 ESC 序列吞掉当前字节即可(循环 ++i 会跳过它) } return out; } // tools/call:exec_command(Windows 一次性远程命令:主连接 COMMAND_SHELL → 子连接终端 → // 哨兵命令行 → 收集 stdout + exit_code → 关子链接)。安全门:只读默认 + 白名单 + 审计。 std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); // 终端仅 Windows 客户端实现(ConPTY / 老 cmd 管道);LNX/MAC 远期再考虑。 CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); if (clientType == "LNX" || clientType == "MAC") return BuildError(id, -32005, "exec_command is only supported on Windows hosts"); std::string command = Trim(GetStringArg(args, "command")); if (command.empty()) return BuildError(id, -32602, "Missing required parameter: command"); CMcpServer& mcp = CMcpServer::Instance(); // 安全门 1:只读模式直接拒绝(默认只读,exec_command 需显式关只读才可用)。 if (mcp.IsReadonly()) return BuildError(id, -32006, "exec_command is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)"); // 安全门 2:拒绝 shell 元字符,防止 "dir && del ..." 之类绕过白名单的注入。 if (command.find_first_of("&|<>^") != std::string::npos) return BuildError(id, -32008, "Command contains forbidden shell characters: " + command); // 安全门 3:命令白名单前缀校验。 std::string whitelist = mcp.GetCmdWhitelist(); if (whitelist.empty()) whitelist = kDefaultCmdWhitelist; if (!IsCommandAllowed(command, whitelist)) return BuildError(id, -32007, "Command not allowed by whitelist: " + command); int timeoutMs = kMcpToolTimeoutMs; std::string t = GetStringArg(args, "timeout_ms"); if (!t.empty() && IsDigits(t)) { int v = atoi(t.c_str()); if (v > 0 && v <= 600000) timeoutMs = v; } std::string nonce = GenerateRandomToken().substr(0, 8); if (!mcp.BeginTermPending(devId, command, nonce)) return BuildError(id, -32003, "Device busy: a terminal session is already active for this host"); // 主连接下发 COMMAND_SHELL,客户端建立 shell 子连接并回 TOKEN_*_START。 BYTE cmd = COMMAND_SHELL; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearTermPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } context* subCtx = nullptr; bool isPty = false; if (!mcp.WaitTerminalReady(devId, subCtx, isPty, timeoutMs)) return BuildError(id, -32001, "Timeout waiting for shell to start"); // 编码:ConPTY(UTF-8) / 老 cmd 管道(GBK)。哨兵为纯 ASCII,两种编码下字节一致。 UINT cp = isPty ? CP_UTF8 : 936; // 哨兵命令行:@echo off 仅 ConPTY 需要(抑制回显噪声);老 ShellManager 已自行跳过回显, // 加 @echo off 反而破坏其回显跳过逻辑。&&/|| 取命令真实退出码(%errorlevel% 在复合句中 // 是解析期展开、已过期,故用控制操作符)。哨兵为随机串,命令输出不可能恰好包含。 std::string line; if (isPty) line += "@echo off & "; line += command; line += " 2>&1 && echo __MCP_DONE_" + nonce + "__0 || echo __MCP_DONE_" + nonce + "__1"; std::string wireLine = ToAnsi(line, cp) + "\r\n"; subCtx->Send2Client((BYTE*)wireLine.data(), (ULONG)wireLine.size()); std::vector raw; int exitCode = -1; bool closed = false; if (!mcp.WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)) { subCtx->CancelIO(); return BuildError(id, -32001, "Timeout waiting for command output"); } subCtx->CancelIO(); // 关子链接,结束 shell 进程 // 清洗:raw → UTF-8 → 剥 ANSI → CRLF 归一。raw 已截断到哨兵前。 std::string stdoutStr; if (!raw.empty()) { std::string rawStr((const char*)raw.data(), raw.size()); stdoutStr = StripAnsi(ToUtf8(rawStr.c_str(), cp)); size_t p = 0; while ((p = stdoutStr.find("\r\n", p)) != std::string::npos) stdoutStr.erase(p, 1); stdoutStr = TrimRight(stdoutStr); } // 审计:命令执行落服务端消息/审计日志(不可关闭)。 if (parent) { std::string text = "host " + std::to_string(devId) + " exec: " + command; parent->PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(ToAnsi(text, 936).c_str()), (LPARAM)new CString(_TR("MCP命令执行"))); // 标题走语言映射(GBK,随语言切换) } Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["stdout"] = stdoutStr; structuredContent["exit_code"] = exitCode; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = stdoutStr.empty() ? std::string(u8"(无输出)") : stdoutStr; content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:get_client_log(主连接下发 COMMAND_QUERY_LOG,子连接回传 TOKEN_REPORT_LOG) std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { uint64_t devId = 0; std::string err; if (!ParseHostIdArg(args, devId, err)) return BuildError(id, -32602, err); context* ctx = FindMainContext(parent, devId); if (!ctx) return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); CMcpServer& mcp = CMcpServer::Instance(); if (!mcp.BeginPending(devId, "get_client_log")) return BuildError(id, -32003, "Device busy: another request is pending for this host"); BYTE cmd = COMMAND_QUERY_LOG; if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return BuildError(id, -32004, "Failed to send command to host"); } std::vector data; if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return BuildError(id, -32001, "Timeout waiting for client log"); // data[0]=token,其后为 Logger 内存 ring buffer 的日志文本(无 '\0' 终止)。 // 客户端在子连接建立时 m_sentIdx=0 返回全量,随后每 3s 推增量;MessageHandle 在 // 取走首条全量后立即 CancelIO 关子链接,增量不再到达,故此处即为完整快照。 // 日志文本为客户端 ANSI(Windows 走 vsnprintf A 版),按 clientType 判定编码。 CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936; std::string log; if (data.size() > 1) { std::string raw((const char*)data.data() + 1, data.size() - 1); log = ToUtf8(raw.c_str(), cp); } Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["log"] = log; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = log.empty() ? std::string(u8"客户端暂无内存日志。") : std::string(u8"客户端运行日志快照如下。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call:get_audit_log(服务端本地消息日志,读取 UI 日志列表的线程安全镜像) std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) { Json::Value entries(Json::arrayValue); if (parent) { EnterCriticalSection(&parent->m_cs); // m_MessageLog 新在前、旧在后(与界面一致),直接顺序输出。 for (const auto& e : parent->m_MessageLog) { Json::Value item(Json::objectValue); item["type"] = ToUtf8(e.type.c_str(), 936); item["time"] = ToUtf8(e.time.c_str(), 936); item["msg"] = ToUtf8(e.msg.c_str(), 936); entries.append(item); } LeaveCriticalSection(&parent->m_cs); } int count = (int)entries.size(); Json::Value result(Json::objectValue); Json::Value structuredContent(Json::objectValue); structuredContent["entries"] = entries; result["structuredContent"] = structuredContent; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = count > 0 ? std::string(u8"共 ") + std::to_string(count) + std::string(u8" 条日志。") : std::string(u8"当前无日志。"); content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } // tools/call 分派 std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { const Json::Value& id = root["id"]; Json::Value params = root.isMember("params") ? root["params"] : Json::Value(Json::objectValue); std::string toolName; if (params.isObject() && params.isMember("name") && params["name"].isString()) { toolName = params["name"].asString(); } const Json::Value args = GetCallArguments(params); if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent); if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent); if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent); if (toolName == "list_processes") return BuildListProcesses(id, args, parent); if (toolName == "list_windows") return BuildListWindows(id, args, parent); if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent); if (toolName == "get_screenshot") return BuildGetScreenshot(id, args, parent); if (toolName == "list_files") return BuildListFiles(id, args, parent); if (toolName == "list_services") return BuildListServices(id, args, parent); if (toolName == "get_client_log") return BuildGetClientLog(id, args, parent); if (toolName == "get_audit_log") return BuildGetAuditLog(id, parent); if (toolName == "list_registry") return BuildListRegistry(id, args, parent); if (toolName == "exec_command") return BuildExecCommand(id, args, parent); return BuildError(id, -32602, "Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName)); } } // namespace // ===== P3:exec_command 哨兵检测与终端会话 ===== // 在原始 shell 输出字节流中定位哨兵(纯 ASCII,编码无关)。真实哨兵由 echo 单独输出、独占 // 一行(前面是 \n 或缓冲区开头);而 ConPTY 会把整条命令行回显进输出流,哨兵字样嵌在 // "&& echo ... || echo ..." 里、前面是空格。据此从后往前找「行首命中」,排除回显行误判: // 否则回显包先于命令输出单独到达时,rfind 会命中回显里的 __1 提前结束、截断真实输出。 // 老 ShellManager 已自行跳过回显,输出里只有真实哨兵(仍为行首)。命中 __0 → exit 0;__1 → exit 1。 static bool FindSentinel(const std::vector& buf, const std::string& nonce, size_t& pos, int& exitCode) { std::string marker = "__MCP_DONE_" + nonce + "__"; if (marker.size() + 1 > buf.size()) return false; std::string s((const char*)buf.data(), buf.size()); size_t from = std::string::npos; while (true) { size_t p = s.rfind(marker, from); if (p == std::string::npos) return false; bool lineStart = (p == 0) || (s[p - 1] == '\n'); size_t digitPos = p + marker.size(); if (lineStart && digitPos < s.size() && (s[digitPos] == '0' || s[digitPos] == '1')) { exitCode = (s[digitPos] == '0') ? 0 : 1; pos = p; return true; } if (p == 0) return false; from = p - 1; } } bool CMcpServer::IsTermPending(uint64_t device_id) { std::lock_guard lk(m_TermMutex); auto it = m_TermSessions.find(device_id); return it != m_TermSessions.end() && !it->second.started; } void CMcpServer::RegisterTerminalContext(uint64_t device_id, context* subCtx, bool isPty) { { std::lock_guard lk(m_TermMutex); auto it = m_TermSessions.find(device_id); if (it == m_TermSessions.end() || it->second.started) return; // 没有等它的会话 TermSession& s = it->second; s.started = true; s.subCtx = subCtx; s.isPty = isPty; m_TermContextToDevice[subCtx] = device_id; } // 关键步骤:告知客户端「启动 shell 输出回流」。客户端读线程靠 COMMAND_NEXT 才启动, // 漏发会导致 shell 在跑但输出永不送回。PTY 还要先告知初始 80x24,否则 TUI 尺寸错乱。 if (isPty) { BYTE resizeBuf[5]; resizeBuf[0] = CMD_TERMINAL_RESIZE; *(short*)(resizeBuf + 1) = (short)80; *(short*)(resizeBuf + 3) = (short)24; subCtx->Send2Client(resizeBuf, 5); } BYTE startCmd = COMMAND_NEXT; subCtx->Send2Client(&startCmd, 1); m_TermCv.notify_all(); } bool CMcpServer::IsTerminalContext(context* subCtx) { std::lock_guard lk(m_TermMutex); return m_TermContextToDevice.find(subCtx) != m_TermContextToDevice.end(); } void CMcpServer::OnTerminalData(context* subCtx, const BYTE* data, ULONG len) { std::lock_guard lk(m_TermMutex); auto it = m_TermContextToDevice.find(subCtx); if (it == m_TermContextToDevice.end()) return; auto sit = m_TermSessions.find(it->second); if (sit == m_TermSessions.end()) return; TermSession& s = sit->second; if (s.done || s.closed) return; // 已结束,忽略迟到数据 s.data.insert(s.data.end(), data, data + len); if (FindSentinel(s.data, s.nonce, s.sentPos, s.exitCode)) { s.done = true; m_TermCv.notify_all(); } } void CMcpServer::OnTerminalClosed(context* subCtx) { std::lock_guard lk(m_TermMutex); auto it = m_TermContextToDevice.find(subCtx); if (it == m_TermContextToDevice.end()) return; auto sit = m_TermSessions.find(it->second); if (sit == m_TermSessions.end()) return; sit->second.closed = true; m_TermCv.notify_all(); } bool CMcpServer::BeginTermPending(uint64_t device_id, const std::string& command, const std::string& nonce) { std::lock_guard lk(m_TermMutex); if (m_TermSessions.find(device_id) != m_TermSessions.end()) return false; // 已有会话 TermSession s; s.command = command; s.nonce = nonce; m_TermSessions[device_id] = std::move(s); return true; } bool CMcpServer::WaitTerminalReady(uint64_t device_id, context*& subCtx, bool& isPty, int timeoutMs) { std::unique_lock lk(m_TermMutex); auto it = m_TermSessions.find(device_id); if (it == m_TermSessions.end()) return false; bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs), [&] { return it->second.started; }); if (!signaled) { m_TermSessions.erase(it); // 超时 → 清理 return false; } subCtx = it->second.subCtx; isPty = it->second.isPty; return true; } bool CMcpServer::WaitTerminalDone(uint64_t device_id, std::vector& out, int& exitCode, bool& closed, int timeoutMs) { std::unique_lock lk(m_TermMutex); auto it = m_TermSessions.find(device_id); if (it == m_TermSessions.end()) return false; bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs), [&] { return it->second.done || it->second.closed; }); if (!signaled) { // 超时 → 清理(含路由表) m_TermContextToDevice.erase(it->second.subCtx); m_TermSessions.erase(it); return false; } TermSession s = it->second; // 拷贝出,避免擦除后悬空 m_TermContextToDevice.erase(s.subCtx); m_TermSessions.erase(it); closed = s.closed; exitCode = s.exitCode; if (s.done && s.sentPos <= s.data.size()) out.assign(s.data.begin(), s.data.begin() + s.sentPos); // 截断到哨兵前 else out = s.data; // 进程退出无哨兵:返回已收集的原始输出 return true; } void CMcpServer::ClearTermPending(uint64_t device_id) { std::lock_guard lk(m_TermMutex); auto it = m_TermSessions.find(device_id); if (it != m_TermSessions.end()) m_TermSessions.erase(it); } ////////////////////////////////////////////////////////////////////////// // CMcpServer Implementation ////////////////////////////////////////////////////////////////////////// CMcpServer& CMcpServer::Instance() { static CMcpServer instance; return instance; } CMcpServer::CMcpServer() { m_server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) { HandleMcp(req, res); }); } CMcpServer::~CMcpServer() { Stop(); // 兜底:确保监听线程 join,避免 std::thread 析构触发 terminate } bool CMcpServer::Start(const std::string& bind, int port) { if (m_running.load()) return true; // 已在运行 m_thread = std::thread([this, bind, port]() { m_server.listen(bind, port); }); // 给 listen 一点时间绑定端口;httplib::Server::is_running() 在 listen 内部置位。 std::this_thread::sleep_for(std::chrono::milliseconds(100)); m_running.store(m_server.is_running()); return m_running.load(); } void CMcpServer::Stop() { m_server.stop(); if (m_thread.joinable()) { m_thread.join(); } m_running.store(false); } void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res) { res.set_header("Content-Type", "application/json"); // 静态 token 校验:Authorization: Bearer (Start 前经 SetToken 保证非空) if (req.get_header_value("Authorization") != ("Bearer " + m_token)) { res.status = 401; res.set_content(BuildError(Json::nullValue, -32000, "Unauthorized"), "application/json"); return; } // 解析 JSON-RPC 请求体 Json::Value root; Json::CharReaderBuilder rbuilder; std::string errs; std::istringstream iss(req.body); if (!Json::parseFromStream(rbuilder, iss, &root, &errs) || !root.isObject()) { res.set_content(BuildError(Json::nullValue, -32700, "Parse error"), "application/json"); return; } // 通知(无 id)→ 不返回 JSON-RPC 响应(如 notifications/initialized) if (!root.isMember("id")) { res.status = 202; res.set_content("", "application/json"); return; } // 结构校验:缺 method if (!root.isMember("method") || !root["method"].isString()) { res.set_content(BuildError(root["id"], -32600, "Invalid Request"), "application/json"); return; } std::string method = root["method"].asString(); if (method == "initialize") { res.set_content(BuildInitializeResult(root["id"]), "application/json"); return; } if (method == "ping") { res.set_content(BuildPingResult(root["id"]), "application/json"); return; } if (method == "tools/list") { res.set_content(BuildToolsListResult(root["id"]), "application/json"); return; } if (method == "tools/call") { res.set_content(BuildToolsCall(root, m_parent), "application/json"); return; } // 未实现的方法 res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json"); } // ===== P2b 挂起请求注册表实现 ===== bool CMcpServer::IsPending(uint64_t device_id) { std::lock_guard lk(m_PendingMutex); return m_Pending.find(device_id) != m_Pending.end(); } void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃 it->second.data.assign(data, data + len); it->second.done = true; m_PendingCv.notify_one(); } bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool) { std::lock_guard lk(m_PendingMutex); if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙 PendingRequest r; r.tool = tool; m_Pending[device_id] = std::move(r); return true; } bool CMcpServer::WaitPending(uint64_t device_id, std::vector& out, int timeoutMs) { std::unique_lock lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return false; bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs), [&] { return it->second.done; }); if (!signaled || it->second.data.empty()) { m_Pending.erase(it); // 超时/空数据 → 清理 return false; } out = std::move(it->second.data); m_Pending.erase(it); return true; } void CMcpServer::ClearPending(uint64_t device_id) { std::lock_guard lk(m_PendingMutex); m_Pending.erase(device_id); } // ===== P2c:list_files / get_screenshot 扩展 ===== bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool, const std::string& path) { std::lock_guard lk(m_PendingMutex); if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙 PendingRequest r; r.tool = tool; r.path = path; m_Pending[device_id] = std::move(r); return true; } uint16_t CMcpServer::NextPreviewReqId() { uint16_t v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed); if (v == 0) v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed); // 跳过 0 return v; } void CMcpServer::SetPendingReqId(uint64_t device_id, uint16_t reqId) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it != m_Pending.end()) it->second.expectedReqId = reqId; } bool CMcpServer::TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return false; // 已超时清理 → 迟到数据,回落 MFC if (it->second.tool != "get_screenshot") return false; if (it->second.expectedReqId == 0 || it->second.expectedReqId != reqId) return false; // 过期/他途响应 it->second.data.assign(data, data + len); it->second.done = true; m_PendingCv.notify_one(); return true; } bool CMcpServer::OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len) { std::string path; bool listDrives = true; { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return true; // 已超时清理 → 调用方 CancelIO 收尾 path = it->second.path; listDrives = path.empty() || path == "." || path == "/" || path == "\\"; if (listDrives) { it->second.data.assign(buf, buf + len); it->second.done = true; m_PendingCv.notify_one(); return true; // 只列盘 → 调用方用完即关 } } // 列目录:锁外下发 COMMAND_LIST_FILES + path,子链接保持,等 TOKEN_FILE_LIST。 // path 已在 BuildListFiles 按客户端 ANSI 转好(OnDriveList 无需再转),结尾 '\0' // 与 FileManagerDlg 的 PacketSize=len+2 一致。 std::vector pkt; pkt.reserve(1 + path.size() + 1); pkt.push_back((BYTE)COMMAND_LIST_FILES); pkt.insert(pkt.end(), path.begin(), path.end()); pkt.push_back(0); subCtx->Send2Client(pkt.data(), (ULONG)pkt.size()); return false; // 继续等 TOKEN_FILE_LIST,调用方不 CancelIO } // ===== P3:list_registry 扩展 ===== bool CMcpServer::OnRegeditReady(uint64_t device_id, context* subCtx) { std::string rootAndPath; { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return false; // 无挂起 → 回落 MFC if (it->second.tool != "list_registry") return false; rootAndPath = it->second.path; // [rootToken:1][相对子键路径...] } if (rootAndPath.empty()) return false; // 不应发生:list_registry 必有 rootToken BYTE rootToken = (BYTE)rootAndPath[0]; std::string relPath = rootAndPath.substr(1); // 下发 COMMAND_REG_FIND:布局与 RegisterDlg::OnTvnSelchangedTree 一致 // [COMMAND_REG_FIND][rootToken][relPath...]['\0']。子链接保持,等 TOKEN_REG_PATH + KEY。 std::vector pkt; pkt.reserve(2 + relPath.size() + 1); pkt.push_back((BYTE)COMMAND_REG_FIND); pkt.push_back(rootToken); pkt.insert(pkt.end(), relPath.begin(), relPath.end()); pkt.push_back(0); subCtx->Send2Client(pkt.data(), (ULONG)pkt.size()); return true; // 接管子链接,不打开 MFC 对话框 } void CMcpServer::TakeRegPath(uint64_t device_id, const BYTE* data, ULONG len) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据丢弃 if (it->second.tool != "list_registry") return; it->second.regPath.assign(data, data + len); it->second.pathDone = true; if (it->second.keyDone) { it->second.done = true; m_PendingCv.notify_one(); } } void CMcpServer::TakeRegKey(uint64_t device_id, const BYTE* data, ULONG len) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return; if (it->second.tool != "list_registry") return; it->second.regKey.assign(data, data + len); it->second.keyDone = true; if (it->second.pathDone) { it->second.done = true; m_PendingCv.notify_one(); } } bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector& subkeys, std::vector& values, int timeoutMs) { std::unique_lock lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return false; bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs), [&] { return it->second.done; }); if (!signaled) { m_Pending.erase(it); // 超时 → 清理 return false; } subkeys = std::move(it->second.regPath); values = std::move(it->second.regKey); m_Pending.erase(it); return true; } // rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 前 // 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。 extern "C" int __cdecl rand_s(unsigned int* randomValue); std::string GenerateRandomToken() { static const char hex[] = "0123456789abcdef"; std::string out; out.reserve(32); for (int i = 0; i < 16; ++i) { unsigned int v = 0; if (rand_s(&v) != 0) { // rand_s 失败(罕见):退化为时间 + 地址熵,保证仍返回非空 token。 v = (unsigned int)(GetTickCount() ^ (ULONG_PTR)&out); } out.push_back(hex[(v >> 4) & 0xF]); out.push_back(hex[v & 0xF]); } return out; }