// Copyright (C) 2024-2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later #include "McpClientsManager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace QodeAssist::Mcp { namespace { constexpr const char *kServersKey = "mcpServers"; QByteArray serializedEntry(const McpServerConfig &cfg) { return QJsonDocument(cfg.toJson()).toJson(QJsonDocument::Compact); } } // namespace McpClientsManager &McpClientsManager::instance() { static McpClientsManager manager; return manager; } QString McpClientsManager::configFilePath() { const QString base = Core::ICore::userResourcePath().toFSPathString(); return QDir(base).filePath(QStringLiteral("qodeassist/mcp-server.json")); } QByteArray McpClientsManager::emptyConfigTemplate() { return QByteArray( "{\n" " \"mcpServers\": {\n" " // Example HTTP/SSE server:\n" " // \"my-http-server\": {\n" " // \"type\": \"sse\",\n" " // \"url\": \"http://127.0.0.1:9000/mcp\",\n" " // \"enable\": true\n" " // },\n" " // Example stdio server:\n" " // \"my-stdio-server\": {\n" " // \"command\": \"/path/to/server\",\n" " // \"args\": [\"--flag\"],\n" " // \"env\": {\"KEY\": \"value\"},\n" " // \"enable\": false\n" " // }\n" " }\n" "}\n"); } McpClientsManager::McpClientsManager(QObject *parent) : QObject(parent) {} McpClientsManager::~McpClientsManager() { for (auto *c : m_connections) { if (c) c->disconnectFromServer(); } m_connections.clear(); } void McpClientsManager::init() { if (m_initialized) return; m_initialized = true; ensureFileExists(); setupWatcher(); connect( &Settings::mcpSettings().enableMcpClients, &Utils::BaseAspect::changed, this, [this]() { loadFromDisk(); }); connect( &Settings::mcpSettings().mcpClientExtraPaths, &Utils::BaseAspect::changed, this, [this]() { for (auto *c : m_connections) { if (c && c->config().transport == McpTransportKind::Stdio && c->config().enabled && Settings::mcpSettings().enableMcpClients()) { c->disconnectFromServer(); c->connectToServer(); } } }); loadFromDisk(); } void McpClientsManager::ensureFileExists() { const QString path = configFilePath(); QFileInfo fi(path); if (fi.exists()) return; QDir().mkpath(fi.absolutePath()); QFile f(path); if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { f.write(emptyConfigTemplate()); f.close(); LOG_MESSAGE(QString("Created empty MCP clients config: %1").arg(path)); } } void McpClientsManager::setupWatcher() { m_watcher = new QFileSystemWatcher(this); m_reloadDebounce = new QTimer(this); m_reloadDebounce->setSingleShot(true); m_reloadDebounce->setInterval(300); connect(m_reloadDebounce.data(), &QTimer::timeout, this, [this]() { const bool suppress = m_suppressNextWatcherReload; m_suppressNextWatcherReload = false; if (!suppress) loadFromDisk(); updateWatchedPaths(); }); connect(m_watcher.data(), &QFileSystemWatcher::fileChanged, this, [this]() { m_reloadDebounce->start(); }); connect(m_watcher.data(), &QFileSystemWatcher::directoryChanged, this, [this]() { m_reloadDebounce->start(); }); updateWatchedPaths(); } void McpClientsManager::updateWatchedPaths() { if (!m_watcher) return; if (!m_watcher->files().isEmpty()) m_watcher->removePaths(m_watcher->files()); if (!m_watcher->directories().isEmpty()) m_watcher->removePaths(m_watcher->directories()); const QString path = configFilePath(); const QFileInfo info(path); if (info.exists()) m_watcher->addPath(path); const QString dir = info.absolutePath(); if (QFileInfo::exists(dir)) m_watcher->addPath(dir); } QList McpClientsManager::connections() const { return m_connections; } QList McpClientsManager::toolsCapableProviders() const { QList out; auto &pm = PluginLLMCore::ProvidersManager::instance(); for (const QString &name : pm.providersNames()) { auto *p = pm.getProviderByName(name); if (!p) continue; if (p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)) out.append(p); } return out; } QJsonObject McpClientsManager::builtinServers() { static const QByteArray pseudoConfig( "{\n" " \"mcpServers\": {\n" " \"qt-docs\": {\n" " \"type\": \"sse\",\n" " \"url\": \"https://qt-docs-mcp.qt.io/mcp\",\n" " \"enable\": false\n" " }\n" " }\n" "}\n"); const QJsonDocument doc = QJsonDocument::fromJson(pseudoConfig); return doc.object().value(QLatin1String(kServersKey)).toObject(); } QJsonObject McpClientsManager::readRoot() const { QJsonObject root{{QLatin1String(kServersKey), QJsonObject{}}}; QFile f(configFilePath()); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { QJsonParseError err; const QJsonDocument doc = QJsonDocument::fromJson(f.readAll(), &err); f.close(); if (err.error == QJsonParseError::NoError && doc.isObject()) root = doc.object(); } QJsonObject servers = root.value(QLatin1String(kServersKey)).toObject(); const QJsonObject builtin = builtinServers(); for (auto it = builtin.begin(); it != builtin.end(); ++it) { if (!servers.contains(it.key())) servers.insert(it.key(), it.value()); } root.insert(QLatin1String(kServersKey), servers); return root; } bool McpClientsManager::writeRoot(const QJsonObject &root) { QSaveFile out(configFilePath()); if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) { const QString reason = out.errorString(); LOG_MESSAGE( QString("MCP clients: cannot write %1: %2").arg(configFilePath(), reason)); emit writeFailed(reason); return false; } out.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); if (!out.commit()) { const QString reason = out.errorString(); LOG_MESSAGE( QString("MCP clients: commit failed for %1: %2").arg(configFilePath(), reason)); emit writeFailed(reason); return false; } m_suppressNextWatcherReload = true; return true; } void McpClientsManager::reload() { loadFromDisk(); updateWatchedPaths(); } bool McpClientsManager::setServerEnabled(const QString &name, bool enabled) { QJsonObject root = readRoot(); QJsonObject servers = root.value(QLatin1String(kServersKey)).toObject(); if (!servers.contains(name)) { LOG_MESSAGE(QString("MCP clients: setServerEnabled: no entry '%1'").arg(name)); return false; } QJsonObject entry = servers.value(name).toObject(); entry.insert(QStringLiteral("enable"), enabled); servers.insert(name, entry); root.insert(QLatin1String(kServersKey), servers); if (!writeRoot(root)) return false; loadFromDisk(); return true; } bool McpClientsManager::addServer(const QString &name, const QJsonObject &entry) { QJsonObject root = readRoot(); QJsonObject servers = root.value(QLatin1String(kServersKey)).toObject(); QString finalName = name; int suffix = 2; while (servers.contains(finalName)) finalName = QStringLiteral("%1-%2").arg(name).arg(suffix++); servers.insert(finalName, entry); root.insert(QLatin1String(kServersKey), servers); if (!writeRoot(root)) return false; loadFromDisk(); return true; } bool McpClientsManager::removeServer(const QString &name) { QJsonObject root = readRoot(); QJsonObject servers = root.value(QLatin1String(kServersKey)).toObject(); if (!servers.contains(name)) return false; servers.remove(name); root.insert(QLatin1String(kServersKey), servers); if (!writeRoot(root)) return false; loadFromDisk(); return true; } void McpClientsManager::loadFromDisk() { const QJsonObject root = readRoot(); const QJsonObject servers = root.value(QLatin1String(kServersKey)).toObject(); QList newConfigs; for (auto it = servers.begin(); it != servers.end(); ++it) { if (!it.value().isObject()) continue; newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject())); } const auto providers = toolsCapableProviders(); const bool masterEnabled = Settings::mcpSettings().enableMcpClients(); QList keep; QList oldConnections = m_connections; m_connections.clear(); for (const McpServerConfig &cfg : newConfigs) { McpServerConnection *existing = nullptr; for (auto *c : oldConnections) { if (c && c->config().name == cfg.name) { existing = c; break; } } const bool configUnchanged = existing && serializedEntry(existing->config()) == serializedEntry(cfg); McpServerConnection *c = nullptr; if (configUnchanged) { oldConnections.removeAll(existing); c = existing; } else { if (existing) { oldConnections.removeAll(existing); existing->disconnectFromServer(); existing->deleteLater(); } c = new McpServerConnection(cfg, this); c->setProviders(providers); connect( c, &McpServerConnection::stateChanged, this, &McpClientsManager::serversChanged); } keep.append(c); const bool wantRunning = cfg.enabled && masterEnabled; const bool currentlyRunning = c->state() == McpConnectionState::Connected || c->state() == McpConnectionState::Connecting; if (wantRunning && !currentlyRunning) c->connectToServer(); else if (!wantRunning && currentlyRunning) c->disconnectFromServer(); } for (auto *c : oldConnections) { if (!c) continue; c->disconnectFromServer(); c->deleteLater(); } m_connections = keep; LOG_MESSAGE(QString("MCP clients: loaded %1 server(s) from %2") .arg(m_connections.size()) .arg(configFilePath())); emit serversChanged(); } } // namespace QodeAssist::Mcp