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}