| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | /** |
| 4 | * MCP Tools Namespace |
| 5 | * |
| 6 | * The MCP Tools namespace provides methods for discovering and executing tools offered by the server. |
| 7 | * In the MCP protocol, tools are discrete operations that clients can invoke to perform specific actions. |
| 8 | * |
| 9 | * Tools are model-controlled actions - meaning the AI decides when and how to use them based on context. |
| 10 | * Each tool has a defined input schema that specifies required and optional parameters. |
| 11 | * |
| 12 | * Standard methods in the MCP specification: |
| 13 | * |
| 14 | * 1. tools/list - Lists available tools with their schemas and metadata |
| 15 | * - Returns a list of all available tools |
| 16 | * - Includes each tool's name, description, and input schema |
| 17 | * |
| 18 | * 2. tools/call - Executes a specific tool with provided parameters |
| 19 | * - Takes a tool name and arguments |
| 20 | * - Returns the result of the tool execution |
| 21 | * - May include resource references, text, images, or other content types |
| 22 | * |
| 23 | * In the Netdata context, tools provide access to operations like: |
| 24 | * - Exploring metrics and their relationships |
| 25 | * - Analyzing time-series data patterns |
| 26 | * - Finding correlations between metrics |
| 27 | * - Root cause analysis for anomalies |
| 28 | * - Summarizing system health |
| 29 | */ |
| 30 | |
| 31 | #include "mcp-tools.h" |
| 32 | |
| 33 | // Include tool-specific header files |
| 34 | #include "mcp-tools-list-metadata.h" |
| 35 | #include "mcp-tools-execute-function.h" |
| 36 | #include "mcp-tools-query-metrics.h" |
| 37 | #include "mcp-tools-weights.h" |
| 38 | #include "mcp-tools-alert-transitions.h" |
| 39 | #include "mcp-tools-configured-alerts.h" |
| 40 | |
| 41 | // Tool handler function prototypes |
| 42 | typedef MCP_RETURN_CODE (*mcp_tool_execute_t)(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id); |
| 43 | typedef void (*mcp_tool_schema_t)(BUFFER *schema_buffer); |
| 44 | |
| 45 | // Tool definition structure |
| 46 | typedef struct { |
| 47 | const char *name; // Tool name |
| 48 | const char *description; // Tool description |
| 49 | mcp_tool_execute_t execute_callback; // Tool execution callback |
| 50 | mcp_tool_schema_t schema_callback; // Tool schema definition callback |
| 51 | |
| 52 | // UI/UX annotations |
| 53 | const char *title; // Human-readable title |
| 54 | bool read_only_hint; // If true, the tool doesn't modify a state |
| 55 | bool open_world_hint; // If true, the tool interacts with the external world |
| 56 | } MCP_TOOL_DEF; |
| 57 | |
| 58 | // Wrapper functions for unified list tools |
| 59 | static MCP_RETURN_CODE unified_list_tool_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id, const char *tool_name) { |
| 60 | const MCP_LIST_TOOL_CONFIG *config = mcp_get_list_tool_config(tool_name); |
| 61 | if (!config) { |
| 62 | buffer_sprintf(mcpc->error, "Unknown list tool: %s", tool_name); |
| 63 | return MCP_RC_INTERNAL_ERROR; |
| 64 | } |
| 65 | return mcp_unified_list_tool_execute(mcpc, config, params, id); |
| 66 | } |
| 67 | |
| 68 | static void unified_list_tool_schema(BUFFER *buffer, const char *tool_name) { |
| 69 | const MCP_LIST_TOOL_CONFIG *config = mcp_get_list_tool_config(tool_name); |
| 70 | if (!config) return; |
| 71 | mcp_unified_list_tool_schema(buffer, config); |
| 72 | } |
| 73 | |
| 74 | // Specific wrappers for each list tool |
| 75 | static MCP_RETURN_CODE list_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 76 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_LIST_METRICS); |
| 77 | } |
| 78 | static void list_metrics_schema(BUFFER *buffer) { |
| 79 | unified_list_tool_schema(buffer, MCP_TOOL_LIST_METRICS); |
| 80 | } |
| 81 | |
| 82 | static MCP_RETURN_CODE get_metrics_details_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 83 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_GET_METRICS_DETAILS); |
| 84 | } |
| 85 | static void get_metrics_details_schema(BUFFER *buffer) { |
| 86 | unified_list_tool_schema(buffer, MCP_TOOL_GET_METRICS_DETAILS); |
| 87 | } |
| 88 | |
| 89 | static MCP_RETURN_CODE list_nodes_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 90 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_LIST_NODES); |
| 91 | } |
| 92 | static void list_nodes_schema(BUFFER *buffer) { |
| 93 | unified_list_tool_schema(buffer, MCP_TOOL_LIST_NODES); |
| 94 | } |
| 95 | |
| 96 | static MCP_RETURN_CODE list_functions_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 97 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_LIST_FUNCTIONS); |
| 98 | } |
| 99 | static void list_functions_schema(BUFFER *buffer) { |
| 100 | unified_list_tool_schema(buffer, MCP_TOOL_LIST_FUNCTIONS); |
| 101 | } |
| 102 | |
| 103 | static MCP_RETURN_CODE get_nodes_details_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 104 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_GET_NODES_DETAILS); |
| 105 | } |
| 106 | static void get_nodes_details_schema(BUFFER *buffer) { |
| 107 | unified_list_tool_schema(buffer, MCP_TOOL_GET_NODES_DETAILS); |
| 108 | } |
| 109 | |
| 110 | static MCP_RETURN_CODE list_raised_alerts_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 111 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_LIST_RAISED_ALERTS); |
| 112 | } |
| 113 | static void list_raised_alerts_schema(BUFFER *buffer) { |
| 114 | unified_list_tool_schema(buffer, MCP_TOOL_LIST_RAISED_ALERTS); |
| 115 | } |
| 116 | |
| 117 | static MCP_RETURN_CODE list_all_alerts_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 118 | return unified_list_tool_execute(mcpc, params, id, MCP_TOOL_LIST_ALL_ALERTS); |
| 119 | } |
| 120 | static void list_all_alerts_schema(BUFFER *buffer) { |
| 121 | unified_list_tool_schema(buffer, MCP_TOOL_LIST_ALL_ALERTS); |
| 122 | } |
| 123 | |
| 124 | |
| 125 | // Static array of tool definitions |
| 126 | static const MCP_TOOL_DEF mcp_tools[] = { |
| 127 | // List tools (using unified implementation) |
| 128 | { |
| 129 | .name = MCP_TOOL_LIST_METRICS, |
| 130 | .title = "Discover available metrics", |
| 131 | .description = "Lists available metrics (contexts) with time-aware filtering. Returns metric names matching search patterns, filtered by nodes and time window. Supports full-text search across names, titles, instances, dimensions, and labels.\n", |
| 132 | .execute_callback = list_metrics_execute, |
| 133 | .schema_callback = list_metrics_schema, |
| 134 | .read_only_hint = true, |
| 135 | .open_world_hint = false |
| 136 | }, |
| 137 | { |
| 138 | .name = MCP_TOOL_GET_METRICS_DETAILS, |
| 139 | .title = "Get detailed information about specific metrics", |
| 140 | .description = "Gets comprehensive metadata for specific metrics. Returns titles, units, dimensions, instances, labels, and collection status.\n", |
| 141 | .execute_callback = get_metrics_details_execute, |
| 142 | .schema_callback = get_metrics_details_schema, |
| 143 | .read_only_hint = true, |
| 144 | .open_world_hint = false |
| 145 | }, |
| 146 | { |
| 147 | .name = MCP_TOOL_LIST_NODES, |
| 148 | .title = "List all monitored nodes in the Netdata ecosystem", |
| 149 | .description = "Lists all monitored nodes in the infrastructure. Returns node IDs, hostnames, connection status, and parent-child relationships. Use this to discover available nodes before querying metrics or executing functions.\n", |
| 150 | .execute_callback = list_nodes_execute, |
| 151 | .schema_callback = list_nodes_schema, |
| 152 | .read_only_hint = true, |
| 153 | .open_world_hint = false |
| 154 | }, |
| 155 | { |
| 156 | .name = MCP_TOOL_LIST_FUNCTIONS, |
| 157 | .title = "List available functions", |
| 158 | .description = "Lists all available Netdata functions that can be executed on nodes. Returns function names, descriptions, and execution requirements. Use this to discover what functions are available before executing them.\n", |
| 159 | .execute_callback = list_functions_execute, |
| 160 | .schema_callback = list_functions_schema, |
| 161 | .read_only_hint = true, |
| 162 | .open_world_hint = false |
| 163 | }, |
| 164 | { |
| 165 | .name = MCP_TOOL_GET_NODES_DETAILS, |
| 166 | .title = "Get detailed information about monitored nodes", |
| 167 | .description = "Gets comprehensive node information including hardware specs, OS details, capabilities, health status, available functions, and monitoring configuration. Essential for understanding node capabilities before executing functions.\n", |
| 168 | .execute_callback = get_nodes_details_execute, |
| 169 | .schema_callback = get_nodes_details_schema, |
| 170 | .read_only_hint = true, |
| 171 | .open_world_hint = false |
| 172 | }, |
| 173 | |
| 174 | // Non-list tools (keep their original implementation) |
| 175 | { |
| 176 | .name = MCP_TOOL_EXECUTE_FUNCTION, |
| 177 | .title = "Execute a function on a specific node", |
| 178 | .description = "Executes live data collection functions on nodes. Common functions: 'processes' (running processes), 'network-connections' (active connections), 'mount-points' (disk mounts), 'systemd-services' (service status). Returns tabular data with filtering and sorting options.\n", |
| 179 | .execute_callback = mcp_tool_execute_function_execute, |
| 180 | .schema_callback = mcp_tool_execute_function_schema, |
| 181 | .read_only_hint = true, // Currently read-only (will change when dynamic config is added) |
| 182 | .open_world_hint = true // Routes requests to remote nodes in the Netdata ecosystem |
| 183 | }, |
| 184 | { |
| 185 | .name = MCP_TOOL_QUERY_METRICS, |
| 186 | .title = "Query metrics data", |
| 187 | .description = "Queries time-series metrics data with powerful aggregation options. Specify context, time range, and grouping (by dimension, instance, node, or label). Returns data points with statistics and contribution analysis.\n", |
| 188 | .execute_callback = mcp_tool_query_metrics_execute, |
| 189 | .schema_callback = mcp_tool_query_metrics_schema, |
| 190 | .read_only_hint = true, |
| 191 | .open_world_hint = false |
| 192 | }, |
| 193 | |
| 194 | // Weights/correlation tools |
| 195 | { |
| 196 | .name = MCP_TOOL_FIND_CORRELATED_METRICS, |
| 197 | .title = "Find metrics that changed during an incident", |
| 198 | .description = "Finds metrics that changed significantly during an incident by comparing a problem time period with a normal baseline period. Essential for root cause analysis. IMPORTANT: For large infrastructures, use filters (metrics, nodes, instances, dimensions, or labels) to narrow the scope and avoid timeouts.", |
| 199 | .execute_callback = mcp_tool_find_correlated_metrics_execute, |
| 200 | .schema_callback = mcp_tool_find_correlated_metrics_schema, |
| 201 | .read_only_hint = true, |
| 202 | .open_world_hint = false |
| 203 | }, |
| 204 | { |
| 205 | .name = MCP_TOOL_FIND_ANOMALOUS_METRICS, |
| 206 | .title = "Find metrics with highest anomaly rates", |
| 207 | .description = "Finds metrics that were behaving anomalously according to Netdata's ML models. Returns metrics ranked by their anomaly rates (0 to 1, representing 0-100% of time anomalous). IMPORTANT: For large infrastructures, use filters (metrics, nodes, instances, dimensions, or labels) to narrow the scope and avoid timeouts.", |
| 208 | .execute_callback = mcp_tool_find_anomalous_metrics_execute, |
| 209 | .schema_callback = mcp_tool_find_anomalous_metrics_schema, |
| 210 | .read_only_hint = true, |
| 211 | .open_world_hint = false |
| 212 | }, |
| 213 | { |
| 214 | .name = MCP_TOOL_FIND_UNSTABLE_METRICS, |
| 215 | .title = "Find metrics with high variability", |
| 216 | .description = "Finds metrics with the highest variability using coefficient of variation (standard deviation as % of mean). Useful for identifying unstable or fluctuating metrics. IMPORTANT: For large infrastructures, use filters (metrics, nodes, instances, dimensions, or labels) to narrow the scope and avoid timeouts.", |
| 217 | .execute_callback = mcp_tool_find_unstable_metrics_execute, |
| 218 | .schema_callback = mcp_tool_find_unstable_metrics_schema, |
| 219 | .read_only_hint = true, |
| 220 | .open_world_hint = false |
| 221 | }, |
| 222 | |
| 223 | // Alert tools |
| 224 | { |
| 225 | .name = MCP_TOOL_LIST_RAISED_ALERTS, |
| 226 | .title = "List raised alerts", |
| 227 | .description = "List currently active alerts (WARNING and CRITICAL status) across all nodes", |
| 228 | .execute_callback = list_raised_alerts_execute, |
| 229 | .schema_callback = list_raised_alerts_schema, |
| 230 | .read_only_hint = true, |
| 231 | .open_world_hint = false |
| 232 | }, |
| 233 | { |
| 234 | .name = MCP_TOOL_LIST_ALL_ALERTS, |
| 235 | .title = "List all alerts", |
| 236 | .description = "List all alerts including cleared, undefined, and uninitialized alerts across all nodes", |
| 237 | .execute_callback = list_all_alerts_execute, |
| 238 | .schema_callback = list_all_alerts_schema, |
| 239 | .read_only_hint = true, |
| 240 | .open_world_hint = false |
| 241 | }, |
| 242 | { |
| 243 | .name = MCP_TOOL_LIST_ALERT_TRANSITIONS, |
| 244 | .title = "List alert transitions", |
| 245 | .description = "List recent alert state transitions showing how alerts changed over time", |
| 246 | .execute_callback = mcp_tool_list_alert_transitions_execute, |
| 247 | .schema_callback = mcp_tool_list_alert_transitions_schema, |
| 248 | .read_only_hint = true, |
| 249 | .open_world_hint = false |
| 250 | }, |
| 251 | |
| 252 | // Commented for the moment - probably dyncfg is a better way to do this |
| 253 | // { |
| 254 | // .name = MCP_TOOL_LIST_CONFIGURED_ALERTS, |
| 255 | // .title = "List configured alert prototypes", |
| 256 | // .description = "Lists all configured alert prototypes (templates) that define how alerts are created. Shows matching criteria, conditions, thresholds, and actions. Useful for understanding what Netdata is configured to monitor.", |
| 257 | // .execute_callback = mcp_tool_list_configured_alerts_execute, |
| 258 | // .schema_callback = mcp_tool_list_configured_alerts_schema, |
| 259 | // .read_only_hint = true, |
| 260 | // .open_world_hint = false |
| 261 | // }, |
| 262 | |
| 263 | // Add more tools here |
| 264 | |
| 265 | // Terminator |
| 266 | { |
| 267 | .name = NULL |
| 268 | } |
| 269 | }; |
| 270 | |
| 271 | // Return a list of available tools |
| 272 | static MCP_RETURN_CODE mcp_tools_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id __maybe_unused) { |
| 273 | if (!mcpc) |
| 274 | return MCP_RC_ERROR; |
| 275 | |
| 276 | // Initialize success response |
| 277 | mcp_init_success_result(mcpc, id); |
| 278 | |
| 279 | // Create tool-array |
| 280 | buffer_json_member_add_array(mcpc->result, "tools"); |
| 281 | |
| 282 | // Iterate through all defined tools and add them to the response |
| 283 | for (size_t i = 0; mcp_tools[i].name != NULL; i++) { |
| 284 | const MCP_TOOL_DEF *tool = &mcp_tools[i]; |
| 285 | |
| 286 | // Add a tool object |
| 287 | buffer_json_add_array_item_object(mcpc->result); |
| 288 | |
| 289 | // Add basic properties |
| 290 | buffer_json_member_add_string(mcpc->result, "name", tool->name); |
| 291 | buffer_json_member_add_string(mcpc->result, "description", tool->description); |
| 292 | |
| 293 | // Add schema using the tool's schema callback |
| 294 | tool->schema_callback(mcpc->result); |
| 295 | |
| 296 | // Add annotations if available |
| 297 | buffer_json_member_add_object(mcpc->result, "annotations"); |
| 298 | if (tool->title) { |
| 299 | buffer_json_member_add_string(mcpc->result, "title", tool->title); |
| 300 | } |
| 301 | buffer_json_member_add_boolean(mcpc->result, "readOnlyHint", tool->read_only_hint); |
| 302 | buffer_json_member_add_boolean(mcpc->result, "openWorldHint", tool->open_world_hint); |
| 303 | buffer_json_object_close(mcpc->result); // Close annotations |
| 304 | |
| 305 | buffer_json_object_close(mcpc->result); // Close tool object |
| 306 | } |
| 307 | |
| 308 | buffer_json_array_close(mcpc->result); // Close tools array |
| 309 | buffer_json_finalize(mcpc->result); // Finalize the JSON |
| 310 | |
| 311 | return MCP_RC_OK; |
| 312 | } |
| 313 | |
| 314 | // Main execute method that routes to specific tool handlers |
| 315 | static MCP_RETURN_CODE mcp_tools_method_call(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) { |
| 316 | if (!mcpc || !params) |
| 317 | return MCP_RC_ERROR; |
| 318 | |
| 319 | // Extract tool name |
| 320 | struct json_object *name_obj = NULL; |
| 321 | if (!json_object_object_get_ex(params, "name", &name_obj)) { |
| 322 | buffer_sprintf(mcpc->error, "Missing required parameter 'name'"); |
| 323 | return MCP_RC_BAD_REQUEST; |
| 324 | } |
| 325 | |
| 326 | if (!json_object_is_type(name_obj, json_type_string)) { |
| 327 | buffer_sprintf(mcpc->error, "Parameter 'name' must be a string"); |
| 328 | return MCP_RC_BAD_REQUEST; |
| 329 | } |
| 330 | |
| 331 | const char *tool_name = json_object_get_string(name_obj); |
| 332 | |
| 333 | // Get arguments if present |
| 334 | struct json_object *args_obj = NULL; |
| 335 | json_object_object_get_ex(params, "arguments", &args_obj); |
| 336 | |
| 337 | // Search for the tool in our static array |
| 338 | for (size_t i = 0; mcp_tools[i].name != NULL; i++) { |
| 339 | if (strcmp(tool_name, mcp_tools[i].name) == 0) { |
| 340 | // Found the tool, execute its callback |
| 341 | return mcp_tools[i].execute_callback(mcpc, args_obj, id); |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | // Tool not found |
| 346 | buffer_sprintf(mcpc->error, "Unknown tool: %s", tool_name); |
| 347 | return MCP_RC_BAD_REQUEST; |
| 348 | } |
| 349 | |
| 350 | // The MCP specification only defines list and call methods for tools |
| 351 | // Other methods are not part of the standard specification |
| 352 | |
| 353 | // Tools namespace method dispatcher (transport-agnostic) |
| 354 | MCP_RETURN_CODE mcp_tools_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id) { |
| 355 | if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR; |
| 356 | |
| 357 | netdata_log_debug(D_MCP, "MCP tools method: %s", method); |
| 358 | |
| 359 | MCP_RETURN_CODE rc; |
| 360 | |
| 361 | if (strcmp(method, "list") == 0) { |
| 362 | // List available tools - standard method in MCP specification |
| 363 | rc = mcp_tools_method_list(mcpc, params, id); |
| 364 | } |
| 365 | else if (strcmp(method, "call") == 0) { |
| 366 | // Execute a tool; standard method in MCP specification |
| 367 | rc = mcp_tools_method_call(mcpc, params, id); |
| 368 | } |
| 369 | else { |
| 370 | // Method not found in tools namespace |
| 371 | buffer_sprintf(mcpc->error, "Method 'tools/%s' not supported. The MCP specification only defines 'list' and 'call' methods.", method); |
| 372 | rc = MCP_RC_NOT_IMPLEMENTED; |
| 373 | } |
| 374 | |
| 375 | return rc; |
| 376 | } |