feat(chat): MCP stdio 工具接入

- McpServer:启动进程、initialize、tools/list、tools/call
- McpManager:多服务器、工具名冲突处理、OpenAI function 定义
- ChatClient:请求携带 tools,流式累积 tool_calls,结束后交回上层
- ChatWindow:工具调用循环(最多 5 轮)、scaffold 消息、设置里的 MCP JSON 编辑
- 无头自测 gxde-ai-see-mcptest + tests/mcp/test_server.py 验证通过
This commit is contained in:
2026-10-30 20:22:14 +08:00
parent c7865fcbf0
commit 77a4369cd6
11 changed files with 681 additions and 8 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 GXDE OS Team
# SPDX-License-Identifier: GPL-3.0-or-later
"""Tiny stdio MCP server used by tests/mcptest.cpp."""
import json
import sys
def reply(message, result):
print(json.dumps({"jsonrpc": "2.0", "id": message["id"], "result": result}), flush=True)
for line in sys.stdin:
line = line.strip()
if not line:
continue
message = json.loads(line)
method = message.get("method")
if method == "initialize":
reply(message, {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "test-server", "version": "0.1"},
})
elif method == "tools/list":
reply(message, {
"tools": [{
"name": "echo",
"description": "Echo the given text back",
"inputSchema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
}]
})
elif method == "tools/call":
arguments = message.get("params", {}).get("arguments", {})
reply(message, {
"content": [{"type": "text", "text": "echo: " + str(arguments.get("text", ""))}]
})
elif "id" in message:
reply(message, {})
+60
View File
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2026 GXDE OS Team
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Headless MCP smoke test: starts the given command as an MCP server, waits for
// tools/list and calls the first tool once.
#include <QCoreApplication>
#include <QJsonDocument>
#include <QTextStream>
#include <QTimer>
#include "chat/mcpclient.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QTextStream out(stdout);
if (argc < 3) {
out << "usage: " << argv[0] << " <command> <script> [args...]\n";
return 2;
}
QStringList args;
for (int i = 2; i < argc; ++i) {
args.append(QString::fromLocal8Bit(argv[i]));
}
McpServer server(QStringLiteral("test"), QString::fromLocal8Bit(argv[1]), args);
QObject::connect(&server, &McpServer::readyChanged, [&]() {
out << "READY tools=" << server.tools().size() << "\n";
if (server.tools().isEmpty()) {
app.exit(1);
return;
}
const QString name =
server.tools().first().toObject().value(QStringLiteral("name")).toString();
out << "TOOL " << name << "\n";
server.callTool(name, QJsonObject{ { QStringLiteral("text"),
QStringLiteral("hello") } },
[&](const QJsonObject &result) {
out << "RESULT "
<< QString::fromUtf8(
QJsonDocument(result).toJson(QJsonDocument::Compact))
<< "\n";
out.flush();
app.quit();
});
});
server.start();
QTimer::singleShot(8000, &app, [&]() {
out << "TIMEOUT\n";
out.flush();
app.exit(1);
});
return app.exec();
}