Status: v0.23.0 Scope: Adapter discovery, lazy loading, caching, error handling, and container considerations
The mcp-debugger server discovers and loads debug adapters dynamically at runtime instead of compiling them into the core. This makes the core smaller, enables optional adapter installation, and keeps startup time fast while still supporting many languages.
High-level flow:
- Client calls
create_debug_sessionto create a session (no adapter loading yet) - Client calls
start_debuggingto begin debugging - During
start_debugging, SessionManager requests an adapter from AdapterRegistry - AdapterRegistry uses AdapterLoader to dynamically import the adapter package by language name
- A factory from the adapter package constructs a concrete IDebugAdapter instance
- Instance is cached and reused subject to limits and auto-dispose rules
flowchart LR
C[MCP Client] -->|Tool Request| SM[SessionManager]
SM --> R[AdapterRegistry]
R -->|lazy load| L[AdapterLoader]
L -->|import()| P[@debugmcp/adapter-<lang>]
P --> F[<Language>AdapterFactory]
F --> A[IDebugAdapter]
A --> R
R --> SM
SM --> C
- Discovers adapter packages by convention:
@debugmcp/adapter-<language> - Primary load path: dynamic
import(packageName) - Fallback load paths (URLs relative to bundle):
../../node_modules/@debugmcp/adapter-<language>/dist/index.js../../packages/adapter-<language>/dist/index.js(monorepo/dev)
- Also tries
createRequirefor CJS/bundled contexts when direct import fails - Expects a named factory class to exist in the module:
- Class name pattern:
<CapitalizedLanguage>AdapterFactory - Example: for
python,PythonAdapterFactory
- Class name pattern:
- Caches the constructed factory by language to avoid re-import
// src/adapters/adapter-loader.ts (summary)
const packageName = `@debugmcp/adapter-${language}`;
const FactoryClass = moduleRef[factoryClassName];
const factory: IAdapterFactory = new FactoryClass();
cache.set(language, factory);- Keeps a registry of adapter factories and tracks active adapter instances
- Lazy loading is opt-in via config or enabled in containers:
enableDynamicLoadingorprocess.env.MCP_CONTAINER === 'true'
- If a factory is not registered and dynamic is enabled:
- Calls
AdapterLoader.loadAdapter(language) - Registers the loaded factory and immediately uses it
- Calls
- Provides:
getSupportedLanguages()for currently registered factorieslistLanguages()returns statically registered languages unioned with the adapters the loader reports as installed (each hardcoded catalog entry is probed via a real dynamic import to set install status). Known-but-uninstalled catalog entries are excluded unless statically registered.listAvailableAdapters()for installed metadata (name, package, description)
- Enforces instance limits and auto-dispose timers
flowchart TD
subgraph Registry
RF[Registered Factories] --> Create
Create --> AA[Active Adapters]
Create -->|none registered| Lazy
Lazy --> Load[AdapterLoader.loadAdapter()]
Load --> RF
end
- Client calls
create_debug_sessionwithlanguage = "python". The server validates language support and creates a session record, but does not load the adapter yet. - Client calls
start_debugging. During the proxy launch (ProxyLauncher.start),SessionManagerrequests an adapter fromAdapterRegistry.create(language, config). - Registry checks for a registered factory. If not found:
- If dynamic enabled, it calls
AdapterLoader.loadAdapter(language). - Otherwise throws
AdapterNotFoundError.
- If dynamic enabled, it calls
- AdapterLoader:
- Attempts
import('@debugmcp/adapter-python'). - On failure, tries fallback URLs (node_modules, then packages).
- If still failing, attempts
createRequireusing file URL.
- Attempts
- On successful import:
- Extracts
<Language>AdapterFactoryclass. - Constructs the factory and returns it to registry.
- Registry runs factory
validate()ifvalidateOnRegisteris enabled (configurable; production ALWAYS disables it viavalidateOnRegister: falsefor faster startup) and registers it.
- Extracts
- Registry constructs an adapter instance via the factory, initializes it, and returns it.
- Subsequent requests benefit from in-memory cache (in both Registry and Loader).
sequenceDiagram
participant Client
participant Server
participant Registry
participant Loader
participant Package as @debugmcp/adapter-<lang>
Client->>Server: start_debugging(sessionId, script)
Server->>Registry: create(language, config)
alt factory present
Registry-->>Server: adapter instance
else factory missing & dynamic enabled
Registry->>Loader: loadAdapter(language)
Loader->>Package: import()
alt import ok
Package-->>Loader: { <Lang>AdapterFactory }
Loader-->>Registry: factory (cached)
Registry->>factory: createAdapter(deps)
factory-->>Registry: adapter
Registry-->>Server: adapter instance
else import fails
Loader-->>Registry: error (MODULE_NOT_FOUND)
Registry-->>Server: AdapterNotFoundError
end
end
Server-->>Client: session created / error
- If the import fails with
MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND, the code alone does not decide the verdict (issue #795):- every attempt (the bare package name, then each fallback path via ESM import and
createRequire) is kept, and the error reported is the first that names something other than the specifier it was asked for — a missing dependency, a broken file inside the package - the package resolver then checks whether the package is on disk (its entry or its
package.jsonresolves, or a fallback directory holds one); if not, AdapterLoader throwsAdapter not installed. Install with: npm install @debugmcp/adapter-<language> - if it is on disk, the message reads
The package is installed but importing it failed: <Node's own words>, names the missing dependency when the import named a bare specifier, and advises a reinstall or rebuild - when this error propagates through AdapterRegistry it becomes an
AdapterNotFoundErrorwhose message carries the loader's reason (No debug adapter could be loaded for language: <lang> — …) with the failed language left out of theAvailablelist and the loader's error ascause; aFactoryValidationErrorfrom registering the loaded factory is re-thrown as itself list_supported_languagesreports both modes unavailable with the same reason, the launch gate refusescreate_debug_session/start_debuggingwith it, andmcp-debugger doctorreports the language as broken
- every attempt (the bare package name, then each fallback path via ESM import and
- If the factory class is not found:
- Loader throws:
Factory class <Name> not found - Ensure the adapter exports the expected named class
- Loader throws:
- If registry cannot dynamically load or
enableDynamicLoadingis off:AdapterNotFoundError(language, availableLanguages)is thrown
- Startup time is unchanged in most cases because adapters are not loaded until needed (lazy).
- First-load vs cached-load:
- First-load includes Node resolution + module import: typically tens of milliseconds
- Cached load is near-zero (in-memory map lookup)
- Caching strategy:
- AdapterLoader caches factory instances by language
- AdapterRegistry keeps a map of active adapters per language and can auto-dispose idle adapters
- Preloading vs Lazy:
- Prefer lazy for most cases to keep cold-start minimal
- Calling
listLanguages()early probes discoverability/availability, but actual registry registration and adapter initialization still occur oncreate(...)or explicitloadAdapter(...)paths. To truly preload, construct sessions or callloadAdapter()on startup if your environment benefits from it. - Container mode exception: When
MCP_CONTAINER=true, startup pre-registers known adapters (mock, python, javascript, ruby, rust, go, java, cpp, cobol) viatryRegistercalls independencies.ts, so the firstcreate(...)call does not incur a dynamic import cost. Note that dotnet is not pre-registered in container mode.
- Minimal runtime image includes only Node runtime and Python (for python adapter) – ensure any runtime Node deps needed by adapters are copied. Example:
whichdepends onisexeat runtime; include both in the image when needed.
- Stdout purity for stdio transport:
- MCP stdio requires newline-delimited JSON (NDJSON) on stdout
- A preloader (scripts/stdio-silencer.cjs) silences console methods and mirrors raw stdio to
/app/logswithout altering protocol framing
- Helpful diagnostics:
- Use
scripts/diagnose-stdio-client.mjsto connect to the container via stdio and exercise tools - Mount
/app/logsto inspectstdout-raw.logandstdin-raw.log
- Use
- Environment:
MCP_CONTAINER=trueenables dynamic loading automatically in the registry
- MODULE_NOT_FOUND / ERR_MODULE_NOT_FOUND
- Cause: Adapter package not installed in the current runtime
- Fix:
npm install @debugmcp/adapter-<language>, rebuild/redeploy
- Factory class not found
- Cause: Adapter doesn’t export
<Language>AdapterFactoryas a named class - Fix: Ensure
export class <Language>AdapterFactory ...is present as a named export
- Cause: Adapter doesn’t export
- Adapter not discoverable in container
- Check that the adapter package and its runtime deps exist in the final runtime image
- Verify that
whichandisexeare both present if used by your adapter
- Connection closed immediately in stdio
- Typical cause: stdout pollution (non-JSON output)
- Fix: Ensure no console.log/console.error on startup; use the provided stdio-silencer preloader
- Increase logging:
- Set your server logs to debug level (via CLI args or env)
- Run
DEBUG=mcp:*in your client environment if supported
- Verify adapter presence:
npm ls @debugmcp/adapter-*- Call
list_supported_languagestool to see what the server reports. Note that this tool merges the hardcoded known-adapter catalog with actually registered factories, and probes each catalog entry with a real dynamic import to report accurateinstalled: true/falsestatus.
- Use the diagnostic client:
node scripts/diagnose-stdio-client.mjsverifies connect → list → create → close flow
flowchart TD
A[Adapter not loading?] --> B{Is package installed?}
B -- No --> C[npm install @debugmcp/adapter-<lang>]
B -- Yes --> D{Factory exported? <Lang>AdapterFactory}
D -- No --> E[Fix export: named class <Lang>AdapterFactory]
D -- Yes --> F{Container stdio clean?}
F -- No --> G[Enable stdio silencer; remove console output]
F -- Yes --> H[Check /app/logs/stdout-raw.log & stdin-raw.log]
H --> I{Errors present?}
I -- Yes --> J[Address specific error; rebuild image]
I -- No --> K[Call list_supported_languages to verify discovery]
- Package name:
@debugmcp/adapter-<language> - Named export required:
// dist/index.js (compiled) export { <Language>AdapterFactory } from './<Language>AdapterFactory.js';
- Class name must match
<CapitalizedLanguage>AdapterFactory(the loader looks for this named export)
Failed to load adapter for 'python' from package '@debugmcp/adapter-python'. Adapter not installed. Install with: npm install @debugmcp/adapter-pythonFailed to load adapter for 'python' from package '@debugmcp/adapter-python'. Error: Factory class PythonAdapterFactory not found in @debugmcp/adapter-python.