Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions include/MCP_Thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Config_Tool_Handler;
class Query_Tool_Handler;
class Admin_Tool_Handler;
class Cache_Tool_Handler;
class Observe_Tool_Handler;
class Stats_Tool_Handler;
class AI_Tool_Handler;
class RAG_Tool_Handler;

Expand Down Expand Up @@ -46,7 +46,7 @@ class MCP_Threads_Handler
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_stats_endpoint_auth; ///< Authentication for /mcp/stats endpoint
char* mcp_query_endpoint_auth; ///< Authentication for /mcp/query endpoint
char* mcp_admin_endpoint_auth; ///< Authentication for /mcp/admin endpoint
char* mcp_cache_endpoint_auth; ///< Authentication for /mcp/cache endpoint
Expand Down Expand Up @@ -98,15 +98,15 @@ class MCP_Threads_Handler
* - query_tool_handler: /mcp/query endpoint (includes two-phase discovery tools)
* - admin_tool_handler: /mcp/admin endpoint
* - cache_tool_handler: /mcp/cache endpoint
* - observe_tool_handler: /mcp/observe endpoint
* - stats_tool_handler: /mcp/stats endpoint
* - ai_tool_handler: /mcp/ai endpoint
* - rag_tool_handler: /mcp/rag endpoint
*/
Config_Tool_Handler* config_tool_handler;
Query_Tool_Handler* query_tool_handler;
Admin_Tool_Handler* admin_tool_handler;
Cache_Tool_Handler* cache_tool_handler;
Observe_Tool_Handler* observe_tool_handler;
Stats_Tool_Handler* stats_tool_handler;
AI_Tool_Handler* ai_tool_handler;
RAG_Tool_Handler* rag_tool_handler;

Expand Down
19 changes: 15 additions & 4 deletions include/MCP_Tool_Handler.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#ifndef CLASS_MCP_TOOL_HANDLER_H
#define CLASS_MCP_TOOL_HANDLER_H

#include "cpp.h"
#include <string>
#include <memory>
#include "cpp.h"

// Include JSON library
#include "../deps/json/json.hpp"
using json = nlohmann::json;
#define PROXYJSON
Expand All @@ -14,7 +12,7 @@ using json = nlohmann::json;
* @brief Base class for all MCP Tool Handlers
*
* This class defines the interface that all tool handlers must implement.
* Each endpoint (config, query, admin, cache, observe) will have its own
* Each endpoint (config, query, admin, cache, stats) will have its own
* dedicated tool handler that provides specific tools for that endpoint's purpose.
*
* Tool handlers are responsible for:
Expand Down Expand Up @@ -183,6 +181,19 @@ class MCP_Tool_Handler {
}
return response;
}

/**
* @brief Convert a SQLite3_result into a JSON array of row objects.
*
* Each row becomes a JSON object keyed by column name. Field values
* that look numeric are stored as integers or doubles; NULL fields
* become JSON null; everything else is stored as a string.
*
* @param resultset The SQLite3_result to convert (may be NULL).
* @param cols Number of columns in the result set.
* @return JSON array of row objects (empty array when resultset is NULL or has no rows).
*/
static json resultset_to_json(SQLite3_result* resultset, int cols);
};

#endif /* CLASS_MCP_TOOL_HANDLER_H */
114 changes: 114 additions & 0 deletions include/Stats_Tool_Handler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#ifndef CLASS_STATS_TOOL_HANDLER_H
#define CLASS_STATS_TOOL_HANDLER_H

#include <map>

#include "MCP_Tool_Handler.h"
#include "MCP_Thread.h"

/**
* @brief Stats Tool Handler for /mcp/stats endpoint
*
* This handler provides tools for real-time metrics, statistics, and monitoring
* of ProxySQL internals including connection pools, query digests, errors,
* cluster status, and more.
*
* Tools provided:
* - get_health: Comprehensive health status summary
* - show_processlist: Active sessions (like MySQL SHOW PROCESSLIST)
* - show_metrics: Prometheus-compatible metrics
* - show_queries: Query digest performance statistics
* - show_connections: Backend connection pool metrics
* - show_errors: Error tracking and analysis
* - show_cluster: Cluster node health and sync status
* - list_stats: List available statistics tables
* - get_stats: Ad-hoc query any stats table
* - show_commands: Command execution statistics with latency distribution
* - show_users: User connection statistics
* - show_client_cache: Client host cache for connection throttling
* - show_gtid: GTID replication information
* - show_query_rules: Query rule hit statistics
* - show_history_connections: Historical connection trends
* - show_history_query_digest: Historical query digest snapshots
* - aggregate_metrics: Custom metric aggregations
*/
class Stats_Tool_Handler : public MCP_Tool_Handler {
private:
MCP_Threads_Handler* mcp_handler; ///< Pointer to MCP handler
pthread_mutex_t handler_lock; ///< Mutex for thread-safe operations

// Tool handlers
json handle_get_health(const json& arguments);
json handle_show_processlist(const json& arguments);
json handle_show_metrics(const json& arguments);
json handle_show_queries(const json& arguments);
json handle_show_connections(const json& arguments);
json handle_show_errors(const json& arguments);
json handle_show_cluster(const json& arguments);
json handle_list_stats(const json& arguments);
json handle_get_stats(const json& arguments);
json handle_show_commands(const json& arguments);
json handle_show_users(const json& arguments);
json handle_show_client_cache(const json& arguments);
json handle_show_gtid(const json& arguments);
json handle_show_query_rules(const json& arguments);
json handle_show_history_connections(const json& arguments);
json handle_show_history_query_digest(const json& arguments);
json handle_aggregate_metrics(const json& arguments);

// Helper methods

/**
* @brief Execute a SQL query against GloAdmin->admindb
* @param sql The SQL query to execute
* @param resultset Output pointer for the result set (caller must delete)
* @param cols Output for number of columns
* @return Empty string on success, error message on failure
*/
std::string execute_admin_query(const char* sql, SQLite3_result** resultset, int* cols);

/**
* @brief Execute a SQL query against GloAdmin->statsdb_disk (historical data)
* @param sql The SQL query to execute
* @param resultset Output pointer for the result set (caller must delete)
* @param cols Output for number of columns
* @return Empty string on success, error message on failure
*/
std::string execute_statsdb_disk_query(const char* sql, SQLite3_result** resultset, int* cols);

/**
* @brief Parse key-value pairs from stats_*_global tables
* @param resultset The result set from a global stats query
* @return Map of variable name to variable value
*/
std::map<std::string, std::string> parse_global_stats(SQLite3_result* resultset);

/**
* @brief Validate a stats table name against a whitelist
* @param table The table name to validate
* @return true if the table name is valid
*/
static bool is_valid_stats_table(const std::string& table);

public:
/**
* @brief Constructor
* @param handler Pointer to MCP_Threads_Handler
*/
Stats_Tool_Handler(MCP_Threads_Handler* handler);

/**
* @brief Destructor
*/
~Stats_Tool_Handler() override;

// MCP_Tool_Handler interface implementation
json get_tool_list() override;
json get_tool_description(const std::string& tool_name) override;
json execute_tool(const std::string& tool_name, const json& arguments) override;
int init() override;
void close() override;
std::string get_handler_name() const override { return "stats"; }
};

#endif /* CLASS_STATS_TOOL_HANDLER_H */
21 changes: 21 additions & 0 deletions include/proxysql_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,27 @@ static inline void set_thread_name(const char(&name)[LEN], const bool en = true)
*/
std::string get_client_addr(struct sockaddr* client_addr);

/**
* @brief Escape single quotes in a string for safe SQL insertion.
* @param input The string to escape.
* @return A new string with single quotes doubled and backslashes escaped.
*/
std::string sql_escape(const std::string& input);

/**
* @brief Calculate an approximate percentile value from histogram bucket counts.
* @param buckets Vector of counts per histogram bucket.
* @param thresholds Vector of upper-bound threshold values for each bucket (same length as buckets).
* @param percentile The percentile to calculate, in the range [0.0, 1.0].
* @return The threshold value of the bucket in which the target percentile falls,
* or 0 if the buckets are empty.
*/
int calculate_percentile_from_histogram(
const std::vector<int>& buckets,
const std::vector<int>& thresholds,
double percentile
);
Comment on lines +369 to +381
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

calculate_percentile_from_histogram: missing input validation and P0 edge-case quirk.

Two minor concerns:

  1. No range check on percentile: Values outside [0.0, 1.0] silently produce thresholds.back() (target exceeds total). Adding an assert or clamp would make misuse visible early.

  2. P0 always returns thresholds[0]: When percentile == 0.0, target = 0; after the first cumulative += buckets[0], the condition cumulative >= 0 is immediately satisfied regardless of whether that bucket contains any counts. For sparse histograms (e.g., first several buckets are zero) this returns the wrong threshold.

💡 Suggested guard and doc note
 int calculate_percentile_from_histogram(
 	const std::vector<int>& buckets,
 	const std::vector<int>& thresholds,
 	double percentile
 ) {
+	assert(percentile >= 0.0 && percentile <= 1.0);
 	long long total = 0;
 	for (int b : buckets) total += b;
 
 	if (total == 0) return 0;
 
 	long long target = (long long)(total * percentile);
+	if (target == 0) target = 1; // ensure at least one count is crossed for P0
 	long long cumulative = 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/proxysql_utils.h` around lines 369 - 381, Validate inputs and fix the
P0 edge case in calculate_percentile_from_histogram: first assert or clamp
percentile to [0.0,1.0] and verify buckets.size() == thresholds.size() and
non-empty buckets/thresholds; if percentile == 0.0, return the threshold of the
first bucket with a non-zero count (and return 0 if all counts are zero) instead
of using the cumulative >= 0 check; otherwise compute the target rank from
percentile and total count and iterate cumulative counts to find the bucket
whose cumulative meets or exceeds that target (return thresholds.back() if
target exceeds total).


/**
* @brief Check if a port is available for binding
*
Expand Down
6 changes: 0 additions & 6 deletions lib/Admin_FlushVariables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ using json = nlohmann::json;
#include "proxysql_config.h"
#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"
Expand Down
4 changes: 2 additions & 2 deletions lib/MCP_Endpoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ bool MCP_JSONRPC_Resource::authenticate_request(const httpserver::http_request&

if (endpoint_name == "config") {
expected_token = handler->variables.mcp_config_endpoint_auth;
} else if (endpoint_name == "observe") {
expected_token = handler->variables.mcp_observe_endpoint_auth;
} else if (endpoint_name == "stats") {
expected_token = handler->variables.mcp_stats_endpoint_auth;
} else if (endpoint_name == "query") {
expected_token = handler->variables.mcp_query_endpoint_auth;
} else if (endpoint_name == "admin") {
Expand Down
30 changes: 15 additions & 15 deletions lib/MCP_Thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include "Query_Tool_Handler.h"
#include "Admin_Tool_Handler.h"
#include "Cache_Tool_Handler.h"
#include "Observe_Tool_Handler.h"
#include "Stats_Tool_Handler.h"
#include "proxysql_debug.h"
#include "ProxySQL_MCP_Server.hpp"

Expand All @@ -19,7 +19,7 @@ static const char* mcp_thread_variables_names[] = {
"port",
"use_ssl",
"config_endpoint_auth",
"observe_endpoint_auth",
"stats_endpoint_auth",
"query_endpoint_auth",
"admin_endpoint_auth",
"cache_endpoint_auth",
Expand All @@ -45,7 +45,7 @@ MCP_Threads_Handler::MCP_Threads_Handler() {
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_stats_endpoint_auth = strdup("");
variables.mcp_query_endpoint_auth = strdup("");
variables.mcp_admin_endpoint_auth = strdup("");
variables.mcp_cache_endpoint_auth = strdup("");
Expand All @@ -70,15 +70,15 @@ MCP_Threads_Handler::MCP_Threads_Handler() {
query_tool_handler = NULL;
admin_tool_handler = NULL;
cache_tool_handler = NULL;
observe_tool_handler = NULL;
stats_tool_handler = NULL;
rag_tool_handler = NULL;
}

MCP_Threads_Handler::~MCP_Threads_Handler() {
if (variables.mcp_config_endpoint_auth)
free(variables.mcp_config_endpoint_auth);
if (variables.mcp_observe_endpoint_auth)
free(variables.mcp_observe_endpoint_auth);
if (variables.mcp_stats_endpoint_auth)
free(variables.mcp_stats_endpoint_auth);
if (variables.mcp_query_endpoint_auth)
free(variables.mcp_query_endpoint_auth);
if (variables.mcp_admin_endpoint_auth)
Expand Down Expand Up @@ -126,9 +126,9 @@ MCP_Threads_Handler::~MCP_Threads_Handler() {
delete cache_tool_handler;
cache_tool_handler = NULL;
}
if (observe_tool_handler) {
delete observe_tool_handler;
observe_tool_handler = NULL;
if (stats_tool_handler) {
delete stats_tool_handler;
stats_tool_handler = NULL;
}
if (rag_tool_handler) {
delete rag_tool_handler;
Expand Down Expand Up @@ -186,8 +186,8 @@ int MCP_Threads_Handler::get_variable(const char* name, char* val) {
sprintf(val, "%s", variables.mcp_config_endpoint_auth ? variables.mcp_config_endpoint_auth : "");
return 0;
}
if (!strcmp(name, "observe_endpoint_auth")) {
sprintf(val, "%s", variables.mcp_observe_endpoint_auth ? variables.mcp_observe_endpoint_auth : "");
if (!strcmp(name, "stats_endpoint_auth")) {
sprintf(val, "%s", variables.mcp_stats_endpoint_auth ? variables.mcp_stats_endpoint_auth : "");
return 0;
}
if (!strcmp(name, "query_endpoint_auth")) {
Expand Down Expand Up @@ -275,10 +275,10 @@ int MCP_Threads_Handler::set_variable(const char* name, const char* value) {
variables.mcp_config_endpoint_auth = strdup(value);
return 0;
}
if (!strcmp(name, "observe_endpoint_auth")) {
if (variables.mcp_observe_endpoint_auth)
free(variables.mcp_observe_endpoint_auth);
variables.mcp_observe_endpoint_auth = strdup(value);
if (!strcmp(name, "stats_endpoint_auth")) {
if (variables.mcp_stats_endpoint_auth)
free(variables.mcp_stats_endpoint_auth);
variables.mcp_stats_endpoint_auth = strdup(value);
return 0;
}
if (!strcmp(name, "query_endpoint_auth")) {
Expand Down
47 changes: 47 additions & 0 deletions lib/MCP_Tool_Handler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include "sqlite3db.h"
#include "MCP_Tool_Handler.h"

#include "../deps/json/json.hpp"
using json = nlohmann::json;
#define PROXYJSON

json MCP_Tool_Handler::resultset_to_json(SQLite3_result* resultset, int cols) {
json rows = json::array();

if (!resultset || resultset->rows_count == 0) {
return rows;
}

for (const auto& row : resultset->rows) {
json obj = json::object();
for (int i = 0; i < cols && i < (int)resultset->column_definition.size(); i++) {
const char* col_name = resultset->column_definition[i]->name;
const char* val = row->fields[i];

if (!val) {
obj[col_name] = nullptr;
continue;
}

// Try to parse the value as a number.
// strtoll / strtod are used directly to avoid the overhead
// of a separate is_numeric() scan followed by a second parse.
char* end = nullptr;
long long ll = strtoll(val, &end, 10);
if (end != val && *end == '\0') {
obj[col_name] = ll;
} else {
// Not a plain integer; try floating-point
double d = strtod(val, &end);
if (end != val && *end == '\0') {
obj[col_name] = d;
} else {
obj[col_name] = std::string(val);
}
}
Comment on lines +31 to +43

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current numeric parsing logic using strtoll can incorrectly handle large unsigned 64-bit integers, which are common for statistics counters. When strtoll is given a value larger than LLONG_MAX, it returns LLONG_MAX and sets errno to ERANGE. Since errno is not checked, this leads to silent data corruption, where large counters are reported as LLONG_MAX.

A more robust approach is to first attempt parsing as unsigned long long for non-negative values, and then fall back to other types, while also checking errno for range errors.

            char* end = nullptr;
            bool parsed = false;

            // Try unsigned long long for non-negative numbers, which is common for stats counters.
            if (val[0] != '-') {
                errno = 0;
                unsigned long long ull = strtoull(val, &end, 10);
                if (end != val && *end == '\0' && errno != ERANGE) {
                    obj[col_name] = ull;
                    parsed = true;
                }
            }

            // Try long long if not parsed yet (e.g., for negative numbers).
            if (!parsed) {
                errno = 0;
                end = nullptr;
                long long ll = strtoll(val, &end, 10);
                if (end != val && *end == '\0' && errno != ERANGE) {
                    obj[col_name] = ll;
                    parsed = true;
                }
            }

            // Fallback to double if it's not an integer.
            if (!parsed) {
                end = nullptr;
                double d = strtod(val, &end);
                if (end != val && *end == '\0') {
                    obj[col_name] = d;
                    parsed = true;
                }
            }

            // Finally, treat as a string if no numeric conversion worked.
            if (!parsed) {
                obj[col_name] = std::string(val);
            }

}
rows.push_back(obj);
}

return rows;
}
4 changes: 2 additions & 2 deletions lib/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo
PgSQL_Variables_Validator.oo PgSQL_ExplicitTxnStateMgr.oo \
PgSQL_PreparedStatement.oo PgSQL_Extended_Query_Message.oo \
pgsql_tokenizer.oo \
MCP_Thread.oo ProxySQL_MCP_Server.oo MCP_Endpoint.oo \
MCP_Thread.oo ProxySQL_MCP_Server.oo MCP_Endpoint.oo MCP_Tool_Handler.oo \
MySQL_Catalog.oo MySQL_Tool_Handler.oo MySQL_FTS.oo \
Config_Tool_Handler.oo Query_Tool_Handler.oo \
Admin_Tool_Handler.oo Cache_Tool_Handler.oo Observe_Tool_Handler.oo \
Admin_Tool_Handler.oo Cache_Tool_Handler.oo Stats_Tool_Handler.oo \
AI_Features_Manager.oo LLM_Bridge.oo LLM_Clients.oo Anomaly_Detector.oo AI_Vector_Storage.oo AI_Tool_Handler.oo \
RAG_Tool_Handler.oo \
Discovery_Schema.oo Static_Harvester.oo
Expand Down
Loading
Loading