Feature: Add exec_command MCP tool

Add a one-shot exec_command MCP tool that runs a command on a remote
Windows host and returns stdout plus exit code, reusing the Web terminal
link (main-connection COMMAND_SHELL, a shell sub-connection, and a
sentinel command line located via rfind to tolerate ConPTY echo).

The sentinel marker is embedded in the command line ConPTY echoes back,
so it is only treated as hit when it starts a line (preceded by a newline
or the buffer start); this keeps the echoed marker from being mistaken
for the real sentinel when the echo packet arrives before the output.

Execution is gated at Web remote-desktop sensitivity: a read-only mode
(McpReadonly, default on, hides the tool) and a command whitelist
(McpCmdWhitelist) with built-in read-only prefixes. Shell metacharacters
(& | < > ^) are rejected before whitelist matching, and each execution is
recorded in the server audit log.

Extend the MCP settings dialog with the read-only checkbox and a
multi-line whitelist box (commas and newlines both accepted, normalized
to a comma-separated list on save), and add English and Traditional
Chinese mappings for the new UI and audit-log strings.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-23 23:23:20 +02:00
parent 8bd6f3b60a
commit 0ddbc1aced
7 changed files with 579 additions and 5 deletions

View File

@@ -2190,6 +2190,9 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
McpServer().SetParentDlg(this);
McpServer().SetToken(mcpToken);
// 安全配置:只读默认 1exec_command 默认禁用);白名单空则用内置只读前缀。
McpServer().SetReadonly(THIS_CFG.GetInt("settings", "McpReadonly", 1) != 0);
McpServer().SetCmdWhitelist(THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""));
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else {
@@ -5505,6 +5508,18 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
return;
}
// ===== MCP exec_command 的 shell 子上下文:被 MCP 接管时,数据走 OnTerminalData =====
// 与上方 Web 终端同构MCP 接管的 shell 子上下文不开 MFC 对话框hDlg 为 NULL
// 每个数据包都落到 MessageHandle 顶部分支,转发给 McpServer 做哨兵检测。
if (McpServer().IsTerminalContext(ContextObject)) {
if (len == 1 && cmd == TOKEN_TERMINAL_CLOSE) {
McpServer().OnTerminalClosed(ContextObject);
} else {
McpServer().OnTerminalData(ContextObject, szBuffer, len);
}
return;
}
// 【L】主机上下线和授权
// 【x】对话框相关功能
switch (cmd) {
@@ -6408,6 +6423,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
// hDlg 留 NULL后续数据继续走 MessageHandle 顶部的 IsTerminalContext 分支
break;
}
// MCP exec_command 触发的 shell 子上下文:交给 McpServer 接管(老 cmd 管道GBK
if (McpServer().IsTermPending(devId)) {
McpServer().RegisterTerminalContext(devId, ContextObject, /*isPty*/false);
break;
}
g_2015RemoteDlg->SendMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)ContextObject);
break;
}
@@ -6418,6 +6438,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
WebService().RegisterTerminalContext(devId, ContextObject, /*isPty*/true);
break;
}
// MCP exec_command 触发的 PTY 子上下文:交给 McpServer 接管ConPTYUTF-8
if (McpServer().IsTermPending(devId)) {
McpServer().RegisterTerminalContext(devId, ContextObject, /*isPty*/true);
break;
}
// 三个前置条件,缺任何一个都回退到经典终端,并把原因贴到信息列表。
// SYSTEM 场景WebView2 不支持 LocalSystem token会出现"窗口能弹但页面空白"