diff --git a/cmake/EnablePython.cmake b/cmake/EnablePython.cmake index d0d4522..1d46e4e 100644 --- a/cmake/EnablePython.cmake +++ b/cmake/EnablePython.cmake @@ -5,14 +5,11 @@ function(target_enable_python target_name) find_package(Python COMPONENTS Interpreter Development) if(Python_FOUND) target_include_directories(${target_name} PUBLIC ${Python_INCLUDE_DIRS}) - if(WIN32) - # On Windows, we need to link to python library - # But we should avoid linking conflicts, so we use the imported targets - if(TARGET Python::Python) - target_link_libraries(${target_name} PUBLIC Python::Python) - else() - target_link_libraries(${target_name} PUBLIC ${Python_LIBRARIES}) - endif() + # Link to python library on all platforms, not just Windows + if(TARGET Python::Python) + target_link_libraries(${target_name} PUBLIC Python::Python) + else() + target_link_libraries(${target_name} PUBLIC ${Python_LIBRARIES}) endif() else() message(WARNING "Python not found. Disabling Python plugin support.") @@ -30,14 +27,11 @@ function(enable_python) message(STATUS "Python include dirs: ${Python_INCLUDE_DIRS}") message(STATUS "Python libraries: ${Python_LIBRARIES}") - if(WIN32) - # On Windows, link to python library for the main executable - # But we should avoid linking conflicts, so we use the imported targets - if(TARGET Python::Python) - target_link_libraries(mcp-server++ PRIVATE Python::Python) - else() - target_link_libraries(mcp-server++ PRIVATE ${Python_LIBRARIES}) - endif() + # Link to python library for the main executable on all platforms + if(TARGET Python::Python) + target_link_libraries(mcp-server++ PRIVATE Python::Python) + else() + target_link_libraries(mcp-server++ PRIVATE ${Python_LIBRARIES}) endif() else() message(WARNING "Python not found. Disabling Python plugin support.") diff --git a/config/config.hpp b/config/config.hpp index 6cd2856..22f7671 100644 --- a/config/config.hpp +++ b/config/config.hpp @@ -1,23 +1,27 @@ #ifndef MCP_CONFIG_HPP #define MCP_CONFIG_HPP +#include "config_observer.hpp" #include "core/executable_path.h" #include "core/logger.h" #include "inicpp.hpp" +#include +#include #include -#include +#include #include +#include +#include namespace mcp { namespace config { + constexpr const char *CONFIG_FILE = "config.ini"; inline std::string g_config_file_path; - // Configuration file path constant - // Use executable directory to construct config file path to ensure - // the config file can be accessed correctly regardless of the working directory + // Use executable directory to construct config file path inline std::string get_config_file_path() { static std::string config_file_path = []() { std::filesystem::path exe_dir(mcp::core::getExecutableDirectory()); @@ -37,56 +41,45 @@ namespace mcp { return get_config_file_path(); } - enum class ConfigMode{ - NONE, //Do not load config,use default settings - STATIC, //Load config from static file - DYNAMIC //Load config dynamically + enum class ConfigMode { + NONE, // Use default settings without file + STATIC,// Load from static file once + DYNAMIC// Load and monitor for changes }; /** - * Server-specific configuration structure - * Contains all network and service-related settings - */ + * Server-specific configuration structure + */ struct ServerConfig { - std::string ip; // Server binding IP address - std::string log_level; // Logging severity level - std::string log_path; // Directory path for log files - std::string log_pattern; // Custom log format pattern - std::string plugin_dir; // Directory containing plugins - std::string ssl_cert_file; // SSL certificate file path - std::string ssl_key_file; // SSL private key file path - std::string ssl_dh_params_file;// Diffie-Hellman parameters file path - std::string auth_type; // Authentication type (X-API-Key, Bearer, etc.) - std::string auth_env_file; // Path to the file containing auth keys/tokens - size_t max_file_size; // Maximum size per log file (bytes) - size_t max_files; // Maximum number of log files to retain - unsigned short port; // Legacy server listening port - unsigned short http_port; // HTTP transport port (0 to disable) - unsigned short https_port; // HTTPS transport port (0 to disable) - bool enable_stdio; // Enable stdio transport protocol - bool enable_http; // Enable HTTP transport protocol - bool enable_https; // Enable HTTPS transport protocol - bool enable_auth; // Enable authentication - - // Rate limiter configuration - size_t max_requests_per_second;// Maximum requests allowed per second - size_t max_concurrent_requests;// Maximum concurrent requests - size_t max_request_size; // Maximum request size in bytes - size_t max_response_size; // Maximum response size in bytes - - /** - * Loads server configuration from INI file - * @param ini Reference to IniManager instance - * @return Populated ServerConfig structure - */ + std::string ip; + std::string log_level; + std::string log_path; + std::string log_pattern; + std::string plugin_dir; + std::string ssl_cert_file; + std::string ssl_key_file; + std::string ssl_dh_params_file; + std::string auth_type; + std::string auth_env_file; + size_t max_file_size; + size_t max_files; + unsigned short port; + unsigned short http_port; + unsigned short https_port; + bool enable_stdio; + bool enable_http; + bool enable_https; + bool enable_auth; + size_t max_requests_per_second; + size_t max_concurrent_requests; + size_t max_request_size; + size_t max_response_size; + static ServerConfig load(inicpp::IniManager &ini) { try { - // Get server section auto server_section = ini["server"]; - ServerConfig config; - // String values config.ip = server_section["ip"].String().empty() ? "127.0.0.1" : server_section["ip"].String(); config.log_level = server_section["log_level"].String().empty() ? "info" : server_section["log_level"].String(); config.log_path = server_section["log_path"].String().empty() ? "logs/mcp_server.log" : server_section["log_path"].String(); @@ -98,20 +91,17 @@ namespace mcp { config.auth_type = server_section["auth_type"].String().empty() ? "X-API-Key" : server_section["auth_type"].String(); config.auth_env_file = server_section["auth_env_file"].String().empty() ? ".env.auth" : server_section["auth_env_file"].String(); - // Numeric values with explicit conversion config.max_file_size = server_section["max_file_size"].String().empty() ? 10485760 : static_cast(server_section["max_file_size"]); config.max_files = server_section["max_files"].String().empty() ? 10 : static_cast(server_section["max_files"]); config.port = server_section["port"].String().empty() ? 6666 : static_cast(server_section["port"]); config.http_port = server_section["http_port"].String().empty() ? 6666 : static_cast(server_section["http_port"]); config.https_port = server_section["https_port"].String().empty() ? 6667 : static_cast(server_section["https_port"]); - // Rate limiter values config.max_requests_per_second = server_section["max_requests_per_second"].String().empty() ? 100 : static_cast(server_section["max_requests_per_second"]); config.max_concurrent_requests = server_section["max_concurrent_requests"].String().empty() ? 1000 : static_cast(server_section["max_concurrent_requests"]); config.max_request_size = server_section["max_request_size"].String().empty() ? 1024 * 1024 : static_cast(server_section["max_request_size"]); config.max_response_size = server_section["max_response_size"].String().empty() ? 10 * 1024 * 1024 : static_cast(server_section["max_response_size"]); - // Boolean values config.enable_stdio = server_section["enable_stdio"].String().empty() ? true : static_cast(server_section["enable_stdio"]); config.enable_http = server_section["enable_http"].String().empty() ? false : static_cast(server_section["enable_http"]); config.enable_https = server_section["enable_https"].String().empty() ? false : static_cast(server_section["enable_https"]); @@ -120,115 +110,86 @@ namespace mcp { return config; } catch (const std::exception &e) { MCP_ERROR("Failed to load server config: {}", e.what()); - std::cerr << "Failed to load server config: " << e.what() << std::endl; throw; } } }; - - /* - * PluginHub Config - */ + /** + * PluginHub configuration + */ struct PluginHubConfig { - std::string plugin_server_baseurl;//The remote url which contains the plugin repo,It is our plugin server. - unsigned short plugin_server_port;//The port of the plugin server's listening port - - std::string latest_fetch_route;//The latest fetch route - std::string download_route; //The download route - - //if we need to enable a plugin,we would copy it to the enable dir. - //if we need to disable it,what we need to do is to delete it. - std::string plugin_install_dir;//The directory where plugins are installed,just a directory - std::string plugin_enable_dir; //The directory where plugins are enabled,it is the same as server's plugin_dir - - std::string tools_install_dir;//The directory where tools.json are installed,just a directory - std::string tools_enable_dir; //The directory where tools.json are enabled,it is the same as server's config_dir - + std::string plugin_server_baseurl; + unsigned short plugin_server_port; + std::string latest_fetch_route; + std::string download_route; + std::string plugin_install_dir; + std::string plugin_enable_dir; + std::string tools_install_dir; + std::string tools_enable_dir; static PluginHubConfig load(inicpp::IniManager &ini) { try { PluginHubConfig config; - - auto plugin_hub_section = ini["plugin_hub"]; - - config.plugin_server_baseurl = plugin_hub_section["plugin_server_baseurl"].String().empty() ? "http://47.120.50.122" : plugin_hub_section["plugin_server_baseurl"].String(); - config.plugin_server_port = plugin_hub_section["plugin_server_port"].String().empty() ? 6680 : static_cast(plugin_hub_section["plugin_server_port"]); - config.latest_fetch_route = plugin_hub_section["latest_fetch_route"].String().empty() ? "/self/latest/info" : plugin_hub_section["latest_fetch_route"].String(); - config.download_route = plugin_hub_section["download_route"].String().empty() ? "/self/latest/download" : plugin_hub_section["download_route"].String(); - config.plugin_install_dir = plugin_hub_section["plugin_install_dir"].String().empty() ? "plugins_install" : plugin_hub_section["plugin_install_dir"].String(); - config.plugin_enable_dir = plugin_hub_section["plugin_enable_dir"].String().empty() ? "plugins" : plugin_hub_section["plugin_enable_dir"].String(); - config.tools_install_dir = plugin_hub_section["tools_install_dir"].String().empty() ? "plugins_install" : plugin_hub_section["tools_install_dir"].String(); - config.tools_enable_dir = plugin_hub_section["tools_enable_dir"].String().empty() ? "configs" : plugin_hub_section["tools_enable_dir"].String(); + auto section = ini["plugin_hub"]; + config.plugin_server_baseurl = section["plugin_server_baseurl"].String().empty() ? "http://47.120.50.122" : section["plugin_server_baseurl"].String(); + config.plugin_server_port = section["plugin_server_port"].String().empty() ? 6680 : static_cast(section["plugin_server_port"]); + config.latest_fetch_route = section["latest_fetch_route"].String().empty() ? "/self/latest/info" : section["latest_fetch_route"].String(); + config.download_route = section["download_route"].String().empty() ? "/self/latest/download" : section["download_route"].String(); + config.plugin_install_dir = section["plugin_install_dir"].String().empty() ? "plugins_install" : section["plugin_install_dir"].String(); + config.plugin_enable_dir = section["plugin_enable_dir"].String().empty() ? "plugins" : section["plugin_enable_dir"].String(); + config.tools_install_dir = section["tools_install_dir"].String().empty() ? "plugins_install" : section["tools_install_dir"].String(); + config.tools_enable_dir = section["tools_enable_dir"].String().empty() ? "configs" : section["tools_enable_dir"].String(); return config; } catch (const std::exception &e) { - std::cerr << "Failed to load plugin hub config: " << e.what() << std::endl; + MCP_ERROR("Failed to load plugin hub config: {}", e.what()); throw; } } }; - /** - * Python environment configuration structure - * Contains settings for Python interpreter environment selection - */ + * Python environment configuration + */ struct PythonEnvConfig { - std::string default_env; // Default Python environment type (system, conda, uv) - std::string conda_prefix;// Conda environment installation prefix - std::string uv_venv_path;// UV virtual environment path - - /** - * Load Python environment configuration from INI file - * @param ini Reference to IniManager instance - * @return Populated PythonEnvConfig structure - */ + std::string default_env; + std::string conda_prefix; + std::string uv_venv_path; + static PythonEnvConfig load(inicpp::IniManager &ini) { try { PythonEnvConfig config; - - // Get python_environment section, create if not exists - auto python_section = ini["python_environment"]; - - // Load configuration values with defaults - config.default_env = python_section["default"].String().empty() ? "system" : python_section["default"].String(); - config.conda_prefix = python_section["conda_prefix"].String().empty() ? "/opt/conda" : python_section["conda_prefix"].String(); - config.uv_venv_path = python_section["uv_venv_path"].String().empty() ? "./venv" : python_section["uv_venv_path"].String(); - + auto section = ini["python_environment"]; + config.default_env = section["default"].String().empty() ? "system" : section["default"].String(); + config.conda_prefix = section["conda_prefix"].String().empty() ? "/opt/conda" : section["conda_prefix"].String(); + config.uv_venv_path = section["uv_venv_path"].String().empty() ? "./venv" : section["uv_venv_path"].String(); return config; } catch (const std::exception &e) { - MCP_ERROR("Failed to load Python environment config: {}", e.what()); - std::cerr << "Failed to load Python environment config: " << e.what() << std::endl; + MCP_ERROR("Failed to load Python env config: {}", e.what()); throw; } } }; /** - * Global application configuration structure - * Contains top-level configuration and nested server settings - */ + * Global configuration + */ struct GlobalConfig { - std::string title; // Configuration file title/description - ServerConfig server; // Nested server configuration - PluginHubConfig plugin_hub;// Nested plugin hub configuration - PythonEnvConfig python_env;// Nested Python environment configuration - - /** - * Loads complete configuration from INI file - * @return Populated GlobalConfig structure - */ + std::string title; + ServerConfig server; + PluginHubConfig plugin_hub; + PythonEnvConfig python_env; + static GlobalConfig load() { try { inicpp::IniManager ini(get_config_file_path()); - std::cout << "Loaded configuration file: " << get_config_file_path() << std::endl; + MCP_INFO("Loading configuration from: {}", get_config_file_path()); GlobalConfig config; config.title = ini[""]["title"].String().empty() ? "MCP Server Configuration" : ini[""]["title"].String(); config.server = ServerConfig::load(ini); config.plugin_hub = PluginHubConfig::load(ini); config.python_env = PythonEnvConfig::load(ini); - return config; } catch (const std::exception &e) { MCP_ERROR("Failed to load global config: {}", e.what()); @@ -237,40 +198,174 @@ namespace mcp { } }; + // Forward declaration + class ConfigLoader; + + // Global state (managed by loader) + inline std::unique_ptr g_config_loader; + inline std::unique_ptr g_current_config; + inline std::mutex g_config_mutex; + inline std::atomic g_config_initialized{false}; + /** - * Initializes default configuration file if it doesn't exist or is empty - * Creates standard sections and default values for first-time setup - */ - inline void initialize_default_config() { - try { - std::string config_file = get_config_file_path(); - // Check if file exists and has content using filesystem - bool file_valid = false; - if (std::filesystem::exists(config_file)) { - if (std::filesystem::file_size(config_file) > 0) { - file_valid = true; - - } else { - MCP_WARN("Configuration file is empty - recreating: {}", config_file); - std::cout << "Configuration file is empty - recreating: " << config_file << std::endl; + * Abstract base class using Template Method and Observer patterns + */ + class ConfigLoader { + protected: + std::vector observers_; + std::atomic monitoring_active{false}; + std::thread monitor_thread; + + virtual std::unique_ptr createDefaultConfig() = 0; + + virtual std::unique_ptr loadFromStaticFile() { + return std::make_unique(GlobalConfig::load()); + } + + virtual void startMonitoring(GlobalConfig *config) { + if (monitoring_active.load()) return; + monitoring_active = true; + + monitor_thread = std::thread([this, config]() { + std::filesystem::file_time_type last_write = {}; + while (monitoring_active.load()) { + try { + auto path = std::filesystem::path(get_config_file_path()); + if (std::filesystem::exists(path)) { + auto curr_time = std::filesystem::last_write_time(path); + if (curr_time != last_write) { + MCP_INFO("Config file changed, reloading..."); + auto newConfig = loadFromStaticFile(); + if (newConfig) { + std::lock_guard lock(g_config_mutex); + *config = *newConfig; + notifyObservers(*config); + last_write = curr_time; + } + } + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } catch (...) { + if (monitoring_active.load()) { + MCP_ERROR("Error monitoring config file"); + } + break; + } } + }); + } + + void notifyObservers(const GlobalConfig &config) { + for (auto *obs: observers_) { + try { + obs->onConfigReloaded(config); + } catch (const std::exception &e) { + MCP_ERROR("Observer notification failed: {}", e.what()); + } + } + } + + public: + virtual ~ConfigLoader() { + monitoring_active = false; + if (monitor_thread.joinable()) { + monitor_thread.join(); + } + } + + void addObserver(ConfigObserver *obs) { + if (obs) { + std::lock_guard lock(g_config_mutex); + if (std::find(observers_.begin(), observers_.end(), obs) == observers_.end()) { + observers_.push_back(obs); + } + } + } + + void removeObserver(ConfigObserver *obs) { + std::lock_guard lock(g_config_mutex); + observers_.erase( + std::remove(observers_.begin(), observers_.end(), obs), + observers_.end()); + } + + std::unique_ptr load(ConfigMode mode) { + std::unique_ptr config; + + switch (mode) { + case ConfigMode::NONE: + config = createDefaultConfig(); + break; + case ConfigMode::STATIC: + if (std::filesystem::exists(get_config_file_path())) { + config = loadFromStaticFile(); + } else { + MCP_WARN("Config file not found, using default settings"); + config = createDefaultConfig(); + } + break; + case ConfigMode::DYNAMIC: + config = loadFromStaticFile(); + startMonitoring(config.get()); + break; + default: + config = createDefaultConfig(); } - if (file_valid) { + notifyObservers(*config); + return config; + } + }; + + /** + * Default implementation of ConfigLoader + */ + class DefaultConfigLoader : public ConfigLoader { + protected: + std::unique_ptr createDefaultConfig() override { + auto config = std::make_unique(); + config->title = "Default MCP Server Config"; + config->server.ip = "127.0.0.1"; + config->server.port = 6666; + config->server.http_port = 6666; + config->server.https_port = 0; + config->server.log_level = "info"; + config->server.plugin_dir = "plugins"; + config->server.enable_stdio = true; + config->server.enable_http = true; + config->server.enable_https = false; + config->server.enable_auth = false; + config->plugin_hub.plugin_server_baseurl = "http://47.120.50.122"; + config->plugin_hub.plugin_server_port = 6680; + config->python_env.default_env = "system"; + config->python_env.conda_prefix = "/opt/conda"; + config->python_env.uv_venv_path = "./venv"; + return config; + } + }; + + /** + * Compatibility layer: keep old interfaces + */ + + inline void initialize_default_config() { + try { + std::string config_file = get_config_file_path(); + if (std::filesystem::exists(config_file) && std::filesystem::file_size(config_file) > 0) { return; } - // Create new configuration file with defaults inicpp::IniManager ini(config_file); - std::cout << "Creating default configuration file: " << config_file << std::endl; - // Server section configuration + MCP_INFO("Creating default config file: {}", config_file); + + // [server] ini.set("server", "ip", "0.0.0.0"); ini.set("server", "port", 6666); ini.set("server", "http_port", 6666); ini.set("server", "https_port", 6667); ini.set("server", "log_level", "trace"); ini.set("server", "log_path", "logs/mcp_server.log"); - ini.set("server", "max_file_size", 10485760);// 10MB + ini.set("server", "max_file_size", 10485760); ini.set("server", "max_files", 10); ini.set("server", "plugin_dir", "plugins"); ini.set("server", "enable_stdio", 1); @@ -282,21 +377,20 @@ namespace mcp { ini.set("server", "ssl_cert_file", "certs/server.crt"); ini.set("server", "ssl_key_file", "certs/server.key"); ini.set("server", "ssl_dh_params_file", "certs/dh2048.pem"); - // Rate limiter configuration ini.set("server", "max_requests_per_second", 100); ini.set("server", "max_concurrent_requests", 1000); - ini.set("server", "max_request_size", 1024 * 1024); // 1MB - ini.set("server", "max_response_size", 10 * 1024 * 1024);// 10MB + ini.set("server", "max_request_size", 1024 * 1024); + ini.set("server", "max_response_size", 10 * 1024 * 1024); - // Plugin hub section configuration + // [plugin_hub] ini.set("plugin_hub", "plugin_server_baseurl", "http://47.120.50.122"); ini.set("plugin_hub", "plugin_server_port", 6680); ini.set("plugin_hub", "latest_fetch_route", "/self/latest/info"); ini.set("plugin_hub", "download_route", "/self/latest/download"); ini.set("plugin_hub", "plugin_install_dir", "plugins_install"); ini.set("plugin_hub", "plugin_enable_dir", "plugins"); - ini.set("plugin_hub", "tools_enable_dir", "configs"); ini.set("plugin_hub", "tools_install_dir", "plugins_install"); + ini.set("plugin_hub", "tools_enable_dir", "configs"); // Add comments for server section ini.setComment("server", "ip", "IP address the server binds to"); @@ -330,83 +424,89 @@ namespace mcp { ini.setComment("plugin_hub", "download_route", "Route for downloading plugin"); ini.setComment("plugin_hub", "plugin_install_dir", "Directory for installing plugins"); ini.setComment("plugin_hub", "plugin_enable_dir", "Directory for enabling plugins"); + + // Add coments for PythonEnvConfig + ini.setComment("PythonEnvConfig", "default_env", "Default environment interpreter to use for Python plugins"); + ini.setComment("PythonEnvConfig", "conda_prefix", "Path to conda prefix"); + ini.setComment("PythonEnvConfig", "uv_venv_path", "Path to uv_venv"); + + // Root section configuration - ini.set("title", "MCP Server Configuration"); - ini.setComment("title", "Auto-generated configuration file for MCP Server"); - // Force configuration write + ini.set("title", "MCP Server Configuration"); + ini.setComment("title", "Auto-generated configuration file"); ini.parse(); - std::cout << "Successfully created default configuration" << std::endl; - } catch (const std::exception &) { - std::cerr << "Configuration initialization failed" << std::endl; + + MCP_INFO("Default config created successfully"); + } catch (const std::exception &e) { + MCP_ERROR("Failed to initialize default config: {}", e.what()); throw; } } - /** - * Prints configuration values to debug log - * @param config Reference to GlobalConfig instance to display - */ inline void print_config(const GlobalConfig &config) { MCP_DEBUG("===== MCP Configuration ====="); MCP_DEBUG("Title: {}", config.title); MCP_DEBUG("Server IP: {}", config.server.ip); - MCP_DEBUG("Server Port: {}", config.server.port); + MCP_DEBUG("Port: {}", config.server.port); MCP_DEBUG("HTTP Port: {}", config.server.http_port); MCP_DEBUG("HTTPS Port: {}", config.server.https_port); MCP_DEBUG("Log Level: {}", config.server.log_level); - MCP_DEBUG("Log Path: {}", config.server.log_path); - MCP_DEBUG("Log Pattern: {}", config.server.log_pattern); - MCP_DEBUG("Plugin Directory: {}", config.server.plugin_dir); - MCP_DEBUG("SSL Certificate File: {}", config.server.ssl_cert_file); - MCP_DEBUG("SSL Key File: {}", config.server.ssl_key_file); - MCP_DEBUG("SSL DH Parameters File: {}", config.server.ssl_dh_params_file); - MCP_DEBUG("Authentication Enabled: {}", config.server.enable_auth ? "Yes" : "No"); - MCP_DEBUG("Authentication Type: {}", config.server.auth_type); - MCP_DEBUG("Auth Environment File: {}", config.server.auth_env_file); - MCP_DEBUG("Max Log File Size: {}", config.server.max_file_size); - MCP_DEBUG("Max Log Files: {}", config.server.max_files); - MCP_DEBUG("Stdio Transport Enabled: {}", config.server.enable_stdio ? "Yes" : "No"); - MCP_DEBUG("HTTP Transport Enabled: {}", config.server.enable_http ? "Yes" : "No"); - MCP_DEBUG("HTTPS Transport Enabled: {}", config.server.enable_https ? "Yes" : "No"); - // Rate limiter configuration - MCP_DEBUG("Max Requests Per Second: {}", config.server.max_requests_per_second); - MCP_DEBUG("Max Concurrent Requests: {}", config.server.max_concurrent_requests); - MCP_DEBUG("Max Request Size: {}", config.server.max_request_size); - MCP_DEBUG("Max Response Size: {}", config.server.max_response_size); - MCP_DEBUG("============================="); - MCP_DEBUG("Plugin Hub Configuration:"); - MCP_DEBUG("Plugin Server Base URL: {}", config.plugin_hub.plugin_server_baseurl); - MCP_DEBUG("Plugin Server Port: {}", config.plugin_hub.plugin_server_port); - MCP_DEBUG("Latest Fetch Route: {}", config.plugin_hub.latest_fetch_route); - MCP_DEBUG("Download Route: {}", config.plugin_hub.download_route); - MCP_DEBUG("Plugin Install Directory: {}", config.plugin_hub.plugin_install_dir); - MCP_DEBUG("Plugin Enable Directory: {}", config.plugin_hub.plugin_enable_dir); + MCP_DEBUG("Plugin Dir: {}", config.server.plugin_dir); + MCP_DEBUG("Auth Enabled: {}", config.server.enable_auth ? "Yes" : "No"); + MCP_DEBUG("Max Requests/sec: {}", config.server.max_requests_per_second); + MCP_DEBUG("Plugin Server: {}:{}", config.plugin_hub.plugin_server_baseurl, config.plugin_hub.plugin_server_port); + MCP_DEBUG("Python Env: {}", config.python_env.default_env); MCP_DEBUG("============================="); } - /** - * Lists all configuration sections and key-value pairs - * Useful for debugging and verification of configuration structure - */ inline void list_config_sections() { try { - std::string config_file = get_config_file_path(); - inicpp::IniManager ini(config_file); - MCP_DEBUG("Listing all configuration sections from: {}", config_file); - - for (const auto §ion: ini.sectionsList()) { - MCP_DEBUG("\n[{}]", section); - for (const auto &[key, value]: ini.sectionMap(section)) { - MCP_DEBUG(" {} = {}", key, value); + std::string path = get_config_file_path(); + inicpp::IniManager ini(path); + MCP_DEBUG("=== Config File: {} ===", path); + for (const auto &sec: ini.sectionsList()) { + MCP_DEBUG("[{}]", sec); + for (const auto &[k, v]: ini.sectionMap(sec)) { + MCP_DEBUG(" {} = {}", k, v); } } } catch (const std::exception &e) { - MCP_ERROR("Failed to list configuration sections: {}", e.what()); + MCP_ERROR("Failed to list config sections: {}", e.what()); throw; } } + /** + * Initialize the new config system + */ + inline void initialize_config_system(ConfigMode mode = ConfigMode::STATIC) { + if (g_config_initialized.load()) return; + + g_config_loader = std::make_unique(); + { + std::lock_guard lock(g_config_mutex); + g_current_config = g_config_loader->load(mode); + } + g_config_initialized = true; + } + + /** + * Get current config (thread-safe) + */ + inline GlobalConfig get_current_config() { + std::lock_guard lock(g_config_mutex); + return *g_current_config; + } + + /** + * Update config (used by observers or reload) + */ + inline void update_current_config(const GlobalConfig &newConfig) { + std::lock_guard lock(g_config_mutex); + *g_current_config = newConfig; + } + }// namespace config }// namespace mcp diff --git a/config/config_observer.hpp b/config/config_observer.hpp new file mode 100644 index 0000000..fc4e07f --- /dev/null +++ b/config/config_observer.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace mcp { + namespace config { + struct GlobalConfig; + class ConfigObserver { + public: + virtual ~ConfigObserver() = default; + virtual void onConfigReloaded(const mcp::config::GlobalConfig &newConfig) = 0; + }; + }// namespace config +}// namespace mcp \ No newline at end of file diff --git a/scripts/format.py b/scripts/format.py index f2f8389..960dfa0 100644 --- a/scripts/format.py +++ b/scripts/format.py @@ -119,7 +119,11 @@ def main(): default_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) if len(sys.argv) <= 1 else sys.argv[1] root_dir = sys.argv[1] if len(sys.argv) > 1 else default_root suffixes = os.environ.get('SUFFIXES', '.h .cc .cpp .hpp .cxx .hxx .C').split() - parallel_jobs = int(os.environ.get('PARALLEL_JOBS', multiprocessing.cpu_count())) + + # Limit parallel jobs to avoid Windows multiprocessing limitations + # Windows has a limit of 63 handles in WaitForMultipleObjects + max_parallel_jobs = 60 + parallel_jobs = min(int(os.environ.get('PARALLEL_JOBS', multiprocessing.cpu_count())), max_parallel_jobs) # Check for clang-format clang_format = find_clang_format() diff --git a/src/business/python_plugin_instance.h b/src/business/python_plugin_instance.h index eaecd6f..2924ce3 100644 --- a/src/business/python_plugin_instance.h +++ b/src/business/python_plugin_instance.h @@ -37,7 +37,7 @@ namespace mcp::business { * @return True if virtual environment is enabled, false otherwise */ virtual bool use_virtual_env() const = 0; - + protected: /** * Detect Python version by trying common versions from 3.6 to 3.13 @@ -46,23 +46,22 @@ namespace mcp::business { std::string detect_python_version() const { // Try common Python versions from 3.13 down to 3.6 static const std::vector versions = { - "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "3.7", "3.6" - }; - + "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "3.7", "3.6"}; + // In a real implementation, we would detect the actual Python version // For now, we default to "3.9" as a reasonable fallback // A more sophisticated implementation might check the filesystem or use Python API return "3.9"; } - + #ifdef _MSC_VER /** * Secure way to get environment variable on Windows * @param name Environment variable name * @return Environment variable value or empty string if not found */ - std::string get_env_var(const char* name) const { - char* value = nullptr; + std::string get_env_var(const char *name) const { + char *value = nullptr; size_t len = 0; if (_dupenv_s(&value, &len, name) == 0 && value != nullptr) { std::string result(value); @@ -77,7 +76,7 @@ namespace mcp::business { * @param name Environment variable name * @return Environment variable value or nullptr if not found */ - const char* get_env_var(const char* name) const { + const char *get_env_var(const char *name) const { return std::getenv(name); } #endif @@ -95,7 +94,7 @@ namespace mcp::business { // Usually Python is in the PATH, so we can just use "python" return "python"; #else - return "/usr/bin/python3"; // Default system Python path + return "/usr/bin/python3";// Default system Python path #endif } @@ -103,9 +102,9 @@ namespace mcp::business { #ifdef _MSC_VER // On Windows, Python packages are typically available in the PATH // or can be found relative to the Python executable - return ""; // Empty string means use default Python path + return "";// Empty string means use default Python path #else - return "/usr/lib/python3/dist-packages"; // System Python packages path + return "/usr/lib/python3/dist-packages";// System Python packages path #endif } @@ -141,7 +140,7 @@ namespace mcp::business { */ class UvEnvConfig : public PythonEnvironmentConfig { public: - explicit UvEnvConfig(const std::string& venv_path) : venv_path_(venv_path) {} + explicit UvEnvConfig(const std::string &venv_path) : venv_path_(venv_path) {} std::string get_python_interpreter_path() const override { return venv_path_ + "/bin/python"; @@ -181,6 +180,6 @@ namespace mcp::business { std::mutex cache_mutex_; }; -} // namespace mcp::business +}// namespace mcp::business #endif// PYTHON_PLUGIN_INSTANCE_H \ No newline at end of file diff --git a/src/business/python_runtime_manager.cpp b/src/business/python_runtime_manager.cpp index 54f7493..7664113 100644 --- a/src/business/python_runtime_manager.cpp +++ b/src/business/python_runtime_manager.cpp @@ -8,175 +8,199 @@ namespace mcp::business { -PythonRuntimeManager &PythonRuntimeManager::getInstance() { - static PythonRuntimeManager instance; - return instance; -} + PythonRuntimeManager &PythonRuntimeManager::getInstance() { + static PythonRuntimeManager instance; + return instance; + } -PythonRuntimeManager::PythonRuntimeManager() : initialized_(false) { -} + PythonRuntimeManager::PythonRuntimeManager() : initialized_(false) { + } -PythonRuntimeManager::~PythonRuntimeManager() { - std::lock_guard lock(runtime_mutex_); - if (!initialized_) return; + PythonRuntimeManager::~PythonRuntimeManager() { + std::lock_guard lock(runtime_mutex_); + if (!initialized_) return; - std::ostringstream oss; - oss << std::this_thread::get_id(); - MCP_DEBUG("[PYTHON] Finalizing Python interpreter (thread: {})", oss.str()); + std::ostringstream oss; + oss << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Finalizing Python interpreter (thread: {})", oss.str()); - // 1. Restore main thread state (must be done before Py_Finalize()) - if (main_thread_state_ != nullptr) { - PyEval_RestoreThread(main_thread_state_);// Restore main thread GIL holding state - main_thread_state_ = nullptr; - MCP_DEBUG("[PYTHON] Main thread state restored"); - } + // 1. Restore main thread state (must be done before Py_Finalize()) + if (main_thread_state_ != nullptr) { + PyEval_RestoreThread(main_thread_state_);// Restore main thread GIL holding state + main_thread_state_ = nullptr; + MCP_DEBUG("[PYTHON] Main thread state restored"); + } - // 2. Destroy Python interpreter - if (Py_IsInitialized()) { - Py_Finalize(); - MCP_DEBUG("[PYTHON] Py_Finalize() called"); - } + // 2. Destroy Python interpreter + if (Py_IsInitialized()) { + Py_Finalize(); + MCP_DEBUG("[PYTHON] Py_Finalize() called"); + } - initialized_ = false; - MCP_DEBUG("[PYTHON] Runtime finalized"); -} + initialized_ = false; + MCP_DEBUG("[PYTHON] Runtime finalized"); + } -void PythonRuntimeManager::setEnvironmentConfig(std::unique_ptr config) { - std::lock_guard lock(runtime_mutex_); - env_config_ = std::move(config); -} + void PythonRuntimeManager::setEnvironmentConfig(std::unique_ptr config) { + std::lock_guard lock(runtime_mutex_); + env_config_ = std::move(config); + } -std::unique_ptr PythonRuntimeManager::createEnvironmentConfig(const std::string& type, const std::string& uv_venv_path) { - if (type == "system") { + std::unique_ptr PythonRuntimeManager::createEnvironmentConfig(const std::string &type, const std::string &uv_venv_path) { + if (type == "system") { + return std::make_unique(); + } else if (type == "conda") { + return std::make_unique(); + } else if (type == "uv") { + return std::make_unique(uv_venv_path); + } + // Default to system environment return std::make_unique(); - } else if (type == "conda") { - return std::make_unique(); - } else if (type == "uv") { - return std::make_unique(uv_venv_path); } - // Default to system environment - return std::make_unique(); -} -bool PythonRuntimeManager::initialize(const std::string &plugin_dir) { - std::lock_guard lock(runtime_mutex_); - if (initialized_) { - std::ostringstream oss; - oss << std::this_thread::get_id(); - MCP_DEBUG("[PYTHON] Runtime already initialized (thread: {})", oss.str()); - return true; - } + bool PythonRuntimeManager::initialize(const std::string &plugin_dir) { + std::lock_guard lock(runtime_mutex_); + if (initialized_) { + std::ostringstream oss; + oss << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Runtime already initialized (thread: {})", oss.str()); + return true; + } - try { - std::ostringstream oss; - oss << std::this_thread::get_id(); - MCP_DEBUG("[PYTHON] Initializing Python interpreter (thread: {})", oss.str()); - - // Set Python home and path if environment config is provided - if (env_config_) { - std::string python_home = env_config_->get_python_interpreter_path(); - if (!python_home.empty()) { - // Use PyConfig for Python 3.8+ (more modern approach) - // This is a safer way to set Python home - #if PY_VERSION_HEX >= 0x03080000 - PyConfig config; - PyConfig_InitPythonConfig(&config); - PyConfig_SetString(&config, &config.home, Py_DecodeLocale(python_home.c_str(), nullptr)); - Py_InitializeFromConfig(&config); - PyConfig_Clear(&config); - #else - Py_SetPythonHome(Py_DecodeLocale(python_home.c_str(), nullptr)); - #endif - MCP_DEBUG("[PYTHON] Set Python home to: {}", python_home); + try { + std::ostringstream oss; + oss << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Initializing Python interpreter (thread: {})", oss.str()); + + // Set Python home and path if environment config is provided + if (env_config_) { + std::string python_home = env_config_->get_python_interpreter_path(); + if (!python_home.empty()) { +// Use PyConfig for Python 3.8+ (more modern approach) +// This is a safer way to set Python home +#if PY_VERSION_HEX >= 0x03080000 + PyConfig config; + PyConfig_InitPythonConfig(&config); + PyConfig_SetString(&config, &config.home, Py_DecodeLocale(python_home.c_str(), nullptr)); + Py_InitializeFromConfig(&config); + PyConfig_Clear(&config); +#else + Py_SetPythonHome(Py_DecodeLocale(python_home.c_str(), nullptr)); +#endif + MCP_DEBUG("[PYTHON] Set Python home to: {}", python_home); + } else { + // Initialize Python interpreter (void type, no need to check return value) + Py_Initialize(); + } } else { // Initialize Python interpreter (void type, no need to check return value) Py_Initialize(); } - } else { - // Initialize Python interpreter (void type, no need to check return value) - Py_Initialize(); - } - - MCP_DEBUG("[PYTHON] Py_Initialize() called (interpreter initialized)"); - - // 2. Enable multi-threading support (Python 3.9+ compatible, alternative to PyEval_InitThreads()) - PyGILState_STATE gil_state = PyGILState_Ensure(); - PyGILState_Release(gil_state); - MCP_DEBUG("[PYTHON] Multi-thread support enabled (via PyGILState_Ensure)"); - - // 3. Release main thread GIL and save thread state (child threads can acquire GIL) - main_thread_state_ = PyEval_SaveThread(); - if (main_thread_state_ == nullptr) { - MCP_WARN("[PYTHON] Warning: PyEval_SaveThread() returned null"); - } else { - std::ostringstream oss2; - oss2 << std::this_thread::get_id(); - MCP_DEBUG("[PYTHON] Main thread GIL released (thread: {})", oss2.str()); - } - // 4. Re-acquire GIL and add plugin directory to sys.path - py::gil_scoped_acquire acquire; - py::module_ sys = py::module_::import("sys"); - sys.attr("path").attr("append")(plugin_dir); - MCP_DEBUG("[PYTHON] Added plugin dir to sys.path: {}", plugin_dir); - - // Add additional paths from environment config if provided - if (env_config_) { - std::string python_path = env_config_->get_python_path(); - if (!python_path.empty()) { - sys.attr("path").attr("append")(python_path); - MCP_DEBUG("[PYTHON] Added Python path to sys.path: {}", python_path); + MCP_DEBUG("[PYTHON] Py_Initialize() called (interpreter initialized)"); + + // 2. Enable multi-threading support (Python 3.9+ compatible, alternative to PyEval_InitThreads()) + PyGILState_STATE gil_state = PyGILState_Ensure(); + PyGILState_Release(gil_state); + MCP_DEBUG("[PYTHON] Multi-thread support enabled (via PyGILState_Ensure)"); + + // 3. Release main thread GIL and save thread state (child threads can acquire GIL) + main_thread_state_ = PyEval_SaveThread(); + if (main_thread_state_ == nullptr) { + MCP_WARN("[PYTHON] Warning: PyEval_SaveThread() returned null"); + } else { + std::ostringstream oss2; + oss2 << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Main thread GIL released (thread: {})", oss2.str()); } - } - // Fix: Convert py::str to std::string before output - std::string sys_path_str = py::str(sys.attr("path")).cast(); - MCP_DEBUG("[PYTHON] sys.path: {}", sys_path_str); + // 4. Re-acquire GIL and add plugin directory to sys.path + py::gil_scoped_acquire acquire; + py::module_ sys = py::module_::import("sys"); + sys.attr("path").attr("append")(plugin_dir); + MCP_DEBUG("[PYTHON] Added plugin dir to sys.path: {}", plugin_dir); + + // Add additional paths from environment config if provided + if (env_config_) { + std::string python_path = env_config_->get_python_path(); + if (!python_path.empty()) { + sys.attr("path").attr("append")(python_path); + MCP_DEBUG("[PYTHON] Added Python path to sys.path: {}", python_path); + } + } - initialized_ = true; - MCP_INFO("[PYTHON] Runtime initialized successfully"); - return true; - } catch (const py::error_already_set &e) { - MCP_ERROR("[PYTHON] Init Python error: {}", e.what()); - //MCP_ERROR("[PYTHON] Traceback: {}", e.trace()); - if (Py_IsInitialized()) { - Py_Finalize();// Only call when already initialized - } - return false; - } catch (const std::exception &e) { - MCP_ERROR("[PYTHON] Init C++ error: {}", e.what()); - if (Py_IsInitialized()) { - Py_Finalize(); + // Fix: Convert py::str to std::string before output + std::string sys_path_str = py::str(sys.attr("path")).cast(); + MCP_DEBUG("[PYTHON] sys.path: {}", sys_path_str); + + initialized_ = true; + MCP_INFO("[PYTHON] Runtime initialized successfully"); + return true; + } catch (const py::error_already_set &e) { + MCP_ERROR("[PYTHON] Init Python error: {}", e.what()); + //MCP_ERROR("[PYTHON] Traceback: {}", e.trace()); + if (Py_IsInitialized()) { + Py_Finalize();// Only call when already initialized + } + return false; + } catch (const std::exception &e) { + MCP_ERROR("[PYTHON] Init C++ error: {}", e.what()); + if (Py_IsInitialized()) { + Py_Finalize(); + } + return false; } - return false; } -} -bool PythonRuntimeManager::isInitialized() const { - std::lock_guard lock(runtime_mutex_); - return initialized_; -} + bool PythonRuntimeManager::isInitialized() const { + std::lock_guard lock(runtime_mutex_); + return initialized_; + } + + py::module_ PythonRuntimeManager::importModule(const std::string &module_name) { + std::lock_guard lock(runtime_mutex_); -py::module_ PythonRuntimeManager::importModule(const std::string &module_name) { - std::lock_guard lock(runtime_mutex_); + if (!initialized_) { + throw std::runtime_error("Python runtime not initialized"); + } - if (!initialized_) { - throw std::runtime_error("Python runtime not initialized"); + return py::module_::import(module_name.c_str()); } - return py::module_::import(module_name.c_str()); -} + void PythonRuntimeManager::addPath(const std::string &path) { + std::lock_guard lock(runtime_mutex_); -void PythonRuntimeManager::addPath(const std::string &path) { - std::lock_guard lock(runtime_mutex_); + if (!initialized_) { + throw std::runtime_error("Python runtime not initialized"); + } - if (!initialized_) { - throw std::runtime_error("Python runtime not initialized"); + py::module_ sys = py::module_::import("sys"); + sys.attr("path").attr("append")(path); } - py::module_ sys = py::module_::import("sys"); - sys.attr("path").attr("append")(path); -} +}// namespace mcp::business + +// Implementation of PythonConfigObserver +namespace mcp::business { + + PythonConfigObserver::PythonConfigObserver(PythonRuntimeManager &manager) + : runtime_manager_(manager) {} + + void PythonConfigObserver::onConfigReloaded(const mcp::config::GlobalConfig &newConfig) { + MCP_INFO("PythonConfigObserver: Applying new Python environment configuration..."); + + try { + auto new_env_config = PythonRuntimeManager::createEnvironmentConfig( + newConfig.python_env.default_env, + newConfig.python_env.uv_venv_path); + runtime_manager_.setEnvironmentConfig(std::move(new_env_config)); + MCP_DEBUG("Python environment updated: default='{}', uv_venv='{}'", + newConfig.python_env.default_env, + newConfig.python_env.uv_venv_path); + } catch (const std::exception &e) { + MCP_ERROR("Failed to update Python environment: {}", e.what()); + } + } -} // namespace mcp::business \ No newline at end of file +}// namespace mcp::business \ No newline at end of file diff --git a/src/business/python_runtime_manager.h b/src/business/python_runtime_manager.h index b99d902..a3ff0e6 100644 --- a/src/business/python_runtime_manager.h +++ b/src/business/python_runtime_manager.h @@ -1,6 +1,8 @@ #ifndef PYTHON_RUNTIME_MANAGER_H #define PYTHON_RUNTIME_MANAGER_H +#include "config/config.hpp" +#include "config/config_observer.hpp" #include "python_plugin_instance.h" #include #include @@ -8,12 +10,30 @@ #include #include + // Forward declaration for Python types -struct _ts; // PyThreadState +struct _ts;// PyThreadState typedef struct _ts PyThreadState; +namespace mcp { + namespace config { + struct GlobalConfig; + } +}// namespace mcp + namespace mcp::business { namespace py = pybind11; + class PythonRuntimeManager; + + // Observer that listens for config changes and updates Python runtime + class PythonConfigObserver : public mcp::config::ConfigObserver { + private: + PythonRuntimeManager &runtime_manager_; + + public: + explicit PythonConfigObserver(PythonRuntimeManager &manager); + void onConfigReloaded(const mcp::config::GlobalConfig &newConfig) override; + }; class PythonRuntimeManager { public: @@ -43,6 +63,6 @@ namespace mcp::business { std::unique_ptr env_config_; }; -} // namespace mcp::business +}// namespace mcp::business #endif// PYTHON_RUNTIME_MANAGER_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index d2e164b..71a2885 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "Auth/AuthManager.hpp" #include "business/python_runtime_manager.h" #include "config/config.hpp"// Configuration management using INI file +#include "config/config_observer.hpp" #include "core/logger.h" #include "core/server.h" #include "metrics/metrics_manager.h" @@ -11,9 +12,9 @@ #include #include #include +#include #include - /** * Entry point of the MCP server application. * This function initializes the configuration, sets up logging, builds the server instance, @@ -25,10 +26,10 @@ int main() { try { // Step 1: Ensure the default configuration file exists. // If the config file is missing or empty, create one with safe defaults. - mcp::config::initialize_default_config(); + mcp::config::initialize_config_system(mcp::config::ConfigMode::DYNAMIC); // Step 2: Load the full configuration from the INI file. - auto config = mcp::config::GlobalConfig::load(); + auto config = mcp::config::get_current_config(); // Step 3: Initialize the asynchronous logger using settings from the config. // Parameters include log file path, log level, maximum file size, and number of rotation files. @@ -50,17 +51,22 @@ int main() { MCP_INFO(" Log Path: {}", config.server.log_path); auto address = config.server.ip; - // Initialize Python environment configuration - + // Step 4: Set up PythonRuntimeManager with initial config and observer auto &python_runtime_manager = mcp::business::PythonRuntimeManager::getInstance(); auto python_env_config = mcp::business::PythonRuntimeManager::createEnvironmentConfig( config.python_env.default_env, config.python_env.uv_venv_path); python_runtime_manager.setEnvironmentConfig(std::move(python_env_config)); - MCP_INFO("Python Environment Configuration:"); - MCP_INFO(" Default Environment: {}", config.python_env.default_env); - MCP_INFO(" Conda Prefix: {}", config.python_env.conda_prefix); - MCP_INFO(" UV Venv Path: {}", config.python_env.uv_venv_path); + + MCP_INFO("Python Environment Initialized:"); + MCP_INFO(" Default: {}", config.python_env.default_env); + MCP_INFO(" UV Venv: {}", config.python_env.uv_venv_path); + + auto python_observer = std::make_unique(python_runtime_manager); + + // Register observer — now it will be notified on every config reload + mcp::config::g_config_loader->addObserver(python_observer.get()); + // Step 5: Initialize the metrics manager. // You can set a metrics callback here