Feature: Add MCP remote_open/remote_close remote control sessions

Add M1 of MCP remote control (docs/Mcp_RemoteControl_Design.md): the
remote_open / remote_close tools plus the ScreenCtrlSession state machine.

remote_open establishes a hidden screen sub-connection by reusing
WebService::StartRemoteDesktop (COMMAND_SCREEN_SPY -> CScreenSpyDlg ->
RegisterScreenContext), polls for the sub-connection plus its physical
resolution (TOKEN_BITMAPINFO -> NotifyResolutionChange -> GetScreenSize),
then records the session (single device, single session, reverse-mapped
subCtx for OfflineProc cleanup) and returns {session_id, screen_w,
screen_h}. remote_close validates session_id and tears down the
sub-connection idempotently. A McpRemoteControl settings checkbox (default
off, requires McpReadonly=0) gates the tools; every open/close is audited
via WM_SHOWERRORMSG.

Gating: multi-monitor hosts are rejected with -32008 (phase 1 supports only
single monitor, where Observe=main screen and Act=virtual desktop coincide);
the monitor count comes from the client heartbeat RES_RESOLUTION ("N:W*H").

Known limitations (deferred to the injection milestones): mutual exclusion
with human remote-desktop viewing is one-directional in M1 (a human who
joins during an MCP session can tear it down on disconnect), and subCtx is
not yet dereferenced so no liveness re-check is needed until
remote_mouse/remote_keyboard.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-25 12:55:52 +02:00
parent 84e3565de1
commit 6045baadb8
7 changed files with 469 additions and 4 deletions

View File

@@ -4,6 +4,8 @@
#include "HostJson.h" // BuildHostJson单台主机序列化公共函数
#include "context.h" // context 接口
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员m_HostList/m_cs/m_ClientMap+ VERSION_STR
#include "WebService.h" // 远程控制复用屏幕子连接StartRemoteDesktop / GetScreenContext / GetScreenSize
#include "Server.h" // CONTEXT_OBJECT 定义GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
#include "LangManager.h" // _TR审计日志标题语言映射
#include <sstream>
@@ -1125,6 +1127,14 @@ std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CM
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// ===== P5 前置声明定义见下方「tools/call 分派」前)=====
Json::Value BuildRemoteOpenInputSchema();
Json::Value BuildRemoteOpenOutputSchema();
Json::Value BuildRemoteCloseInputSchema();
Json::Value BuildRemoteCloseOutputSchema();
std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
@@ -1328,6 +1338,27 @@ std::string BuildToolsListResult(const Json::Value& id) {
}
}
// 15) remote_open / remote_closeP5MCP 远程控制,仅 Windows
// 安全门McpRemoteControl=1 且 McpReadonly=0全程审计 + idle 回收)
if (CMcpServer::Instance().IsRemoteControlEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "remote_open";
tool["description"] = u8"在指定在线 Windows 主机上建立远程控制会话(隐藏屏幕子连接)并返回 session_id 与物理屏幕分辨率screen_w/screen_h。后续 remote_mouse/remote_keyboard 用归一化坐标0..1)注入输入,用毕须 remote_close。";
tool["inputSchema"] = BuildRemoteOpenInputSchema();
tool["outputSchema"] = BuildRemoteOpenOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "remote_close";
tool["description"] = u8"关闭并释放指定远程控制会话(幂等:会话已不存在也返回成功)。";
tool["inputSchema"] = BuildRemoteCloseInputSchema();
tool["outputSchema"] = BuildRemoteCloseOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -2533,6 +2564,287 @@ std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, C
return BuildResult(id, result);
}
// ===== P5MCP 远程控制remote_open / remote_close=====
static const int kScreenCtrlIdleTimeoutSec = 300; // 远程控制 idle 回收超时(秒)
// 共享前置:校验 McpRemoteControl && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。
// 成功返回 true 并输出 devId/ctx失败时 errJson 已写入对应 JSON 错误串。
static bool ResolveScreenCtrlSessionHost(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent,
uint64_t& devId, context*& ctx, std::string& errJson) {
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly()) {
errJson = BuildError(id, -32006,
"Remote control is disabled: requires McpRemoteControl=1 and McpReadonly=0");
return false;
}
std::string err;
if (!ParseHostIdArg(args, devId, err)) {
errJson = BuildError(id, -32602, err);
return false;
}
ctx = FindMainContext(parent, devId);
if (!ctx) {
errJson = BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
return false;
}
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC") {
errJson = BuildError(id, -32005, "Remote control is only supported on Windows hosts");
return false;
}
return true;
}
// remote_open 的 inputSchemaid 必填、timeout_ms 可选)
Json::Value BuildRemoteOpenInputSchema() {
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 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");
schema["required"] = required;
return schema;
}
Json::Value BuildRemoteOpenOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"控制会话 token后续 remote_mouse / remote_keyboard / remote_close 用";
props["session_id"] = sid;
Json::Value sw(Json::objectValue);
sw["type"] = "integer";
sw["description"] = u8"物理屏幕宽度(虚拟桌面像素),用于把归一化坐标 0..1 映射为像素";
props["screen_w"] = sw;
Json::Value sh(Json::objectValue);
sh["type"] = "integer";
sh["description"] = u8"物理屏幕高度(虚拟桌面像素)";
props["screen_h"] = sh;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("session_id");
required.append("screen_w");
required.append("screen_h");
schema["required"] = required;
return schema;
}
Json::Value BuildRemoteCloseInputSchema() {
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 sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"remote_open 返回的 session_id";
props["session_id"] = sid;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
schema["required"] = required;
return schema;
}
Json::Value BuildRemoteCloseOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value closed(Json::objectValue);
closed["type"] = "boolean";
closed["description"] = u8"恒为 true幂等";
props["closed"] = closed;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("closed");
schema["required"] = required;
return schema;
}
// tools/callremote_open建立隐藏屏幕子连接 + 取物理分辨率)
std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveScreenCtrlSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
// 多显示器门§7.2 / §12.1Observe 抓主屏Act 注入在虚拟桌面空间,两者仅单显示器时
// 重合。客户端心跳 RES_RESOLUTION = "N:W*H"N=显示器数N>1 时归一化坐标会系统性
// 偏移,阶段一直接拒绝(-32008。无冒号老客户端格式则无法判定放行不拦。
{
CString res = ctx->GetAdditionalData(RES_RESOLUTION);
int colon = res.Find(':');
if (colon > 0) {
CString countStr = res.Left(colon);
int monitorCount = atoi(countStr);
if (monitorCount > 1)
return BuildError(id, -32008,
"Multi-monitor hosts are not supported yet (phase 1): monitor count " +
std::to_string(monitorCount));
}
}
// 远程控制复用 Web 的隐藏屏幕子连接COMMAND_SCREEN_SPY → CScreenSpyDlg → RegisterScreenContext
// 该路径依赖 Web 远程服务已启动WebSvrPort>0默认开启未启动时子连接无法建立/注册。
if (!WebService().IsRunning())
return BuildError(id, -32005,
"Remote control requires the Web remote service (WebSvrPort) to be enabled");
// 与人类远程桌面观看互斥已有屏幕子连接web 观看)→ 拒绝,避免向人类正在看的画面注入。
if (WebService().HasActiveSession(devId))
return BuildError(id, -32003, "Device busy: a remote desktop session is already active for this host");
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 sessionId = GenerateRandomToken();
if (!mcp.BeginScreenCtrlOpen(devId, sessionId))
return BuildError(id, -32003, "Device busy: a remote control session is already active for this host");
// 建立隐藏屏幕子连接(发 COMMAND_SCREEN_SPY、标记 web-triggered
if (!WebService().StartRemoteDesktop(devId)) {
mcp.CloseScreenCtrlSession(devId, sessionId);
// StartRemoteDesktop 先置 web-triggered 再 Send2Client发送失败需一并清理
// 否则该设备残留 web-triggered 标记、后续人类 web 观看会误走隐藏会话路径。
WebService().ClearWebTriggered(devId);
return BuildError(id, -32004, "Failed to start remote desktop session");
}
// 轮询等待子连接就绪HasActiveSession+ 分辨率到达TOKEN_BITMAPINFO → NotifyResolutionChange
// 屏幕子连接的注册/分辨率由 CScreenSpyDlg 完成,无 McpServer 回调故用轮询open 低频)。
int screenW = 0, screenH = 0;
context* subCtx = nullptr;
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
bool ready = false;
while (std::chrono::steady_clock::now() < deadline) {
if (WebService().HasActiveSession(devId) && WebService().GetScreenSize(devId, screenW, screenH)) {
subCtx = WebService().GetScreenContext(devId);
ready = (subCtx != nullptr);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
if (!ready) {
mcp.CloseScreenCtrlSession(devId, sessionId);
if (parent) parent->CloseWebRemoteDesktopByClientID(devId);
WebService().ClearWebTriggered(devId);
return BuildError(id, -32001, "Timeout waiting for screen sub-connection / resolution");
}
if (!mcp.MarkScreenCtrlReady(devId, sessionId, subCtx, screenW, screenH)) {
// 并发 remote_close 已关掉会话:放弃本次 open子连接已由 close 关闭)。
return BuildError(id, -32002, "Remote control session was closed concurrently: " + sessionId);
}
// 审计:打开会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " remote-open: session " + sessionId
+ " (" + std::to_string(screenW) + "x" + std::to_string(screenH) + ")";
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP远程控制")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["session_id"] = sessionId;
structuredContent["screen_w"] = screenW;
structuredContent["screen_h"] = screenH;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = sessionId;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callremote_close关闭控制会话幂等session_id 不匹配报错)
std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly())
return BuildError(id, -32006, "Remote control is disabled: requires McpRemoteControl=1 and McpReadonly=0");
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
int r = mcp.CloseScreenCtrlSession(devId, sessionId);
if (r == 2)
return BuildError(id, -32002, "Remote control session_id mismatch: " + sessionId);
// 仅当确实关闭了 MCP 会话r==0时才关闭隐藏屏幕子连接r==1幂等会话已不存在
// 如已被 idle 回收)时不得误关人类正在观看的 web 会话CloseWebRemoteDesktopByClientID
// 会关掉该设备任一 IsWebSession 的对话框)。
if (r == 0) {
// 关闭隐藏屏幕子连接幂等WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext。
if (parent) parent->CloseWebRemoteDesktopByClientID(devId);
WebService().ClearWebTriggered(devId);
}
// 审计:关闭会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " remote-close: session " + sessionId;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP远程控制")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["closed"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"closed");
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"];
@@ -2561,6 +2873,8 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "terminal_open") return BuildTerminalOpen(id, args, parent);
if (toolName == "terminal_exec") return BuildTerminalExec(id, args, parent);
if (toolName == "terminal_close") return BuildTerminalClose(id, args, parent);
if (toolName == "remote_open") return BuildRemoteOpen(id, args, parent);
if (toolName == "remote_close") return BuildRemoteClose(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -2842,6 +3156,80 @@ int CMcpServer::SweepIdleTerminals(time_t idleTimeoutSec) {
return (int)toCancel.size();
}
// ===== P5MCP 远程控制(状态机方法)=====
bool CMcpServer::IsScreenCtrlContext(context* subCtx) {
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
return m_ScreenCtrlContextToDevice.find(subCtx) != m_ScreenCtrlContextToDevice.end();
}
bool CMcpServer::BeginScreenCtrlOpen(uint64_t device_id, const std::string& sessionId) {
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
if (m_ScreenCtrlSessions.find(device_id) != m_ScreenCtrlSessions.end()) return false; // 单设备单会话
ScreenCtrlSession s;
s.sessionId = sessionId;
s.lastActiveAt = time(nullptr);
m_ScreenCtrlSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::MarkScreenCtrlReady(uint64_t device_id, const std::string& sessionId,
context* subCtx, int screenW, int screenH) {
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
auto it = m_ScreenCtrlSessions.find(device_id);
if (it == m_ScreenCtrlSessions.end() || it->second.sessionId != sessionId) return false;
ScreenCtrlSession& s = it->second;
s.subCtx = subCtx;
s.started = true;
s.screenW = screenW;
s.screenH = screenH;
s.lastActiveAt = time(nullptr);
m_ScreenCtrlContextToDevice[subCtx] = device_id;
return true;
}
int CMcpServer::CloseScreenCtrlSession(uint64_t device_id, const std::string& sessionId) {
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
auto it = m_ScreenCtrlSessions.find(device_id);
if (it == m_ScreenCtrlSessions.end()) return 1; // 不存在(幂等)
if (it->second.sessionId != sessionId) return 2; // token 不匹配
if (it->second.subCtx) m_ScreenCtrlContextToDevice.erase(it->second.subCtx);
m_ScreenCtrlSessions.erase(it);
return 0;
}
int CMcpServer::SweepIdleScreenCtrl(time_t idleTimeoutSec) {
time_t now = time(nullptr);
std::vector<uint64_t> toClose;
{
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
for (auto it = m_ScreenCtrlSessions.begin(); it != m_ScreenCtrlSessions.end(); ) {
ScreenCtrlSession& s = it->second;
if (difftime(now, s.lastActiveAt) > (double)idleTimeoutSec) {
if (s.subCtx) m_ScreenCtrlContextToDevice.erase(s.subCtx);
toClose.push_back(it->first);
it = m_ScreenCtrlSessions.erase(it);
} else {
++it;
}
}
}
// 锁外关闭隐藏对话框WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext
for (uint64_t devId : toClose) {
if (m_parent) m_parent->CloseWebRemoteDesktopByClientID(devId);
WebService().ClearWebTriggered(devId);
}
return (int)toClose.size();
}
void CMcpServer::OnScreenControlClosed(context* subCtx) {
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
auto it = m_ScreenCtrlContextToDevice.find(subCtx);
if (it == m_ScreenCtrlContextToDevice.end()) return;
m_ScreenCtrlSessions.erase(it->second);
m_ScreenCtrlContextToDevice.erase(it);
}
//////////////////////////////////////////////////////////////////////////
// CMcpServer Implementation
//////////////////////////////////////////////////////////////////////////