| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "mcp-tools-execute-function-registry.h" |
| 4 | #include "database/rrdfunctions.h" |
| 5 | |
| 6 | // Parameter type string mappings |
| 7 | ENUM_STR_MAP_DEFINE(MCP_REQUIRED_PARAMS_TYPE) = { |
| 8 | { MCP_REQUIRED_PARAMS_TYPE_SELECT, "select" }, |
| 9 | { MCP_REQUIRED_PARAMS_TYPE_MULTISELECT, "multiselect" }, |
| 10 | { 0, NULL } |
| 11 | }; |
| 12 | |
| 13 | ENUM_STR_DEFINE_FUNCTIONS(MCP_REQUIRED_PARAMS_TYPE, MCP_REQUIRED_PARAMS_TYPE_SELECT, "select") |
| 14 | |
| 15 | // Pagination units string mappings |
| 16 | ENUM_STR_MAP_DEFINE(MCP_PAGINATION_UNITS) = { |
| 17 | { MCP_PAGINATION_UNITS_TIMESTAMP_USEC, "timestamp_usec" }, |
| 18 | { 0, NULL } |
| 19 | }; |
| 20 | |
| 21 | ENUM_STR_DEFINE_FUNCTIONS(MCP_PAGINATION_UNITS, MCP_PAGINATION_UNITS_UNKNOWN, "unknown") |
| 22 | |
| 23 | // Static dictionary to store function registry entries |
| 24 | static DICTIONARY *functions_registry = NULL; |
| 25 | |
| 26 | // Cleanup function for registry entries |
| 27 | static void registry_entry_cleanup(MCP_FUNCTION_REGISTRY_ENTRY *entry) { |
| 28 | if (!entry) |
| 29 | return; |
| 30 | |
| 31 | // Free STRING pointers |
| 32 | string_freez(entry->help); |
| 33 | |
| 34 | // Free pagination fields |
| 35 | string_freez(entry->pagination.key); |
| 36 | string_freez(entry->pagination.column); |
| 37 | |
| 38 | // Free parameters |
| 39 | for (size_t i = 0; i < entry->required_params_count; i++) { |
| 40 | MCP_FUNCTION_PARAM *param = &entry->required_params[i]; |
| 41 | string_freez(param->id); |
| 42 | string_freez(param->name); |
| 43 | string_freez(param->help); |
| 44 | |
| 45 | // Free options |
| 46 | for (size_t j = 0; j < param->options_count; j++) { |
| 47 | string_freez(param->options[j].id); |
| 48 | string_freez(param->options[j].name); |
| 49 | string_freez(param->options[j].info); |
| 50 | } |
| 51 | freez(param->options); |
| 52 | } |
| 53 | freez(entry->required_params); |
| 54 | } |
| 55 | |
| 56 | // Dictionary callbacks |
| 57 | static void registry_entry_insert_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 58 | MCP_FUNCTION_REGISTRY_ENTRY *entry = (MCP_FUNCTION_REGISTRY_ENTRY *)value; |
| 59 | |
| 60 | rw_spinlock_init(&entry->spinlock); |
| 61 | spinlock_init(&entry->update_spinlock); |
| 62 | } |
| 63 | |
| 64 | static bool registry_entry_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data __maybe_unused) { |
| 65 | MCP_FUNCTION_REGISTRY_ENTRY *old_entry = (MCP_FUNCTION_REGISTRY_ENTRY *)old_value; |
| 66 | MCP_FUNCTION_REGISTRY_ENTRY *new_entry = (MCP_FUNCTION_REGISTRY_ENTRY *)new_value; |
| 67 | |
| 68 | // Get write lock on the old entry |
| 69 | rw_spinlock_write_lock(&old_entry->spinlock); |
| 70 | |
| 71 | // Swap all members between old and new |
| 72 | // This moves the old data to new_entry and new data to old_entry |
| 73 | SWAP(old_entry->type, new_entry->type); |
| 74 | SWAP(old_entry->has_history, new_entry->has_history); |
| 75 | SWAP(old_entry->update_every, new_entry->update_every); |
| 76 | SWAP(old_entry->version, new_entry->version); |
| 77 | SWAP(old_entry->supports_post, new_entry->supports_post); |
| 78 | SWAP(old_entry->help, new_entry->help); |
| 79 | SWAP(old_entry->required_params_count, new_entry->required_params_count); |
| 80 | SWAP(old_entry->required_params, new_entry->required_params); |
| 81 | SWAP(old_entry->pagination, new_entry->pagination); |
| 82 | SWAP(old_entry->last_update, new_entry->last_update); |
| 83 | SWAP(old_entry->expires, new_entry->expires); |
| 84 | |
| 85 | // Release the lock |
| 86 | rw_spinlock_write_unlock(&old_entry->spinlock); |
| 87 | |
| 88 | // Clean up the new entry (which now contains the old data) |
| 89 | registry_entry_cleanup(new_entry); |
| 90 | |
| 91 | // Return false to reject the new value (we've already updated old_value) |
| 92 | return false; |
| 93 | } |
| 94 | |
| 95 | static void registry_entry_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 96 | MCP_FUNCTION_REGISTRY_ENTRY *entry = (MCP_FUNCTION_REGISTRY_ENTRY *)value; |
| 97 | |
| 98 | // Clean up the entry data |
| 99 | registry_entry_cleanup(entry); |
| 100 | } |
| 101 | |
| 102 | // Initialize the functions-registry |
| 103 | void mcp_functions_registry_init(void) { |
| 104 | if (functions_registry) |
| 105 | return; |
| 106 | |
| 107 | functions_registry = dictionary_create_advanced( |
| 108 | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, |
| 109 | NULL, |
| 110 | sizeof(MCP_FUNCTION_REGISTRY_ENTRY)); |
| 111 | |
| 112 | dictionary_register_insert_callback(functions_registry, registry_entry_insert_callback, NULL); |
| 113 | dictionary_register_delete_callback(functions_registry, registry_entry_delete_callback, NULL); |
| 114 | dictionary_register_conflict_callback(functions_registry, registry_entry_conflict_callback, NULL); |
| 115 | } |
| 116 | |
| 117 | // Clean up the functions-registry |
| 118 | void mcp_functions_registry_cleanup(void) { |
| 119 | if (!functions_registry) |
| 120 | return; |
| 121 | |
| 122 | dictionary_destroy(functions_registry); |
| 123 | functions_registry = NULL; |
| 124 | } |
| 125 | |
| 126 | // Parse JSON info response and populate registry entry |
| 127 | static int parse_function_info(struct json_object *json_obj, MCP_FUNCTION_REGISTRY_ENTRY *entry) { |
| 128 | struct json_object *jobj; |
| 129 | |
| 130 | // Parse version (v3+ supports POST) |
| 131 | entry->version = 1; // default to v1 |
| 132 | if (json_object_object_get_ex(json_obj, "v", &jobj)) { |
| 133 | entry->version = json_object_get_int(jobj); |
| 134 | } |
| 135 | entry->supports_post = (entry->version >= 3); |
| 136 | |
| 137 | // Parse type |
| 138 | if (json_object_object_get_ex(json_obj, "type", &jobj)) { |
| 139 | const char *type_str = json_object_get_string(jobj); |
| 140 | if (strcmp(type_str, "table") == 0) { |
| 141 | entry->type = FN_TYPE_TABLE; |
| 142 | } else { |
| 143 | entry->type = FN_TYPE_UNKNOWN; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | // Parse has_history |
| 148 | if (json_object_object_get_ex(json_obj, "has_history", &jobj)) { |
| 149 | entry->has_history = json_object_get_boolean(jobj); |
| 150 | if (entry->has_history && entry->type == FN_TYPE_TABLE) { |
| 151 | entry->type = FN_TYPE_TABLE_WITH_HISTORY; |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | // Parse update_every |
| 156 | if (json_object_object_get_ex(json_obj, "update_every", &jobj)) { |
| 157 | entry->update_every = json_object_get_int(jobj); |
| 158 | } |
| 159 | |
| 160 | // Parse help |
| 161 | if (json_object_object_get_ex(json_obj, "help", &jobj)) { |
| 162 | entry->help = string_strdupz(json_object_get_string(jobj)); |
| 163 | } |
| 164 | |
| 165 | // Parse accepted_params to detect supported optional parameters |
| 166 | if (json_object_object_get_ex(json_obj, "accepted_params", &jobj)) { |
| 167 | if (json_object_is_type(jobj, json_type_array)) { |
| 168 | size_t accepted_count = json_object_array_length(jobj); |
| 169 | for (size_t i = 0; i < accepted_count; i++) { |
| 170 | struct json_object *param_obj = json_object_array_get_idx(jobj, i); |
| 171 | if (param_obj && json_object_is_type(param_obj, json_type_string)) { |
| 172 | const char *param_name = json_object_get_string(param_obj); |
| 173 | |
| 174 | // Check for timeframe parameters |
| 175 | if (strcmp(param_name, "after") == 0 || strcmp(param_name, "before") == 0) { |
| 176 | entry->has_timeframe = true; |
| 177 | } |
| 178 | else if (strcmp(param_name, "last") == 0) { |
| 179 | entry->has_last = true; |
| 180 | } |
| 181 | else if (strcmp(param_name, "data_only") == 0) { |
| 182 | entry->has_data_only = true; |
| 183 | } |
| 184 | else if (strcmp(param_name, "direction") == 0) { |
| 185 | entry->has_direction = true; |
| 186 | } |
| 187 | else if (strcmp(param_name, "query") == 0) { |
| 188 | entry->has_query = true; |
| 189 | } |
| 190 | else if (strcmp(param_name, "slice") == 0) { |
| 191 | entry->has_slice = true; |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // Parse pagination object |
| 199 | if (json_object_object_get_ex(json_obj, "pagination", &jobj)) { |
| 200 | if (json_object_is_type(jobj, json_type_object)) { |
| 201 | struct json_object *field; |
| 202 | |
| 203 | // Parse enabled flag |
| 204 | if (json_object_object_get_ex(jobj, "enabled", &field) && |
| 205 | json_object_is_type(field, json_type_boolean)) { |
| 206 | entry->pagination.enabled = json_object_get_boolean(field); |
| 207 | } |
| 208 | |
| 209 | // Only parse other fields if pagination is enabled |
| 210 | if (entry->pagination.enabled) { |
| 211 | // Parse key |
| 212 | if (json_object_object_get_ex(jobj, "key", &field) && |
| 213 | json_object_is_type(field, json_type_string)) { |
| 214 | entry->pagination.key = string_strdupz(json_object_get_string(field)); |
| 215 | } |
| 216 | |
| 217 | // Parse column |
| 218 | if (json_object_object_get_ex(jobj, "column", &field) && |
| 219 | json_object_is_type(field, json_type_string)) { |
| 220 | entry->pagination.column = string_strdupz(json_object_get_string(field)); |
| 221 | } |
| 222 | |
| 223 | // Parse units |
| 224 | if (json_object_object_get_ex(jobj, "units", &field) && |
| 225 | json_object_is_type(field, json_type_string)) { |
| 226 | const char *units_str = json_object_get_string(field); |
| 227 | entry->pagination.units = MCP_PAGINATION_UNITS_2id(units_str); |
| 228 | |
| 229 | // If units are unknown, disable pagination |
| 230 | if (entry->pagination.units == MCP_PAGINATION_UNITS_UNKNOWN) { |
| 231 | entry->pagination.enabled = false; |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // Parse required_params |
| 239 | if (json_object_object_get_ex(json_obj, "required_params", &jobj)) { |
| 240 | if (json_object_is_type(jobj, json_type_array)) { |
| 241 | entry->required_params_count = json_object_array_length(jobj); |
| 242 | if (entry->required_params_count > 0) { |
| 243 | entry->required_params = callocz(entry->required_params_count, sizeof(MCP_FUNCTION_PARAM)); |
| 244 | |
| 245 | for (size_t i = 0; i < entry->required_params_count; i++) { |
| 246 | struct json_object *param_obj = json_object_array_get_idx(jobj, i); |
| 247 | MCP_FUNCTION_PARAM *param = &entry->required_params[i]; |
| 248 | |
| 249 | struct json_object *field; |
| 250 | |
| 251 | // Parse param fields |
| 252 | if (json_object_object_get_ex(param_obj, "id", &field)) |
| 253 | param->id = string_strdupz(json_object_get_string(field)); |
| 254 | |
| 255 | if (json_object_object_get_ex(param_obj, "name", &field)) |
| 256 | param->name = string_strdupz(json_object_get_string(field)); |
| 257 | |
| 258 | if (json_object_object_get_ex(param_obj, "help", &field)) |
| 259 | param->help = string_strdupz(json_object_get_string(field)); |
| 260 | |
| 261 | if (json_object_object_get_ex(param_obj, "type", &field)) { |
| 262 | const char *type_str = json_object_get_string(field); |
| 263 | param->type = MCP_REQUIRED_PARAMS_TYPE_2id(type_str); |
| 264 | } |
| 265 | |
| 266 | if (json_object_object_get_ex(param_obj, "unique_view", &field)) |
| 267 | param->unique_view = json_object_get_boolean(field); |
| 268 | |
| 269 | // Parse options |
| 270 | if (json_object_object_get_ex(param_obj, "options", &field)) { |
| 271 | if (json_object_is_type(field, json_type_array)) { |
| 272 | param->options_count = json_object_array_length(field); |
| 273 | if (param->options_count > 0) { |
| 274 | param->options = callocz(param->options_count, sizeof(MCP_FUNCTION_PARAM_OPTION)); |
| 275 | |
| 276 | for (size_t j = 0; j < param->options_count; j++) { |
| 277 | struct json_object *opt_obj = json_object_array_get_idx(field, j); |
| 278 | MCP_FUNCTION_PARAM_OPTION *opt = ¶m->options[j]; |
| 279 | |
| 280 | struct json_object *opt_field; |
| 281 | if (json_object_object_get_ex(opt_obj, "id", &opt_field)) |
| 282 | opt->id = string_strdupz(json_object_get_string(opt_field)); |
| 283 | |
| 284 | if (json_object_object_get_ex(opt_obj, "name", &opt_field)) |
| 285 | opt->name = string_strdupz(json_object_get_string(opt_field)); |
| 286 | |
| 287 | if (json_object_object_get_ex(opt_obj, "info", &opt_field)) |
| 288 | opt->info = string_strdupz(json_object_get_string(opt_field)); |
| 289 | } |
| 290 | } |
| 291 | } |
| 292 | } |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | time_t now = now_realtime_sec(); |
| 299 | entry->last_update = now; |
| 300 | entry->expires = now + MCP_FUNCTIONS_REGISTRY_TTL; |
| 301 | |
| 302 | return 0; |
| 303 | } |
| 304 | |
| 305 | // Fetch function info from the node (private function) |
| 306 | static MCP_FUNCTION_REGISTRY_ENTRY *mcp_function_get_info(RRDHOST *host, const char *function_name, BUFFER *error) { |
| 307 | if (!host || !function_name) { |
| 308 | buffer_strcat(error, "Invalid host or function name"); |
| 309 | return NULL; |
| 310 | } |
| 311 | |
| 312 | // Prepare the info request |
| 313 | char info_function[256]; |
| 314 | snprintfz(info_function, sizeof(info_function), "%s info", function_name); |
| 315 | |
| 316 | USER_AUTH auth = { |
| 317 | .user_role = HTTP_USER_ROLE_ADMIN, |
| 318 | .access = HTTP_ACCESS_ALL, |
| 319 | .method = USER_AUTH_METHOD_GOD, |
| 320 | .client_ip = "mcp-info", |
| 321 | .client_name = "mcp-tools-execute-function-registry", |
| 322 | }; |
| 323 | |
| 324 | // Create a source buffer from user_auth |
| 325 | CLEAN_BUFFER *source = buffer_create(0, NULL); |
| 326 | user_auth_to_source_buffer(&auth, source); |
| 327 | buffer_strcat(source, ",modelcontextprotocol"); |
| 328 | |
| 329 | // Call the function with info parameter |
| 330 | BUFFER *response = buffer_create(0, NULL); |
| 331 | int code = rrd_function_run( |
| 332 | host, |
| 333 | response, |
| 334 | 10, |
| 335 | auth.access, |
| 336 | info_function, |
| 337 | true, |
| 338 | NULL, |
| 339 | NULL, |
| 340 | NULL, |
| 341 | NULL, |
| 342 | NULL, |
| 343 | NULL, |
| 344 | NULL, |
| 345 | NULL, |
| 346 | buffer_tostring(source), |
| 347 | false); |
| 348 | |
| 349 | if (code != HTTP_RESP_OK) { |
| 350 | buffer_sprintf(error, "Failed to get function info: HTTP %d", code); |
| 351 | buffer_free(response); |
| 352 | return NULL; |
| 353 | } |
| 354 | |
| 355 | // Parse JSON response |
| 356 | struct json_tokener *tokener = json_tokener_new(); |
| 357 | struct json_object *json_obj = json_tokener_parse_ex(tokener, buffer_tostring(response), buffer_strlen(response)); |
| 358 | json_tokener_free(tokener); |
| 359 | buffer_free(response); |
| 360 | |
| 361 | if (!json_obj) { |
| 362 | buffer_strcat(error, "Failed to parse JSON response"); |
| 363 | return NULL; |
| 364 | } |
| 365 | |
| 366 | // Check if it's a special info response with required_params |
| 367 | struct json_object *required_params_obj; |
| 368 | if (!json_object_object_get_ex(json_obj, "required_params", &required_params_obj)) { |
| 369 | // This function doesn't support parameters |
| 370 | MCP_FUNCTION_REGISTRY_ENTRY *entry = callocz(1, sizeof(MCP_FUNCTION_REGISTRY_ENTRY)); |
| 371 | parse_function_info(json_obj, entry); |
| 372 | json_object_put(json_obj); |
| 373 | return entry; |
| 374 | } |
| 375 | |
| 376 | // Create and populate registry entry |
| 377 | MCP_FUNCTION_REGISTRY_ENTRY *entry = callocz(1, sizeof(MCP_FUNCTION_REGISTRY_ENTRY)); |
| 378 | if (parse_function_info(json_obj, entry) != 0) { |
| 379 | json_object_put(json_obj); |
| 380 | freez(entry); |
| 381 | buffer_strcat(error, "Failed to parse function info"); |
| 382 | return NULL; |
| 383 | } |
| 384 | |
| 385 | json_object_put(json_obj); |
| 386 | return entry; |
| 387 | } |
| 388 | |
| 389 | // Create a dictionary key from host and function name |
| 390 | static void create_registry_key(BUFFER *key_buffer, RRDHOST *host, const char *function_name) { |
| 391 | buffer_flush(key_buffer); |
| 392 | buffer_sprintf(key_buffer, "%s|%s", rrdhost_hostname(host), function_name); |
| 393 | } |
| 394 | |
| 395 | // Get a registry entry for a function |
| 396 | MCP_FUNCTION_REGISTRY_ENTRY *mcp_functions_registry_get(RRDHOST *host, const char *function_name, BUFFER *error) { |
| 397 | if (!functions_registry) { |
| 398 | buffer_strcat(error, "Functions registry not initialized"); |
| 399 | return NULL; |
| 400 | } |
| 401 | |
| 402 | // Create a dictionary key |
| 403 | CLEAN_BUFFER *key = buffer_create(0, NULL); |
| 404 | create_registry_key(key, host, function_name); |
| 405 | const char *key_str = buffer_tostring(key); |
| 406 | |
| 407 | time_t now = now_realtime_sec(); |
| 408 | |
| 409 | // Try to get an existing entry |
| 410 | MCP_FUNCTION_REGISTRY_ENTRY *entry = dictionary_get(functions_registry, key_str); |
| 411 | MCP_FUNCTION_REGISTRY_ENTRY *old_entry = NULL; |
| 412 | |
| 413 | if(entry && entry->last_update + MCP_FUNCTIONS_REGISTRY_TTL < now && spinlock_trylock(&entry->update_spinlock)) { |
| 414 | old_entry = entry; |
| 415 | entry = NULL; |
| 416 | } |
| 417 | |
| 418 | if(!entry) { |
| 419 | MCP_FUNCTION_REGISTRY_ENTRY *new_info = mcp_function_get_info(host, function_name, error); |
| 420 | if(new_info) { |
| 421 | entry = dictionary_set(functions_registry, key_str, new_info, sizeof(MCP_FUNCTION_REGISTRY_ENTRY)); |
| 422 | freez(new_info); |
| 423 | } |
| 424 | else if(old_entry) |
| 425 | entry = old_entry; |
| 426 | else |
| 427 | return NULL; |
| 428 | } |
| 429 | |
| 430 | if(old_entry) |
| 431 | spinlock_unlock(&old_entry->update_spinlock); |
| 432 | |
| 433 | if(entry) |
| 434 | rw_spinlock_read_lock(&entry->spinlock); |
| 435 | |
| 436 | return entry; |
| 437 | } |
| 438 | |
| 439 | // Release a registry entry |
| 440 | void mcp_functions_registry_release(MCP_FUNCTION_REGISTRY_ENTRY *entry) { |
| 441 | if (!entry) |
| 442 | return; |
| 443 | |
| 444 | rw_spinlock_read_unlock(&entry->spinlock); |
| 445 | } |