master
c 394 lines 13.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "mcp.h"
4 #include "mcp-initialize.h"
5 #include "mcp-ping.h"
6 #include "mcp-tools.h"
7 #include "mcp-resources.h"
8 #include "mcp-prompts.h"
9 #include "mcp-logging.h"
10 #include "mcp-completion.h"
11 #include "mcp-tools-execute-function-registry.h"
12 #include "web/api/mcp_auth.h"
13
14 static bool mcp_initialized = false;
15
16 // Define the enum to string mapping for protocol versions
17 ENUM_STR_MAP_DEFINE(MCP_PROTOCOL_VERSION) = {
18 { .id = MCP_PROTOCOL_VERSION_2024_11_05, .name = "2024-11-05" },
19 { .id = MCP_PROTOCOL_VERSION_2025_03_26, .name = "2025-03-26" },
20 { .id = MCP_PROTOCOL_VERSION_UNKNOWN, .name = "unknown" },
21
22 // terminator
23 { .name = NULL, .id = 0 }
24 };
25 ENUM_STR_DEFINE_FUNCTIONS(MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSION_UNKNOWN, "unknown");
26
27 // Define the enum to string mapping for return codes
28 ENUM_STR_MAP_DEFINE(MCP_RETURN_CODE) = {
29 { .id = MCP_RC_OK, .name = "OK" },
30 { .id = MCP_RC_ERROR, .name = "ERROR" },
31 { .id = MCP_RC_INVALID_PARAMS, .name = "INVALID_PARAMS" },
32 { .id = MCP_RC_NOT_FOUND, .name = "NOT_FOUND" },
33 { .id = MCP_RC_INTERNAL_ERROR, .name = "INTERNAL_ERROR" },
34 { .id = MCP_RC_NOT_IMPLEMENTED, .name = "NOT_IMPLEMENTED" },
35 { .id = MCP_RC_BAD_REQUEST, .name = "BAD_REQUEST" },
36
37 // terminator
38 { .name = NULL, .id = 0 }
39 };
40 ENUM_STR_DEFINE_FUNCTIONS(MCP_RETURN_CODE, MCP_RC_ERROR, "ERROR");
41
42 // Define the enum to string mapping for logging levels
43 ENUM_STR_MAP_DEFINE(MCP_LOGGING_LEVEL) = {
44 { .id = MCP_LOGGING_LEVEL_DEBUG, .name = "debug" },
45 { .id = MCP_LOGGING_LEVEL_INFO, .name = "info" },
46 { .id = MCP_LOGGING_LEVEL_NOTICE, .name = "notice" },
47 { .id = MCP_LOGGING_LEVEL_WARNING, .name = "warning" },
48 { .id = MCP_LOGGING_LEVEL_ERROR, .name = "error" },
49 { .id = MCP_LOGGING_LEVEL_CRITICAL, .name = "critical" },
50 { .id = MCP_LOGGING_LEVEL_ALERT, .name = "alert" },
51 { .id = MCP_LOGGING_LEVEL_EMERGENCY, .name = "emergency" },
52 { .id = MCP_LOGGING_LEVEL_UNKNOWN, .name = "unknown" },
53
54 // terminator
55 { .name = NULL, .id = 0 }
56 };
57 ENUM_STR_DEFINE_FUNCTIONS(MCP_LOGGING_LEVEL, MCP_LOGGING_LEVEL_UNKNOWN, "unknown");
58
59 // Create a response context for a transport session
60 MCP_CLIENT *mcp_create_client(MCP_TRANSPORT transport, void *transport_ctx) {
61 MCP_CLIENT *mcpc = callocz(1, sizeof(MCP_CLIENT));
62
63 mcpc->transport = transport;
64 mcpc->protocol_version = MCP_PROTOCOL_VERSION_UNKNOWN; // Will be set during initialization
65 mcpc->ready = false; // Client is not ready until initialized notification is received
66
67 // Set capabilities based on transport type
68 switch (transport) {
69 case MCP_TRANSPORT_WEBSOCKET:
70 mcpc->websocket = (struct websocket_server_client *)transport_ctx;
71 mcpc->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
72 MCP_CAPABILITY_SUBSCRIPTIONS |
73 MCP_CAPABILITY_NOTIFICATIONS;
74 break;
75
76 case MCP_TRANSPORT_HTTP:
77 mcpc->http = (struct web_client *)transport_ctx;
78 mcpc->capabilities = MCP_CAPABILITY_NONE; // HTTP has no special capabilities
79 break;
80
81 case MCP_TRANSPORT_SSE:
82 mcpc->http = (struct web_client *)transport_ctx;
83 mcpc->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
84 MCP_CAPABILITY_SUBSCRIPTIONS |
85 MCP_CAPABILITY_NOTIFICATIONS;
86 break;
87
88 default:
89 mcpc->generic = transport_ctx;
90 mcpc->capabilities = MCP_CAPABILITY_NONE;
91 break;
92 }
93
94 // Default client info (will be updated later from actual client)
95 mcpc->client_name = string_strdupz("unknown");
96 mcpc->client_version = string_strdupz("0.0.0");
97
98 // Set default logging level to info
99 mcpc->logging_level = MCP_LOGGING_LEVEL_INFO;
100
101 // Persistent buffers
102 mcpc->error = buffer_create(1024, NULL);
103 mcpc->result = NULL;
104
105 mcpc->last_return_code = MCP_RC_OK;
106 mcpc->last_response_error = false;
107
108 return mcpc;
109 }
110
111 // Free a response context
112 void mcp_free_client(MCP_CLIENT *mcpc) {
113 if (!mcpc)
114 return;
115
116 string_freez(mcpc->client_name);
117 string_freez(mcpc->client_version);
118
119 if (mcpc->error)
120 buffer_free(mcpc->error);
121
122 mcp_client_release_response(mcpc);
123
124 freez(mcpc);
125 }
126
127 void mcp_client_clear_error(MCP_CLIENT *mcpc) {
128 if (mcpc && mcpc->error)
129 buffer_reset(mcpc->error);
130 }
131
132 static void mcp_client_free_chunks(MCP_CLIENT *mcpc) {
133 if (!mcpc || !mcpc->response_chunks)
134 return;
135
136 for (size_t i = 0; i < mcpc->response_chunks_used; i++) {
137 if (mcpc->response_chunks[i].buffer)
138 buffer_free(mcpc->response_chunks[i].buffer);
139 }
140
141 freez(mcpc->response_chunks);
142 mcpc->response_chunks = NULL;
143 mcpc->response_chunks_used = 0;
144 mcpc->response_chunks_size = 0;
145 mcpc->result = NULL;
146 }
147
148 void mcp_client_prepare_response(MCP_CLIENT *mcpc) {
149 if (!mcpc)
150 return;
151
152 mcp_client_free_chunks(mcpc);
153 mcpc->last_return_code = MCP_RC_OK;
154 mcpc->last_response_error = false;
155 }
156
157 void mcp_client_release_response(MCP_CLIENT *mcpc) {
158 mcp_client_free_chunks(mcpc);
159 }
160
161 static struct mcp_response_chunk *mcp_response_append_chunk(MCP_CLIENT *mcpc, enum mcp_response_chunk_type type) {
162 if (!mcpc)
163 return NULL;
164
165 const size_t MAX_RESPONSE_BYTES = 16 * 1024 * 1024; // 16 MiB per request safeguard
166 if (mcp_client_response_size(mcpc) >= MAX_RESPONSE_BYTES) {
167 netdata_log_error("MCP: response size limit reached");
168 return NULL;
169 }
170
171 if (mcpc->response_chunks_used == mcpc->response_chunks_size) {
172 size_t new_size = mcpc->response_chunks_size ? mcpc->response_chunks_size * 2 : 4;
173 struct mcp_response_chunk *tmp = reallocz(mcpc->response_chunks, new_size * sizeof(*tmp));
174 if (unlikely(!tmp))
175 return NULL;
176 mcpc->response_chunks = tmp;
177 mcpc->response_chunks_size = new_size;
178 }
179
180 struct mcp_response_chunk *chunk = &mcpc->response_chunks[mcpc->response_chunks_used++];
181 chunk->buffer = NULL;
182 chunk->type = type;
183 return chunk;
184 }
185
186 BUFFER *mcp_response_add_json_chunk(MCP_CLIENT *mcpc, size_t initial_capacity) {
187 struct mcp_response_chunk *chunk = mcp_response_append_chunk(mcpc, MCP_RESPONSE_CHUNK_JSON);
188 if (!chunk)
189 return NULL;
190
191 size_t capacity = initial_capacity ? initial_capacity : 4096;
192 chunk->buffer = buffer_create(capacity, NULL);
193 mcpc->result = chunk->buffer;
194 buffer_json_initialize(chunk->buffer, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
195 return chunk->buffer;
196 }
197
198 BUFFER *mcp_response_add_text_chunk(MCP_CLIENT *mcpc, size_t initial_capacity) {
199 struct mcp_response_chunk *chunk = mcp_response_append_chunk(mcpc, MCP_RESPONSE_CHUNK_TEXT);
200 if (!chunk)
201 return NULL;
202
203 size_t capacity = initial_capacity ? initial_capacity : 1024;
204 chunk->buffer = buffer_create(capacity, NULL);
205 chunk->buffer->content_type = CT_TEXT_PLAIN;
206 buffer_no_cacheable(chunk->buffer);
207 mcpc->result = chunk->buffer;
208 return chunk->buffer;
209 }
210
211 size_t mcp_client_response_chunk_count(const MCP_CLIENT *mcpc) {
212 return mcpc ? mcpc->response_chunks_used : 0;
213 }
214
215 const struct mcp_response_chunk *mcp_client_response_chunks(const MCP_CLIENT *mcpc) {
216 return mcpc ? mcpc->response_chunks : NULL;
217 }
218
219 size_t mcp_client_response_size(const MCP_CLIENT *mcpc) {
220 if (!mcpc || !mcpc->response_chunks)
221 return 0;
222
223 size_t total = 0;
224 for (size_t i = 0; i < mcpc->response_chunks_used; i++) {
225 if (mcpc->response_chunks[i].buffer)
226 total += buffer_strlen(mcpc->response_chunks[i].buffer);
227 }
228 return total;
229 }
230
231 const char *mcp_client_error_message(MCP_CLIENT *mcpc) {
232 if (!mcpc || !mcpc->error)
233 return NULL;
234 return buffer_strlen(mcpc->error) ? buffer_tostring(mcpc->error) : NULL;
235 }
236
237 void mcp_init_success_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id __maybe_unused) {
238 if (!mcpc)
239 return;
240
241 BUFFER *chunk = mcp_response_add_json_chunk(mcpc, 4096);
242 if (!chunk)
243 return;
244
245 mcpc->last_return_code = MCP_RC_OK;
246 mcpc->last_response_error = false;
247 mcp_client_clear_error(mcpc);
248 }
249
250 MCP_RETURN_CODE mcp_error_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id __maybe_unused, MCP_RETURN_CODE rc) {
251 if (!mcpc)
252 return rc;
253
254 mcpc->last_return_code = rc;
255 mcpc->last_response_error = true;
256
257 BUFFER *chunk = mcp_response_add_json_chunk(mcpc, 512);
258 if (!chunk)
259 return rc;
260
261 const char *error_message = buffer_strlen(mcpc->error)
262 ? buffer_tostring(mcpc->error)
263 : MCP_RETURN_CODE_2str(rc);
264
265 buffer_json_member_add_string(chunk, "status", "error");
266 buffer_json_member_add_string(chunk, "code", MCP_RETURN_CODE_2str(rc));
267 buffer_json_member_add_int64(chunk, "codeNumeric", rc);
268 if (error_message)
269 buffer_json_member_add_string(chunk, "message", error_message);
270 buffer_json_finalize(chunk);
271
272 return rc;
273 }
274
275 // Parse and extract client info from initialize request params
276 static void mcp_extract_client_info(MCP_CLIENT *mcpc, struct json_object *params) {
277 if (!mcpc || !params) return;
278
279 struct json_object *client_info_obj = NULL;
280 struct json_object *client_name_obj = NULL;
281 struct json_object *client_version_obj = NULL;
282
283 if (json_object_object_get_ex(params, "clientInfo", &client_info_obj)) {
284 if (json_object_object_get_ex(client_info_obj, "name", &client_name_obj)) {
285 string_freez(mcpc->client_name);
286 mcpc->client_name = string_strdupz(json_object_get_string(client_name_obj));
287 }
288 if (json_object_object_get_ex(client_info_obj, "version", &client_version_obj)) {
289 string_freez(mcpc->client_version);
290 mcpc->client_version = string_strdupz(json_object_get_string(client_version_obj));
291 }
292 }
293 }
294
295 MCP_RETURN_CODE mcp_dispatch_method(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
296 if (!mcpc)
297 return MCP_RC_INTERNAL_ERROR;
298
299 if (!method || !*method) {
300 buffer_strcat(mcpc->error, "Empty method name");
301 mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
302 return MCP_RC_INVALID_PARAMS;
303 }
304
305 if (!params || json_object_get_type(params) != json_type_object) {
306 buffer_strcat(mcpc->error, "Parameters must be an object");
307 mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
308 return MCP_RC_INVALID_PARAMS;
309 }
310
311 MCP_RETURN_CODE rc = MCP_RC_OK;
312
313 if (strcmp(method, "notifications/initialized") == 0) {
314 mcpc->ready = true;
315 netdata_log_debug(D_WEB_CLIENT, "MCP client %s v%s is now ready",
316 string2str(mcpc->client_name), string2str(mcpc->client_version));
317 mcp_client_prepare_response(mcpc);
318 mcp_init_success_result(mcpc, 0);
319 buffer_json_finalize(mcpc->result);
320 return MCP_RC_OK;
321 }
322
323 if (!mcpc->ready && strcmp(method, "initialize") != 0) {
324 netdata_log_debug(D_WEB_CLIENT, "MCP method %s called before initialize", method);
325 }
326
327 mcp_client_prepare_response(mcpc);
328 mcp_client_clear_error(mcpc);
329
330 if (strncmp(method, "tools/", 6) == 0) {
331 rc = mcp_tools_route(mcpc, method + 6, params, 0);
332 if (!mcpc->ready)
333 mcpc->ready = true;
334 }
335 else if (strncmp(method, "resources/", 10) == 0) {
336 rc = mcp_resources_route(mcpc, method + 10, params, 0);
337 if (!mcpc->ready)
338 mcpc->ready = true;
339 }
340 else if (strncmp(method, "prompts/", 8) == 0) {
341 rc = mcp_prompts_route(mcpc, method + 8, params, 0);
342 if (!mcpc->ready)
343 mcpc->ready = true;
344 }
345 else if (strncmp(method, "logging/", 8) == 0) {
346 rc = mcp_logging_route(mcpc, method + 8, params, 0);
347 }
348 else if (strncmp(method, "completion/", 11) == 0) {
349 rc = mcp_completion_route(mcpc, method + 11, params, 0);
350 if (!mcpc->ready)
351 mcpc->ready = true;
352 }
353 else if (strcmp(method, "initialize") == 0) {
354 mcp_extract_client_info(mcpc, params);
355 netdata_log_debug(D_WEB_CLIENT, "MCP initialize request from client %s v%s",
356 string2str(mcpc->client_name), string2str(mcpc->client_version));
357 rc = mcp_method_initialize(mcpc, params, 0);
358 }
359 else if (strcmp(method, "ping") == 0) {
360 rc = mcp_method_ping(mcpc, params, 0);
361 }
362 else {
363 buffer_sprintf(mcpc->error, "Method '%s' not found", method);
364 rc = MCP_RC_NOT_FOUND;
365 }
366
367 if (rc != MCP_RC_OK)
368 mcp_error_result(mcpc, 0, rc);
369
370 // Ensure at least one chunk exists on success
371 if (rc == MCP_RC_OK && mcp_client_response_chunk_count(mcpc) == 0) {
372 buffer_strcat(mcpc->error, "method generated empty result");
373 rc = mcp_error_result(mcpc, 0, MCP_RC_INTERNAL_ERROR);
374 }
375
376 return rc;
377 }
378
379 // Initialize the MCP subsystem
380 void mcp_initialize_subsystem(void) {
381 if (unlikely(mcp_initialized))
382 return;
383
384 mcp_functions_registry_init();
385
386 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
387 mcp_api_key_initialize();
388 #endif
389
390 // debug_flags |= D_MCP;
391
392 netdata_log_info("MCP subsystem initialized");
393 mcp_initialized = true;
394 }