源工程在 d:\new-start\CadProject\src\cad_mcp_frame(vcpkg 独立工程), 本次按 code2026 的 preset/CMake 体系移植到 SourceCode/Code2026/cad_mcp_frame。 移植改动: - 源码 + .def + .rc + res/ + cad-mcp-frame.json 拷入 cad_mcp_frame/; 插件接口头 mcp_plugin_api.h 放到 cad_mcp_frame/inc/ - cadsdk.h -> KTCad.h(两者除尾部 4 个 include 与 using namespace MemOper 外完全一致) - nlohmann/json 单头放 SourceCode/dep_inc/nlohmann/json.hpp, ghc::filesystem 单头放 SourceCode/dep_inc/ghc/filesystem.hpp; target_link_objectarx 已把 DEP_INC_ROOT 加进 include 路径,故无需额外配置 - 新增 cad_mcp_frame/cmakelists.txt:按 PipeHangerCmd 的写法 (add_def_file_name / target_set_name_and_suffix .arx / target_link_objectarx / target_include_directories inc / set_debug_post_build) - 补 SUB_ARX 宏:原工程由 CadProject 的 cmake 注入,code2026 只注入 ARX; 在模块自己的 CMake 里补,不动共享 functions.cmake(中望给 0) - 顶层挂载 add_custom_module_2(cad_mcp_frame CadMcpFrame "SH/Other"), 并加条件:nlohmann/json 与 ghc::filesystem 都需要 C++11+, 而 R180/R190 是 C++98,故只在 (IS_ZWCAD OR CAD_SDK_VERSION >= 20) 时编译 - 中望适配两处:_zrx.def 要导出 zcrxEntryPoint/zcrxGetApiVersion; 代码里显式声明的 acrxGetApiVersion() 不会被 SDK 宏改名, 改为 #ifdef ZWCAD 取 zcrxGetApiVersion() 验证: - R230 Debug 编译通过,cad_mcp_frame.arx 818 KB,自动部署到 c:\ShangHuaSoft\Envi3D-2026-M\R230\ - Z2026 Debug 编译通过,cad_mcp_frame.zrx 818 KB,自动部署到 ...\Z2026\ - R190/R180 按条件跳过(C++98 无法编译 nlohmann/json) - 所有 .cpp/.h 均为 UTF-8 with BOM;cmakelists.txt 保持 ASCII 无 BOM 未做:没有在 CAD 里加载实测;配置文件仍是 cad-mcp-frame.json, 而代码里找的是 mcp_config.json,加载前需要统一(详见 cad_mcp_frame-review.md); 插件工程 cad_mcp_plugins 未移植(18 个工具里 16 个依赖它)。
274 lines
6.7 KiB
C++
274 lines
6.7 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")
|
|
|
|
namespace
|
|
{
|
|
std::thread g_serverThread;
|
|
std::atomic<bool> g_bRunning(false);
|
|
SOCKET g_listenSocket = INVALID_SOCKET;
|
|
|
|
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;
|
|
};
|
|
|
|
// 处理工具查询请求
|
|
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 ¶ms)
|
|
{
|
|
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 = R"({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null})";
|
|
}
|
|
else
|
|
{
|
|
std::string method = req.value("method", "");
|
|
int msgId = req.value("id", 0);
|
|
|
|
if (method == "tools/list")
|
|
{
|
|
responseStr = HandleToolsList(msgId);
|
|
}
|
|
else if (method == "tools/call")
|
|
{
|
|
responseStr = HandleToolsCall(msgId, req.value("params", nlohmann::json::object()));
|
|
}
|
|
else
|
|
{
|
|
// 找不到方法
|
|
nlohmann::json err;
|
|
err["jsonrpc"] = "2.0";
|
|
err["id"] = msgId;
|
|
err["error"] = { {"code", -32601}, {"message", "Method not found"} };
|
|
responseStr = err.dump();
|
|
}
|
|
}
|
|
|
|
// 通过传递过来的 Socket 将结果发送回大模型端
|
|
if (pTask->clientSocket != INVALID_SOCKET)
|
|
{
|
|
send(pTask->clientSocket, responseStr.c_str(), responseStr.length(), 0);
|
|
}
|
|
}
|
|
|
|
// 隐式窗口的回调函数 (属于主线程)
|
|
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 监听后台线程
|
|
void TcpServerLoop()
|
|
{
|
|
WSADATA wsaData;
|
|
WSAStartup(MAKEWORD(2, 2), &wsaData);
|
|
|
|
g_listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
sockaddr_in serverAddr = { 0 };
|
|
serverAddr.sin_family = AF_INET;
|
|
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
|
|
serverAddr.sin_port = htons(MCP_SERVER_PORT);
|
|
|
|
bind(g_listenSocket, (sockaddr *)&serverAddr, sizeof(serverAddr));
|
|
listen(g_listenSocket, SOMAXCONN);
|
|
|
|
while (g_bRunning)
|
|
{
|
|
// 使用 select 模型实现带超时的轮询,确保插件卸载时线程能正常退出
|
|
fd_set readSet;
|
|
FD_ZERO(&readSet);
|
|
FD_SET(g_listenSocket, &readSet);
|
|
|
|
timeval timeout = { 1, 0 }; // 1秒超时机制
|
|
int ret = select(0, &readSet, nullptr, nullptr, &timeout);
|
|
|
|
if (ret > 0 && FD_ISSET(g_listenSocket, &readSet))
|
|
{
|
|
SOCKET clientSocket = accept(g_listenSocket, nullptr, nullptr);
|
|
if (clientSocket != INVALID_SOCKET)
|
|
{
|
|
char recvBuf[2048] = { 0 };
|
|
int bytesReceived = recv(clientSocket, recvBuf, sizeof(recvBuf) - 1, 0);
|
|
if (bytesReceived > 0)
|
|
{
|
|
// 将数据拷贝到堆内存,准备派发给主线程
|
|
TaskData *pTask = new TaskData;
|
|
pTask->clientSocket = clientSocket; // 记录 Socket
|
|
pTask->rawJson = recvBuf;
|
|
|
|
// 关键转变:通过 PostMessage 发送给主线程拥有的隐式窗口
|
|
if (g_hMsgWnd != NULL)
|
|
{
|
|
PostMessage(g_hMsgWnd, WM_MCP_EXECUTE_TASK, 0, reinterpret_cast<LPARAM>(pTask));
|
|
}
|
|
else
|
|
{
|
|
delete pTask;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
closesocket(clientSocket);
|
|
}
|
|
}
|
|
}
|
|
|
|
closesocket(g_listenSocket);
|
|
WSACleanup();
|
|
}
|
|
|
|
}
|
|
|
|
namespace mcp
|
|
{
|
|
bool close_mcp_server()
|
|
{
|
|
g_bRunning = false;
|
|
if (g_serverThread.joinable())
|
|
{
|
|
g_serverThread.join();
|
|
}
|
|
|
|
if (g_hMsgWnd != NULL)
|
|
{
|
|
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()
|
|
{
|
|
// 1. 在主线程注册并创建 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
|
|
);
|
|
g_bRunning = true;
|
|
try
|
|
{
|
|
g_serverThread = std::thread(TcpServerLoop);
|
|
acutPrintf(_T("\n[MCP Server] Background TCP listening thread started (Port: 8080)...\n"));
|
|
}
|
|
catch (const std::system_error &e)
|
|
{
|
|
// 线程创建失败,重置状态标志并输出错误
|
|
g_bRunning = false;
|
|
// e.what() 返回的是 const char*,所以在 acutPrintf 中要用 %hs 而不是 %s 或 %ls
|
|
acutPrintf(_T("\n[MCP Server] Fatal error: Unable to create background listening thread! Reason: %hs\n"), e.what());
|
|
}
|
|
|
|
// 如果监听跑起来了,则注册tools
|
|
if (g_bRunning)
|
|
{
|
|
if (!mcp_ConfigParser::LoadAndRegister(mcp_utils::getModelDirPath() / MCP_SERVER_CONFIG, g_mcpTools))
|
|
{
|
|
acutPrintf(_T("\n[MCP Server] Warning: Failed to load tool configuration!\n[MCP Server] Automatic Shutdown.\n"));
|
|
g_bRunning = false;
|
|
}
|
|
|
|
g_mcpTools.print();
|
|
}
|
|
|
|
return g_bRunning;
|
|
}
|
|
}
|