| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "mcp-websocket.h" |
| 4 | #include "web/websocket/websocket-internal.h" |
| 5 | #include "web/mcp/mcp-jsonrpc.h" |
| 6 | |
| 7 | #include <string.h> |
| 8 | |
| 9 | // Store the MCP context in the WebSocket client's data field |
| 10 | void mcp_websocket_set_context(struct websocket_server_client *wsc, MCP_CLIENT *ctx) { |
| 11 | if (!wsc) return; |
| 12 | wsc->user_data = ctx; |
| 13 | } |
| 14 | |
| 15 | // Get the MCP context from a WebSocket client |
| 16 | MCP_CLIENT *mcp_websocket_get_context(struct websocket_server_client *wsc) { |
| 17 | if (!wsc) return NULL; |
| 18 | return (MCP_CLIENT *)wsc->user_data; |
| 19 | } |
| 20 | |
| 21 | // Create a response context for a WebSocket client |
| 22 | static MCP_CLIENT *mcp_websocket_create_context(struct websocket_server_client *wsc) { |
| 23 | if (!wsc) return NULL; |
| 24 | |
| 25 | MCP_CLIENT *ctx = mcp_create_client(MCP_TRANSPORT_WEBSOCKET, wsc); |
| 26 | if (ctx) { |
| 27 | // Set pointer to the websocket client's user_auth |
| 28 | ctx->user_auth = &wsc->user_auth; |
| 29 | } |
| 30 | mcp_websocket_set_context(wsc, ctx); |
| 31 | |
| 32 | return ctx; |
| 33 | } |
| 34 | |
| 35 | // WebSocket connection handler for MCP |
| 36 | void mcp_websocket_on_connect(struct websocket_server_client *wsc) { |
| 37 | if (!wsc) return; |
| 38 | |
| 39 | // Create the MCP context |
| 40 | MCP_CLIENT *ctx = mcp_websocket_create_context(wsc); |
| 41 | if (!ctx) { |
| 42 | websocket_protocol_send_close(wsc, WS_CLOSE_INTERNAL_ERROR, "Failed to create MCP context"); |
| 43 | return; |
| 44 | } |
| 45 | |
| 46 | websocket_debug(wsc, "MCP client connected"); |
| 47 | } |
| 48 | |
| 49 | static void mcp_websocket_send_payload(struct websocket_server_client *wsc, BUFFER *payload) { |
| 50 | if (!wsc || !payload) |
| 51 | return; |
| 52 | |
| 53 | const char *text = buffer_tostring(payload); |
| 54 | if (!text) |
| 55 | return; |
| 56 | |
| 57 | netdata_log_debug(D_MCP, "SND: %s", text); |
| 58 | websocket_protocol_send_text(wsc, text); |
| 59 | } |
| 60 | |
| 61 | |
| 62 | // WebSocket message handler for MCP - receives message and routes to MCP |
| 63 | void mcp_websocket_on_message(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode) { |
| 64 | if (!wsc || !message || length == 0) |
| 65 | return; |
| 66 | |
| 67 | // Log the raw incoming message |
| 68 | netdata_log_debug(D_MCP, "RCV: %s", message); |
| 69 | |
| 70 | // Only handle text messages |
| 71 | if (opcode != WS_OPCODE_TEXT) { |
| 72 | websocket_error(wsc, "Ignoring binary message - mcp supports only TEXT messages"); |
| 73 | return; |
| 74 | } |
| 75 | |
| 76 | // Silently ignore standalone "PING" messages (legacy MCP client behavior) |
| 77 | if (length == 4 && strncmp(message, "PING", 4) == 0) { |
| 78 | websocket_debug(wsc, "Ignoring legacy PING message"); |
| 79 | return; |
| 80 | } |
| 81 | |
| 82 | // Get the MCP context |
| 83 | MCP_CLIENT *mcpc = mcp_websocket_get_context(wsc); |
| 84 | if (!mcpc) { |
| 85 | websocket_error(wsc, "MCP context not found"); |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | // Parse the JSON-RPC request |
| 90 | struct json_object *request = NULL; |
| 91 | enum json_tokener_error jerr = json_tokener_success; |
| 92 | request = json_tokener_parse_verbose(message, &jerr); |
| 93 | |
| 94 | if (!request || jerr != json_tokener_success) { |
| 95 | websocket_error(wsc, "Failed to parse JSON-RPC request: %s", json_tokener_error_desc(jerr)); |
| 96 | |
| 97 | BUFFER *error_payload = mcp_jsonrpc_build_error_payload(NULL, -32700, "Parse error", NULL, 0); |
| 98 | mcp_websocket_send_payload(wsc, error_payload); |
| 99 | buffer_free(error_payload); |
| 100 | return; |
| 101 | } |
| 102 | |
| 103 | if (json_object_is_type(request, json_type_array)) { |
| 104 | int len = (int)json_object_array_length(request); |
| 105 | BUFFER **responses = NULL; |
| 106 | size_t responses_used = 0; |
| 107 | size_t responses_size = 0; |
| 108 | |
| 109 | for (int i = 0; i < len; i++) { |
| 110 | struct json_object *req_item = json_object_array_get_idx(request, i); |
| 111 | BUFFER *resp_item = mcp_jsonrpc_process_single_request(mcpc, req_item, NULL); |
| 112 | if (resp_item) { |
| 113 | if (responses_used == responses_size) { |
| 114 | size_t new_size = responses_size ? responses_size * 2 : 4; |
| 115 | BUFFER **tmp = reallocz(responses, new_size * sizeof(*tmp)); |
| 116 | if (!tmp) { |
| 117 | buffer_free(resp_item); |
| 118 | continue; |
| 119 | } |
| 120 | responses = tmp; |
| 121 | responses_size = new_size; |
| 122 | } |
| 123 | responses[responses_used++] = resp_item; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | if (responses_used > 0) { |
| 128 | size_t total_len = 2; // brackets |
| 129 | for (size_t i = 0; i < responses_used; i++) |
| 130 | total_len += buffer_strlen(responses[i]) + (i ? 1 : 0); |
| 131 | |
| 132 | BUFFER *batch = buffer_create(total_len + 32, NULL); |
| 133 | buffer_fast_strcat(batch, "[", 1); |
| 134 | for (size_t i = 0; i < responses_used; i++) { |
| 135 | if (i) |
| 136 | buffer_fast_strcat(batch, ",", 1); |
| 137 | const char *resp_text = buffer_tostring(responses[i]); |
| 138 | size_t resp_len = buffer_strlen(responses[i]); |
| 139 | buffer_fast_strcat(batch, resp_text, resp_len); |
| 140 | } |
| 141 | buffer_fast_strcat(batch, "]", 1); |
| 142 | mcp_websocket_send_payload(wsc, batch); |
| 143 | buffer_free(batch); |
| 144 | } |
| 145 | |
| 146 | for (size_t i = 0; i < responses_used; i++) |
| 147 | buffer_free(responses[i]); |
| 148 | freez(responses); |
| 149 | } else { |
| 150 | BUFFER *response = mcp_jsonrpc_process_single_request(mcpc, request, NULL); |
| 151 | if (response) { |
| 152 | mcp_websocket_send_payload(wsc, response); |
| 153 | buffer_free(response); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | json_object_put(request); |
| 158 | } |
| 159 | |
| 160 | // WebSocket close handler for MCP |
| 161 | void mcp_websocket_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason) { |
| 162 | if (!wsc) return; |
| 163 | |
| 164 | websocket_debug(wsc, "MCP client closing (code: %d, reason: %s)", code, reason ? reason : "none"); |
| 165 | |
| 166 | // Clean up the MCP context |
| 167 | MCP_CLIENT *ctx = mcp_websocket_get_context(wsc); |
| 168 | if (ctx) { |
| 169 | mcp_free_client(ctx); |
| 170 | mcp_websocket_set_context(wsc, NULL); |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | // WebSocket disconnect handler for MCP |
| 175 | void mcp_websocket_on_disconnect(struct websocket_server_client *wsc) { |
| 176 | if (!wsc) return; |
| 177 | |
| 178 | websocket_debug(wsc, "MCP client disconnected"); |
| 179 | |
| 180 | // Clean up the MCP context |
| 181 | MCP_CLIENT *ctx = mcp_websocket_get_context(wsc); |
| 182 | if (ctx) { |
| 183 | mcp_free_client(ctx); |
| 184 | mcp_websocket_set_context(wsc, NULL); |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | // Register WebSocket callbacks for MCP |
| 189 | void mcp_websocket_adapter_initialize(void) { |
| 190 | mcp_initialize_subsystem(); |
| 191 | netdata_log_info("MCP WebSocket adapter initialized"); |
| 192 | } |