feat: Add qodeassist mcp server

This commit is contained in:
Petr Mironychev
2026-04-23 02:19:19 +02:00
parent ca0a47b160
commit 7a551ed384
9 changed files with 511 additions and 5 deletions

View File

@@ -155,6 +155,7 @@ add_qtc_plugin(QodeAssist
tools/ReadFileTool.hpp tools/ReadFileTool.cpp tools/ReadFileTool.hpp tools/ReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
tools/TodoTool.hpp tools/TodoTool.cpp tools/TodoTool.hpp tools/TodoTool.cpp
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
) )
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)

122
mcp/McpServerManager.cpp Normal file
View File

@@ -0,0 +1,122 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "McpServerManager.hpp"
#include <LLMQore/BaseTool.hpp>
#include <LLMQore/McpHttpServerTransport.hpp>
#include <LLMQore/McpServer.hpp>
#include <LLMQore/McpTypes.hpp>
#include <LLMQore/Version.hpp>
#include <QHostAddress>
#include <logger/Logger.hpp>
#include <settings/McpSettings.hpp>
#include "tools/BuildProjectTool.hpp"
#include "tools/CreateNewFileTool.hpp"
#include "tools/EditFileTool.hpp"
#include "tools/ExecuteTerminalCommandTool.hpp"
#include "tools/FindFileTool.hpp"
#include "tools/GetIssuesListTool.hpp"
#include "tools/ListProjectFilesTool.hpp"
#include "tools/ProjectSearchTool.hpp"
#include "tools/ReadFileTool.hpp"
#include "tools/TodoTool.hpp"
namespace QodeAssist::Mcp {
McpServerManager::McpServerManager(QObject *parent)
: QObject(parent)
{}
McpServerManager::~McpServerManager()
{
stopServer();
}
void McpServerManager::init()
{
auto &s = Settings::mcpSettings();
auto onChanged = [this] { reconfigure(); };
connect(&s.enableMcpServer, &Utils::BaseAspect::changed, this, onChanged);
connect(&s.mcpServerPort, &Utils::BaseAspect::changed, this, onChanged);
reconfigure();
}
void McpServerManager::reconfigure()
{
auto &s = Settings::mcpSettings();
const bool wantRunning = s.enableMcpServer();
const quint16 wantPort = static_cast<quint16>(s.mcpServerPort());
if (wantRunning && m_server && m_runningPort == wantPort)
return;
stopServer();
if (wantRunning) {
startServer();
}
}
void McpServerManager::startServer()
{
auto &s = Settings::mcpSettings();
::LLMQore::Mcp::HttpServerConfig cfg;
cfg.address = QHostAddress::LocalHost;
cfg.port = static_cast<quint16>(s.mcpServerPort());
cfg.path = QStringLiteral("/mcp");
m_transport = new ::LLMQore::Mcp::McpHttpServerTransport(cfg, this);
::LLMQore::Mcp::McpServerConfig scfg;
scfg.serverInfo = {"QodeAssist",
QStringLiteral(LLMQORE_VERSION_STRING)};
scfg.instructions = tr("QodeAssist MCP server exposing Qt Creator project tools.");
m_server = new ::LLMQore::Mcp::McpServer(m_transport.data(), scfg, this);
m_server->addTool(new Tools::ListProjectFilesTool(m_server));
m_server->addTool(new Tools::FindFileTool(m_server));
m_server->addTool(new Tools::ReadFileTool(m_server));
m_server->addTool(new Tools::ProjectSearchTool(m_server));
m_server->addTool(new Tools::CreateNewFileTool(m_server));
m_server->addTool(new Tools::EditFileTool(m_server));
m_server->addTool(new Tools::BuildProjectTool(m_server));
m_server->addTool(new Tools::GetIssuesListTool(m_server));
m_server->addTool(new Tools::ExecuteTerminalCommandTool(m_server));
m_server->addTool(new Tools::TodoTool(m_server));
m_server->start();
if (!m_transport->isOpen()) {
LOG_MESSAGE(QString("QodeAssist MCP server failed to start on port %1").arg(cfg.port));
stopServer();
return;
}
m_runningPort = m_transport->serverPort();
LOG_MESSAGE(QString("QodeAssist MCP server listening on http://127.0.0.1:%1/mcp")
.arg(m_runningPort));
}
void McpServerManager::stopServer()
{
if (m_server) {
m_server->stop();
m_server->deleteLater();
m_server.clear();
}
if (m_transport) {
m_transport->deleteLater();
m_transport.clear();
}
m_runningPort = 0;
}
} // namespace QodeAssist::Mcp

35
mcp/McpServerManager.hpp Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QPointer>
namespace LLMQore::Mcp {
class McpServer;
class McpHttpServerTransport;
} // namespace LLMQore::Mcp
namespace QodeAssist::Mcp {
class McpServerManager : public QObject
{
Q_OBJECT
public:
explicit McpServerManager(QObject *parent = nullptr);
~McpServerManager() override;
void init();
private:
void reconfigure();
void startServer();
void stopServer();
QPointer<::LLMQore::Mcp::McpServer> m_server;
QPointer<::LLMQore::Mcp::McpHttpServerTransport> m_transport;
quint16 m_runningPort = 0;
};
} // namespace QodeAssist::Mcp

View File

@@ -37,6 +37,7 @@
#include "pluginllmcore/PromptProviderFim.hpp" #include "pluginllmcore/PromptProviderFim.hpp"
#include "pluginllmcore/ProvidersManager.hpp" #include "pluginllmcore/ProvidersManager.hpp"
#include "logger/RequestPerformanceLogger.hpp" #include "logger/RequestPerformanceLogger.hpp"
#include "mcp/McpServerManager.hpp"
#include "providers/Providers.hpp" #include "providers/Providers.hpp"
#include "settings/ChatAssistantSettings.hpp" #include "settings/ChatAssistantSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
@@ -163,6 +164,9 @@ public:
Settings::setupProjectPanel(); Settings::setupProjectPanel();
ConfigurationManager::instance().init(); ConfigurationManager::instance().init();
m_mcpServerManager = new Mcp::McpServerManager(this);
m_mcpServerManager->init();
if (Settings::generalSettings().enableCheckUpdate()) { if (Settings::generalSettings().enableCheckUpdate()) {
QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates); QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates);
} }
@@ -298,6 +302,7 @@ private:
UpdateStatusWidget *m_statusWidget{nullptr}; UpdateStatusWidget *m_statusWidget{nullptr};
QString m_lastRefactorInstructions; QString m_lastRefactorInstructions;
QScopedPointer<Chat::ChatView> m_chatView; QScopedPointer<Chat::ChatView> m_chatView;
QPointer<Mcp::McpServerManager> m_mcpServerManager;
}; };
} // namespace QodeAssist::Internal } // namespace QodeAssist::Internal

View File

@@ -5,8 +5,9 @@
#include <utils/aspects.h> #include <utils/aspects.h>
#include <utils/layoutbuilder.h> #include <utils/layoutbuilder.h>
#include <QPushButton>
#include <QIcon> #include <QIcon>
#include <QPushButton>
#include <QTimer>
class ButtonAspect : public Utils::BaseAspect class ButtonAspect : public Utils::BaseAspect
{ {
@@ -21,17 +22,25 @@ public:
{ {
auto button = new QPushButton(m_buttonText); auto button = new QPushButton(m_buttonText);
button->setVisible(m_visible); button->setVisible(m_visible);
button->setAutoDefault(false);
button->setDefault(false);
button->setFocusPolicy(Qt::TabFocus);
QTimer::singleShot(0, button, [button] {
button->setAutoDefault(false);
button->setDefault(false);
});
if (!m_icon.isNull()) { if (!m_icon.isNull()) {
button->setIcon(m_icon); button->setIcon(m_icon);
button->setText(""); // Clear text if icon is set button->setText("");
} }
if (m_isCompact) { if (m_isCompact) {
button->setMaximumWidth(30); button->setMaximumWidth(30);
button->setToolTip(m_tooltip.isEmpty() ? m_buttonText : m_tooltip); button->setToolTip(m_tooltip.isEmpty() ? m_buttonText : m_tooltip);
} }
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked); connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);
connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible); connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible);
parent.addItem(button); parent.addItem(button);

View File

@@ -9,6 +9,7 @@ add_library(QodeAssistSettings STATIC
ChatAssistantSettings.hpp ChatAssistantSettings.cpp ChatAssistantSettings.hpp ChatAssistantSettings.cpp
QuickRefactorSettings.hpp QuickRefactorSettings.cpp QuickRefactorSettings.hpp QuickRefactorSettings.cpp
ToolsSettings.hpp ToolsSettings.cpp ToolsSettings.hpp ToolsSettings.cpp
McpSettings.hpp McpSettings.cpp
SettingsDialog.hpp SettingsDialog.cpp SettingsDialog.hpp SettingsDialog.cpp
ProjectSettings.hpp ProjectSettings.cpp ProjectSettings.hpp ProjectSettings.cpp
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp

296
settings/McpSettings.cpp Normal file
View File

@@ -0,0 +1,296 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "McpSettings.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <QApplication>
#include <QClipboard>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFontDatabase>
#include <QLabel>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QTabWidget>
#include <QVBoxLayout>
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include "SettingsUtils.hpp"
namespace QodeAssist::Settings {
McpSettings &mcpSettings()
{
static McpSettings settings;
return settings;
}
McpSettings::McpSettings()
{
setAutoApply(false);
setDisplayName(Tr::tr("MCP"));
enableMcpServer.setSettingsKey(Constants::MCP_ENABLE_SERVER);
enableMcpServer.setLabelText(Tr::tr("Enable MCP server"));
enableMcpServer.setToolTip(
Tr::tr("Expose QodeAssist tools to external MCP clients over HTTP. "
"Which tools are visible is controlled on the client side."));
enableMcpServer.setDefaultValue(false);
mcpServerPort.setSettingsKey(Constants::MCP_SERVER_PORT);
mcpServerPort.setLabelText(Tr::tr("Server port"));
mcpServerPort.setToolTip(
Tr::tr("TCP port the MCP server listens on (localhost only). "
"Requires restart of the server after change."));
mcpServerPort.setRange(1, 65535);
mcpServerPort.setDefaultValue(3456);
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
showConnectionInstructions.m_buttonText = Tr::tr("How to connect...");
readSettings();
setupConnections();
setLayouter([this]() {
using namespace Layouting;
return Column{
Row{Stretch{1}, resetToDefaults},
Space{8},
Group{
title(Tr::tr("Server")),
Column{
enableMcpServer,
mcpServerPort,
Row{Stretch{1}, showConnectionInstructions}}},
Stretch{1}};
});
}
void McpSettings::setupConnections()
{
connect(
&resetToDefaults,
&ButtonAspect::clicked,
this,
&McpSettings::resetSettingsToDefaults);
connect(
&showConnectionInstructions,
&ButtonAspect::clicked,
this,
&McpSettings::showConnectionInstructionsDialog);
}
void McpSettings::resetSettingsToDefaults()
{
QMessageBox::StandardButton reply;
reply = QMessageBox::question(
Core::ICore::dialogParent(),
Tr::tr("Reset Settings"),
Tr::tr("Are you sure you want to reset all settings to default values?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
resetAspect(enableMcpServer);
resetAspect(mcpServerPort);
writeSettings();
}
}
void McpSettings::showConnectionInstructionsDialog()
{
const quint16 port = static_cast<quint16>(mcpServerPort.volatileValue());
const QString serverUrl = QString("http://127.0.0.1:%1/mcp").arg(port);
const QString bridgeUrl = QStringLiteral("https://github.com/Palm1r/llmqore/releases");
const QString directJson = QString(R"({
"mcpServers": {
"qodeassist": {
"type": "sse",
"url": "%1"
}
}
})").arg(serverUrl);
const QString claudeCodeCmd
= QString("claude mcp add --transport sse qodeassist %1").arg(serverUrl);
const QString vscodeJson = QString(R"({
"servers": {
"qodeassist": {
"type": "sse",
"url": "%1"
}
}
})").arg(serverUrl);
const QString bridgeJson = QString(R"({
"port": 8808,
"host": "127.0.0.1",
"mcpServers": {
"qodeassist": {
"type": "sse",
"url": "%1"
}
}
})").arg(serverUrl);
const QString claudeDesktopJson = QStringLiteral(R"({
"mcpServers": {
"qodeassist": {
"command": "<path-to>/mcp-bridge",
"args": ["--stdio", "<path-to>/mcp-bridge.json"]
}
}
})");
auto *dialog = new QDialog(Core::ICore::dialogParent());
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowTitle(Tr::tr("Connect to QodeAssist MCP"));
dialog->resize(640, 480);
auto *intro = new QLabel(
Tr::tr("Server URL: <code>%1</code>. If your MCP client speaks HTTP/SSE "
"natively, use the <b>Direct</b> tab. If it only speaks stdio "
"(e.g. Claude Desktop), use the <b>Bridge</b> tab.")
.arg(serverUrl),
dialog);
intro->setWordWrap(true);
intro->setTextFormat(Qt::RichText);
auto makeCodeBlock = [](QWidget *parent, const QString &text) -> QWidget * {
auto *container = new QWidget(parent);
auto *layout = new QVBoxLayout(container);
layout->setContentsMargins(0, 0, 0, 0);
auto *edit = new QPlainTextEdit(text, container);
edit->setReadOnly(true);
edit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
edit->setLineWrapMode(QPlainTextEdit::NoWrap);
edit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
edit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
const int lines = text.count(QLatin1Char('\n')) + 1;
const QFontMetrics fm(edit->font());
const int contentHeight = lines * fm.lineSpacing();
const int frame = 2 * edit->frameWidth();
edit->setFixedHeight(contentHeight + frame + 8);
auto *copyBtn = new QPushButton(Tr::tr("Copy"), container);
copyBtn->setAutoDefault(false);
copyBtn->setDefault(false);
QObject::connect(copyBtn, &QPushButton::clicked, container, [edit] {
QApplication::clipboard()->setText(edit->toPlainText());
});
auto *btnRow = new QHBoxLayout();
btnRow->addStretch(1);
btnRow->addWidget(copyBtn);
layout->addWidget(edit);
layout->addLayout(btnRow);
return container;
};
auto *tabs = new QTabWidget(dialog);
// Direct tab
{
auto *tab = new QWidget(tabs);
auto *layout = new QVBoxLayout(tab);
auto *hint = new QLabel(Tr::tr("<b>Claude Code</b> (CLI): run once —"), tab);
hint->setTextFormat(Qt::RichText);
layout->addWidget(hint);
layout->addWidget(makeCodeBlock(tab, claudeCodeCmd));
auto *vscodeHint = new QLabel(
Tr::tr("<b>VS Code</b>: save as <code>.vscode/mcp.json</code> in the workspace:"),
tab);
vscodeHint->setTextFormat(Qt::RichText);
vscodeHint->setWordWrap(true);
layout->addWidget(vscodeHint);
layout->addWidget(makeCodeBlock(tab, vscodeJson));
auto *jsonHint = new QLabel(
Tr::tr("Any other client that reads an <code>mcpServers</code> JSON block:"),
tab);
jsonHint->setTextFormat(Qt::RichText);
jsonHint->setWordWrap(true);
layout->addWidget(jsonHint);
layout->addWidget(makeCodeBlock(tab, directJson));
layout->addStretch(1);
tabs->addTab(tab, Tr::tr("Direct (HTTP/SSE)"));
}
// Bridge tab
{
auto *tab = new QWidget(tabs);
auto *layout = new QVBoxLayout(tab);
auto *step1 = new QLabel(
Tr::tr("<b>1.</b> Download <code>mcp-bridge</code> for your OS from "
"<a href=\"%1\">%1</a>.")
.arg(bridgeUrl),
tab);
step1->setTextFormat(Qt::RichText);
step1->setWordWrap(true);
step1->setOpenExternalLinks(true);
layout->addWidget(step1);
auto *step2 = new QLabel(
Tr::tr("<b>2.</b> Save the following as <code>mcp-bridge.json</code>:"), tab);
step2->setTextFormat(Qt::RichText);
layout->addWidget(step2);
layout->addWidget(makeCodeBlock(tab, bridgeJson));
auto *step3 = new QLabel(
Tr::tr("<b>3.</b> Point the stdio-only client at the bridge. "
"Example for <code>claude_desktop_config.json</code>:"),
tab);
step3->setTextFormat(Qt::RichText);
step3->setWordWrap(true);
layout->addWidget(step3);
layout->addWidget(makeCodeBlock(tab, claudeDesktopJson));
layout->addStretch(1);
tabs->addTab(tab, Tr::tr("Bridge (stdio)"));
}
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, dialog);
QObject::connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::close);
auto *layout = new QVBoxLayout(dialog);
layout->addWidget(intro);
layout->addWidget(tabs, 1);
layout->addWidget(buttons);
dialog->show();
}
class McpSettingsPage : public Core::IOptionsPage
{
public:
McpSettingsPage()
{
setId(Constants::QODE_ASSIST_MCP_SETTINGS_PAGE_ID);
setDisplayName(Tr::tr("MCP"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setSettingsProvider([] { return &mcpSettings(); });
}
};
const McpSettingsPage mcpSettingsPage;
} // namespace QodeAssist::Settings

32
settings/McpSettings.hpp Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <utils/aspects.h>
#include "ButtonAspect.hpp"
namespace QodeAssist::Settings {
class McpSettings : public Utils::AspectContainer
{
public:
McpSettings();
ButtonAspect resetToDefaults{this};
Utils::BoolAspect enableMcpServer{this};
Utils::IntegerAspect mcpServerPort{this};
ButtonAspect showConnectionInstructions{this};
private:
void setupConnections();
void resetSettingsToDefaults();
void showConnectionInstructionsDialog();
};
McpSettings &mcpSettings();
} // namespace QodeAssist::Settings

View File

@@ -100,6 +100,10 @@ const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalC
const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows"; const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows";
const char CA_TERMINAL_COMMAND_TIMEOUT[] = "QodeAssist.caTerminalCommandTimeout"; const char CA_TERMINAL_COMMAND_TIMEOUT[] = "QodeAssist.caTerminalCommandTimeout";
// MCP server settings
const char MCP_ENABLE_SERVER[] = "QodeAssist.mcpEnableServer";
const char MCP_SERVER_PORT[] = "QodeAssist.mcpServerPort";
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
const char QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID[] const char QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID[]
@@ -109,6 +113,7 @@ const char QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID[]
const char QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID[] const char QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID[]
= "QodeAssist.4QuickRefactorSettingsPageId"; = "QodeAssist.4QuickRefactorSettingsPageId";
const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.5ToolsSettingsPageId"; const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.5ToolsSettingsPageId";
const char QODE_ASSIST_MCP_SETTINGS_PAGE_ID[] = "QodeAssist.6McpSettingsPageId";
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category"; const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist"; const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";