Files
SimpleRemoter/server/2015Remote/McpServer.cpp
yuanyuanxiang 13052b7cae Feature: Add MCP (Model Context Protocol) server integration
Add an optional MCP server (JSON-RPC 2.0 over Streamable HTTP) exposing
an online-host listing tool, protected by a Bearer token. Disabled by
default; configured via a new "Extensions > MCP Settings" dialog.

- McpServer: httplib + JSON-RPC 2.0 dispatch (initialize/ping/tools/list/tools/call)
- McpSettingsDlg: runtime-created dialog for enable/port/bind/token
- HostJson: extract single-host JSON serialization shared with WebService
- FRP: expose MCP port (union with listening/Web ports) when bound to 0.0.0.0
- i18n: en_US / zh_TW translations

Co-Authored-By: deepseek-v4-pro
2026-08-16 14:37:11 +02:00

292 lines
9.6 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));
}
// list_online_hosts 的 outputSchema见 docs/Mcp_Design.md §3.3.2
Json::Value BuildHostOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostsProp(Json::objectValue);
hostsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
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;
items["properties"] = itemProps;
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;
}
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
Json::Value tools(Json::arrayValue);
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);
result["tools"] = tools;
return BuildResult(id, result);
}
// tools/calllist_online_hosts
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
const 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();
}
if (toolName != "list_online_hosts") {
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
}
Json::Value hosts(Json::arrayValue);
int count = 0;
if (parent) {
// 与 WebService 侧一致的锁内遍历,复用 BuildHostJson 序列化(方案 C
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
hosts.append(BuildHostJson(ctx, parent->m_ClientMap));
++count;
}
LeaveCriticalSection(&parent->m_cs);
}
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);
}
} // 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;
}