// 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 #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); enableMcpClients.setSettingsKey(Constants::MCP_ENABLE_CLIENTS); enableMcpClients.setLabelText(Tr::tr("Connect to external MCP servers")); enableMcpClients.setToolTip( Tr::tr("Connect to MCP servers listed in mcp-server.json and expose their tools " "to chat/quick-refactor/code-completion. Toggling this off disconnects all " "currently running MCP client sessions.")); enableMcpClients.setDefaultValue(false); mcpClientExtraPaths.setSettingsKey(Constants::MCP_CLIENT_EXTRA_PATHS); mcpClientExtraPaths.setLabelText(Tr::tr("Extra PATH for stdio servers")); mcpClientExtraPaths.setDisplayStyle(Utils::StringAspect::LineEditDisplay); mcpClientExtraPaths.setToolTip( Tr::tr("Directories to prepend to PATH when launching stdio MCP servers. " "Useful when Qt Creator is started from the dock and doesn't see Homebrew, " "nvm, uv, etc. Separate multiple entries with '%1'. " "Per-server 'env' overrides in mcp-server.json still win.") .arg(QDir::listSeparator())); #ifdef Q_OS_MACOS mcpClientExtraPaths.setDefaultValue( QStringLiteral("/opt/homebrew/bin:/usr/local/bin")); #else mcpClientExtraPaths.setDefaultValue(QString{}); #endif 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}}}, Space{8}, Group{ title(Tr::tr("Clients")), Column{enableMcpClients, mcpClientExtraPaths, mcpClientsList}}, Stretch{1}}; }); } void McpSettings::setupConnections() { connect( &resetToDefaults, &ButtonAspect::clicked, this, &McpSettings::resetSettingsToDefaults); connect( &showConnectionInstructions, &ButtonAspect::clicked, this, &McpSettings::showConnectionInstructionsDialog); auto syncServerSubgroup = [this]() { const bool on = enableMcpServer.volatileValue(); mcpServerPort.setEnabled(on); }; auto syncClientsSubgroup = [this]() { const bool on = enableMcpClients.volatileValue(); mcpClientExtraPaths.setEnabled(on); mcpClientsList.setEnabled(on); }; connect( &enableMcpServer, &Utils::BoolAspect::volatileValueChanged, this, syncServerSubgroup); connect( &enableMcpClients, &Utils::BoolAspect::volatileValueChanged, this, syncClientsSubgroup); syncServerSubgroup(); syncClientsSubgroup(); } 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); resetAspect(enableMcpClients); resetAspect(mcpClientExtraPaths); 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