From 1435b864f81fa56ef11e8d493711fbabf2ee5660 Mon Sep 17 00:00:00 2001 From: LittleGuo Date: Fri, 2 Oct 2026 20:39:34 +0800 Subject: [PATCH] refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 mcp-server 功能 --- src/AutoAgent/AutoAgent.cpp | 6 +-- src/AutoAgent/cad-mcp-frame.json | 2 +- src/AutoAgent/cmakelists.txt | 2 + src/AutoAgent/mcp_ConfigParser.cpp | 56 ++++++++++++++++++------ src/AutoAgent/mcp_ConfigParser.h | 8 +++- src/AutoAgent/mcp_server.cpp | 60 +++++++++++++++----------- src/AutoAgent/mcp_tool.cpp | 69 ++++++++++++++++-------------- src/AutoAgent/mcp_tool.h | 4 +- src/AutoAgent/mcp_utils.cpp | 49 +++++++++++++++++++++ src/AutoAgent/mcp_utils.h | 22 ++++++++++ test_autoagent.py | 46 +++++++++++++------- vcpkg.json | 3 +- 12 files changed, 232 insertions(+), 95 deletions(-) create mode 100644 src/AutoAgent/mcp_utils.cpp create mode 100644 src/AutoAgent/mcp_utils.h diff --git a/src/AutoAgent/AutoAgent.cpp b/src/AutoAgent/AutoAgent.cpp index 3545f0f..f16bdad 100644 --- a/src/AutoAgent/AutoAgent.cpp +++ b/src/AutoAgent/AutoAgent.cpp @@ -38,9 +38,12 @@ extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) case AcRx::kInitAppMsg: acrxDynamicLinker->unlockApplication(pkt); acrxDynamicLinker->registerAppMDIAware(pkt); + if (!mcp::init_mcp_server()) + return AcRx::kRetError; InitApplication(); break; case AcRx::kUnloadAppMsg: + mcp::close_mcp_server(); UnloadApplication(); break; } @@ -50,8 +53,6 @@ extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) void CR_SCGXCmdFnc(); void InitApplication() { - mcp::init_mcp_server(); - acrxBuildClassHierarchy(); AddCommand(_T("SHCRACKTOOLS"), _T("CR_SCGX"), _T("CR_SCGX"), ACRX_CMD_TRANSPARENT | ACRX_CMD_USEPICKSET, CR_SCGXCmdFnc); @@ -60,7 +61,6 @@ void InitApplication() void UnloadApplication() { acedRegCmds->removeGroup(_T("SHCRACKTOOLS")); - mcp::close_mcp_server(); } void AddCommand(const TCHAR* cmdGroup, const TCHAR* cmdInt, const TCHAR* cmdLoc, diff --git a/src/AutoAgent/cad-mcp-frame.json b/src/AutoAgent/cad-mcp-frame.json index a7d9651..27facbc 100644 --- a/src/AutoAgent/cad-mcp-frame.json +++ b/src/AutoAgent/cad-mcp-frame.json @@ -2,7 +2,7 @@ "version": "1.0.0", "global_settings": { "plugin_directory": "./mcp_plugins", - "lisp_directory": "./mcp_lisp", + "lisp_directory": "./mcp_plugins", "hot_reload": true }, "tools": [ diff --git a/src/AutoAgent/cmakelists.txt b/src/AutoAgent/cmakelists.txt index f026dd9..270e319 100644 --- a/src/AutoAgent/cmakelists.txt +++ b/src/AutoAgent/cmakelists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.16) project(AutoAgent) find_package(nlohmann_json CONFIG REQUIRED) +find_package(ghc_filesystem CONFIG REQUIRED) set(SOURCES "AutoAgent.cpp" @@ -12,6 +13,7 @@ set(SOURCES "mcp_server.cpp" "mcp_tool.cpp" "mcp_ConfigParser.cpp" + "mcp_utils.cpp" ) add_def_file_name(SOURCES "${PROJECT_NAME}") diff --git a/src/AutoAgent/mcp_ConfigParser.cpp b/src/AutoAgent/mcp_ConfigParser.cpp index c0e39b0..7a24c7c 100644 --- a/src/AutoAgent/mcp_ConfigParser.cpp +++ b/src/AutoAgent/mcp_ConfigParser.cpp @@ -1,9 +1,12 @@ -#include "mcp_ConfigParser.h" +#include "mcp_ConfigParser.h" +#include "cadsdk.h" #include #include "mcp_tool.h" +#include "mcp_utils.h" +#include "ghc/filesystem.hpp" #pragma region mcp_ConfigParser -bool mcp_ConfigParser::LoadAndRegister(const std::string &jsonFilePath, mcp_Tools ®istry) +bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Tools ®istry) { std::ifstream ifs(jsonFilePath); if (!ifs.is_open()) return false; @@ -12,8 +15,15 @@ bool mcp_ConfigParser::LoadAndRegister(const std::string &jsonFilePath, mcp_Tool 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"); + fs::path pluginPath + = mcp_utils::relative2absolute(config["global_settings"].value("plugin_directory", "./mcp_plugins")); + fs::path lispPath + = mcp_utils::relative2absolute(config["global_settings"].value("lisp_directory", "./mcp_lisp")); + + if (!fs::exists(lispPath)) + AfxMessageBox(CString(_T("[MCP Server]Warning: The LISP script directory does not exist; the associated tools will not load!\n")) + pluginPath.c_str()); + if (!fs::exists(pluginPath)) + AfxMessageBox(CString(_T("[MCP Server]Warning: The plugin DLL directory does not exist; the associated tools will not load!\n")) + pluginPath.c_str()); for (const auto &toolConfig : config["tools"]) { @@ -25,28 +35,35 @@ bool mcp_ConfigParser::LoadAndRegister(const std::string &jsonFilePath, mcp_Tool pTool->SetConfig(toolConfig); registry.Register(pTool); } - else if (type == "lisp_file") + else if (type == "lisp_file" && fs::exists(lispPath)) { - auto pTool = std::make_shared(lispDir); + auto pTool = std::make_shared(lispPath); pTool->SetConfig(toolConfig); registry.Register(pTool); } - else if (type == "c++_dll") + else if (type == "c++_dll" && fs::exists(pluginPath)) { - LoadDllTool(toolConfig, pluginDir, registry); + + LoadDllTool(toolConfig, pluginPath, registry); } } return true; } -void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::string &pluginDir, mcp_Tools ®istry) +void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::wstring &pluginDir, mcp_Tools ®istry) { - std::string dllName = toolConfig["backend"].value("dll_name", ""); + fs::path dllPath + = mcp_utils::relative2absolute(toolConfig["backend"].value("dll_name", "") + , fs::path(pluginDir)); std::string factoryName = toolConfig["backend"].value("factory_function", ""); - std::string fullPath = pluginDir + "\\" + dllName; // Windows 环境下拼接 DLL 路径 + if (!fs::exists(dllPath)) + { + AfxMessageBox(CString(_T("[MCP Server]Warning: The DLL file does not exist; the associated tools will not load!\n")) + dllPath.c_str()); + return; + } - HMODULE hMod = LoadLibraryA(fullPath.c_str()); + HMODULE hMod = LoadLibrary(dllPath.c_str()); if (!hMod) return; typedef mcp_Tool *(*CreateFunc)(); @@ -117,7 +134,20 @@ nlohmann::json mcp_Tools::Call(const std::string &name, const nlohmann::json &ar } // 找不到工具时,返回标准的 JSON-RPC 错误对象 - return mcp_Tool_Utility::make_error(-32601, "未找到指定的 Tool: " + name); + return mcp_Tool_Utility::make_error(-32601, "Specified tool not found: " + name); +} + +void mcp_Tools::print() +{ + for (const auto &pair : m_tools) + { + acutPrintf(_T("Tool: %s Already registered.\n"), mcp_utils::utf8ToWString(pair.first.c_str()).c_str()); + } +} + +void mcp_Tools::clear() +{ + m_tools.clear(); } #pragma endregion mcp_Tools diff --git a/src/AutoAgent/mcp_ConfigParser.h b/src/AutoAgent/mcp_ConfigParser.h index c83d5b4..59b0988 100644 --- a/src/AutoAgent/mcp_ConfigParser.h +++ b/src/AutoAgent/mcp_ConfigParser.h @@ -17,13 +17,17 @@ public: // 路由分发执行指令 (用于 tools/call) nlohmann::json Call(const std::string &name, const nlohmann::json &args); + + void print(); + + void clear(); }; class mcp_ConfigParser { public: - static bool LoadAndRegister(const std::string &jsonFilePath, mcp_Tools ®istry); + static bool LoadAndRegister(const std::wstring &jsonFilePath, mcp_Tools ®istry); private: - static void LoadDllTool(const nlohmann::json &toolConfig, const std::string &pluginDir, mcp_Tools ®istry); + static void LoadDllTool(const nlohmann::json &toolConfig, const std::wstring &pluginDir, mcp_Tools ®istry); }; diff --git a/src/AutoAgent/mcp_server.cpp b/src/AutoAgent/mcp_server.cpp index 634b7b4..3745daa 100644 --- a/src/AutoAgent/mcp_server.cpp +++ b/src/AutoAgent/mcp_server.cpp @@ -6,11 +6,15 @@ #include #include #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; @@ -142,7 +146,7 @@ namespace 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); + serverAddr.sin_port = htons(MCP_SERVER_PORT); bind(g_listenSocket, (sockaddr *)&serverAddr, sizeof(serverAddr)); listen(g_listenSocket, SOMAXCONN); @@ -197,6 +201,27 @@ namespace 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 窗口 @@ -214,48 +239,35 @@ namespace mcp 0, 0, 0, 0, 0, HWND_MESSAGE, // 纯消息窗口,无界面、无感知 NULL, - GetModuleHandle(NULL), + _hdllInstance, NULL ); g_bRunning = true; try { g_serverThread = std::thread(TcpServerLoop); - acutPrintf(_T("\n[MCP Server] 后台 TCP 监听线程已启动 (端口: 8080)...\n")); + 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] 致命错误:无法创建后台监听线程!原因: %hs\n"), e.what()); + acutPrintf(_T("\n[MCP Server] Fatal error: Unable to create background listening thread! Reason: %hs\n"), e.what()); } // 如果监听跑起来了,则注册tools if (g_bRunning) { - mcp_ConfigParser::LoadAndRegister("mcp_config.json", g_mcpTools); + 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; } - - 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_tool.cpp b/src/AutoAgent/mcp_tool.cpp index 51b5eea..2aadc1d 100644 --- a/src/AutoAgent/mcp_tool.cpp +++ b/src/AutoAgent/mcp_tool.cpp @@ -1,24 +1,14 @@ -#include "mcp_tool.h" -#include "cadsdk.h" +#include "cadsdk.h" +#include "mcp_tool.h" +#include "mcp_utils.h" namespace { - // UTF-8 转 宽字符 (std::wstring) - std::wstring Utf8ToWString(const std::string &utf8Str) + bool ReplaceTemplateArgs(const std::string &_tpl, const nlohmann::json &args, std::wstring &outScript) { - if (utf8Str.empty()) return std::wstring(); + if (_tpl.empty()) return false; - // 第一次调用获取需要的宽字符缓冲区大小 - 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) - { + std::string tpl(_tpl); for (auto it = args.begin(); it != args.end(); ++it) { std::string placeholder = "{" + it.key() + "}"; @@ -44,7 +34,8 @@ namespace pos += valueStr.length(); } } - return tpl; + outScript = mcp_utils::utf8ToWString(tpl); + return true; } } @@ -52,36 +43,48 @@ namespace 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); + std::wstring finalScript; + + if (!ReplaceTemplateArgs(tpl, args, finalScript)) + return mcp_Tool_Utility::make_error(-32603, "Script template replacement failed."); + + finalScript += _T(" "); // 尾部加空格,等同于在 CAD 命令行敲回车 // 异步推送到 AutoCAD 命令行执行 - acDocManager->sendStringToExecute(acDocManager->curDocument(), wScript.c_str()); + acDocManager->sendStringToExecute(acDocManager->curDocument(), finalScript.c_str()); - return mcp_Tool_Utility::make_text_result("LISP内联指令已下发执行 (异步)"); + return mcp_Tool_Utility::make_text_result("LISP inline command issued for execution (asynchronous)"); } #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", ""); + fs::path filePath + = mcp_utils::relative2absolute(m_config["backend"].value("file_name", "") + , fs::path(m_lispDir)); + + if (!fs::exists(filePath)) + { + return mcp_Tool_Utility::make_error(-32603, "LISP file does not exist: " + filePath.string()); + } + 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::wstring loadCmd = std::wstring(_T("(load \"")) + filePath.generic_wstring() + _T("\" nil) "); + std::wstring executeCmd; + if (!ReplaceTemplateArgs(callTpl, args, executeCmd)) + { + return mcp_Tool_Utility::make_error(-32603, "Script template replacement failed."); + } + executeCmd += _T(" "); - std::string finalScript = loadCmd + executeCmd; - std::wstring wScript = Utf8ToWString(finalScript); + std::wstring finalScript = loadCmd + executeCmd; - acDocManager->sendStringToExecute(acDocManager->curDocument(), wScript.c_str()); + acDocManager->sendStringToExecute(acDocManager->curDocument(), finalScript.c_str()); - return mcp_Tool_Utility::make_text_result("LISP外部文件已加载并下发执行 (异步)"); + return mcp_Tool_Utility::make_text_result("LISP external file loaded and issued for execution (asynchronous)"); } #pragma endregion mcp_Tool_LispFile @@ -105,7 +108,7 @@ nlohmann::json mcp_Tool_DllProxy::Execute(const nlohmann::json &args) { return m_pRealTool->Execute(args); // 完美转发 } - return mcp_Tool_Utility::make_error(-32603, "底层 DLL 工具实例已失效"); + return mcp_Tool_Utility::make_error(-32603, "Underlying DLL tool instance is invalid"); } #pragma endregion mcp_Tool_DllProxy diff --git a/src/AutoAgent/mcp_tool.h b/src/AutoAgent/mcp_tool.h index 735d718..06b6525 100644 --- a/src/AutoAgent/mcp_tool.h +++ b/src/AutoAgent/mcp_tool.h @@ -13,10 +13,10 @@ public: class mcp_Tool_LispFile : public mcp_Tool { private: - std::string m_lispDir; + std::wstring m_lispDir; public: - mcp_Tool_LispFile(const std::string& lispDir) : m_lispDir(lispDir) {} + mcp_Tool_LispFile(const std::wstring& lispDir) : m_lispDir(lispDir) {} virtual nlohmann::json Execute(const nlohmann::json &args) override; }; diff --git a/src/AutoAgent/mcp_utils.cpp b/src/AutoAgent/mcp_utils.cpp new file mode 100644 index 0000000..1bab773 --- /dev/null +++ b/src/AutoAgent/mcp_utils.cpp @@ -0,0 +1,49 @@ +#pragma once +#include "mcp_utils.h" +#include +#include +#include + +namespace mcp_utils +{ + 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; + } + + bool mcp_utils::hasFileExist(const std::wstring &filePath) { return _taccess(filePath.c_str(), 0) == 0; } + + bool mcp_utils::hasDirExist(const std::wstring &filePath) { return _taccess(filePath.c_str(), 0) == 0; } + + std::wstring getModelDir() + { + TCHAR szPath[MAX_PATH]; + ::GetModuleFileName(_hdllInstance, szPath, MAX_PATH); + ghc::filesystem::path path(szPath); + return path.parent_path().wstring(); + } + + fs::path getModelDirPath() + { + return fs::path(getModelDir()); + } + + fs::path relative2absolute(const std::string &relativePath, const fs::path &rootPath) + { + return relative2absolute(fs::path(utf8ToWString(relativePath)), rootPath); + } + + fs::path relative2absolute(const fs::path &relativePath, const fs::path &rootPath) + { + fs::path absolutePath = rootPath / relativePath; + return absolutePath.lexically_normal().wstring(); + } +} diff --git a/src/AutoAgent/mcp_utils.h b/src/AutoAgent/mcp_utils.h new file mode 100644 index 0000000..c499579 --- /dev/null +++ b/src/AutoAgent/mcp_utils.h @@ -0,0 +1,22 @@ +#pragma once +#include "ghc/filesystem.hpp" + +namespace fs = ghc::filesystem; + +namespace mcp_utils +{ + std::wstring utf8ToWString(const std::string &utf8Str); + + bool hasFileExist(const std::wstring &filePath); + bool hasDirExist(const std::wstring &filePath); + + std::wstring getModelDir(); + + ghc::filesystem::path getModelDirPath(); + + fs::path relative2absolute(const std::string &relativePath + , const fs::path &rootPath = getModelDirPath()); + + fs::path relative2absolute(const fs::path &relativePath + , const fs::path &rootPath = getModelDirPath()); +} diff --git a/test_autoagent.py b/test_autoagent.py index 9b4b265..e17cd8a 100644 --- a/test_autoagent.py +++ b/test_autoagent.py @@ -1,20 +1,34 @@ import socket import json -# 模拟大模型生成的 JSON 指令 -command = { - "cmd": "draw_circle", - "x": 500.0, - "y": 500.0, - "r": 250.0 -} +def send_request(req_data): + try: + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect(("127.0.0.1", 8080)) + client.sendall(json.dumps(req_data).encode('utf-8')) + response = client.recv(4096) + print(">>> 收到回复:") + print(json.dumps(json.loads(response.decode('utf-8')), indent=2, ensure_ascii=False)) + client.close() + except Exception as e: + print(f"连接失败: {e}") -try: - client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - client.connect(("127.0.0.1", 8080)) - # 发送序列化后的 JSON 字符串 - client.sendall(json.dumps(command).encode('utf-8')) - client.close() - print("指令发送成功,请查看 AutoCAD 屏幕。") -except Exception as e: - print(f"连接失败: {e}") \ No newline at end of file +if __name__ == "__main__": + print("=== 测试 LISP 外部文件挂载 ===") + request_call = { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "draw_conference_table", + "arguments": { + "cx": 1000.0, + "cy": 1000.0, + "length": 1500.0, + "width": 600.0, + "chair_count": 4, + "layer": "MCP_CONF_ROOM" + } + } + } + send_request(request_call) \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index 864f124..7126409 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,7 +3,8 @@ "version": "1.0.0", "dependencies": [ "nlohmann-json", - "curl" + "curl", + "ghc-filesystem" ], "builtin-baseline": "127402f1c75bb3d5ff6bce04b285faa4930a5aca" } \ No newline at end of file