LearnNewsExamplesServices
Frontmatter
id15185
titleBind local MCP ingress to loopback with a disposable bearer
stateClosed
labels
enhancementaiarchitecturesecurity
assigneesneo-gpt
createdAtJul 15, 2026, 12:02 AM
updatedAt9:55 AM
githubUrlhttps://github.com/neomjs/neo/issues/15185
authorneo-gpt
commentsCount2
parentIssue15184
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 15188 Rename the server transport value to streamable-http
blocking[ ] 15187 Prove the BigData Neural Link probe and erase diagnostics
closedAt9:55 AM

Bind local MCP ingress to loopback with a disposable bearer

Closed Backlog/active-chunk-6 enhancementaiarchitecturesecurity
neo-gpt
neo-gpt commented on Jul 15, 2026, 12:02 AM

Context

Epic #15184 needs a standards-compliant local Streamable HTTP ingress that is actually confined to loopback and does not introduce a user-identity system. The approved source contract lives in D#15173.

Live duplicate sweep: checked the latest 20 open issues, broader GitHub search, recent A2A claims, Knowledge Base tickets, and synced issue/discussion content at 2026-07-14T22:02:06.716Z; no equivalent active ticket exists. Closed transport/auth work provides reusable primitives but does not provide this opt-in local contract.

Self-selection boundary: Neo-maintainer implementation lane. External contributors may supply falsifiers or focused tests, but shared transport/auth/config architecture remains Neo-maintainer-owned.

The Problem

TransportService computes an advertised URL from mcpHttpHost but currently calls app.listen(port), so the actual listener bind is not pinned to that host. The shared Express app also installs wildcard CORS, while the required non-browser probe needs the narrower rule: accept an absent Origin, reject every present Origin, and retain the existing Host allowlist.

The existing auth strategies are OIDC and long-lived GitLab PAT validation. Neither is the correct boundary for a one-run local probe. The missing mode is a generated process-lifetime bearer that proves possession without creating or resolving an identity.

A global rebinding or auth rewrite would put existing KB/MC deployments at risk. The change must be opt-in and fail closed only for the new local mode.

The Architectural Reality

The shared authority is ai/config.mjs / ai/config.template.mjs plus ai/mcp/server/shared/services/TransportService.mjs and AuthService.mjs.

  • mcpHttpHost is the advertised host; it is not presently the listener bind.
  • computeAllowedHosts() already owns Host-header DNS-rebinding protection.
  • TransportService.setup() installs wildcard CORS before /mcp and invokes app.listen(port).
  • auth.mode already selects the shared auth strategy.
  • ADR 0019 governs every new AiConfig leaf: provider-owned declarative configuration, read at the use site, with no env re-derivation, pass-along aliases, hidden defaults, runtime mutation, or defensive optional chaining.

The Fix

Add an opt-in mcpListenHost AiConfig leaf at the shared transport authority. When absent, preserve the existing listener behavior. When auth.mode === 'local-bearer', startup requires mcpListenHost === '127.0.0.1' and a process-lifetime bearer supplied through a declarative auth leaf.

Add a narrow local-bearer verifier to the existing auth seam. The journey/launch path generates 32 cryptographically random bytes and encodes them as unpadded base64url, injects the value into the server and reference client, and never writes it through Neo logging, SQLite, files, or durable configuration.

Install the local-mode Origin rejection before wildcard CORS and before MCP dispatch. Host allowlisting, Origin policy, actual bind, and bearer verification remain independent guards.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Compatibility Docs Evidence
mcpListenHost Shared AiConfig Provider under ADR 0019 Optional actual listener bind; local-bearer mode requires literal 127.0.0.1 Absent leaf preserves current app.listen(port) behavior for existing profiles Config JSDoc + template Provider/config tests + socket reachability probe
auth.mode = 'local-bearer' Shared AuthService strategy switch Validate one process-lifetime secret without identity lookup OIDC and GitLab-PAT behavior unchanged Auth-mode JSDoc Valid/missing/invalid/length-mismatch bearer tests
Origin on local-bearer requests TransportService middleware order Absent accepted; every present value rejected before CORS Other modes keep their current CORS behavior Transport JSDoc Absent/present Origin matrix
Host header computeAllowedHosts() Existing allowlist remains mandatory and independent No widening Existing JSDoc amended only if needed Invalid-Host rejection regression
Bearer material Launch/journey boundary + declarative auth leaf 32 random bytes, unpadded base64url, constant-time equal-length compare, no log/persistence Startup fails closed if local mode lacks token Local launch recipe Secret non-observability assertions

Decision Record impact

aligned-with ADR 0019 and ADR 0020; no ADR amendment.

Decision Record

NOT_NEEDED — consumes existing shared transport/auth/config authority.

Acceptance Criteria

  • Shared AiConfig defines optional mcpListenHost and the local-bearer secret leaf in both canonical config/template surfaces under ADR 0019; consumers read them directly at the use site.
  • Existing profiles preserve their current listener behavior when mcpListenHost is absent.
  • auth.mode === 'local-bearer' fails startup unless the actual bind is literal 127.0.0.1 and a non-empty bearer is configured.
  • The launch/reference path generates exactly 32 random bytes, encodes unpadded base64url, and supplies the same one-run value to server and client without logger, database, file, or durable-config persistence.
  • Bearer validation uses a constant-time comparison for equal-length byte sequences and rejects length mismatches, missing headers, malformed schemes, and invalid values.
  • Local-bearer middleware accepts an absent Origin and rejects every present Origin before CORS or MCP dispatch.
  • Host allowlisting remains independently enforced.
  • Focused evidence proves: valid local request succeeds; present Origin, invalid Host, missing/invalid bearer, and non-loopback reachability all fail.
  • Regression coverage proves existing KB/MC OIDC, GitLab-PAT, proxy-identity, and unauthenticated local-dev profiles retain their current behavior.
  • Changed classes/methods/config leaves satisfy Contextual Completeness JSDoc and @summary requirements.

Out of Scope

  • Public/cloud authentication, GitHub PAT/OAuth/OIDC, or tenant identity.
  • Legacy HTTP+SSE support.
  • Neural Link tool projection or BigData journey behavior.
  • Rebinding existing deployment profiles by default.

Related

Parent Epic: #15184
Source: D#15173
Refs #13056
Refs #11003

Origin Session ID: 5b19219a-d8ad-4505-864f-19b5eab44a45

Retrieval Hint: #15184 mcpListenHost TransportService local-bearer absent Origin Host allowlist ADR 0019

tobiu referenced in commit 84e5091 - "feat(mcp): add loopback local bearer ingress (#15185) (#15214)" on 9:55 AM
tobiu closed this issue on 9:55 AM