Stop dotenv printing its banner onto the MCP stdout transport - #20
Merged
Merged
Conversation
dotenv has defaulted quiet to FALSE since 17.0.0 and prints its injection
banner with console.log — onto stdout, which for the MCP server is the
JSON-RPC transport. Every server boot has emitted
◇ injected env (N) from .env // tip: ⌘ suppress logs { quiet: true }
ahead of the initialize reply. Clients that skip unparseable lines tolerate
it, which is why this stayed invisible; a stricter client sees a corrupt
stream and registers zero tools with no error to read. It also contradicts
the project's own convention: stderr only, stdout is the transport.
dotenv.config() in src/db.ts and tests/integration/helpers.ts now passes
quiet: true beside override: true. Both guards were updated first and watched
fail on their real assertions (3 failures, not import errors) before the fix:
db-guard.test.ts pins the two call sites to an identical shape so they cannot
drift apart, env-loading.test.ts pins each property independently.
src/import.ts and drizzle.config.ts keep bare `import 'dotenv/config'` and
still print the banner — deliberate. Both are CLI entry points where stdout is
not a transport, and rewriting import.ts risks the env-shadowing trap that
src/db.ts's comment documents.
Verified by a controlled stdio comparison. Both runs fail identically at the
DB check (no .env present), so the banner is the only variable: unfixed, the
server writes 1 line to stdout and it is the banner; fixed, it writes 0. The
30 dotenv-guard assertions pass, and tsc is clean on TypeScript 7.0.2.
The 7 embed test failures seen locally are pre-existing and environmental —
the bge-small model is not cached in a fresh clone; CI pre-caches it.
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.
The bug
Every MCP server boot writes this as the first line of stdout, ahead of the
initializereply:stdout is the JSON-RPC transport. dotenv has defaulted
quietto false since 17.0.0 and prints its injection banner viaconsole.log. Clients that skip unparseable lines tolerate it — which is the only reason this has gone unnoticed. A stricter client sees a corrupt stream and registers zero tools, with no error to read, which is about the worst failure mode this stack has.It also contradicts the project's own convention: stderr only, stdout is the transport.
The fix
dotenv.config()insrc/db.tsandtests/integration/helpers.tsnow passesquiet: truebeside the existingoverride: true.Both guard suites were updated first and watched fail on their real assertions — 3 failures, not import errors — before the fix landed:
db-guard.test.tspins the two call sites to an identical shape, so they can't drift apartenv-loading.test.tspins each property independentlyDOTENV_CONFIG_*environment variables take precedence over code options per dotenv's changelog, so settingDOTENV_CONFIG_QUIET=falseanywhere would defeat this.Deliberately not changed
src/import.tsanddrizzle.config.tskeep the bareimport 'dotenv/config'and still print the banner. Both are CLI entry points where stdout is not a transport, and rewritingimport.tsrisks the env-shadowing trap thatsrc/db.ts's comment documents.Verification
A controlled stdio comparison. A fresh clone has no
.env, so the server fails at the DB check in both runs — identical failure path, banner as the only variable:Also: 30 dotenv-guard assertions pass, and
tscis clean on TypeScript 7.0.2.The 7
embedtest failures visible on a local run are pre-existing and environmental —bge-smallisn't cached in a fresh clone (local_files_only=true), and CI pre-caches it in a dedicated step. Confirmed identical on an unmodified tree.