From 998bd82387061667c247368ededf63cc86901313 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Mon, 16 Feb 2026 23:32:34 +0000 Subject: [PATCH] MCP TAP startup: fix tool-handler initialization order, improve MCP PROFILES observability, and seed monitor users This change fixes recurring MCP TAP failures where `/mcp/query` returned: Tool Handler not initialized for endpoint: query and where backend monitor auth failures flooded logs. Problem summary - MCP server startup can occur before runtime target/auth profiles and backend server mappings are loaded. - If that happens, Query_Tool_Handler initialization sees no executable targets and remains NULL. - MCP endpoint resources bind the handler pointer at creation time, so a NULL query handler at startup breaks `/mcp/query` until server restart. Code changes 1) Add explicit admin command logging for MCP PROFILES commands - Added `Received ` logging in the MCP PROFILES command block, matching behavior of other admin command handlers. - File: `lib/Admin_Handler.cpp` 2) Trigger MCP server refresh after `LOAD MCP PROFILES TO RUNTIME` - After copying profiles into runtime and rebuilding target/auth map, call `ProxySQL_Admin::load_mcp_server()`. - This allows MCP to self-heal when profiles become available after initial startup. - File: `lib/Admin_Handler.cpp` 3) Restart MCP server when query handler is missing - Extended `ProxySQL_Admin::load_mcp_server()` restart checks to include: - running server + `query_tool_handler == NULL` - Restart reason now includes tool handler initialization mismatch. - File: `lib/ProxySQL_Admin.cpp` 4) Fix TAP configurator load order to avoid early MCP startup - Reordered `test/tap/tests/mcp_rules_testing/configure_mcp.sh` runtime sequence: - `LOAD MYSQL SERVERS TO RUNTIME` - `LOAD PGSQL SERVERS TO RUNTIME` (best effort) - `LOAD MCP PROFILES TO RUNTIME` - `LOAD MCP VARIABLES TO RUNTIME` (last) - This ensures MCP starts only after routing/auth context is present. 5) Seed monitor credentials in AI local infra pre-hook - Added backend user/role creation for default monitor credentials `monitor/monitor`: - MySQL: create user + monitor-relevant grants - PostgreSQL: create role + `pg_monitor` + DB connect grants - Reduces monitor auth noise in local AI TAP dockerized setup. - File: `test/tap/groups/ai/pre-proxysql.bash` 6) Mark new TAP phase scripts executable - `test_phase10_eval_explain.sh` - `test_phase11_pgsql_target.sh` Expected outcome - MCP query endpoint no longer stays stuck with an uninitialized tool handler after TAP configuration. - MCP query-rules admin commands stop failing due to missing Query_Tool_Handler. - MCP profile command flow is visible in logs for easier debugging. - Local AI TAP infra no longer emits continuous monitor authentication failures for default monitor credentials. --- lib/Admin_Handler.cpp | 4 +++ lib/ProxySQL_Admin.cpp | 7 ++++ test/tap/groups/ai/pre-proxysql.bash | 35 ++++++++++++++++++- .../tests/mcp_rules_testing/configure_mcp.sh | 24 ++++++++----- .../test_phase10_eval_explain.sh | 0 .../test_phase11_pgsql_target.sh | 0 6 files changed, 61 insertions(+), 9 deletions(-) mode change 100644 => 100755 test/tap/tests/mcp_rules_testing/test_phase10_eval_explain.sh mode change 100644 => 100755 test/tap/tests/mcp_rules_testing/test_phase11_pgsql_target.sh diff --git a/lib/Admin_Handler.cpp b/lib/Admin_Handler.cpp index 98443b945..d36bb3566 100644 --- a/lib/Admin_Handler.cpp +++ b/lib/Admin_Handler.cpp @@ -2542,6 +2542,7 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query (!strncasecmp("LOAD MCP PROFILES ", query_no_space, 18)))) { ProxySQL_Admin *SPA = (ProxySQL_Admin *)pa; + proxy_info("Received %s command\n", query_no_space); const auto load_target_auth_map_from_runtime = [&]() -> bool { char* error = NULL; @@ -2656,6 +2657,9 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query SPA->send_error_msg_to_client(sess, (char *)"Failed to refresh MCP runtime profile map"); return false; } + // Ensure MCP server/query handler reflects the newly loaded runtime profiles. + // This recovers cases where MCP server was started before profiles were available. + SPA->load_mcp_server(); SPA->send_ok_msg_to_client(sess, NULL, 0, query_no_space); return false; } diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 92b50b8cb..fdbdbbec5 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -3445,6 +3445,13 @@ void ProxySQL_Admin::load_mcp_server() { needs_restart = true; restart_reason += "SSL mode"; } + if (GloMCPH->query_tool_handler == NULL) { + needs_restart = true; + if (!restart_reason.empty()) { + restart_reason += " "; + } + restart_reason += "tool handler initialization"; + } if (needs_restart) { proxy_info("MCP: Configuration changed (%s), restarting server...\n", restart_reason.c_str()); diff --git a/test/tap/groups/ai/pre-proxysql.bash b/test/tap/groups/ai/pre-proxysql.bash index 3fc1501c5..bab19fd57 100755 --- a/test/tap/groups/ai/pre-proxysql.bash +++ b/test/tap/groups/ai/pre-proxysql.bash @@ -43,15 +43,46 @@ exec_admin() { mysql ${SSLOPT} -h"${ADMIN_HOST}" -P"${ADMIN_PORT}" -u"${ADMIN_USER}" -p"${ADMIN_PASS}" -e "$1" 2>&1 | sed '/^mysql: .*Warning/d' } +compose() { + if docker compose version >/dev/null 2>&1; then + docker compose -f "${SCRIPT_DIR}/docker-compose.yml" "$@" + elif command -v docker-compose >/dev/null 2>&1; then + docker-compose -f "${SCRIPT_DIR}/docker-compose.yml" "$@" + else + echo "[ERROR] docker compose is not available" >&2 + exit 1 + fi +} + +create_mysql_monitor_user() { + echo "[INFO] AI pre-hook: creating MySQL monitor user monitor/monitor on backend ${TAP_MYSQLHOST}:${TAP_MYSQLPORT}" + mysql -h"${TAP_MYSQLHOST}" -P"${TAP_MYSQLPORT}" -u"${TAP_MYSQLUSERNAME}" -p"${TAP_MYSQLPASSWORD}" -e "\ +CREATE USER IF NOT EXISTS 'monitor'@'%' IDENTIFIED BY 'monitor'; \ +GRANT USAGE, PROCESS, REPLICATION CLIENT ON *.* TO 'monitor'@'%'; \ +FLUSH PRIVILEGES;" +} + +create_pgsql_monitor_user() { + echo "[INFO] AI pre-hook: creating PostgreSQL monitor user monitor/monitor on backend ${AI_PGSQL_HOST}:${AI_PGSQL_PORT}" + local sql="DO \$\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname='monitor') THEN CREATE ROLE monitor LOGIN PASSWORD 'monitor'; END IF; END \$\$; GRANT pg_monitor TO monitor; GRANT CONNECT ON DATABASE postgres TO monitor; GRANT CONNECT ON DATABASE ${AI_PGSQL_DB} TO monitor;" + if command -v psql >/dev/null 2>&1; then + PGPASSWORD="${AI_PGSQL_PASSWORD}" psql -h "${AI_PGSQL_HOST}" -p "${AI_PGSQL_PORT}" -U "${AI_PGSQL_USER}" -d "${AI_PGSQL_DB}" -v ON_ERROR_STOP=1 -c "${sql}" + else + compose exec -T pgsql psql -U "${AI_PGSQL_USER}" -d "${AI_PGSQL_DB}" -v ON_ERROR_STOP=1 -c "${sql}" + fi +} + echo "[INFO] AI pre-hook: starting group-local containers" "${SCRIPT_DIR}/docker-compose-init.bash" +create_mysql_monitor_user +create_pgsql_monitor_user echo "[INFO] AI pre-hook: configuring ProxySQL MCP and backend routing" # Configure MCP runtime variables. exec_admin "SET mcp-port='${TAP_MCPPORT}';" exec_admin "SET mcp-use_ssl='true';" -exec_admin "SET mcp-enabled='true';" +exec_admin "SET mcp-enabled='false';" exec_admin "LOAD MCP VARIABLES TO RUNTIME; SAVE MCP VARIABLES TO DISK;" # Keep predictable hostgroups for both direct tests and MCP target routing. @@ -80,6 +111,8 @@ exec_admin "INSERT INTO mcp_target_profiles (target_id, protocol, hostgroup_id, exec_admin "INSERT INTO mcp_target_profiles (target_id, protocol, hostgroup_id, auth_profile_id, description, max_rows, timeout_ms, allow_explain, allow_discovery, active, comment) VALUES ('${MCP_PGSQL_TARGET_ID}', 'pgsql', ${MCP_PGSQL_HOSTGROUP_ID}, '${MCP_PGSQL_AUTH_PROFILE_ID}', 'AI local PostgreSQL target', 200, 5000, 1, 1, 1, 'ai local');" exec_admin "LOAD MCP PROFILES TO RUNTIME; SAVE MCP PROFILES TO DISK;" +exec_admin "SET mcp-enabled='true';" +exec_admin "LOAD MCP VARIABLES TO RUNTIME; SAVE MCP VARIABLES TO DISK;" sleep 2 echo "[INFO] AI pre-hook completed" diff --git a/test/tap/tests/mcp_rules_testing/configure_mcp.sh b/test/tap/tests/mcp_rules_testing/configure_mcp.sh index f2ec817aa..c6730148f 100755 --- a/test/tap/tests/mcp_rules_testing/configure_mcp.sh +++ b/test/tap/tests/mcp_rules_testing/configure_mcp.sh @@ -195,14 +195,21 @@ configure_mcp_profiles() { # Load MCP variables/profiles/server tables to runtime load_to_runtime() { - log_step "Loading MCP variables to RUNTIME..." - if exec_admin_silent "LOAD MCP VARIABLES TO RUNTIME;" >/dev/null 2>&1; then - log_info "MCP variables loaded to RUNTIME" + log_step "Loading MySQL servers to RUNTIME..." + if exec_admin_silent "LOAD MYSQL SERVERS TO RUNTIME;" >/dev/null 2>&1; then + log_info "MySQL servers loaded to RUNTIME" else - log_error "Failed to load MCP variables to RUNTIME" + log_error "Failed to load MySQL servers to RUNTIME" return 1 fi + # Optional in MySQL-only setups, but required if pgsql MCP targets are configured. + if exec_admin_silent "LOAD PGSQL SERVERS TO RUNTIME;" >/dev/null 2>&1; then + log_info "PgSQL servers loaded to RUNTIME" + else + log_warn "LOAD PGSQL SERVERS TO RUNTIME failed (continuing)" + fi + log_step "Loading MCP profiles to RUNTIME..." if exec_admin_silent "LOAD MCP PROFILES TO RUNTIME;" >/dev/null 2>&1; then log_info "MCP profiles loaded to RUNTIME" @@ -211,11 +218,12 @@ load_to_runtime() { return 1 fi - log_step "Loading MySQL servers to RUNTIME..." - if exec_admin_silent "LOAD MYSQL SERVERS TO RUNTIME;" >/dev/null 2>&1; then - log_info "MySQL servers loaded to RUNTIME" + # Load MCP variables last so server startup sees runtime servers+profiles. + log_step "Loading MCP variables to RUNTIME..." + if exec_admin_silent "LOAD MCP VARIABLES TO RUNTIME;" >/dev/null 2>&1; then + log_info "MCP variables loaded to RUNTIME" else - log_error "Failed to load MySQL servers to RUNTIME" + log_error "Failed to load MCP variables to RUNTIME" return 1 fi } diff --git a/test/tap/tests/mcp_rules_testing/test_phase10_eval_explain.sh b/test/tap/tests/mcp_rules_testing/test_phase10_eval_explain.sh old mode 100644 new mode 100755 diff --git a/test/tap/tests/mcp_rules_testing/test_phase11_pgsql_target.sh b/test/tap/tests/mcp_rules_testing/test_phase11_pgsql_target.sh old mode 100644 new mode 100755