master
c 535 lines 25 KB
Raw
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 }