首页 / 资讯中心 / 文章详情

基于Qt的MCP LLM代理服务开发实战:从0到1扩展大语言模型

基于Qt的MCP LLM代理服务开发实战:从0到1扩展大语言模型 ★ FEATURED ARTICLE
1. 为什么 Qt 工程师需要自己写一个 MCP LLM 代理服务如果你手上有大量用 C/Qt 写的科学计算、工业控制、图像处理算法现在想让大语言模型直接调用它们最省事的路径不是把算法重写成 Python而是写一个遵循 MCP 协议的代理服务把已有函数包装成 LLM 可调用的工具。MCPModel Context Protocol本质上是 AI 客户端和工具服务器之间的 JSON-RPC 2.0 通信标准它规定了 initialize、tools/list、tools/call、ping 这几个核心方法只要你的服务端按这个格式应答Claude Desktop、AnythingLLM、Cline 这类客户端就能识别并调用你的工具。Qt 在这里的优势很直接QHttpServer 提供异步 HTTP 服务QJsonDocument/QJsonObject 处理 JSON 序列化QFileInfo、QProcess 等模块直接对接底层系统能力整个工程用 CMake 管理跨 Windows/Linux 编译。对于只会 Qt 和 C 的算法工程师来说不需要额外学 Node.js 或 Python 异步框架用熟悉的信号槽和容器类就能把 MCP 服务跑起来。这篇内容面向的是已经会 Qt Widgets 或 Qt Console 开发、想把自己的 C 能力接入大语言模型的读者。我会从工程骨架开始给出可复制的 CMake 配置、MCP 服务端关键代码、本地启动验证步骤以及和 LLM 客户端联调时最容易踩的坑。整套流程实测在 Qt 6.9 Windows 11 AnythingLLM 环境下跑通Linux 下只需调整路径分隔符。核心检索词先明确Qt MCP LLM 代理服务就是用 Qt 实现一个遵循 MCP 协议的 HTTP 服务把本地 C 函数暴露给大语言模型调用。适合谁有 Qt 基础、想让 AI 操作自己算法库的工程师。能做什么注册工具、接收 tools/call、执行本地逻辑、返回结构化结果。2. TaoToken 前置准备给代理服务接上大语言模型能力Qt 写的 MCP 服务本身只负责“工具执行”它不产生自然语言回答。真正把用户提问解析成 tools/call 请求的是 LLM 客户端。所以你需要一个能访问大语言模型 API 的入口。我实测下来用 TaoToken 作为模型接入层比较顺手它兼容 OpenAI 风格的接口Claude Code、Cline、Codex 这类工具都能直接配。先拿 API Key。打开 https://taotoken.net/api-keys 登录后创建一个新 Key复制保存。这个 Key 后面要填到客户端的配置里不要提交到 Git。如果你用的是 Claude Code 做编码代理配置方式是在项目根目录或用户目录下创建 settings.json填入 Base URL 和 Key。TaoToken 的 API 地址是 https://taotoken.net/api 模型 ID 根据你选的模型填比如 claude-sonnet-4-20250514 或 gpt-4o。一个可复制的 settings.json 片段如下{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }如果你用的是 Cline 或 Roo Code 这类 VS Code 插件在插件设置里选 OpenAI CompatibleBase URL 填 https://taotoken.net/api/v1 API Key 填刚才复制的Model ID 填对应模型名。Cline 的 MCP 配置里还要单独加 MCP Servers 段指向你本地 Qt 服务的地址。Codex 用户注意 auth.json 的写法路径通常在 ~/.codex/auth.json内容包含 api_key 和 base_url 两个字段。Base URL 同样用 https://taotoken.net/api 不要多加 /v1否则会 404。这里要强调三件套Base URL、API Key、Model ID缺一个都连不上。我见过有人只填了 Key 没改 Base URL结果请求打到默认端点报 401。也有人 Model ID 写错返回 model not found。配置完先用模型对话页面 https://taotoken.net/models 发一条测试消息确认 Key 有效再去配客户端。对于长期做编码代理或 Agent 开发的场景可以考虑 Coding Plan https://taotoken.net/coding-plan 额度更划算。但如果你只是本地跑通 MCP 联调按量付费的 API Key 就够了。3. Qt 工程骨架与 MCP 服务端可复制配置先建工程。用 Qt Creator 新建一个 Console Application或者直接手写 CMakeLists.txt。关键是链接 HttpServer 模块。Qt 6.9 的 CMake 配置如下cmake_minimum_required(VERSION 3.16) project(QtMcpServer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Core HttpServer) add_executable(QtMcpServer main.cpp mcp_server.cpp mcp_server.h ) target_link_libraries(QtMcpServer PRIVATE Qt6::Core Qt6::HttpServer )注意 HttpServer 模块在 Qt 6.4 之后才正式进入如果你用的是 Qt 5需要换 QHttpServer 的第三方实现或者升级。我实测 Qt 6.9 最稳。main.cpp 里创建 QHttpServer 实例绑定端口注册路由。MCP 客户端通常把请求发到 /mcp 这个路径所以路由要匹配 POST /mcp。同时为了处理 CORS 预检还要注册 OPTIONS 路由。#include QCoreApplication #include QHttpServer #include QHttpServerResponder #include QJsonDocument #include QJsonObject #include QJsonArray #include QFileInfo #include QDebug QHttpHeaders defaultHeader() { QHttpHeaders h; h.append(Content-Type, application/json); h.append(Cache-Control, no-cache); h.append(Access-Control-Allow-Origin, *); h.append(Access-Control-Allow-Methods, POST, OPTIONS); h.append(Access-Control-Allow-Headers, Content-Type); return h; } void sendResponse(QHttpServerResponder response, const QJsonObject obj, QHttpServerResponder::StatusCode status QHttpServerResponder::StatusCode::Ok) { QHttpServerResponse rp(QJsonDocument(obj).toJson(QJsonDocument::Compact), status); rp.setHeaders(defaultHeader()); response.sendResponse(rp); }路由注册部分把 initialize、tools/list、tools/call、ping 四个方法分发出去。MCP 请求体里 method 字段决定走哪个处理函数。int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QHttpServer server; server.route(/mcp, QHttpServerRequest::Method::Post, [](const QHttpServerRequest request, QHttpServerResponder responder) { QJsonParseError err; QJsonObject req QJsonDocument::fromJson(request.body(), err).object(); if (err.error ! QJsonParseError::NoError) { QJsonObject e{{jsonrpc,2.0},{id,QJsonValue::Null}, {error, QJsonObject{{code,-32700},{message,Parse error}}}}; sendResponse(responder, e); return; } QString method req[method].toString(); if (method initialize) { QJsonObject result{ {capabilities, QJsonObject{{tools, QJsonObject{{listChanged, true}}}}}, {protocolVersion, 2025-03-26}, {serverInfo, QJsonObject{{name,QtMcpServer},{version,1.0}}} }; QJsonObject resp{{jsonrpc,2.0},{id,req[id]},{result,result}}; sendResponse(responder, resp); } else if (method tools/list) { QJsonObject tool{ {name,fileSizeQuery}, {description,查询指定文件的大小参数 filename 为完整路径}, {inputSchema, QJsonObject{ {type,object}, {properties, QJsonObject{ {filename, QJsonObject{{type,string},{description,文件完整路径}}} }}, {required, QJsonArray{filename}} }} }; QJsonObject resp{{jsonrpc,2.0},{id,req[id]}, {result, QJsonObject{{tools, QJsonArray{tool}}}}}; sendResponse(responder, resp); } else if (method tools/call) { QJsonObject params req[params].toObject(); QString name params[name].toString(); QJsonObject args params[arguments].toObject(); if (name fileSizeQuery) { QString filename args[filename].toString(); QFileInfo info(filename); qint64 size info.exists() ? info.size() : -1; QJsonObject content{{type,text}, {text, QString(File %1 size is %2 Bytes.).arg(filename).arg(size)}}; QJsonObject resp{{jsonrpc,2.0},{id,req[id]}, {result, QJsonObject{{isError,false},{content,QJsonArray{content}}}}}; sendResponse(responder, resp); } else { QJsonObject e{{jsonrpc,2.0},{id,req[id]}, {error, QJsonObject{{code,-32601},{message,Method not found}}}}; sendResponse(responder, e); } } else if (method ping) { QJsonObject resp{{jsonrpc,2.0},{id,req[id]},{result,QJsonObject{}}}; sendResponse(responder, resp); } else { QJsonObject e{{jsonrpc,2.0},{id,req[id]}, {error, QJsonObject{{code,-32601},{message,Unknown method}}}}; sendResponse(responder, e); } }); server.route(/mcp, QHttpServerRequest::Method::Options, [](QHttpServerResponder responder) { QHttpServerResponse rp(QHttpServerResponder::StatusCode::NoContent); rp.setHeaders(defaultHeader()); responder.sendResponse(rp); }); const auto port server.listen(QHostAddress::Any, 3456); if (!port) { qCritical() Server failed to listen on port 3456; return -1; } qDebug() MCP server running on port port; return app.exec(); }这段代码可以直接编译运行。注意 QHttpServerResponder 的 sendResponse 在 Qt 6.9 里是异步的不要在里面再访问已析构的局部对象。另外 required 字段我加上了客户端会据此校验参数减少无效调用。4. 本地启动与接口联调验证步骤编译成功后运行 QtMcpServer控制台输出 “MCP server running on port 3456”。先用 curl 验证 initialize 是否正常。Windows 下用 PowerShell 的 curl 别名可能有问题建议用 Git Bash 或 WSL。curl -X POST http://127.0.0.1:3456/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:0,method:initialize,params:{protocolVersion:2025-03-26,capabilities:{},clientInfo:{name:test,version:1.0}}}预期返回{id:0,jsonrpc:2.0,result:{capabilities:{tools:{listChanged:true}},protocolVersion:2025-03-26,serverInfo:{name:QtMcpServer,version:1.0}}}接着测 tools/listcurl -X POST http://127.0.0.1:3456/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list}应该看到 fileSizeQuery 的完整定义。最后测 tools/call把 filename 换成你本地真实存在的文件路径curl -X POST http://127.0.0.1:3456/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/call,params:{name:fileSizeQuery,arguments:{filename:C:\\Windows\\System32\\notepad.exe}}}返回 content 数组里 text 字段就是文件大小。如果返回 -1说明文件不存在或路径转义有问题。Windows 路径在 JSON 里要双反斜杠或者用正斜杠。服务端跑通后去 AnythingLLM 里配置 MCP Server。配置文件 anythingllm_mcp_servers.json 在 Windows 下的路径是C:\Users\你的用户名\AppData\Roaming\anythingllm-desktop\storage\plugins\anythingllm_mcp_servers.json内容如下{ mcpServers: { qtMcpServer: { type: streamable, url: http://127.0.0.1:3456/mcp, headers: {} } } }保存后重启 AnythingLLM在“代理技能”里应该能看到 qtMcpServer 状态为 On。然后在对话里用 agent 前缀提问“agent 请查询 C:\Windows\System32\notepad.exe 的大小”。AnythingLLM 会先发 tools/call 到你 Qt 服务拿到结果后再让 LLM 组织成自然语言回答。如果你用的是 Cline在 MCP 配置里加同样的 JSON 段Cline 会自动发现工具并在需要时调用。Claude Code 的 MCP 配置在 settings.json 的 mcpServers 字段格式类似。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth联调阶段最容易卡在几个报错上我逐个说清楚。401 Unauthorized这个通常不是 Qt 服务的问题而是 LLM 客户端连 TaoToken 时 Key 或 Base URL 配错。检查三件套Base URL 必须是 https://taotoken.net/api Key 是 sk- 开头Model ID 拼写正确。如果客户端日志里出现 “invalid api key”去 https://taotoken.net/api-keys 重新生成一个。注意不要有多余空格JSON 里 Key 用双引号。local proxy failed这个报错说明客户端尝试走本地代理但连不上。常见原因是 Qt 服务没启动或者端口被占用。用 netstat -ano | findstr 3456 检查端口。如果 3456 被占改代码里的 listen 端口同时更新 anythingllm_mcp_servers.json 里的 url。另外 Windows 防火墙可能拦截第一次运行 Qt 程序时弹窗要允许专用网络访问。reading choices 报错这个一般出现在流式响应解析阶段。MCP 的 tools/call 返回是完整 JSON不是 SSE 流。如果你在客户端看到 “error reading choices”多半是 LLM API 返回格式和客户端预期不一致。检查 Model ID 是否支持 function calling。有些蒸馏太狠的小模型对 agent 支持不好换 claude-sonnet 或 gpt-4o 系列。TaoToken 的模型对话页面可以快速切换模型测试。OAuth 相关报错如果你用 Claude Code 且配置了 OAuth 登录但同时又填了 ANTHROPIC_API_KEY可能冲突。Claude Code 的 settings.json 里如果同时存在 OAuth token 和 API Key优先走 OAuth导致请求打到错误端点。解决办法是清掉 OAuth 缓存或者显式设置 ANTHROPIC_AUTH_TOKEN 为空。更简单的方式是直接用 API Key 模式在 https://taotoken.net/api-keys 拿 Key 后填到 env 里。还有一个隐蔽的坑tools/list 返回的 inputSchema 里 required 字段如果写成字符串而不是数组客户端解析会失败工具不显示。确保 required 是 QJsonArray。另外 description 里不要有换行符有些客户端解析会截断。如果工具在 AnythingLLM 里显示为 Off先看 Qt 服务控制台有没有收到 initialize 请求。没有的话是网络问题有的话是响应格式问题。用 curl 手动发一遍 initialize对比返回 JSON 和客户端期望的差异。MCP 协议版本号要一致2025-03-26 是目前主流客户端支持的版本。6. 从单工具到多会话扩展你的 Qt MCP 代理服务跑通 fileSizeQuery 之后扩展方向很明确。第一把工具注册表做成 QMapQString, std::functionQJsonObject(QJsonObject)新增工具只需注册一个 lambda不用改路由分发逻辑。第二支持流式响应MCP 的 tools/call 虽然默认返回完整 JSON但你可以用 QHttpServerResponder 分块发送配合 text/event-stream适合长时间计算任务。第三加身份验证在 defaultHeader 里检查 Authorization 头和 TaoToken 的 Key 做比对防止本地服务被外部调用。对于多会话并发QHttpServer 本身是异步的每个请求在独立的事件循环里处理。但如果你在工具函数里做阻塞 IO会卡住整个服务。解决办法是把耗时操作放到 QtConcurrent::run 里用 QFutureWatcher 监听完成信号再回调 sendResponse。注意 sendResponse 必须在主线程调用跨线程要用 QMetaObject::invokeMethod 切回来。实际项目中我把文件操作、进程调用、数据库查询分别封装成独立工具每个工具函数只做参数校验和结果格式化具体逻辑丢给已有的 C 类。这样 Qt 工程师不需要学新语言直接把现有算法类的 public 方法包装一下就能被 LLM 调用。整套代码在 Qt 6.9 CMake 下编译无警告Windows 和 Ubuntu 22.04 都验证过。最后提醒一点MCP 服务监听 0.0.0.0 有安全风险本地开发用 127.0.0.1 就够了。如果确实需要局域网访问务必加 Token 校验。TaoToken 的接入文档 https://taotoken.net/doc 里有完整的 API 说明配客户端时对照着填能省不少排查时间。
阅读完成 · 觉得有帮助?
咨询建站