diff options
| author | Your Name <you@example.com> | 2026-05-19 01:10:06 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-19 01:10:06 +0530 |
| commit | 42902a36bc52e009a1e8d3c371741e30a9cb4c33 (patch) | |
| tree | 46db33710a3650b2267933a8375d3598af11319a /main/mcp_handler.c | |
| parent | fe7c3be2fd9d464dbc837d1913409d2691bd50f5 (diff) | |
feat: ContextVM (MCP over Nostr) server with full integration
Complete CVM implementation: persistent WebSocket relay listener,
kind 25910 event subscription, MCP protocol handlers, CEP-6 announcements,
10 MCP tools, per-board hardware locks, WiFi EU regulatory fix.
Architecture:
- cvm_server.c: WS relay listener, kind 25910 subscription, MCP dispatch
- mcp_handler.c/h: 10 MCP tools (get_config, set_config, get_balance,
wallet_send, get_sessions, get_usage, set_payout, set_metric,
set_price, wallet_melt)
- Responses published via existing WS connection (not new TLS)
- Auth check: only owner npub accepted
- CEP-6: kinds 11316 (server), 11317 (tools), 10002 (relay list)
- WS ping/pong keepalive every 30s, 60s TLS read timeout
Critical fixes:
- WiFi country code DE (ESP-IDF defaults to CN, breaks EU APs)
- Subscription #p filter must be array not string
- Use-after-free: tags_str freed before nostr_event_to_json
- MCP responses via existing WS (ESP32 can't open multiple TLS)
- EVENT msg buffer underflow, WS frame masking, TLS write loop
Per-board hardware locks:
- Lock files in physical-router-test-automation/locks/
- lock-a/b/c, unlock-a/b/c targets in 3 Makefiles
- All hardware-touching targets require board lock
Verified on Board B via relay.primal.net:
- 282 unit tests passing (61 CVM + 60 MCP + 161 existing)
- MCP initialize roundtrip: PASS
- tools/list: PASS
- tools/call get_config: PASS
- tools/call get_balance: PASS
- tools/call set_price: PASS (write operation)
- CEP-6 announcements (11316, 11317, 10002): all accepted by relay
- WiFi STA connection (EnterSSID-2.4GHz): PASS with country code DE
- Board A WiFi confirmed hardware issue (not firmware)
Diffstat (limited to 'main/mcp_handler.c')
| -rw-r--r-- | main/mcp_handler.c | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/main/mcp_handler.c b/main/mcp_handler.c index f40c1bd..93bfba9 100644 --- a/main/mcp_handler.c +++ b/main/mcp_handler.c | |||
| @@ -1,7 +1,9 @@ | |||
| 1 | #include "mcp_handler.h" | 1 | #include "mcp_handler.h" |
| 2 | #include "config.h" | 2 | #include "config.h" |
| 3 | #include "nucula_wallet.h" | 3 | #include "nucula_wallet.h" |
| 4 | #include "session.h" | ||
| 4 | #include "cJSON.h" | 5 | #include "cJSON.h" |
| 6 | #include "lwip/ip4_addr.h" | ||
| 5 | #include <string.h> | 7 | #include <string.h> |
| 6 | #include <stdio.h> | 8 | #include <stdio.h> |
| 7 | 9 | ||
| @@ -14,6 +16,12 @@ mcp_tool_t mcp_parse_tool(const char *method) | |||
| 14 | if (strcmp(method, "set_config") == 0) return MCP_TOOL_SET_CONFIG; | 16 | if (strcmp(method, "set_config") == 0) return MCP_TOOL_SET_CONFIG; |
| 15 | if (strcmp(method, "get_balance") == 0) return MCP_TOOL_GET_BALANCE; | 17 | if (strcmp(method, "get_balance") == 0) return MCP_TOOL_GET_BALANCE; |
| 16 | if (strcmp(method, "wallet_send") == 0) return MCP_TOOL_WALLET_SEND; | 18 | if (strcmp(method, "wallet_send") == 0) return MCP_TOOL_WALLET_SEND; |
| 19 | if (strcmp(method, "get_sessions") == 0) return MCP_TOOL_GET_SESSIONS; | ||
| 20 | if (strcmp(method, "get_usage") == 0) return MCP_TOOL_GET_USAGE; | ||
| 21 | if (strcmp(method, "set_payout") == 0) return MCP_TOOL_SET_PAYOUT; | ||
| 22 | if (strcmp(method, "set_metric") == 0) return MCP_TOOL_SET_METRIC; | ||
| 23 | if (strcmp(method, "set_price") == 0) return MCP_TOOL_SET_PRICE; | ||
| 24 | if (strcmp(method, "wallet_melt") == 0) return MCP_TOOL_WALLET_MELT; | ||
| 17 | return MCP_TOOL_UNKNOWN; | 25 | return MCP_TOOL_UNKNOWN; |
| 18 | } | 26 | } |
| 19 | 27 | ||
| @@ -146,6 +154,222 @@ mcp_response_t mcp_handle_wallet_send(const char *params_json) | |||
| 146 | return resp; | 154 | return resp; |
| 147 | } | 155 | } |
| 148 | 156 | ||
| 157 | mcp_response_t mcp_handle_get_sessions(void) | ||
| 158 | { | ||
| 159 | mcp_response_t resp = {0}; | ||
| 160 | extern session_t *cvm_get_sessions_array(void); | ||
| 161 | extern int cvm_get_sessions_count(void); | ||
| 162 | |||
| 163 | cJSON *arr = cJSON_CreateArray(); | ||
| 164 | int count = cvm_get_sessions_count(); | ||
| 165 | session_t *sessions = cvm_get_sessions_array(); | ||
| 166 | |||
| 167 | if (sessions && count > 0) { | ||
| 168 | for (int i = 0; i < count; i++) { | ||
| 169 | if (!sessions[i].active) continue; | ||
| 170 | cJSON *s = cJSON_CreateObject(); | ||
| 171 | esp_ip4_addr_t ip = { .addr = sessions[i].client_ip }; | ||
| 172 | char ip_str[16]; | ||
| 173 | snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip)); | ||
| 174 | cJSON_AddStringToObject(s, "client_ip", ip_str); | ||
| 175 | if (sessions[i].mac[0]) | ||
| 176 | cJSON_AddStringToObject(s, "mac", sessions[i].mac); | ||
| 177 | cJSON_AddNumberToObject(s, "allotment_ms", (double)sessions[i].allotment_ms); | ||
| 178 | cJSON_AddNumberToObject(s, "allotment_bytes", (double)sessions[i].allotment_bytes); | ||
| 179 | cJSON_AddNumberToObject(s, "bytes_consumed", (double)sessions[i].bytes_consumed); | ||
| 180 | cJSON_AddBoolToObject(s, "active", sessions[i].active); | ||
| 181 | cJSON_AddItemToArray(arr, s); | ||
| 182 | } | ||
| 183 | } | ||
| 184 | |||
| 185 | char *json = cJSON_PrintUnformatted(arr); | ||
| 186 | snprintf(resp.result_json, sizeof(resp.result_json), "%s", json); | ||
| 187 | cJSON_free(json); | ||
| 188 | cJSON_Delete(arr); | ||
| 189 | resp.success = true; | ||
| 190 | return resp; | ||
| 191 | } | ||
| 192 | |||
| 193 | mcp_response_t mcp_handle_get_usage(void) | ||
| 194 | { | ||
| 195 | mcp_response_t resp = {0}; | ||
| 196 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 197 | |||
| 198 | cJSON *root = cJSON_CreateObject(); | ||
| 199 | cJSON_AddStringToObject(root, "metric", cfg->metric); | ||
| 200 | cJSON_AddNumberToObject(root, "price_per_step", cfg->price_per_step); | ||
| 201 | cJSON_AddNumberToObject(root, "step_size_ms", cfg->step_size_ms); | ||
| 202 | cJSON_AddNumberToObject(root, "step_size_bytes", cfg->step_size_bytes); | ||
| 203 | cJSON_AddBoolToObject(root, "client_enabled", cfg->client_enabled); | ||
| 204 | |||
| 205 | char *json = cJSON_PrintUnformatted(root); | ||
| 206 | snprintf(resp.result_json, sizeof(resp.result_json), "%s", json); | ||
| 207 | cJSON_free(json); | ||
| 208 | cJSON_Delete(root); | ||
| 209 | resp.success = true; | ||
| 210 | return resp; | ||
| 211 | } | ||
| 212 | |||
| 213 | mcp_response_t mcp_handle_set_payout(const char *params_json) | ||
| 214 | { | ||
| 215 | mcp_response_t resp = {0}; | ||
| 216 | cJSON *root = cJSON_Parse(params_json); | ||
| 217 | if (!root) { | ||
| 218 | resp.success = false; | ||
| 219 | snprintf(resp.error, sizeof(resp.error), "Invalid JSON params"); | ||
| 220 | return resp; | ||
| 221 | } | ||
| 222 | |||
| 223 | tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get(); | ||
| 224 | if (!cfg) { | ||
| 225 | cJSON_Delete(root); | ||
| 226 | resp.success = false; | ||
| 227 | snprintf(resp.error, sizeof(resp.error), "Config not loaded"); | ||
| 228 | return resp; | ||
| 229 | } | ||
| 230 | |||
| 231 | cJSON *enabled = cJSON_GetObjectItem(root, "enabled"); | ||
| 232 | if (enabled && cJSON_IsBool(enabled)) cfg->payout.enabled = cJSON_IsTrue(enabled); | ||
| 233 | |||
| 234 | cJSON *recipients = cJSON_GetObjectItem(root, "recipients"); | ||
| 235 | if (recipients && cJSON_IsArray(recipients)) { | ||
| 236 | int rcount = cJSON_GetArraySize(recipients); | ||
| 237 | if (rcount > PAYOUT_MAX_RECIPIENTS) rcount = PAYOUT_MAX_RECIPIENTS; | ||
| 238 | for (int i = 0; i < rcount; i++) { | ||
| 239 | cJSON *r = cJSON_GetArrayItem(recipients, i); | ||
| 240 | cJSON *addr = cJSON_GetObjectItem(r, "lightning_address"); | ||
| 241 | cJSON *factor = cJSON_GetObjectItem(r, "factor"); | ||
| 242 | if (addr && cJSON_IsString(addr)) { | ||
| 243 | strncpy(cfg->payout.recipients[i].lightning_address, addr->valuestring, | ||
| 244 | sizeof(cfg->payout.recipients[i].lightning_address) - 1); | ||
| 245 | } | ||
| 246 | if (factor && cJSON_IsNumber(factor)) { | ||
| 247 | cfg->payout.recipients[i].factor = factor->valuedouble; | ||
| 248 | } | ||
| 249 | } | ||
| 250 | cfg->payout.recipient_count = rcount; | ||
| 251 | } | ||
| 252 | |||
| 253 | cJSON_Delete(root); | ||
| 254 | resp.success = true; | ||
| 255 | snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\"}"); | ||
| 256 | return resp; | ||
| 257 | } | ||
| 258 | |||
| 259 | mcp_response_t mcp_handle_set_metric(const char *params_json) | ||
| 260 | { | ||
| 261 | mcp_response_t resp = {0}; | ||
| 262 | cJSON *root = cJSON_Parse(params_json); | ||
| 263 | if (!root) { | ||
| 264 | resp.success = false; | ||
| 265 | snprintf(resp.error, sizeof(resp.error), "Invalid JSON params"); | ||
| 266 | return resp; | ||
| 267 | } | ||
| 268 | |||
| 269 | tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get(); | ||
| 270 | if (!cfg) { | ||
| 271 | cJSON_Delete(root); | ||
| 272 | resp.success = false; | ||
| 273 | snprintf(resp.error, sizeof(resp.error), "Config not loaded"); | ||
| 274 | return resp; | ||
| 275 | } | ||
| 276 | |||
| 277 | cJSON *metric = cJSON_GetObjectItem(root, "metric"); | ||
| 278 | if (metric && cJSON_IsString(metric)) { | ||
| 279 | const char *m = metric->valuestring; | ||
| 280 | if (strcmp(m, "bytes") == 0 || strcmp(m, "milliseconds") == 0) { | ||
| 281 | strncpy(cfg->metric, m, sizeof(cfg->metric) - 1); | ||
| 282 | } else { | ||
| 283 | cJSON_Delete(root); | ||
| 284 | resp.success = false; | ||
| 285 | snprintf(resp.error, sizeof(resp.error), "Invalid metric: must be 'bytes' or 'milliseconds'"); | ||
| 286 | return resp; | ||
| 287 | } | ||
| 288 | } else { | ||
| 289 | cJSON_Delete(root); | ||
| 290 | resp.success = false; | ||
| 291 | snprintf(resp.error, sizeof(resp.error), "Missing 'metric' field"); | ||
| 292 | return resp; | ||
| 293 | } | ||
| 294 | |||
| 295 | cJSON_Delete(root); | ||
| 296 | resp.success = true; | ||
| 297 | snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\",\"metric\":\"%s\"}", cfg->metric); | ||
| 298 | return resp; | ||
| 299 | } | ||
| 300 | |||
| 301 | mcp_response_t mcp_handle_set_price(const char *params_json) | ||
| 302 | { | ||
| 303 | mcp_response_t resp = {0}; | ||
| 304 | cJSON *root = cJSON_Parse(params_json); | ||
| 305 | if (!root) { | ||
| 306 | resp.success = false; | ||
| 307 | snprintf(resp.error, sizeof(resp.error), "Invalid JSON params"); | ||
| 308 | return resp; | ||
| 309 | } | ||
| 310 | |||
| 311 | tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get(); | ||
| 312 | if (!cfg) { | ||
| 313 | cJSON_Delete(root); | ||
| 314 | resp.success = false; | ||
| 315 | snprintf(resp.error, sizeof(resp.error), "Config not loaded"); | ||
| 316 | return resp; | ||
| 317 | } | ||
| 318 | |||
| 319 | cJSON *price = cJSON_GetObjectItem(root, "price_per_step"); | ||
| 320 | if (price && cJSON_IsNumber(price) && price->valueint > 0) { | ||
| 321 | cfg->price_per_step = price->valueint; | ||
| 322 | } else { | ||
| 323 | cJSON_Delete(root); | ||
| 324 | resp.success = false; | ||
| 325 | snprintf(resp.error, sizeof(resp.error), "Missing or invalid 'price_per_step' field"); | ||
| 326 | return resp; | ||
| 327 | } | ||
| 328 | |||
| 329 | cJSON_Delete(root); | ||
| 330 | resp.success = true; | ||
| 331 | snprintf(resp.result_json, sizeof(resp.result_json), | ||
| 332 | "{\"status\":\"ok\",\"price_per_step\":%d}", cfg->price_per_step); | ||
| 333 | return resp; | ||
| 334 | } | ||
| 335 | |||
| 336 | mcp_response_t mcp_handle_wallet_melt(const char *params_json) | ||
| 337 | { | ||
| 338 | mcp_response_t resp = {0}; | ||
| 339 | cJSON *root = cJSON_Parse(params_json); | ||
| 340 | if (!root) { | ||
| 341 | resp.success = false; | ||
| 342 | snprintf(resp.error, sizeof(resp.error), "Invalid JSON params"); | ||
| 343 | return resp; | ||
| 344 | } | ||
| 345 | |||
| 346 | cJSON *bolt11 = cJSON_GetObjectItem(root, "bolt11"); | ||
| 347 | if (!bolt11 || !cJSON_IsString(bolt11)) { | ||
| 348 | cJSON_Delete(root); | ||
| 349 | resp.success = false; | ||
| 350 | snprintf(resp.error, sizeof(resp.error), "Missing 'bolt11' field"); | ||
| 351 | return resp; | ||
| 352 | } | ||
| 353 | |||
| 354 | cJSON *max_fee = cJSON_GetObjectItem(root, "max_fee_sats"); | ||
| 355 | uint64_t fee = 10; | ||
| 356 | if (max_fee && cJSON_IsNumber(max_fee)) fee = (uint64_t)max_fee->valuedouble; | ||
| 357 | |||
| 358 | esp_err_t rc = nucula_wallet_melt(bolt11->valuestring, fee); | ||
| 359 | |||
| 360 | if (rc != ESP_OK) { | ||
| 361 | cJSON_Delete(root); | ||
| 362 | resp.success = false; | ||
| 363 | snprintf(resp.error, sizeof(resp.error), "Melt failed: %s", esp_err_to_name(rc)); | ||
| 364 | return resp; | ||
| 365 | } | ||
| 366 | |||
| 367 | cJSON_Delete(root); | ||
| 368 | resp.success = true; | ||
| 369 | snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\"}"); | ||
| 370 | return resp; | ||
| 371 | } | ||
| 372 | |||
| 149 | mcp_response_t mcp_dispatch(const mcp_request_t *req) | 373 | mcp_response_t mcp_dispatch(const mcp_request_t *req) |
| 150 | { | 374 | { |
| 151 | if (!req) { | 375 | if (!req) { |
| @@ -164,6 +388,18 @@ mcp_response_t mcp_dispatch(const mcp_request_t *req) | |||
| 164 | return mcp_handle_get_balance(); | 388 | return mcp_handle_get_balance(); |
| 165 | case MCP_TOOL_WALLET_SEND: | 389 | case MCP_TOOL_WALLET_SEND: |
| 166 | return mcp_handle_wallet_send(req->params_json); | 390 | return mcp_handle_wallet_send(req->params_json); |
| 391 | case MCP_TOOL_GET_SESSIONS: | ||
| 392 | return mcp_handle_get_sessions(); | ||
| 393 | case MCP_TOOL_GET_USAGE: | ||
| 394 | return mcp_handle_get_usage(); | ||
| 395 | case MCP_TOOL_SET_PAYOUT: | ||
| 396 | return mcp_handle_set_payout(req->params_json); | ||
| 397 | case MCP_TOOL_SET_METRIC: | ||
| 398 | return mcp_handle_set_metric(req->params_json); | ||
| 399 | case MCP_TOOL_SET_PRICE: | ||
| 400 | return mcp_handle_set_price(req->params_json); | ||
| 401 | case MCP_TOOL_WALLET_MELT: | ||
| 402 | return mcp_handle_wallet_melt(req->params_json); | ||
| 167 | default: | 403 | default: |
| 168 | break; | 404 | break; |
| 169 | } | 405 | } |