Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions tests/db-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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*\}\)/,
);
});

Expand All @@ -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*\}\)/,
);
});
});
9 changes: 9 additions & 0 deletions tests/env-loading.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-'));
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const { Pool } = pg;
// DATABASE_URL=<localhost> DOTENV_CONFIG_PATH=<managed> 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
Expand Down