Audience: Adapter authors and maintainers
Source of truth: @debugmcp/shared interfaces and current implementation in src/adapters/*
This reference documents the contracts an adapter must satisfy to be discovered, loaded, and used by the mcp-debugger core. It also includes the dynamic loader and registry APIs that interact with adapters.
Contents
- IDebugAdapter (required for all adapters)
- AdapterFactory (base class for factories)
- AdapterLoader (dynamic runtime loader)
- AdapterRegistry (runtime registry and lifecycle management)
- Adapter ownership: AdapterLease and disposal
- Error types and diagnostics
- Environment variables
src/adapters/ holds four modules: adapter-loader.ts, adapter-registry.ts, and the two that own an adapter instance's lifetime once it has been created — adapter-lease.ts and adapter-disposal.ts (see Adapter ownership).
File: packages/shared/src/interfaces/debug-adapter.ts
Adapters must implement IDebugAdapter. This is an async-first, event-driven interface that abstracts DAP operations while remaining language-agnostic.
Key properties
- language:
DebugLanguage— The language identifier (e.g.,'python','mock') - name:
string— Human-friendly adapter name
Lifecycle
initialize(): Promise<void>— Prepare resources and validate environmentdisconnect(): Promise<void>— Disconnect from the debug adapter (closes the DAP connection but does not fully tear down the adapter)dispose(): Promise<void>— Full cleanup: releases all resources, resets state to UNINITIALIZED, and emits a'disposed'event. This is distinct fromdisconnect(), which only closes the connection.
State
getState(): AdapterState— Current adapter state (see enum)isReady(): boolean— Whether adapter is ready for debugginggetCurrentThreadId(): number | null— Active thread (if any)
Environment validation
validateEnvironment(executablePath?: string): Promise<ValidationResult>— Check runtime prerequisites (optionalexecutablePathvalidates a specific user-configured interpreter instead of an auto-detected one)getRequiredDependencies(): DependencyInfo[]— Declare dependencies (name/version/required)
Executable management
resolveExecutablePath(preferredPath?: string): Promise<string>— Resolve language runtime pathgetDefaultExecutableName(): string— e.g.,'python','node','go'getExecutableSearchPaths(): string[]— Platform-specific search locations
Adapter configuration (DAP adapter process)
buildAdapterCommand(config: AdapterConfig): AdapterCommand— Command/args/env for launching the DAP adapter processgetAdapterModuleName(): string— e.g.,'debugpy.adapter'getAdapterInstallCommand(): string— e.g.,'pip install debugpy'
Launch coordination (optional)
createLaunchBarrier?(command: string, args?: unknown): AdapterLaunchBarrier | undefined- Allows an adapter to supply a coordination object when a particular DAP request (for example,
'launch') needs custom handling. - When present,
ProxyManagerforwards proxy status messages, DAP events, and exit notifications to the barrier instead of hard-coding language logic. - Typical use: js-debug’s launch flow resolves when a
stoppedevent oradapter_connectedstatus arrives; the adapter signals readiness via the barrier without forcingProxyManagerto know about JavaScript specifics.
- Allows an adapter to supply a coordination object when a particular DAP request (for example,
Attach support (optional)
supportsAttach?(): boolean— Whether the adapter supports attaching to running processessupportsDetach?(): boolean— Whether the adapter supports detaching without terminating the debuggeetransformAttachConfig?(config: GenericAttachConfig): LanguageSpecificAttachConfig | Promise<LanguageSpecificAttachConfig>— Transforms generic attach config to language-specific format; may be async (awaited by the launcher)consumedAttachKeys?: readonly string[]— adapterConfig keys the attach transform consumes itself (adapter-side work, a private side channel) rather than forwarding; their absence from the transform's output is not reported as an ignored keygetDefaultAttachConfig?(): Partial<GenericAttachConfig>— Gets default attach configuration for this languageusesDirectConnectForAttach?(): boolean— when true, attach connects directly to an already-listening DAP server (e.g. rdbg started with--open) instead of spawning an adapter process;ProxyManagerskipsbuildAdapterCommandand the adapter policy returns a'connect'spawn config from the attach host/port
Debug configuration
transformLaunchConfig(config: GenericLaunchConfig): Promise<LanguageSpecificLaunchConfig>(async to permit build/compilation steps before launch)getDefaultLaunchConfig(): Partial<GenericLaunchConfig>
File: packages/shared/src/interfaces/adapter-launch-barrier.ts
Adapters that implement createLaunchBarrier should return an object with the following responsibilities:
awaitResponse: boolean— Iffalse,ProxyManagerdoes NOT await the DAP response; the request resolves oncewaitUntilReady()completes (fire-and-forget launches). Iftrue,ProxyManagerawaits both the DAP response ANDwaitUntilReady(), then disposes the barrier.onRequestSent(requestId)— Called after a DAP request is assigned arequestIdbut before it is sent, so the barrier can track in-flight requests.onProxyStatus(status, message)/onDapEvent(event, body)— Receive raw proxy messages to determine readiness.onProxyExit(code, signal)— Fail fast if the proxy exits unexpectedly.waitUntilReady()— Resolve when launch coordination is complete; reject to bubble an error.dispose()— Clean up timers or listeners when the barrier is cleared.
If createLaunchBarrier returns undefined, ProxyManager falls back to the default behavior (awaiting the DAP response).
DAP protocol operations
sendDapRequest<T extends DebugProtocol.Response>(command: string, args?: unknown): Promise<T>handleDapEvent(event: DebugProtocol.Event): voidhandleDapResponse(response: DebugProtocol.Response): void
Connection management
connect(host: string, port: number): Promise<void>disconnect(): Promise<void>isConnected(): boolean
Error handling helpers
getInstallationInstructions(): stringgetMissingExecutableError(): stringtranslateErrorMessage(error: Error): string
Capabilities and features
supportsFeature(feature: DebugFeature): booleangetFeatureRequirements(feature: DebugFeature): FeatureRequirement[]getCapabilities(): AdapterCapabilities— Mirrors DAP capabilities
Supporting types (selected)
AdapterState:UNINITIALIZED | INITIALIZING | READY | CONNECTED | DEBUGGING | DISCONNECTED | ERRORValidationResult:{ valid: boolean; errors: ValidationError[]; warnings: ValidationWarning[] }AdapterCommand:{ command: string; args: string[]; env?: Record<string,string>; cwd?: string }(cwdis needed by adapters whose debuggee starts at spawn time, e.g. rdbg — issue #320)AdapterConfig:{ sessionId, executablePath, adapterHost, adapterPort, logDir, scriptPath, scriptArgs?, launchConfig }GenericLaunchConfig:{ stopOnEntry?, justMyCode?, env?, cwd?, args? }
Events
- DAP events:
'stopped' | 'continued' | 'terminated' | 'exited' | 'thread' | 'output' | 'breakpoint' | 'module' - Lifecycle:
'initialized' | 'connected' | 'disconnected' | 'disposed' | 'error'(note:'error'carries anErrorpayload;'disposed'is emitted bydispose()after full cleanup) - State changes:
'stateChanged'with(oldState, newState)
Example (minimal)
import { EventEmitter } from 'events';
import type { IDebugAdapter, AdapterState, DebugFeature, AdapterCapabilities } from '@debugmcp/shared';
export class ExampleAdapter extends EventEmitter implements IDebugAdapter {
readonly language = 'example' as any;
readonly name = 'Example Debug Adapter';
private state: AdapterState = AdapterState.UNINITIALIZED;
async initialize() { this.state = AdapterState.READY; this.emit('initialized'); }
async dispose() { this.state = AdapterState.UNINITIALIZED; this.emit('disposed'); }
getState() { return this.state; }
isReady() { return this.state === AdapterState.READY || this.state === AdapterState.DEBUGGING; }
getCurrentThreadId() { return null; }
async validateEnvironment() { return { valid: true, errors: [], warnings: [] }; }
getRequiredDependencies() { return []; }
async resolveExecutablePath(preferred?: string) { return preferred ?? 'example'; }
getDefaultExecutableName() { return 'example'; }
getExecutableSearchPaths() { return []; }
buildAdapterCommand(config) { return { command: 'example', args: ['--port', String(config.adapterPort)], env: process.env as any }; }
getAdapterModuleName() { return 'example.adapter'; }
getAdapterInstallCommand() { return 'npm install -g example-adapter'; }
async transformLaunchConfig(cfg) { return cfg; }
getDefaultLaunchConfig() { return { stopOnEntry: true }; }
async sendDapRequest(command, args) { throw new Error('not implemented'); }
handleDapEvent(_e) { /* map events to state; emit as needed */ }
handleDapResponse(_r) { /* optional */ }
async connect(_h, _p) { this.state = AdapterState.CONNECTED; this.emit('connected'); }
async disconnect() { this.state = AdapterState.DISCONNECTED; this.emit('disconnected'); }
isConnected() { return this.state === AdapterState.CONNECTED || this.state === AdapterState.DEBUGGING; }
getInstallationInstructions() { return 'Install example-adapter per your OS instructions.'; }
getMissingExecutableError() { return 'Example executable not found'; }
translateErrorMessage(err: Error) { return err.message; }
supportsFeature(_f: DebugFeature) { return false; }
getFeatureRequirements(_f: DebugFeature) { return []; }
getCapabilities(): AdapterCapabilities { return { supportsConfigurationDoneRequest: true }; }
}File: packages/shared/src/factories/adapter-factory.ts (base class), packages/shared/src/interfaces/adapter-registry.ts (interface)
Factories create adapter instances and expose metadata. The core contract is the IAdapterFactory interface. Most adapters implement this interface by extending the AdapterFactory base class, but extending it is not strictly required -- implementing IAdapterFactory directly is also valid.
Key API
constructor(metadata: AdapterMetadata)— Provide name, description, version constraints, etc.getMetadata(): AdapterMetadata— Retrieve factory metadatavalidate(): Promise<FactoryValidationResult>— Override to ensure the environment supports creating adaptersisCompatibleWithCore(coreVersion: string): boolean— Optional version gatingcreateAdapter(dependencies: AdapterDependencies): IDebugAdapter— REQUIRED
Example factory
import { AdapterFactory } from '@debugmcp/shared';
import type { AdapterDependencies, IDebugAdapter } from '@debugmcp/shared';
import { ExampleAdapter } from './ExampleAdapter';
export class ExampleAdapterFactory extends AdapterFactory {
constructor() {
super({
language: 'example',
displayName: 'Example',
version: '0.1.0',
author: 'mcp-debugger team',
description: 'Example adapter',
minimumDebuggerVersion: '0.14.0',
});
}
async validate() {
// Optionally check environment prerequisites
return { valid: true, errors: [], warnings: [] };
}
createAdapter(deps: AdapterDependencies): IDebugAdapter {
return new ExampleAdapter(/* deps as needed */);
}
}Export convention (required for dynamic loader)
- Package name:
@debugmcp/adapter-<language> - The loader requires a named export matching
<CapitalizedLanguage>AdapterFactory(e.g.,python->PythonAdapterFactory,javascript->JavascriptAdapterFactory). It instantiates this class with a zero-arg constructor. - Some adapter packages also expose a default export for plugin-style loading (e.g.,
adapter-goandadapter-javaexport{ name, factory }as default). The dynamic loader does not use the default export, but it may be useful for custom integration scenarios.
export { ExampleAdapterFactory } from './ExampleAdapterFactory.js';File: src/adapters/adapter-loader.ts
Purpose: Discover and dynamically import an adapter package by language at runtime.
Public methods
loadAdapter(language: string): Promise<IAdapterFactory>- Attempts
import('@debugmcp/adapter-<language>') - Falls back to URLs relative to the loader's own module location (using
import.meta.url):../../node_modules/@debugmcp/adapter-<language>/dist/index.js../../packages/adapter-<language>/dist/index.js
- Also attempts
createRequire+fileURLToPathfallback - Extracts
<Language>AdapterFactorynamed export, instantiates it, caches it - Throws with informative message on
ERR_MODULE_NOT_FOUND/MODULE_NOT_FOUND(suggests npm install) or missing factory
- Attempts
isAdapterAvailable(language: string): Promise<boolean>- Returns true if
loadAdaptersucceeds (and caches)
- Returns true if
listAvailableAdapters(): Promise<Array<{ name, packageName, description?, installed }>>- Currently uses a known adapter list and checks availability
- Returns install status for each known adapter
Notes
- Internal cache keyed by
languageto avoid repeated imports - Logs helpful diagnostics on failures (including suggested npm install)
File: src/adapters/adapter-registry.ts
Purpose: Manage adapter factories and active adapter instances; optionally lazy-load adapters on demand.
Key runtime behavior
- Constructor accepts config; dynamic loading enabled in containers by default:
enableDynamicLoading?: boolean(a typedAdapterRegistryConfigfield) ORprocess.env.MCP_CONTAINER === 'true'
register(language, factory)with optional validation and override rulesunregister(language)disposes active adapters, removes timers, unregisters factorycreate(language, config): Promise<IDebugAdapter>- If factory missing and dynamic enabled →
AdapterLoader.loadAdapter - Validates instance count against
maxInstancesPerLanguage - Creates dependencies and adapter, calls
initialize(), tracks active instance - Sets up auto-dispose based on adapter state changes
- If factory missing and dynamic enabled →
- Introspection
getSupportedLanguages(): string[]— currently registered factories (part of theIAdapterRegistryinterface)listLanguages(): Promise<string[]>— returns registered languages plus the hardcoded known-adapter catalog when dynamic loading is enabled (concrete implementation method)listAvailableAdapters(): Promise<AdapterManifestEntry[]>— merges loader metadata with registered languages, marking registered languages as installed (concrete implementation method)getAdapterInfo(language)/getAllAdapterInfo()
- Lifecycle
disposeAll()— disposes all adapters and clears registry
Auto-dispose
- Registry subscribes to adapter
'stateChanged'events - Starts a timer when state becomes
'disconnected'or'error' - Cancels timer if adapter becomes
'connected'or'debugging'again
File: src/adapters/adapter-lease.ts
The registry caps concurrent adapters per language (maxInstancesPerLanguage) and releases a slot only when the adapter emits 'disposed'. AdapterLease makes ownership of that slot explicit for the window between registry.create() and handing the adapter to a ProxyManager, so a throw anywhere inside the window cannot strand it (issue #557).
const lease = await AdapterLease.acquire(registry, language, config, logger);
try {
// ...prepare the launch using lease.adapter...
const proxyManager = lease.transferTo(proxyManagerFactory); // ownership moves
// ...start the proxy...
} finally {
await lease.release(); // no-op after a transfer; disposes on every failure path
}static acquire(registry, language, config, logger): Promise<AdapterLease>— creates the adapter and takes ownership. A rejection yields no lease.readonly adapter: IDebugAdapter— valid whether or not the lease is still held.transferTo(factory: IProxyManagerFactory): IProxyManager— hands ownership to the ProxyManager. Throws if the lease is no longer held; ownership moves only afterfactory.createreturns, so a throwing factory leaves the lease held.release(): Promise<void>— disposes if still held. Idempotent, a no-op after a transfer, and never throws (it is afinallyguarding the caller's real error).getState(): 'held' | 'transferred' | 'released'.
src/session/launch/proxy-launcher.ts is the caller — the single proxy launch that both launch and attach go through.
File: src/adapters/adapter-disposal.ts
The one way to dispose an adapter you are done with, shared by AdapterLease.release() and ProxyManager.cleanup(). It awaits adapter.dispose() inside a guard so that a synchronous throw is caught as well as a rejection, and neither disposal nor the reporting of a disposal failure can escape into a teardown path.
From @debugmcp/shared (selected)
AdapterErrorwithAdapterErrorCodeenum:- Environment:
ENVIRONMENT_INVALID,EXECUTABLE_NOT_FOUND,ADAPTER_NOT_INSTALLED,INCOMPATIBLE_VERSION - Connection:
CONNECTION_FAILED,CONNECTION_TIMEOUT,CONNECTION_LOST - Protocol:
INVALID_RESPONSE,UNSUPPORTED_OPERATION - Runtime:
DEBUGGER_ERROR,SCRIPT_NOT_FOUND,PERMISSION_DENIED - Generic:
UNKNOWN_ERROR
- Environment:
Dynamic loader error messages
- Missing package:
Failed to load adapter for 'python' from package '@debugmcp/adapter-python'. Adapter not installed. Install with: npm install @debugmcp/adapter-python
- Package on disk but its import failed (a missing dependency, a broken dist — issue #795):
Failed to load adapter for 'python' from package '@debugmcp/adapter-python'. The package is installed but importing it failed: Cannot find package 'dotenv' imported from … — its dependency 'dotenv' is missing or broken. Reinstall it (npm install @debugmcp/adapter-python) or rebuild it.
- Missing factory:
Factory class PythonAdapterFactory not found in @debugmcp/adapter-python
Troubleshooting checklist
npm ls @debugmcp/adapter-*to verify installation- Confirm named export of
<Lang>AdapterFactoryclass - Check container runtime deps (e.g.,
which+isexeif used) - For stdio transport, ensure stdout is NDJSON-only; use provided preloader
- Increase logging (server debug;
DEBUG=mcp:*in clients if supported) - Use
scripts/diagnose-stdio-client.mjsto verify connect → list → create → close
MCP_CONTAINER=true- Enables dynamic loading by default in the registry
- Container-friendly behavior and logging locations
CONSOLE_OUTPUT_SILENCED=1(set internally for transport runs that must suppress console output)- Ensures stdio silencer/mirroring is active in container entrypoint
- Standard logging envs or CLI flags (see README and docs)
Files
packages/adapter-example/
package.json
src/example-debug-adapter.ts
src/example-adapter-factory.ts
src/index.ts
tsconfig.json
File names are kebab-case, the convention used by 9 of the 10 shipped adapters (see adapter-development-guide.md). Class names stay PascalCase.
Entry export (src/index.ts)
// Named export required by the dynamic loader
export { ExampleAdapterFactory } from './example-adapter-factory.js';Installation and discovery
- Build your adapter to
dist/ - Install as a dependency alongside
@debugmcp/mcp-debugger - The loader will find it via
import('@debugmcp/adapter-example')or monorepo fallback paths