Contributes to #12456 (batch 3 — MCP server-shared layer env-read census + the clear config-owned fix).
Context
Batch 3 of the ADR-0019 cleanup sweeps the MCP server-shared layer for raw process.env reads: BaseServer.mjs, shared/services/TransportService.mjs, shared/services/DestructiveOperationGuard.mjs, gitlab-workflow/Server.mjs, knowledge-base/Server.mjs + toolService.mjs (remaining reads), and neural-link/Bridge.mjs. Every surveyed read is classified in the census table below; the single CLEAR config-owned violation is fixed on the owning config.
The fix (A1 — module/inline env re-derivation with a hidden default): TransportService.setup() derived the advertised SSE/HTTP transport host as process.env.HOST || 'localhost' at the use site — a raw env read with a shadow default, sitting right next to config-owned aiConfig.publicUrl / aiConfig.mcpHttpPort reads. The deployment host is config, so it now lives as a mcpHttpHost: leaf('localhost', 'HOST', 'string') on the root ai/config.template.mjs transport cluster (between allowedHosts and mcpHttpPort), and TransportService consumes aiConfig.mcpHttpHost at the use site. The existing HOST env binding stays bound through the leaf — no operator-visible env-var rename. Placement on the root (not duplicated per-server) is the sanctioned realm-chain shape: per-server configs resolve mcpHttpHost override-else-inherit via getParent().
Exact-semantics preservation: Neo.util.Env.parseString returns undefined for absent and empty-string env values (Neo.isEmpty guard), so the leaf reproduces the || fallback exactly, including the HOST='' edge. Env re-resolution is bounded (construct / setEnvOverride / load / refreshEnv) per the documented ConfigProvider contract — for a server that reads the value once at boot, behavior is identical.
Spec-shape shift: the three mcpServerUrl resolution tests in TransportService.spec.mjs previously mutated process.env.HOST; they now model the resolved config contract (mcpHttpHost in the mock aiConfig), and the env binding itself is covered by a new root config.template.spec.mjs test (setEnvOverride('HOST', ...) resolving through the leaf on a fresh provider instance). Net: zero process.env mutation left in the transport spec.
Gitignored overlays (ai/config.mjs) were re-synced to the updated template in both the worktree and the operator checkout, so live servers pick up the leaf on next restart.
Evidence: runtime-verified — realm-chain resolution probed on live configs (root/kb/memory-core all resolve mcpHttpHost, and HOST=internal-host resolves through the root leaf from a child config read), plus 402-spec unit run (see Test Evidence).
Deltas from ticket
- The batch prompt listed
knowledge-base/toolService.mjs:23 (NEO_AI_MCP_KB_OPENAPI_PATH) among census targets; it is already converted by open batch-1 PR #14935, so it is census-rowed here but deliberately untouched to avoid a cross-batch conflict.
- The
npm_package_version duplicates in gitlab-workflow/Server.mjs / knowledge-base/Server.mjs are census-deferred rather than fixed: the identical shape exists in three sibling servers outside this batch's file list (github-workflow, file-system, memory-core), and removing only the in-scope two would create sibling drift. See census rows 3–4 for the recommended one-shot fold-out.
DestructiveOperationGuard's UNIT_TEST_MODE read is NOT mapped onto the useTestDatabase leaf pattern — deliberate skip with rationale (census row 10): the guard is fail-closed defense-in-depth against config-resolution mistakes; routing its bypass through config would reintroduce the exact trust-the-config dependency it exists to defend against.
Census — every surveyed env read in the batch-3 file set
| # |
File:Line |
Env var(s) |
Classification |
Disposition |
| 1 |
ai/mcp/server/shared/services/TransportService.mjs:160 |
HOST |
CONFIG-OWNED (A1) — deployment host re-derived inline with hidden 'localhost' default |
FIXED — mcpHttpHost leaf on root ai/config.template.mjs; consumed at use site |
| 2 |
ai/mcp/server/BaseServer.mjs:301 |
npm_package_version |
Runtime process metadata — npm-injected package version, not a deployment value |
Keep — the single canonical fallback site (metadata.version || env || '1.0.0') |
| 3 |
ai/mcp/server/gitlab-workflow/Server.mjs:37 |
npm_package_version |
Redundant duplicate — the expression is always truthy, so BaseServer:301's identical fallback never fires for this server |
DEFERRED — fold out all five per-server duplicates in one pass (also github-workflow/Server.mjs:38, file-system/Server.mjs:46, memory-core/Server.mjs:84); partial removal here would create sibling drift |
| 4 |
ai/mcp/server/knowledge-base/Server.mjs:40 |
npm_package_version |
Redundant duplicate (same as row 3) |
DEFERRED — same fold-out |
| 5 |
ai/mcp/server/knowledge-base/Server.mjs:106 |
NEO_AGENT_ID, USER |
Runtime-identity contract — per-process identity injected by the spawner |
Never config — untouched |
| 6 |
ai/mcp/server/knowledge-base/Server.mjs:110 |
NEO_SESSION_ID |
Runtime-identity contract |
Never config — untouched |
| 7 |
ai/mcp/server/knowledge-base/toolService.mjs:23 |
NEO_AI_MCP_KB_OPENAPI_PATH |
Config-owned (file path + test-isolation seam) |
FIXED IN BATCH-1 (open PR #14935 — openApiPath leaf on the kb server config); untouched here to avoid cross-batch conflict |
| 8 |
ai/mcp/server/knowledge-base/toolService.mjs:94 |
NEO_AGENT_ID, USER |
Runtime-identity contract |
Never config — untouched |
| 9 |
ai/mcp/server/knowledge-base/toolService.mjs:95 |
NEO_SESSION_ID |
Runtime-identity contract |
Never config — untouched |
| 10 |
ai/mcp/server/shared/services/DestructiveOperationGuard.mjs:113 |
UNIT_TEST_MODE |
Deliberate ambient safety check — the canonical-collection delete guard is fail-closed defense-in-depth against config-resolution mistakes (its own contract hardcodes canonical names for exactly this reason); a config leaf would reintroduce the trust-the-config dependency the guard defends against |
Skip — intentional; not the useTestDatabase storage-selection case |
| 11 |
ai/mcp/server/shared/services/DestructiveOperationGuard.mjs:236 |
env = process.env DI default (gates NEO_ALLOW_PRODUCTION_DESTRUCTIVE_AI_SUBSTRATE in #isProductionConfirmed) |
Operator-confirmation contract — dual-gate (env + explicit token) for deliberate production maintenance, injectable-for-tests by design |
Skip — intentional (lines 78/199/225 are doc-comment mentions of the same contract, not code reads) |
| 12 |
ai/mcp/server/neural-link/Bridge.mjs:141 |
NEO_FLEET_BRIDGE_PUBLIC_KEY |
Credential injection — per-process key material injected by the fleet spawner |
Never config — untouched |
Totals: 12 code-level env-read sites surveyed → 1 fixed here · 1 fixed in batch-1 · 5 runtime-identity/credential · 1 runtime process metadata (canonical) · 2 deferred (duplicate fold-out) · 2 deliberate safety-guard contracts.
Test Evidence
npm run test-unit -- test/playwright/unit/ai/mcp/ test/playwright/unit/ai/config.template.spec.mjs test/playwright/unit/ai/scripts/lint/lintConfigTemplateSsot.spec.mjs --workers=1 → 400 passed / 402 on the merged origin/dev base (4d4b20dca), reproducible across two runs.
- Control run of the identical suite on detached clean
origin/dev (no batch-3 diff): 399 passed / 401 with the exact same 2 failures — both are pre-existing in this environment, independent of this diff (the +1 test on the branch is the new mcpHttpHost leaf-binding test, green).
BootEnvelopeResolver.spec.mjs:51 ("empty-string env vars are treated as absent (default instance)") — pre-existing, environment-specific (also fails on a clean stashed tree in isolation).
memory-core/Server.spec.mjs:383 (#14388 separate-graph-process identity) — order-dependent in the full run; passes 17/17 when the spec file runs in isolation; outside this diff's surface.
- Touched-surface subset (shared services + root config template + SSOT lint + kb/memory-core config templates): 179/180, same single pre-existing BootEnvelopeResolver failure.
- SSOT lint (
lint-config-template-ssot) green: the new leaf default is a literal (no inline env), and no baseline row referenced TransportService/HOST, so no baseline churn.
- Runtime probe (Neo-bootstrapped node): root
mcpHttpHost === 'localhost'; kb + memory-core configs resolve 'localhost' via the realm chain; HOST=internal-host resolves 'internal-host' from a child-config read.
node --check green on all four touched files; whitespace / shorthand / jsdoc-types / ticket-archaeology / block-alignment / aiconfig-test-mutation gates green (lint-staged at commit).
Post-Merge Validation
- Restart any SSE-mode MCP server (
NEO_TRANSPORT=sse) and confirm the advertised mcpServerUrl is unchanged for the deployment (HOST env still wins over the localhost default; publicUrl still wins over both).
- Operators with a customized
ai/config.mjs overlay: re-sync the overlay from the template (or add the mcpHttpHost leaf) — the shipped worktree/checkout overlays were already re-synced; a missing leaf on a stale overlay resolves to undefined and fails loud at SSE boot, per the no-defensive-fallback contract.
- Batches 4+ can consume census rows 3–4 (npm version duplicate fold-out, five siblings in one pass).
Commits
- 38fcee8d0
refactor(mcp): ADR-0019 batch-3 — server-shared env reads onto owning config leaves + census (#12456)
Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.
Contributes to #12456 (batch 3 — MCP server-shared layer env-read census + the clear config-owned fix).
Context
Batch 3 of the ADR-0019 cleanup sweeps the MCP server-shared layer for raw
process.envreads:BaseServer.mjs,shared/services/TransportService.mjs,shared/services/DestructiveOperationGuard.mjs,gitlab-workflow/Server.mjs,knowledge-base/Server.mjs+toolService.mjs(remaining reads), andneural-link/Bridge.mjs. Every surveyed read is classified in the census table below; the single CLEAR config-owned violation is fixed on the owning config.The fix (A1 — module/inline env re-derivation with a hidden default):
TransportService.setup()derived the advertised SSE/HTTP transport host asprocess.env.HOST || 'localhost'at the use site — a raw env read with a shadow default, sitting right next to config-ownedaiConfig.publicUrl/aiConfig.mcpHttpPortreads. The deployment host is config, so it now lives as amcpHttpHost: leaf('localhost', 'HOST', 'string')on the rootai/config.template.mjstransport cluster (betweenallowedHostsandmcpHttpPort), and TransportService consumesaiConfig.mcpHttpHostat the use site. The existingHOSTenv binding stays bound through the leaf — no operator-visible env-var rename. Placement on the root (not duplicated per-server) is the sanctioned realm-chain shape: per-server configs resolvemcpHttpHostoverride-else-inherit viagetParent().Exact-semantics preservation:
Neo.util.Env.parseStringreturnsundefinedfor absent and empty-string env values (Neo.isEmptyguard), so the leaf reproduces the||fallback exactly, including theHOST=''edge. Env re-resolution is bounded (construct /setEnvOverride/load/refreshEnv) per the documented ConfigProvider contract — for a server that reads the value once at boot, behavior is identical.Spec-shape shift: the three
mcpServerUrlresolution tests inTransportService.spec.mjspreviously mutatedprocess.env.HOST; they now model the resolved config contract (mcpHttpHostin the mockaiConfig), and the env binding itself is covered by a new rootconfig.template.spec.mjstest (setEnvOverride('HOST', ...)resolving through the leaf on a fresh provider instance). Net: zeroprocess.envmutation left in the transport spec.Gitignored overlays (
ai/config.mjs) were re-synced to the updated template in both the worktree and the operator checkout, so live servers pick up the leaf on next restart.Evidence: runtime-verified — realm-chain resolution probed on live configs (root/kb/memory-core all resolve
mcpHttpHost, andHOST=internal-hostresolves through the root leaf from a child config read), plus 402-spec unit run (see Test Evidence).Deltas from ticket
knowledge-base/toolService.mjs:23(NEO_AI_MCP_KB_OPENAPI_PATH) among census targets; it is already converted by open batch-1 PR #14935, so it is census-rowed here but deliberately untouched to avoid a cross-batch conflict.npm_package_versionduplicates ingitlab-workflow/Server.mjs/knowledge-base/Server.mjsare census-deferred rather than fixed: the identical shape exists in three sibling servers outside this batch's file list (github-workflow,file-system,memory-core), and removing only the in-scope two would create sibling drift. See census rows 3–4 for the recommended one-shot fold-out.DestructiveOperationGuard'sUNIT_TEST_MODEread is NOT mapped onto theuseTestDatabaseleaf pattern — deliberate skip with rationale (census row 10): the guard is fail-closed defense-in-depth against config-resolution mistakes; routing its bypass through config would reintroduce the exact trust-the-config dependency it exists to defend against.Census — every surveyed env read in the batch-3 file set
ai/mcp/server/shared/services/TransportService.mjs:160HOST'localhost'defaultmcpHttpHostleaf on rootai/config.template.mjs; consumed at use siteai/mcp/server/BaseServer.mjs:301npm_package_versionmetadata.version || env || '1.0.0')ai/mcp/server/gitlab-workflow/Server.mjs:37npm_package_versiongithub-workflow/Server.mjs:38,file-system/Server.mjs:46,memory-core/Server.mjs:84); partial removal here would create sibling driftai/mcp/server/knowledge-base/Server.mjs:40npm_package_versionai/mcp/server/knowledge-base/Server.mjs:106NEO_AGENT_ID,USERai/mcp/server/knowledge-base/Server.mjs:110NEO_SESSION_IDai/mcp/server/knowledge-base/toolService.mjs:23NEO_AI_MCP_KB_OPENAPI_PATHopenApiPathleaf on the kb server config); untouched here to avoid cross-batch conflictai/mcp/server/knowledge-base/toolService.mjs:94NEO_AGENT_ID,USERai/mcp/server/knowledge-base/toolService.mjs:95NEO_SESSION_IDai/mcp/server/shared/services/DestructiveOperationGuard.mjs:113UNIT_TEST_MODEuseTestDatabasestorage-selection caseai/mcp/server/shared/services/DestructiveOperationGuard.mjs:236env = process.envDI default (gatesNEO_ALLOW_PRODUCTION_DESTRUCTIVE_AI_SUBSTRATEin#isProductionConfirmed)ai/mcp/server/neural-link/Bridge.mjs:141NEO_FLEET_BRIDGE_PUBLIC_KEYTotals: 12 code-level env-read sites surveyed → 1 fixed here · 1 fixed in batch-1 · 5 runtime-identity/credential · 1 runtime process metadata (canonical) · 2 deferred (duplicate fold-out) · 2 deliberate safety-guard contracts.
Test Evidence
npm run test-unit -- test/playwright/unit/ai/mcp/ test/playwright/unit/ai/config.template.spec.mjs test/playwright/unit/ai/scripts/lint/lintConfigTemplateSsot.spec.mjs --workers=1→ 400 passed / 402 on the mergedorigin/devbase (4d4b20dca), reproducible across two runs.origin/dev(no batch-3 diff): 399 passed / 401 with the exact same 2 failures — both are pre-existing in this environment, independent of this diff (the +1 test on the branch is the newmcpHttpHostleaf-binding test, green).BootEnvelopeResolver.spec.mjs:51("empty-string env vars are treated as absent (default instance)") — pre-existing, environment-specific (also fails on a clean stashed tree in isolation).memory-core/Server.spec.mjs:383(#14388 separate-graph-process identity) — order-dependent in the full run; passes 17/17 when the spec file runs in isolation; outside this diff's surface.lint-config-template-ssot) green: the new leaf default is a literal (no inline env), and no baseline row referencedTransportService/HOST, so no baseline churn.mcpHttpHost === 'localhost'; kb + memory-core configs resolve'localhost'via the realm chain;HOST=internal-hostresolves'internal-host'from a child-config read.node --checkgreen on all four touched files; whitespace / shorthand / jsdoc-types / ticket-archaeology / block-alignment / aiconfig-test-mutation gates green (lint-staged at commit).Post-Merge Validation
NEO_TRANSPORT=sse) and confirm the advertisedmcpServerUrlis unchanged for the deployment (HOSTenv still wins over thelocalhostdefault;publicUrlstill wins over both).ai/config.mjsoverlay: re-sync the overlay from the template (or add themcpHttpHostleaf) — the shipped worktree/checkout overlays were already re-synced; a missing leaf on a stale overlay resolves toundefinedand fails loud at SSE boot, per the no-defensive-fallback contract.Commits
refactor(mcp): ADR-0019 batch-3 — server-shared env reads onto owning config leaves + census (#12456)Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.