mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
feat: Add mcp client hub
This commit is contained in:
503
settings/McpClientsListAspect.cpp
Normal file
503
settings/McpClientsListAspect.cpp
Normal file
@@ -0,0 +1,503 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "McpClientsListAspect.hpp"
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <utils/filepath.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDir>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPalette>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "McpSettings.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "StatusDot.hpp"
|
||||
#include "mcp/McpClientsManager.hpp"
|
||||
#include "mcp/McpServerConnection.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
namespace {
|
||||
|
||||
QString mutedColorHex()
|
||||
{
|
||||
if (auto *t = Utils::creatorTheme())
|
||||
return t->color(Utils::Theme::TextColorDisabled).name();
|
||||
return qApp->palette().color(QPalette::PlaceholderText).name();
|
||||
}
|
||||
|
||||
QString errorColorHex()
|
||||
{
|
||||
if (auto *t = Utils::creatorTheme())
|
||||
return t->color(Utils::Theme::TextColorError).name();
|
||||
return QStringLiteral("#d8444d");
|
||||
}
|
||||
|
||||
QColor dotColorFor(Mcp::McpConnectionState state)
|
||||
{
|
||||
auto *t = Utils::creatorTheme();
|
||||
switch (state) {
|
||||
case Mcp::McpConnectionState::Connected:
|
||||
return t ? t->color(Utils::Theme::IconsRunColor) : QColor(0x3fb950);
|
||||
case Mcp::McpConnectionState::Failed:
|
||||
return t ? t->color(Utils::Theme::TextColorError) : QColor(0xd8444d);
|
||||
case Mcp::McpConnectionState::Connecting:
|
||||
return t ? t->color(Utils::Theme::IconsWarningColor) : QColor(0xd29922);
|
||||
case Mcp::McpConnectionState::Disabled:
|
||||
break;
|
||||
}
|
||||
return t ? t->color(Utils::Theme::TextColorDisabled) : QColor(0x888888);
|
||||
}
|
||||
|
||||
QString statusTooltip(Mcp::McpConnectionState state, const QString &detail)
|
||||
{
|
||||
switch (state) {
|
||||
case Mcp::McpConnectionState::Connected:
|
||||
return detail.isEmpty() ? McpClientsListAspect::tr("Connected.") : detail;
|
||||
case Mcp::McpConnectionState::Connecting:
|
||||
return McpClientsListAspect::tr("Connecting…");
|
||||
case Mcp::McpConnectionState::Failed:
|
||||
return detail.isEmpty() ? McpClientsListAspect::tr("Failed.")
|
||||
: McpClientsListAspect::tr("Failed: %1").arg(detail);
|
||||
case Mcp::McpConnectionState::Disabled:
|
||||
break;
|
||||
}
|
||||
return McpClientsListAspect::tr("Disabled.");
|
||||
}
|
||||
|
||||
QString formatDetails(const Mcp::McpServerConfig &cfg)
|
||||
{
|
||||
const QString muted = mutedColorHex();
|
||||
const QString type = cfg.transport == Mcp::McpTransportKind::Http
|
||||
? QStringLiteral("sse")
|
||||
: QStringLiteral("stdio");
|
||||
|
||||
QString details;
|
||||
if (cfg.transport == Mcp::McpTransportKind::Http) {
|
||||
details = QString("<span style=\"color:%1\">%2</span>")
|
||||
.arg(muted, cfg.url.toString().toHtmlEscaped());
|
||||
} else {
|
||||
QStringList parts;
|
||||
if (!cfg.command.isEmpty())
|
||||
parts << cfg.command.toHtmlEscaped();
|
||||
for (const QString &a : cfg.args)
|
||||
parts << a.toHtmlEscaped();
|
||||
details = QString("<span style=\"color:%1\">%2</span>").arg(muted, parts.join(' '));
|
||||
}
|
||||
|
||||
if (!cfg.env.isEmpty()) {
|
||||
QStringList envKeys;
|
||||
for (auto it = cfg.env.begin(); it != cfg.env.end(); ++it)
|
||||
envKeys << it.key().toHtmlEscaped();
|
||||
details
|
||||
+= QString(" <span style=\"color:%1\">env: %2</span>")
|
||||
.arg(muted, envKeys.join(", "));
|
||||
}
|
||||
|
||||
return QString("<b>%1</b> <span style=\"color:%2\">[%3]</span><br><tt>%4</tt>")
|
||||
.arg(cfg.name.toHtmlEscaped(), muted, type, details);
|
||||
}
|
||||
|
||||
struct ExamplePreset
|
||||
{
|
||||
QString label;
|
||||
QString defaultName;
|
||||
QJsonObject body;
|
||||
};
|
||||
|
||||
QList<ExamplePreset> buildExamplePresets()
|
||||
{
|
||||
QList<ExamplePreset> out;
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("everything (reference test server)"),
|
||||
QStringLiteral("everything"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "npx"},
|
||||
{"args", QJsonArray{"-y", "@modelcontextprotocol/server-everything"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("filesystem (local files)"),
|
||||
QStringLiteral("filesystem"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "npx"},
|
||||
{"args",
|
||||
QJsonArray{
|
||||
"-y", "@modelcontextprotocol/server-filesystem", QDir::homePath()}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("memory (in-memory key-value)"),
|
||||
QStringLiteral("memory"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "npx"},
|
||||
{"args", QJsonArray{"-y", "@modelcontextprotocol/server-memory"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("git (local git ops)"),
|
||||
QStringLiteral("git"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "uvx"},
|
||||
{"args", QJsonArray{"mcp-server-git"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("time (system clock)"),
|
||||
QStringLiteral("time"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "uvx"},
|
||||
{"args", QJsonArray{"mcp-server-time"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("qtcreator (Qt Creator's built-in MCP server)"),
|
||||
QStringLiteral("qtcreator"),
|
||||
QJsonObject{
|
||||
{"enable", false},
|
||||
{"type", "sse"},
|
||||
{"url", "http://127.0.0.1:3001/sse"},
|
||||
{"spec", "2024-11-05"}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("remote (SSE / HTTP)"),
|
||||
QStringLiteral("remote"),
|
||||
QJsonObject{
|
||||
{"enable", false},
|
||||
{"type", "sse"},
|
||||
{"url", "https://example.com/mcp"},
|
||||
{"headers", QJsonObject{{"Authorization", "Bearer <token>"}}}}});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
struct RowWidgets
|
||||
{
|
||||
QPointer<StatusDot> dot;
|
||||
QPointer<QLabel> status;
|
||||
QPointer<QLabel> tools;
|
||||
};
|
||||
|
||||
void applyState(const RowWidgets &w, Mcp::McpServerConnection *conn)
|
||||
{
|
||||
if (!conn)
|
||||
return;
|
||||
if (w.dot) {
|
||||
w.dot->setColor(dotColorFor(conn->state()));
|
||||
w.dot->setToolTip(statusTooltip(conn->state(), conn->statusText()));
|
||||
}
|
||||
if (w.status) {
|
||||
w.status->setText(
|
||||
QString("<span style=\"color:%1\">%2</span>")
|
||||
.arg(mutedColorHex(),
|
||||
statusTooltip(conn->state(), conn->statusText()).toHtmlEscaped()));
|
||||
}
|
||||
if (w.tools) {
|
||||
const QStringList names = conn->toolNames();
|
||||
if (names.isEmpty()) {
|
||||
if (conn->state() == Mcp::McpConnectionState::Connected) {
|
||||
w.tools->setText(
|
||||
QString("<i style=\"color:%1\">%2</i>")
|
||||
.arg(mutedColorHex(),
|
||||
McpClientsListAspect::tr("Server reports no tools.")));
|
||||
w.tools->show();
|
||||
} else {
|
||||
w.tools->clear();
|
||||
w.tools->hide();
|
||||
}
|
||||
} else {
|
||||
QStringList escaped;
|
||||
escaped.reserve(names.size());
|
||||
for (const QString &n : names)
|
||||
escaped << n.toHtmlEscaped();
|
||||
w.tools->setText(
|
||||
QString("<b>%1</b> (%2): %3")
|
||||
.arg(McpClientsListAspect::tr("Tools"))
|
||||
.arg(names.size())
|
||||
.arg(escaped.join(", ")));
|
||||
w.tools->show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QWidget *makeRow(Mcp::McpServerConnection *conn, QHash<QString, RowWidgets> *widgets, QWidget *host)
|
||||
{
|
||||
auto *entry = new QFrame(host);
|
||||
entry->setFrameShape(QFrame::StyledPanel);
|
||||
auto *outer = new QVBoxLayout(entry);
|
||||
outer->setContentsMargins(8, 6, 8, 6);
|
||||
outer->setSpacing(2);
|
||||
|
||||
auto *row = new QWidget(entry);
|
||||
auto *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rowLayout->setSpacing(6);
|
||||
|
||||
auto *dot = new StatusDot(row);
|
||||
rowLayout->addWidget(dot, 0, Qt::AlignTop);
|
||||
|
||||
auto *check = new QCheckBox(row);
|
||||
check->setChecked(conn->config().enabled);
|
||||
check->setToolTip(McpClientsListAspect::tr("Enable / disable this MCP server"));
|
||||
rowLayout->addWidget(check, 0, Qt::AlignTop);
|
||||
|
||||
auto *info = new QLabel(formatDetails(conn->config()), row);
|
||||
info->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
info->setWordWrap(true);
|
||||
rowLayout->addWidget(info, 1);
|
||||
|
||||
auto *statusLabel = new QLabel(row);
|
||||
statusLabel->setMinimumWidth(120);
|
||||
statusLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
rowLayout->addWidget(statusLabel, 0, Qt::AlignTop);
|
||||
|
||||
auto *removeBtn = new QPushButton(QStringLiteral("✕"), row);
|
||||
removeBtn->setToolTip(McpClientsListAspect::tr("Remove this server from the config."));
|
||||
removeBtn->setFlat(true);
|
||||
removeBtn->setFixedWidth(24);
|
||||
removeBtn->setCursor(Qt::PointingHandCursor);
|
||||
removeBtn->setStyleSheet(
|
||||
QString("QPushButton:hover { color: %1; }").arg(errorColorHex()));
|
||||
rowLayout->addWidget(removeBtn, 0, Qt::AlignTop);
|
||||
|
||||
outer->addWidget(row);
|
||||
|
||||
auto *tools = new QLabel(entry);
|
||||
tools->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
tools->setWordWrap(true);
|
||||
tools->setContentsMargins(38, 0, 0, 0);
|
||||
tools->hide();
|
||||
outer->addWidget(tools);
|
||||
|
||||
RowWidgets w{dot, statusLabel, tools};
|
||||
widgets->insert(conn->config().name, w);
|
||||
applyState(w, conn);
|
||||
|
||||
QObject::connect(check, &QCheckBox::toggled, row, [name = conn->config().name](bool on) {
|
||||
Mcp::McpClientsManager::instance().setServerEnabled(name, on);
|
||||
});
|
||||
|
||||
QObject::connect(removeBtn, &QPushButton::clicked, row, [name = conn->config().name, host]() {
|
||||
const auto reply = QMessageBox::question(
|
||||
host,
|
||||
McpClientsListAspect::tr("Remove server"),
|
||||
McpClientsListAspect::tr("Remove server '%1' from the config?").arg(name),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No);
|
||||
if (reply == QMessageBox::Yes)
|
||||
Mcp::McpClientsManager::instance().removeServer(name);
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void clearLayout(QVBoxLayout *lay)
|
||||
{
|
||||
while (auto *item = lay->takeAt(0)) {
|
||||
if (auto *w = item->widget())
|
||||
w->deleteLater();
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
McpClientsListAspect::McpClientsListAspect(Utils::AspectContainer *container)
|
||||
: Utils::BaseAspect(container)
|
||||
{}
|
||||
|
||||
void McpClientsListAspect::addToLayoutImpl(Layouting::Layout &parent)
|
||||
{
|
||||
auto *outer = new QWidget();
|
||||
auto *outerLayout = new QVBoxLayout(outer);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
outerLayout->setSpacing(4);
|
||||
|
||||
auto *openBtn = new QPushButton(tr("Open Config"), outer);
|
||||
openBtn->setToolTip(Mcp::McpClientsManager::configFilePath());
|
||||
auto *refreshBtn = new QPushButton(tr("Refresh MCP List"), outer);
|
||||
|
||||
auto *buttonsRow = new QHBoxLayout();
|
||||
buttonsRow->setContentsMargins(0, 0, 0, 0);
|
||||
buttonsRow->addWidget(openBtn);
|
||||
buttonsRow->addWidget(refreshBtn);
|
||||
buttonsRow->addStretch(1);
|
||||
|
||||
auto *restartHint = new QLabel(outer);
|
||||
restartHint->setWordWrap(true);
|
||||
restartHint->setText(
|
||||
tr("Note: restart Qt Creator to apply MCP changes to already-opened chats "
|
||||
"and running sessions."));
|
||||
|
||||
auto *summaryLabel = new QLabel(outer);
|
||||
summaryLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
auto *serversHost = new QWidget(outer);
|
||||
auto *serversLayout = new QVBoxLayout(serversHost);
|
||||
serversLayout->setContentsMargins(0, 0, 0, 0);
|
||||
serversLayout->setSpacing(4);
|
||||
|
||||
auto *scroll = new QScrollArea(outer);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setMinimumHeight(160);
|
||||
scroll->setFrameShape(QFrame::StyledPanel);
|
||||
scroll->setWidget(serversHost);
|
||||
|
||||
auto *quickSetupLabel = new QLabel(tr("Quick Setup"), outer);
|
||||
auto *presetsCombo = new QComboBox(outer);
|
||||
presetsCombo->setToolTip(
|
||||
tr("Pick a preset to append a ready-made server entry to the config "
|
||||
"(auto-suffixed if the name is taken)."));
|
||||
presetsCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
presetsCombo->addItem(tr("-- Select Preset --"));
|
||||
const auto presets = buildExamplePresets();
|
||||
for (const auto &p : presets)
|
||||
presetsCombo->addItem(p.label);
|
||||
|
||||
auto *btnRow = new QHBoxLayout();
|
||||
btnRow->setContentsMargins(0, 0, 0, 0);
|
||||
btnRow->addWidget(quickSetupLabel);
|
||||
btnRow->addWidget(presetsCombo);
|
||||
btnRow->addStretch(1);
|
||||
|
||||
outerLayout->addLayout(buttonsRow);
|
||||
outerLayout->addWidget(restartHint);
|
||||
outerLayout->addWidget(summaryLabel);
|
||||
outerLayout->addWidget(scroll);
|
||||
outerLayout->addLayout(btnRow);
|
||||
|
||||
auto rowsState = std::make_shared<QHash<QString, RowWidgets>>();
|
||||
|
||||
auto rebuild = [outer, serversLayout, summaryLabel, rowsState]() {
|
||||
clearLayout(serversLayout);
|
||||
rowsState->clear();
|
||||
|
||||
const auto connections = Mcp::McpClientsManager::instance().connections();
|
||||
if (connections.isEmpty()) {
|
||||
auto *empty = new QLabel(
|
||||
QString("<p style=\"color:%1\">%2</p>")
|
||||
.arg(mutedColorHex(),
|
||||
tr("No servers configured. Add a preset below or edit the JSON.")),
|
||||
outer);
|
||||
serversLayout->addWidget(empty);
|
||||
serversLayout->addStretch();
|
||||
summaryLabel->setText(
|
||||
QString("<i>%1</i>").arg(tr("0 server(s) defined.")));
|
||||
return;
|
||||
}
|
||||
|
||||
int enabled = 0;
|
||||
for (auto *conn : connections) {
|
||||
serversLayout->addWidget(makeRow(conn, rowsState.get(), outer));
|
||||
if (conn->config().enabled)
|
||||
++enabled;
|
||||
}
|
||||
serversLayout->addStretch();
|
||||
|
||||
summaryLabel->setText(
|
||||
QString("<i>%1</i>")
|
||||
.arg(tr("%1 server(s) defined, %2 enabled.")
|
||||
.arg(connections.size())
|
||||
.arg(enabled)));
|
||||
};
|
||||
|
||||
rebuild();
|
||||
|
||||
QObject::connect(
|
||||
&Mcp::McpClientsManager::instance(),
|
||||
&Mcp::McpClientsManager::serversChanged,
|
||||
outer,
|
||||
[rebuild, rowsState]() {
|
||||
const auto connections = Mcp::McpClientsManager::instance().connections();
|
||||
|
||||
QStringList currentNames;
|
||||
currentNames.reserve(connections.size());
|
||||
for (auto *c : connections)
|
||||
currentNames << c->config().name;
|
||||
|
||||
QStringList knownNames = rowsState->keys();
|
||||
std::sort(currentNames.begin(), currentNames.end());
|
||||
std::sort(knownNames.begin(), knownNames.end());
|
||||
|
||||
if (currentNames != knownNames) {
|
||||
rebuild();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto *conn : connections)
|
||||
applyState(rowsState->value(conn->config().name), conn);
|
||||
});
|
||||
|
||||
QObject::connect(refreshBtn, &QPushButton::clicked, outer, []() {
|
||||
Mcp::McpClientsManager::instance().reload();
|
||||
});
|
||||
QObject::connect(openBtn, &QPushButton::clicked, outer, []() {
|
||||
Core::EditorManager::openEditor(
|
||||
Utils::FilePath::fromString(Mcp::McpClientsManager::configFilePath()));
|
||||
});
|
||||
QObject::connect(
|
||||
&Mcp::McpClientsManager::instance(),
|
||||
&Mcp::McpClientsManager::writeFailed,
|
||||
outer,
|
||||
[outer](const QString &reason) {
|
||||
QMessageBox::warning(
|
||||
outer,
|
||||
McpClientsListAspect::tr("MCP configuration"),
|
||||
McpClientsListAspect::tr("Failed to write %1:\n%2")
|
||||
.arg(Mcp::McpClientsManager::configFilePath(), reason));
|
||||
});
|
||||
auto syncEnabled = [outer]() {
|
||||
outer->setEnabled(mcpSettings().enableMcpClients.volatileValue());
|
||||
};
|
||||
syncEnabled();
|
||||
QObject::connect(
|
||||
&mcpSettings().enableMcpClients,
|
||||
&Utils::BoolAspect::volatileValueChanged,
|
||||
outer,
|
||||
syncEnabled);
|
||||
|
||||
QObject::connect(
|
||||
presetsCombo,
|
||||
&QComboBox::currentIndexChanged,
|
||||
outer,
|
||||
[presetsCombo, presets](int idx) {
|
||||
if (idx <= 0)
|
||||
return;
|
||||
const int presetIdx = idx - 1;
|
||||
if (presetIdx < 0 || presetIdx >= presets.size()) {
|
||||
presetsCombo->setCurrentIndex(0);
|
||||
return;
|
||||
}
|
||||
Mcp::McpClientsManager::instance().addServer(
|
||||
presets[presetIdx].defaultName, presets[presetIdx].body);
|
||||
// Snap back to the placeholder so the next pick fires again even
|
||||
// if the user chooses the same preset twice.
|
||||
presetsCombo->setCurrentIndex(0);
|
||||
});
|
||||
|
||||
parent.addItem(outer);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
21
settings/McpClientsListAspect.hpp
Normal file
21
settings/McpClientsListAspect.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class McpClientsListAspect : public Utils::BaseAspect
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit McpClientsListAspect(Utils::AspectContainer *container = nullptr);
|
||||
|
||||
void addToLayoutImpl(Layouting::Layout &parent) override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDir>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFontDatabase>
|
||||
@@ -51,6 +52,30 @@ McpSettings::McpSettings()
|
||||
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...");
|
||||
|
||||
@@ -70,6 +95,10 @@ McpSettings::McpSettings()
|
||||
enableMcpServer,
|
||||
mcpServerPort,
|
||||
Row{Stretch{1}, showConnectionInstructions}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Clients")),
|
||||
Column{enableMcpClients, mcpClientExtraPaths, mcpClientsList}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
@@ -87,6 +116,28 @@ void McpSettings::setupConnections()
|
||||
&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()
|
||||
@@ -101,6 +152,8 @@ void McpSettings::resetSettingsToDefaults()
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(enableMcpServer);
|
||||
resetAspect(mcpServerPort);
|
||||
resetAspect(enableMcpClients);
|
||||
resetAspect(mcpClientExtraPaths);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
#include "McpClientsListAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
@@ -21,6 +22,10 @@ public:
|
||||
|
||||
ButtonAspect showConnectionInstructions{this};
|
||||
|
||||
Utils::BoolAspect enableMcpClients{this};
|
||||
Utils::StringAspect mcpClientExtraPaths{this};
|
||||
McpClientsListAspect mcpClientsList{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
|
||||
@@ -103,6 +103,8 @@ 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 MCP_ENABLE_CLIENTS[] = "QodeAssist.mcpEnableClients";
|
||||
const char MCP_CLIENT_EXTRA_PATHS[] = "QodeAssist.mcpClientExtraPaths";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
|
||||
43
settings/StatusDot.hpp
Normal file
43
settings/StatusDot.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QWidget>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class StatusDot : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit StatusDot(QWidget *parent = nullptr)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setFixedSize(12, 12);
|
||||
}
|
||||
|
||||
void setColor(const QColor &color)
|
||||
{
|
||||
if (m_color == color)
|
||||
return;
|
||||
m_color = color;
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(m_color);
|
||||
p.drawEllipse(rect().adjusted(2, 2, -2, -2));
|
||||
}
|
||||
|
||||
private:
|
||||
QColor m_color{Qt::gray};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
Reference in New Issue
Block a user