@debugmcp/mcp-debugger
Version:
Step-through debugging MCP server for LLMs
347 lines • 12.4 kB
JavaScript
/**
* Implementation of the Adapter Registry for managing debug adapters
*
* @since 2.0.0
*/
import { EventEmitter } from 'events';
import { AdapterNotFoundError, DuplicateRegistrationError, FactoryValidationError } from '@debugmcp/shared';
import { AdapterLoader } from './adapter-loader.js';
/**
* Default registry configuration
*/
const DEFAULT_CONFIG = {
validateOnRegister: true,
allowOverride: false,
maxInstancesPerLanguage: 10,
autoDispose: true,
autoDisposeTimeout: 300000, // 5 minutes
};
/**
* Implementation of the adapter registry
*/
export class AdapterRegistry extends EventEmitter {
factories = new Map();
activeAdapters = new Map();
config;
disposeTimers = new Map();
loader = new AdapterLoader();
// Dynamic loading is opt-in via constructor config to preserve backward compatibility in unit tests
dynamicEnabled;
constructor(config = {}) {
super();
this.config = { ...DEFAULT_CONFIG, ...config };
// Enable dynamic loading only when explicitly requested (default false to keep legacy behavior in tests)
this.dynamicEnabled = Boolean(config?.enableDynamicLoading ??
(process.env.MCP_CONTAINER === 'true'));
}
/**
* Register a new adapter factory for a language
*/
async register(language, factory) {
// Check for duplicate registration
if (this.factories.has(language) && !this.config.allowOverride) {
throw new DuplicateRegistrationError(language);
}
// Validate factory if configured
if (this.config.validateOnRegister) {
const validationResult = await factory.validate();
if (!validationResult.valid) {
throw new FactoryValidationError(language, validationResult);
}
}
// Register the factory
this.factories.set(language, factory);
this.emit('factoryRegistered', language, factory.getMetadata());
}
/**
* Unregister an adapter factory
*/
unregister(language) {
const factory = this.factories.get(language);
if (!factory) {
return false;
}
// Dispose all active adapters for this language
const activeSet = this.activeAdapters.get(language);
if (activeSet) {
for (const adapter of activeSet) {
adapter.dispose().catch(err => {
this.emit('error', new Error(`Failed to dispose adapter: ${err.message}`));
});
this.clearDisposeTimer(adapter);
}
this.activeAdapters.delete(language);
}
// Remove the factory
this.factories.delete(language);
this.emit('factoryUnregistered', language);
return true;
}
/**
* Create a new adapter instance for the specified language
*/
async create(language, config) {
let factory = this.factories.get(language);
if (!factory) {
if (this.dynamicEnabled) {
try {
const loadedFactory = await this.loader.loadAdapter(language);
// Register but also use the loadedFactory directly to avoid undefined from map lookup
await this.register(language, loadedFactory);
factory = loadedFactory;
}
catch {
// If load fails, include dynamically detected languages (installed ones)
const available = await this.listLanguages().catch(() => this.getSupportedLanguages());
throw new AdapterNotFoundError(language, available);
}
}
else {
// Legacy behavior: not dynamically loading -> throw not found using registered languages only
throw new AdapterNotFoundError(language, this.getSupportedLanguages());
}
}
// Check instance limit
const activeSet = this.activeAdapters.get(language) || new Set();
if (activeSet.size >= this.config.maxInstancesPerLanguage) {
throw new Error(`Maximum adapter instances (${this.config.maxInstancesPerLanguage}) reached for language: ${language}`);
}
// Create dependencies for the adapter
const dependencies = await this.createDependencies(config);
// Create the adapter
const adapter = factory.createAdapter(dependencies);
// Initialize the adapter
await adapter.initialize();
// Track the active adapter
if (!this.activeAdapters.has(language)) {
this.activeAdapters.set(language, new Set());
}
this.activeAdapters.get(language).add(adapter);
// Set up auto-dispose if configured
if (this.config.autoDispose) {
this.setupAutoDispose(language, adapter);
}
// Listen for adapter disposal
adapter.once('disposed', () => {
const set = this.activeAdapters.get(language);
if (set) {
set.delete(adapter);
if (set.size === 0) {
this.activeAdapters.delete(language);
}
}
});
this.emit('adapterCreated', language, adapter);
return adapter;
}
/**
* Get list of all supported languages
*/
getSupportedLanguages() {
return Array.from(this.factories.keys());
}
/**
* Check if a language is supported
*/
isLanguageSupported(language) {
return this.factories.has(language);
}
/**
* Get metadata about a registered adapter
*/
getAdapterInfo(language) {
const factory = this.factories.get(language);
if (!factory) {
return undefined;
}
const metadata = factory.getMetadata();
const activeSet = this.activeAdapters.get(language);
return {
...metadata,
language,
available: true,
activeInstances: activeSet?.size || 0,
registeredAt: new Date(), // In a real implementation, track this
};
}
/**
* Get all registered adapter information
*/
getAllAdapterInfo() {
const result = new Map();
for (const [language] of this.factories) {
const info = this.getAdapterInfo(language);
if (info) {
result.set(language, info);
}
}
return result;
}
/**
* List languages that are actually installed and available via dynamic loader
*/
async listLanguages() {
const registered = this.getSupportedLanguages();
if (!this.dynamicEnabled) {
// Without dynamic loading, advertise the statically registered adapters.
return registered;
}
const installed = new Set();
try {
const adapters = await this.loader.listAvailableAdapters();
for (const adapter of adapters) {
if (adapter.installed) {
installed.add(adapter.name);
}
}
}
catch {
// Ignore loader errors in bundled environments where adapters are embedded.
}
// Always include statically registered adapters so bundled builds expose them.
for (const language of registered) {
installed.add(language);
}
return Array.from(installed);
}
/**
* List detailed adapter metadata (known + install status)
*/
async listAvailableAdapters() {
const registered = new Set(this.getSupportedLanguages());
const buildEntry = (language) => ({
name: language,
packageName: `/adapter-${language}`,
description: undefined,
installed: true
});
if (!this.dynamicEnabled) {
// Provide minimal metadata from registered factories
return Array.from(registered).map(buildEntry);
}
const results = new Map();
try {
const adapters = await this.loader.listAvailableAdapters();
for (const adapter of adapters) {
const installed = registered.has(adapter.name) ? true : adapter.installed;
results.set(adapter.name, { ...adapter, installed });
registered.delete(adapter.name);
}
}
catch {
// Ignore loader failures and fall back to registered adapters.
}
for (const language of registered) {
results.set(language, buildEntry(language));
}
return Array.from(results.values());
}
/**
* Dispose all created adapters and clear registry
*/
async disposeAll() {
const disposePromises = [];
// Dispose all active adapters
for (const [language, activeSet] of this.activeAdapters) {
for (const adapter of activeSet) {
disposePromises.push(adapter.dispose().catch(err => {
this.emit('error', new Error(`Failed to dispose adapter for ${language}: ${err.message}`));
}));
}
}
// Clear all dispose timers
for (const timer of this.disposeTimers.values()) {
clearTimeout(timer);
}
this.disposeTimers.clear();
// Wait for all disposals to complete
await Promise.all(disposePromises);
// Clear all tracking
this.activeAdapters.clear();
this.factories.clear();
this.emit('registryDisposed');
}
/**
* Get count of active adapter instances
*/
getActiveAdapterCount() {
let count = 0;
for (const activeSet of this.activeAdapters.values()) {
count += activeSet.size;
}
return count;
}
/**
* Create dependencies for adapter creation
*/
async createDependencies(config) {
// In a real implementation, these would be injected or created from a container
// For now, we'll import the implementations directly
const { createProductionDependencies } = await import('../container/dependencies.js');
const deps = createProductionDependencies({
logLevel: 'debug',
logFile: `${config.logDir}/${config.sessionId}.log`
});
return {
fileSystem: deps.fileSystem,
logger: deps.logger,
environment: deps.environment,
processLauncher: deps.processLauncher,
networkManager: deps.networkManager,
};
}
/**
* Set up auto-dispose for an adapter
*/
setupAutoDispose(language, adapter) {
this.clearDisposeTimer(adapter);
// Listen for adapter state changes
adapter.on('stateChanged', (oldState, newState) => {
if (newState === 'disconnected' || newState === 'error') {
// Start dispose timer
const timer = setTimeout(() => {
adapter.dispose().catch(err => {
this.emit('error', new Error(`Auto-dispose failed: ${err.message}`));
});
}, this.config.autoDisposeTimeout);
this.disposeTimers.set(adapter, timer);
}
else if (newState === 'connected' || newState === 'debugging') {
// Cancel dispose timer if adapter becomes active again
this.clearDisposeTimer(adapter);
}
});
}
clearDisposeTimer(adapter) {
const timer = this.disposeTimers.get(adapter);
if (timer) {
clearTimeout(timer);
this.disposeTimers.delete(adapter);
}
}
}
/**
* Create a singleton instance of the adapter registry
*/
let registryInstance = null;
/**
* Get the singleton adapter registry instance
*/
export function getAdapterRegistry(config) {
if (!registryInstance) {
registryInstance = new AdapterRegistry(config);
}
return registryInstance;
}
/**
* Reset the singleton instance (mainly for testing)
*/
export function resetAdapterRegistry() {
if (registryInstance) {
registryInstance.disposeAll().catch(() => {
// Ignore errors during reset
});
registryInstance = null;
}
}
//# sourceMappingURL=adapter-registry.js.map