master
c 700 lines 24.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "mcp-params.h"
4
5 // Utility function to check if a string contains wildcards
6 static bool contains_wildcards(const char *str) {
7 if (!str || !*str) return false;
8
9 if(*str == '!') {
10 // simple patterns negative match (only if it starts with '!')
11 return true;
12 }
13
14 // Check for common wildcard characters
15 for (const char *p = str; *p; p++) {
16 if (*p == '*') {
17 // simple patterns wildcard match
18 return true;
19 }
20
21 // let's check for simple pattern separators
22 for (const char *c = SIMPLE_PATTERN_DEFAULT_WEB_SEPARATORS; *c ; c++) {
23 if (*p == *c)
24 return true;
25 }
26 }
27 return false;
28 }
29
30 // Convert JSON array to pipe-separated string
31 static int mcp_params_array_to_pipe_string(
32 struct json_object *array_obj,
33 BUFFER *output_buffer,
34 bool allow_wildcards
35 ) {
36 if (!array_obj || !output_buffer)
37 return -1;
38
39 if (!json_object_is_type(array_obj, json_type_array))
40 return -1;
41
42 size_t array_len = json_object_array_length(array_obj);
43 if (array_len == 0)
44 return 0; // Empty array is valid
45
46 for (size_t i = 0; i < array_len; i++) {
47 struct json_object *item = json_object_array_get_idx(array_obj, i);
48 if (!json_object_is_type(item, json_type_string))
49 return -1;
50
51 const char *str = json_object_get_string(item);
52 if (!str || !*str)
53 return -1;
54
55 // Check for wildcards if not allowed
56 if (!allow_wildcards && contains_wildcards(str))
57 return -2;
58
59 if (i > 0)
60 buffer_strcat(output_buffer, "|");
61 buffer_strcat(output_buffer, str);
62 }
63
64 return 0;
65 }
66
67 // Parse array parameters (nodes, instances, dimensions)
68 BUFFER *mcp_params_parse_array_to_pattern(
69 struct json_object *params,
70 const char *param_name,
71 bool required,
72 bool allow_wildcards,
73 const char *list_tool,
74 BUFFER *error
75 ) {
76 struct json_object *array_obj = NULL;
77
78 if (!json_object_object_get_ex(params, param_name, &array_obj) || !array_obj) {
79 if (required && error) {
80 buffer_flush(error);
81 buffer_sprintf(error, "Missing required parameter '%s'", param_name);
82 }
83 return NULL; // Parameter not provided
84 }
85
86 if (!json_object_is_type(array_obj, json_type_array)) {
87 if (error) {
88 buffer_flush(error);
89 buffer_sprintf(error, "%s must be an array of strings, not %s",
90 param_name, json_type_to_name(json_object_get_type(array_obj)));
91 }
92 return NULL;
93 }
94
95 // Check for an empty array if required
96 if (required && json_object_array_length(array_obj) == 0) {
97 if (error) {
98 buffer_flush(error);
99 buffer_sprintf(error, "The '%s' parameter cannot be an empty array", param_name);
100 }
101 return NULL;
102 }
103
104 BUFFER *wb = buffer_create(256, NULL);
105 int result = mcp_params_array_to_pipe_string(array_obj, wb, allow_wildcards);
106
107 if (result == -2) {
108 buffer_free(wb);
109 if (error) {
110 buffer_flush(error);
111 if (allow_wildcards) {
112 buffer_strcat(error, "invalid array format");
113 } else {
114 if (list_tool) {
115 buffer_sprintf(error,
116 "must contain exact values, not patterns. "
117 "Wildcards are not supported. "
118 "Use the '%s' tool to discover exact values.", list_tool);
119 } else {
120 buffer_strcat(error,
121 "must contain exact values, not patterns. Wildcards are not supported.");
122 }
123 }
124 }
125 return NULL;
126 } else if (result != 0) {
127 buffer_free(wb);
128 if (error) {
129 buffer_flush(error);
130 buffer_sprintf(error, "%s must be an array of strings", param_name);
131 }
132 return NULL;
133 }
134
135 return wb; // Success - return the buffer
136 }
137
138 // Parse labels object parameter
139 BUFFER *mcp_params_parse_labels_object(
140 struct json_object *params,
141 const char *list_tool,
142 BUFFER *error
143 ) {
144 struct json_object *labels_obj = NULL;
145
146 if (!json_object_object_get_ex(params, "labels", &labels_obj) || !labels_obj)
147 return NULL; // Parameter not provided
148
149 if (!json_object_is_type(labels_obj, json_type_object)) {
150 if (error) {
151 buffer_flush(error);
152 buffer_strcat(error, "must be an object where each key maps to an array of string values");
153 }
154 return NULL;
155 }
156
157 BUFFER *wb = buffer_create(256, NULL);
158 int first = 1;
159
160 struct json_object_iterator it = json_object_iter_begin(labels_obj);
161 struct json_object_iterator itEnd = json_object_iter_end(labels_obj);
162
163 while (!json_object_iter_equal(&it, &itEnd)) {
164 const char *key = json_object_iter_peek_name(&it);
165 struct json_object *val = json_object_iter_peek_value(&it);
166
167 if (!json_object_is_type(val, json_type_array)) {
168 buffer_free(wb);
169 if (error) {
170 buffer_flush(error);
171 buffer_strcat(error, "each label key must map to an array of string values");
172 }
173 return NULL;
174 }
175
176 size_t array_len = json_object_array_length(val);
177 for (size_t i = 0; i < array_len; i++) {
178 struct json_object *item = json_object_array_get_idx(val, i);
179 if (!json_object_is_type(item, json_type_string)) {
180 buffer_free(wb);
181 if (error) {
182 buffer_flush(error);
183 buffer_strcat(error, "label values must be strings");
184 }
185 return NULL;
186 }
187
188 const char *value = json_object_get_string(item);
189 if (!value || !*value) {
190 buffer_free(wb);
191 if (error) {
192 buffer_flush(error);
193 buffer_strcat(error, "label values cannot be empty");
194 }
195 return NULL;
196 }
197
198 // Check for wildcards in label values
199 if (contains_wildcards(value)) {
200 buffer_free(wb);
201 if (error) {
202 buffer_flush(error);
203 if (list_tool) {
204 buffer_sprintf(error,
205 "label values must be exact values, not patterns. "
206 "Wildcards are not supported. "
207 "Use the %s tool to discover available label values.", list_tool);
208 } else {
209 buffer_strcat(error,
210 "label values must be exact values, not patterns. Wildcards are not supported.");
211 }
212 }
213 return NULL;
214 }
215
216 if (!first) buffer_strcat(wb, "|");
217 buffer_sprintf(wb, "%s:%s", key, value);
218 first = 0;
219 }
220
221 json_object_iter_next(&it);
222 }
223
224 return wb; // Success
225 }
226
227
228 // Schema generation functions
229
230 // Add array parameter schema
231 void mcp_schema_add_array_param(
232 BUFFER *buffer,
233 const char *param_name,
234 const char *title,
235 const char *description
236 ) {
237 buffer_json_member_add_object(buffer, param_name);
238 {
239 buffer_json_member_add_string(buffer, "type", "array");
240 buffer_json_member_add_string(buffer, "title", title);
241 buffer_json_member_add_string(buffer, "description", description);
242 buffer_json_member_add_object(buffer, "items");
243 buffer_json_member_add_string(buffer, "type", "string");
244 buffer_json_object_close(buffer); // items
245 }
246 buffer_json_object_close(buffer);
247 }
248
249 // Add labels object parameter schema
250 void mcp_schema_add_labels_object(
251 BUFFER *buffer,
252 const char *title,
253 const char *description
254 ) {
255 buffer_json_member_add_object(buffer, "labels");
256 {
257 buffer_json_member_add_string(buffer, "type", "object");
258 buffer_json_member_add_string(buffer, "title",
259 title ? title : "Filter by labels");
260 buffer_json_member_add_string(buffer, "description",
261 description ? description :
262 "Filter using labels where each key maps to an array of exact values. "
263 "Values in the same array are ORed, different keys are ANDed. "
264 "Example: {\"disk_type\": [\"ssd\", \"nvme\"], \"mount_point\": [\"/\"]}\n"
265 "Note: Wildcards are not supported. Use exact label keys and values only.");
266 buffer_json_member_add_object(buffer, "additionalProperties");
267 {
268 buffer_json_member_add_string(buffer, "type", "array");
269 buffer_json_member_add_object(buffer, "items");
270 buffer_json_member_add_string(buffer, "type", "string");
271 buffer_json_object_close(buffer); // items
272 }
273 buffer_json_object_close(buffer); // additionalProperties
274 }
275 buffer_json_object_close(buffer); // labels
276 }
277
278 // Add time window parameters to the schema
279 void mcp_schema_add_time_params(
280 BUFFER *buffer,
281 const char *time_description_prefix,
282 bool required
283 ) {
284 // Build descriptions for after and before
285 BUFFER *after_desc = buffer_create(256, NULL);
286 BUFFER *before_desc = buffer_create(256, NULL);
287
288 if (time_description_prefix && *time_description_prefix) {
289 buffer_sprintf(after_desc, "Start time for %s.", time_description_prefix);
290 buffer_sprintf(before_desc, "End time for %s.", time_description_prefix);
291 } else {
292 buffer_strcat(after_desc, "Start time for the query.");
293 buffer_strcat(before_desc, "End time for the query.");
294 }
295
296 // Use the new function for both parameters
297 mcp_schema_add_time_param(buffer, "after", "Start time", buffer_tostring(after_desc),
298 "'before'", required ? 0 : MCP_DEFAULT_AFTER_TIME, required);
299
300 mcp_schema_add_time_param(buffer, "before", "End time", buffer_tostring(before_desc),
301 "now", required ? 0 : MCP_DEFAULT_BEFORE_TIME, required);
302
303 buffer_free(after_desc);
304 buffer_free(before_desc);
305 }
306
307 // Add cardinality limit parameter to schema
308 void mcp_schema_add_cardinality_limit(
309 BUFFER *buffer,
310 const char *description,
311 size_t default_value,
312 size_t min_value,
313 size_t max_value
314 ) {
315 buffer_json_member_add_object(buffer, "cardinality_limit");
316 {
317 buffer_json_member_add_string(buffer, "type", "number");
318 buffer_json_member_add_string(buffer, "title", "Cardinality Limit");
319 buffer_json_member_add_string(
320 buffer, "description",
321 description ? description : "When multiple nodes, instances, dimensions, labels are queried, "
322 "limit their numbers to prevent response size explosion.");
323
324 if (default_value > 0)
325 buffer_json_member_add_uint64(buffer, "default", default_value);
326
327 buffer_json_member_add_uint64(buffer, "minimum", min_value > 0 ? min_value : 1);
328
329 if (max_value > 0)
330 buffer_json_member_add_uint64(buffer, "maximum", max_value);
331 }
332 buffer_json_object_close(buffer);
333 }
334
335 // Extract string parameter with optional default
336 const char *mcp_params_extract_string(
337 struct json_object *params,
338 const char *param_name,
339 const char *default_value
340 ) {
341 struct json_object *obj = NULL;
342
343 if (json_object_object_get_ex(params, param_name, &obj) && obj) {
344 if (json_object_is_type(obj, json_type_string)) {
345 const char *value = json_object_get_string(obj);
346 if (value && *value) {
347 return value;
348 }
349 }
350 }
351
352 return default_value;
353 }
354
355 // Extract numeric size parameter with bounds checking
356 size_t mcp_params_extract_size(
357 struct json_object *params,
358 const char *param_name,
359 size_t default_value,
360 size_t min_value,
361 size_t max_value,
362 BUFFER *error
363 ) {
364 struct json_object *obj = NULL;
365
366 if (json_object_object_get_ex(params, param_name, &obj) && obj) {
367 if (json_object_is_type(obj, json_type_int)) {
368 int64_t value = json_object_get_int64(obj);
369 if (value < 0) {
370 if (error) {
371 buffer_flush(error);
372 buffer_sprintf(error, "%s must be a positive number", param_name);
373 }
374 return default_value;
375 }
376 size_t size_value = (size_t)value;
377 if (size_value < min_value || (max_value > 0 && size_value > max_value)) {
378 if (error) {
379 buffer_flush(error);
380 if (max_value > 0) {
381 buffer_sprintf(error,
382 "%s must be between %zu and %zu",
383 param_name, min_value, max_value);
384 } else {
385 buffer_sprintf(error,
386 "%s must be at least %zu",
387 param_name, min_value);
388 }
389 }
390 return default_value;
391 }
392 return size_value;
393 }
394 }
395
396 return default_value;
397 }
398
399 // Extract timeout parameter (in seconds)
400 int mcp_params_extract_timeout(
401 struct json_object *params,
402 const char *param_name,
403 int default_seconds,
404 int min_seconds,
405 int max_seconds,
406 BUFFER *error
407 ) {
408 struct json_object *obj = NULL;
409
410 if (json_object_object_get_ex(params, param_name, &obj) && obj) {
411 if (json_object_is_type(obj, json_type_int)) {
412 int value = json_object_get_int(obj);
413 if (value < min_seconds || (max_seconds > 0 && value > max_seconds)) {
414 if (error) {
415 buffer_flush(error);
416 if (max_seconds > 0) {
417 buffer_sprintf(error,
418 "%s must be between %d and %d seconds",
419 param_name, min_seconds, max_seconds);
420 } else {
421 buffer_sprintf(error,
422 "%s must be at least %d seconds",
423 param_name, min_seconds);
424 }
425 }
426 return default_seconds;
427 }
428 return value;
429 }
430 }
431
432 return default_seconds;
433 }
434
435 // Schema generation for timeout parameter
436 void mcp_schema_add_timeout(
437 BUFFER *buffer,
438 const char *param_name,
439 const char *title,
440 const char *description,
441 int default_seconds,
442 int min_seconds,
443 int max_seconds,
444 bool required
445 ) {
446 buffer_json_member_add_object(buffer, param_name);
447 {
448 buffer_json_member_add_string(buffer, "type", "number");
449 buffer_json_member_add_string(buffer, "title", title);
450 buffer_json_member_add_string(buffer, "description", description);
451
452 if (!required && default_seconds >= 0)
453 buffer_json_member_add_int64(buffer, "default", default_seconds);
454
455 if (min_seconds >= 0)
456 buffer_json_member_add_int64(buffer, "minimum", min_seconds);
457
458 if (max_seconds > 0)
459 buffer_json_member_add_int64(buffer, "maximum", max_seconds);
460 }
461 buffer_json_object_close(buffer);
462 }
463
464 // Schema generation for generic string parameter
465 void mcp_schema_add_string_param(
466 BUFFER *buffer,
467 const char *param_name,
468 const char *title,
469 const char *description,
470 const char *default_value,
471 bool required
472 ) {
473 buffer_json_member_add_object(buffer, param_name);
474 {
475 buffer_json_member_add_string(buffer, "type", "string");
476 buffer_json_member_add_string(buffer, "title", title);
477 buffer_json_member_add_string(buffer, "description", description);
478
479 if (!required && default_value)
480 buffer_json_member_add_string(buffer, "default", default_value);
481 }
482 buffer_json_object_close(buffer);
483 }
484
485 // Schema generation for size parameter
486 void mcp_schema_add_size_param(
487 BUFFER *buffer,
488 const char *param_name,
489 const char *title,
490 const char *description,
491 size_t default_value,
492 size_t min_value,
493 size_t max_value,
494 bool required
495 ) {
496 buffer_json_member_add_object(buffer, param_name);
497 {
498 buffer_json_member_add_string(buffer, "type", "number");
499 buffer_json_member_add_string(buffer, "title", title);
500 buffer_json_member_add_string(buffer, "description", description);
501
502 if (!required)
503 buffer_json_member_add_uint64(buffer, "default", default_value);
504
505 if (min_value > 0)
506 buffer_json_member_add_uint64(buffer, "minimum", min_value);
507
508 if (max_value < SIZE_MAX)
509 buffer_json_member_add_uint64(buffer, "maximum", max_value);
510 }
511 buffer_json_object_close(buffer);
512 }
513
514 // Validate and auto-correct time window parameters
515 // Contract: 'after' is relative to 'before', 'before' is relative to 'now'
516 // Since we're a monitoring solution, we almost always work in the past
517 void mcp_params_validate_time_window(time_t *after, time_t *before, time_t now) {
518 if (!now) now = now_realtime_sec();
519
520 // Check if both are relative times (within 3 years of zero)
521 bool after_is_relative = (ABS(*after) <= API_RELATIVE_TIME_MAX);
522 bool before_is_relative = (ABS(*before) <= API_RELATIVE_TIME_MAX);
523
524 if (after_is_relative && before_is_relative) {
525 // Case 1: Both are relative and positive - assistant didn't read instructions
526 if (*after > 0 && *before > 0) {
527 *after = -*after;
528 *before = -*before;
529 }
530 // Case 2: After is positive, before is negative, check if result makes sense
531 else if (*after > 0 && *before <= 0) {
532 // If after + before > 0, the assistant is confused about relative time
533 if (*after + *before > 0) {
534 *after = -*after;
535 }
536 }
537 }
538 }
539
540 // Parse and validate time window parameters (after and before) together
541 // This ensures consistent parsing and validation across all MCP tools
542 bool mcp_params_parse_time_window(
543 struct json_object *params,
544 time_t *after,
545 time_t *before,
546 time_t default_after,
547 time_t default_before,
548 bool allow_both_zero,
549 BUFFER *error
550 ) {
551 if (!after || !before) {
552 if (error) {
553 buffer_flush(error);
554 buffer_strcat(error, "Internal error: after and before pointers cannot be NULL");
555 }
556 return false;
557 }
558
559 // Parse both time parameters
560 *after = mcp_params_parse_time(params, "after", default_after);
561 *before = mcp_params_parse_time(params, "before", default_before);
562
563 // Apply validation and auto-correction
564 mcp_params_validate_time_window(after, before, 0);
565
566 // Basic validation - both cannot be zero (unless explicitly allowed)
567 if (*after == 0 && *before == 0 && !allow_both_zero) {
568 if (error) {
569 buffer_flush(error);
570 buffer_strcat(error, "Invalid time range: both 'after' and 'before' cannot be zero. "
571 "Use negative values for relative times (e.g., after=-3600, before=0 for the last hour) "
572 "or specific timestamps for absolute times.");
573 }
574 return false;
575 }
576
577 // Check if after is later than before (when both are absolute timestamps)
578 bool after_is_absolute = (ABS(*after) > API_RELATIVE_TIME_MAX);
579 bool before_is_absolute = (ABS(*before) > API_RELATIVE_TIME_MAX);
580
581 if (after_is_absolute && before_is_absolute && *after >= *before) {
582 if (error) {
583 buffer_flush(error);
584 buffer_sprintf(error, "Invalid time range: 'after' (%ld) must be earlier than 'before' (%ld) "
585 "when both are absolute timestamps.", *after, *before);
586 }
587 return false;
588 }
589
590 return true;
591 }
592
593 time_t mcp_params_parse_time(struct json_object *params, const char *name, time_t default_value) {
594 if (!params)
595 return default_value;
596
597 struct json_object *obj = NULL;
598 if (!json_object_object_get_ex(params, name, &obj) || !obj)
599 return default_value;
600
601 // First try as integer
602 if (json_object_is_type(obj, json_type_int))
603 return json_object_get_int64(obj);
604
605 // Then try as string
606 if (json_object_is_type(obj, json_type_string)) {
607 const char *val_str = json_object_get_string(obj);
608 if (!val_str || !*val_str)
609 return default_value;
610
611 // Try to parse as RFC3339 first
612 char *endptr = NULL;
613 usec_t timestamp_ut = rfc3339_parse_ut(val_str, &endptr);
614
615 // Check if RFC3339 parsing was successful
616 // Note: We check endptr to see if the entire string was consumed
617 // We don't check timestamp_ut > 0 because dates before 1970 are valid
618 if (endptr && (*endptr == '\0' || isspace((unsigned char)*endptr))) {
619 // Successfully parsed as RFC3339, convert to seconds
620 return (time_t)(timestamp_ut / USEC_PER_SEC);
621 }
622
623 // Check for special "now" keyword first
624 if (strcasecmp(val_str, "now") == 0) {
625 return 0; // "now" means no offset from current time
626 }
627
628 // Try duration parsing for human-readable durations
629 // This handles human-readable durations like:
630 // - "7d", "7 days", "2h", "30m"
631 // - "7 days ago", "2h ago" (negative)
632 // - Complex expressions: "1d12h", "2h30m ago"
633 int64_t duration_seconds;
634 if (duration_parse(val_str, &duration_seconds, "s", "s")) {
635 return (time_t)duration_seconds;
636 }
637
638 // Duration parsing failed, fall back to parsing as integer
639 // This handles:
640 // - Unix timestamps as strings: "1705318200"
641 // - Relative times as strings: "-3600", "-86400"
642 return str2l(val_str);
643 }
644
645 return default_value;
646 }
647
648 // Schema generation for individual time parameter
649 void mcp_schema_add_time_param(
650 BUFFER *buffer,
651 const char *param_name,
652 const char *title,
653 const char *description,
654 const char *relative_to,
655 time_t default_value,
656 bool required
657 ) {
658 buffer_json_member_add_object(buffer, param_name);
659 {
660 buffer_json_member_add_string(buffer, "title", title);
661
662 if (description && *description)
663 buffer_json_member_add_string(buffer, "description", description);
664 else
665 buffer_json_member_add_string(
666 buffer, "description",
667 "Unix epoch timestamp in seconds (e.g. 1705318200), "
668 "number of seconds (use NEGATIVE for past times), "
669 "human-readable duration (e.g. '-7d', '-2h', '-30m', '7 days ago'), "
670 "or RFC3339 datetime string");
671
672 // Use anyOf for multiple types
673 buffer_json_member_add_array(buffer, "anyOf");
674 {
675 buffer_json_add_array_item_object(buffer);
676 {
677 buffer_json_member_add_string(buffer, "type", "number");
678 buffer_json_member_add_sprintf(buffer, "description",
679 "Unix epoch timestamp in seconds (e.g. 1705318200), or number of seconds relative to %s (e.g. -3600 for an hour before %s). NOTE: Use NEGATIVE values for past times.",
680 relative_to ? relative_to : "now", relative_to ? relative_to : "now");
681 }
682 buffer_json_object_close(buffer);
683
684 buffer_json_add_array_item_object(buffer);
685 {
686 buffer_json_member_add_string(buffer, "type", "string");
687 buffer_json_member_add_string(buffer, "description",
688 "RFC3339 datetime string (e.g., \"2024-01-15T10:30:00Z\", \"2024-01-15T10:30:00-05:00\"), "
689 "or human-readable duration (e.g., \"-7d\", \"-2h\", \"-30m\", \"7 days ago\", \"now\"). NOTE: Use NEGATIVE values for past times.");
690 }
691 buffer_json_object_close(buffer);
692 }
693 buffer_json_array_close(buffer);
694
695 if (!required && default_value != 0) {
696 buffer_json_member_add_int64(buffer, "default", default_value);
697 }
698 }
699 buffer_json_object_close(buffer);
700 }