MCP: Add stats endpoint and tools

Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
v4.0-mcp-stats
Wazir Ahmed 3 months ago
parent 53bd4b6065
commit 643b322f29

@ -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;
@ -46,7 +46,7 @@ public:
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
@ -98,7 +98,7 @@ public:
* - 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
*/
@ -106,7 +106,7 @@ public:
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;

@ -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
@ -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:
@ -183,6 +181,19 @@ protected:
}
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 */

@ -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 */

@ -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
);
/**
* @brief Check if a port is available for binding
*

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

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

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

@ -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);
}
}
}
rows.push_back(obj);
}
return rows;
}

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

@ -11,7 +11,7 @@ using json = nlohmann::json;
#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 "AI_Tool_Handler.h"
#include "RAG_Tool_Handler.h"
#include "AI_Features_Manager.h"
@ -122,10 +122,10 @@ ProxySQL_MCP_Server::ProxySQL_MCP_Server(int p, MCP_Threads_Handler* h)
proxy_info("Cache Tool Handler initialized\n");
}
// 5. Observe Tool Handler
handler->observe_tool_handler = new Observe_Tool_Handler(handler);
if (handler->observe_tool_handler->init() == 0) {
proxy_info("Observe Tool Handler initialized\n");
// 5. Stats Tool Handler
handler->stats_tool_handler = new Stats_Tool_Handler(handler);
if (handler->stats_tool_handler->init() == 0) {
proxy_info("Stats Tool Handler initialized\n");
}
// 6. AI Tool Handler (for LLM and other AI features)
@ -151,10 +151,10 @@ ProxySQL_MCP_Server::ProxySQL_MCP_Server(int p, MCP_Threads_Handler* h)
ws->register_resource("/mcp/config", config_resource.get(), true);
_endpoints.push_back({"/mcp/config", std::move(config_resource)});
std::unique_ptr<httpserver::http_resource> observe_resource =
std::unique_ptr<httpserver::http_resource>(new MCP_JSONRPC_Resource(handler, handler->observe_tool_handler, "observe"));
ws->register_resource("/mcp/observe", observe_resource.get(), true);
_endpoints.push_back({"/mcp/observe", std::move(observe_resource)});
std::unique_ptr<httpserver::http_resource> stats_resource =
std::unique_ptr<httpserver::http_resource>(new MCP_JSONRPC_Resource(handler, handler->stats_tool_handler, "stats"));
ws->register_resource("/mcp/stats", stats_resource.get(), true);
_endpoints.push_back({"/mcp/stats", std::move(stats_resource)});
std::unique_ptr<httpserver::http_resource> query_resource =
std::unique_ptr<httpserver::http_resource>(new MCP_JSONRPC_Resource(handler, handler->query_tool_handler, "query"));
@ -202,7 +202,7 @@ ProxySQL_MCP_Server::ProxySQL_MCP_Server(int p, MCP_Threads_Handler* h)
}
int endpoint_count = (handler->ai_tool_handler ? 1 : 0) + (handler->rag_tool_handler ? 1 : 0) + 5;
std::string endpoints_list = "/mcp/config, /mcp/observe, /mcp/query, /mcp/admin, /mcp/cache";
std::string endpoints_list = "/mcp/config, /mcp/stats, /mcp/query, /mcp/admin, /mcp/cache";
if (handler->ai_tool_handler) {
endpoints_list += ", /mcp/ai";
}
@ -254,11 +254,11 @@ ProxySQL_MCP_Server::~ProxySQL_MCP_Server() {
handler->cache_tool_handler = NULL;
}
// Clean up Observe Tool Handler
if (handler->observe_tool_handler) {
proxy_info("Cleaning up Observe Tool Handler...\n");
delete handler->observe_tool_handler;
handler->observe_tool_handler = NULL;
// Clean up Stats Tool Handler
if (handler->stats_tool_handler) {
proxy_info("Cleaning up Stats Tool Handler...\n");
delete handler->stats_tool_handler;
handler->stats_tool_handler = NULL;
}
// Clean up AI Tool Handler (uses shared components, don't delete them)

File diff suppressed because it is too large Load Diff

@ -741,3 +741,41 @@ std::string get_client_addr(struct sockaddr* client_addr) {
return str_client_addr;
}
std::string sql_escape(const std::string& input) {
std::string output;
output.reserve(input.size() * 2);
for (char c : input) {
if (c == '\'') {
output += "''";
} else if (c == '\\') {
output += "\\\\";
} else {
output += c;
}
}
return output;
}
int calculate_percentile_from_histogram(
const std::vector<int>& buckets,
const std::vector<int>& thresholds,
double percentile
) {
int total = 0;
for (int b : buckets) total += b;
if (total == 0) return 0;
int target = (int)(total * percentile);
int cumulative = 0;
for (size_t i = 0; i < buckets.size() && i < thresholds.size(); i++) {
cumulative += buckets[i];
if (cumulative >= target) {
return thresholds[i];
}
}
return thresholds.empty() ? 0 : thresholds.back();
}

@ -63,7 +63,7 @@ mcp_variables=
mcp_port=6071
mcp_use_ssl=false # Enable/disable SSL/TLS (default: true for security)
mcp_config_endpoint_auth=""
mcp_observe_endpoint_auth=""
mcp_stats_endpoint_auth=""
mcp_query_endpoint_auth=""
mcp_admin_endpoint_auth=""
mcp_cache_endpoint_auth=""

@ -230,7 +230,7 @@ int test_variable_persistence(MYSQL* admin) {
MYSQL_QUERY(admin, "SET mcp-enabled=false");
MYSQL_QUERY(admin, "SET mcp-port=6071");
MYSQL_QUERY(admin, "SET mcp-config_endpoint_auth=''");
MYSQL_QUERY(admin, "SET mcp-observe_endpoint_auth=''");
MYSQL_QUERY(admin, "SET mcp-stats_endpoint_auth=''");
MYSQL_QUERY(admin, "SET mcp-query_endpoint_auth=''");
MYSQL_QUERY(admin, "SET mcp-admin_endpoint_auth=''");
MYSQL_QUERY(admin, "SET mcp-cache_endpoint_auth=''");

Loading…
Cancel
Save