From 01f08ea901b32b91d65d9b2903119323a40a0c70 Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Sat, 17 Jan 2026 01:44:52 +0500 Subject: [PATCH 01/12] Fix a crash (SIGABRT) that occurred when reloading MCP variables while the MCP server was already running. The issue was caused by improper cleanup of handler objects during reinitialization. Root cause: - ProxySQL_MCP_Server destructor deletes mysql_tool_handler - The old code tried to delete handlers again after deleting the server, causing double-free corruption The fix properly handles handler lifecycle during reinitialization: 1. Delete Query_Tool_Handler first (server destructor doesn't clean this) 2. Delete the server (which also deletes MySQL_Tool_Handler via destructor) 3. Delete other handlers (config/admin/cache/observe) created by old server 4. Create new MySQL_Tool_Handler with updated configuration 5. Create new Query_Tool_Handler 6. Create new server (recreates all handlers with new endpoints) This ensures proper cleanup and prevents double-free issues while allowing runtime reconfiguration of MySQL connection parameters. --- lib/Admin_FlushVariables.cpp | 74 ++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/lib/Admin_FlushVariables.cpp b/lib/Admin_FlushVariables.cpp index 26b954a638..5fb3a7cd00 100644 --- a/lib/Admin_FlushVariables.cpp +++ b/lib/Admin_FlushVariables.cpp @@ -1512,17 +1512,44 @@ void ProxySQL_Admin::flush_mcp_variables___runtime_to_database(SQLite3DB* db, bo } } } else { - // Server is already running - check if MySQL configuration changed - // and reinitialize the tool handler if needed - proxy_info("MCP: Server already running, checking MySQL tool handler reinitialization\n"); - if (GloMCPH->mysql_tool_handler) { - // Delete old handler - delete GloMCPH->mysql_tool_handler; - GloMCPH->mysql_tool_handler = NULL; + // Server is already running - need to stop, delete server, and recreate everything + proxy_info("MCP: Server already running, reinitializing MySQL tool handler\n"); + + // 1. Delete Query_Tool_Handler first (server destructor doesn't delete this) + if (GloMCPH->query_tool_handler) { + proxy_info("MCP: Deleting old Query Tool Handler\n"); + delete GloMCPH->query_tool_handler; + GloMCPH->query_tool_handler = NULL; + } + + // 2. Stop and delete the server (server destructor also deletes MySQL_Tool_Handler) + proxy_info("MCP: Stopping and deleting old server\n"); + delete GloMCPH->mcp_server; + GloMCPH->mcp_server = NULL; + // Note: mysql_tool_handler is already deleted by server destructor and set to NULL + proxy_info("MCP: Old server deleted\n"); + + // 3. Delete other handlers that were created by old server + // The server destructor doesn't clean these up, so we need to do it manually + if (GloMCPH->config_tool_handler) { + delete GloMCPH->config_tool_handler; + GloMCPH->config_tool_handler = NULL; + } + if (GloMCPH->admin_tool_handler) { + delete GloMCPH->admin_tool_handler; + GloMCPH->admin_tool_handler = NULL; + } + if (GloMCPH->cache_tool_handler) { + delete GloMCPH->cache_tool_handler; + GloMCPH->cache_tool_handler = NULL; + } + if (GloMCPH->observe_tool_handler) { + delete GloMCPH->observe_tool_handler; + GloMCPH->observe_tool_handler = NULL; } - // Create new tool handler with current configuration - proxy_info("MCP: Reinitializing MySQL Tool Handler with current configuration\n"); + // 4. Create new MySQL_Tool_Handler with current configuration + proxy_info("MCP: Creating new MySQL Tool Handler with updated configuration\n"); GloMCPH->mysql_tool_handler = new MySQL_Tool_Handler( GloMCPH->variables.mcp_mysql_hosts ? GloMCPH->variables.mcp_mysql_hosts : "", GloMCPH->variables.mcp_mysql_ports ? GloMCPH->variables.mcp_mysql_ports : "", @@ -1533,11 +1560,36 @@ void ProxySQL_Admin::flush_mcp_variables___runtime_to_database(SQLite3DB* db, bo ); if (GloMCPH->mysql_tool_handler->init() != 0) { - proxy_error("MCP: Failed to reinitialize MySQL Tool Handler\n"); + proxy_error("MCP: Failed to initialize new MySQL Tool Handler\n"); delete GloMCPH->mysql_tool_handler; GloMCPH->mysql_tool_handler = NULL; } else { - proxy_info("MCP: MySQL Tool Handler reinitialized successfully\n"); + proxy_info("MCP: New MySQL Tool Handler initialized successfully\n"); + + // 5. Create new Query_Tool_Handler that wraps the new MySQL_Tool_Handler + GloMCPH->query_tool_handler = new Query_Tool_Handler(GloMCPH->mysql_tool_handler); + if (GloMCPH->query_tool_handler->init() != 0) { + proxy_error("MCP: Failed to initialize new Query Tool Handler\n"); + delete GloMCPH->query_tool_handler; + GloMCPH->query_tool_handler = NULL; + } else { + proxy_info("MCP: New Query Tool Handler initialized successfully\n"); + } + } + + // 6. Create and start new server (which will recreate all handlers including config/admin/cache/observe) + if (GloMCPH->mysql_tool_handler && GloMCPH->query_tool_handler) { + proxy_info("MCP: Creating and starting new server\n"); + int port = GloMCPH->variables.mcp_port; + GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); + if (GloMCPH->mcp_server) { + GloMCPH->mcp_server->start(); + proxy_info("MCP: New server created and started successfully\n"); + } else { + proxy_error("MCP: Failed to create new server instance\n"); + } + } else { + proxy_error("MCP: Server not created due to handler initialization failure\n"); } } } else { From ddc4e65706eb0a46f36fb4b95443c63b112318dc Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Sun, 18 Jan 2026 18:08:57 +0500 Subject: [PATCH 02/12] Add plain HTTP support for MCP server and fix SSL/port restart issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add full support for both HTTP and HTTPS modes in MCP server via the mcp_use_ssl configuration variable, enabling plain HTTP for development and HTTPS for production with proper certificate validation * Server now automatically restarts when SSL mode or port configuration changes, fixing silent configuration failures where changes appeared to succeed but didn't take effect until manual restart. Features: - Explicit support for HTTP mode (mcp_use_ssl=false) without SSL certificates - Explicit support for HTTPS mode (mcp_use_ssl=true) with certificate validation - Configurable via configure_mcp.sh with --no-ssl or --use-ssl flags - Settable via admin interface: SET mcp-use_ssl=true/false - Automatic restart detection for SSL mode changes (HTTP ↔ HTTPS) - Automatic restart detection for port changes (mcp_port) --- include/MCP_Thread.h | 9 ++- include/ProxySQL_MCP_Server.hpp | 27 +++++-- lib/Admin_FlushVariables.cpp | 126 ++++++++++++++++++++++++++++---- lib/MCP_Thread.cpp | 21 +++++- lib/ProxySQL_MCP_Server.cpp | 113 ++++++++++++++++++++-------- scripts/mcp/configure_mcp.sh | 49 ++++++++++--- src/proxysql.cfg | 3 +- 7 files changed, 284 insertions(+), 64 deletions(-) diff --git a/include/MCP_Thread.h b/include/MCP_Thread.h index acf68dfb47..fa92fe7231 100644 --- a/include/MCP_Thread.h +++ b/include/MCP_Thread.h @@ -41,7 +41,8 @@ class MCP_Threads_Handler */ struct { bool mcp_enabled; ///< Enable/disable MCP server - int mcp_port; ///< HTTPS port for MCP server (default: 6071) + int mcp_port; ///< HTTP/HTTPS port for MCP server (default: 6071) + bool mcp_use_ssl; ///< Enable/disable SSL/TLS (default: true) char* mcp_config_endpoint_auth; ///< Authentication for /mcp/config endpoint char* mcp_observe_endpoint_auth; ///< Authentication for /mcp/observe endpoint char* mcp_query_endpoint_auth; ///< Authentication for /mcp/query endpoint @@ -67,9 +68,9 @@ class MCP_Threads_Handler } status_variables; /** - * @brief Pointer to the HTTPS server instance + * @brief Pointer to the HTTP/HTTPS server instance * - * This is managed by the MCP_Thread module and provides HTTPS + * This is managed by the MCP_Thread module and provides HTTP/HTTPS * endpoints for MCP protocol communication. */ ProxySQL_MCP_Server* mcp_server; @@ -136,7 +137,7 @@ class MCP_Threads_Handler * @brief Initialize the MCP module * * Sets up the module with default configuration values and starts - * the HTTPS server if enabled. Must be called before using any + * the HTTP/HTTPS server if enabled. Must be called before using any * other methods. */ void init(); diff --git a/include/ProxySQL_MCP_Server.hpp b/include/ProxySQL_MCP_Server.hpp index e4ed237db3..33df7a92a8 100644 --- a/include/ProxySQL_MCP_Server.hpp +++ b/include/ProxySQL_MCP_Server.hpp @@ -17,14 +17,16 @@ class MCP_Threads_Handler; /** * @brief ProxySQL MCP Server class * - * This class wraps an HTTPS server using libhttpserver to provide - * MCP (Model Context Protocol) endpoints. It supports multiple + * This class wraps an HTTP/HTTPS server using libhttpserver to provide + * MCP (Model Context Protocol) endpoints. Supports both HTTP and HTTPS + * modes based on mcp_use_ssl configuration. It supports multiple * MCP server endpoints with their own authentication. */ class ProxySQL_MCP_Server { private: std::unique_ptr ws; int port; + bool use_ssl; // SSL mode the server was started with pthread_t thread_id; // Endpoint resources @@ -36,7 +38,8 @@ class ProxySQL_MCP_Server { /** * @brief Constructor for ProxySQL_MCP_Server * - * Creates a new HTTPS server instance on the specified port. + * Creates a new HTTP/HTTPS server instance on the specified port. + * Uses HTTPS if mcp_use_ssl is true, otherwise uses HTTP. * * @param p The port number to listen on * @param h Pointer to the MCP_Threads_Handler instance @@ -51,18 +54,32 @@ class ProxySQL_MCP_Server { ~ProxySQL_MCP_Server(); /** - * @brief Start the HTTPS server + * @brief Start the HTTP/HTTPS server * * Starts the webserver in a dedicated thread. */ void start(); /** - * @brief Stop the HTTPS server + * @brief Stop the HTTP/HTTPS server * * Stops the webserver and waits for the thread to complete. */ void stop(); + + /** + * @brief Get the port the server is listening on + * + * @return int The port number + */ + int get_port() const { return port; } + + /** + * @brief Check if the server is using SSL/TLS + * + * @return true if server is using HTTPS, false if using HTTP + */ + bool is_using_ssl() const { return use_ssl; } }; #endif /* CLASS_PROXYSQL_MCP_SERVER_H */ diff --git a/lib/Admin_FlushVariables.cpp b/lib/Admin_FlushVariables.cpp index 5fb3a7cd00..568cbb92bc 100644 --- a/lib/Admin_FlushVariables.cpp +++ b/lib/Admin_FlushVariables.cpp @@ -27,6 +27,11 @@ using json = nlohmann::json; #include "proxysql_restapi.h" #include "MCP_Thread.h" #include "MySQL_Tool_Handler.h" +#include "Query_Tool_Handler.h" +#include "Config_Tool_Handler.h" +#include "Admin_Tool_Handler.h" +#include "Cache_Tool_Handler.h" +#include "Observe_Tool_Handler.h" #include "ProxySQL_MCP_Server.hpp" #include "proxysql_utils.h" #include "prometheus_helpers.h" @@ -1365,12 +1370,27 @@ void ProxySQL_Admin::flush_mcp_variables___database_to_runtime(SQLite3DB* db, bo if (enabled) { // Start the server if not already running if (GloMCPH->mcp_server == NULL) { - // Check if SSL certificates are available - if (!GloVars.global.ssl_key_pem_mem || !GloVars.global.ssl_cert_pem_mem) { - proxy_error("MCP: Cannot start server - SSL certificates not loaded. Please configure ssl_key_fp and ssl_cert_fp.\n"); + // Only check SSL certificates if SSL mode is enabled + if (GloMCPH->variables.mcp_use_ssl) { + if (!GloVars.global.ssl_key_pem_mem || !GloVars.global.ssl_cert_pem_mem) { + proxy_error("MCP: Cannot start server in SSL mode - SSL certificates not loaded. " + "Please configure ssl_key_fp and ssl_cert_fp, or set mcp_use_ssl=false.\n"); + } else { + int port = GloMCPH->variables.mcp_port; + const char* mode = GloMCPH->variables.mcp_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("MCP: Starting %s server on port %d\n", mode, port); + GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); + if (GloMCPH->mcp_server) { + GloMCPH->mcp_server->start(); + proxy_info("MCP: Server started successfully\n"); + } else { + proxy_error("MCP: Failed to create server instance\n"); + } + } } else { + // HTTP mode - start without SSL certificates int port = GloMCPH->variables.mcp_port; - proxy_info("MCP: Starting HTTPS server on port %d\n", port); + proxy_info("MCP: Starting HTTP server on port %d (unencrypted)\n", port); GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); if (GloMCPH->mcp_server) { GloMCPH->mcp_server->start(); @@ -1380,14 +1400,78 @@ void ProxySQL_Admin::flush_mcp_variables___database_to_runtime(SQLite3DB* db, bo } } } else { - proxy_info("MCP: Server already running, updating configuration...\n"); - // Server is already running - we could update port/restart if needed - // For now, just log that it's running + proxy_info("MCP: Server already running, checking if configuration changed...\n"); + + // Check if restart is needed due to configuration changes + bool needs_restart = false; + std::string restart_reason; + + // Check if port changed + int current_port = GloMCPH->variables.mcp_port; + int server_port = GloMCPH->mcp_server->get_port(); + if (current_port != server_port) { + needs_restart = true; + restart_reason += "port (" + std::to_string(server_port) + " -> " + std::to_string(current_port) + ") "; + } + + // Check if SSL mode changed + bool current_use_ssl = GloMCPH->variables.mcp_use_ssl; + bool server_use_ssl = GloMCPH->mcp_server->is_using_ssl(); + if (current_use_ssl != server_use_ssl) { + needs_restart = true; + restart_reason += "SSL mode (" + std::string(server_use_ssl ? "HTTPS" : "HTTP") + " -> " + std::string(current_use_ssl ? "HTTPS" : "HTTP") + ") "; + } + + if (needs_restart) { + proxy_info("MCP: Configuration changed (%s), restarting server...\n", restart_reason.c_str()); + + // Stop server with old configuration + const char* old_mode = server_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("MCP: Stopping %s server on port %d\n", old_mode, server_port); + delete GloMCPH->mcp_server; + GloMCPH->mcp_server = NULL; + + // Start server with new configuration + int new_port = GloMCPH->variables.mcp_port; + bool new_use_ssl = GloMCPH->variables.mcp_use_ssl; + const char* new_mode = new_use_ssl ? "HTTPS" : "HTTP"; + + // Check SSL certificates if needed + if (new_use_ssl) { + if (!GloVars.global.ssl_key_pem_mem || !GloVars.global.ssl_cert_pem_mem) { + proxy_error("MCP: Cannot start server in SSL mode - SSL certificates not loaded. " + "Please configure ssl_key_fp and ssl_cert_fp, or set mcp_use_ssl=false.\n"); + // Leave server stopped + } else { + proxy_info("MCP: Starting %s server on port %d\n", new_mode, new_port); + GloMCPH->mcp_server = new ProxySQL_MCP_Server(new_port, GloMCPH); + if (GloMCPH->mcp_server) { + GloMCPH->mcp_server->start(); + proxy_info("MCP: Server restarted successfully\n"); + } else { + proxy_error("MCP: Failed to create server instance\n"); + } + } + } else { + // HTTP mode - no SSL certificates needed + proxy_info("MCP: Starting %s server on port %d (unencrypted)\n", new_mode, new_port); + GloMCPH->mcp_server = new ProxySQL_MCP_Server(new_port, GloMCPH); + if (GloMCPH->mcp_server) { + GloMCPH->mcp_server->start(); + proxy_info("MCP: Server restarted successfully\n"); + } else { + proxy_error("MCP: Failed to create server instance\n"); + } + } + } else { + proxy_info("MCP: Server already running, no configuration changes detected\n"); + } } } else { // Stop the server if running if (GloMCPH->mcp_server != NULL) { - proxy_info("MCP: Stopping HTTPS server\n"); + const char* mode = GloMCPH->variables.mcp_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("MCP: Stopping %s server\n", mode); delete GloMCPH->mcp_server; GloMCPH->mcp_server = NULL; proxy_info("MCP: Server stopped successfully\n"); @@ -1497,12 +1581,27 @@ void ProxySQL_Admin::flush_mcp_variables___runtime_to_database(SQLite3DB* db, bo if (enabled) { // Start the server if not already running if (GloMCPH->mcp_server == NULL) { - // Check if SSL certificates are available - if (!GloVars.global.ssl_key_pem_mem || !GloVars.global.ssl_cert_pem_mem) { - proxy_error("MCP: Cannot start server - SSL certificates not loaded. Please configure ssl_key_fp and ssl_cert_fp.\n"); + // Only check SSL certificates if SSL mode is enabled + if (GloMCPH->variables.mcp_use_ssl) { + if (!GloVars.global.ssl_key_pem_mem || !GloVars.global.ssl_cert_pem_mem) { + proxy_error("MCP: Cannot start server in SSL mode - SSL certificates not loaded. " + "Please configure ssl_key_fp and ssl_cert_fp, or set mcp_use_ssl=false.\n"); + } else { + int port = GloMCPH->variables.mcp_port; + const char* mode = GloMCPH->variables.mcp_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("MCP: Starting %s server on port %d\n", mode, port); + GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); + if (GloMCPH->mcp_server) { + GloMCPH->mcp_server->start(); + proxy_info("MCP: Server started successfully\n"); + } else { + proxy_error("MCP: Failed to create server instance\n"); + } + } } else { + // HTTP mode - start without SSL certificates int port = GloMCPH->variables.mcp_port; - proxy_info("MCP: Starting HTTPS server on port %d\n", port); + proxy_info("MCP: Starting HTTP server on port %d (unencrypted)\n", port); GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); if (GloMCPH->mcp_server) { GloMCPH->mcp_server->start(); @@ -1595,7 +1694,8 @@ void ProxySQL_Admin::flush_mcp_variables___runtime_to_database(SQLite3DB* db, bo } else { // Stop the server if running if (GloMCPH->mcp_server != NULL) { - proxy_info("MCP: Stopping HTTPS server\n"); + const char* mode = GloMCPH->variables.mcp_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("MCP: Stopping %s server\n", mode); delete GloMCPH->mcp_server; GloMCPH->mcp_server = NULL; proxy_info("MCP: Server stopped successfully\n"); diff --git a/lib/MCP_Thread.cpp b/lib/MCP_Thread.cpp index 9d8a578608..5a61f23851 100644 --- a/lib/MCP_Thread.cpp +++ b/lib/MCP_Thread.cpp @@ -17,6 +17,7 @@ static const char* mcp_thread_variables_names[] = { "enabled", "port", + "use_ssl", "config_endpoint_auth", "observe_endpoint_auth", "query_endpoint_auth", @@ -42,6 +43,7 @@ MCP_Threads_Handler::MCP_Threads_Handler() { // Initialize variables with default values variables.mcp_enabled = false; variables.mcp_port = 6071; + variables.mcp_use_ssl = true; // Default to true for security variables.mcp_config_endpoint_auth = strdup(""); variables.mcp_observe_endpoint_auth = strdup(""); variables.mcp_query_endpoint_auth = strdup(""); @@ -135,7 +137,7 @@ MCP_Threads_Handler::~MCP_Threads_Handler() { void MCP_Threads_Handler::init() { proxy_info("Initializing MCP Threads Handler\n"); // For now, this is a simple initialization - // The HTTPS server will be started when mcp_enabled is set to true + // The HTTP/HTTPS server will be started when mcp_enabled is set to true // and will be managed through ProxySQL_Admin print_version(); } @@ -144,7 +146,7 @@ void MCP_Threads_Handler::shutdown() { proxy_info("Shutting down MCP Threads Handler\n"); shutdown_ = 1; - // Stop the HTTPS server if it's running + // Stop the HTTP/HTTPS server if it's running if (mcp_server) { delete mcp_server; mcp_server = NULL; @@ -171,6 +173,10 @@ int MCP_Threads_Handler::get_variable(const char* name, char* val) { sprintf(val, "%d", variables.mcp_port); return 0; } + if (!strcmp(name, "use_ssl")) { + sprintf(val, "%s", variables.mcp_use_ssl ? "true" : "false"); + return 0; + } if (!strcmp(name, "config_endpoint_auth")) { sprintf(val, "%s", variables.mcp_config_endpoint_auth ? variables.mcp_config_endpoint_auth : ""); return 0; @@ -247,6 +253,17 @@ int MCP_Threads_Handler::set_variable(const char* name, const char* value) { } return -1; } + if (!strcmp(name, "use_ssl")) { + if (strcasecmp(value, "true") == 0 || strcasecmp(value, "1") == 0) { + variables.mcp_use_ssl = true; + return 0; + } + if (strcasecmp(value, "false") == 0 || strcasecmp(value, "0") == 0) { + variables.mcp_use_ssl = false; + return 0; + } + return -1; + } if (!strcmp(name, "config_endpoint_auth")) { if (variables.mcp_config_endpoint_auth) free(variables.mcp_config_endpoint_auth); diff --git a/lib/ProxySQL_MCP_Server.cpp b/lib/ProxySQL_MCP_Server.cpp index fc58f6405c..ed1d01e9a1 100644 --- a/lib/ProxySQL_MCP_Server.cpp +++ b/lib/ProxySQL_MCP_Server.cpp @@ -34,31 +34,44 @@ static void *mcp_server_thread(void *arg) { } ProxySQL_MCP_Server::ProxySQL_MCP_Server(int p, MCP_Threads_Handler* h) - : port(p), handler(h), thread_id(0) + : port(p), handler(h), thread_id(0), use_ssl(h->variables.mcp_use_ssl) { - proxy_info("Creating ProxySQL MCP Server on port %d\n", port); - - // Get SSL certificates from ProxySQL - char* ssl_key = NULL; - char* ssl_cert = NULL; - GloVars.get_SSL_pem_mem(&ssl_key, &ssl_cert); + proxy_info("Creating ProxySQL MCP Server on port %d (SSL: %s)\n", + port, use_ssl ? "enabled" : "disabled"); + + // Create webserver - conditionally use SSL + if (handler->variables.mcp_use_ssl) { + // HTTPS mode: Get SSL certificates from ProxySQL + char* ssl_key = NULL; + char* ssl_cert = NULL; + GloVars.get_SSL_pem_mem(&ssl_key, &ssl_cert); + + // Check if SSL certificates are available + if (!ssl_key || !ssl_cert) { + proxy_error("Cannot start MCP server in SSL mode: SSL certificates not loaded. " + "Please configure ssl_key_fp and ssl_cert_fp, or set mcp_use_ssl=false.\n"); + return; + } - // Check if SSL certificates are available - if (!ssl_key || !ssl_cert) { - proxy_error("Cannot start MCP server: SSL certificates not loaded. Please configure ssl_key_fp and ssl_cert_fp.\n"); - return; + // Create HTTPS webserver using ProxySQL TLS certificates + ws = std::unique_ptr(new webserver( + create_webserver(port) + .use_ssl() + .raw_https_mem_key(std::string(ssl_key)) + .raw_https_mem_cert(std::string(ssl_cert)) + .no_post_process() + )); + proxy_info("MCP server configured for HTTPS\n"); + } else { + // HTTP mode: No SSL certificates required + ws = std::unique_ptr(new webserver( + create_webserver(port) + .no_ssl() // Explicitly disable SSL + .no_post_process() + )); + proxy_info("MCP server configured for HTTP (unencrypted)\n"); } - // Create HTTPS webserver using existing ProxySQL TLS certificates - // Use raw_https_mem_key/raw_https_mem_cert to pass in-memory PEM buffers - ws = std::unique_ptr(new webserver( - create_webserver(port) - .use_ssl() - .raw_https_mem_key(std::string(ssl_key)) - .raw_https_mem_cert(std::string(ssl_cert)) - .no_post_process() - )); - // Initialize tool handlers for each endpoint proxy_info("Initializing MCP tool handlers...\n"); @@ -152,11 +165,49 @@ ProxySQL_MCP_Server::ProxySQL_MCP_Server(int p, MCP_Threads_Handler* h) ProxySQL_MCP_Server::~ProxySQL_MCP_Server() { stop(); - // Clean up MySQL Tool Handler - if (handler && handler->mysql_tool_handler) { - proxy_info("Cleaning up MySQL Tool Handler...\n"); - delete handler->mysql_tool_handler; - handler->mysql_tool_handler = NULL; + // Clean up all tool handlers stored in the handler object + if (handler) { + // Clean up MySQL Tool Handler + if (handler->mysql_tool_handler) { + proxy_debug(PROXY_DEBUG_GENERIC, 4, "Cleaning up MySQL Tool Handler\n"); + delete handler->mysql_tool_handler; + handler->mysql_tool_handler = NULL; + } + + // Clean up Config Tool Handler + if (handler->config_tool_handler) { + proxy_debug(PROXY_DEBUG_GENERIC, 4, "Cleaning up Config Tool Handler\n"); + delete handler->config_tool_handler; + handler->config_tool_handler = NULL; + } + + // Clean up Query Tool Handler + if (handler->query_tool_handler) { + proxy_debug(PROXY_DEBUG_GENERIC, 4, "Cleaning up Query Tool Handler\n"); + delete handler->query_tool_handler; + handler->query_tool_handler = NULL; + } + + // Clean up Admin Tool Handler + if (handler->admin_tool_handler) { + proxy_debug(PROXY_DEBUG_GENERIC, 4, "Cleaning up Admin Tool Handler\n"); + delete handler->admin_tool_handler; + handler->admin_tool_handler = NULL; + } + + // Clean up Cache Tool Handler + if (handler->cache_tool_handler) { + proxy_debug(PROXY_DEBUG_GENERIC, 4, "Cleaning up Cache Tool Handler\n"); + delete handler->cache_tool_handler; + handler->cache_tool_handler = NULL; + } + + // Clean up Observe Tool Handler + if (handler->observe_tool_handler) { + proxy_debug(PROXY_DEBUG_GENERIC, 4, "Cleaning up Observe Tool Handler\n"); + delete handler->observe_tool_handler; + handler->observe_tool_handler = NULL; + } } } @@ -166,7 +217,8 @@ void ProxySQL_MCP_Server::start() { return; } - proxy_info("Starting MCP HTTPS server on port %d\n", port); + const char* mode = handler->variables.mcp_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("Starting MCP %s server on port %d\n", mode, port); // Start the server in a dedicated thread if (pthread_create(&thread_id, NULL, mcp_server_thread, ws.get()) != 0) { @@ -174,12 +226,13 @@ void ProxySQL_MCP_Server::start() { return; } - proxy_info("MCP HTTPS server started successfully\n"); + proxy_info("MCP %s server started successfully\n", mode); } void ProxySQL_MCP_Server::stop() { if (ws) { - proxy_info("Stopping MCP HTTPS server\n"); + const char* mode = handler->variables.mcp_use_ssl ? "HTTPS" : "HTTP"; + proxy_info("Stopping MCP %s server\n", mode); ws->stop(); if (thread_id) { @@ -187,6 +240,6 @@ void ProxySQL_MCP_Server::stop() { thread_id = 0; } - proxy_info("MCP HTTPS server stopped\n"); + proxy_info("MCP %s server stopped\n", mode); } } diff --git a/scripts/mcp/configure_mcp.sh b/scripts/mcp/configure_mcp.sh index 3cfcd6a549..f11482326d 100755 --- a/scripts/mcp/configure_mcp.sh +++ b/scripts/mcp/configure_mcp.sh @@ -27,6 +27,7 @@ MYSQL_PASSWORD="${MYSQL_PASSWORD=test123}" # Use = instead of :- to allow empty MYSQL_DATABASE="${TEST_DB_NAME:-testdb}" MCP_PORT="${MCP_PORT:-6071}" MCP_ENABLED="false" +MCP_USE_SSL="true" # Default to true for security # ProxySQL admin configuration PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}" @@ -115,6 +116,7 @@ configure_mcp() { exec_admin_silent "SET mcp-mysql_schema='${MYSQL_DATABASE}';" || { log_error "Failed to set mcp-mysql_schema"; errors=$((errors + 1)); } exec_admin_silent "SET mcp-catalog_path='mcp_catalog.db';" || { log_error "Failed to set mcp-catalog_path"; errors=$((errors + 1)); } exec_admin_silent "SET mcp-port='${MCP_PORT}';" || { log_error "Failed to set mcp-port"; errors=$((errors + 1)); } + exec_admin_silent "SET mcp-use_ssl='${MCP_USE_SSL}';" || { log_error "Failed to set mcp-use_ssl"; errors=$((errors + 1)); } exec_admin_silent "SET mcp-enabled='${enable}';" || { log_error "Failed to set mcp-enabled"; errors=$((errors + 1)); } if [ $errors -gt 0 ]; then @@ -130,6 +132,7 @@ configure_mcp() { echo " mcp-mysql_schema = ${MYSQL_DATABASE}" echo " mcp-catalog_path = mcp_catalog.db (relative to datadir)" echo " mcp-port = ${MCP_PORT}" + echo " mcp-use_ssl = ${MCP_USE_SSL}" echo " mcp-enabled = ${enable}" } @@ -159,9 +162,15 @@ test_mcp_server() { # Wait a moment for server to start sleep 2 + # Determine protocol based on SSL setting + local proto="https" + if [ "${MCP_USE_SSL}" = "false" ]; then + proto="http" + fi + # Test ping endpoint local response - response=$(curl -k -s -X POST "https://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/config" \ + response=$(curl -s -X POST "${proto}://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/config" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"ping","id":1}' 2>/dev/null || echo "") @@ -201,6 +210,14 @@ parse_args() { MCP_PORT="$2" shift 2 ;; + --use-ssl) + MCP_USE_SSL="true" + shift + ;; + --no-ssl) + MCP_USE_SSL="false" + shift + ;; --enable) MCP_ENABLED="true" shift @@ -234,6 +251,8 @@ Options: -p, --password PASS MySQL password (default: test123) -d, --database DB MySQL database (default: testdb) --mcp-port PORT MCP server port (default: 6071) + --use-ssl Enable SSL/TLS for MCP server (HTTPS mode) + --no-ssl Disable SSL/TLS for MCP server (HTTP mode) --enable Enable MCP server --disable Disable MCP server --status Show current MCP configuration @@ -245,15 +264,19 @@ Environment Variables: MYSQL_PASSWORD MySQL password (default: test123) TEST_DB_NAME MySQL database (default: testdb) MCP_PORT MCP server port (default: 6071) + MCP_USE_SSL MCP SSL mode (default: true) PROXYSQL_ADMIN_HOST ProxySQL admin host (default: 127.0.0.1) PROXYSQL_ADMIN_PORT ProxySQL admin port (default: 6032) PROXYSQL_ADMIN_USER ProxySQL admin user (default: admin) PROXYSQL_ADMIN_PASSWORD ProxySQL admin password (default: admin) Examples: - # Configure with test MySQL on port 3307 and enable MCP + # Configure with test MySQL on port 3307 and enable MCP (HTTPS mode) $0 --host 127.0.0.1 --port 3307 --enable + # Configure with HTTP mode (no SSL) for development + $0 --no-ssl --enable + # Disable MCP server $0 --disable @@ -266,6 +289,7 @@ Examples: export MYSQL_USER=myuser export MYSQL_PASSWORD=mypass export TEST_DB_NAME=production + export MCP_USE_SSL=false $0 --enable EOF } @@ -285,7 +309,7 @@ main() { echo "" # Print environment variables if set - if [ -n "${MYSQL_HOST}" ] || [ -n "${MYSQL_PORT}" ] || [ -n "${MYSQL_USER}" ] || [ -n "${MYSQL_PASSWORD}" ] || [ -n "${TEST_DB_NAME}" ] || [ -n "${MCP_PORT}" ]; then + if [ -n "${MYSQL_HOST}" ] || [ -n "${MYSQL_PORT}" ] || [ -n "${MYSQL_USER}" ] || [ -n "${MYSQL_PASSWORD}" ] || [ -n "${TEST_DB_NAME}" ] || [ -n "${MCP_PORT}" ] || [ -n "${MCP_USE_SSL}" ]; then log_info "Environment Variables:" [ -n "${MYSQL_HOST}" ] && echo " MYSQL_HOST=${MYSQL_HOST}" [ -n "${MYSQL_PORT}" ] && echo " MYSQL_PORT=${MYSQL_PORT}" @@ -293,6 +317,7 @@ main() { [ -n "${MYSQL_PASSWORD}" ] && echo " MYSQL_PASSWORD=${MYSQL_PASSWORD}" [ -n "${TEST_DB_NAME}" ] && echo " TEST_DB_NAME=${TEST_DB_NAME}" [ -n "${MCP_PORT}" ] && echo " MCP_PORT=${MCP_PORT}" + [ -n "${MCP_USE_SSL}" ] && echo " MCP_USE_SSL=${MCP_USE_SSL}" echo "" fi @@ -334,12 +359,18 @@ main() { log_info "Configuration complete!" if [ "${MCP_ENABLED}" = "true" ]; then echo "" - echo "MCP server is now enabled and accessible at:" - echo " https://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/config (config endpoint)" - echo " https://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/observe (observe endpoint)" - echo " https://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/query (query endpoint)" - echo " https://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/admin (admin endpoint)" - echo " https://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/cache (cache endpoint)" + if [ "${MCP_USE_SSL}" = "true" ]; then + local proto="https" + echo "MCP server is now enabled (HTTPS mode) and accessible at:" + else + local proto="http" + echo "MCP server is now enabled (HTTP mode - unencrypted) and accessible at:" + fi + echo " ${proto}://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/config (config endpoint)" + echo " ${proto}://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/observe (observe endpoint)" + echo " ${proto}://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/query (query endpoint)" + echo " ${proto}://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/admin (admin endpoint)" + echo " ${proto}://${PROXYSQL_ADMIN_HOST}:${MCP_PORT}/mcp/cache (cache endpoint)" echo "" echo "Run './test_mcp_tools.sh' to test MCP tools" fi diff --git a/src/proxysql.cfg b/src/proxysql.cfg index aada833802..2ca37647b1 100644 --- a/src/proxysql.cfg +++ b/src/proxysql.cfg @@ -8,7 +8,7 @@ uuid="9588556d-fc2d-48ac-9c48-201abab06768" cluster_sync_interfaces=false restart_on_missing_heartbeats=10 #set_thread_name=false -datadir="/var/lib/proxysql" +datadir="/home/rkanji/claude_code/proxysql_vec/proxysql-vec/src/" //execute_on_exit_failure="/path/to/script" //ldap_auth_plugin="../../proxysql_ldap_plugin/MySQL_LDAP_Authentication_plugin.so" #web_interface_plugin="../../proxysql_web_interface_plugin/src/Web_Interface_plugin.so" @@ -61,6 +61,7 @@ mcp_variables= { mcp_enabled=false mcp_port=6071 + mcp_use_ssl=false # Enable/disable SSL/TLS (default: true for security) mcp_config_endpoint_auth="" mcp_observe_endpoint_auth="" mcp_query_endpoint_auth="" From a15be695e0f5342a46680d0bdd35aa9dade4becb Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Sun, 18 Jan 2026 23:19:04 +0500 Subject: [PATCH 03/12] Add GET/OPTIONS handlers for MCP HTTP transport - Add render_GET() returning 405 Method Not Allowed - Add render_OPTIONS() --- include/MCP_Endpoint.h | 37 +++++++++++++++++++++++++++---- lib/MCP_Endpoint.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/include/MCP_Endpoint.h b/include/MCP_Endpoint.h index 7e7bd5f050..2793b5a792 100644 --- a/include/MCP_Endpoint.h +++ b/include/MCP_Endpoint.h @@ -61,12 +61,12 @@ class MCP_JSONRPC_Resource : public httpserver::http_resource { * @brief Create a JSON-RPC 2.0 success response * * @param result The result data to include - * @param id The request ID + * @param id The request ID (can be string, number, or null) * @return JSON string representing the response */ std::string create_jsonrpc_response( const std::string& result, - const std::string& id = "1" + const json& id = nullptr ); /** @@ -74,13 +74,13 @@ class MCP_JSONRPC_Resource : public httpserver::http_resource { * * @param code The error code (JSON-RPC standard or custom) * @param message The error message - * @param id The request ID + * @param id The request ID (can be string, number, or null) * @return JSON string representing the error response */ std::string create_jsonrpc_error( int code, const std::string& message, - const std::string& id = "" + const json& id = nullptr ); /** @@ -127,6 +127,35 @@ class MCP_JSONRPC_Resource : public httpserver::http_resource { */ ~MCP_JSONRPC_Resource(); + /** + * @brief Handle GET requests + * + * Returns HTTP 405 Method Not Allowed for GET requests. + * + * According to the MCP specification (Streamable HTTP transport): + * "The server MUST either return Content-Type: text/event-stream in response to + * this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that + * the server does not offer an SSE stream at this endpoint." + * + * @param req The HTTP request + * @return HTTP 405 response with Allow: POST header + */ + const std::shared_ptr render_GET( + const httpserver::http_request& req + ) override; + + /** + * @brief Handle OPTIONS requests (CORS preflight) + * + * Returns CORS headers for OPTIONS preflight requests. + * + * @param req The HTTP request + * @return HTTP response with CORS headers + */ + const std::shared_ptr render_OPTIONS( + const httpserver::http_request& req + ) override; + /** * @brief Handle POST requests * diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index f5484a94a9..7b39c31cf0 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -98,6 +98,55 @@ bool MCP_JSONRPC_Resource::authenticate_request(const httpserver::http_request& return authenticated; } +const std::shared_ptr MCP_JSONRPC_Resource::render_GET( + const httpserver::http_request& req +) { + std::string req_path = req.get_path(); + proxy_debug(PROXY_DEBUG_GENERIC, 2, "Received MCP GET request on %s - returning 405 Method Not Allowed\n", req_path.c_str()); + + // According to the MCP specification (Streamable HTTP transport): + // "The server MUST either return Content-Type: text/event-stream in response to + // this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that + // the server does not offer an SSE stream at this endpoint." + // + // This server does not currently support SSE streaming, so we return 405. + auto response = std::shared_ptr(new string_response( + "", + http::http_utils::http_method_not_allowed // 405 + )); + response->with_header("Allow", "POST"); // Tell client what IS allowed + + if (handler) { + handler->status_variables.total_requests++; + } + + return response; +} + +const std::shared_ptr MCP_JSONRPC_Resource::render_OPTIONS( + const httpserver::http_request& req +) { + std::string req_path = req.get_path(); + proxy_debug(PROXY_DEBUG_GENERIC, 2, "Received MCP OPTIONS request on %s\n", req_path.c_str()); + + // Handle CORS preflight requests for MCP HTTP transport + // Return 200 OK with appropriate CORS headers + auto response = std::shared_ptr(new string_response( + "", + http::http_utils::http_ok + )); + response->with_header("Content-Type", "application/json"); + response->with_header("Access-Control-Allow-Origin", "*"); + response->with_header("Access-Control-Allow-Methods", "POST, OPTIONS"); + response->with_header("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + if (handler) { + handler->status_variables.total_requests++; + } + + return response; +} + std::string MCP_JSONRPC_Resource::create_jsonrpc_response( const std::string& result, const std::string& id From 4a858521c903cd3cd93e8e661b96793a62259e4d Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Sun, 18 Jan 2026 23:22:08 +0500 Subject: [PATCH 04/12] Fix JSON-RPC ID type Change id parameter from string to json& to support JSON-RPC 2.0 spec (id can be string, number, or null) --- lib/MCP_Endpoint.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index 7b39c31cf0..fd08f71666 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -149,7 +149,7 @@ const std::shared_ptr MCP_JSONRPC_Resource::render_OPTIONS( std::string MCP_JSONRPC_Resource::create_jsonrpc_response( const std::string& result, - const std::string& id + const json& id ) { json j; j["jsonrpc"] = "2.0"; @@ -161,7 +161,7 @@ std::string MCP_JSONRPC_Resource::create_jsonrpc_response( std::string MCP_JSONRPC_Resource::create_jsonrpc_error( int code, const std::string& message, - const std::string& id + const json& id ) { json j; j["jsonrpc"] = "2.0"; @@ -232,13 +232,9 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( } // Get request ID (optional but recommended) - std::string req_id = ""; + json req_id = nullptr; if (req_json.contains("id")) { - if (req_json["id"].is_string()) { - req_id = req_json["id"].get(); - } else if (req_json["id"].is_number()) { - req_id = std::to_string(req_json["id"].get()); - } + req_id = req_json["id"]; } // Get method name From 7564306e181bd5f7ffe428deea6da402c00d6c13 Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Sun, 18 Jan 2026 23:26:43 +0500 Subject: [PATCH 05/12] Handledwq "notifications/initialized" method --- lib/MCP_Endpoint.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index fd08f71666..0a68e0e7ba 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -244,6 +244,20 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( // Handle different methods json result; + // Check if this is a notification + if (method == "notifications/initialized") { + // MCP spec: notifications/initialized is sent by client after initialization + // This is a notification - return HTTP 200 OK with {} body per spec + // See: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports + proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP notification 'notifications/initialized' received on endpoint '%s'\n", endpoint_name.c_str()); + auto response = std::shared_ptr(new string_response( + "{}", + http::http_utils::http_accepted // 202 Accepted + )); + response->with_header("Content-Type", "application/json"); + return response; + } + if (method == "tools/call" || method == "tools/list" || method == "tools/describe") { // Route tool-related methods to the endpoint's tool handler if (!tool_handler) { From f7397f633c4b5bc917d0376d7e3f1adf0d6f61b6 Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Mon, 19 Jan 2026 00:34:40 +0500 Subject: [PATCH 06/12] Fix catalog search to use FTS5 and enhance test suite The catalog_fts FTS5 virtual table was being created but the search() function was using slow LIKE queries instead of FTS5 MATCH operator. Changes to lib/MySQL_Catalog.cpp: - Use FTS5 MATCH with INNER JOIN to catalog_fts when query provided - Add BM25 relevance ranking (ORDER BY bm25(f) ASC) - Significant performance improvement: O(log n) vs O(n) Changes to scripts/mcp/test_catalog.sh: - Add 8 new FTS5-specific tests (CAT013-CAT020): - Multi-term search (AND logic) - Phrase search with quotes - Boolean operators (OR, NOT) - Prefix search with wildcards - Kind and tags filter combinations - Relevance ranking verification - Add SSL/HTTP support with auto-detection - New options: --ssl, --no-ssl, MCP_USE_SSL env var - Fix endpoint path: /query -> /mcp/querywq --- lib/MySQL_Catalog.cpp | 34 +-- scripts/mcp/test_catalog.sh | 409 +++++++++++++++++++++++++++++++++++- src/proxysql.cfg | 2 +- 3 files changed, 425 insertions(+), 20 deletions(-) diff --git a/lib/MySQL_Catalog.cpp b/lib/MySQL_Catalog.cpp index 86f085c607..8794cc3fc2 100644 --- a/lib/MySQL_Catalog.cpp +++ b/lib/MySQL_Catalog.cpp @@ -188,31 +188,35 @@ std::string MySQL_Catalog::search( int limit, int offset ) { + // FTS5 search requires a query + if (query.empty()) { + proxy_error("Catalog search requires a query parameter\n"); + return "[]"; + } + std::ostringstream sql; - sql << "SELECT kind, key, document, tags, links FROM catalog WHERE 1=1"; + char* error = NULL; + int cols = 0, affected = 0; + SQLite3_result* resultset = NULL; + + // FTS5 search with BM25 ranking + sql << "SELECT c.kind, c.key, c.document, c.tags, c.links " + << "FROM catalog c " + << "INNER JOIN catalog_fts f ON c.id = f.rowid " + << "WHERE catalog_fts MATCH '" << query << "'"; // Add kind filter if (!kind.empty()) { - sql << " AND kind = '" << kind << "'"; + sql << " AND c.kind = '" << kind << "'"; } // Add tags filter if (!tags.empty()) { - sql << " AND tags LIKE '%" << tags << "%'"; + sql << " AND c.tags LIKE '%" << tags << "%'"; } - // Add search query - if (!query.empty()) { - sql << " AND (key LIKE '%" << query << "%' " - << "OR document LIKE '%" << query << "%' " - << "OR tags LIKE '%" << query << "%')"; - } - - sql << " ORDER BY updated_at DESC LIMIT " << limit << " OFFSET " << offset; - - char* error = NULL; - int cols = 0, affected = 0; - SQLite3_result* resultset = NULL; + // Order by relevance (BM25) and recency + sql << " ORDER BY bm25(f) ASC, c.updated_at DESC LIMIT " << limit << " OFFSET " << offset; db->execute_statement(sql.str().c_str(), &error, &cols, &affected, &resultset); if (error) { diff --git a/scripts/mcp/test_catalog.sh b/scripts/mcp/test_catalog.sh index 0f983cbf98..a90999b59c 100755 --- a/scripts/mcp/test_catalog.sh +++ b/scripts/mcp/test_catalog.sh @@ -7,6 +7,8 @@ # # Options: # -v, --verbose Show verbose output +# -s, --ssl Use HTTPS (SSL/TLS) for MCP connection (default: auto-detect) +# --no-ssl Use HTTP (no SSL) for MCP connection # -h, --help Show help # @@ -15,10 +17,11 @@ set -e # Configuration MCP_HOST="${MCP_HOST:-127.0.0.1}" MCP_PORT="${MCP_PORT:-6071}" -MCP_URL="https://${MCP_HOST}:${MCP_PORT}/query" +MCP_USE_SSL="${MCP_USE_SSL:-auto}" # Test options VERBOSE=false +USE_SSL="" # Colors RED='\033[0;31m' @@ -39,14 +42,50 @@ log_test() { echo -e "${BLUE}[TEST]${NC} $1" } +# Determine URL and curl options based on SSL setting +setup_connection() { + local ssl_mode="${MCP_USE_SSL}" + + # Auto-detect: try HTTPS first, fall back to HTTP + if [ "$ssl_mode" = "auto" ]; then + # Try HTTPS first + if curl -k -s -m 2 "https://${MCP_HOST}:${MCP_PORT}" >/dev/null 2>&1; then + USE_SSL=true + MCP_URL="https://${MCP_HOST}:${MCP_PORT}/mcp/query" + log_info "Auto-detected: Using HTTPS (SSL)" + elif curl -s -m 2 "http://${MCP_HOST}:${MCP_PORT}" >/dev/null 2>&1; then + USE_SSL=false + MCP_URL="http://${MCP_HOST}:${MCP_PORT}/mcp/query" + log_info "Auto-detected: Using HTTP (no SSL)" + else + # Default to HTTPS if can't detect + USE_SSL=true + MCP_URL="https://${MCP_HOST}:${MCP_PORT}/mcp/query" + log_info "Auto-detect failed, defaulting to HTTPS" + fi + elif [ "$ssl_mode" = "true" ] || [ "$ssl_mode" = "1" ]; then + USE_SSL=true + MCP_URL="https://${MCP_HOST}:${MCP_PORT}/mcp/query" + else + USE_SSL=false + MCP_URL="http://${MCP_HOST}:${MCP_PORT}/mcp/query" + fi +} + # Execute MCP request mcp_request() { local payload="$1" local response - response=$(curl -k -s -X POST "${MCP_URL}" \ - -H "Content-Type: application/json" \ - -d "${payload}" 2>/dev/null) + if [ "$USE_SSL" = "true" ]; then + response=$(curl -k -s -X POST "${MCP_URL}" \ + -H "Content-Type: application/json" \ + -d "${payload}" 2>/dev/null) + else + response=$(curl -s -X POST "${MCP_URL}" \ + -H "Content-Type: application/json" \ + -d "${payload}" 2>/dev/null) + fi echo "${response}" } @@ -86,6 +125,9 @@ run_catalog_tests() { echo "Catalog (LLM Memory) Test Suite" echo "======================================" echo "" + echo "MCP Server: ${MCP_URL}" + echo "SSL Mode: ${USE_SSL:-detecting...}" + echo "" echo "Testing catalog operations for LLM memory persistence" echo "" @@ -360,6 +402,330 @@ run_catalog_tests() { failed=$((failed + 1)) fi + echo "" + echo "======================================" + echo "FTS5 Enhanced Tests" + echo "======================================" + + # Setup: Add multiple entries for FTS5 testing + log_test "Setup: Adding test data for FTS5 tests" + + local setup_payload1='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_upsert", + "arguments": { + "kind": "table", + "key": "fts_test.users", + "document": "{\"table\": \"users\", \"description\": \"User accounts table with authentication data\", \"columns\": [\"id\", \"username\", \"email\", \"password_hash\"]}", + "tags": "authentication,users,security", + "links": "" + } + }, + "id": 1001 +}' + + local setup_payload2='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_upsert", + "arguments": { + "kind": "table", + "key": "fts_test.products", + "document": "{\"table\": \"products\", \"description\": \"Product catalog with pricing and inventory\", \"columns\": [\"id\", \"name\", \"price\", \"stock\"]}", + "tags": "ecommerce,products,catalog", + "links": "" + } + }, + "id": 1002 +}' + + local setup_payload3='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_upsert", + "arguments": { + "kind": "domain", + "key": "user_authentication", + "document": "{\"description\": \"User authentication and authorization domain\", \"flows\": [\"login\", \"logout\", \"password_reset\"], \"policies\": [\"MFA\", \"password_complexity\"]}", + "tags": "security,authentication", + "links": "" + } + }, + "id": 1003 +}' + + local setup_payload4='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_upsert", + "arguments": { + "kind": "domain", + "key": "product_management", + "document": "{\"description\": \"Product inventory and catalog management\", \"features\": [\"bulk_import\", \"pricing_rules\", \"stock_alerts\"]}", + "tags": "ecommerce,inventory", + "links": "" + } + }, + "id": 1004 +}' + + # Run setup + mcp_request "${setup_payload1}" > /dev/null + mcp_request "${setup_payload2}" > /dev/null + mcp_request "${setup_payload3}" > /dev/null + mcp_request "${setup_payload4}" > /dev/null + + log_info "Setup complete: Added 4 test entries for FTS5 testing" + + # Test CAT013: FTS5 multi-term search (AND logic) + local payload13='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "authentication user", + "limit": 10 + } + }, + "id": 13 +}' + + if test_catalog "CAT013" "FTS5 multi-term search (AND)" "${payload13}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT014: FTS5 phrase search with quotes + local payload14='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "\"user authentication\"", + "limit": 10 + } + }, + "id": 14 +}' + + if test_catalog "CAT014" "FTS5 phrase search" "${payload14}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT015: FTS5 OR search + local payload15='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "authentication OR inventory", + "limit": 10 + } + }, + "id": 15 +}' + + if test_catalog "CAT015" "FTS5 OR search" "${payload15}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT016: FTS5 NOT search + local payload16='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "authentication NOT domain", + "limit": 10 + } + }, + "id": 16 +}' + + if test_catalog "CAT016" "FTS5 NOT search" "${payload16}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT017: FTS5 search with kind filter + local payload17='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "user", + "kind": "table", + "limit": 10 + } + }, + "id": 17 +}' + + if test_catalog "CAT017" "FTS5 search with kind filter" "${payload17}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT018: FTS5 prefix search (ends with *) + local payload18='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "auth*", + "limit": 10 + } + }, + "id": 18 +}' + + if test_catalog "CAT018" "FTS5 prefix search" "${payload18}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT019: FTS5 relevance ranking (search for common term, check results exist) + local payload19='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "table", + "limit": 5 + } + }, + "id": 19 +}' + + if test_catalog "CAT019" "FTS5 relevance ranking" "${payload19}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT020: FTS5 search with tags filter + local payload20='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "user", + "tags": "security", + "limit": 10 + } + }, + "id": 20 +}' + + if test_catalog "CAT020" "FTS5 search with tags filter" "${payload20}" '"results"'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Test CAT021: Empty query should return empty results (FTS5 requires query) + local payload21='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_search", + "arguments": { + "query": "", + "limit": 10 + } + }, + "id": 21 +}' + + if test_catalog "CAT021" "Empty query returns empty array" "${payload21}" '"results"[[:space:]]*:[[:space:]]*\[\]'; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + + # Cleanup: Remove FTS5 test entries + log_test "Cleanup: Removing FTS5 test entries" + + local cleanup_payload1='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_delete", + "arguments": { + "kind": "table", + "key": "fts_test.users" + } + }, + "id": 2001 +}' + + local cleanup_payload2='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_delete", + "arguments": { + "kind": "table", + "key": "fts_test.products" + } + }, + "id": 2002 +}' + + local cleanup_payload3='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_delete", + "arguments": { + "kind": "domain", + "key": "user_authentication" + } + }, + "id": 2003 +}' + + local cleanup_payload4='{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "catalog_delete", + "arguments": { + "kind": "domain", + "key": "product_management" + } + }, + "id": 2004 +}' + + mcp_request "${cleanup_payload1}" > /dev/null + mcp_request "${cleanup_payload2}" > /dev/null + mcp_request "${cleanup_payload3}" > /dev/null + mcp_request "${cleanup_payload4}" > /dev/null + + log_info "Cleanup complete: Removed FTS5 test entries" + # Print summary echo "" echo "======================================" @@ -387,6 +753,14 @@ parse_args() { VERBOSE=true shift ;; + -s|--ssl) + MCP_USE_SSL=true + shift + ;; + --no-ssl) + MCP_USE_SSL=false + shift + ;; -h|--help) cat < Date: Mon, 19 Jan 2026 13:33:40 +0500 Subject: [PATCH 07/12] Fix critical double-free bug, SQL injection vulnerability, and hardcoded path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses three issues identified by code review: 1. CRITICAL: Fix double-free bug in MCP server restart logic - Remove manual handler deletions in Admin_FlushVariables.cpp - ProxySQL_MCP_Server destructor already properly cleans up all handlers - Previously caused crashes when toggling SSL mode or changing port - Simplified restart: delete server (destructor cleanup) → create new server - Verified with 10+ rapid SSL toggles without crashes 2. HIGH: Fix SQL injection vulnerability in catalog search - Rewrite MySQL_Catalog::search() to use prepared statements - Use parameter binding (proxy_sqlite3_bind_text/bind_int) for user input - Escape single quotes in FTS5 MATCH clause (doesn't support parameters) - Tested against multiple injection attempts (single quote, backslash, comments, UNION SELECT, kind/tags parameter injection) - All 21 catalog tests still pass with new implementation 3. MEDIUM: Fix hardcoded user-specific path in config - Revert datadir from user-specific absolute path to /var/lib/proxysql - Ensures portability across different environments Testing: - SSL toggle: 7 tests passed (HTTP↔HTTPS, port changes, stress test) - SQL injection: 10 tests passed (various injection attempts blocked) - Catalog functionality: 21 tests passed (FTS5, BM25 ranking, etc.) - Total: 38 tests passed, 0 failed Fixes issues identified in GitHub PR #16 review. --- lib/Admin_FlushVariables.cpp | 84 ++++++------------------------- lib/MySQL_Catalog.cpp | 96 +++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 100 deletions(-) diff --git a/lib/Admin_FlushVariables.cpp b/lib/Admin_FlushVariables.cpp index 568cbb92bc..772f63a0b8 100644 --- a/lib/Admin_FlushVariables.cpp +++ b/lib/Admin_FlushVariables.cpp @@ -1612,83 +1612,27 @@ void ProxySQL_Admin::flush_mcp_variables___runtime_to_database(SQLite3DB* db, bo } } else { // Server is already running - need to stop, delete server, and recreate everything - proxy_info("MCP: Server already running, reinitializing MySQL tool handler\n"); + proxy_info("MCP: Server already running, reinitializing\n"); - // 1. Delete Query_Tool_Handler first (server destructor doesn't delete this) - if (GloMCPH->query_tool_handler) { - proxy_info("MCP: Deleting old Query Tool Handler\n"); - delete GloMCPH->query_tool_handler; - GloMCPH->query_tool_handler = NULL; - } - - // 2. Stop and delete the server (server destructor also deletes MySQL_Tool_Handler) + // Delete the old server - its destructor will clean up all handlers + // (mysql_tool_handler, config_tool_handler, query_tool_handler, + // admin_tool_handler, cache_tool_handler, observe_tool_handler) proxy_info("MCP: Stopping and deleting old server\n"); delete GloMCPH->mcp_server; GloMCPH->mcp_server = NULL; - // Note: mysql_tool_handler is already deleted by server destructor and set to NULL + // All handlers are now deleted and set to NULL by the destructor proxy_info("MCP: Old server deleted\n"); - // 3. Delete other handlers that were created by old server - // The server destructor doesn't clean these up, so we need to do it manually - if (GloMCPH->config_tool_handler) { - delete GloMCPH->config_tool_handler; - GloMCPH->config_tool_handler = NULL; - } - if (GloMCPH->admin_tool_handler) { - delete GloMCPH->admin_tool_handler; - GloMCPH->admin_tool_handler = NULL; - } - if (GloMCPH->cache_tool_handler) { - delete GloMCPH->cache_tool_handler; - GloMCPH->cache_tool_handler = NULL; - } - if (GloMCPH->observe_tool_handler) { - delete GloMCPH->observe_tool_handler; - GloMCPH->observe_tool_handler = NULL; - } - - // 4. Create new MySQL_Tool_Handler with current configuration - proxy_info("MCP: Creating new MySQL Tool Handler with updated configuration\n"); - GloMCPH->mysql_tool_handler = new MySQL_Tool_Handler( - GloMCPH->variables.mcp_mysql_hosts ? GloMCPH->variables.mcp_mysql_hosts : "", - GloMCPH->variables.mcp_mysql_ports ? GloMCPH->variables.mcp_mysql_ports : "", - GloMCPH->variables.mcp_mysql_user ? GloMCPH->variables.mcp_mysql_user : "", - GloMCPH->variables.mcp_mysql_password ? GloMCPH->variables.mcp_mysql_password : "", - GloMCPH->variables.mcp_mysql_schema ? GloMCPH->variables.mcp_mysql_schema : "", - GloMCPH->variables.mcp_catalog_path ? GloMCPH->variables.mcp_catalog_path : "" - ); - - if (GloMCPH->mysql_tool_handler->init() != 0) { - proxy_error("MCP: Failed to initialize new MySQL Tool Handler\n"); - delete GloMCPH->mysql_tool_handler; - GloMCPH->mysql_tool_handler = NULL; - } else { - proxy_info("MCP: New MySQL Tool Handler initialized successfully\n"); - - // 5. Create new Query_Tool_Handler that wraps the new MySQL_Tool_Handler - GloMCPH->query_tool_handler = new Query_Tool_Handler(GloMCPH->mysql_tool_handler); - if (GloMCPH->query_tool_handler->init() != 0) { - proxy_error("MCP: Failed to initialize new Query Tool Handler\n"); - delete GloMCPH->query_tool_handler; - GloMCPH->query_tool_handler = NULL; - } else { - proxy_info("MCP: New Query Tool Handler initialized successfully\n"); - } - } - - // 6. Create and start new server (which will recreate all handlers including config/admin/cache/observe) - if (GloMCPH->mysql_tool_handler && GloMCPH->query_tool_handler) { - proxy_info("MCP: Creating and starting new server\n"); - int port = GloMCPH->variables.mcp_port; - GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); - if (GloMCPH->mcp_server) { - GloMCPH->mcp_server->start(); - proxy_info("MCP: New server created and started successfully\n"); - } else { - proxy_error("MCP: Failed to create new server instance\n"); - } + // Create and start new server with current configuration + // The server constructor will recreate all handlers with updated settings + proxy_info("MCP: Creating and starting new server\n"); + int port = GloMCPH->variables.mcp_port; + GloMCPH->mcp_server = new ProxySQL_MCP_Server(port, GloMCPH); + if (GloMCPH->mcp_server) { + GloMCPH->mcp_server->start(); + proxy_info("MCP: New server created and started successfully\n"); } else { - proxy_error("MCP: Server not created due to handler initialization failure\n"); + proxy_error("MCP: Failed to create new server instance\n"); } } } else { diff --git a/lib/MySQL_Catalog.cpp b/lib/MySQL_Catalog.cpp index 8794cc3fc2..6ee68cfa82 100644 --- a/lib/MySQL_Catalog.cpp +++ b/lib/MySQL_Catalog.cpp @@ -194,57 +194,93 @@ std::string MySQL_Catalog::search( return "[]"; } - std::ostringstream sql; - char* error = NULL; - int cols = 0, affected = 0; - SQLite3_result* resultset = NULL; + // Helper lambda to escape single quotes for SQLite SQL literals + auto escape_sql = [](const std::string& str) -> std::string { + std::string result; + result.reserve(str.length() * 2); // Reserve space for potential escaping + for (char c : str) { + if (c == '\'') { + result += '\''; // Escape single quote by doubling it + } + result += c; + } + return result; + }; - // FTS5 search with BM25 ranking + // Escape query for use in FTS5 MATCH (MATCH doesn't support parameter binding) + std::string escaped_query = escape_sql(query); + + // Build SQL query with placeholders for parameters + std::ostringstream sql; sql << "SELECT c.kind, c.key, c.document, c.tags, c.links " << "FROM catalog c " << "INNER JOIN catalog_fts f ON c.id = f.rowid " - << "WHERE catalog_fts MATCH '" << query << "'"; + << "WHERE catalog_fts MATCH '" << escaped_query << "'"; - // Add kind filter + int param_count = 1; // Track parameter binding position + + // Add kind filter with parameter placeholder if (!kind.empty()) { - sql << " AND c.kind = '" << kind << "'"; + sql << " AND c.kind = ?"; } - // Add tags filter + // Add tags filter with parameter placeholder if (!tags.empty()) { - sql << " AND c.tags LIKE '%" << tags << "%'"; + sql << " AND c.tags LIKE ?"; } // Order by relevance (BM25) and recency - sql << " ORDER BY bm25(f) ASC, c.updated_at DESC LIMIT " << limit << " OFFSET " << offset; + sql << " ORDER BY bm25(f) ASC, c.updated_at DESC LIMIT ? OFFSET ?"; - db->execute_statement(sql.str().c_str(), &error, &cols, &affected, &resultset); - if (error) { - proxy_error("Catalog search error: %s\n", error); + // Prepare the statement + sqlite3_stmt* stmt = NULL; + int rc = db->prepare_v2(sql.str().c_str(), &stmt); + if (rc != SQLITE_OK) { + proxy_error("Catalog search: Failed to prepare statement: %d\n", rc); return "[]"; } - // Build JSON result + // Bind parameters + param_count = 1; + if (!kind.empty()) { + (*proxy_sqlite3_bind_text)(stmt, param_count++, kind.c_str(), -1, SQLITE_TRANSIENT); + } + if (!tags.empty()) { + // Add wildcards for LIKE search + std::string tags_pattern = "%" + tags + "%"; + (*proxy_sqlite3_bind_text)(stmt, param_count++, tags_pattern.c_str(), -1, SQLITE_TRANSIENT); + } + (*proxy_sqlite3_bind_int)(stmt, param_count++, limit); + (*proxy_sqlite3_bind_int)(stmt, param_count, offset); + + // Execute query and build JSON result std::ostringstream json; json << "["; bool first = true; - if (resultset) { - for (std::vector::iterator it = resultset->rows.begin(); - it != resultset->rows.end(); ++it) { - SQLite3_row* row = *it; - if (!first) json << ","; - first = false; + while ((rc = (*proxy_sqlite3_step)(stmt)) == SQLITE_ROW) { + if (!first) json << ","; + first = false; + + const char* kind_val = (const char*)(*proxy_sqlite3_column_text)(stmt, 0); + const char* key_val = (const char*)(*proxy_sqlite3_column_text)(stmt, 1); + const char* doc_val = (const char*)(*proxy_sqlite3_column_text)(stmt, 2); + const char* tags_val = (const char*)(*proxy_sqlite3_column_text)(stmt, 3); + const char* links_val = (const char*)(*proxy_sqlite3_column_text)(stmt, 4); + + json << "{" + << "\"kind\":\"" << (kind_val ? kind_val : "") << "\"," + << "\"key\":\"" << (key_val ? key_val : "") << "\"," + << "\"document\":" << (doc_val ? doc_val : "null") << "," + << "\"tags\":\"" << (tags_val ? tags_val : "") << "\"," + << "\"links\":\"" << (links_val ? links_val : "") << "\"" + << "}"; + } - json << "{" - << "\"kind\":\"" << (row->fields[0] ? row->fields[0] : "") << "\"," - << "\"key\":\"" << (row->fields[1] ? row->fields[1] : "") << "\"," - << "\"document\":" << (row->fields[2] ? row->fields[2] : "null") << "," - << "\"tags\":\"" << (row->fields[3] ? row->fields[3] : "") << "\"," - << "\"links\":\"" << (row->fields[4] ? row->fields[4] : "") << "\"" - << "}"; - } - delete resultset; + (*proxy_sqlite3_finalize)(stmt); + + if (rc != SQLITE_DONE && rc != SQLITE_ROW) { + proxy_error("Catalog search: Error executing query: %d\n", rc); } json << "]"; From bf429f0a52aa7ddb16f4a72f625c16ab6969bc6e Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Tue, 20 Jan 2026 11:49:34 +0500 Subject: [PATCH 08/12] Fixed multiple issues --- lib/MCP_Endpoint.cpp | 92 +++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index 293251b1a6..85c66197c9 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -151,10 +151,13 @@ std::string MCP_JSONRPC_Resource::create_jsonrpc_response( const std::string& result, const json& id ) { - json j; + nlohmann::ordered_json j; // Use ordered_json to preserve field order j["jsonrpc"] = "2.0"; + // Only include id if it's not null (per JSON-RPC 2.0 and MCP spec) + if (!id.is_null()) { + j["id"] = id; + } j["result"] = json::parse(result); - j["id"] = id; return j.dump(); } @@ -163,13 +166,16 @@ std::string MCP_JSONRPC_Resource::create_jsonrpc_error( const std::string& message, const json& id ) { - json j; + nlohmann::ordered_json j; // Use ordered_json to preserve field order j["jsonrpc"] = "2.0"; json error; error["code"] = code; error["message"] = message; j["error"] = error; - j["id"] = id; + // Only include id if it's not null (per JSON-RPC 2.0 and MCP spec) + if (!id.is_null()) { + j["id"] = id; + } return j.dump(); } @@ -197,13 +203,20 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( handler->status_variables.failed_requests++; } auto response = std::shared_ptr(new string_response( - create_jsonrpc_error(-32700, "Parse error", ""), + create_jsonrpc_error(-32700, "Parse error", nullptr), http::http_utils::http_bad_request )); response->with_header("Content-Type", "application/json"); return response; } + // Extract request ID immediately after parsing (JSON-RPC 2.0 spec) + // This must be done BEFORE validation so we can include the ID in error responses + json req_id = nullptr; + if (req_json.contains("id")) { + req_id = req_json["id"]; + } + // Validate JSON-RPC 2.0 basic structure if (!req_json.contains("jsonrpc") || req_json["jsonrpc"] != "2.0") { proxy_error("MCP request on %s: Missing or invalid jsonrpc version\n", req_path.c_str()); @@ -211,7 +224,7 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( handler->status_variables.failed_requests++; } auto response = std::shared_ptr(new string_response( - create_jsonrpc_error(-32600, "Invalid Request", ""), + create_jsonrpc_error(-32600, "Invalid Request", req_id), http::http_utils::http_bad_request )); response->with_header("Content-Type", "application/json"); @@ -223,20 +236,16 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( if (handler) { handler->status_variables.failed_requests++; } + // Use -32601 "Method not found" for compatibility with MCP clients + // (even though -32600 "Invalid Request" is technically correct per JSON-RPC spec) auto response = std::shared_ptr(new string_response( - create_jsonrpc_error(-32600, "Invalid Request", ""), + create_jsonrpc_error(-32601, "Method not found", req_id), http::http_utils::http_bad_request )); response->with_header("Content-Type", "application/json"); return response; } - // Get request ID (optional but recommended) - json req_id = nullptr; - if (req_json.contains("id")) { - req_id = req_json["id"]; - } - // Get method name std::string method = req_json["method"].get(); proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP method '%s' requested on endpoint '%s'\n", method.c_str(), endpoint_name.c_str()); @@ -244,20 +253,6 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( // Handle different methods json result; - // Check if this is a notification - if (method == "notifications/initialized") { - // MCP spec: notifications/initialized is sent by client after initialization - // This is a notification - return HTTP 200 OK with {} body per spec - // See: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports - proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP notification 'notifications/initialized' received on endpoint '%s'\n", endpoint_name.c_str()); - auto response = std::shared_ptr(new string_response( - "{}", - http::http_utils::http_accepted // 202 Accepted - )); - response->with_header("Content-Type", "application/json"); - return response; - } - if (method == "tools/call" || method == "tools/list" || method == "tools/describe") { // Route tool-related methods to the endpoint's tool handler if (!tool_handler) { @@ -270,7 +265,7 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( http::http_utils::http_internal_server_error )); response->with_header("Content-Type", "application/json"); - return response; + return response; } // Route to appropriate tool handler method @@ -281,24 +276,32 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( } else if (method == "tools/call") { result = handle_tools_call(req_json); } - } else if (method == "initialize" || method == "ping") { + } else if (method == "initialize") { // Handle MCP protocol methods - if (method == "initialize") { - result["protocolVersion"] = "2024-11-05"; - result["capabilities"] = json::object(); - result["serverInfo"] = { - {"name", "proxysql-mcp-mysql-tools"}, - {"version", MCP_THREAD_VERSION} - }; - } else if (method == "ping") { - result["status"] = "ok"; - } + result["protocolVersion"] = "2024-11-05"; + result["capabilities"]["tools"] = json::object(); // Explicitly declare tools support + result["serverInfo"] = { + {"name", "proxysql-mcp-mcp-mysql-tools"}, + {"version", MCP_THREAD_VERSION} + }; + } else if (method == "ping") { + result["status"] = "ok"; + } else if (method == "initialized") { + // MCP notification: "initialized" - Return HTTP 200 with empty body + proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP notification 'initialized' received on endpoint '%s'\n", endpoint_name.c_str()); + auto response = std::shared_ptr(new string_response( + "", + http::http_utils::http_ok + )); + response->with_header("Content-Type", "application/json"); + return response; } else { // Unknown method proxy_info("MCP: Unknown method '%s' on endpoint '%s'\n", method.c_str(), endpoint_name.c_str()); + // Return HTTP 200 OK with JSON-RPC error (not HTTP 404) for compatibility with MCP clients auto response = std::shared_ptr(new string_response( create_jsonrpc_error(-32601, "Method not found", req_id), - http::http_utils::http_not_found + http::http_utils::http_ok )); response->with_header("Content-Type", "application/json"); return response; @@ -327,8 +330,9 @@ const std::shared_ptr MCP_JSONRPC_Resource::render_POST( if (handler) { handler->status_variables.failed_requests++; } + // Use nullptr for ID since we haven't parsed JSON yet (JSON-RPC 2.0 spec) auto response = std::shared_ptr(new string_response( - create_jsonrpc_error(-32600, "Invalid Request: Content-Type must be application/json", ""), + create_jsonrpc_error(-32600, "Invalid Request: Content-Type must be application/json", nullptr), http::http_utils::http_unsupported_media_type )); response->with_header("Content-Type", "application/json"); @@ -341,8 +345,9 @@ const std::shared_ptr MCP_JSONRPC_Resource::render_POST( if (handler) { handler->status_variables.failed_requests++; } + // Use nullptr for ID since we haven't parsed JSON yet (JSON-RPC 2.0 spec) auto response = std::shared_ptr(new string_response( - create_jsonrpc_error(-32001, "Unauthorized", ""), + create_jsonrpc_error(-32001, "Unauthorized", nullptr), http::http_utils::http_unauthorized )); response->with_header("Content-Type", "application/json"); @@ -435,6 +440,7 @@ json MCP_JSONRPC_Resource::handle_tools_call(const json& req_json) { } mcp_result["content"] = json::array({text_content}); - mcp_result["isError"] = false; + // Note: Per MCP spec, only include isError when true (error case) + // For success responses, omit the isError field entirely return mcp_result; } From 155a77f9699d541703f4a55bdf68cf80d51ace0b Mon Sep 17 00:00:00 2001 From: Wazir Ahmed Date: Tue, 20 Jan 2026 11:05:13 +0530 Subject: [PATCH 09/12] MCP: Bump protocolVersion to 2025-06-18 - Version 2024-11-05 only supports HTTP_SSE as transport. - ProxySQL's MCP implemenation aligns more with the StreamableHTTP transport specified in version 2025-06-18. - Support for SSE in StreamableHTTP transport is optional. Signed-off-by: Wazir Ahmed --- lib/MCP_Endpoint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index 85c66197c9..e1470c9f2c 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -278,7 +278,7 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( } } else if (method == "initialize") { // Handle MCP protocol methods - result["protocolVersion"] = "2024-11-05"; + result["protocolVersion"] = "2025-06-18"; result["capabilities"]["tools"] = json::object(); // Explicitly declare tools support result["serverInfo"] = { {"name", "proxysql-mcp-mcp-mysql-tools"}, From 2f38def403a4ab194658594dfc9cf120ead2be59 Mon Sep 17 00:00:00 2001 From: Wazir Ahmed Date: Tue, 20 Jan 2026 11:36:22 +0530 Subject: [PATCH 10/12] MCP: Handle client notifications properly - Fix incorrect method name for notification - Handle all notification messages in a generic way - Respond with HTTP 202 Accepted (no response body) Signed-off-by: Wazir Ahmed --- lib/MCP_Endpoint.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index e1470c9f2c..f19d8ab70b 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -286,15 +286,16 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( }; } else if (method == "ping") { result["status"] = "ok"; - } else if (method == "initialized") { - // MCP notification: "initialized" - Return HTTP 200 with empty body - proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP notification 'initialized' received on endpoint '%s'\n", endpoint_name.c_str()); - auto response = std::shared_ptr(new string_response( - "", - http::http_utils::http_ok - )); - response->with_header("Content-Type", "application/json"); - return response; + } else if (method.compare(0, strlen("notifications/"), "notifications/") == 0) { + // Handle notifications sent by the client + // notifications/initialized + // - https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization + // notifications/cancelled + // - https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#cancellation-flow + + proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP notification '%s' received on endpoint '%s'\n", method.c_str(), endpoint_name.c_str()); + // simple acknowledgement with HTTP 202 Accepted (no response body) + return std::shared_ptr(new string_response("",http::http_utils::http_accepted)); } else { // Unknown method proxy_info("MCP: Unknown method '%s' on endpoint '%s'\n", method.c_str(), endpoint_name.c_str()); From 68a41d6db8152f634f3ed81a728128bdf865d37f Mon Sep 17 00:00:00 2001 From: Wazir Ahmed Date: Tue, 20 Jan 2026 13:04:06 +0530 Subject: [PATCH 11/12] MCP: Add handler for prompts and resources Issue ----- - ProxySQL only supports the `tools` feature of the MCP protocol and does not support features such as `prompts` and `resources`. - Although ProxySQL expresses this in its `initialize` response, (server capabilities list contains only the `tools` object), clients such as Warp Terminal ignore it and continue to send requests for methods such `prompts/list` and `resources/list`. - Any response other than `HTTP 200 OK` is treated as an error and client fails to initialize. Fix --- - Handle prompt and resource list requests by returning an empty array. Signed-off-by: Wazir Ahmed --- include/MCP_Endpoint.h | 18 ++++++++++++++++++ lib/MCP_Endpoint.cpp | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/include/MCP_Endpoint.h b/include/MCP_Endpoint.h index 2793b5a792..1c0c54b432 100644 --- a/include/MCP_Endpoint.h +++ b/include/MCP_Endpoint.h @@ -112,6 +112,24 @@ class MCP_JSONRPC_Resource : public httpserver::http_resource { */ json handle_tools_call(const json& req_json); + /** + * @brief Handle prompts/list method + * + * Returns an empty prompts array since ProxySQL doesn't support prompts. + * + * @return JSON with empty prompts array + */ + json handle_prompts_list(); + + /** + * @brief Handle resources/list method + * + * Returns an empty resources array since ProxySQL doesn't support resources. + * + * @return JSON with empty resources array + */ + json handle_resources_list(); + public: /** * @brief Constructor for MCP_JSONRPC_Resource diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index f19d8ab70b..dd61a2becd 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -276,6 +276,10 @@ std::shared_ptr MCP_JSONRPC_Resource::handle_jsonrpc_request( } else if (method == "tools/call") { result = handle_tools_call(req_json); } + } else if (method == "prompts/list") { + result = handle_prompts_list(); + } else if (method == "resources/list") { + result = handle_resources_list(); } else if (method == "initialize") { // Handle MCP protocol methods result["protocolVersion"] = "2025-06-18"; @@ -445,3 +449,21 @@ json MCP_JSONRPC_Resource::handle_tools_call(const json& req_json) { // For success responses, omit the isError field entirely return mcp_result; } + +// Helper method to handle prompts/list +json MCP_JSONRPC_Resource::handle_prompts_list() { + proxy_debug(PROXY_DEBUG_GENERIC, 3, "MCP: prompts/list called\n"); + // Returns an empty prompts array since ProxySQL doesn't support prompts + json result; + result["prompts"] = json::array(); + return result; +} + +// Helper method to handle resources/list +json MCP_JSONRPC_Resource::handle_resources_list() { + proxy_debug(PROXY_DEBUG_GENERIC, 3, "MCP: resources/list called\n"); + // Returns an empty resources array since ProxySQL doesn't support resources + json result; + result["resources"] = json::array(); + return result; +} From e450f1b30fb47ec1b9662f70f488fd3d4c39bc76 Mon Sep 17 00:00:00 2001 From: Wazir Ahmed Date: Tue, 20 Jan 2026 13:43:23 +0530 Subject: [PATCH 12/12] MCP: Handle DELETE method - Respond with 405 Method Not Allowed, when clients send DELETE request for session termination. Signed-off-by: Wazir Ahmed --- include/MCP_Endpoint.h | 18 +++++++++++++++++- lib/MCP_Endpoint.cpp | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/include/MCP_Endpoint.h b/include/MCP_Endpoint.h index 1c0c54b432..b1bd989486 100644 --- a/include/MCP_Endpoint.h +++ b/include/MCP_Endpoint.h @@ -150,7 +150,7 @@ class MCP_JSONRPC_Resource : public httpserver::http_resource { * * Returns HTTP 405 Method Not Allowed for GET requests. * - * According to the MCP specification (Streamable HTTP transport): + * According to the MCP specification 2025-06-18 (Streamable HTTP transport): * "The server MUST either return Content-Type: text/event-stream in response to * this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that * the server does not offer an SSE stream at this endpoint." @@ -174,6 +174,22 @@ class MCP_JSONRPC_Resource : public httpserver::http_resource { const httpserver::http_request& req ) override; + /** + * @brief Handle DELETE requests + * + * Returns HTTP 405 Method Not Allowed for DELETE requests. + * + * According to the MCP specification 2025-06-18 (Streamable HTTP transport): + * "The server MAY respond to this request with HTTP 405 Method Not Allowed, + * indicating that the server does not allow clients to terminate sessions." + * + * @param req The HTTP request + * @return HTTP 405 response with Allow header + */ + const std::shared_ptr render_DELETE( + const httpserver::http_request& req + ) override; + /** * @brief Handle POST requests * diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index dd61a2becd..983978b84a 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -147,6 +147,27 @@ const std::shared_ptr MCP_JSONRPC_Resource::render_OPTIONS( return response; } +const std::shared_ptr MCP_JSONRPC_Resource::render_DELETE( + const httpserver::http_request& req +) { + std::string req_path = req.get_path(); + proxy_debug(PROXY_DEBUG_GENERIC, 2, "Received MCP DELETE request on %s - returning 405 Method Not Allowed\n", req_path.c_str()); + + // ProxySQL doesn't support session termination + // Return 405 Method Not Allowed with Allow header indicating supported methods + auto response = std::shared_ptr(new string_response( + "", + http::http_utils::http_method_not_allowed // 405 + )); + response->with_header("Allow", "POST, OPTIONS"); // Tell client what IS allowed + + if (handler) { + handler->status_variables.total_requests++; + } + + return response; +} + std::string MCP_JSONRPC_Resource::create_jsonrpc_response( const std::string& result, const json& id