Audience: Adapter authors who want to add a new language to mcp-debugger
Scope: Full end-to-end instructions to build, test, publish, and troubleshoot a language adapter for the dynamic loading system.
An adapter is a language-specific component that implements the Debug Adapter Protocol (DAP) behind a common TypeScript interface (IDebugAdapter). The mcp-debugger core discovers and loads adapters dynamically at runtime using the package naming convention @debugmcp/adapter-<language> and a factory class named <CapitalizedLanguage>AdapterFactory.
Benefits:
- Pluggable language support (install only what you need)
- Small core with lazy loading (faster startup)
- Clean separation of concerns (core vs language details)
packages/adapter-<language>/
package.json
tsconfig.json
src/
<Language>Adapter.ts
<Language>AdapterFactory.ts
index.ts
dist/
(compiled output)
Naming conventions:
- Package name:
@debugmcp/adapter-<language> - Factory class:
<CapitalizedLanguage>AdapterFactory(e.g.,PythonAdapterFactory) - Default export:
{ name: '<language>', factory: <Language>AdapterFactory }
Why? The dynamic loader expects the package name and factory class name to follow these conventions.
- Create the package directory under
packages/
packages/
adapter-mock/
adapter-python/
adapter-yourlang/ ← new package here
- package.json (ESM + proper exports)
{
"name": "@debugmcp/adapter-yourlang",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"exports": {
".": {
"import": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist"
},
"peerDependencies": {
"@debugmcp/shared": "^0.14.1"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^20.0.0"
}
}- Implement
IDebugAdapter(core contract)
- File:
src/<Language>Adapter.ts - Use
packages/shared/src/interfaces/debug-adapter.tsfor signatures
- Implement the factory extending
AdapterFactory
- File:
src/<Language>AdapterFactory.ts - Extend
AdapterFactoryfrom@debugmcp/shared - Implement
createAdapter(dependencies)
- Export from
src/index.tswith the required default shape
- Must export named factory class and default
{ name, factory }
- Add TypeScript configuration
- File:
tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"declaration": true
},
"include": ["src/**/*"]
}src/ExampleAdapter.ts
import { EventEmitter } from 'events';
import {
AdapterState,
type IDebugAdapter,
type AdapterCapabilities,
type DebugFeature,
} from '@debugmcp/shared';
import type { DebugProtocol } from '@vscode/debugprotocol';
export class ExampleAdapter extends EventEmitter implements IDebugAdapter {
readonly language = 'example' as any;
readonly name = 'Example Debug Adapter';
private state: AdapterState = AdapterState.UNINITIALIZED;
private threadId: number | null = null;
async initialize(): Promise<void> {
this.state = AdapterState.READY;
this.emit('initialized');
}
async dispose(): Promise<void> {
this.state = AdapterState.DISCONNECTED;
this.emit('disconnected');
}
getState(): AdapterState { return this.state; }
isReady(): boolean { return this.state === AdapterState.READY || this.state === AdapterState.DEBUGGING; }
getCurrentThreadId(): number | null { return this.threadId; }
async validateEnvironment() { return { valid: true, errors: [], warnings: [] }; }
getRequiredDependencies() { return []; }
async resolveExecutablePath(preferred?: string): Promise<string> { return preferred ?? 'example'; }
getDefaultExecutableName(): string { return 'example'; }
getExecutableSearchPaths(): string[] { return []; }
buildAdapterCommand(config: any) {
return { command: 'example', args: ['--port', String(config.adapterPort)], env: process.env as Record<string, string> };
}
getAdapterModuleName(): string { return 'example.adapter'; }
getAdapterInstallCommand(): string { return 'npm install -g example-adapter'; }
async transformLaunchConfig(config: any): Promise<any> { return config; }
getDefaultLaunchConfig() { return { stopOnEntry: true }; }
async sendDapRequest<T extends DebugProtocol.Response>(_command: string, _args?: unknown): Promise<T> {
throw new Error('Not implemented');
}
handleDapEvent(_event: DebugProtocol.Event): void { /* map events to state; emit as needed */ }
handleDapResponse(_response: DebugProtocol.Response): void { /* optional */ }
async connect(_host: string, _port: number): Promise<void> {
this.state = AdapterState.CONNECTED;
this.emit('connected');
}
async disconnect(): Promise<void> {
this.state = AdapterState.DISCONNECTED;
this.emit('disconnected');
}
isConnected(): boolean {
return this.state === AdapterState.CONNECTED || this.state === AdapterState.DEBUGGING;
}
getInstallationInstructions(): string { return 'Install example adapter per your OS instructions.'; }
getMissingExecutableError(): string { return 'Example executable not found'; }
translateErrorMessage(error: Error): string { return error.message; }
supportsFeature(_feature: DebugFeature): boolean { return false; }
getFeatureRequirements(_feature: DebugFeature) { return []; }
getCapabilities(): AdapterCapabilities { return { supportsConfigurationDoneRequest: true }; }
}src/ExampleAdapterFactory.ts
import { AdapterFactory, type AdapterDependencies, type IDebugAdapter } from '@debugmcp/shared';
import { ExampleAdapter } from './ExampleAdapter.js';
export class ExampleAdapterFactory extends AdapterFactory {
constructor() {
super({
name: 'example',
displayName: 'Example',
description: 'Example adapter',
minimumDebuggerVersion: '0.14.0'
});
}
async validate() {
// Optional: verify environment prerequisites here
return { valid: true, errors: [], warnings: [] };
}
createAdapter(_deps: AdapterDependencies): IDebugAdapter {
return new ExampleAdapter();
}
}src/index.ts
export { ExampleAdapterFactory } from './ExampleAdapterFactory.js';
export default { name: 'example', factory: ExampleAdapterFactory };Unit tests (Vitest)
import { describe, it, expect, beforeEach } from 'vitest';
import { ExampleAdapter } from '../src/ExampleAdapter';
describe('ExampleAdapter', () => {
let adapter: ExampleAdapter;
beforeEach(() => {
adapter = new ExampleAdapter();
});
it('initializes to READY', async () => {
await adapter.initialize();
expect(adapter.isReady()).toBe(true);
});
});Integration smoke (using the server tools)
- Ensure your adapter is built and discoverable (installed into the same node_modules or available via monorepo fallback)
- From an MCP client or test harness:
- Call
list_supported_languagesand confirm your language appears - Call
create_debug_sessionwith"language": "<yourlang>"and ensure success
- Call
Local linking
npm packand install the generated.tgzinto your test project- Or use
npm link(note: watch out for ESM + symlink constraints;npm packis often more predictable)
- Build to
dist/(tsc -p tsconfig.json) - Publish to npm (
npm publish) - SemVer policy:
- Avoid breaking
IDebugAdaptercontracts across minor versions - Use
minimumDebuggerVersionin factory metadata if the core version is required
- Avoid breaking
Modularity by default
- mcp-debugger loads adapters dynamically from a fixed catalog using package names
@debugmcp/adapter-<language>. - There is no auto-install on first use. Users install or build only the adapters they need.
“Installed” semantics
- list_supported_languages marks an adapter as installed:true when the loader can resolve/import the adapter package (or its dist via monorepo fallback).
- Validation of deeper requirements (e.g., vendor files like js-debug) happens later at session creation/start; discovery remains fast and non-invasive.
Developer workflow (monorepo)
- Build all adapters for local testing:
- pnpm -w run build:adapters:all
- Build only the JavaScript adapter and vendor js-debug:
- pnpm -w run dev:js
- Equivalent steps:
- pnpm -w -F @debugmcp/adapter-javascript build
- pnpm -w -F @debugmcp/adapter-javascript run build:adapter
- After building, list_supported_languages should show:
- available includes javascript with installed:true
- languages contains a JavaScript/TypeScript entry with defaultExecutable: node
Published/package consumption
- Consumers install only the adapters they need, for example:
- npm i @debugmcp/adapter-javascript
- The adapter-javascript package includes dist and vendor/js-debug (to support offline/air-gapped use).
Monorepo dynamic loading
- The loader first tries to import the package by name.
- If that fails, it attempts fallback URLs that include:
- node_modules/@debugmcp/adapter-/dist/index.js
- packages/adapter-/dist/index.js (handy for monorepo development)
- A small unit test ensures javascript is marked installed:true when only packages/adapter-javascript/dist exists.
Windows notes
- Fallback paths are constructed via file URLs; tests cover resolution on Windows.
- Use Node.js 18+ for the JS adapter and ensure node is on PATH for defaultExecutable behavior.
Verification
- Build and vendor JS:
- pnpm -w run dev:js
- Build all adapters (optional):
- pnpm -w run build:adapters:all
- Start the server and call list_supported_languages:
- javascript appears in available with installed:true
- languages contains the JavaScript/TypeScript metadata with defaultExecutable: node
- Lazy initialization: do minimal work in the constructor and in
initialize() - Cache executable discovery results
- First-load vs cached-load:
- The loader caches the factory in-memory; repeated sessions load instantly
- Preloading:
- If your deployment benefits, trigger
list_supported_languagesearly to prime the loader
- If your deployment benefits, trigger
- If your adapter uses
which, remember it depends onisexeat runtime in minimal Node images- Ensure both are included in the runtime image or avoid shell resolution entirely
- Stdout must be NDJSON-only in stdio mode (no non-JSON output)
- The runtime entry preloads a silencer that mirrors to
/app/logs/stdout-raw.logand/app/logs/stdin-raw.logwithout altering protocol framing
- The runtime entry preloads a silencer that mirrors to
- Diagnostics:
- Use
node scripts/diagnose-stdio-client.mjsto validate connect → list → create → close - Volume-mount logs and review
/app/logs/*
- Use
Common errors & fixes
MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND- Not installed in the runtime. Fix:
npm install @debugmcp/adapter-<language>and rebuild image/app
- Not installed in the runtime. Fix:
Factory class <Language>AdapterFactory not found- Export mismatch. Ensure you export the named class and default
{ name, factory }in your entry
- Export mismatch. Ensure you export the named class and default
- Adapter not listed by
list_supported_languages- Verify npm installation (
npm ls @debugmcp/adapter-*) - Confirm package’s
exportsandtype: "module"are correct - In monorepo/dev, confirm dist exists and fallback paths are correct
- Verify npm installation (
- Immediate disconnect on stdio
- Likely stdout pollution. Ensure your adapter does not log to stdout/stderr on import/initialize
- Use the provided stdio silencer and inspect
/app/logs/stdout-raw.log
Debugging dynamic loading
- Increase server logging (debug level) and watch loader logs
- Note:
DEBUG=mcp:*is misleading in STDIO mode because console output is silenced to protect JSON-RPC framing. Use--log-file <path>instead to capture verbose logs to disk - Confirm default export shape in the compiled
dist/index.js - Validate
@debugmcp/sharedpeer version compatibility with the core
Troubleshooting flow
Adapter not loading?
├── npm ls @debugmcp/adapter-*
├── Verify export structure { name: '<lang>', factory: <Lang>AdapterFactory }
├── Check compiled dist/index.js exists and is ESM
├── Check stdout purity in stdio (no logs)
└── Inspect /app/logs/stdout-raw.log and stdin-raw.log (container)
-
IDebugAdapterfully implemented - Factory extends
AdapterFactoryand validates environment (if needed) - Export conventions followed (
@debugmcp/adapter-<language>, factory class name, default export) - TypeScript built to
dist/(ESM) - Unit and integration tests pass
- Adapter discovers and loads dynamically
- Container runtime dependencies included (if applicable)
- No stdout pollution in stdio mode
- Publishable package metadata (version, license, description)
You can scaffold from the minimal example above, or copy the following files and replace Example with your language:
src/<Language>Adapter.tssrc/<Language>AdapterFactory.tssrc/index.tspackage.json(with correctname,exports,peerDependencies)tsconfig.json
Once built and installed alongside @debugmcp/mcp-debugger, your adapter will be discovered by the dynamic loader automatically.