code wiki / (root) / nx_mcp_call.nx

nx_mcp_call.nx source

↩ module page · 272 lines · 15107 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 if m == 0 { t[0] = 48 as u8; k = 1 } 57 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 58 var w: i64 = o; var i: i64 = 0 59 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 60 return w 61} 62 63func mc_write_n(fd: i64, buf: *u8, n: i64) -> i64 { 64 var off: i64 = 0 65 while off < n { 66 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off) 67 if w <= 0 { return 0 - 1 } 68 off = off + w 69 } 70 return 0 71} 72func mc_read_file(path: *u8, out: *u8, cap: i64) -> i64 { 73 let fd: i64 = sys_openat_rd(path) 74 if fd <= 0 { return 0 - 1 } 75 var off: i64 = 0 76 var go: i64 = 1 77 while go == 1 { 78 if off >= cap { go = 0 } 79 else { let r: i64 = sys_read(fd, ((out as i64) + off) as *u8, cap - off); if r <= 0 { go = 0 } else { off = off + r } } 80 } 81 sys_close(fd) 82 return off 83} 84func mc_rtrim_nl(buf: *u8, n: i64) -> i64 { 85 var m: i64 = n 86 var go: i64 = 1 87 while go == 1 { 88 if m <= 0 { go = 0 } 89 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 } } } 90 } 91 return m 92} 93func mc_join_url(base_url: *u8, suffix: *u8, out: *u8) -> i64 { 94 var o: i64 = mc_cat(out, 0, base_url) 95 if o > 0 { if out[o - 1] == (47 as u8) { o = o - 1 } } 96 if suffix[0] != (47 as u8) { out[o] = 47 as u8; o = o + 1 } 97 o = mc_cat(out, o, suffix) 98 out[o] = 0 as u8 99 return o 100} 101 102// Build a full HTTP/1.1 request (origin-form target). Content-Type parameterized -> we pass application/json. 103func 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 { 104 var o: i64 = 0 105 o = mc_catb(out, o, method, method_len) 106 out[o] = 0x20 as u8; o = o + 1 107 o = mc_catb(out, o, path, path_len) 108 o = mc_cat(out, o, " HTTP/1.1\r\nHost: " as *u8) 109 o = mc_catb(out, o, host, host_len) 110 o = mc_cat(out, o, "\r\n" as *u8) 111 o = mc_cat(out, o, "Accept: application/json, text/event-stream\r\n" as *u8) // MCP Streamable-HTTP transport requires it 112 if cap_len > 0 { // primary auth path (Claude Code presents the cap this way) 113 o = mc_cat(out, o, "X-Nishi-Cap: " as *u8) 114 o = mc_catb(out, o, cap, cap_len) 115 o = mc_cat(out, o, "\r\n" as *u8) 116 } 117 if body_len > 0 { 118 o = mc_cat(out, o, "Content-Type: " as *u8) 119 o = mc_catb(out, o, ct, ct_len) 120 o = mc_cat(out, o, "\r\nContent-Length: " as *u8) 121 o = mc_catn(out, o, body_len) 122 o = mc_cat(out, o, "\r\n" as *u8) 123 } 124 o = mc_cat(out, o, "Connection: close\r\n\r\n" as *u8) 125 if body_len > 0 { o = mc_catb(out, o, body, body_len) } 126 return o 127} 128 129// Send a PRE-BUILT request over a CONNECTED TLS-1.3 session, then drain the full response into out. Verbatim 130// copy of nx_mgmt_client's proven mcl_send_drain (RFC 8446 <=16KB record fragmentation + decrypt loop). 131func mc_send_drain(s: *Tls13ClientSession, fd: i64, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 { 132 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { return 0 - 1 } 133 let rec_buf: *u8 = sys_mmap(MC_MAGIC_16384 + 64) 134 var snd_off: i64 = 0 135 var first_frag: i64 = 1 136 while first_frag == 1 { first_frag = 0 137 var frag: i64 = req_len - snd_off 138 if frag > MC_MAGIC_16384 { frag = MC_MAGIC_16384 } 139 let header_out: *u8 = rec_buf 140 let ct_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8 141 let tag_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN + frag + 1) as *u8 142 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) 143 s.client_app_seq = s.client_app_seq + 1 144 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - 2 } 145 let total: i64 = NX_TLS13_RECORD_HEADER_LEN + frag + 1 + NX_TLS13_RECORD_TAG_LEN 146 if mc_write_n(fd, rec_buf, total) < 0 { return 0 - 3 } 147 snd_off = snd_off + frag 148 if snd_off < req_len { first_frag = 1 } 149 } 150 var acc: i64 = 0 151 let rec_in: *u8 = sys_mmap(MC_MAGIC_16645) 152 let plain: *u8 = sys_mmap(MC_MAGIC_16645) 153 let cttype: *i64 = sys_mmap(16) as *i64 154 let ptlen: *i64 = sys_mmap(16) as *i64 155 while acc < out_cap { 156 let rin: i64 = nx_tls13_read_record_from_fd(fd, rec_in, MC_MAGIC_16645) 157 if rin < 0 { return acc } 158 let ctlen: i64 = rin - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN 159 let rin_ct: *u8 = ((rec_in as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8 160 let rin_tag: *u8 = ((rec_in as i64) + rin - NX_TLS13_RECORD_TAG_LEN) as *u8 161 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) 162 s.server_app_seq = s.server_app_seq + 1 163 if dv != NX_TLS13_REC_VERDICT_OK { return acc } 164 if cttype[0] == NX_TLS13_CT_ALERT { return acc } 165 if cttype[0] == NX_TLS13_CT_APPLICATION_DATA { 166 let tc: i64 = ptlen[0] 167 if acc + tc > out_cap { return acc } 168 var i: i64 = 0 169 while i < tc { out[acc + i] = plain[i]; i = i + 1 } 170 acc = acc + tc 171 } 172 } 173 return acc 174} 175 176// Connect to full_url's host:443, validated TLS-1.3 handshake, send req, drain response. Verbatim copy of mcl_req. 177func mc_req(store: *TrustStore, full_url: *u8, target: *NxHttpsTarget, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 { 178 let now: i64 = sys_now_realtime_sec() 179 let fd_p: *i64 = sys_mmap(16) as *i64 180 if nx_https_url_connect(target, full_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return MC_CONNECT_FAIL } 181 let fd: i64 = fd_p[0] 182 let host: *u8 = ((full_url as i64) + target.url.host_off) as *u8 183 let hlen: i64 = target.url.host_len 184 let cr: *u8 = sys_mmap(32) 185 let priv: *u8 = sys_mmap(32) 186 nx_csprng_fill(cr, 32) 187 nx_csprng_fill(priv, 32) 188 let vc: *TlsValidationContext = sys_mmap(128) as *TlsValidationContext 189 vc.store = store 190 vc.sni_host = host 191 vc.sni_host_len = hlen 192 vc.now_epoch = now 193 let sr: i64 = nx_tls13_client_session_run(fd, host, hlen, cr, priv, vc) 194 if sr <= 0 { sys_close(fd); return MC_HANDSHAKE_FAIL } 195 let session: *Tls13ClientSession = sr as *Tls13ClientSession 196 let n: i64 = mc_send_drain(session, fd, req, req_len, out, out_cap) 197 sys_close(fd) 198 return n 199} 200 201func main(argc: i64, argv: *i64) -> i64 { 202 if argc < 4 { 203 mc_puts("usage: nx_mcp_call <base_url> <tool_name> <cap_file> [args_json_fragment]\n" as *u8) 204 sys_exit(1); return 1 205 } 206 let base_url: *u8 = argv[1] as *u8 207 let tool: *u8 = argv[2] as *u8 208 let capfile: *u8 = argv[3] as *u8 209 210 let store_rc: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, MC_MAGIC_4194304) 211 if store_rc <= 0 { mc_puts("certdata load failed (run from the nxc2 dir)\n" as *u8); sys_exit(1); return 1 } 212 let store: *TrustStore = store_rc as *TrustStore 213 214 // cap from a file (secret hygiene, never argv) -- rtrim the trailing newline 215 let cbuf: *u8 = sys_mmap(MC_MAGIC_8192) 216 let craw: i64 = mc_read_file(capfile, cbuf, MC_MAGIC_8191) 217 if craw < 0 { mc_puts("cannot read cap file\n" as *u8); sys_exit(1); return 1 } 218 let cn: i64 = mc_rtrim_nl(cbuf, craw) 219 cbuf[cn] = 0 as u8 220 221 // build the JSON-RPC tools/call body: {"jsonrpc":"2.0","id":1,"method":"tools/call", 222 // "params":{"name":"<tool>","arguments":{"_cap":"<cap>"[,<extra>]}}} 223 let body: *u8 = sys_mmap(MC_MAGIC_65536) 224 var b: i64 = 0 225 // DIAGNOSTIC: tool_name "__initialize__" sends an MCP initialize (probe whether the daemon needs a session). 226 if mc_slen(tool) == 14 { if tool[0] == (95 as u8) { // "__initialize__" 227 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) 228 } } 229 if b == 0 { 230 b = mc_cat(body, b, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"" as *u8) 231 b = mc_cat(body, b, tool) 232 b = mc_cat(body, b, "\",\"arguments\":{\"_cap\":\"" as *u8) 233 b = mc_catb(body, b, cbuf, cn) 234 b = mc_cat(body, b, "\"" as *u8) 235 if argc >= 5 { b = mc_cat(body, b, "," as *u8); b = mc_cat(body, b, argv[4] as *u8) } 236 b = mc_cat(body, b, "}}}" as *u8) 237 } 238 let body_len: i64 = b 239 240 let full: *u8 = sys_mmap(MC_MAGIC_4096) 241 let ulen: i64 = mc_join_url(base_url, "/mcp" as *u8, full) 242 let target_raw: *u8 = sys_mmap(64) 243 let target: *NxHttpsTarget = target_raw as *NxHttpsTarget 244 target.url = nx_url_new(); target.port = 0 245 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 } 246 let host: *u8 = ((full as i64) + target.url.host_off) as *u8 247 let hlen: i64 = target.url.host_len 248 let rpath: *u8 = ((full as i64) + target.url.path_off) as *u8 249 let rplen: i64 = target.url.path_len 250 251 let req: *u8 = sys_mmap(MC_MAGIC_131072) 252 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) 253 254 let out: *u8 = sys_mmap(MC_MAGIC_1048576) 255 let n: i64 = mc_req(store, full, target, req, req_len, out, MC_MAGIC_1048576) 256 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 } 257 if n == 0 { mc_puts("MCP-CALL empty response (status=0 / lost response)\n" as *u8); sys_exit(1); return 1 } 258 259 // parse the HTTP response -> print status + body (the JSON-RPC result / error) 260 let rr: *i64 = nx_http_resp_alloc() 261 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 } 262 let status: i64 = rr[1] 263 let body_off: i64 = rr[6] 264 mc_puts("HTTP status=" as *u8); mc_putn(status); mc_puts("\n" as *u8) 265 mc_puts("--- headers ---\n" as *u8); sys_write(1, out, body_off) 266 mc_puts("--- body ---\n" as *u8) 267 let rb_len: i64 = n - body_off 268 if rb_len > 0 { sys_write(1, ((out as i64) + body_off) as *u8, rb_len) } 269 mc_puts("\n" as *u8) 270 sys_exit(0) 271 return 0 272}