mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 10:59:30 -04:00
350 lines
11 KiB
C++
350 lines
11 KiB
C++
// 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 <QDir>
|
|
#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);
|
|
|
|
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<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
|