| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "mcp-tools-list-metadata.h" |
| 4 | #include "mcp-tools.h" |
| 5 | #include "mcp-params.h" |
| 6 | #include "database/contexts/rrdcontext.h" |
| 7 | |
| 8 | // Static configuration for all list tools |
| 9 | static const MCP_LIST_TOOL_CONFIG mcp_list_tools[] = { |
| 10 | { |
| 11 | .name = MCP_TOOL_LIST_METRICS, |
| 12 | .title = "List available metrics", |
| 13 | .description = "Search and list available metrics to query, across some or all nodes, for any time-frame", |
| 14 | .output_type = MCP_LIST_OUTPUT_METRICS, |
| 15 | .mode = CONTEXTS_V2_CONTEXTS, |
| 16 | .options = 0, // Just MCP will be added |
| 17 | .params = { |
| 18 | .has_q = true, |
| 19 | .has_metrics = true, |
| 20 | .has_nodes = true, |
| 21 | .has_time_range = true, |
| 22 | .has_cardinality_limit = true, |
| 23 | .nodes_as_array = true, // list_metrics uses array for nodes |
| 24 | }, |
| 25 | }, |
| 26 | { |
| 27 | .name = MCP_TOOL_GET_METRICS_DETAILS, |
| 28 | .title = "Get metrics details", |
| 29 | .description = "Get retention and cardinality information about specific metrics", |
| 30 | .output_type = MCP_LIST_OUTPUT_METRICS, |
| 31 | .mode = CONTEXTS_V2_CONTEXTS, |
| 32 | .options = CONTEXTS_OPTION_TITLES | CONTEXTS_OPTION_INSTANCES | |
| 33 | CONTEXTS_OPTION_DIMENSIONS | CONTEXTS_OPTION_LABELS | |
| 34 | CONTEXTS_OPTION_RETENTION | CONTEXTS_OPTION_LIVENESS | |
| 35 | CONTEXTS_OPTION_FAMILY | CONTEXTS_OPTION_UNITS, |
| 36 | .params = { |
| 37 | .has_metrics = true, |
| 38 | .has_nodes = true, |
| 39 | .has_time_range = true, |
| 40 | .has_cardinality_limit = true, |
| 41 | .metrics_required = true, |
| 42 | .nodes_as_array = true, // get_metrics_details uses array for nodes |
| 43 | .metrics_as_array = true, // get_metrics_details uses array for metrics |
| 44 | }, |
| 45 | }, |
| 46 | { |
| 47 | .name = MCP_TOOL_LIST_NODES, |
| 48 | .title = "List monitored nodes", |
| 49 | .description = "Search for and list monitored nodes by hostname patterns. Use the 'nodes' parameter to search for specific nodes instead of retrieving all nodes", |
| 50 | .output_type = MCP_LIST_OUTPUT_NODES, |
| 51 | .mode = CONTEXTS_V2_NODES, |
| 52 | .options = 0, // Just MCP will be added |
| 53 | .params = { |
| 54 | .has_nodes = true, |
| 55 | .has_metrics = true, // Filters nodes that collect these metrics |
| 56 | .has_time_range = true, |
| 57 | .has_cardinality_limit = true, |
| 58 | .metrics_as_array = true, // list_nodes uses an array for metrics filter |
| 59 | }, |
| 60 | }, |
| 61 | { |
| 62 | .name = MCP_TOOL_LIST_FUNCTIONS, |
| 63 | .title = "List available functions", |
| 64 | .description = "List available Netdata functions that can be executed on specific nodes", |
| 65 | .output_type = MCP_LIST_OUTPUT_FUNCTIONS, |
| 66 | .mode = CONTEXTS_V2_FUNCTIONS, |
| 67 | .options = 0, |
| 68 | .params = { |
| 69 | .has_nodes = true, |
| 70 | .has_time_range = false, // Functions are live, not historical |
| 71 | .has_cardinality_limit = false, // Functions list is small, no limit needed |
| 72 | .nodes_required = true, // Must specify which nodes to query |
| 73 | .nodes_as_array = true, // list_functions uses array for nodes |
| 74 | }, |
| 75 | }, |
| 76 | { |
| 77 | .name = MCP_TOOL_GET_NODES_DETAILS, |
| 78 | .title = "Get detailed information about monitored nodes", |
| 79 | .description = "Gets comprehensive node information including hardware specs, OS details, capabilities, health status, available functions, streaming and monitoring configuration", |
| 80 | .output_type = MCP_LIST_OUTPUT_NODES, |
| 81 | .mode = CONTEXTS_V2_NODES | CONTEXTS_V2_NODES_INFO | CONTEXTS_V2_NODE_INSTANCES, |
| 82 | .options = 0, // Just MCP will be added |
| 83 | .params = { |
| 84 | .has_nodes = true, |
| 85 | .has_metrics = true, // Filters nodes that collect these metrics |
| 86 | .has_time_range = true, |
| 87 | .has_cardinality_limit = true, |
| 88 | .nodes_required = true, // Must specify nodes due to large output |
| 89 | .nodes_as_array = true, // get_nodes_details uses array for nodes |
| 90 | .metrics_as_array = true, // get_nodes_details uses array for metrics |
| 91 | }, |
| 92 | }, |
| 93 | { |
| 94 | .name = MCP_TOOL_LIST_RAISED_ALERTS, |
| 95 | .title = "List raised alerts", |
| 96 | .description = "List currently active alerts (WARNING and CRITICAL status)", |
| 97 | .output_type = MCP_LIST_OUTPUT_ALERTS, |
| 98 | .mode = CONTEXTS_V2_ALERTS, |
| 99 | .options = CONTEXTS_OPTION_INSTANCES | CONTEXTS_OPTION_VALUES, |
| 100 | .params = { |
| 101 | .has_nodes = true, |
| 102 | .has_metrics = true, // Filter by context pattern |
| 103 | .has_alert_pattern = true, |
| 104 | .has_time_range = false, // Raised alerts are current, not historical |
| 105 | .has_cardinality_limit = true, |
| 106 | .nodes_as_array = true, |
| 107 | .metrics_as_array = true, // Metrics should be an array for alerts |
| 108 | }, |
| 109 | .defaults = { |
| 110 | .alert_status = CONTEXT_ALERT_RAISED, // Only raised alerts |
| 111 | .cardinality_limit = 200, |
| 112 | }, |
| 113 | }, |
| 114 | { |
| 115 | .name = MCP_TOOL_LIST_ALL_ALERTS, |
| 116 | .title = "List all alerts", |
| 117 | .description = "List all currently running alerts", |
| 118 | .output_type = MCP_LIST_OUTPUT_ALERTS, |
| 119 | .mode = CONTEXTS_V2_ALERTS, |
| 120 | .options = CONTEXTS_OPTION_SUMMARY, |
| 121 | .params = { |
| 122 | .has_nodes = true, |
| 123 | .has_metrics = true, // Filter by context pattern |
| 124 | .has_alert_pattern = true, |
| 125 | .has_time_range = true, |
| 126 | .has_cardinality_limit = true, |
| 127 | .nodes_as_array = true, |
| 128 | .metrics_as_array = true, // Metrics should be an array for alerts |
| 129 | }, |
| 130 | .defaults = { |
| 131 | .alert_status = CONTEXTS_ALERT_STATUSES, // All statuses |
| 132 | .cardinality_limit = 200, |
| 133 | }, |
| 134 | }, |
| 135 | }; |
| 136 | |
| 137 | // Get tool configuration by name |
| 138 | const MCP_LIST_TOOL_CONFIG *mcp_get_list_tool_config(const char *name) { |
| 139 | for (size_t i = 0; i < sizeof(mcp_list_tools) / sizeof(mcp_list_tools[0]); i++) { |
| 140 | if (strcmp(mcp_list_tools[i].name, name) == 0) { |
| 141 | return &mcp_list_tools[i]; |
| 142 | } |
| 143 | } |
| 144 | return NULL; |
| 145 | } |
| 146 | |
| 147 | // Unified schema generation |
| 148 | void mcp_unified_list_tool_schema(BUFFER *buffer, const MCP_LIST_TOOL_CONFIG *config) { |
| 149 | if (!buffer || !config) return; |
| 150 | |
| 151 | // Determine output type name |
| 152 | const char *output_name; |
| 153 | switch (config->output_type) { |
| 154 | case MCP_LIST_OUTPUT_NODES: |
| 155 | output_name = "nodes"; |
| 156 | break; |
| 157 | case MCP_LIST_OUTPUT_METRICS: |
| 158 | output_name = "metrics"; |
| 159 | break; |
| 160 | case MCP_LIST_OUTPUT_FUNCTIONS: |
| 161 | output_name = "functions"; |
| 162 | break; |
| 163 | case MCP_LIST_OUTPUT_ALERTS: |
| 164 | output_name = "alerts"; |
| 165 | break; |
| 166 | default: |
| 167 | output_name = "items"; |
| 168 | break; |
| 169 | } |
| 170 | |
| 171 | // Buffers for composing strings |
| 172 | char title[256]; |
| 173 | char description[1024]; |
| 174 | |
| 175 | // Tool input schema |
| 176 | buffer_json_member_add_object(buffer, "inputSchema"); |
| 177 | buffer_json_member_add_string(buffer, "type", "object"); |
| 178 | buffer_json_member_add_string(buffer, "title", config->title); |
| 179 | |
| 180 | // Properties |
| 181 | buffer_json_member_add_object(buffer, "properties"); |
| 182 | |
| 183 | // Add a metrics pattern if supported |
| 184 | if (config->params.has_metrics) { |
| 185 | if (config->params.metrics_as_array) { |
| 186 | // Use an array for metrics |
| 187 | if (config->output_type != MCP_LIST_OUTPUT_METRICS) { |
| 188 | // This is a node/function/alert query - metrics acts as a filter |
| 189 | if (config->output_type == MCP_LIST_OUTPUT_ALERTS) { |
| 190 | // For alerts, metrics refers to contexts |
| 191 | mcp_schema_add_array_param(buffer, "metrics", |
| 192 | config->params.metrics_required ? "Specify the contexts to filter by" : "Filter by contexts", |
| 193 | config->params.metrics_required ? |
| 194 | "Array of specific context names to filter alerts by. This parameter is required. " |
| 195 | "Each context must be an exact match - no wildcards or patterns allowed. " |
| 196 | "Use '" MCP_TOOL_LIST_METRICS "' to discover available contexts. " |
| 197 | "Examples: [\"system.cpu\", \"disk.space\"], [\"mysql.queries\", \"redis.memory\"]" : |
| 198 | "Array of specific context names to filter alerts by. " |
| 199 | "Each context must be an exact match - no wildcards or patterns allowed. " |
| 200 | "Use '" MCP_TOOL_LIST_METRICS "' to discover available contexts. " |
| 201 | "If not specified, alerts from all contexts are included. " |
| 202 | "Examples: [\"system.cpu\", \"disk.space\"], [\"mysql.queries\", \"redis.memory\"]"); |
| 203 | } else { |
| 204 | // For nodes/functions, metrics is a filter |
| 205 | mcp_schema_add_array_param(buffer, "metrics", |
| 206 | config->params.metrics_required ? "Specify the metrics to filter by" : "Filter by metrics", |
| 207 | config->params.metrics_required ? |
| 208 | "Array of specific metric names to filter by. This parameter is required. " |
| 209 | "Each metric must be an exact match - no wildcards or patterns allowed. " |
| 210 | "Use '" MCP_TOOL_LIST_METRICS "' to discover available metrics. " |
| 211 | "Examples: [\"system.cpu\", \"system.load\"], [\"disk.io\", \"disk.space\"]" : |
| 212 | "Array of specific metric names to filter by. " |
| 213 | "Each metric must be an exact match - no wildcards or patterns allowed. " |
| 214 | "Use '" MCP_TOOL_LIST_METRICS "' to discover available metrics. " |
| 215 | "If not specified, all metrics are included. " |
| 216 | "Examples: [\"system.cpu\", \"system.load\"], [\"disk.io\", \"disk.space\"]"); |
| 217 | } |
| 218 | } else { |
| 219 | // This is a metrics query |
| 220 | mcp_schema_add_array_param(buffer, "metrics", |
| 221 | config->params.metrics_required ? "Specify the metrics" : "Filter metrics", |
| 222 | config->params.metrics_required ? |
| 223 | "Array of specific metric names to retrieve details for. This parameter is required. " |
| 224 | "Each metric must be an exact match - no wildcards or patterns allowed. " |
| 225 | "Examples: [\"system.cpu\", \"system.load\", \"system.ram\"]" : |
| 226 | "Array of specific metric names to filter. " |
| 227 | "Each metric must be an exact match - no wildcards or patterns allowed. " |
| 228 | "If not specified, all metrics are included. " |
| 229 | "Examples: [\"system.cpu\", \"system.load\", \"system.ram\"]"); |
| 230 | } |
| 231 | } else { |
| 232 | // Use string pattern for metrics |
| 233 | buffer_json_member_add_object(buffer, "metrics"); |
| 234 | buffer_json_member_add_string(buffer, "type", "string"); |
| 235 | |
| 236 | // Compose title and description based on context |
| 237 | if (config->output_type != MCP_LIST_OUTPUT_METRICS) { |
| 238 | // This is a node/function query - metrics acts as a filter |
| 239 | snprintfz(title, sizeof(title), "%s metrics", |
| 240 | config->params.metrics_required ? "Specify the" : "Filter"); |
| 241 | snprintfz(description, sizeof(description), |
| 242 | "Filter %s to only those collecting these metrics. " |
| 243 | "Use pipe (|) to separate multiple patterns. Supports wildcards. " |
| 244 | "Examples: 'system.*', '*cpu*|*memory*', 'disk.*|net.*'", |
| 245 | output_name); |
| 246 | } else { |
| 247 | // This is a metrics query |
| 248 | snprintfz(title, sizeof(title), "%s metrics", |
| 249 | config->params.metrics_required ? "Specify the" : "Filter"); |
| 250 | if (config->params.metrics_required) { |
| 251 | snprintfz(description, sizeof(description), |
| 252 | "Pipe-separated list of metric names. " |
| 253 | "Example: 'system.cpu|system.load|system.ram'"); |
| 254 | } else { |
| 255 | snprintfz(description, sizeof(description), |
| 256 | "Pattern matching on metric names. Use pipe (|) to separate multiple patterns. " |
| 257 | "Supports wildcards. Examples: 'system.*', '*cpu*|*memory*', 'disk.*|net.*|system.*'"); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | buffer_json_member_add_string(buffer, "title", title); |
| 262 | buffer_json_member_add_string(buffer, "description", description); |
| 263 | if (!config->params.metrics_required) { |
| 264 | buffer_json_member_add_string(buffer, "default", "*"); |
| 265 | } |
| 266 | buffer_json_object_close(buffer); // metrics |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | // Add full-text search if supported |
| 271 | if (config->params.has_q) { |
| 272 | buffer_json_member_add_object(buffer, "q"); |
| 273 | { |
| 274 | buffer_json_member_add_string(buffer, "type", "string"); |
| 275 | buffer_json_member_add_string(buffer, "title", "Full-text search on metrics metadata"); |
| 276 | buffer_json_member_add_string(buffer, "description", |
| 277 | "Filter metrics by searching across all their metadata (names, titles, instances, dimensions, labels). " |
| 278 | "Use pipe (|) to separate multiple search terms. Examples: 'memory|pressure', 'cpu|load|system'"); |
| 279 | } |
| 280 | buffer_json_object_close(buffer); // q |
| 281 | } |
| 282 | |
| 283 | // Add a nodes' pattern if supported |
| 284 | if (config->params.has_nodes) { |
| 285 | if (config->params.nodes_as_array) { |
| 286 | // Use an array for nodes |
| 287 | mcp_schema_add_array_param(buffer, "nodes", |
| 288 | config->params.nodes_required ? "Specify the nodes" : "Filter by nodes", |
| 289 | config->params.nodes_required ? |
| 290 | "Array of specific node names to query. This parameter is required because this tool produces detailed output. " |
| 291 | "Each node must be an exact match - no wildcards or patterns allowed. " |
| 292 | "Use '" MCP_TOOL_LIST_NODES "' to discover available nodes. " |
| 293 | "Examples: [\"node1\", \"node2\"], [\"web-server-01\", \"db-server-01\"]" : |
| 294 | "Array of specific node names to filter by. " |
| 295 | "Each node must be an exact match - no wildcards or patterns allowed. " |
| 296 | "Use '" MCP_TOOL_LIST_NODES "' to discover available nodes. " |
| 297 | "If not specified, all nodes are included. " |
| 298 | "Examples: [\"node1\", \"node2\"], [\"web-server-01\", \"db-server-01\"]"); |
| 299 | } else { |
| 300 | // Use string pattern for nodes |
| 301 | buffer_json_member_add_object(buffer, "nodes"); |
| 302 | buffer_json_member_add_string(buffer, "type", "string"); |
| 303 | |
| 304 | // Compose title and description based on context |
| 305 | snprintfz(title, sizeof(title), "%s nodes", |
| 306 | config->params.nodes_required ? "Specify the" : "Filter"); |
| 307 | |
| 308 | if (config->params.nodes_required) { |
| 309 | snprintfz(description, sizeof(description), |
| 310 | "Specify which nodes to query. This parameter is required because this tool produces detailed output. " |
| 311 | "Use pipe (|) to separate multiple patterns. Examples: 'node1|node2', '*web*|*db*', 'prod-*'"); |
| 312 | } else if (config->output_type == MCP_LIST_OUTPUT_NODES || config->output_type == MCP_LIST_OUTPUT_FUNCTIONS) { |
| 313 | // This is a node/function query - direct filtering |
| 314 | snprintfz(description, sizeof(description), |
| 315 | "Search for nodes by hostname patterns. This is the primary way to find specific nodes without retrieving the full list. " |
| 316 | "Use pipe (|) to separate multiple patterns. Wildcards (*) are supported for flexible matching. " |
| 317 | "Examples: 'node1|node2' (exact names), '*web*' (contains 'web'), 'prod-*' (starts with 'prod-'), '*db*|*cache*' (contains 'db' or 'cache')"); |
| 318 | } else { |
| 319 | // This is a metrics query - 'nodes' acts as a filter |
| 320 | snprintfz(description, sizeof(description), |
| 321 | "Filter %s to only those collected by these nodes. " |
| 322 | "Use pipe (|) to separate multiple patterns. " |
| 323 | "Examples: 'node1|node2', '*web*|*db*', 'prod-*|staging-*'", |
| 324 | output_name); |
| 325 | } |
| 326 | |
| 327 | buffer_json_member_add_string(buffer, "title", title); |
| 328 | buffer_json_member_add_string(buffer, "description", description); |
| 329 | if (!config->params.nodes_required) { |
| 330 | buffer_json_member_add_string(buffer, "default", "*"); |
| 331 | } |
| 332 | buffer_json_object_close(buffer); // nodes |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | // Add time range parameters if supported |
| 337 | if (config->params.has_time_range) { |
| 338 | // Build description prefix for metadata queries |
| 339 | char description_prefix[256]; |
| 340 | snprintfz(description_prefix, sizeof(description_prefix), |
| 341 | "%s with data collected", output_name ? output_name : "results"); |
| 342 | mcp_schema_add_time_params(buffer, description_prefix, false); |
| 343 | } |
| 344 | |
| 345 | // Add cardinality limit if supported |
| 346 | if (config->params.has_cardinality_limit) { |
| 347 | size_t default_cardinality = config->defaults.cardinality_limit ?: MCP_METADATA_CARDINALITY_LIMIT; |
| 348 | mcp_schema_add_cardinality_limit(buffer, |
| 349 | "Maximum number of items to return per category (dimensions, instances, labels, etc.). " |
| 350 | "Prevents response explosion. When exceeded, the response will indicate how many items were omitted.", |
| 351 | default_cardinality, |
| 352 | 1, // minimum |
| 353 | MAX(default_cardinality, MCP_METADATA_CARDINALITY_LIMIT_MAX)); |
| 354 | } |
| 355 | |
| 356 | // Add alert-specific parameters |
| 357 | if (config->params.has_alert_pattern) { |
| 358 | // Use string pattern for alerts |
| 359 | buffer_json_member_add_object(buffer, "alerts"); |
| 360 | buffer_json_member_add_string(buffer, "type", "string"); |
| 361 | buffer_json_member_add_string(buffer, "title", "Filter alerts"); |
| 362 | buffer_json_member_add_string(buffer, "description", |
| 363 | "Pattern matching on alert names. Use pipe (|) to separate multiple patterns. " |
| 364 | "Supports wildcards. Examples: 'disk_*', '*cpu*|*memory*', 'health.*'"); |
| 365 | buffer_json_member_add_string(buffer, "default", "*"); |
| 366 | buffer_json_object_close(buffer); // alerts |
| 367 | } |
| 368 | |
| 369 | buffer_json_object_close(buffer); // properties |
| 370 | |
| 371 | // Required fields |
| 372 | if (config->params.metrics_required || config->params.nodes_required) { |
| 373 | buffer_json_member_add_array(buffer, "required"); |
| 374 | if (config->params.metrics_required) { |
| 375 | buffer_json_add_array_item_string(buffer, "metrics"); |
| 376 | } |
| 377 | if (config->params.nodes_required) { |
| 378 | buffer_json_add_array_item_string(buffer, "nodes"); |
| 379 | } |
| 380 | buffer_json_array_close(buffer); |
| 381 | } |
| 382 | |
| 383 | buffer_json_object_close(buffer); // inputSchema |
| 384 | } |
| 385 | |
| 386 | // Removed extract_string_param and extract_size_param - now using mcp-params functions |
| 387 | |
| 388 | // Unified execution |
| 389 | MCP_RETURN_CODE mcp_unified_list_tool_execute(MCP_CLIENT *mcpc, const MCP_LIST_TOOL_CONFIG *config, |
| 390 | struct json_object *params, MCP_REQUEST_ID id __maybe_unused) |
| 391 | { |
| 392 | if (!mcpc || !config) |
| 393 | return MCP_RC_ERROR; |
| 394 | |
| 395 | // Extract parameters based on configuration |
| 396 | const char *q = config->params.has_q ? mcp_params_extract_string(params, "q", NULL) : NULL; |
| 397 | |
| 398 | // Handle metrics - either as array or string pattern |
| 399 | const char *metrics_pattern = NULL; |
| 400 | CLEAN_BUFFER *metrics_buffer = NULL; |
| 401 | |
| 402 | if (config->params.has_metrics) { |
| 403 | if (config->params.metrics_as_array) { |
| 404 | // Parse metrics as an array |
| 405 | metrics_buffer = mcp_params_parse_array_to_pattern(params, "metrics", false, false, MCP_TOOL_LIST_METRICS, mcpc->error); |
| 406 | if (buffer_strlen(mcpc->error) > 0) { |
| 407 | return MCP_RC_BAD_REQUEST; |
| 408 | } |
| 409 | metrics_pattern = buffer_tostring(metrics_buffer); |
| 410 | } else { |
| 411 | // Parse metrics as a string pattern |
| 412 | metrics_pattern = mcp_params_extract_string(params, "metrics", NULL); |
| 413 | } |
| 414 | if(metrics_pattern && !*metrics_pattern) { |
| 415 | metrics_pattern = NULL; // Treat empty string as no metrics specified |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | // Handle nodes - either as array or string pattern |
| 420 | const char *nodes_pattern = NULL; |
| 421 | CLEAN_BUFFER *nodes_buffer = NULL; |
| 422 | |
| 423 | if (config->params.has_nodes) { |
| 424 | if (config->params.nodes_as_array) { |
| 425 | // Parse nodes as array |
| 426 | nodes_buffer = mcp_params_parse_array_to_pattern(params, "nodes", false, false, MCP_TOOL_LIST_NODES, mcpc->error); |
| 427 | if (buffer_strlen(mcpc->error) > 0) { |
| 428 | return MCP_RC_BAD_REQUEST; |
| 429 | } |
| 430 | nodes_pattern = buffer_tostring(nodes_buffer); |
| 431 | } else { |
| 432 | // Parse nodes as a string pattern |
| 433 | nodes_pattern = mcp_params_extract_string(params, "nodes", NULL); |
| 434 | } |
| 435 | if(nodes_pattern && !*nodes_pattern) { |
| 436 | nodes_pattern = NULL; // Treat empty string as no nodes specified |
| 437 | } |
| 438 | } |
| 439 | |
| 440 | // Check required parameters |
| 441 | if (config->params.metrics_required && !metrics_pattern) { |
| 442 | buffer_sprintf(mcpc->error, "Missing required parameter 'metrics'. Use '" MCP_TOOL_LIST_METRICS "' to discover available metrics."); |
| 443 | return MCP_RC_ERROR; |
| 444 | } |
| 445 | |
| 446 | if (config->params.nodes_required && !nodes_pattern) { |
| 447 | buffer_sprintf(mcpc->error, "Missing required parameter 'nodes'. Use '" MCP_TOOL_LIST_NODES "' to discover available nodes."); |
| 448 | return MCP_RC_ERROR; |
| 449 | } |
| 450 | |
| 451 | // Extract time parameters (only if the tool supports them) |
| 452 | time_t after = 0; |
| 453 | time_t before = 0; |
| 454 | if (config->params.has_time_range) { |
| 455 | if (!mcp_params_parse_time_window(params, &after, &before, |
| 456 | MCP_DEFAULT_AFTER_TIME, MCP_DEFAULT_BEFORE_TIME, |
| 457 | false, mcpc->error)) { |
| 458 | return MCP_RC_BAD_REQUEST; |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | // Extract cardinality limit if supported |
| 463 | size_t cardinality_limit = 0; |
| 464 | if (config->params.has_cardinality_limit) { |
| 465 | size_t default_cardinality = config->defaults.cardinality_limit ?: MCP_METADATA_CARDINALITY_LIMIT; |
| 466 | cardinality_limit = mcp_params_extract_size(params, "cardinality_limit", default_cardinality, 1, 500, mcpc->error); |
| 467 | if (buffer_strlen(mcpc->error) > 0) { |
| 468 | return MCP_RC_BAD_REQUEST; |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | // Extract alert-specific parameters |
| 473 | const char *alert_pattern = NULL; |
| 474 | |
| 475 | if (config->params.has_alert_pattern) { |
| 476 | // Parse alerts as a string pattern |
| 477 | alert_pattern = mcp_params_extract_string(params, "alerts", NULL); |
| 478 | if(alert_pattern && !*alert_pattern) { |
| 479 | alert_pattern = NULL; // Treat empty string as no alerts specified |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | CLEAN_BUFFER *t = buffer_create(0, NULL); |
| 484 | |
| 485 | struct api_v2_contexts_request req = { |
| 486 | .scope_contexts = metrics_pattern, |
| 487 | .scope_nodes = nodes_pattern, |
| 488 | .contexts = NULL, |
| 489 | .nodes = NULL, |
| 490 | .q = q, |
| 491 | .after = after, |
| 492 | .before = before, |
| 493 | .cardinality_limit = cardinality_limit, |
| 494 | .options = config->options | CONTEXTS_OPTION_MCP | CONTEXTS_OPTION_RFC3339 | CONTEXTS_OPTION_JSON_LONG_KEYS | CONTEXTS_OPTION_MINIFY, |
| 495 | .alerts = { |
| 496 | .alert = alert_pattern, |
| 497 | .status = config->defaults.alert_status, |
| 498 | }, |
| 499 | }; |
| 500 | |
| 501 | // Determine mode - add SEARCH if q is provided |
| 502 | CONTEXTS_V2_MODE mode = config->mode; |
| 503 | if (config->params.has_q && q) { |
| 504 | mode = CONTEXTS_V2_SEARCH; |
| 505 | req.options |= CONTEXTS_OPTION_FAMILY | CONTEXTS_OPTION_UNITS | CONTEXTS_OPTION_TITLES | |
| 506 | CONTEXTS_OPTION_LABELS | CONTEXTS_OPTION_INSTANCES | CONTEXTS_OPTION_DIMENSIONS; |
| 507 | } |
| 508 | |
| 509 | int code = rrdcontext_to_json_v2(t, &req, mode); |
| 510 | if (code != HTTP_RESP_OK) { |
| 511 | buffer_sprintf(mcpc->error, "Failed to fetch %s, query returned http error code %d", config->name, code); |
| 512 | return MCP_RC_ERROR; |
| 513 | } |
| 514 | |
| 515 | // Initialize success response |
| 516 | mcp_init_success_result(mcpc, id); |
| 517 | { |
| 518 | // Start building a content array for the result |
| 519 | buffer_json_member_add_array(mcpc->result, "content"); |
| 520 | { |
| 521 | // Return text content for LLM compatibility |
| 522 | buffer_json_add_array_item_object(mcpc->result); |
| 523 | { |
| 524 | buffer_json_member_add_string(mcpc->result, "type", "text"); |
| 525 | buffer_json_member_add_string(mcpc->result, "text", buffer_tostring(t)); |
| 526 | } |
| 527 | buffer_json_object_close(mcpc->result); // Close text content |
| 528 | } |
| 529 | buffer_json_array_close(mcpc->result); // Close content array |
| 530 | } |
| 531 | buffer_json_object_close(mcpc->result); // Close result object |
| 532 | buffer_json_finalize(mcpc->result); // Finalize the JSON |
| 533 | |
| 534 | return MCP_RC_OK; |
| 535 | } |