Feature: Add remote_mouse MCP tool

Add the remote_mouse tool to the MCP remote control surface, injecting
mouse events (move / down / up / click / right_click / middle_click /
drag / scroll) into an established remote_open session. Normalized 0..1
coordinates are mapped to physical pixels via the session's captured
resolution and clamped to screen bounds. Scroll follows the existing web
console's wheel sign convention (positive delta scrolls down) and is
vertical-only.

Extract BuildMouseMsg64 into WebService.h as a shared helper, reused by
both the web console's HandleMouse and the new MCP handler so the two
injection paths cannot drift.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-25 13:38:09 +02:00
parent f04d892ac8
commit 18259b8526
3 changed files with 321 additions and 31 deletions

View File

@@ -1134,9 +1134,12 @@ Json::Value BuildRemoteCloseInputSchema();
Json::Value BuildRemoteCloseOutputSchema();
Json::Value BuildRemoteKeyboardInputSchema();
Json::Value BuildRemoteKeyboardOutputSchema();
Json::Value BuildRemoteMouseInputSchema();
Json::Value BuildRemoteMouseOutputSchema();
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);
std::string BuildRemoteKeyboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteMouse(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
@@ -1368,6 +1371,14 @@ std::string BuildToolsListResult(const Json::Value& id) {
tool["outputSchema"] = BuildRemoteKeyboardOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "remote_mouse";
tool["description"] = u8"向已建立的远程控制会话注入鼠标事件。action 可选 move / down / up / click / right_click / middle_click / drag / scroll坐标 x/y及 drag 的 x2/y2为归一化 0..1 浮点。button 可选 left/middle/right默认 leftclick 的 clicks 可选 1/2/3默认 1scroll 的 delta 正=向下滚、负=向上滚。";
tool["inputSchema"] = BuildRemoteMouseInputSchema();
tool["outputSchema"] = BuildRemoteMouseOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools;
@@ -3113,6 +3124,290 @@ std::string BuildRemoteKeyboard(const Json::Value& id, const Json::Value& args,
return BuildResult(id, result);
}
// ===== P5remote_mouse鼠标注入move/down/up/click/right_click/middle_click/drag/scroll=====
// 读取归一化坐标0..1 浮点,接受 JSON number 或数字字符串),越界钳到 [0,1]。
static bool GetNormCoord(const Json::Value& args, const char* key, double& out, std::string& err) {
if (!args.isMember(key)) { err = std::string("Missing required parameter: ") + key; return false; }
const Json::Value& v = args[key];
if (v.isNumeric()) {
out = v.asDouble();
} else if (v.isString()) {
const std::string s = v.asString();
if (s.empty()) { err = std::string("Invalid ") + key + ": expected a number in 0..1"; return false; }
char* end = nullptr;
double d = strtod(s.c_str(), &end);
if (end == s.c_str() || *end != '\0') { err = std::string("Invalid ") + key + ": expected a number in 0..1"; return false; }
out = d;
} else {
err = std::string("Invalid ") + key + ": expected a number in 0..1";
return false;
}
if (out != out) { // NaN如字符串 "nan")会绕过钳制并令 (int)(n*screen) 未定义行为
err = std::string("Invalid ") + key + ": not a finite number";
return false;
}
if (out < 0.0) out = 0.0;
if (out > 1.0) out = 1.0;
return true;
}
// 读取可选整数参数JSON number 或数字字符串);缺失/非法返回 false不写 out
static bool GetIntArg(const Json::Value& args, const char* key, int& out) {
if (!args.isMember(key)) return false;
const Json::Value& v = args[key];
if (v.isNumeric()) { out = v.asInt(); return true; }
if (v.isString()) {
const std::string s = v.asString();
if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) {
out = atoi(s.c_str());
return true;
}
}
return false;
}
// button 字符串 → 0=左/1=中/2=右;空或 left 默认左。
static bool ParseButton(const std::string& s, int& out) {
if (s.empty() || s == "left") { out = 0; return true; }
if (s == "middle") { out = 1; return true; }
if (s == "right") { out = 2; return true; }
return false;
}
// button → 按下/抬起消息 + 按下 wParam对齐 WebService::HandleMouse
static void ButtonMessages(int button, UINT& downMsg, UINT& upMsg, uint64_t& downWParam) {
if (button == 1) { downMsg = WM_MBUTTONDOWN; upMsg = WM_MBUTTONUP; downWParam = MK_MBUTTON; }
else if (button == 2) { downMsg = WM_RBUTTONDOWN; upMsg = WM_RBUTTONUP; downWParam = MK_RBUTTON; }
else { downMsg = WM_LBUTTONDOWN; upMsg = WM_LBUTTONUP; downWParam = MK_LBUTTON; }
}
Json::Value BuildRemoteMouseInputSchema() {
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 act(Json::objectValue);
act["type"] = "string";
act["description"] = u8"动作move / down / up / click / right_click / middle_click / drag / scroll";
Json::Value actEnum(Json::arrayValue);
actEnum.append("move"); actEnum.append("down"); actEnum.append("up"); actEnum.append("click");
actEnum.append("right_click"); actEnum.append("middle_click"); actEnum.append("drag"); actEnum.append("scroll");
act["enum"] = actEnum;
props["action"] = act;
Json::Value xProp(Json::objectValue);
xProp["type"] = "number";
xProp["description"] = u8"归一化 X 坐标0=屏幕最左1=最右)";
props["x"] = xProp;
Json::Value yProp(Json::objectValue);
yProp["type"] = "number";
yProp["description"] = u8"归一化 Y 坐标0=屏幕最上1=最下)";
props["y"] = yProp;
Json::Value x2Prop(Json::objectValue);
x2Prop["type"] = "number";
x2Prop["description"] = u8"拖拽终点 X仅 drag";
props["x2"] = x2Prop;
Json::Value y2Prop(Json::objectValue);
y2Prop["type"] = "number";
y2Prop["description"] = u8"拖拽终点 Y仅 drag";
props["y2"] = y2Prop;
Json::Value btn(Json::objectValue);
btn["type"] = "string";
btn["description"] = u8"按键left / middle / right默认 left";
Json::Value btnEnum(Json::arrayValue);
btnEnum.append("left"); btnEnum.append("middle"); btnEnum.append("right");
btn["enum"] = btnEnum;
props["button"] = btn;
Json::Value clk(Json::objectValue);
clk["type"] = "integer";
clk["description"] = u8"点击次数 1/2/3仅 click默认 1";
props["clicks"] = clk;
Json::Value del(Json::objectValue);
del["type"] = "integer";
del["description"] = u8"滚动量(仅 scroll正=向下滚,负=向上滚,与 Web 控制台一致;仅垂直滚轮,忽略 button";
props["delta"] = del;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
required.append("action");
required.append("x");
required.append("y");
schema["required"] = required;
return schema;
}
Json::Value BuildRemoteMouseOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// tools/callremote_mouse鼠标注入归一化坐标 → 物理像素)
std::string BuildRemoteMouse(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;
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
std::string action = GetStringArg(args, "action");
if (action.empty())
return BuildError(id, -32602, "Missing required parameter: action");
// ---- 纯参数解析不依赖会话分辨率BeginScreenCtrlAction 之后再无提前返回)----
bool knownAction =
action == "move" || action == "down" || action == "up" || action == "click" ||
action == "right_click" || action == "middle_click" || action == "drag" || action == "scroll";
if (!knownAction)
return BuildError(id, -32602, "Unknown action: " + action);
int button = 0;
std::string buttonStr = GetStringArg(args, "button");
if (!ParseButton(buttonStr, button))
return BuildError(id, -32602, "Invalid button: " + buttonStr + " (expected left/middle/right)");
int clicks = 1;
if (args.isMember("clicks")) {
if (!GetIntArg(args, "clicks", clicks))
return BuildError(id, -32602, "clicks must be an integer 1..3");
if (clicks < 1 || clicks > 3)
return BuildError(id, -32602, "clicks must be 1, 2 or 3");
}
int delta = 0;
if (args.isMember("delta") && !GetIntArg(args, "delta", delta))
return BuildError(id, -32602, "delta must be an integer");
int notches = 0; // scroll 实际注入的滚动档数(= clamp(delta, -10, 10)),审计用
double nx = 0, ny = 0, nx2 = 0, ny2 = 0;
std::string coordErr;
if (!GetNormCoord(args, "x", nx, coordErr)) return BuildError(id, -32602, coordErr);
if (!GetNormCoord(args, "y", ny, coordErr)) return BuildError(id, -32602, coordErr);
if (action == "drag") {
if (!GetNormCoord(args, "x2", nx2, coordErr)) return BuildError(id, -32602, coordErr);
if (!GetNormCoord(args, "y2", ny2, coordErr)) return BuildError(id, -32602, coordErr);
}
// 校验 session_id/busy 并置 busy同时取物理分辨率screenW/H>0 由会话就绪保证)。
context* subCtx = nullptr;
int screenW = 0, screenH = 0;
int r = mcp.BeginScreenCtrlAction(devId, sessionId, subCtx, screenW, screenH);
if (r == 2) return BuildError(id, -32003, "Device busy: another injection is in flight for this session");
if (r == 1) return BuildError(id, -32002, "Remote control session not found, not ready, or closed: " + sessionId);
// 归一化坐标 → 物理像素round + clamp 到 [0, screen-1]
auto toPxX = [&](double n) -> int { int p = (int)(n * screenW + 0.5); return p < 0 ? 0 : (p > screenW - 1 ? screenW - 1 : p); };
auto toPxY = [&](double n) -> int { int p = (int)(n * screenH + 0.5); return p < 0 ? 0 : (p > screenH - 1 ? screenH - 1 : p); };
int px = toPxX(nx), py = toPxY(ny);
int px2 = 0, py2 = 0;
UINT downMsg, upMsg; uint64_t downWParam;
ButtonMessages(button, downMsg, upMsg, downWParam);
// 组装注入批次(按发送顺序;每个 MSG64 由客户端 ProcessCommand 分发到 SendInput
std::vector<MSG64> batch;
if (action == "move") {
batch.push_back(BuildMouseMsg64(px, py, WM_MOUSEMOVE, 0));
} else if (action == "down") {
batch.push_back(BuildMouseMsg64(px, py, downMsg, downWParam));
} else if (action == "up") {
batch.push_back(BuildMouseMsg64(px, py, upMsg, 0));
} else if (action == "click" || action == "right_click" || action == "middle_click") {
int b = (action == "right_click") ? 2 : (action == "middle_click" ? 1 : button);
UINT dMsg, uMsg; uint64_t dW;
ButtonMessages(b, dMsg, uMsg, dW);
for (int i = 0; i < clicks; ++i) {
batch.push_back(BuildMouseMsg64(px, py, dMsg, dW));
batch.push_back(BuildMouseMsg64(px, py, uMsg, 0));
}
} else if (action == "drag") {
px2 = toPxX(nx2); py2 = toPxY(ny2);
batch.push_back(BuildMouseMsg64(px, py, downMsg, downWParam));
batch.push_back(BuildMouseMsg64(px2, py2, WM_MOUSEMOVE, 0));
batch.push_back(BuildMouseMsg64(px2, py2, upMsg, 0));
} else if (action == "scroll") {
// delta 正=向下滚(与 Web 控制台一致WM_MOUSEWHEEL 负值=向下滚,故取反。
// 客户端对 WM_MOUSEWHEEL 会先 SetCursorPos(x,y) 再 MOUSEEVENTF_WHEEL单条消息即可定位+滚动。
notches = delta;
if (notches > 10) notches = 10;
if (notches < -10) notches = -10;
short wheelDelta = (short)(-notches * 120);
batch.push_back(BuildMouseMsg64(px, py, WM_MOUSEWHEEL, MAKEWPARAM(0, wheelDelta)));
}
// 组包 [COMMAND_SCREEN_CONTROL][MSG64*N],经屏幕子连接发送(主连接无效,设计 P2
const int len = (int)(1 + batch.size() * sizeof(MSG64));
std::vector<BYTE> packet((size_t)len);
packet[0] = COMMAND_SCREEN_CONTROL;
memcpy(packet.data() + 1, batch.data(), batch.size() * sizeof(MSG64));
bool ok = subCtx->Send2Client(packet.data(), (ULONG)len) != FALSE;
mcp.EndScreenCtrlAction(devId, sessionId);
if (!ok)
return BuildError(id, -32004, "Failed to send mouse injection");
// 审计(不可关闭;含归一化坐标与换算后的物理像素)
if (parent) {
std::string audit = "host " + std::to_string(devId) + " remote-mouse: session " + sessionId
+ " action=" + action;
char cbuf[96];
sprintf(cbuf, " norm=(%.3f,%.3f) px=(%d,%d)", nx, ny, px, py);
audit += cbuf;
if (action == "drag") {
sprintf(cbuf, " -> norm2=(%.3f,%.3f) px2=(%d,%d)", nx2, ny2, px2, py2);
audit += cbuf;
}
if (action == "scroll")
audit += " notches=" + std::to_string(notches);
if (action == "click" || action == "right_click" || action == "middle_click")
audit += " clicks=" + std::to_string(clicks);
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP远程控制")));
}
Json::Value result(Json::objectValue);
result["structuredContent"] = Json::Value(Json::objectValue);
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"ok");
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"];
@@ -3144,6 +3439,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "remote_open") return BuildRemoteOpen(id, args, parent);
if (toolName == "remote_close") return BuildRemoteClose(id, args, parent);
if (toolName == "remote_keyboard") return BuildRemoteKeyboard(id, args, parent);
if (toolName == "remote_mouse") return BuildRemoteMouse(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));

View File

@@ -799,34 +799,18 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
return;
}
// Build MSG64 structure
MSG64 msg64;
memset(&msg64, 0, sizeof(MSG64));
msg64.pt.x = x;
msg64.pt.y = y;
msg64.lParam = MAKELPARAM(x, y);
msg64.time = GetTickCount();
// Build MSG64 structure(共享 helper见 WebService.h与 MCP remote_mouse 复用)
MSG64 msg64 = BuildMouseMsg64(x, y, 0, 0); // 默认 WM_NULL未知 button 时仍发送(保持原行为)
// Map type and button to Windows message
if (type == "down") {
if (button == 0) {
msg64.message = WM_LBUTTONDOWN;
msg64.wParam = MK_LBUTTON;
} else if (button == 1) {
msg64.message = WM_MBUTTONDOWN;
msg64.wParam = MK_MBUTTON;
} else if (button == 2) {
msg64.message = WM_RBUTTONDOWN;
msg64.wParam = MK_RBUTTON;
}
if (button == 0) { msg64.message = WM_LBUTTONDOWN; msg64.wParam = MK_LBUTTON; }
else if (button == 1) { msg64.message = WM_MBUTTONDOWN; msg64.wParam = MK_MBUTTON; }
else if (button == 2) { msg64.message = WM_RBUTTONDOWN; msg64.wParam = MK_RBUTTON; }
} else if (type == "up") {
if (button == 0) {
msg64.message = WM_LBUTTONUP;
} else if (button == 1) {
msg64.message = WM_MBUTTONUP;
} else if (button == 2) {
msg64.message = WM_RBUTTONUP;
}
if (button == 0) { msg64.message = WM_LBUTTONUP; }
else if (button == 1) { msg64.message = WM_MBUTTONUP; }
else if (button == 2) { msg64.message = WM_RBUTTONUP; }
} else if (type == "move") {
msg64.message = WM_MOUSEMOVE;
} else if (type == "wheel") {
@@ -845,13 +829,8 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
if (clientType != GetClientType(CLIENT_TYPE_MACOS) && clientType != "macOS") {
return; // Skip dblclick for non-macOS clients
}
if (button == 0) {
msg64.message = WM_LBUTTONDBLCLK;
msg64.wParam = MK_LBUTTON;
} else if (button == 2) {
msg64.message = WM_RBUTTONDBLCLK;
msg64.wParam = MK_RBUTTON;
}
if (button == 0) { msg64.message = WM_LBUTTONDBLCLK; msg64.wParam = MK_LBUTTON; }
else if (button == 2) { msg64.message = WM_RBUTTONDBLCLK; msg64.wParam = MK_RBUTTON; }
} else {
return; // Unknown type
}

View File

@@ -48,6 +48,21 @@ inline MSG64 BuildKeyMsg64(int keyCode, bool isDown, bool isSysKey) {
return msg64;
}
// 构造单个鼠标注入 MSG64复用 WebService::HandleMouse 的基座字段逻辑,抽取为共享 helper
// 供 Web 与 MCP 远程控制两处使用,见 docs/Mcp_RemoteControl_Design.md §6.2)。
// x/y 为物理像素坐标已由调用方从归一化坐标映射message/wParam 由调用方按动作设定。
inline MSG64 BuildMouseMsg64(int x, int y, UINT message, uint64_t wParam) {
MSG64 msg64;
memset(&msg64, 0, sizeof(MSG64));
msg64.pt.x = x;
msg64.pt.y = y;
msg64.lParam = MAKELPARAM(x, y);
msg64.time = GetTickCount();
msg64.message = message;
msg64.wParam = wParam;
return msg64;
}
// Web client state
struct WebClient {
std::string token;