Feature: Add list_services and get_client_log MCP tools

Add the two P2d read-only MCP tools, both one-shot sub-links (mode A') on
the existing single-flight pending registry.

list_services sends COMMAND_SERVICES; the client's CServicesManager emits
TOKEN_SERVERLIST on sub-link creation, parsed as 5 null-terminated fields per
record (display_name/service_name/binary_path/status/start_type) with
zero-padding termination. Services are Windows-only, so LNX/MAC hosts get
-32005 up front instead of a 20s timeout.

get_client_log sends COMMAND_QUERY_LOG; the client's CClientLogManager dumps
its full in-memory Logger ring buffer once, then pushes deltas every 3s. The
MCP path takes the first (full) TOKEN_REPORT_LOG and cancels the sub-link so
later deltas stop, while the MFC log dialog keeps receiving deltas on the
non-pending branch. Log text is client ANSI on Windows, decoded by clientType
like process/file names. MessageHandle intercepts both with
IsPending -> TakeMainResponse + CancelIO. Sync the design doc (P2d section +
verification notes; Linux get_client_log timeout is a client-version gap).

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-19 14:28:10 +02:00
parent c6c6e1d5ef
commit 5611ba621c
3 changed files with 282 additions and 4 deletions

View File

@@ -5467,6 +5467,15 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
// 【x】对话框相关功能
switch (cmd) {
case TOKEN_REPORT_LOG: {
// P3MCP 挂起时接管。客户端子连接建立即回全量日志、随后每 3s 推增量MCP
// 只取首条全量,取走即 CancelIO 关子链接,停止后续增量推送(与 MFC 日志对话框
// 持续接收增量的路径区分开)。
uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsPending(devId)) {
McpServer().TakeMainResponse(devId, szBuffer, len);
ContextObject->CancelIO();
break;
}
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
if (!ContextObject->hDlg) {
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
@@ -6411,6 +6420,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
break;
}
case TOKEN_SERVERLIST: { // 服务管理【x】
// P3MCP 挂起时接管。服务列表为一次性子链接回传CServicesManager 构造即发),
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsPending(devId)) {
McpServer().TakeMainResponse(devId, szBuffer, len);
ContextObject->CancelIO();
break;
}
g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);
break;
}

View File

@@ -366,6 +366,41 @@ Json::Value ParseFileList(const std::vector<BYTE>& data, int maxEntries, UINT cp
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<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; // 跳过 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;
}
// 收集所有在线主机 JSON 数组m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
if (!parent) return;
@@ -559,6 +594,51 @@ Json::Value BuildActivityHistoryOutputSchema() {
return schema;
}
// list_services 的 outputSchemaservices 数组)
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;
}
// 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_screenshot 的 outputSchemaimage 元数据base64 数据在 content 的 image 块中)
Json::Value BuildScreenshotOutputSchema() {
Json::Value props(Json::objectValue);
@@ -793,6 +873,30 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool);
}
// 9) list_servicesP3服务链路仅 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_logP3客户端运行日志一次性子链接取首条全量
{
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);
}
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -1207,6 +1311,113 @@ std::string BuildListFiles(const Json::Value& id, const Json::Value& args, CMy20
return BuildResult(id, result);
}
// tools/calllist_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<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for service list");
// 服务名/路径/显示名来自 Windows A 接口 = 客户端 ANSIGBK/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/callget_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<BYTE> 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 关子链接,增量不再到达,故此处即为完整快照。
// 日志文本为客户端 ANSIWindows 走 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 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
@@ -1227,6 +1438,8 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* 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);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));