master
c 222 lines 7.25 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "mcp-http.h"
4
5 #include "web/server/web_client.h"
6 #include "web/mcp/mcp-jsonrpc.h"
7 #include "web/mcp/mcp.h"
8 #include "web/mcp/adapters/mcp-sse.h"
9 #include "mcp-http-common.h"
10
11 #include "web/api/mcp_auth.h"
12
13 #include "libnetdata/libnetdata.h"
14 #include "libnetdata/http/http_defs.h"
15 #include "libnetdata/http/content_type.h"
16
17 #include <stdbool.h>
18 #include <json-c/json.h>
19 #include <string.h>
20 #include <strings.h>
21
22 #define IS_PARAM_SEPARATOR(c) ((c) == '&' || (c) == '\0')
23
24 static const char *mcp_http_body(struct web_client *w, size_t *len) {
25 if (!w || !w->payload)
26 return NULL;
27
28 const char *body = buffer_tostring(w->payload);
29 if (!body)
30 return NULL;
31
32 if (len)
33 *len = buffer_strlen(w->payload);
34 return body;
35 }
36
37 static bool mcp_http_accepts_sse(struct web_client *w) {
38 if (!w)
39 return false;
40
41 if (web_client_flag_check(w, WEB_CLIENT_FLAG_ACCEPT_SSE))
42 return true;
43
44 if (!w->url_query_string_decoded)
45 return false;
46
47 const char *qs = buffer_tostring(w->url_query_string_decoded);
48 if (!qs || !*qs)
49 return false;
50
51 if (*qs == '?')
52 qs++;
53
54 if (!*qs)
55 return false;
56
57 const char *param = strstr(qs, "transport=");
58 if (!param)
59 return false;
60
61 param += strlen("transport=");
62 if (strncasecmp(param, "sse", 3) == 0 && IS_PARAM_SEPARATOR(param[3]))
63 return true;
64
65 return false;
66 }
67
68 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
69 static void mcp_http_apply_api_key(struct web_client *w) {
70 if (web_client_has_mcp_preview_key(w)) {
71 web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, USER_AUTH_METHOD_GOD);
72 return;
73 }
74
75 char api_key_buffer[MCP_DEV_PREVIEW_API_KEY_LENGTH + 1];
76 if (mcp_http_extract_api_key(w, api_key_buffer, sizeof(api_key_buffer)) &&
77 mcp_api_key_verify(api_key_buffer, false)) { // silent=false for MCP requests
78 web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, USER_AUTH_METHOD_GOD);
79 }
80 }
81 #endif
82
83 static void mcp_http_write_json_payload(struct web_client *w, BUFFER *payload) {
84 if (!w)
85 return;
86
87 buffer_flush(w->response.data);
88 w->response.data->content_type = CT_APPLICATION_JSON;
89
90 if (payload && buffer_strlen(payload))
91 buffer_fast_strcat(w->response.data, buffer_tostring(payload), buffer_strlen(payload));
92 }
93
94 static int mcp_http_prepare_error_response(struct web_client *w, BUFFER *payload, int http_code) {
95 w->response.code = http_code;
96 mcp_http_write_json_payload(w, payload);
97 if (payload)
98 buffer_free(payload);
99 return http_code;
100 }
101
102 int mcp_http_handle_request(struct rrdhost *host __maybe_unused, struct web_client *w) {
103 if (!w)
104 return HTTP_RESP_INTERNAL_SERVER_ERROR;
105
106 if (w->mode != HTTP_REQUEST_MODE_POST && w->mode != HTTP_REQUEST_MODE_GET) {
107 buffer_flush(w->response.data);
108 buffer_strcat(w->response.data, "Unsupported HTTP method for /mcp\n");
109 w->response.data->content_type = CT_TEXT_PLAIN;
110 w->response.code = HTTP_RESP_METHOD_NOT_ALLOWED;
111 return w->response.code;
112 }
113
114 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
115 mcp_http_apply_api_key(w);
116 #endif
117
118 size_t body_len = 0;
119 const char *body = mcp_http_body(w, &body_len);
120 if (!body || !body_len) {
121 BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32600, "Empty request body", NULL, 0);
122 return mcp_http_prepare_error_response(w, payload, HTTP_RESP_BAD_REQUEST);
123 }
124
125 enum json_tokener_error jerr = json_tokener_success;
126 struct json_object *root = json_tokener_parse_verbose(body, &jerr);
127 if (!root || jerr != json_tokener_success) {
128 BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32700, json_tokener_error_desc(jerr), NULL, 0);
129 if (root)
130 json_object_put(root);
131 return mcp_http_prepare_error_response(w, payload, HTTP_RESP_BAD_REQUEST);
132 }
133
134 MCP_CLIENT *mcpc = mcp_create_client(MCP_TRANSPORT_HTTP, w);
135 if (!mcpc) {
136 json_object_put(root);
137 BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32603, "Failed to allocate MCP client", NULL, 0);
138 return mcp_http_prepare_error_response(w, payload, HTTP_RESP_INTERNAL_SERVER_ERROR);
139 }
140 mcpc->user_auth = &w->user_auth;
141
142 bool wants_sse = mcp_http_accepts_sse(w);
143
144 int result_code = HTTP_RESP_INTERNAL_SERVER_ERROR;
145
146 if (wants_sse) {
147 mcpc->transport = MCP_TRANSPORT_SSE;
148 mcpc->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
149 MCP_CAPABILITY_SUBSCRIPTIONS |
150 MCP_CAPABILITY_NOTIFICATIONS;
151 result_code = mcp_sse_serialize_response(w, mcpc, root);
152 } else {
153 BUFFER *response_payload = NULL;
154 bool has_response = false;
155
156 if (json_object_is_type(root, json_type_array)) {
157 size_t len = json_object_array_length(root);
158 BUFFER **responses = NULL;
159 size_t responses_used = 0;
160 size_t responses_size = 0;
161
162 for (size_t i = 0; i < len; i++) {
163 struct json_object *req_item = json_object_array_get_idx(root, i);
164 BUFFER *resp_item = mcp_jsonrpc_process_single_request(mcpc, req_item, NULL);
165 if (!resp_item)
166 continue;
167
168 if (responses_used == responses_size) {
169 size_t new_size = responses_size ? responses_size * 2 : 4;
170 BUFFER **tmp = reallocz(responses, new_size * sizeof(*tmp));
171 if (!tmp) {
172 buffer_free(resp_item);
173 continue;
174 }
175 responses = tmp;
176 responses_size = new_size;
177 }
178 responses[responses_used++] = resp_item;
179 }
180
181 if (responses_used) {
182 response_payload = mcp_jsonrpc_build_batch_response(responses, responses_used);
183 has_response = response_payload && buffer_strlen(response_payload);
184 }
185
186 for (size_t i = 0; i < responses_used; i++)
187 buffer_free(responses[i]);
188 freez(responses);
189 } else {
190 response_payload = mcp_jsonrpc_process_single_request(mcpc, root, NULL);
191 has_response = response_payload && buffer_strlen(response_payload);
192 }
193
194 if (response_payload) {
195 mcp_http_write_json_payload(w, response_payload);
196 } else {
197 buffer_flush(w->response.data);
198 mcp_http_disable_compression(w);
199 w->response.data->content_type = CT_APPLICATION_JSON;
200 buffer_flush(w->response.header);
201 }
202
203 w->response.code = has_response ? HTTP_RESP_OK : HTTP_RESP_ACCEPTED;
204
205 if (response_payload)
206 buffer_free(response_payload);
207
208 result_code = w->response.code;
209 }
210
211 // Stateless Mcp-Session-Id: generate if absent, then emit as response header
212 if (uuid_is_null(w->mcp_session_id))
213 uuid_generate_random(w->mcp_session_id);
214
215 char session_id_str[UUID_STR_LEN];
216 uuid_unparse_lower(w->mcp_session_id, session_id_str);
217 buffer_sprintf(w->response.header, "Mcp-Session-Id: %s\r\n", session_id_str);
218
219 json_object_put(root);
220 mcp_free_client(mcpc);
221 return result_code;
222 }