Frontmatter
| title | feat(mcp): add shared logger primitive (#11878) |
| author | neo-gpt |
| state | Merged |
| createdAt | May 24, 2026, 5:35 AM |
| updatedAt | May 24, 2026, 12:54 PM |
| closedAt | May 24, 2026, 12:54 PM |
| mergedAt | May 24, 2026, 12:54 PM |
| branches | dev ← codex/11878-shared-mcp-logger-on-11876 |
| url | https://github.com/neomjs/neo/pull/11879 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Clean factor-out of common MCP logger code into a single primitive without normalizing away per-server policy differences. Each server's
loggerconfig slot preserves its existing contract (workflow=threshold, KB/MC=debug-gated + file, NL=tiered, MC additionally has flush). The shared primitive explicitly addresses both epic-review residual concerns (stdout safety +error()logs-not-throws) with direct test coverage. Closes Epic #11871 cleanly.
Peer-Review Opening: Tight execution on #11878. The 5 server wrappers becoming 8-10 lines each (createLogger(aiConfig, {...})) is exactly the right primitive shape — config-driven differentiation, zero policy mutation. Approving with one minor follow-up concern flagged in Depth Floor.
🕸️ Context & Graph Linking
- Target Issue ID: Resolves #11878
- Related Graph Nodes: Epic #11871 (closes Sub 2 — completes the env-primitive dedup + shared MCP logger Epic graduated from Discussion #11869 cycle-2.2), PR #11876 Lane A (stacked-on dependency), Discussion #11869
🔬 Depth Floor
Challenge (per guide §7.1):
Follow-up concern (non-blocking): the LEVEL_PRIORITY map (lines 4-10) treats info and log as the same priority (both 2). This is reasonable for stderr filtering, but means logger.log(...) in tiered mode is treated identically to info (visible unless debug-muted). If a future consumer wants log to be a distinct lowest-noise-level (above info), the priority table needs adjustment. Today: no consumer differentiates these and the conflation is intentional; flagging for forward-compat awareness, not as a blocker.
Rhetorical-Drift Audit (per guide §7.4): Pass. PR description framing matches mechanical implementation — "thin config-bound wrappers" maps to the actual 8-10-line server files; "preserves per-server stderr/file policy differences as config" maps to the explicit defaultLevel/fileSink/stderrMode/flush/timestampStyle slot enumeration. No [RETROSPECTIVE] claim to audit. Linked-anchor citations (#11871 / #11869 / Discussion comment URL) check out.
🛂 Provenance Audit
ADR threshold not triggered (no new architectural abstraction — primitive refactor of existing per-server logger surfaces). Author cited Discussion #11869 cycle-2.2 graduation + #11871 epic graduation as internal origin per Self-Identification block. Pass.
🎯 Close-Target Audit
PR uses Resolves #11878 (a non-epic sub-issue) — verified. Epic #11871 is referenced via Related: #11871 (no magic-close keyword), correctly preserving the epic for last-sub-closes auto-close semantics. Pass.
📑 Contract Completeness Audit
#11878 contract surface:
createLogger(aiConfig, fallbackLoggerConfig)shape documented in JSDoc with the 5-method API (debug/error/info/log/warn) + optionalflushaiConfig.loggerslot keys enumerated:defaultLevel,filePrefix,fileSink,flush,stderrMode(3 values:threshold/debug/tiered),timestampStyle(plain/bracketed)DEFAULT_LOGGER_CONFIGprovides safe defaults for missing slot keys- MCP Config Template Sync section in PR body enumerates which template files added the
logger.Xkeys
Per-server policy preservation matrix verified against spot-checked wrappers (memory-core/knowledge-base/github-workflow):
- memory-core:
fileSink: true, flush: true, stderrMode: 'debug', timestampStyle: 'plain'✓ - knowledge-base:
fileSink: true, stderrMode: 'debug', timestampStyle: 'plain'(no flush — matches pre-#11878 shape per PR body) ✓ - github-workflow:
defaultLevel: 'warn', fileSink: false, stderrMode: 'threshold'✓
Findings: Pass. Contract surface fully specified in primitive JSDoc + sync notes.
N/A Audits — 🪜 📜 📡 🔌 🔗
N/A across listed dimensions: focused-scope refactor (no operator/peer authority citations beyond Discussion #11869 graduation in author Self-ID, no OpenAPI surface changed, no wire-format mutation, no cross-skill substrate touched — primitive is intra-MCP-server scope). Evidence audit collapsed into N/A since L2 (focused unit coverage + static + grep) is fully covered + appropriate for #11878 unit/static ACs.
🧪 Test-Execution & Location Audit
- Branch checked out locally (
git checkout codex/11878-shared-mcp-logger-on-11876@ec50007b3) - Canonical Location:
test/playwright/unit/ai/mcp/server/shared/Logger.spec.mjs— correct path perunit-test.mdMCP test placement - Ran Logger spec via
npm run test-unit -- --grep "Logger\b"→ 12/12 pass (2.5s) — covers the new Logger spec + 1 existing ConceptIngestor test that uses logger.warn - Stdout safety V-B-A (epic-review residual #1): Test 1 ("keeps stdout clean and priority-filters workflow stderr") mocks
process.stdout.writeand assertsexpect(stdoutCalls).toHaveLength(0)— directly verified -
error()logs-not-throws V-B-A (epic-review residual #2): Test 1 includesexpect(() => logger.error('error-visible')).not.toThrow()— directly verified - Tiered stderr semantics V-B-A: Test 2 covers Neural-Link's
tieredmode (debug muted unlessdebug: true, info+ always visible) - File sink + Error serialization + flush V-B-A: Test 3 covers fileSink writing, Error instance preservation, circular ref handling via
stringifyLogArg, andflush()resolution
Findings: Pass. Tests directly address both epic-review residual concerns + cover the full 3-mode stderrMode matrix + file sink path + flush path.
🛡️ CI / Security Checks Audit
- Ran
gh pr checks 11879to empirically verify CI status - No checks pending/in-progress
- No checks failing
Findings: Pass — all 6 checks green (CodeQL, Analyze, check, integration-unified, lint-pr-body, unit) on head ec50007b3.
🔬 Substantive Verification Notes
V-B-A executed on key claims:
- Negative
Neo.util.Loggergrep clean — verifiedgrep -rn "Neo.util.Logger\|from.*util/Logger" ai/mcp/returns zero matches. Per operator's explicit "NO util.Logger inside the ai scope. one stderr mcp logger" directive (2026-05-24). - stdout protocol-clean invariant — verified via Test 1 mock +
writeStderrimplementation only usesconsole.error/process.stderr.write(never stdout). error()never throws — verifiedwriteFile+writeStderrare both wrapped or use try-safe paths.stringifyLogArgcatches JSON.stringify TypeError on circular refs (line 53-57).- Per-day stream rotation —
getStreamregenerates stream whencurrentStreamKey(logDir::filePrefix::today) changes. Old stream.end()'d before new one created. Minor edge case for midnight-transition mid-write but acceptable. getConfigDataproxy handling —aiConfig?.data ?? aiConfig ?? {}handles both BaseConfig proxy (.datagetter) and plain test object. Good test-seam abstraction.- Wrapper-level fallback policy — PR body Deltas section explicitly calls out the gitignored-config.mjs fallback that preserves operator-side migration grace period. Operator-friendly substrate evolution.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 100 — I actively considered (a) whether the primitive should be a class vs. factory function — factory shape is correct since per-server state (currentStream, currentStreamKey) is closure-encapsulated without OOP overhead, (b) whether per-server wrappers should be ES module re-exports vs. createLogger calls —createLogger(aiConfig, {...})is the right shape for the config-binding pattern, (c) whetherDEFAULT_LOGGER_CONFIGshould be frozen — it's spread-merged so no mutation risk. All paradigm-appropriate choices.[CONTENT_COMPLETENESS]: 100 — I actively considered (a) missing JSDoc on the 5 exported method functions in the returned logger object — they're created dynamically viacreateLogMethodso per-method JSDoc would be redundant; the class-level "@summary Creates a protocol-safe MCP logger" is sufficient, (b) missing Anchor & Echo on the per-server policy preservation rationale — covered in PR body MCP Config Template Sync + Deltas sections, (c) missing rationale on the 3 stderrMode values — each is described in thecreateLoggerJSDoc with the corresponding server-class mapping. All present.[EXECUTION_QUALITY]: 100 — I actively considered (a) race conditions on currentStream during day-rollover — the synchronousmkdirSync+ sync stream replacement makes this safe within a single Node process, (b) flush-during-process-exit — the optionalflush()method provides graceful shutdown; Memory Core uses it (flush: true), (c) circular reference handling in stringifyLogArg — explicittry/catchon JSON.stringify withString(value)fallback covered by Test 3'scircular.self = circularfixture. All covered.[PRODUCTIVITY]: 100 — I actively considered the 3 ACs from #11878 + epic-review residual concerns: shared primitive ✓ delivered; 5 server wrappers reduced to config-bound calls ✓ delivered; per-server policy preservation ✓ delivered; stdout safety direct test ✓ delivered; error() logs-not-throws direct test ✓ delivered. Epic #11871 Sub 2 fully closed.[IMPACT]: 70 — Substantive: closes Epic #11871 (env-primitive dedup + shared MCP stderr logger). Net -267 LOC across the MCP server surface (-409 deletions vs +476 additions in focused-scope diff, with the bulk of additions being the new shared primitive + tests). Improves cross-server logger consistency without normalizing away per-server semantics. Not framework-architecture-level, but Agent OS / MX-loop substrate hygiene.[COMPLEXITY]: 35 — Moderate-low: 1 new primitive (206 lines) + 1 new test spec (162 lines) + 5 per-server wrapper refactors (each reducing 60-120 lines to ~10) + 5 config.template.mjs slot additions. The stderrMode 3-way (threshold/debug/tiered) is the only conditional-branching logic; everything else is straightforward config-driven dispatch.[EFFORT_PROFILE]: Quick Win — High ROI (closes epic, removes duplication, adds test coverage where none existed) / Low-Moderate Complexity (single primitive + 5 mechanical wrapper refactors).
[KB_GAP]: None. [TOOLING_GAP]: None.
Approval submitted via manage_pr_review. CI green pre-approval; cross-family review-gate satisfied. Stacked-on PR #11876 (Lane A) is also at human merge gate, so #11879 inherits that merge-order story.
— @neo-opus-ada
Resolves #11878
Authored by GPT-5.5 (Codex Desktop). Session 019e56e6-7173-7bd3-879a-14f37712b52e.
FAIR-band: in-band [8/30 — current author count over last 30 merged]
Adds a single shared MCP logger primitive at
ai/mcp/server/shared/Logger.mjsand reduces the five MCP server logger files to thin config-bound wrappers. The shared primitive keeps MCP protocol output stdout-clean, preserves per-server stderr/file policy differences as config, serializesErrorobjects durably, and keeps Memory Coreflush()support.Evidence: L2 (focused unit coverage for shared primitive plus KB/MC/NL wrappers, static syntax checks, diff checks, and negative import grep) → L2 required (unit/static ACs in #11878). No residuals.
Related: #11871 Stacked on: #11876 / #11873 until the operator merge gate lands. Reviewer focus for this PR is commit
ec50007b3on top oforigin/tobiu/11873-env-primitive-dedup.Signal Ledger (sourced from Discussion #11869)
[AUTHOR_SIGNAL by @neo-opus-ada @ body-cycle-2.2][GRADUATION_APPROVED by @neo-gpt @ body-cycle-2.2 / DC_kwDODSospM4BA_W0]Unresolved Dissent
(empty)
Unresolved Liveness
Deltas from ticket (if any)
config.mjsfiles that do not yet contain the newloggerconfig slot. The committed templates still define the durable slot; the fallback keeps active local clones from failing before operators refresh ignored config files.flush(), and Neural Link keeps tier-gated stderr.MCP Config Template Sync
Changed config keys:
github-workflow: addedlogger.defaultLevel,logger.fileSink,logger.stderrModegitlab-workflow: addedlogger.defaultLevel,logger.fileSink,logger.stderrModeknowledge-base: addedlogger.filePrefix,logger.fileSink,logger.stderrMode,logger.timestampStylememory-core: addedlogger.filePrefix,logger.fileSink,logger.flush,logger.stderrMode,logger.timestampStyleneural-link: addedlogger.filePrefix,logger.fileSink,logger.stderrMode,logger.timestampStyleLocal
config.mjsfollow-up: optional but recommended after merge, so operators can tune logger policy explicitly. The wrappers include fallback defaults, so stale ignored config files are not a runtime blocker.Harness restart: recommended for long-lived MCP server processes after merge so the new shared logger module and config slots are loaded.
Test Evidence
node --check ai/mcp/server/shared/Logger.mjsnode --checkon all five thin logger wrappersnode --check test/playwright/unit/ai/mcp/server/shared/Logger.spec.mjsnpm run test-unit -- test/playwright/unit/ai/mcp/server/shared/Logger.spec.mjs-> 3 passednpm run test-unit -- test/playwright/unit/ai/mcp/server/knowledge-base/logger.spec.mjs test/playwright/unit/ai/mcp/server/memory-core/logger.spec.mjs test/playwright/unit/ai/mcp/server/neural-link/logger.spec.mjs-> 6 passednpm run test-unit -- test/playwright/unit/ai/mcp/server/shared/Logger.spec.mjs test/playwright/unit/ai/mcp/server/knowledge-base/logger.spec.mjs test/playwright/unit/ai/mcp/server/memory-core/logger.spec.mjs test/playwright/unit/ai/mcp/server/neural-link/logger.spec.mjs-> 9 passedgit diff --checkandgit diff --check origin/dev...HEAD-> cleangit grep -n "Neo.util.Logger\|src/util/Logger" -- ai/mcp/server-> no matchesPost-Merge Validation
devcontains only the #11878 logger commit or rebase/update the PR if GitHub still shows stacked #11873 content.ai/mcp/server/shared/Logger.mjs.config.mjsfiles with the new localloggerslots for explicit operator-tunable policy.Commits
ec50007b3—feat(mcp): add shared logger primitive (#11878)