Files
envi-code/SourceCode/Code2026/cad_mcp_frame/mcp_server.cpp
T
company-desktop b9c4ce56f9 fix: cad_mcp_frame 修 P0 收包/socket 泄漏与 P1 静默失败、无鉴权
P0 收包协议(大请求被截断 + 分包即失败)
原来只 recv 一次、缓冲固定 2048 字节。改为:
- IsJsonComplete():按 {} 与 [] 配平判断报文完整性(跳过字符串内与转义字符)
- RecvOneRequest():select + 循环 recv 追加,单次等 5s、整请求 30s、上限 4MB
- SendAll():send 循环到发完(tools/list 应答可能超一个包)
不改客户端协议:客户端仍是 sendall 后等响应断开。

P0 socket 泄漏
recv<=0 的分支原来什么都不做。现在收包失败/超时回一个 JSON-RPC 错误帧并 closesocket;
accept 失败直接 continue(原代码对 INVALID_SOCKET 也调 closesocket)。

P1 监听失败不再静默
socket/WSASocket/bind/listen/CreateWindowEx/std::thread 全部检查返回值,
监听 socket 改在主线程创建,失败时 init_mcp_server() 返回 false 并打印 WSAGetLastError,
端口被占用时能看到 "Fatal: bind 127.0.0.1:8080 failed, err=10048"。

P1 端口与配置文件名可配
- server.port 可配(默认 8080)
- 配置先找 mcp_config.json,再找 cad-mcp-frame.json(此前代码只找前者,
  而仓库里带的是后者,导致加载即自关)
- 启动日志打印实际端口与 token 是否启用

P1 可选 token 鉴权
配置写了 server.token 就要求请求根级带 token,不匹配返回 -32001 且不执行工具;
没配 token 保持原行为,不破坏现有客户端。

顺带 P2:close_mcp_server() 先排空挂起的 WM_MCP_EXECUTE_TASK 并 delete TaskData
(原来会漏 socket + 内存),再销毁窗口。

验证:R230 与 Z2026 Debug 均编译通过(818->826 KB,已部署)。
未做:没有在 CAD 里实际跑 tools/list 与 tools/call 验证协议改动。
2026-10-28 23:28:02 +08:00

474 lines
13 KiB
C++

#include "StdAfx.h"
#include "KTCad.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <thread>
#include <atomic>
#include <nlohmann/json.hpp>
#include "mcp_ConfigParser.h"
#include "mcp_utils.h"
#pragma comment(lib, "ws2_32.lib")
#define WM_MCP_EXECUTE_TASK (WM_USER + 2048)
extern HINSTANCE _hdllInstance;
#define MCP_SERVER_PORT 8080
#define MCP_SERVER_CONFIG _T("mcp_config.json")
#define MCP_SERVER_CONFIG_ALT _T("cad-mcp-frame.json")
#define MCP_MAX_REQUEST_BYTES (4 * 1024 * 1024)
namespace
{
std::thread g_serverThread;
std::atomic<bool> g_bRunning(false);
SOCKET g_listenSocket = INVALID_SOCKET;
int g_nPort = MCP_SERVER_PORT;
std::string g_sToken; // 非空时要求客户端带 token
std::wstring g_sConfigPath; // 实际使用的配置文件全路径
HWND g_hMsgWnd = NULL;
const wchar_t *g_szMsgClassName = L"AutoAgentMcpMsgWndClass";
// 声明一个全局工具集对象
mcp_Tools g_mcpTools;
// 用于跨线程传递数据的结构体
struct TaskData
{
~TaskData()
{
if (clientSocket != INVALID_SOCKET)
{
::closesocket(clientSocket);
clientSocket = INVALID_SOCKET;
}
}
SOCKET clientSocket = INVALID_SOCKET;
std::string rawJson;
};
// ------------------------------------------------------------------
// 报文边界:客户端发完不等应答就等结果(不做 shutdown),所以服务端只能靠
// "JSON 结构是否闭合" 判断一个请求是否收完。字符串内的括号不算。
// ------------------------------------------------------------------
bool IsJsonComplete(const std::string &s)
{
int nBrace = 0;
int nBracket = 0;
bool bInStr = false;
bool bEsc = false;
bool bAny = false;
for (size_t i = 0; i < s.size(); ++i)
{
const char c = s[i];
if (bInStr)
{
if (bEsc) { bEsc = false; }
else if (c == '\\') { bEsc = true; }
else if (c == '"') { bInStr = false; }
continue;
}
switch (c)
{
case '"': bInStr = true; bAny = true; break;
case '{': ++nBrace; bAny = true; break;
case '}': --nBrace; bAny = true; break;
case '[': ++nBracket; bAny = true; break;
case ']': --nBracket; bAny = true; break;
default: break;
}
if (bAny && nBrace == 0 && nBracket == 0 && !bInStr)
return true;
}
return false;
}
// 循环接收直到 JSON 闭合(或超时/超长)。
// 原来只 recv 一次、缓冲固定 2048,大请求会被截断,且 TCP 分包必失败。
bool RecvOneRequest(SOCKET s, std::string &out)
{
out.clear();
char buf[4096];
const ULONGLONG nDeadline = ::GetTickCount64() + 30000; // 整个请求 30s
for (;;)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(s, &readSet);
timeval tv = { 5, 0 }; // 单次等待 5s
const int ret = ::select(0, &readSet, nullptr, nullptr, &tv);
if (ret < 0)
return false;
if (ret == 0)
{
if (::GetTickCount64() > nDeadline) return false;
continue;
}
const int nRecv = ::recv(s, buf, (int)sizeof(buf), 0);
if (nRecv <= 0)
return IsJsonComplete(out); // 对端关闭:已收完整才算成功
out.append(buf, (size_t)nRecv);
if (out.size() > MCP_MAX_REQUEST_BYTES)
return false;
if (IsJsonComplete(out))
return true;
if (::GetTickCount64() > nDeadline)
return IsJsonComplete(out);
}
}
// send 可能只发一部分,必须循环(tools/list 的应答可能上万字节)
bool SendAll(SOCKET s, const std::string &data)
{
size_t nSent = 0;
while (nSent < data.size())
{
const int n = ::send(s, data.c_str() + nSent, (int)(data.size() - nSent), 0);
if (n <= 0)
return false;
nSent += (size_t)n;
}
return true;
}
std::string MakeErrorResponse(const char *pszMessage, int nCode = -32603, int nId = 0)
{
nlohmann::json err;
err["jsonrpc"] = "2.0";
err["id"] = nId;
err["error"] = { { "code", nCode }, { "message", pszMessage } };
return err.dump();
}
// 处理工具查询请求
std::string HandleToolsList(int msgId)
{
nlohmann::json resp;
resp["jsonrpc"] = "2.0";
resp["id"] = msgId;
// 直接向全局工具集要数据
resp["result"]["tools"] = g_mcpTools.ToLLM();
return resp.dump();
}
// 处理工具调用请求
std::string HandleToolsCall(int msgId, const nlohmann::json &params)
{
nlohmann::json resp;
resp["jsonrpc"] = "2.0";
resp["id"] = msgId;
std::string toolName = params.value("name", "");
nlohmann::json args = params.value("arguments", nlohmann::json::object());
// 让工具集去执行,返回 payload 可能是 {"result": ...} 也可能是 {"error": ...}
nlohmann::json payload = g_mcpTools.Call(toolName, args);
// 将 payload 注入到外层信封中
resp.update(payload);
return resp.dump();
}
// 此函数在 AutoCAD 主 UI 线程运行,绝对安全.
void ExecuteTaskInMainThread(TaskData *pTask)
{
if (pTask == nullptr) return;
std::string responseStr;
// 关闭 nlohmann 的异常抛出机制,安全解析
nlohmann::json req = nlohmann::json::parse(pTask->rawJson, nullptr, false);
if (req.is_discarded())
{
// 非法 JSON 报文
responseStr = MakeErrorResponse("Parse error", -32700, 0);
}
else
{
std::string method = req.value("method", "");
int msgId = req.value("id", 0);
// 鉴权:配置里写了 token 就必须带对(请求根级的 token 字段)
if (!g_sToken.empty())
{
const std::string sGot = req.value("token", "");
if (sGot != g_sToken)
{
acutPrintf(_T("\n[MCP Server] 请求被拒:token 缺失或不匹配(客户端 IP 与本机相同路径也要带 token)。"));
responseStr = MakeErrorResponse("Unauthorized", -32001, msgId);
if (pTask->clientSocket != INVALID_SOCKET)
SendAll(pTask->clientSocket, responseStr);
return;
}
}
if (method == "tools/list")
{
responseStr = HandleToolsList(msgId);
}
else if (method == "tools/call")
{
responseStr = HandleToolsCall(msgId, req.value("params", nlohmann::json::object()));
}
else
{
// 找不到方法
responseStr = MakeErrorResponse("Method not found", -32601, msgId);
}
}
// 通过传递过来的 Socket 将结果发送回大模型端
if (pTask->clientSocket != INVALID_SOCKET)
{
SendAll(pTask->clientSocket, responseStr);
}
}
// 隐式窗口的回调函数 (属于主线程)
LRESULT CALLBACK McpWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_MCP_EXECUTE_TASK)
{
TaskData *pTask = reinterpret_cast<TaskData *>(lParam);
if (pTask)
{
ExecuteTaskInMainThread(pTask);
delete pTask; // 执行完毕,清理内存
}
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
// TCP 监听后台线程(监听 socket 由主线程创建好并传入,失败在启动时就暴露)
void TcpServerLoop(SOCKET listenSocket)
{
while (g_bRunning)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(listenSocket, &readSet);
timeval timeout = { 1, 0 }; // 1秒超时机制
int ret = select(0, &readSet, nullptr, nullptr, &timeout);
if (ret > 0 && FD_ISSET(listenSocket, &readSet))
{
SOCKET clientSocket = accept(listenSocket, nullptr, nullptr);
if (clientSocket == INVALID_SOCKET)
continue;
// 循环收直到 JSON 闭合;原来只 recv 一次 2048,大请求会被截断
std::string rawJson;
const bool bOk = RecvOneRequest(clientSocket, rawJson);
if (!bOk || rawJson.empty())
{
// 收包失败/超时:回一个错误并关闭,避免客户端干等
SendAll(clientSocket, MakeErrorResponse("Bad request", -32700));
::closesocket(clientSocket); // 原来这里没关,会泄漏 socket
continue;
}
// 将数据拷贝到堆内存,准备派发给主线程
TaskData *pTask = new TaskData;
pTask->clientSocket = clientSocket; // 记录 Socket,由 TaskData 析构关闭
pTask->rawJson = rawJson;
// 关键转变:通过 PostMessage 发送给主线程拥有的隐式窗口
if (g_hMsgWnd != NULL)
{
PostMessage(g_hMsgWnd, WM_MCP_EXECUTE_TASK, 0, reinterpret_cast<LPARAM>(pTask));
}
else
{
delete pTask;
}
}
}
closesocket(listenSocket);
WSACleanup();
}
}
namespace mcp
{
// 兼容两种配置文件名:先找 mcp_config.json,再找 cad-mcp-frame.json
static std::wstring ResolveConfigPath()
{
const fs::path dir = mcp_utils::getModelDirPath();
fs::path p1 = dir / MCP_SERVER_CONFIG;
if (fs::exists(p1)) return p1.wstring();
fs::path p2 = dir / MCP_SERVER_CONFIG_ALT;
if (fs::exists(p2)) return p2.wstring();
return p1.wstring(); // 都不存在:返回默认名,后续加载会失败并报错
}
// 读取 server.port / server.token(可选)
static void LoadServerSettings(const std::wstring &wsConfig)
{
try
{
std::ifstream ifs(wsConfig);
if (!ifs.is_open()) return;
nlohmann::json cfg;
ifs >> cfg;
if (!cfg.contains("server")) return;
const nlohmann::json &srv = cfg["server"];
const int nPort = srv.value("port", MCP_SERVER_PORT);
if (nPort > 0 && nPort < 65536) g_nPort = nPort;
g_sToken = srv.value("token", std::string());
}
catch (...)
{
// 配置读坏了不影响默认值
}
}
bool close_mcp_server()
{
g_bRunning = false;
if (g_serverThread.joinable())
{
g_serverThread.join();
}
if (g_hMsgWnd != NULL)
{
// 排空仍挂在本窗口消息队列里的任务,否则 TaskData(含 socket)会泄漏
MSG msg;
int nGuard = 0;
while (nGuard++ < 1000 && PeekMessage(&msg, g_hMsgWnd, WM_MCP_EXECUTE_TASK, WM_MCP_EXECUTE_TASK, PM_REMOVE))
{
TaskData *pTask = reinterpret_cast<TaskData *>(msg.lParam);
if (pTask != NULL) delete pTask;
}
DestroyWindow(g_hMsgWnd);
UnregisterClass(g_szMsgClassName, _hdllInstance);
g_hMsgWnd = NULL;
}
g_mcpTools.clear();
acutPrintf(_T("\n[MCP Server] The plugin has been uninstalled, and the thread has been closed.\n"));
return true;
}
bool init_mcp_server()
{
// 0. 配置文件与服务器设置(端口/可选 token)
g_sConfigPath = ResolveConfigPath();
LoadServerSettings(g_sConfigPath);
// 1. 先把监听 socket 建好:失败就在启动时暴露,不再静默
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
acutPrintf(_T("\n[MCP Server] Fatal: WSAStartup failed.\n"));
return false;
}
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET)
{
acutPrintf(_T("\n[MCP Server] Fatal: socket() failed, err=%d.\n"), ::WSAGetLastError());
WSACleanup();
return false;
}
sockaddr_in serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons((u_short)g_nPort);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
if (bind(listenSocket, (sockaddr *)&serverAddr, sizeof(serverAddr)) != 0)
{
const int nErr = ::WSAGetLastError();
acutPrintf(_T("\n[MCP Server] Fatal: bind 127.0.0.1:%d failed, err=%d(端口被占用?)\n"), g_nPort, nErr);
closesocket(listenSocket);
WSACleanup();
return false;
}
if (listen(listenSocket, SOMAXCONN) != 0)
{
acutPrintf(_T("\n[MCP Server] Fatal: listen failed, err=%d.\n"), ::WSAGetLastError());
closesocket(listenSocket);
WSACleanup();
return false;
}
g_listenSocket = listenSocket;
// 2. 在主线程注册并创建 Message-Only 窗口
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = McpWndProc;
wc.hInstance = _hdllInstance;
wc.lpszClassName = g_szMsgClassName;
RegisterClassEx(&wc);
g_hMsgWnd = CreateWindowEx(
0,
g_szMsgClassName,
L"AutoAgentMcpMsgWindow",
0, 0, 0, 0, 0,
HWND_MESSAGE, // 纯消息窗口,无界面、无感知
NULL,
_hdllInstance,
NULL
);
if (g_hMsgWnd == NULL)
{
acutPrintf(_T("\n[MCP Server] Fatal: CreateWindowEx failed, err=%d.\n"), ::GetLastError());
closesocket(listenSocket);
g_listenSocket = INVALID_SOCKET;
UnregisterClass(g_szMsgClassName, _hdllInstance);
WSACleanup();
return false;
}
g_bRunning = true;
try
{
g_serverThread = std::thread(TcpServerLoop, listenSocket);
acutPrintf(_T("\n[MCP Server] Background TCP listening thread started (Port: %d, token: %s)...\n"),
g_nPort, g_sToken.empty() ? _T("none") : _T("required"));
}
catch (const std::system_error &e)
{
g_bRunning = false;
acutPrintf(_T("\n[MCP Server] Fatal error: Unable to create background listening thread! Reason: %hs\n"), e.what());
DestroyWindow(g_hMsgWnd);
g_hMsgWnd = NULL;
UnregisterClass(g_szMsgClassName, _hdllInstance);
closesocket(listenSocket);
g_listenSocket = INVALID_SOCKET;
WSACleanup();
return false;
}
// 3. 监听跑起来了才注册工具
if (!mcp_ConfigParser::LoadAndRegister(g_sConfigPath, g_mcpTools))
{
acutPrintf(_T("\n[MCP Server] Warning: Failed to load tool configuration!")
_T("\n[MCP Server] 只支持 mcp_config.json 或 cad-mcp-frame.json,且必须是合法 JSON。Automatic Shutdown.\n"));
close_mcp_server();
return false;
}
g_mcpTools.print();
return g_bRunning;
}
}