Skip to content

Commit c251b3f

Browse files
committed
fix(container-stdio): eliminate stdout pollution and restore container E2E tests
Stdio stability and diagnostics:\n- src/index.ts: set DEBUG_MCP_STDIO as early as possible, silence console in stdio, clean argv quoting\n- scripts/stdio-silencer.cjs: remove protocol gating; add pure mirroring of stdout to /app/logs/stdout-raw.log and stdin to /app/logs/stdin-raw.log; suppress process warnings\n- scripts/diagnose-stdio-client.mjs: add diagnostic SDK client to connect via Docker stdio and exercise basic tools\n- Dockerfile: include missing runtime dependency 'isexe' (required by 'which'); keep adapter/shared dist artifacts; preload silencer entry shim\nResults:\n- Container E2E tests now pass (2/2)\n- Full suite passes (1019/1019)\n- Dynamic adapter loading remains intact
1 parent 725fa91 commit c251b3f

22 files changed

Lines changed: 1055 additions & 59 deletions

‎COVERAGE_AUTO_DISPLAY.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Coverage Analysis - Auto-Display Setup
2+
3+
## What Changed
4+
5+
Now when you run `npm run test:coverage`, you'll automatically see a coverage analysis at the end showing which files to focus on for testing.
6+
7+
## Automatic Display (Compact)
8+
9+
After running:
10+
```bash
11+
npm run test:coverage
12+
```
13+
14+
You'll automatically see:
15+
```
16+
══════════════════════════════════════════════════════════════════════
17+
COVERAGE ANALYSIS - Files to focus on (sorted by impact):
18+
══════════════════════════════════════════════════════════════════════
19+
Overall: 83.6% | Focus on files with most uncovered lines
20+
──────────────────────────────────────────────────────────────────────
21+
Lines Cov% Impact File
22+
──────────────────────────────────────────────────────────────────────
23+
86 72% +2.7% src/server.ts
24+
62 81% +2.0% src/session/session-manager-operations.ts
25+
53 71% +1.7% src/proxy/dap-proxy-worker.ts
26+
... (top 10 files)
27+
──────────────────────────────────────────────────────────────────────
28+
Run "npm run test:coverage:analyze" for detailed analysis
29+
══════════════════════════════════════════════════════════════════════
30+
```
31+
32+
## Manual Commands (Detailed)
33+
34+
For detailed analysis:
35+
```bash
36+
# Just analyze existing coverage (detailed view)
37+
npm run test:coverage:analyze
38+
39+
# Generate coverage and analyze (detailed view)
40+
npm run coverage:analyze
41+
```
42+
43+
## Summary
44+
45+
- **Automatic**: Compact view after every `npm run test:coverage`
46+
- **Manual**: Detailed view with `npm run test:coverage:analyze`
47+
- **No memorization needed**: The analysis appears automatically!
48+
49+
The table shows files sorted by number of uncovered lines (not coverage %), so you know exactly where to focus your testing efforts for maximum impact.

‎Dockerfile‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,37 @@ COPY --from=builder /app/packages/shared/dist/ /app/packages/shared/dist/
6565
COPY --from=builder /app/packages/adapter-mock/dist/ /app/packages/adapter-mock/dist/
6666
COPY --from=builder /app/packages/adapter-python/dist/ /app/packages/adapter-python/dist/
6767

68+
# Create lightweight node_modules entries so adapters can resolve @debugmcp/* at runtime
69+
RUN mkdir -p /app/node_modules/@debugmcp/shared \
70+
&& mkdir -p /app/node_modules/@debugmcp/adapter-mock \
71+
&& mkdir -p /app/node_modules/@debugmcp/adapter-python
72+
73+
# Copy package.json and dist for workspace packages into node_modules for resolution
74+
COPY --from=builder /app/packages/shared/package.json /app/node_modules/@debugmcp/shared/package.json
75+
COPY --from=builder /app/packages/shared/dist/ /app/node_modules/@debugmcp/shared/dist/
76+
77+
COPY --from=builder /app/packages/adapter-mock/package.json /app/node_modules/@debugmcp/adapter-mock/package.json
78+
COPY --from=builder /app/packages/adapter-mock/dist/ /app/node_modules/@debugmcp/adapter-mock/dist/
79+
80+
COPY --from=builder /app/packages/adapter-python/package.json /app/node_modules/@debugmcp/adapter-python/package.json
81+
COPY --from=builder /app/packages/adapter-python/dist/ /app/node_modules/@debugmcp/adapter-python/dist/
82+
83+
# Provide adapter/runtime Node deps from builder's node_modules
84+
COPY --from=builder /app/node_modules/@vscode/debugprotocol /app/node_modules/@vscode/debugprotocol
85+
COPY --from=builder /app/node_modules/which /app/node_modules/which
86+
# which depends on isexe; include it to avoid MODULE_NOT_FOUND at runtime
87+
COPY --from=builder /app/node_modules/isexe /app/node_modules/isexe
88+
6889
# Expose ports
6990
EXPOSE 3000 5679
7091

71-
# Set the entrypoint to run the bundled application
72-
ENTRYPOINT ["node", "dist/bundle.cjs"]
92+
# Copy stdio silencer preloader into runtime image
93+
COPY --from=builder /app/scripts/stdio-silencer.cjs /app/scripts/stdio-silencer.cjs
94+
# Create an entrypoint wrapper that logs early startup context and preloads the silencer, then execs the server
95+
RUN printf '#!/bin/sh\nmkdir -p /app/logs\n{\n echo \"==== entry.sh ====\";\n date;\n echo \"argv: $*\";\n} >> /app/logs/entry.log 2>&1\nexec node --no-warnings -r /app/scripts/stdio-silencer.cjs dist/bundle.cjs \"$@\"\n' > /app/entry.sh && chmod +x /app/entry.sh
96+
97+
# Use the wrapper as entrypoint
98+
ENTRYPOINT ["/app/entry.sh"]
7399

74100
# Default command arguments
75101
CMD ["stdio"]

‎analyze-coverage-detailed.js‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Detailed Coverage Attribution Analysis for mcp-debugger
3+
* Run manually with: npm run test:coverage:analyze
4+
*/
5+
6+
import fs from 'fs';
7+
import path from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = path.dirname(__filename);
12+
13+
try {
14+
const summaryPath = path.join(__dirname, 'coverage', 'coverage-summary.json');
15+
16+
if (!fs.existsSync(summaryPath)) {
17+
console.log('No coverage data found. Run: npm run test:coverage');
18+
process.exit(1);
19+
}
20+
21+
const coverage = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
22+
const overall = coverage.total ? coverage.total.lines.pct : 0;
23+
24+
let totalUncovered = 0;
25+
let totalLines = 0;
26+
const files = [];
27+
28+
for (const [filePath, data] of Object.entries(coverage)) {
29+
if (filePath === 'total') continue;
30+
31+
const uncovered = data.lines.total - data.lines.covered;
32+
totalUncovered += uncovered;
33+
totalLines += data.lines.total;
34+
35+
// Extract relative path for cleaner display
36+
let cleanPath = filePath.replace(process.cwd(), '').replace(/^[/\\]/, '');
37+
cleanPath = cleanPath.replace(/\\/g, '/');
38+
39+
files.push({
40+
path: cleanPath,
41+
coverage: data.lines.pct,
42+
uncovered,
43+
total: data.lines.total
44+
});
45+
}
46+
47+
// Calculate impact on overall coverage
48+
files.forEach(f => {
49+
f.impact = totalLines > 0 ? (f.uncovered / totalLines) * 100 : 0;
50+
});
51+
52+
// Sort by uncovered lines (descending)
53+
files.sort((a, b) => b.uncovered - a.uncovered);
54+
55+
console.log('\nMCP-DEBUGGER DETAILED COVERAGE ANALYSIS');
56+
console.log('═'.repeat(80));
57+
console.log(`Overall Coverage: ${overall.toFixed(1)}%`);
58+
console.log(`Total Lines: ${totalLines.toLocaleString()}`);
59+
console.log(`Uncovered Lines: ${totalUncovered.toLocaleString()}`);
60+
console.log('═'.repeat(80));
61+
console.log('\nFiles sorted by number of uncovered lines (highest impact first):\n');
62+
console.log('Uncovered Coverage Impact File');
63+
console.log('─'.repeat(80));
64+
65+
// Show all files with uncovered lines
66+
files.forEach(f => {
67+
if (f.uncovered > 0) {
68+
console.log(
69+
f.uncovered.toString().padStart(9) +
70+
f.coverage.toFixed(1).padStart(9) + '%' +
71+
('+' + f.impact.toFixed(1) + '%').padStart(8) +
72+
' ' + f.path
73+
);
74+
}
75+
});
76+
77+
console.log('\n' + '─'.repeat(80));
78+
console.log('Impact: Percentage points overall coverage would increase if file reaches 100%');
79+
80+
// Summary insights
81+
console.log('\n' + '═'.repeat(80));
82+
console.log('INSIGHTS:');
83+
console.log('─'.repeat(80));
84+
85+
const top5 = files.slice(0, 5);
86+
const top5Impact = top5.reduce((sum, f) => sum + f.impact, 0);
87+
console.log(`• Top 5 files contain ${top5.reduce((sum, f) => sum + f.uncovered, 0)} uncovered lines`);
88+
console.log(`• Fixing top 5 files would improve coverage by ${top5Impact.toFixed(1)} percentage points`);
89+
console.log(`• This would bring overall coverage from ${overall.toFixed(1)}% to ${(overall + top5Impact).toFixed(1)}%`);
90+
91+
if (files[0] && files[0].uncovered > 50) {
92+
console.log(`\n• Priority: Focus on ${files[0].path.split('/').pop()}`);
93+
console.log(` ${files[0].uncovered} uncovered lines, currently ${files[0].coverage.toFixed(1)}% covered`);
94+
}
95+
96+
console.log('═'.repeat(80) + '\n');
97+
98+
} catch (error) {
99+
console.error('Error:', error.message);
100+
}

‎analyze-coverage.js‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Coverage Attribution Analysis for mcp-debugger
3+
* Automatically runs after npm run test:coverage
4+
*/
5+
6+
import fs from 'fs';
7+
import path from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = path.dirname(__filename);
12+
13+
try {
14+
const summaryPath = path.join(__dirname, 'coverage', 'coverage-summary.json');
15+
16+
if (!fs.existsSync(summaryPath)) {
17+
// Silently exit if no coverage data (might be running in a different context)
18+
process.exit(0);
19+
}
20+
21+
const coverage = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
22+
const overall = coverage.total ? coverage.total.lines.pct : 0;
23+
24+
let totalUncovered = 0;
25+
let totalLines = 0;
26+
const files = [];
27+
28+
for (const [filePath, data] of Object.entries(coverage)) {
29+
if (filePath === 'total') continue;
30+
31+
const uncovered = data.lines.total - data.lines.covered;
32+
totalUncovered += uncovered;
33+
totalLines += data.lines.total;
34+
35+
// Extract relative path for cleaner display
36+
let cleanPath = filePath.replace(process.cwd(), '').replace(/^[/\\]/, '');
37+
cleanPath = cleanPath.replace(/\\/g, '/');
38+
39+
files.push({
40+
path: cleanPath,
41+
coverage: data.lines.pct,
42+
uncovered,
43+
total: data.lines.total
44+
});
45+
}
46+
47+
// Calculate impact on overall coverage
48+
files.forEach(f => {
49+
f.impact = totalLines > 0 ? (f.uncovered / totalLines) * 100 : 0;
50+
});
51+
52+
// Sort by uncovered lines (descending)
53+
files.sort((a, b) => b.uncovered - a.uncovered);
54+
55+
// Print compact analysis
56+
console.log('\n' + '═'.repeat(70));
57+
console.log(' COVERAGE ANALYSIS - Files to focus on (sorted by impact):');
58+
console.log('═'.repeat(70));
59+
console.log(`Overall: ${overall.toFixed(1)}% | Focus on files with most uncovered lines`);
60+
console.log('─'.repeat(70));
61+
62+
// Show top 10 files
63+
const topFiles = files.slice(0, 10);
64+
if (topFiles.length > 0) {
65+
console.log('Lines Cov% Impact File');
66+
console.log('─'.repeat(70));
67+
topFiles.forEach(f => {
68+
// Shorten path if needed
69+
let displayPath = f.path;
70+
if (displayPath.length > 45) {
71+
displayPath = '...' + displayPath.slice(-42);
72+
}
73+
74+
console.log(
75+
f.uncovered.toString().padStart(5) +
76+
f.coverage.toFixed(0).padStart(6) + '%' +
77+
('+' + f.impact.toFixed(1) + '%').padStart(8) +
78+
' ' + displayPath
79+
);
80+
});
81+
82+
if (files.length > 10) {
83+
console.log(`... +${files.length - 10} more files`);
84+
}
85+
}
86+
87+
console.log('─'.repeat(70));
88+
console.log('Run "npm run test:coverage:analyze" for detailed analysis');
89+
console.log('═'.repeat(70) + '\n');
90+
91+
} catch (error) {
92+
// Silently fail - don't disrupt the test flow
93+
console.log('\n[Coverage analysis unavailable]');
94+
}

‎git-nomerge-local.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
 backup/uncommitted-work-20250726
2+
feature/javascript-adapter

‎git-nomerge-remote.txt‎

Whitespace-only changes.

‎package.json‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@
4848
"test:e2e:smoke": "vitest run tests/e2e/mcp-server-smoke*.test.ts",
4949
"test:e2e:container": "docker build --no-cache -t mcp-debugger:local . && vitest run tests/e2e/mcp-server-smoke-container.test.ts",
5050
"test:coverage": "npm run build && npm run pretest:docker && vitest run --coverage",
51-
"posttest:coverage": "node scripts/cleanup-test-processes.js",
51+
"posttest:coverage": "node scripts/cleanup-test-processes.js && node analyze-coverage.js",
5252
"test:coverage:quiet": "vitest run --coverage --reporter=dot --silent",
5353
"posttest:coverage:quiet": "node scripts/cleanup-test-processes.js",
5454
"test:coverage:summary": "node tests/utils/test-coverage-summary.js",
55+
"test:coverage:analyze": "node analyze-coverage-detailed.js",
56+
"coverage:analyze": "npm run test:coverage && node analyze-coverage-detailed.js",
5557
"test:coverage:json": "npm run build && vitest run --coverage --reporter=json --outputFile=test-results.json",
5658
"posttest:coverage:json": "node scripts/cleanup-test-processes.js",
5759
"test:dot": "vitest run --reporter=dot",
@@ -104,8 +106,6 @@
104106
},
105107
"dependencies": {
106108
"@debugmcp/shared": "workspace:*",
107-
"@debugmcp/adapter-mock": "workspace:*",
108-
"@debugmcp/adapter-python": "workspace:*",
109109
"@modelcontextprotocol/sdk": "^1.10.2",
110110
"@types/which": "^3.0.4",
111111
"@vscode/debugadapter": "^1.68.0",
@@ -122,6 +122,10 @@
122122
"winston": "^3.11.0",
123123
"zod": "^3.22.4"
124124
},
125+
"optionalDependencies": {
126+
"@debugmcp/adapter-mock": "workspace:*",
127+
"@debugmcp/adapter-python": "workspace:*"
128+
},
125129
"devDependencies": {
126130
"@eslint/js": "^9.27.0",
127131
"@microsoft/fetch-event-source": "^2.0.1",

‎scripts/bundle.js‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,55 @@ async function bundle() {
3636
// Write metafile for analysis
3737
fs.writeFileSync('dist/bundle-meta.json', JSON.stringify(result.metafile));
3838

39+
// CRITICAL: Add console silencing at the very beginning of the bundle
40+
let bundleContent = fs.readFileSync('dist/bundle.cjs', 'utf8');
41+
42+
// Remove any shebang lines that got bundled (they're invalid in the middle of the file)
43+
bundleContent = bundleContent.replace(/^#!.*$/gm, '// shebang removed');
44+
45+
const consoleSilencer = `#!/usr/bin/env node
46+
// CRITICAL: Console silencing MUST be first - before ANY code runs
47+
// This prevents stdout pollution in stdio mode which breaks MCP protocol
48+
(function() {
49+
// Handle both quoted and unquoted stdio arguments
50+
const hasStdio = process.argv.some(arg =>
51+
arg === 'stdio' ||
52+
arg === '"stdio"' ||
53+
arg.includes('stdio')
54+
);
55+
56+
if (hasStdio || process.env.DEBUG_MCP_STDIO === '1') {
57+
const noop = () => {};
58+
console.log = noop;
59+
console.error = noop;
60+
console.warn = noop;
61+
console.info = noop;
62+
console.debug = noop;
63+
console.trace = noop;
64+
console.dir = noop;
65+
console.table = noop;
66+
console.group = noop;
67+
console.groupEnd = noop;
68+
console.time = noop;
69+
console.timeEnd = noop;
70+
console.assert = noop;
71+
72+
// Suppress process warnings
73+
process.removeAllListeners('warning');
74+
process.on('warning', noop);
75+
}
76+
})();
77+
78+
// Clean argv before any code processes it - strip quotes from all arguments
79+
process.argv = process.argv.map(arg =>
80+
typeof arg === 'string' ? arg.replace(/^["'](.*)["']$/, '$1') : arg
81+
);
82+
83+
`;
84+
85+
// Write the modified bundle with console silencing at the top
86+
fs.writeFileSync('dist/bundle.cjs', consoleSilencer + bundleContent);
87+
3988
// Copy proxy-bootstrap.js to dist if it exists
4089
const proxyBootstrapSrc = path.join('src', 'proxy', 'proxy-bootstrap.js');
4190
const proxyBootstrapDest = path.join('dist', 'proxy', 'proxy-bootstrap.js');

0 commit comments

Comments
 (0)