Fix: terminal_exec sentinel timeout on numeric timeout_ms and silent commands

Two independent bugs made MCP terminal tools time out despite the command
finishing.

First, timeout_ms is declared integer in the tool schemas but was parsed
through GetStringArg, which only reads JSON strings. A numeric value fell
through to the 20s default, so callers asking for a longer wait were cut off
at 20s. exec_command, terminal_open, terminal_exec and remote_open now parse
timeout_ms through GetIntArg, which accepts both JSON numbers and digit
strings, keeping the 1..600000 range guard.

Second, FindSentinel treated the __MCP_DONE_<nonce>__ marker as a line start
only when preceded by \n. Commands that produce no output (ping > nul,
Start-Sleep, tar -czf) echo their command line ending in \r, so the marker
never matched and the wait ran to timeout even though the command had
completed. The check now also accepts \r.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-30 13:19:22 +02:00
parent 52b4afb8cf
commit ac2855198b

View File

@@ -117,6 +117,11 @@ std::string GetStringArg(const Json::Value& args, const char* key) {
return JsonStrField(args, key); return JsonStrField(args, key);
} }
// 读取可选整数入参JSON number 或数字字符串);缺失/非法返回 false不写 out
// 定义见文件后半段(随 P3 终端会话辅助一起),此处前置声明供早于定义的 exec_command /
// terminal_exec 等工具复用。
static bool GetIntArg(const Json::Value& args, const char* key, int& out);
// ========== P2b 辅助 ========== // ========== P2b 辅助 ==========
// 解析 id 入参(必填、纯数字)为 uint64非法返回 false。 // 解析 id 入参(必填、纯数字)为 uint64非法返回 false。
@@ -2051,11 +2056,8 @@ std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy
return BuildError(id, -32007, "Command not allowed by whitelist: " + command); return BuildError(id, -32007, "Command not allowed by whitelist: " + command);
int timeoutMs = kMcpToolTimeoutMs; int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms"); int v = 0;
if (!t.empty() && IsDigits(t)) { if (GetIntArg(args, "timeout_ms", v) && v > 0 && v <= 600000) timeoutMs = v;
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string nonce = GenerateRandomToken().substr(0, 8); std::string nonce = GenerateRandomToken().substr(0, 8);
@@ -2426,11 +2428,8 @@ std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CM
return errJson; return errJson;
int timeoutMs = kMcpToolTimeoutMs; int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms"); int v = 0;
if (!t.empty() && IsDigits(t)) { if (GetIntArg(args, "timeout_ms", v) && v > 0 && v <= 600000) timeoutMs = v;
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string sessionId = GenerateRandomToken(); std::string sessionId = GenerateRandomToken();
if (!mcp.BeginTermOpen(devId, sessionId)) if (!mcp.BeginTermOpen(devId, sessionId))
@@ -2498,11 +2497,8 @@ std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CM
"Command contains & or | or a newline (breaks output capture); run chained/piped commands as separate terminal_exec calls"); "Command contains & or | or a newline (breaks output capture); run chained/piped commands as separate terminal_exec calls");
int timeoutMs = kMcpToolTimeoutMs; int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms"); int v = 0;
if (!t.empty() && IsDigits(t)) { if (GetIntArg(args, "timeout_ms", v) && v > 0 && v <= 600000) timeoutMs = v;
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string nonce = GenerateRandomToken().substr(0, 8); std::string nonce = GenerateRandomToken().substr(0, 8);
@@ -2751,11 +2747,8 @@ std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2
return BuildError(id, -32003, "Device busy: a remote desktop session is already active for this host"); return BuildError(id, -32003, "Device busy: a remote desktop session is already active for this host");
int timeoutMs = kMcpToolTimeoutMs; int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms"); int v = 0;
if (!t.empty() && IsDigits(t)) { if (GetIntArg(args, "timeout_ms", v) && v > 0 && v <= 600000) timeoutMs = v;
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string sessionId = GenerateRandomToken(); std::string sessionId = GenerateRandomToken();
if (!mcp.BeginScreenCtrlOpen(devId, sessionId)) if (!mcp.BeginScreenCtrlOpen(devId, sessionId))
@@ -3579,7 +3572,7 @@ static bool FindSentinel(const std::vector<BYTE>& buf, const std::string& nonce,
while (true) { while (true) {
size_t p = s.rfind(marker, from); size_t p = s.rfind(marker, from);
if (p == std::string::npos) return false; if (p == std::string::npos) return false;
bool lineStart = (p == 0) || (s[p - 1] == '\n'); bool lineStart = (p == 0) || (s[p - 1] == '\n') || (s[p - 1] == '\r');
size_t digitPos = p + marker.size(); size_t digitPos = p + marker.size();
if (lineStart && digitPos < s.size() && if (lineStart && digitPos < s.size() &&
(s[digitPos] == '0' || s[digitPos] == '1')) { (s[digitPos] == '0' || s[digitPos] == '1')) {