Skip to content

Commit 2a39ced

Browse files
committed
feat: add npx distribution package for MCP debugger CLI
- Create @debugmcp/mcp-debugger package for npx distribution - Bundle CLI with esbuild to include all dependencies - Preserve stdio console silencing logic for MCP compatibility - Update release workflow to publish CLI package - Maintain dynamic adapter loading capability
1 parent 11ef2e3 commit 2a39ced

123 files changed

Lines changed: 6814 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/release.yml‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ jobs:
173173
- name: Install dependencies
174174
run: npm ci --ignore-scripts
175175

176+
- name: Set CLI package version from tag
177+
run: |
178+
VERSION=${GITHUB_REF#refs/tags/v}
179+
echo "Setting CLI package version to $VERSION"
180+
cd packages/mcp-debugger
181+
npm version $VERSION --no-git-tag-version --allow-same-version
182+
cd ../..
183+
176184
- name: Build project
177185
run: npm run build
178186

@@ -181,6 +189,7 @@ jobs:
181189
npm pack --dry-run -w @debugmcp/shared
182190
npm pack --dry-run -w @debugmcp/adapter-mock
183191
npm pack --dry-run -w @debugmcp/adapter-python
192+
npm pack --dry-run -w @debugmcp/mcp-debugger
184193
185194
- name: Publish to npm (workspaces)
186195
run: |
@@ -190,6 +199,7 @@ jobs:
190199
npm publish -w @debugmcp/shared --access public
191200
npm publish -w @debugmcp/adapter-mock --access public
192201
npm publish -w @debugmcp/adapter-python --access public
202+
npm publish -w @debugmcp/mcp-debugger --access public
193203
env:
194204
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
195205

@@ -237,16 +247,27 @@ jobs:
237247
docker pull debugmcp/mcp-debugger:${{ steps.changelog.outputs.VERSION }}
238248
```
239249
240-
**npm:**
250+
**npm (global install):**
241251
```bash
242252
npm install -g @debugmcp/mcp-debugger@${{ steps.changelog.outputs.VERSION }}
243253
```
244254
255+
**npx (no install):**
256+
```bash
257+
npx @debugmcp/mcp-debugger@${{ steps.changelog.outputs.VERSION }} stdio
258+
```
259+
245260
**PyPI:**
246261
```bash
247262
pip install debug-mcp-server-launcher==${{ steps.changelog.outputs.VERSION }}
248263
```
249264
265+
**Optional adapters:**
266+
```bash
267+
npm install -g @debugmcp/adapter-python
268+
npm install -g @debugmcp/adapter-mock
269+
```
270+
250271
### 📚 Documentation
251272
See the [README](https://github.com/debugmcp/mcp-debugger#readme) for usage instructions.
252273
EOF

‎packages/mcp-debugger/README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# @debugmcp/mcp-debugger
2+
3+
Step-through debugging MCP server for LLMs
4+
5+
## Installation
6+
7+
You can use this package without installation via npx:
8+
9+
```bash
10+
npx @debugmcp/mcp-debugger stdio
11+
```
12+
13+
Or install it globally:
14+
15+
```bash
16+
npm install -g @debugmcp/mcp-debugger
17+
```
18+
19+
## Usage
20+
21+
### STDIO mode (default)
22+
```bash
23+
mcp-debugger stdio
24+
```
25+
26+
### SSE mode
27+
```bash
28+
mcp-debugger sse --port 3001
29+
```
30+
31+
## Optional Adapters
32+
33+
To debug specific languages, install the corresponding adapter packages:
34+
35+
```bash
36+
# Python debugging support
37+
npm install -g @debugmcp/adapter-python
38+
39+
# Mock adapter for testing
40+
npm install -g @debugmcp/adapter-mock
41+
```
42+
43+
## Options
44+
45+
- `--log-level <level>` - Set log level (error, warn, info, debug)
46+
- `--log-file <path>` - Log to file instead of console
47+
- `--port <number>` - Port for SSE mode (default: 3001)
48+
49+
## Documentation
50+
51+
See the [main repository](https://github.com/debugmcp/mcp-debugger) for full documentation.
52+
53+
## License
54+
55+
MIT
748 KB
Binary file not shown.

‎packages/mcp-debugger/package.json‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@debugmcp/mcp-debugger",
3+
"version": "0.15.4",
4+
"description": "Step-through debugging MCP server for LLMs",
5+
"homepage": "https://github.com/debugmcp/mcp-debugger",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/debugmcp/mcp-debugger.git",
9+
"directory": "packages/mcp-debugger"
10+
},
11+
"license": "MIT",
12+
"type": "module",
13+
"bin": {
14+
"mcp-debugger": "./dist/cli.cjs"
15+
},
16+
"files": [
17+
"dist",
18+
"README.md",
19+
"LICENSE"
20+
],
21+
"engines": {
22+
"node": ">=18.0.0"
23+
},
24+
"scripts": {
25+
"build": "node scripts/bundle-cli.js",
26+
"build:ci": "node scripts/bundle-cli.js",
27+
"clean": "rimraf dist && rimraf tsconfig.tsbuildinfo"
28+
},
29+
"publishConfig": {
30+
"access": "public"
31+
}
32+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Bundle the MCP debugger CLI for npx distribution
4+
*/
5+
6+
import * as esbuild from 'esbuild';
7+
import fs from 'fs';
8+
9+
async function bundleCLI() {
10+
console.log('Bundling MCP debugger CLI...');
11+
12+
try {
13+
// Bundle the CLI entry point directly from TypeScript source
14+
const result = await esbuild.build({
15+
entryPoints: ['src/cli-entry.ts'],
16+
bundle: true,
17+
platform: 'node',
18+
target: 'node18',
19+
format: 'cjs',
20+
outfile: 'dist/cli.cjs',
21+
define: {
22+
'import.meta.url': JSON.stringify('file:///app/dist/cli.cjs'),
23+
'__dirname': JSON.stringify('/app/dist')
24+
},
25+
external: [
26+
// Keep adapter packages external for dynamic loading
27+
'@debugmcp/adapter-python',
28+
'@debugmcp/adapter-mock',
29+
// Keep native modules external
30+
'fsevents'
31+
],
32+
minify: false, // Keep readable for debugging
33+
sourcemap: false,
34+
metafile: true,
35+
logLevel: 'info',
36+
loader: {
37+
'.ts': 'ts',
38+
'.js': 'js'
39+
}
40+
});
41+
42+
// Write metafile for analysis
43+
fs.writeFileSync('dist/bundle-meta.json', JSON.stringify(result.metafile));
44+
45+
// Make the bundle executable
46+
const bundlePath = 'dist/cli.cjs';
47+
if (fs.existsSync(bundlePath)) {
48+
// Add shebang if not present
49+
let bundleContent = fs.readFileSync(bundlePath, 'utf8');
50+
if (!bundleContent.startsWith('#!/usr/bin/env node')) {
51+
bundleContent = '#!/usr/bin/env node\n' + bundleContent;
52+
fs.writeFileSync(bundlePath, bundleContent);
53+
}
54+
55+
// Make executable on Unix-like systems
56+
if (process.platform !== 'win32') {
57+
fs.chmodSync(bundlePath, '755');
58+
}
59+
}
60+
61+
// Calculate bundle size
62+
const stats = fs.statSync(bundlePath);
63+
const sizeInKB = (stats.size / 1024).toFixed(2);
64+
console.log(`CLI bundle created successfully: ${sizeInKB} KB`);
65+
66+
// Show what's included
67+
const text = await esbuild.analyzeMetafile(result.metafile, {
68+
verbose: false
69+
});
70+
console.log('\nBundle analysis:');
71+
console.log(text);
72+
73+
} catch (error) {
74+
console.error('Bundle failed:', error);
75+
process.exit(1);
76+
}
77+
}
78+
79+
bundleCLI();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Publishable CLI shim for npx usage.
4+
* Preserves critical stdio console silencing and delegates to the existing main() implementation.
5+
*/
6+
7+
// CRITICAL: Console silencing MUST be first - before ANY imports
8+
// This prevents stdout pollution in stdio mode which breaks MCP protocol
9+
(() => {
10+
// Handle both quoted and unquoted stdio arguments
11+
const hasStdio = process.argv.some(arg =>
12+
arg === 'stdio' ||
13+
arg === '"stdio"' ||
14+
arg === '"\'stdio\'"' ||
15+
arg.includes('stdio')
16+
);
17+
18+
// Auto-detect STDIO mode:
19+
// 1. Explicit stdio argument
20+
// 2. Environment variable set
21+
// 3. No transport argument specified (default is STDIO)
22+
// 4. stdin is a pipe (typical for MCP STDIO mode)
23+
const hasTransportArg = process.argv.some(arg =>
24+
arg === '--transport' || arg.includes('transport')
25+
);
26+
const isStdinPipe = !process.stdin.isTTY;
27+
const shouldSilenceConsole = hasStdio ||
28+
process.env.DEBUG_MCP_STDIO === '1' ||
29+
(!hasTransportArg && isStdinPipe);
30+
31+
if (shouldSilenceConsole) {
32+
// Set env flag immediately so any early imports see it
33+
if (hasStdio || shouldSilenceConsole) {
34+
process.env.DEBUG_MCP_STDIO = '1';
35+
}
36+
37+
const noop = () => {};
38+
console.log = noop;
39+
console.error = noop;
40+
console.warn = noop;
41+
console.info = noop;
42+
console.debug = noop;
43+
console.trace = noop;
44+
console.dir = noop;
45+
console.table = noop;
46+
console.group = noop;
47+
console.groupEnd = noop;
48+
console.time = noop;
49+
console.timeEnd = noop;
50+
console.assert = noop;
51+
52+
// Suppress process warnings
53+
process.removeAllListeners('warning');
54+
process.on('warning', noop);
55+
}
56+
})();
57+
58+
// Clean argv before any code processes it - strip quotes from all arguments
59+
process.argv = process.argv.map(arg =>
60+
typeof arg === 'string' ? arg.replace(/^["'](.*)["']$/, '$1') : arg
61+
);
62+
63+
// Import and run the existing CLI main from the root source to avoid duplicating logic.
64+
// esbuild will bundle the referenced source into this package so npx works standalone.
65+
import { main } from '../../../src/index.js';
66+
67+
Promise.resolve()
68+
.then(() => main())
69+
.catch((error) => {
70+
// In stdio mode, we must not write to console
71+
const isStdio = process.argv.includes('stdio') || process.env.DEBUG_MCP_STDIO === '1';
72+
if (!isStdio) {
73+
console.error('Fatal error:', error);
74+
}
75+
process.exit(1);
76+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "../..",
5+
"outDir": "./dist",
6+
"declarationDir": "./dist",
7+
"composite": true,
8+
"noEmit": false,
9+
"declaration": true,
10+
"sourceMap": false
11+
},
12+
"include": ["./src/**/*"],
13+
"references": [
14+
{ "path": "../shared" },
15+
{ "path": "../../tsconfig.src.json" }
16+
]
17+
}

‎pnpm-lock.yaml‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/adapters/adapter-loader.d.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { IAdapterFactory } from '@debugmcp/shared';
2+
import type { Logger as WinstonLogger } from 'winston';
3+
export interface ModuleLoader {
4+
load(modulePath: string): Promise<Record<string, unknown>>;
5+
}
6+
export interface AdapterMetadata {
7+
name: string;
8+
packageName: string;
9+
description?: string;
10+
installed: boolean;
11+
}
12+
export declare class AdapterLoader {
13+
private cache;
14+
private logger;
15+
private moduleLoader;
16+
constructor(logger?: WinstonLogger, moduleLoader?: ModuleLoader);
17+
private createDefaultModuleLoader;
18+
/**
19+
* Dynamically load an adapter by language name
20+
*/
21+
loadAdapter(language: string): Promise<IAdapterFactory>;
22+
/**
23+
* Check if an adapter is available (and cache it if so)
24+
*/
25+
isAdapterAvailable(language: string): Promise<boolean>;
26+
/**
27+
* List all potentially available adapters (known list for now)
28+
*/
29+
listAvailableAdapters(): Promise<AdapterMetadata[]>;
30+
private getPackageName;
31+
/**
32+
* Compute a monorepo fallback path to the adapter dist entry.
33+
* This supports running from the source tree and inside the Docker image where packages are copied.
34+
*/
35+
private getFallbackModulePath;
36+
private getFallbackModulePaths;
37+
private getFactoryClassName;
38+
}

0 commit comments

Comments
 (0)