diff --git a/inc/mcp_plugin_api.h b/inc/mcp_plugin_api.h new file mode 100644 index 0000000..8b284bc --- /dev/null +++ b/inc/mcp_plugin_api.h @@ -0,0 +1,110 @@ +#pragma once +#include +#include + +class mcp_Tool_Utility +{ +public: + // 1. 纯文本响应 + static nlohmann::json make_text_result(const std::string &text) + { + return { + { "result", { + { "content", { + { { "type", "text" }, { "text", text } } + } } + } } + }; + } + + // 2. 图片响应 (传入 base64 编码的图片数据) + static nlohmann::json make_image_result(const std::string &base64Data + , const std::string &mimeType = "image/png") + { + return { + { "result", { + { "content", { + { { "type", "image" }, { "data", base64Data }, { "mimeType", mimeType } } + } } + } } + }; + } + + // 3. 图文混合响应 (文字汇报结果 + 附带 CAD 视口截图) + static nlohmann::json make_mixed_result(const std::string &text + , const std::string &base64Data + , const std::string &mimeType = "image/png") + { + return { + { "result", { + { "content", { + { { "type", "text" }, { "text", text } }, + { { "type", "image" }, { "data", base64Data }, { "mimeType", mimeType } } + } } + } } + }; + } + + static nlohmann::json make_error(int code, const std::string &message) + { + return { + { "error",{ + { "code", code }, + { "message", message } + } } + }; + } +}; + +class mcp_Tool +{ +protected: + nlohmann::json m_config; + +public: + virtual ~mcp_Tool() = default; + + void SetConfig(const nlohmann::json &config) { m_config = config; } + + // 兜底三剑客:当 JSON 中不写这些字段时,才调用这里(均可选重载) + virtual std::string private_name() const { return "unnamed_tool"; } + + virtual std::string private_description() const { return ""; } + + virtual nlohmann::json private_input_schema() const { return nlohmann::json::object(); } + + // 统一由框架接管 Name 的获取 (供路由字典 mcp_Tools::Register 调用) + virtual std::string GetName() const final { return m_config.value("name", private_name()); } + + // 统一由框架接管 ToLLM 组装 + virtual nlohmann::json ToLLM() const final + { + nlohmann::json tool; + tool["name"] = GetName(); + std::string desc = m_config.value("description", ""); + tool["description"] = desc.empty() ? private_description() : desc; + + if (m_config.contains("inputSchema") && !m_config["inputSchema"].empty()) + { + tool["inputSchema"] = m_config["inputSchema"]; + } + else + { + tool["inputSchema"] = private_input_schema(); + } + return tool; + } + + // 整个类唯一一个纯虚函数:全心全意干活! + virtual nlohmann::json Execute(const nlohmann::json &args) = 0; +}; + +#define EXPORT_MCP_TOOL(ToolClass, FactoryName) \ +extern "C" __declspec(dllexport) mcp_Tool* FactoryName() \ +{ \ + return new ToolClass(); \ +} \ +extern "C" __declspec(dllexport) void Destroy_##FactoryName(mcp_Tool* pTool) \ +{ \ + delete pTool; \ +} \ No newline at end of file diff --git a/src/AutoAgent/AutoAgent.cpp b/src/AutoAgent/AutoAgent.cpp index abae7bc..3545f0f 100644 --- a/src/AutoAgent/AutoAgent.cpp +++ b/src/AutoAgent/AutoAgent.cpp @@ -1,7 +1,7 @@ -#include "StdAfx.h" +#include "StdAfx.h" #include "resource.h" #include "cadsdk.h" -#include "mcp.h" +#include "mcp_server.h" HINSTANCE _hdllInstance = NULL ; @@ -50,7 +50,7 @@ extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) void CR_SCGXCmdFnc(); void InitApplication() { - mcp::init_mcp(); + mcp::init_mcp_server(); acrxBuildClassHierarchy(); AddCommand(_T("SHCRACKTOOLS"), _T("CR_SCGX"), _T("CR_SCGX"), ACRX_CMD_TRANSPARENT | ACRX_CMD_USEPICKSET, CR_SCGXCmdFnc); @@ -60,7 +60,7 @@ void InitApplication() void UnloadApplication() { acedRegCmds->removeGroup(_T("SHCRACKTOOLS")); - mcp::close_mcp(); + mcp::close_mcp_server(); } void AddCommand(const TCHAR* cmdGroup, const TCHAR* cmdInt, const TCHAR* cmdLoc, diff --git a/src/AutoAgent/StdAfx.cpp b/src/AutoAgent/StdAfx.cpp index c232d35..3743d0c 100644 --- a/src/AutoAgent/StdAfx.cpp +++ b/src/AutoAgent/StdAfx.cpp @@ -1,4 +1,4 @@ -// StdAfx.cpp : source file that includes just the standard includes +// StdAfx.cpp : source file that includes just the standard includes // StdAfx.pch will be the pre-compiled header // StdAfx.obj will contain the pre-compiled type information diff --git a/src/AutoAgent/StdAfx.h b/src/AutoAgent/StdAfx.h index 39a9247..3fb8896 100644 --- a/src/AutoAgent/StdAfx.h +++ b/src/AutoAgent/StdAfx.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #if _MSC_VER > 1000 #pragma once diff --git a/src/AutoAgent/cad-mcp-frame.json b/src/AutoAgent/cad-mcp-frame.json new file mode 100644 index 0000000..a7d9651 --- /dev/null +++ b/src/AutoAgent/cad-mcp-frame.json @@ -0,0 +1,95 @@ +{ + "version": "1.0.0", + "global_settings": { + "plugin_directory": "./mcp_plugins", + "lisp_directory": "./mcp_lisp", + "hot_reload": true + }, + "tools": [ + { + "name": "draw_circle_inline", + "description": "在指定的WCS坐标绘制一个圆。适用于简单的标注场景。", + "inputSchema": { + "type": "object", + "properties": { + "x": { + "type": "number", + "description": "圆心X坐标" + }, + "y": { + "type": "number", + "description": "圆心Y坐标" + }, + "r": { + "type": "number", + "description": "半径" + } + }, + "required": [ + "x", + "y", + "r" + ] + }, + "backend": { + "type": "lisp_inline", + "script_template": "(command \"_.CIRCLE\" \"{x},{y}\" \"{r}\")" + } + }, + { + "name": "auto_dimension_room", + "description": "调用外部复杂的 LISP 脚本,根据指定的房间ID自动生成四周的尺寸标注。", + "inputSchema": { + "type": "object", + "properties": { + "room_id": { + "type": "string", + "description": "房间的标识符,例如 ROOM-101" + }, + "scale": { + "type": "number", + "description": "标注全局比例因子" + }, + "layer": { + "type": "string", + "description": "标注存放的图层" + } + }, + "required": [ + "room_id", + "scale" + ] + }, + "backend": { + "type": "lisp_file", + "file_name": "auto_dimension.lsp", + "call_template": "(mcp-auto-dimension \"{room_id}\" {scale} \"{layer}\")" + } + }, + { + "name": "check_interference", + "description": "调用 C++ 底层插件,进行3D实体的高性能干涉检查,返回碰撞点坐标数组。", + "inputSchema": { + "type": "object", + "properties": { + "tolerance": { + "type": "number", + "description": "干涉容差值" + }, + "layer_filter": { + "type": "string", + "description": "需要检查的图层名,支持通配符" + } + }, + "required": [ + "tolerance" + ] + }, + "backend": { + "type": "c++_dll", + "dll_name": "mcp_hardcore_tools.dll", + "factory_function": "CreateInterferenceTool" + } + } + ] +} \ No newline at end of file diff --git a/src/AutoAgent/cmakelists.txt b/src/AutoAgent/cmakelists.txt index e016af6..f026dd9 100644 --- a/src/AutoAgent/cmakelists.txt +++ b/src/AutoAgent/cmakelists.txt @@ -3,14 +3,15 @@ cmake_minimum_required(VERSION 3.16) project(AutoAgent) find_package(nlohmann_json CONFIG REQUIRED) -find_package(CURL CONFIG REQUIRED) set(SOURCES "AutoAgent.cpp" "command.cpp" "stdafx.cpp" "AutoAgent.rc" - "mcp.cpp" + "mcp_server.cpp" + "mcp_tool.cpp" + "mcp_ConfigParser.cpp" ) add_def_file_name(SOURCES "${PROJECT_NAME}") @@ -24,7 +25,8 @@ target_set_name_and_suffix(${PROJECT_NAME} "${PROJECT_NAME}" ".arx") target_compile_options(${PROJECT_NAME} PRIVATE /Zc:forScope) +target_include_directories(${PROJECT_NAME} INTERFACE "${CMAKE_SOURCE_DIR}/inc") + target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json - CURL::libcurl ) \ No newline at end of file diff --git a/src/AutoAgent/command.cpp b/src/AutoAgent/command.cpp index 9b445e6..e1a3b86 100644 --- a/src/AutoAgent/command.cpp +++ b/src/AutoAgent/command.cpp @@ -1,4 +1,4 @@ -#include "StdAfx.h" +#include "StdAfx.h" void CR_SCGXCmdFnc() { diff --git a/src/AutoAgent/mcp.cpp b/src/AutoAgent/mcp.cpp deleted file mode 100644 index a23829b..0000000 --- a/src/AutoAgent/mcp.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "StdAfx.h" -#include "cadsdk.h" -#include -#include -#include -#include -#include - -#pragma comment(lib, "ws2_32.lib") - -#define WM_MCP_EXECUTE_TASK (WM_USER + 2048) - -static std::thread g_serverThread; -static std::atomic g_bRunning(false); -static SOCKET g_listenSocket = INVALID_SOCKET; - -static HWND g_hMsgWnd = NULL; -static const wchar_t* g_szMsgClassName = L"AutoAgentMcpMsgWndClass"; - -// 用于跨线程传递数据的结构体 -struct TaskData -{ - std::string rawJson; -}; - -// 此函数在 AutoCAD 主 UI 线程运行,绝对安全. -void ExecuteTaskInMainThread(TaskData *pTask) -{ - if (pTask == nullptr) - { - return; - } - - try - { - auto j = nlohmann::json::parse(pTask->rawJson); - if (j.contains("cmd") && j["cmd"] == "draw_circle") - { - double x = j.value("x", 0.0); - double y = j.value("y", 0.0); - double r = j.value("r", 100.0); - - AcApDocument *pDoc = acDocManager->curDocument(); - if (pDoc) - { - // 此时在主线程,安全锁文档 - Acad::ErrorStatus es = acDocManager->lockDocument(pDoc, AcAp::kWrite, nullptr, nullptr, false); - if (es == Acad::eOk) - { - AcDbDatabase *pDb = pDoc->database(); - AcDbBlockTable *pBlockTable = nullptr; - pDb->getBlockTable(pBlockTable, AcDb::kForRead); - - AcDbBlockTableRecord *pBTR = nullptr; - pBlockTable->getAt(ACDB_MODEL_SPACE, pBTR, AcDb::kForWrite); - pBlockTable->close(); - - AcGePoint3d center(x, y, 0.0); - AcGeVector3d normal(0.0, 0.0, 1.0); - AcDbCircle *pCircle = new AcDbCircle(center, normal, r); - - AcDbObjectId circleId; - pBTR->appendAcDbEntity(circleId, pCircle); - - pBTR->close(); - pCircle->close(); - - acDocManager->unlockDocument(pDoc); - acutPrintf(_T("\n[MCP Server] 成功在坐标(%.2f, %.2f)绘制了半径为%.2f的圆\n"), x, y, r); - } - } - } - } - catch (...) - { - acutPrintf(_T("\n[MCP Server] JSON 解析或绘图异常!\n")); - } -} - -// 隐式窗口的回调函数 (属于主线程) -LRESULT CALLBACK McpWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - if (message == WM_MCP_EXECUTE_TASK) - { - TaskData *pTask = reinterpret_cast(lParam); - if (pTask) - { - ExecuteTaskInMainThread(pTask); - delete pTask; // 执行完毕,清理内存 - } - return 0; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} - -// TCP 监听后台线程 -static 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(8080); - - 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->rawJson = recvBuf; - - // 关键转变:通过 PostMessage 发送给主线程拥有的隐式窗口 - if (g_hMsgWnd != NULL) - { - PostMessage(g_hMsgWnd, WM_MCP_EXECUTE_TASK, 0, reinterpret_cast(pTask)); - } - else - { - delete pTask; - } - } - closesocket(clientSocket); - } - } - } - - closesocket(g_listenSocket); - WSACleanup(); -} - -namespace mcp -{ - bool init_mcp() - { - // 1. 在主线程注册并创建 Message-Only 窗口 - WNDCLASSEX wc = { 0 }; - wc.cbSize = sizeof(WNDCLASSEX); - wc.lpfnWndProc = McpWndProc; - wc.hInstance = GetModuleHandle(NULL); - wc.lpszClassName = g_szMsgClassName; - RegisterClassEx(&wc); - - g_hMsgWnd = CreateWindowEx( - 0, - g_szMsgClassName, - L"AutoAgentMcpMsgWindow", - 0, 0, 0, 0, 0, - HWND_MESSAGE, // 纯消息窗口,无界面、无感知 - NULL, - GetModuleHandle(NULL), - NULL - ); - g_bRunning = true; - try - { - g_serverThread = std::thread(TcpServerLoop); - acutPrintf(_T("\n[MCP Server] 后台 TCP 监听线程已启动 (端口: 8080)...\n")); - } - catch (const std::system_error &e) - { - // 线程创建失败,重置状态标志并输出错误 - g_bRunning = false; - // e.what() 返回的是 const char*,所以在 acutPrintf 中要用 %hs 而不是 %s 或 %ls - acutPrintf(_T("\n[MCP Server] 致命错误:无法创建后台监听线程!原因: %hs\n"), e.what()); - } - return g_bRunning; - } - - bool close_mcp() - { - g_bRunning = false; - if (g_serverThread.joinable()) - { - g_serverThread.join(); - } - - if (g_hMsgWnd != NULL) - { - DestroyWindow(g_hMsgWnd); - UnregisterClass(g_szMsgClassName, GetModuleHandle(NULL)); - g_hMsgWnd = NULL; - } - - acutPrintf(_T("\n[MCP Server] 插件已卸载,线程已关闭。\n")); - return true; - } -} diff --git a/src/AutoAgent/mcp.h b/src/AutoAgent/mcp.h deleted file mode 100644 index 5f7c3d8..0000000 --- a/src/AutoAgent/mcp.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -namespace mcp -{ - bool init_mcp(); - bool close_mcp(); -} - - diff --git a/src/AutoAgent/mcp_ConfigParser.cpp b/src/AutoAgent/mcp_ConfigParser.cpp new file mode 100644 index 0000000..c0e39b0 --- /dev/null +++ b/src/AutoAgent/mcp_ConfigParser.cpp @@ -0,0 +1,124 @@ +#include "mcp_ConfigParser.h" +#include +#include "mcp_tool.h" + +#pragma region mcp_ConfigParser +bool mcp_ConfigParser::LoadAndRegister(const std::string &jsonFilePath, mcp_Tools ®istry) +{ + std::ifstream ifs(jsonFilePath); + if (!ifs.is_open()) return false; + + nlohmann::json config; + try { ifs >> config; } + catch (...) { return false; } // 防御非法 JSON + + std::string pluginDir = config["global_settings"].value("plugin_directory", "./mcp_plugins"); + std::string lispDir = config["global_settings"].value("lisp_directory", "./mcp_lisp"); + + for (const auto &toolConfig : config["tools"]) + { + std::string type = toolConfig["backend"].value("type", ""); + + if (type == "lisp_inline") + { + auto pTool = std::make_shared(); + pTool->SetConfig(toolConfig); + registry.Register(pTool); + } + else if (type == "lisp_file") + { + auto pTool = std::make_shared(lispDir); + pTool->SetConfig(toolConfig); + registry.Register(pTool); + } + else if (type == "c++_dll") + { + LoadDllTool(toolConfig, pluginDir, registry); + } + } + return true; +} + +void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::string &pluginDir, mcp_Tools ®istry) +{ + std::string dllName = toolConfig["backend"].value("dll_name", ""); + std::string factoryName = toolConfig["backend"].value("factory_function", ""); + + std::string fullPath = pluginDir + "\\" + dllName; // Windows 环境下拼接 DLL 路径 + + HMODULE hMod = LoadLibraryA(fullPath.c_str()); + if (!hMod) return; + + typedef mcp_Tool *(*CreateFunc)(); + typedef void (*DestroyFunc)(mcp_Tool *); + + std::string destroyName = "Destroy_" + factoryName; + CreateFunc createFn = (CreateFunc)GetProcAddress(hMod, factoryName.c_str()); + DestroyFunc destroyFn = (DestroyFunc)GetProcAddress(hMod, destroyName.c_str()); + + if (createFn && destroyFn) + { + mcp_Tool *pRealTool = createFn(); + if (pRealTool) + { + // 给真正的 DLL 实例也注入配置,以防第三方需要在内部读取 + pRealTool->SetConfig(toolConfig); + + // 实例化代理类,接管生命周期 + auto pProxy = std::make_shared(hMod, pRealTool, destroyFn); + + // 代理类继承自基类,也需要配置来处理 GetName 和 ToLLM + pProxy->SetConfig(toolConfig); + + registry.Register(pProxy); + } + else + { + FreeLibrary(hMod); + } + } + else + { + FreeLibrary(hMod); // 导出函数找不到,直接卸载 + } +} + +#pragma endregion mcp_ConfigParser + + +#pragma region mcp_Tools +// 注册工具 +void mcp_Tools::Register(std::shared_ptr pTool) +{ + if (pTool != nullptr) + { + m_tools[pTool->GetName()] = pTool; + } +} + +// 组装所有工具的描述,返回 JSON 数组 (用于 tools/list) +nlohmann::json mcp_Tools::ToLLM() const +{ + nlohmann::json arr = nlohmann::json::array(); + for (const auto &pair : m_tools) + { + arr.push_back(pair.second->ToLLM()); + } + return arr; +} + +// 路由分发执行指令 (用于 tools/call) +nlohmann::json mcp_Tools::Call(const std::string &name, const nlohmann::json &args) +{ + auto it = m_tools.find(name); + if (it != m_tools.end()) + { + return it->second->Execute(args); + } + + // 找不到工具时,返回标准的 JSON-RPC 错误对象 + return mcp_Tool_Utility::make_error(-32601, "未找到指定的 Tool: " + name); +} +#pragma endregion mcp_Tools + + diff --git a/src/AutoAgent/mcp_ConfigParser.h b/src/AutoAgent/mcp_ConfigParser.h new file mode 100644 index 0000000..c83d5b4 --- /dev/null +++ b/src/AutoAgent/mcp_ConfigParser.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include "mcp_plugin_api.h" + +class mcp_Tools +{ +private: + std::unordered_map> m_tools; + +public: + // 注册工具 + void Register(std::shared_ptr pTool); + + // 组装所有工具的描述,返回 JSON 数组 (用于 tools/list) + nlohmann::json ToLLM() const; + + // 路由分发执行指令 (用于 tools/call) + nlohmann::json Call(const std::string &name, const nlohmann::json &args); +}; + +class mcp_ConfigParser +{ +public: + static bool LoadAndRegister(const std::string &jsonFilePath, mcp_Tools ®istry); + +private: + static void LoadDllTool(const nlohmann::json &toolConfig, const std::string &pluginDir, mcp_Tools ®istry); +}; diff --git a/src/AutoAgent/mcp_server.cpp b/src/AutoAgent/mcp_server.cpp new file mode 100644 index 0000000..634b7b4 --- /dev/null +++ b/src/AutoAgent/mcp_server.cpp @@ -0,0 +1,261 @@ +#include "StdAfx.h" +#include "cadsdk.h" +#include +#include +#include +#include +#include +#include "mcp_ConfigParser.h" + +#pragma comment(lib, "ws2_32.lib") +#define WM_MCP_EXECUTE_TASK (WM_USER + 2048) +extern HINSTANCE _hdllInstance; + +namespace +{ + std::thread g_serverThread; + std::atomic 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(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(8080); + + 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(pTask)); + } + else + { + delete pTask; + } + } + } + else + { + closesocket(clientSocket); + } + } + } + + closesocket(g_listenSocket); + WSACleanup(); + } + +} + +namespace mcp +{ + 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, + GetModuleHandle(NULL), + NULL + ); + g_bRunning = true; + try + { + g_serverThread = std::thread(TcpServerLoop); + acutPrintf(_T("\n[MCP Server] 后台 TCP 监听线程已启动 (端口: 8080)...\n")); + } + catch (const std::system_error &e) + { + // 线程创建失败,重置状态标志并输出错误 + g_bRunning = false; + // e.what() 返回的是 const char*,所以在 acutPrintf 中要用 %hs 而不是 %s 或 %ls + acutPrintf(_T("\n[MCP Server] 致命错误:无法创建后台监听线程!原因: %hs\n"), e.what()); + } + + // 如果监听跑起来了,则注册tools + if (g_bRunning) + { + mcp_ConfigParser::LoadAndRegister("mcp_config.json", g_mcpTools); + } + + return g_bRunning; + } + + 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; + } + + acutPrintf(_T("\n[MCP Server] 插件已卸载,线程已关闭。\n")); + return true; + } +} diff --git a/src/AutoAgent/mcp_server.h b/src/AutoAgent/mcp_server.h new file mode 100644 index 0000000..2603155 --- /dev/null +++ b/src/AutoAgent/mcp_server.h @@ -0,0 +1,7 @@ +#pragma once + +namespace mcp +{ + bool init_mcp_server(); + bool close_mcp_server(); +} diff --git a/src/AutoAgent/mcp_tool.cpp b/src/AutoAgent/mcp_tool.cpp new file mode 100644 index 0000000..51b5eea --- /dev/null +++ b/src/AutoAgent/mcp_tool.cpp @@ -0,0 +1,111 @@ +#include "mcp_tool.h" +#include "cadsdk.h" + +namespace +{ + // UTF-8 转 宽字符 (std::wstring) + std::wstring Utf8ToWString(const std::string &utf8Str) + { + if (utf8Str.empty()) return std::wstring(); + + // 第一次调用获取需要的宽字符缓冲区大小 + int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, &utf8Str[0], (int)utf8Str.size(), NULL, 0); + std::wstring wstrTo(sizeNeeded, 0); + + // 第二次调用真正进行转换 + MultiByteToWideChar(CP_UTF8, 0, &utf8Str[0], (int)utf8Str.size(), &wstrTo[0], sizeNeeded); + return wstrTo; + } + + std::string ReplaceTemplateArgs(std::string tpl, const nlohmann::json &args) + { + for (auto it = args.begin(); it != args.end(); ++it) + { + std::string placeholder = "{" + it.key() + "}"; + std::string valueStr; + + if (it.value().is_string()) + { + valueStr = it.value().get(); + } + else if (it.value().is_boolean()) + { + valueStr = it.value().get() ? "T" : "nil"; // 适配 AutoLISP 的布尔逻辑 + } + else + { + valueStr = it.value().dump(); // 数字类型直接转字符串 + } + + size_t pos = 0; + while ((pos = tpl.find(placeholder, pos)) != std::string::npos) + { + tpl.replace(pos, placeholder.length(), valueStr); + pos += valueStr.length(); + } + } + return tpl; + } +} + +#pragma region mcp_Tool_LispInline +nlohmann::json mcp_Tool_LispInline::Execute(const nlohmann::json &args) +{ + std::string tpl = m_config["backend"].value("script_template", ""); + std::string finalScript = ReplaceTemplateArgs(tpl, args) + " "; // 尾部加空格,等同于在 CAD 命令行敲回车 + std::wstring wScript = Utf8ToWString(finalScript); + + // 异步推送到 AutoCAD 命令行执行 + acDocManager->sendStringToExecute(acDocManager->curDocument(), wScript.c_str()); + + return mcp_Tool_Utility::make_text_result("LISP内联指令已下发执行 (异步)"); +} +#pragma endregion mcp_Tool_LispInline + +#pragma region mcp_Tool_LispFile +nlohmann::json mcp_Tool_LispFile::Execute(const nlohmann::json &args) +{ + std::string fileName = m_config["backend"].value("file_name", ""); + std::string callTpl = m_config["backend"].value("call_template", ""); + + // 组装文件完整路径,并将反斜杠 \ 转换为 LISP 安全的正斜杠 / + std::string fullPath = m_lispDir + "/" + fileName; + std::replace(fullPath.begin(), fullPath.end(), '\\', '/'); + + // 组装前置加载命令,末尾的 nil 是为了防止文件不存在时 LISP 引擎报错弹窗 + std::string loadCmd = "(load \"" + fullPath + "\" nil) "; + std::string executeCmd = ReplaceTemplateArgs(callTpl, args) + " "; + + std::string finalScript = loadCmd + executeCmd; + std::wstring wScript = Utf8ToWString(finalScript); + + acDocManager->sendStringToExecute(acDocManager->curDocument(), wScript.c_str()); + + return mcp_Tool_Utility::make_text_result("LISP外部文件已加载并下发执行 (异步)"); +} +#pragma endregion mcp_Tool_LispFile + +#pragma region mcp_Tool_DllProxy +// RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块 +mcp_Tool_DllProxy::~mcp_Tool_DllProxy() +{ + if (m_pRealTool && m_destroyFunc) + { + m_destroyFunc(m_pRealTool); + } + if (m_hModule) + { + FreeLibrary(m_hModule); + } +} + +nlohmann::json mcp_Tool_DllProxy::Execute(const nlohmann::json &args) +{ + if (m_pRealTool) + { + return m_pRealTool->Execute(args); // 完美转发 + } + return mcp_Tool_Utility::make_error(-32603, "底层 DLL 工具实例已失效"); +} +#pragma endregion mcp_Tool_DllProxy + diff --git a/src/AutoAgent/mcp_tool.h b/src/AutoAgent/mcp_tool.h new file mode 100644 index 0000000..735d718 --- /dev/null +++ b/src/AutoAgent/mcp_tool.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +#include "mcp_plugin_api.h" + +class mcp_Tool_LispInline : public mcp_Tool +{ +public: + virtual nlohmann::json Execute(const nlohmann::json &args) override; +}; + +class mcp_Tool_LispFile : public mcp_Tool +{ +private: + std::string m_lispDir; + +public: + mcp_Tool_LispFile(const std::string& lispDir) : m_lispDir(lispDir) {} + + virtual nlohmann::json Execute(const nlohmann::json &args) override; +}; + +class mcp_Tool_DllProxy : public mcp_Tool +{ +private: + HMODULE m_hModule; + mcp_Tool* m_pRealTool; // 真正由第三方 DLL 创建的工具对象 + + typedef void (*DestroyFunc)(mcp_Tool*); + DestroyFunc m_destroyFunc; + +public: + mcp_Tool_DllProxy(HMODULE hMod, mcp_Tool* pReal, DestroyFunc pDestroy) + : m_hModule(hMod), m_pRealTool(pReal), m_destroyFunc(pDestroy) {} + + // RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块 + virtual ~mcp_Tool_DllProxy(); + + virtual nlohmann::json Execute(const nlohmann::json &args) override; +}; + diff --git a/src/AutoAgent/resource.h b/src/AutoAgent/resource.h index 88a4c80..1295b9b 100644 --- a/src/AutoAgent/resource.h +++ b/src/AutoAgent/resource.h @@ -1,4 +1,4 @@ -//{{NO_DEPENDENCIES}} +//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by newProject.rc //