master
c 445 lines 17.1 KB
Raw
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 = &param->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 }