code wiki / (root) / nx_mcp_transport.nx

nx_mcp_transport.nx source

↩ module page · 223 lines · 12353 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_https_url_for_fetch.nx" // nx_https_url_for_fetch, NxUrl, nx_url_new, NxHttpsTarget, NX_HTTPS_URL_OK 24import "nx_https_url_connect.nx" // nx_https_url_connect, NX_HTTPS_CONNECT_OK 25import "nx_tls13_client_session_run.nx" // nx_tls13_client_session_run, TlsValidationContext 26import "nx_tls13_client_session.nx" // Tls13ClientSession, NX_TLS13_CSESSION_STATE_CONNECTED 27import "nx_tls13.nx" // NX_TLS13_CT_APPLICATION_DATA, NX_TLS13_CT_ALERT 28import "nx_tls13_record.nx" // nx_tls13_record_encrypt_v2/_decrypt_v2 + record header/tag consts 29import "nx_tls13_read_record_from_fd.nx" // nx_tls13_read_record_from_fd 30import "nx_csprng.nx" // nx_csprng_fill 31 32const MC_CONNECT_FAIL: i64 = 0 - 11 33const MC_HANDSHAKE_FAIL: i64 = 0 - 12 34 35func 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 } 36func mc_putn(v: i64) -> i64 { 37 let b: *u8 = sys_mmap(28); var x: i64 = v 38 if x < 0 { b[0] = 45 as u8; sys_write(1, b, 1); x = 0 - x } 39 if x == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 } 40 var d: i64 = 0; var y: i64 = x 41 while y > 0 { d = d + 1; y = y / 10 } 42 var i: i64 = d - 1; y = x 43 while i >= 0 { b[i] = (48 + (y % 10)) as u8; y = y / 10; i = i - 1 } 44 sys_write(1, b, d); return 0 45} 46func mc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 47func 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 } 48func 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 } 49func mc_catn(d: *u8, o: i64, v: i64) -> i64 { 50 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 51 if m == 0 { t[0] = 48 as u8; k = 1 } 52 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 53 var w: i64 = o; var i: i64 = 0 54 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 55 return w 56} 57 58func mc_write_n(fd: i64, buf: *u8, n: i64) -> i64 { 59 var off: i64 = 0 60 while off < n { 61 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off) 62 if w <= 0 { return 0 - 1 } 63 off = off + w 64 } 65 return 0 66} 67func mc_read_file(path: *u8, out: *u8, cap: i64) -> i64 { 68 let fd: i64 = sys_openat_rd(path) 69 if fd <= 0 { return 0 - 1 } 70 var off: i64 = 0 71 var go: i64 = 1 72 while go == 1 { 73 if off >= cap { go = 0 } 74 else { let r: i64 = sys_read(fd, ((out as i64) + off) as *u8, cap - off); if r <= 0 { go = 0 } else { off = off + r } } 75 } 76 sys_close(fd) 77 return off 78} 79func mc_rtrim_nl(buf: *u8, n: i64) -> i64 { 80 var m: i64 = n 81 var go: i64 = 1 82 while go == 1 { 83 if m <= 0 { go = 0 } 84 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 } } } 85 } 86 return m 87} 88func mc_join_url(base_url: *u8, suffix: *u8, out: *u8) -> i64 { 89 var o: i64 = mc_cat(out, 0, base_url) 90 if o > 0 { if out[o - 1] == (47 as u8) { o = o - 1 } } 91 if suffix[0] != (47 as u8) { out[o] = 47 as u8; o = o + 1 } 92 o = mc_cat(out, o, suffix) 93 out[o] = 0 as u8 94 return o 95} 96 97// Build a full HTTP/1.1 request (origin-form target). Content-Type parameterized -> we pass application/json. 98func 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 { 99 var o: i64 = 0 100 o = mc_catb(out, o, method, method_len) 101 out[o] = 0x20 as u8; o = o + 1 102 o = mc_catb(out, o, path, path_len) 103 o = mc_cat(out, o, " HTTP/1.1\r\nHost: " as *u8) 104 o = mc_catb(out, o, host, host_len) 105 o = mc_cat(out, o, "\r\n" as *u8) 106 o = mc_cat(out, o, "Accept: application/json, text/event-stream\r\n" as *u8) // MCP Streamable-HTTP transport requires it 107 if cap_len > 0 { // primary auth path (Claude Code presents the cap this way) 108 o = mc_cat(out, o, "X-Nishi-Cap: " as *u8) 109 o = mc_catb(out, o, cap, cap_len) 110 o = mc_cat(out, o, "\r\n" as *u8) 111 } 112 if body_len > 0 { 113 o = mc_cat(out, o, "Content-Type: " as *u8) 114 o = mc_catb(out, o, ct, ct_len) 115 o = mc_cat(out, o, "\r\nContent-Length: " as *u8) 116 o = mc_catn(out, o, body_len) 117 o = mc_cat(out, o, "\r\n" as *u8) 118 } 119 o = mc_cat(out, o, "Connection: close\r\n\r\n" as *u8) 120 if body_len > 0 { o = mc_catb(out, o, body, body_len) } 121 return o 122} 123 124// A complete Content-Length response does not require waiting for the peer's idle-close timer. 125// Zero means headers incomplete; -1 retains close-delimited behavior; -2 is an invalid/oversized frame. 126func mc_http_expected(src: *u8, n: i64, capacity: i64) -> i64 { 127 if nx_http_resp_find_crlfcrlf(src, n, 0) < 0 { return 0 } 128 let parsed: *i64 = nx_http_resp_alloc() 129 if nx_http_response_parse(src, n, parsed) != NX_HTTP_RESP_OK { return 0 - 2 } 130 if parsed[8] != NX_HTTP_BODY_CONTENT_LENGTH { return 0 - 1 } 131 if parsed[7] < 0 { return 0 - 2 } 132 if parsed[6] > capacity { return 0 - 2 } 133 if parsed[7] > capacity - parsed[6] { return 0 - 2 } 134 return parsed[6] + parsed[7] 135} 136 137func mc_received_result(acc: i64, expected: i64) -> i64 { 138 if expected == 0 { return 0 - 6 } 139 if expected > 0 { if acc < expected { return 0 - 6 } } 140 return acc 141} 142 143// Send a PRE-BUILT request over a CONNECTED TLS-1.3 session, then drain the full response into out. Verbatim 144// copy of nx_mgmt_client's proven mcl_send_drain (RFC 8446 <=16KB record fragmentation + decrypt loop). 145func mc_send_drain(s: *Tls13ClientSession, fd: i64, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 { 146 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { return 0 - 1 } 147 let rec_buf: *u8 = sys_mmap(16384 + 64) 148 var snd_off: i64 = 0 149 var first_frag: i64 = 1 150 while first_frag == 1 { first_frag = 0 151 var frag: i64 = req_len - snd_off 152 if frag > 16384 { frag = 16384 } 153 let header_out: *u8 = rec_buf 154 let ct_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8 155 let tag_out: *u8 = ((rec_buf as i64) + NX_TLS13_RECORD_HEADER_LEN + frag + 1) as *u8 156 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) 157 s.client_app_seq = s.client_app_seq + 1 158 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - 2 } 159 let total: i64 = NX_TLS13_RECORD_HEADER_LEN + frag + 1 + NX_TLS13_RECORD_TAG_LEN 160 if mc_write_n(fd, rec_buf, total) < 0 { return 0 - 3 } 161 snd_off = snd_off + frag 162 if snd_off < req_len { first_frag = 1 } 163 } 164 var acc: i64 = 0 165 var expected: i64 = 0 166 let rec_in: *u8 = sys_mmap(16645) 167 let plain: *u8 = sys_mmap(16645) 168 let cttype: *i64 = sys_mmap(16) as *i64 169 let ptlen: *i64 = sys_mmap(16) as *i64 170 while acc < out_cap { 171 let rin: i64 = nx_tls13_read_record_from_fd(fd, rec_in, 16645) 172 if rin < 0 { return mc_received_result(acc, expected) } 173 let ctlen: i64 = rin - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN 174 let rin_ct: *u8 = ((rec_in as i64) + NX_TLS13_RECORD_HEADER_LEN) as *u8 175 let rin_tag: *u8 = ((rec_in as i64) + rin - NX_TLS13_RECORD_TAG_LEN) as *u8 176 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) 177 s.server_app_seq = s.server_app_seq + 1 178 if dv != NX_TLS13_REC_VERDICT_OK { return 0 - 7 } 179 if cttype[0] == NX_TLS13_CT_ALERT { return mc_received_result(acc, expected) } 180 if cttype[0] == NX_TLS13_CT_APPLICATION_DATA { 181 let tc: i64 = ptlen[0] 182 if acc + tc > out_cap { return 0 - 5 } 183 var i: i64 = 0 184 while i < tc { out[acc + i] = plain[i]; i = i + 1 } 185 acc = acc + tc 186 if expected == 0 { expected = mc_http_expected(out, acc, out_cap) } 187 if expected == (0 - 2) { return 0 - 5 } 188 if expected > 0 { if acc >= expected { return expected } } 189 } 190 } 191 return 0 - 5 192} 193 194// Connect to full_url's host:443, validated TLS-1.3 handshake, send req, drain response. Verbatim copy of mcl_req. 195func mc_req_timed(store: *TrustStore, full_url: *u8, target: *NxHttpsTarget, req: *u8, req_len: i64, out: *u8, out_cap: i64, timeout_secs: i64) -> i64 { 196 let now: i64 = sys_now_realtime_sec() 197 let fd_p: *i64 = sys_mmap(16) as *i64 198 if nx_https_url_connect(target, full_url, now, fd_p) != NX_HTTPS_CONNECT_OK { return MC_CONNECT_FAIL } 199 let fd: i64 = fd_p[0] 200 if timeout_secs > 0 { sys_set_socket_timeout(fd, timeout_secs) } 201 let host: *u8 = ((full_url as i64) + target.url.host_off) as *u8 202 let hlen: i64 = target.url.host_len 203 let cr: *u8 = sys_mmap(32) 204 let priv: *u8 = sys_mmap(32) 205 nx_csprng_fill(cr, 32) 206 nx_csprng_fill(priv, 32) 207 let vc: *TlsValidationContext = sys_mmap(128) as *TlsValidationContext 208 vc.store = store 209 vc.sni_host = host 210 vc.sni_host_len = hlen 211 vc.now_epoch = now 212 let sr: i64 = nx_tls13_client_session_run(fd, host, hlen, cr, priv, vc) 213 if sr <= 0 { sys_close(fd); return MC_HANDSHAKE_FAIL } 214 let session: *Tls13ClientSession = sr as *Tls13ClientSession 215 let n: i64 = mc_send_drain(session, fd, req, req_len, out, out_cap) 216 sys_close(fd) 217 return n 218} 219 220// Preserve the original CLI API and its existing connection timeout. 221func mc_req(store: *TrustStore, full_url: *u8, target: *NxHttpsTarget, req: *u8, req_len: i64, out: *u8, out_cap: i64) -> i64 { 222 return mc_req_timed(store, full_url, target, req, req_len, out, out_cap, 0) 223}