Feature: Add list_processes, list_windows and get_activity_history MCP tools

Add three read-only P2b MCP tools over the existing protocol. list_processes and list_windows trigger the client via COMMAND_SYSTEM / COMMAND_WSLIST on the main connection and receive TOKEN_PSLIST / TOKEN_WSLIST on a one-shot sub-link; get_activity_history uses the main-connection RPC COMMAND_QUERY_ACTIVITY -> TOKEN_REPORT_ACTIVITY. A per-host single-flight pending registry (m_Pending) with a 20s timeout correlates each response to its request and rejects a concurrent request for the same host with -32003. Parsers stop on the first empty record to ignore the client's LocalSize trailing zero padding, and window titles are decoded per the client UTF-8 capability bit. MessageHandle only adds if-guarded branches, so the MFC dialogs are untouched. Sync Mcp_Phase2_Design.md with the mode A'/A architecture, the verification notes, and the registry-backed config location.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-19 09:58:58 +02:00
parent 86c4f14cf4
commit 6ba6b0289d
4 changed files with 626 additions and 54 deletions

View File

@@ -23,6 +23,9 @@
namespace {
// P2b 工具等待响应的超时ms。MCP 一次性请求:等待子连接回传进程/窗口列表。
static const int kMcpToolTimeoutMs = 20000;
// Json::Value → 紧凑 JSON 字符串
std::string JsonToString(const Json::Value& v) {
Json::StreamWriterBuilder b;
@@ -111,6 +114,155 @@ 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;
}
// 有界 strlenp 最多可读 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;
}
// 从 "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] 记录)。
// 进程名/路径来自客户端 ANSICP_ACP转 UTF-8 输出。
Json::Value ParseProcessList(const std::vector<BYTE>& data) {
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_ACP);
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), CP_ACP);
item["path"] = ToUtf8(fullPath, CP_ACP);
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<BYTE>& 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;
}
// 收集所有在线主机 JSON 数组m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
if (!parent) return;
@@ -218,6 +370,92 @@ Json::Value BuildGetHostDetailInputSchema() {
return schema;
}
// list_processes 的 outputSchemaprocesses 数组)
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 的 outputSchemawindows 数组)
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 的 outputSchemarecords 数组 + 原始文本)
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;
}
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
@@ -266,6 +504,42 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool);
}
// 4) list_processesP2b主连接 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_windowsP2b主连接 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_historyP2b主连接 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);
}
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -375,6 +649,156 @@ std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, C
return BuildError(id, -32002, "Host not found or offline: " + sid);
}
// tools/calllist_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<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for process list");
Json::Value procs = ParseProcessList(data);
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/calllist_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<BYTE> 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/callget_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<BYTE> 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 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
@@ -390,6 +814,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
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);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -493,6 +920,52 @@ void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res)
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
}
// ===== P2b 挂起请求注册表实现 =====
bool CMcpServer::IsPending(uint64_t device_id) {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<BYTE>& out, int timeoutMs) {
std::unique_lock<std::mutex> 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<std::mutex> lk(m_PendingMutex);
m_Pending.erase(device_id);
}
// rand_sWindows CRT 加密安全随机源(基于系统 CSPRNG。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S为避免依赖 PCH 的包含顺序这里手动声明其导出原型errno_t == int
extern "C" int __cdecl rand_s(unsigned int* randomValue);