feat(mcp): close the container gap + HTTP/SSE transport + opt-in resources/prompts#91
Merged
Conversation
…urces/prompts Four upgrades so MCP works everywhere and reaches remote servers: 1. Container gap closed — the model's MCP path is now a host-dispatched `mcp` tool (in loopTools(), the execute registry, alongside read/write/git), backed by a process-wide internal/mcp.Manager. Running host-side means MCP works on every sandbox tier, including the macOS container default where the nilcore binary + server runtime are not inside the box. Recorded as a bounded I4 relaxation (operator-configured servers only; model picks server+tool/resource/prompt+args as data, I7-fenced; audited). 2. HTTP/SSE transport — the JSON-RPC client is refactored onto a transport seam (transport.go): stdio (subprocess) or Streamable HTTP (remote url, JSON or SSE reply, Mcp-Session-Id echoed, operator headers host-side, I3). Stdlib net/http only; go.mod untouched (I6). 3. Persistent Manager — one live initialized connection per server, reused across calls, concurrency-safe, evict+reconnect-once on a dropped connection, ErrToolFailed sentinel so a tool-level failure is never retried. 4. Resources + prompts — opt-in via NILCORE_MCP_RESOURCES=1; off by default (tools-only, byte-identical). Adds STATE.md (dated architecture snapshot). Tests cover stdio/HTTP(JSON)/ HTTP(SSE) flows, Manager reuse+close over real stdio + HTTP, the failure sentinel, resource/prompt fetch + descriptor gen, HTTP config parsing; -race clean. make verify + tui-verify + full-repo golangci-lint (0 issues) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lifetime, single-flight, HTTP interop) A 4-lens adversarial review of PR #91 confirmed 14 findings (no invariant breaks). Remediated the substantive ones: - Retry-safety (dup side-effect): withRetry now retries ONLY a provably- undelivered send (errDeliveryFailed sentinel from the stdio send side); any server-RECEIVED failure (tool isError, JSON-RPC error, HTTP non-2xx, dropped reply) is returned as-is and never re-run — a delete/payment tool can't run twice. - I7 error-path fencing: the mcp tool guard.Wraps its untrusted error text, symmetric with the loop's guard.Wrap on the success path. - Subprocess lifetime: connections spawn under a Manager-owned context cancelled only by Close, not the per-turn request ctx, so a lazily-opened/reconnected stdio server survives the turn (request ctx still bounds the round-trip). - Single-flight get(): connect+initialize run outside the lock behind a reservation — no global head-of-line stall, no double-spawn. - HTTP interop: advertise protocol 2025-06-18 and send MCP-Protocol-Version on post-initialize requests; capture the negotiated version. - SSE multi-line data join; HTTP-notify status check; clearer SSE decode error. Deferred to a follow-up (low severity / riskier fixes): interruptible stdio Decode on a hung server, richer per-call audit detail, descriptor slug hygiene/pruning. Tests added: delivery-vs-received classification, concurrent single-flight, protocol-version header, error fencing. make verify + tui-verify + full-repo golangci-lint (0 issues) + -race green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 30, 2026
RNT56
added a commit
that referenced
this pull request
Jul 1, 2026
#93) Full grounded review (17 reviewers × adversarial verify) of main post #91/#92: 7 invariants intact; 29 issues surfaced; 26 distinct fixes, each with a test. HIGH (both in the merged #91 mcp code): - stdio transport could deadlock every caller under an uninterruptible blocking Decode holding the per-server mutex — now ctx-cancellable + evict-on-cancel, plus a bounded Discover boot timeout. - untrusted MCP tool name written to a host path unsanitized (traversal) — now slug()-sanitized like resources/prompts. Also: live token streaming restored (Resilient forwards live on attempt 0) + partial-on-cancel; experience Fold moved off the eventlog append mutex (single ordered drainer); kill-switch WithRoot wired; browser Enter-submit gated; desktop stale-ref versioned; secret reflow scrubbed for all field types; flywheel RotatedLogPaths wired; project SWITCH threads *State; code.test_passes runs ./...; LSP Content-Length capped; keychain fail-closed; +more. make verify (119 pkgs) + tui-verify + golangci-lint + -race all green. Co-authored-by: RNT56 <ridfox44@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Four MCP upgrades so it works everywhere and reaches remote servers. Requested end-to-end: STATE.md, close the container gap (MCP on macOS default), HTTP/SSE transport, resources/prompts (opt-in).
1. Container gap closed (the "ultra important" one)
Previously the model invoked MCP by running
nilcore mcp-callvia the sandboxedruntool. That works in the Linux namespace sandbox (hostnilcore+ runtime reachable) but fails in the default container (debian-slim has nonilcore, no node/python) — so MCP did not work on macOS default. Bind-mounting the host binary can't fix it on macOS either (a darwin binary can't run in a Linux box).Fix: a host-dispatched native
mcptool (registered inloopTools(), the execute registry, alongside read/write/git), backed by a process-wideinternal/mcp.Manager. The call runs host-side, so it never needsnilcore/a runtime inside the box → MCP now works on every sandbox tier, including the macOS container default.Recorded as a bounded I4 relaxation in
docs/ARCHITECTURE.md§Execution model: servers are operator-configured (never model-emitted), the model selects only server + tool/resource/prompt + JSON args (data, I7-fenced viaguard.Wrap), output is audited — the same host-side placesetupMCPalready spawned servers for discovery.2. HTTP/SSE transport
The JSON-RPC client is refactored onto a
transportseam (internal/mcp/transport.go): stdio (subprocess) or Streamable HTTP (remoteurlinmcp.json, JSON or SSE reply,Mcp-Session-Idechoed, operatorheadersresolved host-side per I3). Stdlibnet/httponly —go.moduntouched (I6).3. Persistent Manager
One live, initialized connection per server, reused across calls (stdio spawned once, HTTP session kept), concurrency-safe, evict + reconnect once on a dropped connection, and an
ErrToolFailedsentinel so a tool-level failure is never silently retried (no repeated side effect).4. Resources + prompts — opt-in
Via
NILCORE_MCP_RESOURCES=1: descriptors also generated for resources/prompts and themcptool honors{server,resource}/{server,prompt,args}. Off by default ⇒ tools-only, byte-identical.nilcore mcp-callretained (operator CLI + namespace-sandbox shell path) and now also speaks HTTP. AddsSTATE.md(dated architecture snapshot).mcp.json
{ "servers": [ { "name": "docs", "command": ["npx","-y","@modelcontextprotocol/server-filesystem","/data"] }, { "name": "remote", "url": "https://mcp.example.com/v1", "headers": {"Authorization": "Bearer …"} } ] }Invariants
backend.CodingBackendunchanged). I6 holds (go.mod/go.sumunchanged; stdlibnet/http). I3 HTTP auth headers host-side, never to the model. I7 server outputguard.Wrap'd. I4 host-side MCP recorded as a bounded relaxation (operator-configured surface only).Verification
make verify✅ ·make tui-verify✅ · full-repogolangci-lint run→ 0 issues ✅ ·gofmt -l .clean ✅go test -race ./internal/mcp/ ./cmd/nilcore/✅ (stdio + HTTP-JSON + HTTP-SSE flows; Manager reuse/close over real stdio subprocess + HTTP; failure sentinel; resource/prompt fetch + descriptor gen; HTTP config parse)CGO_ENABLED=0 GOOS=linuxcross-build ✅🤖 Generated with Claude Code