nurl-mcp — a local MCP (Model Context Protocol) server for the NURL toolchain. The LLM-facing counterpart of nurl-lsp: where nurl-lsp serves editors over LSP, nurl-mcp serves an LLM agent over MCP so it can drive the locally installed compiler — build, run, type-check, format NURL, compile it to wasm32-wasi (via the wasmbuilder package, no build service), and read the installed standard library — against the host's real filesystem.
Transport: newline-delimited JSON-RPC 2.0 over stdin/stdout (the transport every MCP client supports). Logs go to stderr; stdout is protocol-only. Wire it into an MCP client by pointing its command at the built binary, e.g. claude mcp add nurl -- nurl-mcp.
The build/run/check/fmt tools shell out to nurl / nurlc / nurlfmt (found on $PATH — the installed toolchain shims export $NURL_STDLIB so the compiler resolves stdlib imports). Running NURL is full code execution (libc FFI, no sandbox); over stdio that is safe because only the process that spawned this server talks to it. The --http transport serves the MCP Streamable-HTTP protocol via the stdlib facade (ext/mcp_http.nu: POST single/batch, GET SSE probe, Mcp-Session-Id echo, DELETE, CORS) with bearer-token auth layered on top; nurl_run stays off over HTTP unless --allow-run is given.
: ~ i g_read_only 0── Policy (set once in main, read by the dispatcher) ───────────────
g_read_only : 1 ⇒ expose only the read/analysis tools (check, fmt, list_stdlib, read_stdlib) — no build, no run. g_run_allowed : 1 ⇒ nurl_run is available. Defaults to 1 over stdio (only the spawning process can reach the server); over HTTP it is 0 unless --allow-run is passed, because run is unsandboxed code execution. (The --token value is run_http's: non-empty ⇒ HTTP requests must carry Authorization: Bearer <token>.)
: ~ i g_run_allowed 1@ nm_version → s@ nm_log_banner s suffix → vLog a startup banner "nurl-mcp <version> <suffix>" through mcp_log, building the line from the single-source version so the banners can never drift from the handshake version again.
: ~ i nm_seq 0@ nm_tmp_path s suffix → String@ nm_unlink s path → v@ nm_unlink_artifacts s base → vRemove a build's artifacts: the binary base and the base.ll IR that the build driver leaves beside it.
: NmInput { String path String source b piped }A tool's program: a path on the host, or inline source (piped), known by path — the filename argument, default main.nu.
@ nm_input Json args → ?NmInputThe program a tool's arguments name; None when neither path nor source is present.
@ nm_run_on s cmd s pre NmInput prog s post → !Output ProcessErrRun cmd on a program: pre (a flag, or ""), then the program — --stdin <name> with the source piped in, or its path — then post (the output path, or "").
@ nm_fail_from_output s prefix Output o b with_stdout → JsonTurn a non-zero-exit Output into an MCP error result carrying the exit code and captured diagnostics. with_stdout folds stdout in too (the build driver prints some diagnostics there; the front-end check prints IR there, so check passes F).
@ nm_proc_err ProcessErr e → Json@ nm_missing_input → Json@ nm_tool_check Json args → Json@ nm_tool_build Json args → Json@ nm_safe_rel s p → bReject a workspace-relative path that escapes the workspace or is absolute.
@ nm_ws_write s ws s rel s content → bWrite one workspace file, creating parent directories under ws. F when a directory or the file could not be written.
@ nm_last_slash s p → iIndex of the last '/' in p, or 0.
@ nm_tool_build_project Json args → Json@ nm_tool_build_wasm Json args → Json── Tool: nurl_build_wasm (compile to wasm32-wasi, fully local) ──────
Drives the wasmbuilder library in-process: nurlc → wasm32 IR rewrite → the toolchain's bundled zig cc. No build service, no network (a machine with no zig anywhere downloads it once — see the wasmbuilder README).
Output shape:
out argument given → module written there, text summarypath input, no out → written next to the input as<input minus .nu>.wasm
source, no out → module + its .ll kept at a temppath, JSON text with wasm_path / ll_path (mirrors the playground MCP tool — never inline base64, which would land verbatim in the calling model's context)
@ nm_run_result Output o → Json@ nm_tool_run Json args → Json@ nm_tool_fmt Json args → Json@ nm_tool_list_stdlib Json args → Json@ nm_has_dotdot s p → b@ nm_tool_read_stdlib Json args → Json@ nm_prefix → String$NURL_STDLIB is the toolchain prefix the shims export — the directory that HOLDS stdlib/ (the compiler resolves $ stdlib/…` against it) — or, set by hand, the stdlib directory itself. Every tool here names a module the way nurl_list_stdlib lists it and the playground does, relative to the stdlib directory (core/vec.nu), so that is the root they share. It was the prefix: module='core/vec.nu' and nurl_read_stdlib name='core/string.nu'` — the README's own examples — were "not found" on every installed toolchain.
@ nm_stdlib_root → String@ nm_docs_root → String── Tool: nurl_docs (shared engine: stdlib/ext/mcp_search) ──────────
nurl_api answers "what is the signature", never "who frees this" or "which cipher suites ship" — that is docs/MEMORY.md and docs/CRYPTO.md. install-toolchain.sh ships the tree next to the stdlib, so $NURL_STDLIB/docs is the default; $NURL_DOCS overrides it (a source checkout, or a prefix assembled by hand).
@ nm_tool_docs Json args → Json@ nm_tool_api Json args → Json@ nm_tool_grep Json args → Json@ nm_schema_api → Json@ nm_schema_grep → Json@ nm_schema_docs → Json@ nm_schema_src_path → Json@ nm_schema_project → Json@ nm_schema_name → Json@ nm_schema_empty → Json@ nm_build_ok → bPolicy predicates (read the globals set in main).
@ nm_run_ok → b@ nm_instructions → String@ nm_server → McpServer@ nm_cors HttpResponse r → v@ nm_is_loopback s host → b@ run_http McpServer srv s host i port s token → itoken is main's --token value, which lives as long as main: kept in a global it was a view a global outlived.
@ main → i