端到端实测(/b 脚本按 AecRxBase -> CoreCommand -> cad_mcp_frame 顺序加载)
暴露两个真问题:
1) 版本校验拿到垃圾值,15/16 个工具被误踢
原实现 GetProcAddress(hMod,"acrxGetApiVersion") 转函数指针调用,
和基座自己的 acrxGetApiVersion() 比数值。实测两边返回值是乱的
(1507328 / 2052390912 / 0 交替,其中 1507328=0x170000 才是正确的 23.0),
结果只有第 3 个工具加载成功,其余全被判 SDK 版本不匹配。
改为插件导出数据符号 g_mcpPluginApiVersion=(ARX<<16)|SUB_ARX
(mcp_plugin_api.h 新增 EXPORT_MCP_PLUGIN_VERSION(),每个插件 DLL 调用一次),
基座 GetProcAddress 读内存比对,不涉及调用约定;旧插件没有该符号时只告警按兼容处理。
2) 加载期 AfxMessageBox 会卡住主线程
mcp_ConfigParser 里的几处弹窗全部换成 McpLog:
同时写 %TEMP%\mcp_plugin_load.log 和命令行,并补上每个工具的
[try]/[skip] 明细(dll 是否存在、版本、create/destroy 指针)。
这也是"看起来只加载 3 个工具"的原因——弹窗把主线程挡住了。
实测结果(R230,自动化烟测):
端口 127.0.0.1:8080 6 s 内监听
tools/list 返回 18 个工具(2 lisp_inline + 16 c++_dll)
5 KB 大请求 -32601,证明整包被读完(旧实现会截断)
tools/call get_layers 真正执行插件,返回 {"count":1,"items":[{"name":"0",...}]}
加载日志 16 个 c++_dll 全部 [try] 成功,0 个 [skip]
另外把配置文件加入构建部署:copyarx.exe 能拷任意文件,照 set_debug_post_build 的写法
再加一条 POST_BUILD,把 cad-mcp-frame.json 也拷到部署目录(ZWCAD 走 zw_output_path.json)。
不部署的话 frame 起来后 LoadAndRegister 找不到配置会直接自关。
验证:R230 与 Z2026 的 frame + plugins 均编译通过并部署;R230 已做上述端到端实测。
226 lines
6.4 KiB
C++
226 lines
6.4 KiB
C++
#include "mcp_ConfigParser.h"
|
|
#include "KTCad.h"
|
|
#include <windows.h>
|
|
#include "mcp_tool.h"
|
|
#include "mcp_utils.h"
|
|
#include "ghc/filesystem.hpp"
|
|
#include "rxregsvc.h"
|
|
|
|
namespace
|
|
{
|
|
// 加载期不能弹模态框(会卡住 CAD 启动、也让后续工具注册不上),统一写日志 + 命令行输出
|
|
void McpLog(const char* pszFmt, ...)
|
|
{
|
|
char szBuf[2048];
|
|
va_list args;
|
|
va_start(args, pszFmt);
|
|
_vsnprintf_s(szBuf, sizeof(szBuf), _TRUNCATE, pszFmt, args);
|
|
va_end(args);
|
|
|
|
acutPrintf(_T("\n[MCP Server] %hs"), szBuf);
|
|
|
|
char szTmp[MAX_PATH + 1];
|
|
::memset(szTmp, 0, sizeof(szTmp));
|
|
if (::GetTempPathA(MAX_PATH, szTmp) == 0)
|
|
return;
|
|
std::string sPath(szTmp);
|
|
sPath += "mcp_plugin_load.log";
|
|
FILE* fp = NULL;
|
|
if (fopen_s(&fp, sPath.c_str(), "a") == 0 && fp != NULL)
|
|
{
|
|
fprintf(fp, "%s\n", szBuf);
|
|
fclose(fp);
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma region mcp_ConfigParser
|
|
bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Tools ®istry)
|
|
{
|
|
std::ifstream ifs(jsonFilePath);
|
|
if (!ifs.is_open()) return false;
|
|
|
|
nlohmann::json config;
|
|
try { ifs >> config; }
|
|
catch (...) { return false; } // 防御非法 JSON
|
|
|
|
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))
|
|
McpLog("lisp directory: %hs (exists=%d)", lispPath.string().c_str(), (int)fs::exists(lispPath));
|
|
McpLog("plugin directory: %hs (exists=%d)", pluginPath.string().c_str(), (int)fs::exists(pluginPath));
|
|
if (!fs::exists(pluginPath))
|
|
McpLog("WARNING: 插件目录不存在,c++_dll 工具都不会加载");
|
|
|
|
for (const auto &toolConfig : config["tools"])
|
|
{
|
|
std::string type = toolConfig["backend"].value("type", "");
|
|
|
|
if (type == "lisp_inline")
|
|
{
|
|
auto pTool = std::make_shared<mcp_Tool_LispInline>();
|
|
pTool->SetConfig(toolConfig);
|
|
registry.Register(pTool);
|
|
}
|
|
else if (type == "lisp_file" && fs::exists(lispPath))
|
|
{
|
|
auto pTool = std::make_shared<mcp_Tool_LispFile>(lispPath);
|
|
pTool->SetConfig(toolConfig);
|
|
registry.Register(pTool);
|
|
}
|
|
else if (type == "c++_dll" && fs::exists(pluginPath))
|
|
{
|
|
|
|
LoadDllTool(toolConfig, pluginPath, registry);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 中望平台用的是 zcrx* 家族的符号(ARX 下 acrxEntryPoint 会被 SDK 宏改名,
|
|
// 但这里显式声明的版本号函数不会,所以按平台分开)
|
|
#ifdef ZWCAD
|
|
extern "C" int zcrxGetApiVersion();
|
|
#define MCP_OWN_API_VERSION() zcrxGetApiVersion()
|
|
#else
|
|
extern "C" int acrxGetApiVersion();
|
|
#define MCP_OWN_API_VERSION() acrxGetApiVersion()
|
|
#endif
|
|
void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::wstring &pluginDir, mcp_Tools ®istry)
|
|
{
|
|
fs::path dllPath
|
|
= mcp_utils::relative2absolute(toolConfig["backend"].value("dll_name", "")
|
|
, fs::path(pluginDir));
|
|
std::string factoryName = toolConfig["backend"].value("factory_function", "");
|
|
|
|
if (!fs::exists(dllPath))
|
|
{
|
|
McpLog(" [skip] %hs: dll 不存在 (%hs)", factoryName.c_str(), dllPath.string().c_str());
|
|
return;
|
|
}
|
|
|
|
HMODULE hMod = LoadLibrary(dllPath.c_str());
|
|
|
|
|
|
CString verStr;
|
|
verStr.Format(_T("Major ver:%d, Minor ver:%d"), ARX, SUB_ARX);
|
|
CString ver_errMsg;
|
|
ver_errMsg.Format(
|
|
CString(_T("[MCP Server]Error: Failed to load the DLL. "))
|
|
+ CString(_T("Please check whether the version of the ObjectARX SDK used by "))
|
|
+ CString("the DLL matches the current ARX version(%s).\n"), verStr );
|
|
|
|
if (!hMod)
|
|
{
|
|
McpLog(" [skip] %hs: 版本或导出校验失败", factoryName.c_str());
|
|
return;
|
|
}
|
|
|
|
// 1. 版本校验:读插件导出的数据符号 g_mcpPluginApiVersion(见 mcp_plugin_api.h)。
|
|
// 不用 acrxGetApiVersion 函数指针比对——实测通过 GetProcAddress 调它拿到的是垃圾值,
|
|
// 会把 15/16 个工具误判成"版本不匹配"。
|
|
const int nMine = ((ARX) << 16) | (SUB_ARX);
|
|
const int *pDllVer = (const int *)::GetProcAddress(hMod, "g_mcpPluginApiVersion");
|
|
if (pDllVer == NULL)
|
|
{
|
|
McpLog(" [warn] %hs: 插件未声明 SDK 版本(旧插件),按兼容处理", factoryName.c_str());
|
|
}
|
|
else if (*pDllVer != nMine)
|
|
{
|
|
McpLog(" [skip] %hs: SDK 版本不匹配 (base=0x%x dll=0x%x)",
|
|
factoryName.c_str(), nMine, *pDllVer);
|
|
FreeLibrary(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());
|
|
|
|
McpLog(" [try ] %hs: create=%p destroy=%p", factoryName.c_str(), (void*)createFn, (void*)destroyFn);
|
|
if (createFn && destroyFn)
|
|
{
|
|
mcp_Tool *pRealTool = createFn();
|
|
if (pRealTool)
|
|
{
|
|
// 给真正的 DLL 实例也注入配置,以防第三方需要在内部读取
|
|
std::string cfgStr = toolConfig.dump();
|
|
pRealTool->SetConfigAbiSafe(cfgStr.c_str());
|
|
//pRealTool->SetConfig(toolConfig);
|
|
|
|
// 实例化代理类,接管生命周期
|
|
auto pProxy = std::make_shared<mcp_Tool_DllProxy>(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<mcp_Tool> 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, L"Specified tool not found: " + mcp_utils::utf8ToWString(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
|
|
|
|
|