Feature: Add remote_clipboard MCP tool
Add the remote_clipboard tool to the MCP remote control surface, writing text to the remote host's clipboard via COMMAND_SCREEN_SET_CLIPBOARD through the established screen sub-connection. Input UTF-8 is converted to GBK (ToAnsi, code page 936) to match the client's CF_TEXT/ANSI clipboard path, mirroring the existing CScreenSpyDlg::SendServerClipboard packet format. Pasting remains a separate step (remote_keyboard Ctrl+V). Known MVP limitation, documented in the tool description: non-GBK characters (e.g. emoji) are replaced by '?' on the CF_TEXT path. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -1136,10 +1136,13 @@ Json::Value BuildRemoteKeyboardInputSchema();
|
||||
Json::Value BuildRemoteKeyboardOutputSchema();
|
||||
Json::Value BuildRemoteMouseInputSchema();
|
||||
Json::Value BuildRemoteMouseOutputSchema();
|
||||
Json::Value BuildRemoteClipboardInputSchema();
|
||||
Json::Value BuildRemoteClipboardOutputSchema();
|
||||
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);
|
||||
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
// tools/list
|
||||
std::string BuildToolsListResult(const Json::Value& id) {
|
||||
@@ -1379,6 +1382,14 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
tool["outputSchema"] = BuildRemoteMouseOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "remote_clipboard";
|
||||
tool["description"] = u8"把文本写入远程主机的剪贴板(UTF-8 → GBK/ANSI,非 GBK 字符如 emoji 会丢失)。仅设置剪贴板,粘贴需随后用 remote_keyboard 注入 Ctrl+V。";
|
||||
tool["inputSchema"] = BuildRemoteClipboardInputSchema();
|
||||
tool["outputSchema"] = BuildRemoteClipboardOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
}
|
||||
|
||||
result["tools"] = tools;
|
||||
@@ -3408,6 +3419,110 @@ std::string BuildRemoteMouse(const Json::Value& id, const Json::Value& args, CMy
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// ===== P5:remote_clipboard(写远程剪贴板,UTF-8 → GBK 直发,见设计 §6.4)=====
|
||||
|
||||
Json::Value BuildRemoteClipboardInputSchema() {
|
||||
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 text(Json::objectValue);
|
||||
text["type"] = "string";
|
||||
text["description"] = u8"要写入剪贴板的文本(UTF-8;非 GBK 字符如 emoji 会丢失)";
|
||||
props["text"] = text;
|
||||
|
||||
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("text");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
Json::Value BuildRemoteClipboardOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// tools/call:remote_clipboard(写远程剪贴板;成功返回空对象)
|
||||
std::string BuildRemoteClipboard(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 text = GetStringArg(args, "text");
|
||||
if (text.empty())
|
||||
return BuildError(id, -32602, "Missing required parameter: text");
|
||||
if (text.size() > 262144) // 256 KB 上限(CF_TEXT 单包,防内存/传输失控)
|
||||
return BuildError(id, -32602, "text too long (max 262144 bytes)");
|
||||
|
||||
// UTF-8 → GBK:客户端 UpdateClientClipboard 以 CF_TEXT(ANSI/GBK)落剪贴板(设计 §6.4)。
|
||||
// 非 GBK 字符(emoji 等)在此被替换为 '?',MVP 已知限制。
|
||||
std::string ansi = ToAnsi(text, 936);
|
||||
|
||||
// 校验 session_id/busy 并置 busy,取屏幕子连接(注入与剪贴板共用同一子连接串行化)。
|
||||
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);
|
||||
|
||||
// 组包 [COMMAND_SCREEN_SET_CLIPBOARD][GBK text],经屏幕子连接发送(客户端自行补 '\0')。
|
||||
const int len = (int)(1 + ansi.size());
|
||||
std::vector<BYTE> packet((size_t)len);
|
||||
packet[0] = COMMAND_SCREEN_SET_CLIPBOARD;
|
||||
memcpy(packet.data() + 1, ansi.data(), ansi.size());
|
||||
bool ok = subCtx->Send2Client(packet.data(), (ULONG)len) != FALSE;
|
||||
|
||||
mcp.EndScreenCtrlAction(devId, sessionId);
|
||||
|
||||
if (!ok)
|
||||
return BuildError(id, -32004, "Failed to send clipboard text");
|
||||
|
||||
// 审计(不可关闭;记字节数,不落全文以免审计日志膨胀)
|
||||
if (parent) {
|
||||
std::string audit = "host " + std::to_string(devId) + " remote-clipboard: session " + sessionId
|
||||
+ " input_bytes=" + std::to_string(text.size());
|
||||
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"];
|
||||
@@ -3440,6 +3555,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* 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);
|
||||
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
|
||||
|
||||
return BuildError(id, -32602,
|
||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||
|
||||
Reference in New Issue
Block a user