diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fa4f36..8d052ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,7 @@ add_qtc_plugin(QodeAssist tools/ReadFileTool.hpp tools/ReadFileTool.cpp tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp tools/TodoTool.hpp tools/TodoTool.cpp + mcp/McpServerManager.hpp mcp/McpServerManager.cpp ) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) diff --git a/mcp/McpServerManager.cpp b/mcp/McpServerManager.cpp new file mode 100644 index 0000000..2d112ff --- /dev/null +++ b/mcp/McpServerManager.cpp @@ -0,0 +1,122 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "McpServerManager.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#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(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(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 diff --git a/mcp/McpServerManager.hpp b/mcp/McpServerManager.hpp new file mode 100644 index 0000000..7c76799 --- /dev/null +++ b/mcp/McpServerManager.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +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 diff --git a/qodeassist.cpp b/qodeassist.cpp index 7a7f23d..9138d52 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -37,6 +37,7 @@ #include "pluginllmcore/PromptProviderFim.hpp" #include "pluginllmcore/ProvidersManager.hpp" #include "logger/RequestPerformanceLogger.hpp" +#include "mcp/McpServerManager.hpp" #include "providers/Providers.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/GeneralSettings.hpp" @@ -163,6 +164,9 @@ public: Settings::setupProjectPanel(); ConfigurationManager::instance().init(); + m_mcpServerManager = new Mcp::McpServerManager(this); + m_mcpServerManager->init(); + if (Settings::generalSettings().enableCheckUpdate()) { QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates); } @@ -298,6 +302,7 @@ private: UpdateStatusWidget *m_statusWidget{nullptr}; QString m_lastRefactorInstructions; QScopedPointer m_chatView; + QPointer m_mcpServerManager; }; } // namespace QodeAssist::Internal diff --git a/settings/ButtonAspect.hpp b/settings/ButtonAspect.hpp index 31f62a0..a773cbc 100644 --- a/settings/ButtonAspect.hpp +++ b/settings/ButtonAspect.hpp @@ -5,8 +5,9 @@ #include #include -#include #include +#include +#include class ButtonAspect : public Utils::BaseAspect { @@ -21,17 +22,25 @@ public: { auto button = new QPushButton(m_buttonText); 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()) { button->setIcon(m_icon); - button->setText(""); // Clear text if icon is set + button->setText(""); } - + if (m_isCompact) { button->setMaximumWidth(30); button->setToolTip(m_tooltip.isEmpty() ? m_buttonText : m_tooltip); } - + connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked); connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible); parent.addItem(button); diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index 19173e7..b6ffdaa 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(QodeAssistSettings STATIC ChatAssistantSettings.hpp ChatAssistantSettings.cpp QuickRefactorSettings.hpp QuickRefactorSettings.cpp ToolsSettings.hpp ToolsSettings.cpp + McpSettings.hpp McpSettings.cpp SettingsDialog.hpp SettingsDialog.cpp ProjectSettings.hpp ProjectSettings.cpp ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp diff --git a/settings/McpSettings.cpp b/settings/McpSettings.cpp new file mode 100644 index 0000000..f73d163 --- /dev/null +++ b/settings/McpSettings.cpp @@ -0,0 +1,296 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "McpSettings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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": "/mcp-bridge", + "args": ["--stdio", "/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: %1. If your MCP client speaks HTTP/SSE " + "natively, use the Direct tab. If it only speaks stdio " + "(e.g. Claude Desktop), use the Bridge 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("Claude Code (CLI): run once —"), tab); + hint->setTextFormat(Qt::RichText); + layout->addWidget(hint); + layout->addWidget(makeCodeBlock(tab, claudeCodeCmd)); + + auto *vscodeHint = new QLabel( + Tr::tr("VS Code: save as .vscode/mcp.json 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 mcpServers 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("1. Download mcp-bridge for your OS from " + "%1.") + .arg(bridgeUrl), + tab); + step1->setTextFormat(Qt::RichText); + step1->setWordWrap(true); + step1->setOpenExternalLinks(true); + layout->addWidget(step1); + + auto *step2 = new QLabel( + Tr::tr("2. Save the following as mcp-bridge.json:"), tab); + step2->setTextFormat(Qt::RichText); + layout->addWidget(step2); + layout->addWidget(makeCodeBlock(tab, bridgeJson)); + + auto *step3 = new QLabel( + Tr::tr("3. Point the stdio-only client at the bridge. " + "Example for claude_desktop_config.json:"), + 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 diff --git a/settings/McpSettings.hpp b/settings/McpSettings.hpp new file mode 100644 index 0000000..86c47d2 --- /dev/null +++ b/settings/McpSettings.hpp @@ -0,0 +1,32 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#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 diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index b2d9ed6..be615d6 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -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_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_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; 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[] = "QodeAssist.4QuickRefactorSettingsPageId"; 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_DISPLAY_CATEGORY[] = "QodeAssist";