master
c 2,691 lines 113 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "mcp-tools-execute-function.h"
4 #include "mcp-tools-execute-function-internal.h"
5 #include "mcp-tools-execute-function-registry.h"
6 #include "mcp-params.h"
7 #include "database/rrdfunctions.h"
8
9 // Analyze the JSON response and determine its type
10 MCP_FUNCTION_TYPE mcp_functions_analyze_response(struct json_object *json_obj, int *out_status) {
11 if (!json_obj) return FN_TYPE_UNKNOWN;
12
13 struct json_object *type_obj = NULL;
14 struct json_object *has_history_obj = NULL;
15 struct json_object *status_obj = NULL;
16
17 // Type is required
18 if (!json_object_object_get_ex(json_obj, "type", &type_obj)) {
19 return FN_TYPE_NOT_TABLE;
20 }
21
22 const char *type = json_object_get_string(type_obj);
23 if (!type || strcmp(type, "table") != 0) {
24 return FN_TYPE_NOT_TABLE;
25 }
26
27 // has_history is optional - assume false if missing
28 bool has_history = false;
29 if (json_object_object_get_ex(json_obj, "has_history", &has_history_obj)) {
30 has_history = json_object_get_boolean(has_history_obj);
31 }
32
33 // Status is optional - assume 200 if missing
34 int status = 200;
35 if (json_object_object_get_ex(json_obj, "status", &status_obj)) {
36 status = json_object_get_int(status_obj);
37 }
38
39 if (out_status) *out_status = status;
40
41 // Return appropriate type based on has_history
42 if (has_history) {
43 return FN_TYPE_TABLE_WITH_HISTORY;
44 } else {
45 return FN_TYPE_TABLE;
46 }
47 }
48
49 // Helper function to create a filtered copy of a column definition
50 static struct json_object *create_filtered_column(struct json_object *col_obj, const char *col_id) {
51 struct json_object *col_copy = json_object_new_object();
52
53 // Handle type conversion for better LLM understanding
54 struct json_object *type_obj = NULL;
55 if (json_object_object_get_ex(col_obj, "type", &type_obj) &&
56 json_object_is_type(type_obj, json_type_string)) {
57 const char *type_str = json_object_get_string(type_obj);
58 RRDF_FIELD_TYPE field_type = RRDF_FIELD_TYPE_2id(type_str);
59
60 // Replace the original type with a simplified scalar type for LLM
61 json_object_object_add(col_copy, "type",
62 json_object_new_string(field_type_to_json_scalar_type(field_type)));
63 }
64
65 // Copy only necessary properties
66 struct json_object_iterator col_it = json_object_iter_begin(col_obj);
67 struct json_object_iterator col_itEnd = json_object_iter_end(col_obj);
68
69 while (!json_object_iter_equal(&col_it, &col_itEnd)) {
70 const char *field_key = json_object_iter_peek_name(&col_it);
71 struct json_object *field_val = json_object_iter_peek_value(&col_it);
72
73 // Skip properties we don't need for LLM
74 if (strcmp(field_key, "visible") != 0 &&
75 strcmp(field_key, "visualization") != 0 &&
76 strcmp(field_key, "value_options") != 0 &&
77 strcmp(field_key, "sort") != 0 &&
78 strcmp(field_key, "sortable") != 0 &&
79 strcmp(field_key, "sticky") != 0 &&
80 strcmp(field_key, "summary") != 0 &&
81 strcmp(field_key, "filter") != 0 &&
82 strcmp(field_key, "full_width") != 0 &&
83 strcmp(field_key, "wrap") != 0 &&
84 strcmp(field_key, "default_expanded_filter") != 0 &&
85 strcmp(field_key, "unique_key") != 0 &&
86 // Skip type as we've already handled it
87 strcmp(field_key, "type") != 0) {
88
89 // Skip "name" field if it's the same as the column id
90 if (strcmp(field_key, "name") == 0 && col_id) {
91 if (json_object_is_type(field_val, json_type_string)) {
92 const char *name_str = json_object_get_string(field_val);
93 if (name_str && strcmp(col_id, name_str) == 0) {
94 // Name is same as id, skip it
95 json_object_iter_next(&col_it);
96 continue;
97 }
98 }
99 }
100
101 json_object_object_add(col_copy, field_key, json_object_get(field_val));
102 }
103
104 json_object_iter_next(&col_it);
105 }
106
107 return col_copy;
108 }
109
110
111 // Structure to hold column type and transform information
112 typedef struct column_transform_info {
113 RRDF_FIELD_TYPE type;
114 RRDF_FIELD_TRANSFORM transform;
115 } COLUMN_TRANSFORM_INFO;
116
117 // Check if a column represents a timestamp
118 static inline bool is_transformable_timestamp(RRDF_FIELD_TYPE type, RRDF_FIELD_TRANSFORM transform) {
119 return (type == RRDF_FIELD_TYPE_TIMESTAMP ||
120 transform == RRDF_FIELD_TRANSFORM_DATETIME_MS ||
121 transform == RRDF_FIELD_TRANSFORM_DATETIME_USEC);
122 }
123
124 // Check if a column represents a duration
125 static inline bool is_transformable_duration(RRDF_FIELD_TYPE type, RRDF_FIELD_TRANSFORM transform) {
126 (void)transform; // unused for duration
127 return (type == RRDF_FIELD_TYPE_DURATION);
128 }
129
130 // Check if a column type/transform combination should be transformed to string
131 static inline bool is_transformable_to_string(RRDF_FIELD_TYPE type, RRDF_FIELD_TRANSFORM transform) {
132 return is_transformable_timestamp(type, transform) || is_transformable_duration(type, transform);
133 }
134
135 // Transform a value based on its type and transform settings
136 // Returns either a new object (caller owns) or NULL if no transformation needed
137 // IMPORTANT: When this returns a non-NULL value, it's a NEW object that the caller owns.
138 // When it returns NULL, the caller should use json_object_get() on the original value
139 // if they need to store it somewhere that takes ownership.
140 static struct json_object *transform_value_for_mcp(struct json_object *val, RRDF_FIELD_TYPE type, RRDF_FIELD_TRANSFORM transform) {
141 if (!val || json_object_is_type(val, json_type_null))
142 return NULL;
143
144 // If it's not an integer, no transformation possible
145 if (!json_object_is_type(val, json_type_int))
146 return NULL;
147
148 int64_t num_val = json_object_get_int64(val);
149
150 if (is_transformable_timestamp(type, transform)) {
151 // Convert to microseconds based on transform
152 usec_t usec_val;
153 if (transform == RRDF_FIELD_TRANSFORM_DATETIME_MS) {
154 usec_val = (usec_t)num_val * USEC_PER_MS;
155 } else if (transform == RRDF_FIELD_TRANSFORM_DATETIME_USEC) {
156 usec_val = (usec_t)num_val;
157 } else {
158 // Default: seconds
159 usec_val = (usec_t)num_val * USEC_PER_SEC;
160 }
161
162 // Format as RFC3339
163 char datetime_buf[RFC3339_MAX_LENGTH];
164 rfc3339_datetime_ut(datetime_buf, sizeof(datetime_buf), usec_val, 0, true);
165 return json_object_new_string(datetime_buf);
166 } else if (is_transformable_duration(type, transform)) {
167 // Duration is always in seconds
168 char duration_buf[256];
169 duration_snprintf_time_t(duration_buf, sizeof(duration_buf), (time_t)num_val);
170 return json_object_new_string(duration_buf);
171 }
172
173 // Value is 0 or negative, no transformation
174 return NULL;
175 }
176
177 // Extract column transform information from column definitions
178 static void extract_column_transforms(struct json_object *columns_obj,
179 const int *column_indices,
180 char **column_names,
181 size_t selected_count,
182 COLUMN_TRANSFORM_INFO *col_transforms) {
183 for (size_t i = 0; i < selected_count; i++) {
184 int col_idx = column_indices[i];
185 const char *col_name = column_names[col_idx];
186
187 struct json_object *col_obj = NULL;
188 if (json_object_object_get_ex(columns_obj, col_name, &col_obj)) {
189 // Get type
190 struct json_object *type_obj = NULL;
191 if (json_object_object_get_ex(col_obj, "type", &type_obj) &&
192 json_object_is_type(type_obj, json_type_string)) {
193 const char *type_str = json_object_get_string(type_obj);
194 col_transforms[i].type = RRDF_FIELD_TYPE_2id(type_str);
195 }
196
197 // Get transform from value_options
198 struct json_object *value_options_obj = NULL;
199 if (json_object_object_get_ex(col_obj, "value_options", &value_options_obj) &&
200 json_object_is_type(value_options_obj, json_type_object)) {
201 // value_options is an object, extract the "transform" field
202 struct json_object *transform_obj = NULL;
203 if (json_object_object_get_ex(value_options_obj, "transform", &transform_obj) &&
204 json_object_is_type(transform_obj, json_type_string)) {
205 const char *transform_str = json_object_get_string(transform_obj);
206 col_transforms[i].transform = RRDF_FIELD_TRANSFORM_2id(transform_str);
207 }
208 }
209 }
210 }
211 }
212
213 // Convert string operator to enum type
214 OPERATOR_TYPE mcp_functions_string_to_operator(const char *op_str) {
215 if (!op_str)
216 return OP_UNKNOWN;
217
218 if (strcmp(op_str, "==") == 0)
219 return OP_EQUALS;
220
221 if (strcmp(op_str, "!=") == 0 || strcmp(op_str, "<>") == 0)
222 return OP_NOT_EQUALS;
223
224 if (strcmp(op_str, "<") == 0)
225 return OP_LESS;
226
227 if (strcmp(op_str, "<=") == 0)
228 return OP_LESS_EQUALS;
229
230 if (strcmp(op_str, ">") == 0)
231 return OP_GREATER;
232
233 if (strcmp(op_str, ">=") == 0)
234 return OP_GREATER_EQUALS;
235
236 if (strcmp(op_str, "match") == 0 || strcmp(op_str, "like") == 0 || strcmp(op_str, "in") == 0)
237 return OP_MATCH;
238
239 if (strcmp(op_str, "not match") == 0 || strcmp(op_str, "not like") == 0 || strcmp(op_str, "not in") == 0)
240 return OP_NOT_MATCH;
241
242 return OP_UNKNOWN;
243 }
244
245 // Free patterns in the condition array
246 void mcp_functions_free_condition_patterns(CONDITION_ARRAY *condition_array) {
247 if (!condition_array)
248 return;
249
250 for (size_t i = 0; i < condition_array->count; i++) {
251 if (condition_array->items[i].pattern)
252 simple_pattern_free(condition_array->items[i].pattern);
253 }
254 }
255
256 // Check if a single value matches a condition
257 static bool value_matches_condition(struct json_object *value, const CONDITION *condition)
258 {
259 if (!value || !condition)
260 return false;
261
262 // Handle NULL values
263 if (json_object_is_type(value, json_type_null)) {
264 if (condition->v_type == COND_VALUE_NULL) {
265 return (condition->op == OP_EQUALS);
266 } else {
267 return (condition->op == OP_NOT_EQUALS);
268 }
269 }
270
271 // Handle MATCH and NOT MATCH operators (pattern matching) - always convert to strings
272 if (condition->op == OP_MATCH || condition->op == OP_NOT_MATCH) {
273 if (condition->pattern) {
274 const char *val_str = json_object_get_string(value);
275 bool pattern_match = simple_pattern_matches(condition->pattern, val_str);
276 return (condition->op == OP_MATCH) ? pattern_match : !pattern_match;
277 }
278 return false;
279 }
280
281 // Handle comparisons based on condition value type
282 if (condition->v_type == COND_VALUE_NUMBER) {
283 // Try to get numeric value from JSON
284 double val_num = 0.0;
285 if (json_object_is_type(value, json_type_int) || json_object_is_type(value, json_type_double)) {
286 val_num = json_object_get_double(value);
287 } else if (json_object_is_type(value, json_type_string)) {
288 // Try to parse string as number
289 char *endptr;
290 const char *str = json_object_get_string(value);
291 if (!str) {
292 // NULL string cannot be converted to number, fall back to string comparison
293 goto string_compare;
294 }
295 val_num = strtod(str, &endptr);
296 if (endptr == str || *endptr != '\0') {
297 // Not a valid number, do string comparison
298 goto string_compare;
299 }
300 } else {
301 // Can't convert to number, treat as not equal
302 return (condition->op == OP_NOT_EQUALS);
303 }
304
305 switch (condition->op) {
306 case OP_EQUALS:
307 return (val_num == condition->v_num);
308 case OP_NOT_EQUALS:
309 return (val_num != condition->v_num);
310 case OP_LESS:
311 return (val_num < condition->v_num);
312 case OP_LESS_EQUALS:
313 return (val_num <= condition->v_num);
314 case OP_GREATER:
315 return (val_num > condition->v_num);
316 case OP_GREATER_EQUALS:
317 return (val_num >= condition->v_num);
318 default:
319 return false;
320 }
321 } else if (condition->v_type == COND_VALUE_BOOLEAN) {
322 bool val_bool = json_object_get_boolean(value);
323
324 switch (condition->op) {
325 case OP_EQUALS:
326 return (val_bool == condition->v_bool);
327 case OP_NOT_EQUALS:
328 return (val_bool != condition->v_bool);
329 default:
330 // Boolean doesn't support ordering comparisons
331 return false;
332 }
333 }
334
335 string_compare:
336 {
337 // String comparisons (including when condition is string or as fallback)
338 const char *val_str = json_object_get_string(value);
339 const char *cond_str = (condition->v_type == COND_VALUE_STRING) ? condition->v_str : NULL;
340
341 // Handle NULL condition string
342 if (!cond_str) {
343 if (condition->v_type == COND_VALUE_NULL) {
344 return (condition->op == OP_EQUALS && !val_str);
345 }
346 // Use empty string for comparison
347 cond_str = "";
348 }
349
350 if (!val_str)
351 val_str = "";
352
353 int cmp = strcmp(val_str, cond_str);
354
355 switch (condition->op) {
356 case OP_EQUALS:
357 return (cmp == 0);
358 case OP_NOT_EQUALS:
359 return (cmp != 0);
360 case OP_LESS:
361 return (cmp < 0);
362 case OP_LESS_EQUALS:
363 return (cmp <= 0);
364 case OP_GREATER:
365 return (cmp > 0);
366 case OP_GREATER_EQUALS:
367 return (cmp >= 0);
368 default:
369 return false;
370 }
371 }
372 }
373
374 // Check if a row matches all the conditions
375 static bool row_matches_conditions(struct json_object *row, const CONDITION_ARRAY *conditions) {
376 if (!conditions || conditions->count == 0)
377 return true; // No conditions means everything matches
378
379 for (size_t i = 0; i < conditions->count; i++) {
380 const CONDITION *current = &conditions->items[i];
381 bool condition_match = false;
382
383 // Special case: column_index == -1 means search all columns
384 if (current->column_index == -1) {
385 // Search across all columns for a match
386 size_t row_length = json_object_array_length(row);
387 for (size_t col_idx = 0; col_idx < row_length; col_idx++) {
388 struct json_object *row_val = json_object_array_get_idx(row, col_idx);
389 if (!row_val) continue;
390
391 // Check if this column value matches the condition
392 if (value_matches_condition(row_val, current)) {
393 condition_match = true;
394 break; // Found a match, no need to check other columns
395 }
396 }
397 }
398 else {
399 // Normal case: specific column index
400 struct json_object *row_val = json_object_array_get_idx(row, current->column_index);
401
402 // Handle null values
403 if (!row_val) {
404 return false;
405 }
406
407 condition_match = value_matches_condition(row_val, current);
408 }
409
410 // If any condition doesn't match, return false
411 if (!condition_match) {
412 return false;
413 }
414 }
415
416 // All conditions matched
417 return true;
418 }
419
420 // Resolve column indices for conditions that were parsed during request parsing
421 static void resolve_condition_columns(CONDITION_ARRAY *condition_array, struct json_object *columns_obj) {
422 if (!condition_array || !columns_obj || condition_array->count == 0)
423 return;
424
425 condition_array->has_missing_columns = false;
426
427 for (size_t i = 0; i < condition_array->count; i++) {
428 CONDITION *curr = &condition_array->items[i];
429
430 // Check for special column names that mean "search all columns"
431 bool is_all_columns = (strcmp(curr->column_name, "*") == 0 || strcmp(curr->column_name, "") == 0);
432
433 // Find column in column definitions
434 struct json_object *col_obj = NULL;
435 if (is_all_columns || !json_object_object_get_ex(columns_obj, curr->column_name, &col_obj)) {
436 // Column not found or explicitly all columns - mark it as a wildcard search (use -1 as special index)
437 curr->column_index = -1;
438
439 // Only report as missing if it's not a special "all columns" indicator
440 if (!is_all_columns) {
441 condition_array->has_missing_columns = true;
442 }
443 } else {
444 // Get column index
445 struct json_object *index_obj = NULL;
446 if (json_object_object_get_ex(col_obj, "index", &index_obj)) {
447 curr->column_index = json_object_get_int(index_obj);
448 } else {
449 // Column found but no index - treat as missing
450 curr->column_index = -1;
451 condition_array->has_missing_columns = true;
452 }
453 }
454 }
455 }
456
457 // Flags to indicate what additional content should be added for errors
458 typedef enum {
459 MCP_TABLE_ADD_NOTHING = 0,
460 MCP_TABLE_ADD_COLUMNS = 1 << 0,
461 MCP_TABLE_ADD_RAW_DATA = 1 << 1,
462 MCP_TABLE_ADD_FILTERING_INSTRUCTIONS = 1 << 2
463 } MCP_TABLE_ADDITIONAL_CONTENT;
464
465 // Forward declaration
466 static usec_t calculate_next_cursor_from_input(MCP_FUNCTION_DATA *data);
467
468 // Initialize MCP_FUNCTION_DATA structure
469 void mcp_functions_data_init(MCP_FUNCTION_DATA *data) {
470 memset(data, 0, sizeof(MCP_FUNCTION_DATA));
471 data->output.result = buffer_create(0, NULL);
472 }
473
474 // Clean up MCP_FUNCTION_DATA structure
475 void mcp_functions_data_cleanup(MCP_FUNCTION_DATA *data) {
476 if (!data) return;
477
478 // Free the parsed JSON object
479 if (data->input.jobj) {
480 json_object_put(data->input.jobj);
481 data->input.jobj = NULL;
482 }
483
484 // Free the output result buffer
485 if (data->output.result) {
486 buffer_free(data->output.result);
487 data->output.result = NULL;
488 }
489
490 // Free condition patterns
491 mcp_functions_free_condition_patterns(&data->request.conditions);
492
493 // Free pagination column string
494 string_freez(data->pagination.column);
495
496 // Free the input JSON buffer
497 if (data->input.json) {
498 buffer_free(data->input.json);
499 data->input.json = NULL;
500 }
501 }
502
503 // Reset output for reprocessing
504 static void mcp_function_data_reset_output(MCP_FUNCTION_DATA *data) {
505 buffer_flush(data->output.result);
506 data->output.status = MCP_TABLE_OK;
507 data->output.rows = 0;
508 data->output.columns = 0;
509 }
510
511 // Helper to create a filtered columns object for error messages
512 static struct json_object *create_filtered_columns_for_errors(struct json_object *columns_obj) {
513 if (!columns_obj) return NULL;
514
515 struct json_object *filtered = json_object_new_object();
516 struct json_object_iterator it = json_object_iter_begin(columns_obj);
517 struct json_object_iterator itEnd = json_object_iter_end(columns_obj);
518
519 while (!json_object_iter_equal(&it, &itEnd)) {
520 const char *col_name = json_object_iter_peek_name(&it);
521 struct json_object *col_obj = json_object_iter_peek_value(&it);
522
523 if (col_obj) {
524 struct json_object *filtered_col = create_filtered_column(col_obj, col_name);
525 json_object_object_add(filtered, col_name, filtered_col);
526 }
527
528 json_object_iter_next(&it);
529 }
530
531 return filtered;
532 }
533
534 // Helper function to generate comprehensive error messages for LLMs
535 static MCP_TABLE_ADDITIONAL_CONTENT generate_table_error_message(MCP_FUNCTION_DATA *data) {
536 buffer_flush(data->request.mcpc->error);
537 MCP_TABLE_ADDITIONAL_CONTENT additional_content = MCP_TABLE_ADD_NOTHING;
538
539 switch (data->output.status) {
540 case MCP_TABLE_ERROR_INVALID_CONDITIONS:
541 buffer_sprintf(data->request.mcpc->error,
542 "Error processing conditions: %s\n\n"
543 "Conditions should be formatted as:\n"
544 "```json\n"
545 "\"conditions\": [\n"
546 " [\"column_name\", \"operator\", value],\n"
547 " [\"another_column\", \"another_operator\", another_value]\n"
548 "]\n"
549 "```",
550 buffer_tostring(data->output.result)
551 );
552 additional_content = MCP_TABLE_ADD_FILTERING_INSTRUCTIONS;
553 break;
554
555 case MCP_TABLE_ERROR_NO_MATCHES_WITH_MISSING_COLUMNS:
556 buffer_strcat(data->request.mcpc->error,
557 "No rows matched the specified conditions.\n\n"
558 "Note: Some columns were not found, so a full-text search was performed across all columns, but no matches were found."
559 );
560 additional_content = MCP_TABLE_ADD_COLUMNS | MCP_TABLE_ADD_FILTERING_INSTRUCTIONS;
561 break;
562
563 case MCP_TABLE_ERROR_NO_MATCHES:
564 buffer_strcat(data->request.mcpc->error,
565 "No results match the specified conditions.\n\n"
566 "Tips:\n"
567 "• Verify the column names in your conditions\n"
568 "• Check the values and operators used\n"
569 "• For 'match' operators, ensure your pattern format is correct\n"
570 "• To match multiple values, use 'match' with patterns separated by the pipe (|) character: '*value1*|*value2*'\n"
571 "• Try broadening your filter criteria"
572 );
573 additional_content = MCP_TABLE_ADD_COLUMNS | MCP_TABLE_ADD_FILTERING_INSTRUCTIONS;
574 break;
575
576 case MCP_TABLE_ERROR_INVALID_SORT_ORDER:
577 buffer_sprintf(data->request.mcpc->error,
578 "Invalid sort_order: '%s'. Valid options are 'asc' (ascending) or 'desc' (descending).\n\n"
579 "Example:\n"
580 "```json\n"
581 "\"sort_order\": \"desc\"\n"
582 "```",
583 buffer_tostring(data->output.result)
584 );
585 additional_content = MCP_TABLE_ADD_NOTHING;
586 break;
587
588 case MCP_TABLE_ERROR_COLUMNS_NOT_FOUND:
589 buffer_sprintf(data->request.mcpc->error,
590 "Column(s) not found: %s",
591 buffer_tostring(data->output.result)
592 );
593 additional_content = MCP_TABLE_ADD_COLUMNS;
594 break;
595
596 case MCP_TABLE_ERROR_SORT_COLUMN_NOT_FOUND:
597 buffer_sprintf(data->request.mcpc->error,
598 "Sort column '%s' not found.",
599 buffer_tostring(data->output.result)
600 );
601 additional_content = MCP_TABLE_ADD_COLUMNS;
602 break;
603
604 case MCP_TABLE_ERROR_TOO_MANY_COLUMNS:
605 buffer_sprintf(data->request.mcpc->error,
606 "Error: Table has %zu columns, which exceeds the maximum supported (%d). Showing raw output.",
607 data->input.columns, MAX_COLUMNS
608 );
609 additional_content = MCP_TABLE_ADD_RAW_DATA;
610 break;
611
612 case MCP_TABLE_NOT_JSON:
613 buffer_strcat(data->request.mcpc->error,
614 "This response is not valid JSON. Showing raw output.");
615 additional_content = MCP_TABLE_ADD_RAW_DATA;
616 break;
617
618 case MCP_TABLE_NOT_PROCESSABLE:
619 buffer_strcat(data->request.mcpc->error,
620 "The function returned JSON but it's not a table format we can filter. Showing raw output.");
621 additional_content = MCP_TABLE_ADD_RAW_DATA;
622 break;
623
624 case MCP_TABLE_EMPTY_RESULT: {
625 buffer_strcat(data->request.mcpc->error,
626 "The function returned an empty result (no rows).");
627
628 // Add contextual tips based on what parameters were used
629 bool has_history = (data->input.type == FN_TYPE_TABLE_WITH_HISTORY);
630 bool has_query = (data->request.query && *data->request.query);
631 bool has_conditions = (data->request.conditions.count > (has_query ? 1 : 0));
632
633 if(has_history || has_query || has_conditions) {
634 buffer_strcat(data->request.mcpc->error, "\n\nTips:");
635 }
636
637 if (has_history)
638 buffer_strcat(data->request.mcpc->error, "\n• Expand the search time window by adjusting 'after' and 'before' parameters");
639
640 if (has_query)
641 buffer_strcat(data->request.mcpc->error,
642 "\n• Review the search query terms in 'q' parameter"
643 "\n - Use wildcards: '*error*', '*warning*', '*fail*'"
644 "\n - Combine terms: '*systemd*|*kernel*', '*eth0*|*eth1*'");
645
646 if (has_conditions) {
647 buffer_strcat(data->request.mcpc->error,
648 "\n• Review the conditions - ensure column names and values match");
649 }
650
651 additional_content = MCP_TABLE_ADD_COLUMNS;
652 break;
653 }
654
655 case MCP_TABLE_INFO_MISSING_COLUMNS_FOUND_RESULTS:
656 buffer_strcat(data->request.mcpc->error,
657 "Note: Not all columns in the conditions were found, so a full-text search was performed across all columns, and matching results were found.");
658 additional_content = MCP_TABLE_ADD_NOTHING;
659 break;
660
661 case MCP_TABLE_RESPONSE_TOO_BIG:
662 buffer_sprintf(data->request.mcpc->error,
663 "The response is too big, having %zu rows and %zu columns. Limiting to 1 row for readability.",
664 data->input.rows, data->input.columns
665 );
666 additional_content = MCP_TABLE_ADD_FILTERING_INSTRUCTIONS;
667 break;
668
669 default:
670 additional_content = MCP_TABLE_ADD_NOTHING;
671 break;
672 }
673
674 return additional_content;
675 }
676
677 // Helper to add filtering instructions as a separate content entry
678 static void add_filtering_instructions_to_mcp_result(MCP_CLIENT *mcpc, bool has_history) {
679 buffer_json_add_array_item_object(mcpc->result);
680 {
681 buffer_json_member_add_string(mcpc->result, "type", "text");
682
683 if (has_history) {
684 // Instructions for history/logs functions (limited capabilities)
685 buffer_json_member_add_string(mcpc->result, "text",
686 "FILTERING INSTRUCTIONS:\n"
687 "• **columns**: Select specific columns to reduce width (e.g., [\"Column1\", \"Column2\", \"Column3\"])\n"
688 "• **conditions**: Filter rows using exact matches and value sets:\n"
689 " - Single value: [\"column\", \"==\", \"exact_value\"]\n"
690 " - Multiple values: [\"column\", \"match\", \"value1|value2|value3\"] (values are OR'd)\n"
691 "• **limit**: Control number of rows returned (e.g., 10)\n"
692 "• **q**: Full-text search across all columns (simple patterns like \"*term1*|*term2*\")\n"
693 "• **direction**: Controls time-based sorting (\"forward\" or \"backward\")\n"
694 "\n"
695 "Example filtering:\n"
696 "```json\n"
697 "{\n"
698 " \"columns\": [\"MESSAGE\", \"PRIORITY\", \"_HOSTNAME\"],\n"
699 " \"conditions\": [\n"
700 " [\"PRIORITY\", \"match\", \"1|2|3\"],\n"
701 " [\"_HOSTNAME\", \"=\", \"server1\"]\n"
702 " ],\n"
703 " \"q\": \"*systemd*|*logind*|*dbus*\",\n"
704 " \"direction\": \"backward\",\n"
705 " \"limit\": 20\n"
706 "}\n"
707 "```\n"
708 "\n"
709 "Valid operators for history functions: == (exact match), match (value set with | separator)\n"
710 "Invalid operators: !=, <>, not match, <, <=, >, >=\n"
711 "Full-text search: Use 'q' parameter with wildcards like '*pattern1*|*pattern2*' to search all columns\n"
712 "Sorting: Use 'direction' parameter only - column sorting is not supported for history functions"
713 );
714 } else {
715 // Instructions for regular functions (full capabilities)
716 buffer_json_member_add_string(mcpc->result, "text",
717 "FILTERING INSTRUCTIONS:\n"
718 "• **columns**: Select specific columns to reduce width (e.g., [\"Column1\", \"Column2\", \"Column3\"])\n"
719 "• **conditions**: Filter rows using [ [column1, operator1, value1], [column2, operator2, value2], ... ]\n"
720 "• **limit**: Control number of rows returned (e.g., 10)\n"
721 "• **q**: Full-text search across all columns (supports wildcards like \"*term1*|*term2*\")\n"
722 "• **sort_column** + **sort_order**: Order results by a column ('asc' or 'desc')\n"
723 "\n"
724 "Example filtering:\n"
725 "```json\n"
726 "{\n"
727 " \"columns\": [\"CmdLine\", \"CPU\", \"Memory\", \"Status\"],\n"
728 " \"conditions\": [\n"
729 " [\"Memory\", \">\", 1.0],\n"
730 " [\"CmdLine\", \"match\", \"*systemd*|*postgresql*|*docker*\"],\n"
731 " ],\n"
732 " \"sort_column\": \"CPU\",\n"
733 " \"sort_order\": \"desc\",\n"
734 " \"limit\": 10\n"
735 "}\n"
736 "```\n"
737 "\n"
738 "Operators: ==, !=, <, <=, >, >=, match (simple pattern), not match (simple pattern)\n"
739 "Simple patterns: '*this*|*that*|*other*' (wildcard search to find strings that include 'this', or 'that', or 'other')\n"
740 );
741 }
742 }
743 buffer_json_object_close(mcpc->result);
744 }
745
746 // Helper to add columns info as a separate content entry
747 static void add_columns_info_to_mcp_result(MCP_CLIENT *mcpc, struct json_object *columns_obj) {
748 if (!columns_obj) return;
749
750 struct json_object *filtered_columns = create_filtered_columns_for_errors(columns_obj);
751 if (filtered_columns) {
752 // Create wrapper object
753 struct json_object *wrapper = json_object_new_object();
754 json_object_object_add(wrapper, "available_columns", filtered_columns);
755
756 const char *columns_json = json_object_to_json_string_ext(wrapper, JSON_C_TO_STRING_PRETTY);
757
758 buffer_json_add_array_item_object(mcpc->result);
759 {
760 buffer_json_member_add_string(mcpc->result, "type", "text");
761 buffer_json_member_add_string(mcpc->result, "text", columns_json);
762 }
763 buffer_json_object_close(mcpc->result);
764
765 json_object_put(wrapper);
766 }
767 }
768
769 // Helper to add messages to MCP result based on table result status
770 static void add_table_messages_to_mcp_result(MCP_FUNCTION_DATA *data,
771 struct json_object *columns_obj) {
772 // Generate the appropriate error message and get additional content flags
773 MCP_TABLE_ADDITIONAL_CONTENT additional_content = generate_table_error_message(data);
774
775 // Add the message if there's an error or guidance
776 if (data->output.status != MCP_TABLE_OK && buffer_strlen(data->request.mcpc->error) > 0) {
777 buffer_json_add_array_item_object(data->request.mcpc->result);
778 {
779 buffer_json_member_add_string(data->request.mcpc->result, "type", "text");
780 buffer_json_member_add_string(data->request.mcpc->result, "text", buffer_tostring(data->request.mcpc->error));
781 }
782 buffer_json_object_close(data->request.mcpc->result);
783 }
784
785 // Add columns info if requested
786 if ((additional_content & MCP_TABLE_ADD_COLUMNS) && columns_obj) {
787 add_columns_info_to_mcp_result(data->request.mcpc, columns_obj);
788 }
789
790 // Add filtering instructions if requested
791 if (additional_content & MCP_TABLE_ADD_FILTERING_INSTRUCTIONS) {
792 bool has_history = (data->input.type == FN_TYPE_TABLE_WITH_HISTORY);
793 add_filtering_instructions_to_mcp_result(data->request.mcpc, has_history);
794 }
795
796 // Add raw data if requested
797 if ((additional_content & MCP_TABLE_ADD_RAW_DATA) &&
798 buffer_strlen(data->output.result) > 0) {
799 buffer_json_add_array_item_object(data->request.mcpc->result);
800 {
801 buffer_json_member_add_string(data->request.mcpc->result, "type", "text");
802 buffer_json_member_add_string(data->request.mcpc->result, "text", buffer_tostring(data->output.result));
803 }
804 buffer_json_object_close(data->request.mcpc->result);
805 }
806 }
807
808 void mcp_tool_execute_function_schema(BUFFER *buffer) {
809 // Tool input schema
810 buffer_json_member_add_object(buffer, "inputSchema");
811 buffer_json_member_add_string(buffer, "type", "object");
812 buffer_json_member_add_string(
813 buffer, "title",
814 "Execute a function on a specific node. "
815 "Functions provide live information and they are automatically routed "
816 "and executed to Netdata running on the given node.");
817
818 // Properties
819 buffer_json_member_add_object(buffer, "properties");
820
821 buffer_json_member_add_object(buffer, "node");
822 {
823 buffer_json_member_add_string(buffer, "type", "string");
824 buffer_json_member_add_string(buffer, "title", "The node on which to execute the function");
825 buffer_json_member_add_string(
826 buffer, "description",
827 "The hostname or machine_guid or node_id of the node where the function should be executed. "
828 "The node needs to be online (live) and reachable.");
829 }
830 buffer_json_object_close(buffer); // node
831
832 buffer_json_member_add_object(buffer, "function");
833 {
834 buffer_json_member_add_string(buffer, "type", "string");
835 buffer_json_member_add_string(buffer, "title", "The name of the function to execute.");
836 buffer_json_member_add_string(buffer, "description", "The function name, as available in the node_details tool output");
837 }
838 buffer_json_object_close(buffer); // function
839
840 mcp_schema_add_timeout(
841 buffer, "timeout", "Execution timeout in seconds",
842 "Maximum time to wait for function execution (default: 60)",
843 60, 1, 3600, false);
844
845 buffer_json_member_add_object(buffer, "columns");
846 {
847 buffer_json_member_add_string(buffer, "type", "array");
848 buffer_json_member_add_string(buffer, "title", "Columns to include");
849 buffer_json_member_add_string(
850 buffer, "description",
851 "Array of column names to include in the result. "
852 "Each function has its own columns, so first check the function without this parameter.");
853
854 buffer_json_member_add_object(buffer, "items");
855 {
856 buffer_json_member_add_string(buffer, "type", "string");
857 }
858 buffer_json_object_close(buffer); // items
859 }
860 buffer_json_object_close(buffer); // columns
861
862 mcp_schema_add_string_param(
863 buffer, "sort_column", "Column to sort by",
864 "Name of the column to sort the results by.",
865 NULL, false);
866
867 buffer_json_member_add_object(buffer, "sort_order");
868 {
869 buffer_json_member_add_string(buffer, "type", "string");
870 buffer_json_member_add_string(buffer, "title", "Sort order");
871 buffer_json_member_add_string(
872 buffer, "description",
873 "Order to sort results: 'asc' for ascending, 'desc' for descending");
874 buffer_json_member_add_string(buffer, "default", "desc");
875 buffer_json_member_add_array(buffer, "enum");
876 buffer_json_add_array_item_string(buffer, "asc");
877 buffer_json_add_array_item_string(buffer, "desc");
878 buffer_json_array_close(buffer);
879 }
880 buffer_json_object_close(buffer); // sort_order
881
882 mcp_schema_add_size_param(
883 buffer, "limit", "Limit",
884 "Number of entries to return",
885 0, 0, SIZE_MAX, false);
886
887 // Time-based parameters for functions with history
888 mcp_schema_add_time_params(buffer, "query window", false);
889
890 mcp_schema_add_string_param(
891 buffer, "cursor", "Pagination cursor",
892 "Opaque cursor for pagination (follows MCP standard)",
893 NULL, false);
894
895 buffer_json_member_add_object(buffer, "direction");
896 {
897 buffer_json_member_add_string(buffer, "type", "string");
898 buffer_json_member_add_string(buffer, "title", "Query direction");
899 buffer_json_member_add_string(
900 buffer, "description",
901 "Direction for query processing: 'forward' (oldest first) or 'backward' (newest first)");
902 buffer_json_member_add_string(buffer, "default", "backward");
903 buffer_json_member_add_array(buffer, "enum");
904 buffer_json_add_array_item_string(buffer, "forward");
905 buffer_json_add_array_item_string(buffer, "backward");
906 buffer_json_array_close(buffer);
907 }
908 buffer_json_object_close(buffer); // direction
909
910 mcp_schema_add_string_param(
911 buffer, "q", "Full-text search",
912 "Full-text search to filter results. Use pipe character (|) to separate multiple search patterns. "
913 "Example: '*fail*|*error*|*systemd*'. "
914 "Wildcards (*) are supported for pattern matching.",
915 NULL, false);
916
917 buffer_json_member_add_object(buffer, "conditions");
918 {
919 buffer_json_member_add_string(buffer, "type", "array");
920 buffer_json_member_add_string(buffer, "title", "Filter conditions");
921 buffer_json_member_add_string(
922 buffer, "description",
923 "Array of conditions to filter rows. "
924 "Each condition is an array of [column, operator, value] where operator "
925 "can be ==, !=, <>, <, <=, >, >=, match, not match. "
926 "Use '*' or '' (empty string) as column name to search across all columns.");
927
928 buffer_json_member_add_object(buffer, "items");
929 {
930 buffer_json_member_add_string(buffer, "type", "array");
931 buffer_json_member_add_object(buffer, "items");
932 {
933 buffer_json_member_add_array(buffer, "oneOf");
934 {
935 // First item of the condition array - column name
936 buffer_json_add_array_item_object(buffer);
937 {
938 buffer_json_member_add_string(buffer, "type", "string");
939 }
940 buffer_json_object_close(buffer);
941
942 // Second item - operator
943 buffer_json_add_array_item_object(buffer);
944 {
945 buffer_json_member_add_string(buffer, "type", "string");
946 buffer_json_member_add_array(buffer, "enum");
947 {
948 buffer_json_add_array_item_string(buffer, "==");
949 buffer_json_add_array_item_string(buffer, "!=");
950 buffer_json_add_array_item_string(buffer, "<>");
951 buffer_json_add_array_item_string(buffer, "<");
952 buffer_json_add_array_item_string(buffer, "<=");
953 buffer_json_add_array_item_string(buffer, ">");
954 buffer_json_add_array_item_string(buffer, ">=");
955 buffer_json_add_array_item_string(buffer, "match");
956 buffer_json_add_array_item_string(buffer, "not match");
957 }
958 buffer_json_array_close(buffer);
959 }
960 buffer_json_object_close(buffer);
961
962 // Third item - value (can be string, number, or boolean)
963 buffer_json_add_array_item_object(buffer);
964 {
965 buffer_json_member_add_array(buffer, "oneOf");
966 {
967 buffer_json_add_array_item_object(buffer);
968 {
969 buffer_json_member_add_string(buffer, "type", "string");
970 }
971 buffer_json_object_close(buffer);
972
973 buffer_json_add_array_item_object(buffer);
974 {
975 buffer_json_member_add_string(buffer, "type", "number");
976 }
977 buffer_json_object_close(buffer);
978
979 buffer_json_add_array_item_object(buffer);
980 {
981 buffer_json_member_add_string(buffer, "type", "boolean");
982 }
983 buffer_json_object_close(buffer);
984 }
985 buffer_json_array_close(buffer);
986 }
987 buffer_json_object_close(buffer); // third item
988 }
989 buffer_json_array_close(buffer); // oneOf
990 }
991 buffer_json_object_close(buffer); // inner items
992 }
993 buffer_json_object_close(buffer); // items
994 }
995 buffer_json_object_close(buffer); // conditions
996
997 buffer_json_member_add_object(buffer, "selections");
998 {
999 buffer_json_member_add_string(buffer, "type", "object");
1000 buffer_json_member_add_string(buffer, "title", "Function parameter selections");
1001 buffer_json_member_add_string(
1002 buffer, "description",
1003 "Key-value pairs where each key is a parameter name and the value depends on the parameter type: "
1004 "for 'select' type parameters, use a single string value; "
1005 "for 'multiselect' type parameters, use an array of strings. "
1006 "Functions that require selections will prompt you with available options when called without this parameter. "
1007 "Example: {\"param1\": \"single_value\", \"param2\": [\"value1\", \"value2\"]}");
1008 }
1009 buffer_json_object_close(buffer); // selections
1010
1011 buffer_json_object_close(buffer); // properties
1012
1013 // Required fields
1014 buffer_json_member_add_array(buffer, "required");
1015 buffer_json_add_array_item_string(buffer, "node");
1016 buffer_json_add_array_item_string(buffer, "function");
1017 buffer_json_array_close(buffer); // required
1018
1019 buffer_json_object_close(buffer); // inputSchema
1020 }
1021
1022 /**
1023 * Filter and sort a table-formatted JSON result based on parameters
1024 *
1025 * @param data Function data structure with request context, input and output
1026 * @param max_size_threshold Maximum size in bytes before truncation is recommended
1027 *
1028 * @return void (result is returned in data->output)
1029 */
1030 static void mcp_process_table_result(MCP_FUNCTION_DATA *data, size_t max_size_threshold)
1031 {
1032 // Reset output for processing
1033 mcp_function_data_reset_output(data);
1034
1035 // Clear error buffer for any error messages that might be generated
1036 buffer_flush(data->request.mcpc->error);
1037
1038 // Get original JSON string from input
1039 const char *json_str = buffer_tostring(data->input.json);
1040 size_t result_size = buffer_strlen(data->input.json);
1041
1042 if (!data->input.jobj) {
1043 buffer_strcat(data->output.result, json_str); // Return original if JSON is NULL
1044 data->output.status = MCP_TABLE_NOT_JSON;
1045 return;
1046 }
1047
1048 // Check if it's a processable table format
1049 if (data->input.type != FN_TYPE_TABLE && data->input.type != FN_TYPE_TABLE_WITH_HISTORY) {
1050 buffer_strcat(data->output.result, json_str); // Not a processable table format
1051 data->output.status = MCP_TABLE_NOT_PROCESSABLE;
1052 return;
1053 }
1054
1055 // Get data and columns
1056 struct json_object *data_obj = NULL;
1057 struct json_object *columns_obj = NULL;
1058
1059 if (!json_object_object_get_ex(data->input.jobj, "data", &data_obj) ||
1060 !json_object_object_get_ex(data->input.jobj, "columns", &columns_obj)) {
1061 buffer_strcat(data->output.result, json_str); // Missing required elements
1062 return;
1063 }
1064
1065 size_t row_count = json_object_array_length(data_obj);
1066 size_t column_count = json_object_object_length(columns_obj);
1067
1068 // Store original counts in input
1069 data->input.rows = row_count;
1070 data->input.columns = column_count;
1071
1072 // Check if we need to show guidance - only when no filtering is specified
1073 if (result_size > max_size_threshold && data->request.columns.count == 0 &&
1074 (data->request.conditions.count == 0 || (data->request.conditions.count == 1 && data->request.query && *data->request.query))) {
1075 // Store first row in result for guidance
1076 data->output.status = MCP_TABLE_RESPONSE_TOO_BIG;
1077 data->request.limit = 1;
1078 }
1079
1080 // Even with no filtering parameters, we need to process to remove unwanted fields
1081
1082 // Sort direction is already parsed in data->request.sort.descending
1083
1084 // Use fixed-size arrays for column selection
1085 uint8_t column_selected[MAX_COLUMNS] = {0}; // 1 if column is selected, 0 if not
1086 char *column_names[MAX_COLUMNS] = {0}; // Names of selected columns (references)
1087 int column_indices[MAX_COLUMNS] = {0}; // Index mapping for ordered access
1088 size_t selected_count = 0;
1089
1090 // Check if we have too many columns
1091 if (column_count > MAX_COLUMNS) {
1092 data->output.status = MCP_TABLE_ERROR_TOO_MANY_COLUMNS;
1093 data->output.columns = column_count;
1094
1095 return;
1096 }
1097
1098 if (data->request.columns.count > 0) {
1099 // Process the pre-parsed column names
1100 bool all_columns_found = true;
1101 CLEAN_BUFFER *missing_columns = buffer_create(0, NULL);
1102
1103 for (size_t i = 0; i < data->request.columns.count; i++) {
1104 const char *col = data->request.columns.array[i];
1105
1106 // Find column and add to selected set
1107 struct json_object *col_obj = NULL;
1108 if (json_object_object_get_ex(columns_obj, col, &col_obj)) {
1109 // Get index of this column
1110 struct json_object *index_obj = NULL;
1111 if (json_object_object_get_ex(col_obj, "index", &index_obj)) {
1112 int idx = json_object_get_int(index_obj);
1113 if (idx >= 0 && idx < MAX_COLUMNS) {
1114 column_selected[idx] = 1;
1115 column_names[idx] = (char*)col; // Store reference only - columns_obj must stay alive
1116 }
1117 }
1118 } else {
1119 // Column not found
1120 all_columns_found = false;
1121 if (buffer_strlen(missing_columns) > 0) {
1122 buffer_strcat(missing_columns, ", ");
1123 }
1124 buffer_strcat(missing_columns, col);
1125 }
1126 }
1127
1128 // If any columns were not found, set error status
1129 if (!all_columns_found) {
1130 data->output.status = MCP_TABLE_ERROR_COLUMNS_NOT_FOUND;
1131 buffer_strcat(data->output.result, buffer_tostring(missing_columns));
1132
1133 return;
1134 }
1135 } else {
1136 // If no columns specified, include all
1137 struct json_object_iterator it = json_object_iter_begin(columns_obj);
1138 struct json_object_iterator itEnd = json_object_iter_end(columns_obj);
1139
1140 while (!json_object_iter_equal(&it, &itEnd)) {
1141 const char *col_key = json_object_iter_peek_name(&it);
1142 struct json_object *col_val = json_object_iter_peek_value(&it);
1143
1144 struct json_object *index_obj = NULL;
1145 if (json_object_object_get_ex(col_val, "index", &index_obj)) {
1146 int idx = json_object_get_int(index_obj);
1147 if (idx >= 0 && idx < MAX_COLUMNS) {
1148 column_selected[idx] = 1;
1149 column_names[idx] = (char*)col_key; // Store reference only - columns_obj must stay alive
1150 }
1151 }
1152
1153 json_object_iter_next(&it);
1154 }
1155 }
1156
1157 // Create ordered index mapping for selected columns
1158 for (int i = 0; i < MAX_COLUMNS; i++) {
1159 if (column_selected[i]) {
1160 column_indices[selected_count] = i;
1161 selected_count++;
1162 }
1163 }
1164
1165 // Find sort column index if sort requested
1166 int sort_idx = -1;
1167
1168 if (data->request.sort.column) {
1169 struct json_object *sort_col_obj = NULL;
1170 if (json_object_object_get_ex(columns_obj, data->request.sort.column, &sort_col_obj)) {
1171 struct json_object *index_obj = NULL;
1172 if (json_object_object_get_ex(sort_col_obj, "index", &index_obj)) {
1173 sort_idx = json_object_get_int(index_obj);
1174 }
1175 } else {
1176 // Column not found
1177 data->output.status = MCP_TABLE_ERROR_SORT_COLUMN_NOT_FOUND;
1178 buffer_strcat(data->output.result, data->request.sort.column);
1179
1180 return;
1181 }
1182 }
1183
1184 // Create new arrays for filtered/sorted data
1185 struct json_object **rows = (struct json_object **)callocz(row_count, sizeof(struct json_object *));
1186 size_t row_idx = 0;
1187
1188 // Resolve column indices for pre-parsed conditions if they exist
1189 if (data->request.conditions.count > 0) {
1190 resolve_condition_columns(&data->request.conditions, columns_obj);
1191 }
1192
1193 // Copy rows for filtering/sorting, applying filters if specified
1194 for (size_t i = 0; i < row_count; i++) {
1195 struct json_object *row = json_object_array_get_idx(data_obj, i);
1196 if (!row)
1197 continue;
1198
1199 bool include_row = true;
1200
1201 // Apply preprocessed conditions if they exist
1202 if (data->request.conditions.count > 0) {
1203 include_row = row_matches_conditions(row, &data->request.conditions);
1204 }
1205
1206 if (include_row) {
1207 rows[row_idx++] = row;
1208 }
1209 }
1210
1211 // If we have missing columns and found no matches, provide helpful error
1212 if (data->request.conditions.has_missing_columns && row_idx == 0) {
1213 data->output.status = MCP_TABLE_ERROR_NO_MATCHES_WITH_MISSING_COLUMNS;
1214
1215 // Clean up and return
1216 freez((void *)rows);
1217
1218 return;
1219 }
1220
1221 // Sort if requested
1222 if (sort_idx >= 0) {
1223 // Simple bubble sort implementation
1224 for (size_t i = 0; i < row_idx; i++) {
1225 for (size_t j = i + 1; j < row_idx; j++) {
1226 bool should_swap = false;
1227
1228 struct json_object *val_i = json_object_array_get_idx(rows[i], sort_idx);
1229 struct json_object *val_j = json_object_array_get_idx(rows[j], sort_idx);
1230
1231 // Handle null values
1232 if (!val_i && !val_j) {
1233 should_swap = false;
1234 } else if (!val_i) {
1235 should_swap = !data->request.sort.descending;
1236 } else if (!val_j) {
1237 should_swap = data->request.sort.descending;
1238 } else {
1239 // Try to match based on apparent type
1240 // Check if either value is a number type
1241 if (json_object_is_type(val_i, json_type_int) ||
1242 json_object_is_type(val_i, json_type_double) ||
1243 json_object_is_type(val_j, json_type_int) ||
1244 json_object_is_type(val_j, json_type_double)) {
1245
1246 // Let json-c do the type conversion
1247 double i_val = json_object_get_double(val_i);
1248 double j_val = json_object_get_double(val_j);
1249 should_swap = data->request.sort.descending ? (i_val < j_val) : (i_val > j_val);
1250
1251 } else if (json_object_is_type(val_i, json_type_boolean) ||
1252 json_object_is_type(val_j, json_type_boolean)) {
1253
1254 // Let json-c do the type conversion
1255 bool i_val = json_object_get_boolean(val_i);
1256 bool j_val = json_object_get_boolean(val_j);
1257 should_swap = data->request.sort.descending ? (i_val && !j_val) : (!i_val && j_val);
1258
1259 } else {
1260 // Default to string comparison for everything else
1261 int cmp = strcmp(json_object_get_string(val_i), json_object_get_string(val_j));
1262 should_swap = data->request.sort.descending ? (cmp < 0) : (cmp > 0);
1263 }
1264 }
1265
1266 if (should_swap) {
1267 struct json_object *temp = rows[i];
1268 rows[i] = rows[j];
1269 rows[j] = temp;
1270 }
1271 }
1272 }
1273 }
1274
1275 // Apply row limit
1276 size_t limit = row_idx;
1277 bool force_limit = (data->output.status == MCP_TABLE_RESPONSE_TOO_BIG && data->request.limit > 0);
1278
1279 if (force_limit && data->request.limit < limit) {
1280 limit = data->request.limit;
1281 }
1282 else if (!force_limit && data->request.limit > 0 && data->request.limit < limit && data->input.type == FN_TYPE_TABLE) {
1283 // we don't limit history functions, only regular tables
1284 // for history functions, we sent the limit to the backend
1285 // so whatever it returns is what we show
1286 // This is important, otherwise the cursor will be wrong!
1287 limit = data->request.limit;
1288 }
1289
1290 // Create new filtered result
1291 struct json_object *filtered_result = json_object_new_object();
1292 struct json_object *filtered_data = json_object_new_array();
1293 struct json_object *filtered_columns = json_object_new_object();
1294
1295 // Copy only specific metadata fields from original
1296 {
1297 // Keep only status, type, update_every, has_history + data and columns
1298 const char *keep_fields[] = {"status", "type", "update_every", "has_history"};
1299 size_t keep_count = sizeof(keep_fields) / sizeof(keep_fields[0]);
1300
1301 for (size_t i = 0; i < keep_count; i++) {
1302 struct json_object *field_obj = NULL;
1303
1304 if (json_object_object_get_ex(data->input.jobj, keep_fields[i], &field_obj)) {
1305 json_object_object_add(filtered_result, keep_fields[i], json_object_get(field_obj));
1306 }
1307 }
1308 }
1309
1310 // Extract column transform information BEFORE creating filtered columns
1311 COLUMN_TRANSFORM_INFO col_transforms[MAX_COLUMNS] = {{0}};
1312 extract_column_transforms(columns_obj, column_indices, column_names, selected_count, col_transforms);
1313
1314 // Create filtered data rows with transformation
1315 for (size_t i = 0; i < limit; i++) {
1316 struct json_object *row = rows[i];
1317 struct json_object *new_row = json_object_new_array();
1318
1319 // Extract only selected columns
1320 for (size_t j = 0; j < selected_count; j++) {
1321 int col_idx = column_indices[j];
1322 struct json_object *val = json_object_array_get_idx(row, col_idx);
1323
1324 // Try to transform the value
1325 struct json_object *transformed = transform_value_for_mcp(val, col_transforms[j].type, col_transforms[j].transform);
1326
1327 if (transformed) {
1328 // Use the transformed value (we own it)
1329 json_object_array_add(new_row, transformed);
1330 } else if (val) {
1331 // No transformation needed, use original with ref count increase
1332 // json_object_array_add takes ownership, so we must increment ref count
1333 json_object_array_add(new_row, json_object_get(val));
1334 } else {
1335 // NULL value
1336 json_object_array_add(new_row, NULL);
1337 }
1338 }
1339
1340 json_object_array_add(filtered_data, new_row);
1341 }
1342
1343 // Create filtered column definitions AFTER processing data
1344 for (size_t i = 0; i < selected_count; i++) {
1345 int col_idx = column_indices[i];
1346 const char *col_name = column_names[col_idx];
1347
1348 struct json_object *col_obj = NULL;
1349 if (json_object_object_get_ex(columns_obj, col_name, &col_obj)) {
1350 struct json_object *col_copy = create_filtered_column(col_obj, col_name);
1351
1352 // Update index to match new position
1353 json_object_object_add(col_copy, "index", json_object_new_int((int)i));
1354
1355 // Check if this column was transformed to string
1356 if (is_transformable_to_string(col_transforms[i].type, col_transforms[i].transform)) {
1357 // Override type to string since we transformed the value
1358 json_object_object_del(col_copy, "type");
1359 json_object_object_add(col_copy, "type", json_object_new_string("string"));
1360
1361 // Remove numeric-specific properties that don't make sense for strings
1362 json_object_object_del(col_copy, "max");
1363 json_object_object_del(col_copy, "min");
1364 json_object_object_del(col_copy, "units");
1365 }
1366
1367 json_object_object_add(filtered_columns, col_name, col_copy);
1368 }
1369 }
1370
1371 // Add filtered data and columns to result
1372 json_object_object_add(filtered_result, "data", filtered_data);
1373 json_object_object_add(filtered_result, "columns", filtered_columns);
1374
1375 // Check if we found any rows
1376 if (limit == 0 && data->request.conditions.count > 0) {
1377 // No rows matched the conditions
1378 json_object_put(filtered_result);
1379 data->output.status = MCP_TABLE_ERROR_NO_MATCHES;
1380 } else {
1381 // Set status flag if we used wildcard search and found results
1382 if (data->request.conditions.has_missing_columns && row_idx > 0) {
1383 data->output.status = MCP_TABLE_INFO_MISSING_COLUMNS_FOUND_RESULTS;
1384 }
1385
1386 // Add nextCursor for pagination if applicable (only for successful results)
1387 if (data->pagination.enabled && data->input.rows > 0) {
1388 usec_t next_cursor_timestamp = calculate_next_cursor_from_input(data);
1389 if (next_cursor_timestamp > 0) {
1390 CLEAN_BUFFER *cursor_str = buffer_create(0, NULL);
1391 buffer_sprintf(cursor_str, "%" PRIu64, next_cursor_timestamp);
1392 json_object_object_add(filtered_result, "nextCursor", json_object_new_string(buffer_tostring(cursor_str)));
1393 }
1394 }
1395
1396 // Convert to string and store result
1397 const char *filtered_json = json_object_to_json_string_ext(filtered_result, JSON_C_TO_STRING_PRETTY);
1398 buffer_strcat(data->output.result, filtered_json);
1399
1400 // Update actual counts from filtered result
1401 data->output.rows = limit;
1402 data->output.columns = selected_count;
1403
1404 // Free the filtered result
1405 json_object_put(filtered_result);
1406 }
1407
1408 // Clean up
1409 freez((void *)rows);
1410
1411 }
1412
1413 // Parse conditions without column information (early parsing during request)
1414 static MCP_RETURN_CODE mcp_parse_conditions_early(CONDITION_ARRAY *condition_array, struct json_object *conditions_json, BUFFER *error_buffer) {
1415 if (!condition_array || !conditions_json)
1416 return MCP_RC_ERROR;
1417
1418 // Initialize the condition array
1419 memset(condition_array, 0, sizeof(CONDITION_ARRAY));
1420
1421 if (!json_object_is_type(conditions_json, json_type_array))
1422 return MCP_RC_INVALID_PARAMS;
1423
1424 size_t conditions_count = json_object_array_length(conditions_json);
1425 if (conditions_count == 0)
1426 return MCP_RC_OK; // Empty array is valid
1427
1428 if (conditions_count > MAX_CONDITIONS) {
1429 if (error_buffer)
1430 buffer_sprintf(error_buffer, "Too many conditions. Maximum is %d.", MAX_CONDITIONS);
1431 return MCP_RC_INVALID_PARAMS;
1432 }
1433
1434 for (size_t i = 0; i < conditions_count; i++) {
1435 struct json_object *condition = json_object_array_get_idx(conditions_json, i);
1436
1437 // Each condition should be an array of [column, operator, value]
1438 if (!condition || !json_object_is_type(condition, json_type_array) ||
1439 json_object_array_length(condition) != 3) {
1440 if (error_buffer)
1441 buffer_sprintf(error_buffer, "Invalid condition format at index %zu. Expected [column, operator, value]", i);
1442 mcp_functions_free_condition_patterns(condition_array);
1443 return MCP_RC_INVALID_PARAMS;
1444 }
1445
1446 struct json_object *col_name_obj = json_object_array_get_idx(condition, 0);
1447 struct json_object *operator_obj = json_object_array_get_idx(condition, 1);
1448 struct json_object *value_obj = json_object_array_get_idx(condition, 2);
1449
1450 if (!col_name_obj || !json_object_is_type(col_name_obj, json_type_string) ||
1451 !operator_obj || !json_object_is_type(operator_obj, json_type_string) ||
1452 !value_obj) {
1453 if (error_buffer)
1454 buffer_sprintf(error_buffer, "Invalid condition element types at index %zu. Expected [string, string, any]", i);
1455 mcp_functions_free_condition_patterns(condition_array);
1456 return MCP_RC_INVALID_PARAMS;
1457 }
1458
1459 CONDITION *curr = &condition_array->items[condition_array->count];
1460
1461 // Get column name and operator
1462 curr->column_name = json_object_get_string(col_name_obj);
1463 const char *op_str = json_object_get_string(operator_obj);
1464
1465 curr->op = mcp_functions_string_to_operator(op_str);
1466 if (curr->op == OP_UNKNOWN) {
1467 if (error_buffer)
1468 buffer_sprintf(error_buffer, "Invalid operator '%s' at index %zu. Valid operators are: ==, !=, <>, <, <=, >, >=, match, not match",
1469 op_str, i);
1470 mcp_functions_free_condition_patterns(condition_array);
1471 return MCP_RC_INVALID_PARAMS;
1472 }
1473
1474 // Parse and store the value based on its type
1475 if (json_object_is_type(value_obj, json_type_null)) {
1476 curr->v_type = COND_VALUE_NULL;
1477 } else if (json_object_is_type(value_obj, json_type_boolean)) {
1478 curr->v_type = COND_VALUE_BOOLEAN;
1479 curr->v_bool = json_object_get_boolean(value_obj);
1480 } else if (json_object_is_type(value_obj, json_type_int) || json_object_is_type(value_obj, json_type_double)) {
1481 curr->v_type = COND_VALUE_NUMBER;
1482 curr->v_num = json_object_get_double(value_obj);
1483 } else {
1484 // Everything else is treated as string
1485 curr->v_type = COND_VALUE_STRING;
1486 curr->v_str = json_object_get_string(value_obj);
1487 }
1488
1489 // Pre-compile patterns for MATCH operators
1490 if (curr->op == OP_MATCH || curr->op == OP_NOT_MATCH) {
1491 const char *pattern_str = NULL;
1492 if (curr->v_type == COND_VALUE_STRING) {
1493 pattern_str = curr->v_str;
1494 } else {
1495 // For non-string types, use json-c's string conversion
1496 pattern_str = json_object_get_string(value_obj);
1497 }
1498 if (pattern_str) {
1499 curr->pattern = string_to_simple_pattern_nocase_substring(pattern_str);
1500 }
1501 }
1502
1503 // Mark column index as unknown for now (-1)
1504 curr->column_index = -1;
1505
1506 condition_array->count++;
1507 }
1508
1509 return MCP_RC_OK;
1510 }
1511
1512 // Build the function name with GET parameters appended
1513 static void build_function_name_with_params(BUFFER *dest, const char *function_name, struct json_object *selections, MCP_FUNCTION_DATA *data, MCP_FUNCTION_REGISTRY_ENTRY *entry) {
1514 buffer_strcat(dest, function_name);
1515
1516 // Add time-based parameters if supported and specified
1517 if (entry->has_timeframe) {
1518 buffer_sprintf(dest, " after:%ld", data->request.after);
1519 buffer_sprintf(dest, " before:%ld", data->request.before);
1520 }
1521
1522 if (entry->pagination.enabled && data->request.anchor > 0) {
1523 buffer_sprintf(dest, " %s:%llu", string2str(entry->pagination.key), (unsigned long long)data->request.anchor);
1524 }
1525
1526 if (entry->has_last && data->request.limit > 0) {
1527 buffer_sprintf(dest, " last:%zu", data->request.limit);
1528 }
1529
1530 if (entry->has_direction && data->request.direction && *data->request.direction) {
1531 buffer_sprintf(dest, " direction:%s", data->request.direction);
1532 }
1533
1534 if (entry->has_query && data->request.query && *data->request.query) {
1535 buffer_sprintf(dest, " query:%s", data->request.query);
1536 }
1537
1538 if (entry->has_data_only) {
1539 buffer_sprintf(dest, " data_only:true");
1540 }
1541
1542 if (entry->has_slice) {
1543 buffer_sprintf(dest, " slice:true");
1544 }
1545
1546 // Add selections parameters
1547 if (selections && json_object_is_type(selections, json_type_object)) {
1548 struct json_object_iterator it = json_object_iter_begin(selections);
1549 struct json_object_iterator itEnd = json_object_iter_end(selections);
1550
1551 while (!json_object_iter_equal(&it, &itEnd)) {
1552 const char *key = json_object_iter_peek_name(&it);
1553 struct json_object *val = json_object_iter_peek_value(&it);
1554
1555 if (!val) {
1556 json_object_iter_next(&it);
1557 continue;
1558 }
1559
1560 buffer_sprintf(dest, " %s:", key);
1561
1562 if (json_object_is_type(val, json_type_string)) {
1563 // Single string value
1564 buffer_strcat(dest, json_object_get_string(val));
1565 } else if (json_object_is_type(val, json_type_array)) {
1566 // Array of values
1567 size_t array_len = json_object_array_length(val);
1568 for (size_t i = 0; i < array_len; i++) {
1569 if (i > 0) buffer_strcat(dest, ",");
1570 struct json_object *item = json_object_array_get_idx(val, i);
1571 if (item && json_object_is_type(item, json_type_string)) {
1572 buffer_strcat(dest, json_object_get_string(item));
1573 }
1574 }
1575 }
1576
1577 json_object_iter_next(&it);
1578 }
1579 }
1580 }
1581
1582 // Calculate nextCursor from original input data for MCP-compliant pagination
1583 // Returns the next cursor timestamp, or 0 if not applicable
1584 static usec_t calculate_next_cursor_from_input(MCP_FUNCTION_DATA *data) {
1585 if (!data->pagination.enabled || data->input.rows == 0 || !data->input.jobj) {
1586 return 0;
1587 }
1588
1589 // Only handle timestamp_usec units for now
1590 if (data->pagination.units != MCP_PAGINATION_UNITS_TIMESTAMP_USEC) {
1591 return 0;
1592 }
1593
1594 // Get the data array from original input
1595 struct json_object *data_array;
1596 if (!json_object_object_get_ex(data->input.jobj, "data", &data_array) ||
1597 !json_object_is_type(data_array, json_type_array)) {
1598 return 0;
1599 }
1600
1601 // Get the columns object to find the timestamp column
1602 struct json_object *columns_obj;
1603 if (!json_object_object_get_ex(data->input.jobj, "columns", &columns_obj)) {
1604 return 0;
1605 }
1606
1607 // Find the timestamp column index - handle both array and object formats
1608 int timestamp_column_index = -1;
1609 const char *timestamp_column_name = string2str(data->pagination.column);
1610
1611 if (json_object_is_type(columns_obj, json_type_array)) {
1612 // Array format: ["timestamp", "field1", "field2"]
1613 size_t columns_count = json_object_array_length(columns_obj);
1614 for (size_t i = 0; i < columns_count; i++) {
1615 struct json_object *col_obj = json_object_array_get_idx(columns_obj, i);
1616 if (!col_obj) continue;
1617
1618 if (json_object_is_type(col_obj, json_type_string)) {
1619 const char *col_name = json_object_get_string(col_obj);
1620 if (strcmp(col_name, timestamp_column_name) == 0) {
1621 timestamp_column_index = (int)i;
1622 break;
1623 }
1624 }
1625 }
1626 } else if (json_object_is_type(columns_obj, json_type_object)) {
1627 // Object format: {"timestamp": {...}, "field1": {...}}
1628 // Need to find the index by iterating through keys in order
1629 struct json_object_iterator it = json_object_iter_begin(columns_obj);
1630 struct json_object_iterator it_end = json_object_iter_end(columns_obj);
1631 int index = 0;
1632
1633 while (!json_object_iter_equal(&it, &it_end)) {
1634 const char *key = json_object_iter_peek_name(&it);
1635 if (key && strcmp(key, timestamp_column_name) == 0) {
1636 timestamp_column_index = index;
1637 break;
1638 }
1639 json_object_iter_next(&it);
1640 index++;
1641 }
1642 } else {
1643 return 0;
1644 }
1645
1646 if (timestamp_column_index == -1) {
1647 return 0;
1648 }
1649
1650 // Extract timestamps from all rows in original data
1651 size_t rows_count = json_object_array_length(data_array);
1652 if (rows_count == 0) {
1653 return 0;
1654 }
1655
1656 usec_t min_timestamp = UINT64_MAX;
1657 usec_t max_timestamp = 0;
1658 bool found_any = false;
1659
1660 for (size_t i = 0; i < rows_count; i++) {
1661 struct json_object *row_obj = json_object_array_get_idx(data_array, i);
1662 if (!row_obj || !json_object_is_type(row_obj, json_type_array)) {
1663 continue;
1664 }
1665
1666 size_t row_length = json_object_array_length(row_obj);
1667 if ((size_t)timestamp_column_index >= row_length) {
1668 continue;
1669 }
1670
1671 struct json_object *timestamp_obj = json_object_array_get_idx(row_obj, timestamp_column_index);
1672 if (!timestamp_obj) {
1673 continue;
1674 }
1675
1676 usec_t timestamp;
1677 if (json_object_is_type(timestamp_obj, json_type_int)) {
1678 timestamp = (usec_t)json_object_get_int64(timestamp_obj);
1679 } else if (json_object_is_type(timestamp_obj, json_type_string)) {
1680 const char *timestamp_str = json_object_get_string(timestamp_obj);
1681 timestamp = str2ull(timestamp_str, NULL);
1682 } else {
1683 continue;
1684 }
1685
1686 if (timestamp < min_timestamp) {
1687 min_timestamp = timestamp;
1688 }
1689 if (timestamp > max_timestamp) {
1690 max_timestamp = timestamp;
1691 }
1692 found_any = true;
1693 }
1694
1695 if (!found_any) {
1696 return 0;
1697 }
1698
1699 // Determine direction - default to forward if not specified
1700 bool is_backward = (data->request.direction && strcmp(data->request.direction, "backward") == 0);
1701
1702 // Calculate nextCursor based on direction:
1703 // - Forward: use max timestamp + 1
1704 // - Backward: use min timestamp - 1
1705 usec_t next_cursor;
1706 if (is_backward) {
1707 next_cursor = (min_timestamp > 0) ? min_timestamp - 1 : 0;
1708 } else {
1709 next_cursor = (max_timestamp < UINT64_MAX) ? max_timestamp + 1 : 0;
1710 }
1711
1712 return next_cursor;
1713 }
1714
1715 // Build POST payload for v3+ functions
1716 // Format: { "after": timestamp, "before": timestamp, "last": N, "data_only": true, "selections": { "key1": ["value1", "value2"] } }
1717 static BUFFER *build_post_payload_with_selections(struct json_object *selections, MCP_FUNCTION_DATA *data, MCP_FUNCTION_REGISTRY_ENTRY *entry) {
1718 BUFFER *payload = buffer_create(0, NULL);
1719 buffer_json_initialize(payload, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
1720
1721 // Add time-based parameters if supported and specified
1722 if (entry->has_timeframe && data->request.after != 0) {
1723 buffer_json_member_add_time_t(payload, "after", data->request.after);
1724 }
1725
1726 if (entry->has_timeframe && data->request.before != 0) {
1727 buffer_json_member_add_time_t(payload, "before", data->request.before);
1728 }
1729
1730 if (entry->pagination.enabled && data->request.anchor > 0) {
1731 buffer_json_member_add_uint64(payload, string2str(entry->pagination.key), data->request.anchor);
1732 }
1733
1734 if (entry->has_last && data->request.limit > 0) {
1735 buffer_json_member_add_uint64(payload, "last", data->request.limit);
1736 }
1737
1738 if (entry->has_direction && data->request.direction) {
1739 buffer_json_member_add_string(payload, "direction", data->request.direction);
1740 }
1741
1742 if (entry->has_query && data->request.query) {
1743 buffer_json_member_add_string(payload, "query", data->request.query);
1744 }
1745
1746 if (entry->has_data_only) {
1747 buffer_json_member_add_boolean(payload, "data_only", true);
1748 }
1749
1750 if (entry->has_slice) {
1751 buffer_json_member_add_boolean(payload, "slice", true);
1752 }
1753
1754 // Build selections object - start with provided selections and add condition-based selections for has_history functions
1755 bool selections_added = false;
1756
1757 // For has_history functions, add selections based on conditions with == or match operators
1758 if (entry->has_history && data->request.conditions.count > 0) {
1759 buffer_json_member_add_object(payload, "selections");
1760 selections_added = true;
1761
1762 for (size_t i = 0; i < data->request.conditions.count; i++) {
1763 CONDITION *cond = &data->request.conditions.items[i];
1764
1765 // Skip wildcard searches and unsupported operators (these were validated earlier)
1766 if (cond->column_index == -1 || (cond->op != OP_EQUALS && cond->op != OP_MATCH)) {
1767 continue;
1768 }
1769
1770 const char *column_name = cond->column_name;
1771
1772 if (cond->op == OP_EQUALS && cond->v_type == COND_VALUE_STRING) {
1773 // Single value equality: key == value -> "key": ["value"]
1774 buffer_json_member_add_array(payload, column_name);
1775 buffer_json_add_array_item_string(payload, cond->v_str);
1776 buffer_json_array_close(payload);
1777 }
1778 else if (cond->op == OP_MATCH && cond->v_type == COND_VALUE_STRING) {
1779 // Pattern match without wildcards: key match a|b|c -> "key": ["a", "b", "c"]
1780 const char *pattern_str = cond->v_str;
1781
1782 // Parse the pipe-separated values
1783 buffer_json_member_add_array(payload, column_name);
1784
1785 CLEAN_BUFFER *temp_value = buffer_create(0, NULL);
1786 const char *current = pattern_str;
1787 const char *pipe_pos;
1788
1789 while ((pipe_pos = strchr(current, '|')) != NULL) {
1790 // Extract value between current and pipe_pos
1791 buffer_flush(temp_value);
1792 buffer_strncat(temp_value, current, pipe_pos - current);
1793
1794 // Add to array if non-empty
1795 if (buffer_strlen(temp_value) > 0) {
1796 buffer_json_add_array_item_string(payload, buffer_tostring(temp_value));
1797 }
1798
1799 current = pipe_pos + 1;
1800 }
1801
1802 // Add the last value after the final pipe (or the only value if no pipes)
1803 if (*current) {
1804 buffer_json_add_array_item_string(payload, current);
1805 }
1806
1807 buffer_json_array_close(payload);
1808 }
1809 }
1810 }
1811
1812 // Add user-provided selections if provided
1813 if (selections && json_object_is_type(selections, json_type_object)) {
1814 if (!selections_added) {
1815 buffer_json_member_add_object(payload, "selections");
1816 selections_added = true;
1817 }
1818
1819 struct json_object_iterator it = json_object_iter_begin(selections);
1820 struct json_object_iterator itEnd = json_object_iter_end(selections);
1821
1822 while (!json_object_iter_equal(&it, &itEnd)) {
1823 const char *key = json_object_iter_peek_name(&it);
1824 struct json_object *val = json_object_iter_peek_value(&it);
1825
1826 if (!val) {
1827 json_object_iter_next(&it);
1828 continue;
1829 }
1830
1831 if (json_object_is_type(val, json_type_string)) {
1832 // Single string value - convert to array for consistency
1833 buffer_json_member_add_array(payload, key);
1834 buffer_json_add_array_item_string(payload, json_object_get_string(val));
1835 buffer_json_array_close(payload);
1836 } else if (json_object_is_type(val, json_type_array)) {
1837 // Array of values
1838 buffer_json_member_add_array(payload, key);
1839
1840 size_t array_len = json_object_array_length(val);
1841 for (size_t i = 0; i < array_len; i++) {
1842 struct json_object *item = json_object_array_get_idx(val, i);
1843 if (item && json_object_is_type(item, json_type_string)) {
1844 buffer_json_add_array_item_string(payload, json_object_get_string(item));
1845 }
1846 }
1847
1848 buffer_json_array_close(payload);
1849 }
1850
1851 json_object_iter_next(&it);
1852 }
1853 }
1854
1855 if (selections_added) {
1856 buffer_json_object_close(payload); // close "selections"
1857 }
1858
1859 buffer_json_finalize(payload);
1860
1861 return payload;
1862 }
1863
1864 // Generate helpful message about required parameters
1865 static void generate_required_params_message(BUFFER *message, MCP_FUNCTION_REGISTRY_ENTRY *entry,
1866 bool missing_timeframe, BUFFER *missing_params) {
1867 bool has_missing_params = (buffer_strlen(missing_params) > 0);
1868
1869 if (!missing_timeframe && !has_missing_params) {
1870 return; // Nothing to show
1871 }
1872
1873 buffer_sprintf(message,
1874 "This function has some required parameters.\n"
1875 "Please repeat the request adding the following:\n\n");
1876
1877 // Add timeframe requirements ONLY if timeframe is missing
1878 if (missing_timeframe && entry->has_timeframe) {
1879 buffer_strcat(message,
1880 "TIMEFRAME PARAMETERS (Required):\n"
1881 "- 'after': Start time (timestamp in seconds or RFC3339 datetime string)\n"
1882 "- 'before': End time (timestamp in seconds or RFC3339 datetime string)\n");
1883
1884 // Add optional time-based parameters that are commonly used with history functions
1885 if (entry->has_direction) {
1886 buffer_strcat(message, "- 'direction': Query direction ('forward' or 'backward')\n");
1887 }
1888 if (entry->has_last) {
1889 buffer_strcat(message, "- 'limit': Maximum number of entries to return\n");
1890 }
1891
1892 buffer_strcat(message, "\n");
1893 }
1894
1895 // Only show sections for parameters that are actually missing
1896 if (has_missing_params) {
1897 size_t selection_count = 0;
1898 for (size_t i = 0; i < entry->required_params_count; i++) {
1899 MCP_FUNCTION_PARAM *param = &entry->required_params[i];
1900 const char *param_id = string2str(param->id);
1901
1902 // Check if this parameter is in the missing list
1903 if (strstr(buffer_tostring(missing_params), param_id)) {
1904 selection_count++;
1905 buffer_sprintf(message, "SELECTION %zu:\n", selection_count);
1906 buffer_sprintf(message, "Description: %s\n", string2str(param->help));
1907 buffer_sprintf(message, "key: '%s'\n", param_id);
1908
1909 if (param->type == MCP_REQUIRED_PARAMS_TYPE_SELECT) {
1910 buffer_strcat(message, "With one of the following values:\n");
1911 } else {
1912 buffer_strcat(message, "With one or more of the following values:\n");
1913 }
1914
1915 for (size_t j = 0; j < param->options_count; j++) {
1916 const char *id = string2str(param->options[j].id);
1917 const char *name = string2str(param->options[j].name);
1918 const char *info = string2str(param->options[j].info);
1919
1920 // Only show name if it's different from id
1921 bool show_name = (name && *name && strcmp(id, name) != 0);
1922
1923 if (info && *info) {
1924 if (show_name) {
1925 buffer_sprintf(message, " - '%s': %s (%s)\n", id, name, info);
1926 } else {
1927 buffer_sprintf(message, " - '%s' (%s)\n", id, info);
1928 }
1929 } else {
1930 if (show_name) {
1931 buffer_sprintf(message, " - '%s': %s\n", id, name);
1932 } else {
1933 buffer_sprintf(message, " - '%s'\n", id);
1934 }
1935 }
1936 }
1937
1938 buffer_strcat(message, "\n");
1939 }
1940 }
1941 }
1942
1943 // Only show example if there are missing parameters
1944 if (missing_timeframe || has_missing_params) {
1945 buffer_strcat(message, "\nExample:\n\n```json\n{\n");
1946
1947 // Add timeframe parameters to example ONLY if they're missing
1948 if (missing_timeframe && entry->has_timeframe) {
1949 buffer_strcat(message, " \"after\": 1648627200,\n");
1950 buffer_strcat(message, " \"before\": 1648630800,\n");
1951
1952 // Add optional time-based parameters to the example
1953 if (entry->has_direction) {
1954 buffer_strcat(message, " \"direction\": \"backward\",\n");
1955 }
1956 if (entry->has_last) {
1957 buffer_strcat(message, " \"limit\": 100,\n");
1958 }
1959 }
1960
1961 // Add selections ONLY for missing required params
1962 if (has_missing_params) {
1963 buffer_strcat(message, " \"selections\": {\n");
1964
1965 bool first_param = true;
1966 for (size_t i = 0; i < entry->required_params_count; i++) {
1967 MCP_FUNCTION_PARAM *param = &entry->required_params[i];
1968 const char *param_id = string2str(param->id);
1969
1970 // Only include this parameter if it's missing
1971 if (strstr(buffer_tostring(missing_params), param_id)) {
1972 if (!first_param) buffer_strcat(message, ",\n");
1973 first_param = false;
1974
1975 if (param->type == MCP_REQUIRED_PARAMS_TYPE_SELECT) {
1976 // For select type, show single value (not array)
1977 if (param->options_count > 0) {
1978 buffer_sprintf(message, " \"%s\": \"%s\"",
1979 param_id,
1980 string2str(param->options[0].id));
1981 } else {
1982 buffer_sprintf(message, " \"%s\": \"value\"", param_id);
1983 }
1984 } else {
1985 // For multiselect type, show array
1986 buffer_sprintf(message, " \"%s\": [", param_id);
1987 if (param->options_count > 0) {
1988 // Show first few options as examples
1989 size_t examples = param->options_count > 2 ? 2 : param->options_count;
1990 for (size_t j = 0; j < examples; j++) {
1991 if (j > 0) buffer_strcat(message, ", ");
1992 buffer_sprintf(message, "\"%s\"", string2str(param->options[j].id));
1993 }
1994 } else {
1995 buffer_strcat(message, "\"value1\", \"value2\"");
1996 }
1997 buffer_strcat(message, "]");
1998 }
1999 }
2000 }
2001
2002 buffer_strcat(message, "\n }");
2003 }
2004
2005 buffer_strcat(message, "\n}\n```");
2006 }
2007 }
2008
2009 // Parse the request parameters and populate the MCP_FUNCTION_DATA structure
2010 static MCP_RETURN_CODE mcp_parse_function_request(MCP_FUNCTION_DATA *data, MCP_CLIENT *mcpc, struct json_object *params) {
2011 if (!data || !mcpc || !params)
2012 return MCP_RC_ERROR;
2013
2014 // Set request context
2015 data->request.mcpc = mcpc;
2016 data->request.params = params;
2017 data->request.auth = mcpc->user_auth;
2018
2019 // Parse required parameter: node
2020 struct json_object *obj = NULL;
2021 if (json_object_object_get_ex(params, "node", &obj) &&
2022 json_object_is_type(obj, json_type_string)) {
2023 data->request.node = json_object_get_string(obj);
2024 }
2025
2026 if (!data->request.node || !*data->request.node) {
2027 buffer_sprintf(mcpc->error, "Missing required parameter 'node'");
2028 return MCP_RC_BAD_REQUEST;
2029 }
2030
2031 // Parse required parameter: function
2032 if (json_object_object_get_ex(params, "function", &obj) &&
2033 json_object_is_type(obj, json_type_string)) {
2034 data->request.function = json_object_get_string(obj);
2035 }
2036
2037 if (!data->request.function || !*data->request.function) {
2038 buffer_sprintf(mcpc->error, "Missing required parameter 'function'");
2039 return MCP_RC_BAD_REQUEST;
2040 }
2041
2042 // Parse timeout parameter
2043 data->request.timeout = (time_t)mcp_params_extract_timeout(params, "timeout", 60, 1, 3600, mcpc->error);
2044 if (buffer_strlen(mcpc->error) > 0) {
2045 return MCP_RC_BAD_REQUEST;
2046 }
2047
2048 // Parse optional filtering parameters
2049
2050 // Parse time-based parameters
2051 if (!mcp_params_parse_time_window(params, &data->request.after, &data->request.before,
2052 0, 0, true, mcpc->error)) {
2053 return MCP_RC_BAD_REQUEST;
2054 }
2055
2056 // Check if timeframe parameters are required but missing (will be validated later with registry entry)
2057
2058 // Parse cursor parameter
2059 data->request.cursor = mcp_params_extract_string(params, "cursor", NULL);
2060
2061 // Convert cursor to internal anchor timestamp
2062 data->request.anchor = 0; // Will be set if cursor is valid
2063 if (data->request.cursor && *data->request.cursor) {
2064 // For now, assume cursor is a timestamp_usec string
2065 char *endptr;
2066 usec_t cursor_timestamp = strtoull(data->request.cursor, &endptr, 10);
2067 if (*endptr == '\0' && cursor_timestamp > 0) {
2068 data->request.anchor = cursor_timestamp;
2069 }
2070 }
2071 if (buffer_strlen(mcpc->error) > 0) {
2072 return MCP_RC_BAD_REQUEST;
2073 }
2074
2075 // Extract limit parameter (used for both history and non-history functions)
2076 data->request.limit = (size_t)mcp_params_extract_size(params, "limit", 0, 0, SIZE_MAX, mcpc->error);
2077 if (buffer_strlen(mcpc->error) > 0) {
2078 return MCP_RC_BAD_REQUEST;
2079 }
2080
2081 // direction parameter
2082 if (json_object_object_get_ex(params, "direction", &obj) &&
2083 json_object_is_type(obj, json_type_string)) {
2084 const char *direction = json_object_get_string(obj);
2085 if (direction && (strcmp(direction, "forward") == 0 || strcmp(direction, "backward") == 0)) {
2086 data->request.direction = direction;
2087 } else if (direction) {
2088 buffer_sprintf(mcpc->error, "Invalid direction: '%s'. Valid options are 'forward' or 'backward'.", direction);
2089 return MCP_RC_BAD_REQUEST;
2090 }
2091 }
2092
2093 // query parameter for full-text search
2094 if (json_object_object_get_ex(params, "q", &obj) &&
2095 json_object_is_type(obj, json_type_string)) {
2096 const char *query = json_object_get_string(obj);
2097 if (query && *query) {
2098 data->request.query = query;
2099 }
2100 }
2101
2102
2103 // columns array - parse it early
2104 if (json_object_object_get_ex(params, "columns", &obj) &&
2105 json_object_is_type(obj, json_type_array) &&
2106 json_object_array_length(obj) > 0) {
2107 size_t count = json_object_array_length(obj);
2108 if (count > MAX_SELECTED_COLUMNS) {
2109 buffer_sprintf(mcpc->error, "Too many columns requested. Maximum is %d.", MAX_SELECTED_COLUMNS);
2110 return MCP_RC_BAD_REQUEST;
2111 }
2112
2113 data->request.columns.count = 0;
2114 for (size_t i = 0; i < count; i++) {
2115 struct json_object *col_obj = json_object_array_get_idx(obj, i);
2116 if (col_obj && json_object_is_type(col_obj, json_type_string)) {
2117 data->request.columns.array[data->request.columns.count++] = json_object_get_string(col_obj);
2118 }
2119 }
2120 }
2121
2122 // sort_column
2123 if (json_object_object_get_ex(params, "sort_column", &obj) &&
2124 json_object_is_type(obj, json_type_string)) {
2125 const char *value = json_object_get_string(obj);
2126 if (value && *value) {
2127 data->request.sort.column = value;
2128 }
2129 }
2130
2131 // sort_order and parse it to boolean
2132 data->request.sort.descending = true; // default to DESC
2133 if (json_object_object_get_ex(params, "sort_order", &obj) &&
2134 json_object_is_type(obj, json_type_string)) {
2135 const char *sort_order = json_object_get_string(obj);
2136 if (sort_order) {
2137 if (strcasecmp(sort_order, "asc") == 0) {
2138 data->request.sort.descending = false;
2139 } else if (strcasecmp(sort_order, "desc") != 0) {
2140 // Invalid sort order
2141 buffer_sprintf(mcpc->error, "Invalid sort_order: '%s'. Valid options are 'asc' or 'desc'.", sort_order);
2142 return MCP_RC_BAD_REQUEST;
2143 }
2144 }
2145 }
2146
2147 // limit is already extracted above for all function types
2148
2149 // conditions array - parse it early
2150 if (json_object_object_get_ex(params, "conditions", &obj) &&
2151 json_object_is_type(obj, json_type_array) &&
2152 json_object_array_length(obj) > 0) {
2153 // Parse conditions early
2154 MCP_RETURN_CODE rc = mcp_parse_conditions_early(&data->request.conditions, obj, mcpc->error);
2155 if (rc != MCP_RC_OK) {
2156 return rc;
2157 }
2158 }
2159
2160 // Convert query parameter to a full-text search condition
2161 if (data->request.query && *data->request.query) {
2162 // Check if we have room for one more condition
2163 if (data->request.conditions.count >= MAX_CONDITIONS) {
2164 buffer_sprintf(mcpc->error, "Too many search criteria. Cannot add full-text search query on top of %zu existing conditions. Maximum is %d.",
2165 data->request.conditions.count, MAX_CONDITIONS);
2166 return MCP_RC_BAD_REQUEST;
2167 }
2168
2169 // Add a wildcard search condition for the query
2170 CONDITION *query_condition = &data->request.conditions.items[data->request.conditions.count];
2171 query_condition->column_name = "*";
2172 query_condition->column_index = -1; // Will be set properly during resolve
2173 query_condition->op = OP_MATCH;
2174 query_condition->v_type = COND_VALUE_STRING;
2175 query_condition->v_str = data->request.query;
2176
2177 // Create pattern using substring function (no need to add wildcards manually)
2178 query_condition->pattern = string_to_simple_pattern_nocase_substring(data->request.query);
2179
2180 data->request.conditions.count++;
2181 }
2182
2183 // Find the host
2184 data->request.host = rrdhost_find_by_hostname(data->request.node);
2185 if (!data->request.host) {
2186 data->request.host = rrdhost_find_by_guid(data->request.node);
2187 if (!data->request.host) {
2188 data->request.host = rrdhost_find_by_node_id(data->request.node);
2189 }
2190 }
2191
2192 if (!data->request.host) {
2193 buffer_sprintf(mcpc->error, "Node not found: %s", data->request.node);
2194 return MCP_RC_NOT_FOUND;
2195 }
2196
2197 // Generate transaction UUID
2198 uuid_generate(data->request.transaction_uuid);
2199 uuid_unparse_lower(data->request.transaction_uuid, data->request.transaction);
2200
2201 return MCP_RC_OK;
2202 }
2203
2204 // Execute the function and populate the input section of data
2205 static MCP_RETURN_CODE mcp_function_run(MCP_FUNCTION_DATA *data, BUFFER *payload) {
2206 if (!data || !data->request.host || !data->request.function)
2207 return MCP_RC_ERROR;
2208
2209 // Create source buffer from user_auth
2210 CLEAN_BUFFER *source = buffer_create(0, NULL);
2211 user_auth_to_source_buffer(data->request.auth, source);
2212 buffer_strcat(source, ",modelcontextprotocol");
2213
2214 // Create result buffer (will be owned by data->input.json)
2215 BUFFER *result_buffer = buffer_create(0, NULL);
2216
2217 // Execute the function
2218 int ret = rrd_function_run(
2219 data->request.host,
2220 result_buffer,
2221 (int)data->request.timeout,
2222 data->request.auth->access,
2223 data->request.function,
2224 true,
2225 data->request.transaction,
2226 NULL,
2227 NULL,
2228 NULL,
2229 NULL,
2230 NULL,
2231 NULL,
2232 payload,
2233 buffer_tostring(source),
2234 false
2235 );
2236
2237 if (ret != HTTP_RESP_OK) {
2238 buffer_sprintf(data->request.mcpc->error,
2239 "Failed to execute function '%s' on node '%s', "
2240 "http error code %d (%s):\n"
2241 "```json\n%s\n```",
2242 data->request.function, data->request.node, ret,
2243 http_response_code2string(ret),
2244 buffer_tostring(result_buffer));
2245 buffer_free(result_buffer);
2246 return MCP_RC_ERROR;
2247 }
2248
2249 // Store the result in data->input
2250 data->input.json = result_buffer;
2251 data->input.jobj = json_tokener_parse(buffer_tostring(result_buffer));
2252
2253 // Analyze the response type
2254 int status = 0;
2255 data->input.type = mcp_functions_analyze_response(data->input.jobj, &status);
2256
2257 return MCP_RC_OK;
2258 }
2259
2260
2261 // Process table response
2262 static MCP_RETURN_CODE mcp_functions_process_table(MCP_FUNCTION_DATA *data, MCP_REQUEST_ID id) {
2263 const size_t max_size_threshold = 20UL * 1024;
2264
2265 // Initialize success response
2266 mcp_init_success_result(data->request.mcpc, id);
2267
2268 // Start building content array for the result
2269 buffer_json_member_add_array(data->request.mcpc->result, "content");
2270
2271 if (!data->input.jobj) {
2272 // Not valid JSON - return raw output with message
2273 data->output.status = MCP_TABLE_NOT_JSON;
2274 buffer_strcat(data->output.result, buffer_tostring(data->input.json));
2275 add_table_messages_to_mcp_result(data, NULL);
2276 }
2277 else if (data->input.type == FN_TYPE_NOT_TABLE && data->input.type != FN_TYPE_TABLE_WITH_HISTORY) {
2278 // Not a processable table format
2279 data->output.status = MCP_TABLE_NOT_PROCESSABLE;
2280 buffer_strcat(data->output.result, buffer_tostring(data->input.json));
2281 add_table_messages_to_mcp_result(data, NULL);
2282 }
2283 else {
2284 // It's a processable table - get data and columns
2285 struct json_object *data_obj = NULL;
2286 struct json_object *columns_obj = NULL;
2287
2288 if (!json_object_object_get_ex(data->input.jobj, "data", &data_obj) ||
2289 !json_object_object_get_ex(data->input.jobj, "columns", &columns_obj)) {
2290 // Missing required fields, treat as not processable
2291 data->output.status = MCP_TABLE_NOT_PROCESSABLE;
2292 buffer_strcat(data->output.result, buffer_tostring(data->input.json));
2293 add_table_messages_to_mcp_result(data, NULL);
2294 }
2295 else {
2296 // Check if data is empty
2297 size_t original_row_count = json_object_array_length(data_obj);
2298
2299 if (original_row_count == 0) {
2300 // Empty result
2301 data->output.status = MCP_TABLE_EMPTY_RESULT;
2302 buffer_strcat(data->output.result, buffer_tostring(data->input.json));
2303 add_table_messages_to_mcp_result(data, columns_obj);
2304 }
2305 else {
2306 // Process the table with filtering
2307 mcp_process_table_result(data, max_size_threshold);
2308
2309 // Check for errors or special conditions
2310 if (data->output.status != MCP_TABLE_OK &&
2311 data->output.status != MCP_TABLE_RESPONSE_TOO_BIG &&
2312 data->output.status != MCP_TABLE_INFO_MISSING_COLUMNS_FOUND_RESULTS) {
2313 // Handle errors
2314 add_table_messages_to_mcp_result(data, columns_obj);
2315 }
2316 else {
2317 // Check if we need to add any informational messages
2318 if (data->output.status == MCP_TABLE_INFO_MISSING_COLUMNS_FOUND_RESULTS) {
2319 add_table_messages_to_mcp_result(data, columns_obj);
2320 data->output.status = MCP_TABLE_OK;
2321 }
2322 else if (data->output.status == MCP_TABLE_RESPONSE_TOO_BIG) {
2323 // The table was already processed with limit=1 due to size
2324 // Add the guidance message
2325 add_table_messages_to_mcp_result(data, columns_obj);
2326 }
2327
2328 // Add the final result
2329 buffer_json_add_array_item_object(data->request.mcpc->result);
2330 {
2331 buffer_json_member_add_string(data->request.mcpc->result, "type", "text");
2332 buffer_json_member_add_string(data->request.mcpc->result, "text", buffer_tostring(data->output.result));
2333 }
2334 buffer_json_object_close(data->request.mcpc->result);
2335 }
2336 }
2337 }
2338 }
2339
2340 buffer_json_array_close(data->request.mcpc->result); // Close content array
2341
2342 // Add nextCursor for pagination if applicable (only for successful results)
2343 if (data->pagination.enabled && data->input.rows > 0 && data->output.status == MCP_TABLE_OK) {
2344 usec_t next_cursor_timestamp = calculate_next_cursor_from_input(data);
2345 if (next_cursor_timestamp > 0) {
2346 CLEAN_BUFFER *cursor_str = buffer_create(0, NULL);
2347 buffer_sprintf(cursor_str, "%" PRIu64, next_cursor_timestamp);
2348 buffer_json_member_add_string(data->request.mcpc->result, "nextCursor", buffer_tostring(cursor_str));
2349 }
2350 }
2351
2352 buffer_json_object_close(data->request.mcpc->result); // Close result object
2353 buffer_json_finalize(data->request.mcpc->result); // Finalize the JSON
2354
2355 return MCP_RC_OK;
2356 }
2357
2358 // Check all requirements and violations collectively for a function execution
2359 // Returns true if all checks pass, false if there are violations
2360 // If false is returned, mcpc->error and mcpc->result contain appropriate error messages
2361 static bool check_requirements_and_violations(MCP_FUNCTION_DATA *data,
2362 MCP_FUNCTION_REGISTRY_ENTRY *registry_entry,
2363 struct json_object *selections,
2364 MCP_REQUEST_ID id) {
2365 MCP_CLIENT *mcpc = data->request.mcpc;
2366
2367 // 1. Set booleans for each invalid parameter/violation
2368 bool invalid_timeframe_on_non_history = false;
2369 bool missing_timeframe_on_history = false;
2370 bool invalid_condition_operators = false;
2371 bool invalid_wildcard_patterns = false;
2372 bool invalid_column_sorting = false;
2373 bool missing_required_params = false;
2374 bool invalid_selections_on_non_required = false;
2375
2376 // Arrays to track specific invalid items
2377 CLEAN_BUFFER *invalid_conditions = buffer_create(0, NULL);
2378 CLEAN_BUFFER *missing_params = buffer_create(0, NULL);
2379
2380 // Check 1: Functions with has_history=false should not receive time parameters
2381 if (!registry_entry->has_history) {
2382 if (data->request.after > 0 || data->request.before > 0 ||
2383 data->request.anchor > 0 || data->request.direction) {
2384 invalid_timeframe_on_non_history = true;
2385 }
2386 }
2387
2388 // Check 2: Functions with has_history=true should have required parameters
2389 if (registry_entry->has_history) {
2390 if (data->request.after == 0) {
2391 missing_timeframe_on_history = true;
2392 }
2393
2394 // Check 3: Functions with has_history=true should not use unsupported operations
2395 if (data->request.conditions.count > 0) {
2396 for (size_t i = 0; i < data->request.conditions.count; i++) {
2397 CONDITION *cond = &data->request.conditions.items[i];
2398
2399 // Check for unsupported operators
2400 if (cond->op != OP_EQUALS && cond->op != OP_MATCH) {
2401 invalid_condition_operators = true;
2402 if (buffer_strlen(invalid_conditions) > 0)
2403 buffer_strcat(invalid_conditions, ", ");
2404 buffer_sprintf(invalid_conditions, "'%s' (uses %s operator)",
2405 cond->column_name,
2406 cond->op == OP_NOT_EQUALS ? "!=" :
2407 cond->op == OP_LESS ? "<" :
2408 cond->op == OP_LESS_EQUALS ? "<=" :
2409 cond->op == OP_GREATER ? ">" :
2410 cond->op == OP_GREATER_EQUALS ? ">=" :
2411 cond->op == OP_NOT_MATCH ? "not match" : "unknown");
2412 continue;
2413 }
2414
2415 // Check for pattern matching with wildcards (except for "*" column which is full-text search)
2416 if (cond->op == OP_MATCH && cond->pattern && strcmp(cond->column_name, "*") != 0) {
2417 const char *pattern_str = (cond->v_type == COND_VALUE_STRING) ? cond->v_str : NULL;
2418 if (pattern_str && (strchr(pattern_str, '*') || strchr(pattern_str, '?'))) {
2419 invalid_wildcard_patterns = true;
2420 if (buffer_strlen(invalid_conditions) > 0)
2421 buffer_strcat(invalid_conditions, ", ");
2422 buffer_sprintf(invalid_conditions, "'%s' (uses wildcards in pattern '%s')",
2423 cond->column_name, pattern_str);
2424 }
2425 }
2426 }
2427 }
2428
2429 // Check 4: Functions with has_history=true should not use column sorting
2430 if (data->request.sort.column) {
2431 invalid_column_sorting = true;
2432 }
2433 }
2434
2435 // Check 5: Required parameters validation
2436 if (registry_entry->required_params_count > 0) {
2437 if (!selections) {
2438 missing_required_params = true;
2439 for (size_t i = 0; i < registry_entry->required_params_count; i++) {
2440 const char *param_id = string2str(registry_entry->required_params[i].id);
2441 if (buffer_strlen(missing_params) > 0)
2442 buffer_strcat(missing_params, ", ");
2443 buffer_strcat(missing_params, param_id);
2444 }
2445 } else {
2446 // Check if all required parameters have non-empty values
2447 for (size_t i = 0; i < registry_entry->required_params_count; i++) {
2448 const char *param_id = string2str(registry_entry->required_params[i].id);
2449 MCP_FUNCTION_PARAM *param = &registry_entry->required_params[i];
2450 struct json_object *param_value = NULL;
2451 bool param_invalid = false;
2452
2453 if (!json_object_object_get_ex(selections, param_id, &param_value)) {
2454 param_invalid = true;
2455 } else {
2456 // Validate based on parameter type
2457 if (param->type == MCP_REQUIRED_PARAMS_TYPE_SELECT) {
2458 // For select type, accept either string or array with single element
2459 if (json_object_is_type(param_value, json_type_string)) {
2460 const char *str_val = json_object_get_string(param_value);
2461 if (!str_val || !*str_val) {
2462 param_invalid = true;
2463 }
2464 } else if (json_object_is_type(param_value, json_type_array)) {
2465 if (json_object_array_length(param_value) == 0) {
2466 param_invalid = true;
2467 }
2468 } else {
2469 param_invalid = true;
2470 }
2471 } else {
2472 // For multiselect type, require array with at least one element
2473 if (!json_object_is_type(param_value, json_type_array) ||
2474 json_object_array_length(param_value) == 0) {
2475 param_invalid = true;
2476 }
2477 }
2478 }
2479
2480 if (param_invalid) {
2481 missing_required_params = true;
2482 if (buffer_strlen(missing_params) > 0)
2483 buffer_strcat(missing_params, ", ");
2484 buffer_strcat(missing_params, param_id);
2485 }
2486 }
2487 }
2488 } else {
2489 // Function has no required parameters - selections should not be provided
2490 if (selections && json_object_is_type(selections, json_type_object) &&
2491 json_object_object_length(selections) > 0) {
2492 invalid_selections_on_non_required = true;
2493 }
2494 }
2495
2496 // 2. Check if we have any violations
2497 bool has_violations = invalid_timeframe_on_non_history || missing_timeframe_on_history ||
2498 invalid_condition_operators || invalid_wildcard_patterns ||
2499 invalid_column_sorting || missing_required_params ||
2500 invalid_selections_on_non_required;
2501
2502 if (!has_violations) {
2503 return true;
2504 }
2505
2506 // 3. Generate error response with summary and detailed messages
2507 mcp_init_success_result(mcpc, id);
2508 buffer_json_member_add_array(mcpc->result, "content");
2509 buffer_json_add_array_item_object(mcpc->result);
2510 {
2511 buffer_json_member_add_string(mcpc->result, "type", "text");
2512
2513 CLEAN_BUFFER *message = buffer_create(0, NULL);
2514
2515 // Summary of what's wrong
2516 buffer_strcat(message, "FUNCTION EXECUTION FAILED\n\n");
2517 buffer_strcat(message, "Summary of issues:\n");
2518
2519 int issue_count = 0;
2520 if (invalid_timeframe_on_non_history) {
2521 buffer_sprintf(message, "%d. Invalid timeframe parameters for non-history function\n", ++issue_count);
2522 }
2523 if (missing_timeframe_on_history) {
2524 buffer_sprintf(message, "%d. Missing required timeframe parameters\n", ++issue_count);
2525 }
2526 if (invalid_condition_operators) {
2527 buffer_sprintf(message, "%d. Invalid condition operators used\n", ++issue_count);
2528 }
2529 if (invalid_wildcard_patterns) {
2530 buffer_sprintf(message, "%d. Invalid wildcard patterns in conditions\n", ++issue_count);
2531 }
2532 if (invalid_column_sorting) {
2533 buffer_sprintf(message, "%d. Invalid column sorting for history function\n", ++issue_count);
2534 }
2535 if (missing_required_params) {
2536 buffer_sprintf(message, "%d. Missing or invalid required parameters\n", ++issue_count);
2537 }
2538 if (invalid_selections_on_non_required) {
2539 buffer_sprintf(message, "%d. Invalid selections parameter for function without requirements\n", ++issue_count);
2540 }
2541
2542 // Detailed explanations
2543 buffer_strcat(message, "\nDetailed explanations:\n\n");
2544
2545 if (invalid_timeframe_on_non_history) {
2546 buffer_strcat(message, "❌ TIMEFRAME PARAMETERS NOT SUPPORTED\n");
2547 buffer_strcat(message, " Problem: This function does not support time-based parameters\n");
2548 buffer_strcat(message, " Invalid parameters: after, before, cursor, direction\n");
2549 buffer_strcat(message, " Solution: Remove all timeframe parameters from your request\n\n");
2550 }
2551
2552 if (missing_timeframe_on_history) {
2553 buffer_strcat(message, "❌ MISSING TIMEFRAME PARAMETERS\n");
2554 buffer_strcat(message, " Problem: History functions require the 'after' parameter\n");
2555 buffer_strcat(message, " Required: 'after' parameter must be specified (cannot be 0)\n");
2556 buffer_strcat(message, " Time relationship: 'before' is relative to now, 'after' is relative to 'before'\n");
2557 buffer_strcat(message, " Solution: Add after parameter, e.g., \"after\": -3600 (1 hour before 'before')\n\n");
2558 }
2559
2560
2561 if (invalid_condition_operators) {
2562 buffer_strcat(message, "❌ INVALID CONDITION OPERATORS\n");
2563 buffer_sprintf(message, " Problem: Invalid operators used in conditions for: %s\n", buffer_tostring(invalid_conditions));
2564 buffer_strcat(message, " Allowed: Only '==' (equals) and 'match' (for value lists)\n");
2565 buffer_strcat(message, " Solution: Change operators to '==' or use 'match' with pipe-separated values\n\n");
2566 }
2567
2568 if (invalid_wildcard_patterns) {
2569 buffer_strcat(message, "❌ INVALID WILDCARD PATTERNS\n");
2570 buffer_sprintf(message, " Problem: Wildcard patterns not supported in conditions for: %s\n", buffer_tostring(invalid_conditions));
2571 buffer_strcat(message, " Solution: Use exact values or pipe-separated lists (e.g., 'value1|value2|value3')\n\n");
2572 }
2573
2574 if (invalid_column_sorting) {
2575 buffer_strcat(message, "❌ INVALID COLUMN SORTING\n");
2576 buffer_strcat(message, " Problem: Column-based sorting not supported for history functions\n");
2577 buffer_strcat(message, " Solution: Remove 'sort_column' parameter and use 'direction' for time-based sorting\n\n");
2578 }
2579
2580 if (missing_required_params) {
2581 buffer_strcat(message, "❌ MISSING OR INVALID REQUIRED PARAMETERS\n");
2582 buffer_sprintf(message, " Problem: Required parameters missing or invalid: %s\n", buffer_tostring(missing_params));
2583 buffer_strcat(message, " Solution: Provide all required parameters with valid values\n\n");
2584
2585 // Include the helpful parameter message
2586 generate_required_params_message(message, registry_entry, missing_timeframe_on_history, missing_params);
2587 }
2588
2589 if (invalid_selections_on_non_required) {
2590 buffer_strcat(message, "❌ INVALID SELECTIONS PARAMETER\n");
2591 buffer_strcat(message, " Problem: This function does not accept the 'selections' parameter\n");
2592 buffer_strcat(message, " Solution: Remove the 'selections' parameter from your request\n\n");
2593 }
2594
2595 buffer_json_member_add_string(mcpc->result, "text", buffer_tostring(message));
2596 }
2597 buffer_json_object_close(mcpc->result);
2598 buffer_json_array_close(mcpc->result);
2599 buffer_json_object_close(mcpc->result);
2600 buffer_json_finalize(mcpc->result);
2601 return false;
2602 }
2603
2604 MCP_RETURN_CODE mcp_tool_execute_function_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused)
2605 {
2606 if (!mcpc || !params)
2607 return MCP_RC_ERROR;
2608
2609 // Create and initialize function data structure
2610 MCP_FUNCTION_DATA data;
2611 mcp_functions_data_init(&data);
2612
2613 // Parse the request
2614 MCP_RETURN_CODE rc = mcp_parse_function_request(&data, mcpc, params);
2615 if (rc != MCP_RC_OK) {
2616 mcp_functions_data_cleanup(&data);
2617 return rc;
2618 }
2619
2620 // Get function registry entry
2621 CLEAN_BUFFER *registry_error = buffer_create(0, NULL);
2622 MCP_FUNCTION_REGISTRY_ENTRY *registry_entry = mcp_functions_registry_get(data.request.host, data.request.function, registry_error);
2623
2624 if (!registry_entry) {
2625 buffer_sprintf(mcpc->error, "Failed to get function info: %s", buffer_tostring(registry_error));
2626 mcp_functions_data_cleanup(&data);
2627 return MCP_RC_ERROR;
2628 }
2629
2630 // Check if function requires parameters
2631 struct json_object *selections = NULL;
2632 if (json_object_object_get_ex(params, "selections", &selections)) {
2633 // Selections provided - validate they are an object
2634 if (!json_object_is_type(selections, json_type_object)) {
2635 buffer_strcat(mcpc->error, "The 'selections' parameter must be an object with key-value pairs where values are arrays of strings");
2636 mcp_functions_registry_release(registry_entry);
2637 mcp_functions_data_cleanup(&data);
2638 return MCP_RC_BAD_REQUEST;
2639 }
2640 }
2641
2642 // Check all requirements and violations collectively
2643 if (!check_requirements_and_violations(&data, registry_entry, selections, id)) {
2644 mcp_functions_registry_release(registry_entry);
2645 mcp_functions_data_cleanup(&data);
2646 return MCP_RC_OK; // We've already set up the response in the validation function
2647 }
2648
2649 // Build the actual function name to execute and POST payload if needed
2650 CLEAN_BUFFER *actual_function = buffer_create(0, NULL);
2651 CLEAN_BUFFER *post_payload = NULL;
2652
2653 if (registry_entry->supports_post) {
2654 // v3+ function with POST support - create POST payload (even if no selections)
2655 buffer_strcat(actual_function, data.request.function);
2656 post_payload = build_post_payload_with_selections(selections, &data, registry_entry);
2657 } else if (selections) {
2658 // GET function with parameters - append to function name
2659 build_function_name_with_params(actual_function, data.request.function, selections, &data, registry_entry);
2660 } else {
2661 // No parameters needed or provided
2662 buffer_strcat(actual_function, data.request.function);
2663 }
2664
2665 // Update the function name in data
2666 data.request.function = buffer_tostring(actual_function);
2667
2668 // Copy pagination settings to avoid keeping registry_entry locked
2669 data.pagination.enabled = registry_entry->pagination.enabled;
2670 data.pagination.units = registry_entry->pagination.units;
2671 data.pagination.column = string_dup(registry_entry->pagination.column);
2672
2673 // Release registry entry - we're done with it now
2674 mcp_functions_registry_release(registry_entry);
2675 registry_entry = NULL; // Prevent accidental access
2676
2677 // Execute the function
2678 rc = mcp_function_run(&data, post_payload);
2679 if (rc != MCP_RC_OK) {
2680 mcp_functions_data_cleanup(&data);
2681 return rc;
2682 }
2683
2684 // Process the response (all function types use table processing now)
2685 rc = mcp_functions_process_table(&data, id);
2686
2687 // Cleanup
2688 mcp_functions_data_cleanup(&data);
2689
2690 return rc;
2691 }