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 <command>` 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.
pull/5386/head
Rene Cannao 2 months ago
parent af0411bd46
commit 998bd82387

@ -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;
}

@ -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());

@ -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"

@ -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
}

Loading…
Cancel
Save