code wiki / (root) / nx_mcp_call.nx

nx_mcp_call.nx source

↩ module page · 274 lines · 15165 B

1// nx_mcp_call.nx -- the SOVEREIGN MCP client: POST a JSON-RPC tools/call to /mcp over our own TLS-1.3 and PRINT 2// the response body. Fills the gap surfaced 2026-07-17 ([[reference-sovereign-mcp-call-client-gap-2026-07-17]]): 3// nx_mgmt_client's `call` sends x-www-form-urlencoded (built for /api/login) so it mis-frames a JSON tools/call 4// (status=0); this client sends Content-Type: application/json + Accept + X-Nishi-Cap and echoes the body. 5// ✅PROVEN 2026-07-17: `tools/call` WORKS end-to-end -- nishi_search returned real search hits, and 6// nx_dataplane_census returned 655 flat-file-debt rows (56KB, HTTP 200, isError:false) over sovereign TLS /mcp 7// with a least-authority cap. `initialize`/`tools/list`/`tools/call` all work; the sovereign JSON POST /mcp + 8// response-parse + body-print are complete. nx_mgmt_client CANNOT do this (it sends x-www-form-urlencoded -> the 9// daemon mis-frames the JSON -> status=0). That form-encoding -- NOT a daemon bug -- is why the doctrine's 10// "raw tools/call via nx_mgmt_client" path always failed. ⚠fork-exec tools are SLOW (census ~16s) so a client 11// needs patience; ⚠tools whose ELF FORKS CHILDREN (e.g. nx_status -> nx_hostctl sub-forks) return empty because 12// tr_run_capture (nx_tool_run.nx:21) dups the pipe to stdout/stderr but does NOT close the inherited request 13// SOCKET fd before execve -> forked grandchildren hold the connection open -> no clean close (a SEPARATE daemon 14// fix: add a close-fds-3..N loop in the child). Non-forking tools (search, census) work today. 15// tool_name "__initialize__" sends an initialize (open, no cap) probe. [[reference-sovereign-mcp-call-client-gap-2026-07-17]] 16// nx_mcp_call <base_url> <tool_name> <cap_file> [args_json_fragment] 17// e.g. nx_mcp_call https://nishifamily.com nx_dataplane_census /tmp/capfile 18// args_json_fragment (optional) is spliced into arguments after _cap, e.g. "argv":["a","b"] 19// REUSE (compose, don't rebuild TLS): copies nx_mgmt_client's PROVEN transport (build_request/send_drain/req) 20// verbatim -- renamed mc_* -- flipping ONLY the content-type. DRY debt: extract the shared transport into a 21// no-main lib later (nx_mgmt_client has a main(), so it can't be imported today). license_tier: ORIGINAL 22import "nx_acme_http.nx" // nx_http_response_parse/_alloc + nx_trust_store_load_from_certdata + TrustStore + full TLS-1.3 stack (transitive) 23import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 24import "nx_https_url_for_fetch.nx" // nx_https_url_for_fetch, NxUrl, nx_url_new, NxHttpsTarget, NX_HTTPS_URL_OK 25import "nx_https_url_connect.nx" // nx_https_url_connect, NX_HTTPS_CONNECT_OK 26import "nx_tls13_client_session_run.nx" // nx_tls13_client_session_run, TlsValidationContext 27import "nx_tls13_client_session.nx" // Tls13ClientSession, NX_TLS13_CSESSION_STATE_CONNECTED 28import "nx_tls13.nx" // NX_TLS13_CT_APPLICATION_DATA, NX_TLS13_CT_ALERT 29import "nx_tls13_record.nx" // nx_tls13_record_encrypt_v2/_decrypt_v2 + record header/tag consts 30import "nx_tls13_read_record_from_fd.nx" // nx_tls13_read_record_from_fd 31import "nx_csprng.nx" // nx_csprng_fill 32const MC_MAGIC_16384: i64 = 16384 33const MC_MAGIC_16645: i64 = 16645 34const MC_MAGIC_4194304: i64 = 4194304 35const MC_MAGIC_8192: i64 = 8192 36const MC_MAGIC_8191: i64 = 8191 37const MC_MAGIC_65536: i64 = 65536 38const MC_MAGIC_4096: i64 = 4096 39const MC_MAGIC_131072: i64 = 131072 40const MC_MAGIC_1048576: i64 = 1048576 41 42const MC_CONNECT_FAIL: i64 = 0 - 11 43const MC_HANDSHAKE_FAIL: i64 = 0 - 12 44 45func mc_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 46// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 47// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 48// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 49// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 50func mc_putn(v: i64) -> i64 { nxi_out(v); return 0 } 51func mc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 52func mc_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i } 53func mc_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[o + i] = s[i]; i = i + 1 } return o + i } 54func mc_catn(d: *u8, o: i64, v: i64) -> i64 { 55 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 56 var w: i64 = o 57 if m < 0 { d[w] = 45 as u8; w = w + 1; m = 0 - m } 58 if m == 0 { t[0] = 48 as u8; k = 1 } 59 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 60 var i: i64 = 0 61 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 62 return w 63} 64 65func mc_write_n(fd: i64, buf: *u8, n: i64) -> i64 { 66 var off: i64 = 0 67 while off < n { 68 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off) 69 if w <= 0 { return 0 - 1 } 70 off = off + w 71 } 72 return 0 73} 74func mc_read_file(path: *u8, out: *u8, cap: i64) -> i64 { 75 let fd: i64 = sys_openat_rd(path) 76 if fd <= 0 { return 0 - 1 } 77 var off: i64 = 0 78 var go: i64 = 1 79 while go == 1 { 80 if off >= cap { go = 0 } 81 else { let r: i64 = sys_read(fd, ((out as i64) + off) as *u8, cap - off); if r <= 0 { go = 0 } else { off = off + r } } 82 } 83 sys_close(fd) 84 return off 85} 86func mc_rtrim_nl(buf: *u8, n: i64) -> i64 { 87 var m: i64 = n 88 var go: i64 = 1 89 while go == 1 { 90 if m <= 0 { go = 0 } 91 else { let c: i64 = buf[m - 1] as i64; if c == 10 { m = m - 1 } else { if c == 13 { m = m - 1 } else { go = 0 } } } 92 } 93 return m 94} 95func mc_join_url(base_url: *u8, suffix: *u8, out: *u8) -> i64 { 96 var o: i64 = mc_cat(out, 0, base_url) 97 if o > 0 { if out[o - 1] == (47 as u8) { o = o - 1 } } 98 if suffix[0] != (47 as u8) { out[o] = 47 as u8; o = o + 1 } 99 o = mc_cat(out, o, suffix) 100 out[o] = 0 as u8 101 return o 102} 103 104// Build a full HTTP/1.1 request (origin-form target). Content-Type parameterized -> we pass application/json. 105func mc_build_request(method: *u8, method_len: i64, path: *u8, path_len: i64, host: *u8, host_len: i64, cap: *u8, cap_len: i64, ct: *u8, ct_len: i64, body: *u8, body_len: i64, out: *u8) -> i64 { 106 var o: i64 = 0 107 o = mc_catb(out, o, method, method_len) 108 out[o] = 0x20 as u8; o = o + 1 109 o = mc_catb(out, o, path, path_len) 110 o = mc_cat(out, o, " HTTP/1.1\r\nHost: " as *u8) 111 o = mc_catb(out, o, host, host_len) 112 o = mc_cat(out, o, "\r\n" as *u8) 113 o = mc_cat(out, o, "Accept: application/json, text/event-stream\r\n" as *u8) // MCP Streamable-HTTP transport requires it 114 if cap_len > 0 { // primary auth path (Claude Code presents the cap this way) 115 o = mc_cat(out, o, "X-Nishi-Cap: " as *u8) 116 o = mc_catb(out, o, cap, cap_len) 117 o = mc_cat(out, o, "\r\n" as *u8) 118 } 119 if body_len > 0 { 120 o = mc_cat(out, o, "Content-Type: " as *u8) 121 o = mc_catb(out, o, ct, ct_len) 122 o = mc_cat(out, o, "\r\nContent-Length: " as *u8) 123 o = mc_catn(out, o, body_len) 124 o = mc_cat(out, o, "\r\n" as *u8) 125 } 126 o = mc_cat(out, o, "Connection: close\r\n\r\n" as *u8) 127 if body_len > 0 { o = mc_catb(out, o, body, body_len) } 128 return o 129} 130 131// Send a PRE-BUILT request over a CONNECTED TLS-1.3 session, then drain the full response into out. Verbatim 132// copy of nx_mgmt_client's proven mcl_send_drain (RFC 8446 <=16KB record fragmentation + decrypt loop). 133func mc_send_drain(s: *Tls13ClientSession, fd: i64, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 { 134 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { return 0 - 1 } 135 let rec_buf: *u8 = sys_mmap(MC_MAGIC_16384 + 64) 136 var snd_off: i64 = 0 137 var first_frag: i64 = 1 138 while first_frag == 1 { first_frag = 0 139 var frag: i64 = req_len - snd_off 140 if frag > MC_MAGIC_16384 { frag = MC_MAGIC_16384 } 141 let header_out: *u8 = rec_buf 142 let ct_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8 143 let tag_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN + frag + 1) as *u8 144 let enc_v: i64 = nx_tls13_record_encrypt_v2(s.cipher_suite, s.client_app_traffic_key, s.client_app_iv, s.client_app_seq, ((req as i64) + snd_off) as *u8, frag, NX_TLS13_CT_APPLICATION_DATA, 0, header_out, ct_out, tag_out) 145 s.client_app_seq = s.client_app_seq + 1 146 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - 2 } 147 let total: i64 = NX_TLS13_RECORD_HEADER_LEN + frag + 1 + NX_TLS13_RECORD_TAG_LEN 148 if mc_write_n(fd, rec_buf, total) < 0 { return 0 - 3 } 149 snd_off = snd_off + frag 150 if snd_off < req_len { first_frag = 1 } 151 } 152 var acc: i64 = 0 153 let rec_in: *u8 = sys_mmap(MC_MAGIC_16645) 154 let plain: *u8 = sys_mmap(MC_MAGIC_16645) 155 let cttype: *i64 = sys_mmap(16) as *i64 156 let ptlen: *i64 = sys_mmap(16) as *i64 157 while acc < out_cap { 158 let rin: i64 = nx_tls13_read_record_from_fd(fd, rec_in, MC_MAGIC_16645) 159 if rin < 0 { return acc } 160 let ctlen: i64 = rin - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN 161 let rin_ct: *u8 = ((rec_in as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8 162 let rin_tag: *u8 = ((rec_in as i64) + rin - NX_TLS13_RECORD_TAG_LEN) as *u8 163 let dv: i64 = nx_tls13_record_decrypt_v2(s.cipher_suite, s.server_app_traffic_key, s.server_app_iv, s.server_app_seq, rec_in, rin_ct, ctlen, rin_tag, plain, cttype, ptlen) 164 s.server_app_seq = s.server_app_seq + 1 165 if dv != NX_TLS13_REC_VERDICT_OK { return acc } 166 if cttype[0] == NX_TLS13_CT_ALERT { return acc } 167 if cttype[0] == NX_TLS13_CT_APPLICATION_DATA { 168 let tc: i64 = ptlen[0] 169 if acc + tc > out_cap { return acc } 170 var i: i64 = 0 171 while i < tc { out[acc + i] = plain[i]; i = i + 1 } 172 acc = acc + tc 173 } 174 } 175 return acc 176} 177 178// Connect to full_url's host:443, validated TLS-1.3 handshake, send req, drain response. Verbatim copy of mcl_req. 179func mc_req(store: *TrustStore, full_url: *u8, target: *NxHttpsTarget, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 { 180 let now: i64 = sys_now_realtime_sec() 181 let fd_p: *i64 = sys_mmap(16) as *i64 182 if nx_https_url_connect(target, full_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return MC_CONNECT_FAIL } 183 let fd: i64 = fd_p[0] 184 let host: *u8 = ((full_url as i64) + target.url.host_off) as *u8 185 let hlen: i64 = target.url.host_len 186 let cr: *u8 = sys_mmap(32) 187 let priv: *u8 = sys_mmap(32) 188 nx_csprng_fill(cr, 32) 189 nx_csprng_fill(priv, 32) 190 let vc: *TlsValidationContext = sys_mmap(128) as *TlsValidationContext 191 vc.store = store 192 vc.sni_host = host 193 vc.sni_host_len = hlen 194 vc.now_epoch = now 195 let sr: i64 = nx_tls13_client_session_run(fd, host, hlen, cr, priv, vc) 196 if sr <= 0 { sys_close(fd); return MC_HANDSHAKE_FAIL } 197 let session: *Tls13ClientSession = sr as *Tls13ClientSession 198 let n: i64 = mc_send_drain(session, fd, req, req_len, out, out_cap) 199 sys_close(fd) 200 return n 201} 202 203func main(argc: i64, argv: *i64) -> i64 { 204 if argc < 4 { 205 mc_puts("usage: nx_mcp_call <base_url> <tool_name> <cap_file> [args_json_fragment]\n" as *u8) 206 sys_exit(1); return 1 207 } 208 let base_url: *u8 = argv[1] as *u8 209 let tool: *u8 = argv[2] as *u8 210 let capfile: *u8 = argv[3] as *u8 211 212 let store_rc: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, MC_MAGIC_4194304) 213 if store_rc <= 0 { mc_puts("certdata load failed (run from the nxc2 dir)\n" as *u8); sys_exit(1); return 1 } 214 let store: *TrustStore = store_rc as *TrustStore 215 216 // cap from a file (secret hygiene, never argv) -- rtrim the trailing newline 217 let cbuf: *u8 = sys_mmap(MC_MAGIC_8192) 218 let craw: i64 = mc_read_file(capfile, cbuf, MC_MAGIC_8191) 219 if craw < 0 { mc_puts("cannot read cap file\n" as *u8); sys_exit(1); return 1 } 220 let cn: i64 = mc_rtrim_nl(cbuf, craw) 221 cbuf[cn] = 0 as u8 222 223 // build the JSON-RPC tools/call body: {"jsonrpc":"2.0","id":1,"method":"tools/call", 224 // "params":{"name":"<tool>","arguments":{"_cap":"<cap>"[,<extra>]}}} 225 let body: *u8 = sys_mmap(MC_MAGIC_65536) 226 var b: i64 = 0 227 // DIAGNOSTIC: tool_name "__initialize__" sends an MCP initialize (probe whether the daemon needs a session). 228 if mc_slen(tool) == 14 { if tool[0] == (95 as u8) { // "__initialize__" 229 b = mc_cat(body, b, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"nx_mcp_call\",\"version\":\"1\"}}}" as *u8) 230 } } 231 if b == 0 { 232 b = mc_cat(body, b, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"" as *u8) 233 b = mc_cat(body, b, tool) 234 b = mc_cat(body, b, "\",\"arguments\":{\"_cap\":\"" as *u8) 235 b = mc_catb(body, b, cbuf, cn) 236 b = mc_cat(body, b, "\"" as *u8) 237 if argc >= 5 { b = mc_cat(body, b, "," as *u8); b = mc_cat(body, b, argv[4] as *u8) } 238 b = mc_cat(body, b, "}}}" as *u8) 239 } 240 let body_len: i64 = b 241 242 let full: *u8 = sys_mmap(MC_MAGIC_4096) 243 let ulen: i64 = mc_join_url(base_url, "/mcp" as *u8, full) 244 let target_raw: *u8 = sys_mmap(64) 245 let target: *NxHttpsTarget = target_raw as *NxHttpsTarget 246 target.url = nx_url_new(); target.port = 0 247 if nx_https_url_for_fetch(full, target) != NX_HTTPS_URL_OK { mc_puts("bad base_url\n" as *u8); sys_exit(1); return 1 } 248 let host: *u8 = ((full as i64) + target.url.host_off) as *u8 249 let hlen: i64 = target.url.host_len 250 let rpath: *u8 = ((full as i64) + target.url.path_off) as *u8 251 let rplen: i64 = target.url.path_len 252 253 let req: *u8 = sys_mmap(MC_MAGIC_131072) 254 let req_len: i64 = mc_build_request("POST" as *u8, 4, rpath, rplen, host, hlen, cbuf, cn, "application/json" as *u8, 16, body, body_len, req) 255 256 let out: *u8 = sys_mmap(MC_MAGIC_1048576) 257 let n: i64 = mc_req(store, full, target, req, req_len, out, MC_MAGIC_1048576) 258 if n < 0 { mc_puts("MCP-CALL transport FAIL rc=" as *u8); mc_putn(n); mc_puts("\n" as *u8); sys_exit(1); return 1 } 259 if n == 0 { mc_puts("MCP-CALL empty response (status=0 / lost response)\n" as *u8); sys_exit(1); return 1 } 260 261 // parse the HTTP response -> print status + body (the JSON-RPC result / error) 262 let rr: *i64 = nx_http_resp_alloc() 263 if nx_http_response_parse(out, n, rr) != 0 { mc_puts("MCP-CALL unparseable response; raw:\n" as *u8); sys_write(1, out, n); sys_exit(1); return 1 } 264 let status: i64 = rr[1] 265 let body_off: i64 = rr[6] 266 mc_puts("HTTP status=" as *u8); mc_putn(status); mc_puts("\n" as *u8) 267 mc_puts("--- headers ---\n" as *u8); sys_write(1, out, body_off) 268 mc_puts("--- body ---\n" as *u8) 269 let rb_len: i64 = n - body_off 270 if rb_len > 0 { sys_write(1, ((out as i64) + body_off) as *u8, rb_len) } 271 mc_puts("\n" as *u8) 272 sys_exit(0) 273 return 0 274}