Skip to content

feat(mcp): close the container gap + HTTP/SSE transport + opt-in resources/prompts#91

Merged
RNT56 merged 2 commits into
mainfrom
feat/mcp-upgrade
Jun 30, 2026
Merged

feat(mcp): close the container gap + HTTP/SSE transport + opt-in resources/prompts#91
RNT56 merged 2 commits into
mainfrom
feat/mcp-upgrade

Conversation

@RNT56

@RNT56 RNT56 commented Jun 30, 2026

Copy link
Copy Markdown
Owner

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-call via the sandboxed run tool. That works in the Linux namespace sandbox (host nilcore + runtime reachable) but fails in the default container (debian-slim has no nilcore, 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 mcp tool (registered in loopTools(), the execute registry, alongside read/write/git), backed by a process-wide internal/mcp.Manager. The call runs host-side, so it never needs nilcore/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 via guard.Wrap), output is audited — the same host-side place setupMCP already spawned servers for discovery.

2. HTTP/SSE transport

The JSON-RPC client is refactored onto a transport seam (internal/mcp/transport.go): stdio (subprocess) or Streamable HTTP (remote url in mcp.json, JSON or SSE reply, Mcp-Session-Id echoed, operator headers resolved host-side per I3). Stdlib net/http only — go.mod untouched (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 ErrToolFailed sentinel 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 the mcp tool honors {server,resource} / {server,prompt,args}. Off by default ⇒ tools-only, byte-identical.

nilcore mcp-call retained (operator CLI + namespace-sandbox shell path) and now also speaks HTTP. Adds STATE.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

  • I1 untouched (backend.CodingBackend unchanged). I6 holds (go.mod/go.sum unchanged; stdlib net/http). I3 HTTP auth headers host-side, never to the model. I7 server output guard.Wrap'd. I4 host-side MCP recorded as a bounded relaxation (operator-configured surface only).

Verification

  • make verify ✅ · make tui-verify ✅ · full-repo golangci-lint run0 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=linux cross-build ✅

🤖 Generated with Claude Code

RNT56 and others added 2 commits June 30, 2026 10:59
…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>
@RNT56 RNT56 merged commit 6cb82d1 into main Jun 30, 2026
6 checks passed
@RNT56 RNT56 deleted the feat/mcp-upgrade branch June 30, 2026 10:26
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant