Files
SimpleRemoter/server/2015Remote/McpServer.cpp
yuanyuanxiang 86c4f14cf4 Feature: Add search_hosts and get_host_detail MCP tools
Add two pure in-memory MCP tools over the online host list. search_hosts filters by name/remark, IP, group and OS (all optional, AND-combined, ASCII case-insensitive substring match); get_host_detail returns one online host by id and answers -32602 for a missing/non-numeric id and -32002 for an unknown or offline id. Sync Mcp_Phase2_Design.md with the second-review corrections and the P2a implementation.

Co-Authored-By: deepseek-v4-pro
2026-08-18 16:53:28 +02:00

515 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "stdafx.h"
#include "McpServer.h"
#include "jsoncpp/json.h"
#include "HostJson.h" // BuildHostJson单台主机序列化公共函数
#include "context.h" // context 接口
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员m_HostList/m_cs/m_ClientMap+ VERSION_STR
#include <sstream>
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
namespace {
// Json::Value → 紧凑 JSON 字符串
std::string JsonToString(const Json::Value& v) {
Json::StreamWriterBuilder b;
b["indentation"] = "";
return Json::writeString(b, v);
}
// JSON-RPC 2.0 成功响应
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
resp["result"] = result;
return JsonToString(resp);
}
// JSON-RPC 2.0 错误响应
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
Json::Value err(Json::objectValue);
err["code"] = code;
err["message"] = msg;
resp["error"] = err;
return JsonToString(resp);
}
// initialize 握手MCP 规范protocolVersion + capabilities + serverInfo
std::string BuildInitializeResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
result["protocolVersion"] = "2025-06-18";
Json::Value caps(Json::objectValue);
caps["tools"] = Json::Value(Json::objectValue);
result["capabilities"] = caps;
Json::Value serverInfo(Json::objectValue);
serverInfo["name"] = "yama";
serverInfo["version"] = VERSION_STR;
result["serverInfo"] = serverInfo;
return BuildResult(id, result);
}
// ping 健康检查:返回空 result
std::string BuildPingResult(const Json::Value& id) {
return BuildResult(id, Json::Value(Json::objectValue));
}
// ========== P2a 通用辅助 ==========
// 小写化(仅 ASCIIUTF-8 多字节原样保留):用于不区分大小写的子串匹配
std::string ToLowerAscii(const std::string& s) {
std::string r = s;
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return r;
}
// 不区分大小写的子串匹配
bool ContainsCI(const std::string& haystack, const std::string& needle) {
if (needle.empty()) return true;
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
}
// 取对象的字符串字段,缺失/非字符串返回 ""
std::string JsonStrField(const Json::Value& v, const char* key) {
if (v.isObject() && v.isMember(key) && v[key].isString())
return v[key].asString();
return "";
}
// 是否纯数字host id 为 uint64 十进制字符串)
bool IsDigits(const std::string& s) {
if (s.empty()) return false;
for (char c : s) if (c < '0' || c > '9') return false;
return true;
}
// 读取 tools/call 的入参MCP 规范params.arguments 为工具入参对象)
Json::Value GetCallArguments(const Json::Value& params) {
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
return params["arguments"];
return Json::Value(Json::objectValue);
}
// 读取可选字符串入参,缺失返回 ""
std::string GetStringArg(const Json::Value& args, const char* key) {
return JsonStrField(args, key);
}
// 收集所有在线主机 JSON 数组m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
if (!parent) return;
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
hosts.append(BuildHostJson(ctx, parent->m_ClientMap));
}
LeaveCriticalSection(&parent->m_cs);
}
// ========== 工具 schema ==========
// 单台主机字段 schemahosts 数组元素 / 单机详情共用的形状)
Json::Value BuildHostItemSchema() {
Json::Value itemProps(Json::objectValue);
const char* strFields[] = {
"id", "name", "remark", "ip", "os", "location", "rtt",
"version", "activeWindow", "group", "screen", "clientType"
};
for (const char* f : strFields) {
Json::Value p(Json::objectValue);
p["type"] = "string";
itemProps[f] = p;
}
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
itemProps["online"] = onlineProp;
return itemProps;
}
// list_online_hosts / search_hosts 的 outputSchemahosts 数组)
Json::Value BuildHostOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostsProp(Json::objectValue);
hostsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
items["properties"] = BuildHostItemSchema();
hostsProp["items"] = items;
props["hosts"] = hostsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("hosts");
schema["required"] = required;
return schema;
}
// get_host_detail 的 outputSchema单台主机
Json::Value BuildHostDetailOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostProp(Json::objectValue);
hostProp["type"] = "object";
hostProp["properties"] = BuildHostItemSchema();
props["host"] = hostProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("host");
schema["required"] = required;
return schema;
}
// search_hosts 的 inputSchema全部可选
Json::Value BuildSearchHostsInputSchema() {
Json::Value props(Json::objectValue);
const char* strParams[] = { "name", "ip", "group", "os" };
for (const char* p : strParams) {
Json::Value s(Json::objectValue);
s["type"] = "string";
props[p] = s;
}
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
props["online"] = onlineProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// get_host_detail 的 inputSchemaid 必填)
Json::Value BuildGetHostDetailInputSchema() {
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 schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
Json::Value tools(Json::arrayValue);
// 1) list_online_hosts
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_online_hosts";
// 说明文字为 UTF-8项目 /execution-charset:.936 会把普通窄字面量编译成 GBK
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
tool["description"] = u8"获取当前所有在线主机的列表包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
Json::Value inputSchema(Json::objectValue);
inputSchema["type"] = "object";
inputSchema["properties"] = Json::Value(Json::objectValue);
inputSchema["required"] = Json::Value(Json::arrayValue);
tool["inputSchema"] = inputSchema;
tool["outputSchema"] = BuildHostOutputSchema();
tools.append(tool);
}
// 2) search_hostsP2a纯内存过滤无子链接
{
Json::Value tool(Json::objectValue);
tool["name"] = "search_hosts";
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合子串匹配ASCII 不区分大小写)。只返回在线主机。";
tool["inputSchema"] = BuildSearchHostsInputSchema();
tool["outputSchema"] = BuildHostOutputSchema();
tools.append(tool);
}
// 3) get_host_detailP2a单机详情纯内存
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_host_detail";
tool["description"] = u8"获取单台在线主机的详细信息id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等";
tool["inputSchema"] = BuildGetHostDetailInputSchema();
tool["outputSchema"] = BuildHostDetailOutputSchema();
tools.append(tool);
}
result["tools"] = tools;
return BuildResult(id, result);
}
// tools/calllist_online_hosts
std::string BuildListOnlineHosts(const Json::Value& id, CMy2015RemoteDlg* parent) {
Json::Value hosts(Json::arrayValue);
CollectOnlineHosts(parent, hosts);
int count = (int)hosts.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["hosts"] = hosts;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 台主机在线。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callsearch_hosts
std::string BuildSearchHosts(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
Json::Value all(Json::arrayValue);
CollectOnlineHosts(parent, all);
std::string fName = GetStringArg(args, "name");
std::string fIp = GetStringArg(args, "ip");
std::string fGroup = GetStringArg(args, "group");
std::string fOs = GetStringArg(args, "os");
bool hasOnline = args.isObject() && args.isMember("online") && args["online"].isBool();
bool wantOnline = hasOnline ? args["online"].asBool() : true;
Json::Value hosts(Json::arrayValue);
// 列表只含在线主机:显式 online=false 时直接空结果
if (!hasOnline || wantOnline) {
for (unsigned int i = 0; i < all.size(); ++i) {
const Json::Value& h = all[i];
if (!fName.empty()) {
std::string name = JsonStrField(h, "name");
std::string remark = JsonStrField(h, "remark");
if (!ContainsCI(name, fName) && !ContainsCI(remark, fName)) continue;
}
if (!fIp.empty() && !ContainsCI(JsonStrField(h, "ip"), fIp)) continue;
if (!fGroup.empty() && !ContainsCI(JsonStrField(h, "group"), fGroup)) continue;
if (!fOs.empty() && !ContainsCI(JsonStrField(h, "os"), fOs)) continue;
hosts.append(h);
}
}
int count = (int)hosts.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["hosts"] = hosts;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 台主机匹配。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callget_host_detail
std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
std::string sid = GetStringArg(args, "id");
if (sid.empty()) {
return BuildError(id, -32602, "Missing required parameter: id");
}
if (!IsDigits(sid)) {
return BuildError(id, -32602, "Invalid id: expected a decimal host id string");
}
Json::Value all(Json::arrayValue);
CollectOnlineHosts(parent, all);
for (unsigned int i = 0; i < all.size(); ++i) {
const Json::Value& h = all[i];
if (JsonStrField(h, "id") == sid) {
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["host"] = h;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
std::string name = JsonStrField(h, "name");
std::string ip = JsonStrField(h, "ip");
item["text"] = std::string(u8"主机 ") + name + " (" + ip + ")" + u8" 的详情。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
}
return BuildError(id, -32002, "Host not found or offline: " + sid);
}
// tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
Json::Value params = root.isMember("params") ? root["params"] : Json::Value(Json::objectValue);
std::string toolName;
if (params.isObject() && params.isMember("name") && params["name"].isString()) {
toolName = params["name"].asString();
}
const Json::Value args = GetCallArguments(params);
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
}
} // namespace
//////////////////////////////////////////////////////////////////////////
// CMcpServer Implementation
//////////////////////////////////////////////////////////////////////////
CMcpServer& CMcpServer::Instance() {
static CMcpServer instance;
return instance;
}
CMcpServer::CMcpServer() {
m_server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
HandleMcp(req, res);
});
}
CMcpServer::~CMcpServer() {
Stop(); // 兜底:确保监听线程 join避免 std::thread 析构触发 terminate
}
bool CMcpServer::Start(const std::string& bind, int port) {
if (m_running.load()) return true; // 已在运行
m_thread = std::thread([this, bind, port]() {
m_server.listen(bind, port);
});
// 给 listen 一点时间绑定端口httplib::Server::is_running() 在 listen 内部置位。
std::this_thread::sleep_for(std::chrono::milliseconds(100));
m_running.store(m_server.is_running());
return m_running.load();
}
void CMcpServer::Stop() {
m_server.stop();
if (m_thread.joinable()) {
m_thread.join();
}
m_running.store(false);
}
void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res) {
res.set_header("Content-Type", "application/json");
// 静态 token 校验Authorization: Bearer <token>Start 前经 SetToken 保证非空)
if (req.get_header_value("Authorization") != ("Bearer " + m_token)) {
res.status = 401;
res.set_content(BuildError(Json::nullValue, -32000, "Unauthorized"), "application/json");
return;
}
// 解析 JSON-RPC 请求体
Json::Value root;
Json::CharReaderBuilder rbuilder;
std::string errs;
std::istringstream iss(req.body);
if (!Json::parseFromStream(rbuilder, iss, &root, &errs) || !root.isObject()) {
res.set_content(BuildError(Json::nullValue, -32700, "Parse error"), "application/json");
return;
}
// 通知(无 id→ 不返回 JSON-RPC 响应(如 notifications/initialized
if (!root.isMember("id")) {
res.status = 202;
res.set_content("", "application/json");
return;
}
// 结构校验:缺 method
if (!root.isMember("method") || !root["method"].isString()) {
res.set_content(BuildError(root["id"], -32600, "Invalid Request"), "application/json");
return;
}
std::string method = root["method"].asString();
if (method == "initialize") {
res.set_content(BuildInitializeResult(root["id"]), "application/json");
return;
}
if (method == "ping") {
res.set_content(BuildPingResult(root["id"]), "application/json");
return;
}
if (method == "tools/list") {
res.set_content(BuildToolsListResult(root["id"]), "application/json");
return;
}
if (method == "tools/call") {
res.set_content(BuildToolsCall(root, m_parent), "application/json");
return;
}
// 未实现的方法
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
}
// rand_sWindows CRT 加密安全随机源(基于系统 CSPRNG。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S为避免依赖 PCH 的包含顺序这里手动声明其导出原型errno_t == int
extern "C" int __cdecl rand_s(unsigned int* randomValue);
std::string GenerateRandomToken() {
static const char hex[] = "0123456789abcdef";
std::string out;
out.reserve(32);
for (int i = 0; i < 16; ++i) {
unsigned int v = 0;
if (rand_s(&v) != 0) {
// rand_s 失败(罕见):退化为时间 + 地址熵,保证仍返回非空 token。
v = (unsigned int)(GetTickCount() ^ (ULONG_PTR)&out);
}
out.push_back(hex[(v >> 4) & 0xF]);
out.push_back(hex[v & 0xF]);
}
return out;
}