Skip to content

Stop dotenv printing its banner onto the MCP stdout transport - #20

Merged
effecet merged 1 commit into
mainfrom
fix/dotenv-stdout-transport
Aug 18, 2026
Merged

effecet merged 1 commit into
mainfrom
fix/dotenv-stdout-transport

Conversation

@effecet

@effecet effecet commented Aug 18, 2026

Copy link
Copy Markdown
Owner

The bug

Every MCP server boot writes this as the first line of stdout, ahead of the initialize reply:

◇ injected env (N) from .env // tip: ⌘ suppress logs { quiet: true }

stdout is the JSON-RPC transport. dotenv has defaulted quiet to false since 17.0.0 and prints its injection banner via console.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() in src/db.ts and tests/integration/helpers.ts now passes quiet: true beside the existing override: 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.ts pins the two call sites to an identical shape, so they can't drift apart
  • env-loading.test.ts pins each property independently

⚠️ DOTENV_CONFIG_* environment variables take precedence over code options per dotenv's changelog, so setting DOTENV_CONFIG_QUIET=false anywhere would defeat this.

Deliberately not changed

src/import.ts and drizzle.config.ts keep the bare import 'dotenv/config' and still print the banner. 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.

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:

tree stdout lines first line
unfixed 1 the banner
fixed 0 —

Also: 30 dotenv-guard assertions pass, and tsc is clean on TypeScript 7.0.2.

The 7 embed test failures visible on a local run are pre-existing and environmental — bge-small isn'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.

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.
@effecet
effecet merged commit eec7508 into main Aug 18, 2026
6 checks passed
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