diff --git a/src/db.ts b/src/db.ts index 48acd86..c2f522f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -8,7 +8,14 @@ import * as dotenv from 'dotenv'; // imports) already called `import 'dotenv/config'`, DATABASE_URL from ./.env // would otherwise shadow the explicit path here, silently routing all writes // to local Docker. Observed 2026-04-07 → 2026-04-13 (6 days of events drift). -dotenv.config({ path: process.env.DOTENV_CONFIG_PATH, override: true }); +// +// `quiet: true` is equally load-bearing: dotenv defaults it to FALSE since +// 17.0.0 and prints its injection banner via console.log — onto stdout, which +// for the MCP server is the JSON-RPC transport itself. The banner lands ahead +// of the initialize reply; clients that skip unparseable lines tolerate it, +// clients that don't see a corrupt stream and register zero tools with no +// error to read. +dotenv.config({ path: process.env.DOTENV_CONFIG_PATH, override: true, quiet: true }); import { drizzle } from 'drizzle-orm/node-postgres'; import pg from 'pg'; import * as schema from './schema.js'; diff --git a/tests/db-guard.test.ts b/tests/db-guard.test.ts index 8605a0b..0ee9401 100644 --- a/tests/db-guard.test.ts +++ b/tests/db-guard.test.ts @@ -194,7 +194,7 @@ describe('helpers.ts resolves DATABASE_URL the same way src/db.ts does', () => { it('helpers.ts overrides from DOTENV_CONFIG_PATH, like db.ts', () => { expect(read('./integration/helpers.ts')).toMatch( - /dotenv\.config\(\{\s*path:\s*process\.env\.DOTENV_CONFIG_PATH,\s*override:\s*true\s*\}\)/, + /dotenv\.config\(\{\s*path:\s*process\.env\.DOTENV_CONFIG_PATH,\s*override:\s*true,\s*quiet:\s*true\s*\}\)/, ); }); @@ -209,7 +209,7 @@ describe('helpers.ts resolves DATABASE_URL the same way src/db.ts does', () => { // If db.ts ever changes its resolution, this pairing is stale and the two // pools can diverge again — fail here rather than against a live database. expect(read('../src/db.ts')).toMatch( - /dotenv\.config\(\{\s*path:\s*process\.env\.DOTENV_CONFIG_PATH,\s*override:\s*true\s*\}\)/, + /dotenv\.config\(\{\s*path:\s*process\.env\.DOTENV_CONFIG_PATH,\s*override:\s*true,\s*quiet:\s*true\s*\}\)/, ); }); }); diff --git a/tests/env-loading.test.ts b/tests/env-loading.test.ts index d7214a3..566290b 100644 --- a/tests/env-loading.test.ts +++ b/tests/env-loading.test.ts @@ -37,6 +37,15 @@ describe('env loading invariants', () => { expect(src).toMatch(/dotenv\.config\([^)]*override:\s*true/); }); + it('db.ts must pass { quiet: true } — stdout is the MCP transport', () => { + // dotenv defaults quiet:false since 17.0.0 and logs its injection banner + // with console.log — i.e. onto stdout, which for the MCP server IS the + // JSON-RPC transport. A client that does not skip unparseable lines sees + // a corrupt stream and registers no tools, with no error to read. + const src = readFileSync(join(repoRoot, 'src/db.ts'), 'utf8'); + expect(src).toMatch(/dotenv\.config\([^)]*quiet:\s*true/); + }); + it('dotenv { override: true } overrides a pre-set env var', () => { // Guard against dotenv upgrade silently changing override semantics. const tmp = mkdtempSync(join(tmpdir(), 'env-test-')); diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index a2ba7fc..a0465b8 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -23,7 +23,7 @@ const { Pool } = pg; // DATABASE_URL= DOTENV_CONFIG_PATH= vitest // would satisfy the guard and then write to the managed database anyway. // tests/db-guard.test.ts pins this pairing at the source level. -dotenv.config({ path: process.env.DOTENV_CONFIG_PATH, override: true }); +dotenv.config({ path: process.env.DOTENV_CONFIG_PATH, override: true, quiet: true }); // Guard against running this suite against a shared managed database. // Enforced at module load, before the pool is constructed, so it protects