feat
搭建 mcp server+mcp plugin
This commit is contained in:
1 parent
09f9d69d36
commit
5d6f1c74d1
16 files changed
+792
-229
No files matched your search
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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; \
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if _MSC_VER > 1000
|
||||
#pragma once
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "StdAfx.h"
|
||||
#include "StdAfx.h"
|
||||
|
||||
void CR_SCGXCmdFnc()
|
||||
{
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
#include "StdAfx.h"
|
||||
#include "cadsdk.h"
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
|
||||
#define WM_MCP_EXECUTE_TASK (WM_USER + 2048)
|
||||
|
||||
static std::thread g_serverThread;
|
||||
static std::atomic<bool> 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<TaskData *>(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<LPARAM>(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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace mcp
|
||||
{
|
||||
bool init_mcp();
|
||||
bool close_mcp();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "mcp_ConfigParser.h"
|
||||
#include <windows.h>
|
||||
#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<mcp_Tool_LispInline>();
|
||||
pTool->SetConfig(toolConfig);
|
||||
registry.Register(pTool);
|
||||
}
|
||||
else if (type == "lisp_file")
|
||||
{
|
||||
auto pTool = std::make_shared<mcp_Tool_LispFile>(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<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, "未找到指定的 Tool: " + name);
|
||||
}
|
||||
#pragma endregion mcp_Tools
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include <fstream>
|
||||
#include <unordered_map>
|
||||
#include "mcp_plugin_api.h"
|
||||
|
||||
class mcp_Tools
|
||||
{
|
||||
private:
|
||||
std::unordered_map<std::string, std::shared_ptr<mcp_Tool>> m_tools;
|
||||
|
||||
public:
|
||||
// 注册工具
|
||||
void Register(std::shared_ptr<mcp_Tool> 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);
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "StdAfx.h"
|
||||
#include "cadsdk.h"
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <nlohmann/json.hpp>
|
||||
#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<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(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<LPARAM>(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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace mcp
|
||||
{
|
||||
bool init_mcp_server();
|
||||
bool close_mcp_server();
|
||||
}
|
||||
@@ -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<std::string>();
|
||||
}
|
||||
else if (it.value().is_boolean())
|
||||
{
|
||||
valueStr = it.value().get<bool>() ? "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
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#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;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by newProject.rc
|
||||
//
|
||||
|
||||
Reference in new issue
Block a user