# Dependency Injection Pattern in MCP Debug Server
This document explains how the MCP Debug Server implements dependency injection to achieve testability, flexibility, and maintainability.
## Overview
The dependency injection (DI) pattern is used throughout the codebase to:
- Enable comprehensive unit testing without real external dependencies
- Allow swapping implementations (e.g., for different platforms)
- Make dependencies explicit and documented
- Facilitate modular architecture
## Core Principles
### 1. Constructor Injection
All dependencies are injected through constructors, making them explicit and immutable.
### 2. Interface Segregation
Dependencies are defined as focused interfaces, not concrete implementations.
### 3. Dependency Inversion
High-level modules depend on abstractions, not concrete implementations.
## Implementation Examples
### SessionManager Dependency Injection
**Location**: `src/session/session-manager-core.ts`
> **Note**: `SessionManager` is a thin facade class (in `session-manager.ts`) that extends `SessionManagerOperations`, which extends `SessionManagerCore`. The dependency injection and core logic live in `SessionManagerCore`.
```typescript
// Define dependencies interface
export interface SessionManagerDependencies {
fileSystem: IFileSystem;
networkManager: INetworkManager;
logger: ILogger;
proxyManagerFactory: IProxyManagerFactory;
sessionStoreFactory: ISessionStoreFactory;
debugTargetLauncher: IDebugTargetLauncher;
environment: IEnvironment;
adapterRegistry: IAdapterRegistry;
}
// Constructor injection (in SessionManagerCore)
constructor(
config: SessionManagerConfig,
dependencies: SessionManagerDependencies
) {
this.logger = dependencies.logger;
this.fileSystem = dependencies.fileSystem;
this.networkManager = dependencies.networkManager;
this.environment = dependencies.environment;
this.proxyManagerFactory = dependencies.proxyManagerFactory;
this.sessionStoreFactory = dependencies.sessionStoreFactory;
this.debugTargetLauncher = dependencies.debugTargetLauncher;
this.adapterRegistry = dependencies.adapterRegistry;
// Use injected dependencies
this.sessionStore = this.sessionStoreFactory.create();
this.fileSystem.ensureDirSync(this.logDirBase);
}
```
### ProxyManager Dependency Injection
**Location**: `src/proxy/proxy-manager.ts`
```typescript
export class ProxyManager extends EventEmitter implements IProxyManager {
constructor(
private adapter: IDebugAdapter | null, // Optional adapter for language-agnostic support
private proxyProcessLauncher: IProxyProcessLauncher,
private fileSystem: IFileSystem,
private logger: ILogger
) {
super();
}
}
```
Benefits:
- All dependencies are visible in the constructor signature
- Easy to create test instances with mock dependencies
- No hidden dependencies or global state
### Factory Pattern for Complex Dependencies
**Location**: `src/factories/proxy-manager-factory.ts`
```typescript
export interface IProxyManagerFactory {
create(adapter?: IDebugAdapter): IProxyManager;
}
export class ProxyManagerFactory implements IProxyManagerFactory {
constructor(
private proxyProcessLauncher: IProxyProcessLauncher,
private fileSystem: IFileSystem,
private logger: ILogger
) {}
create(adapter?: IDebugAdapter): IProxyManager {
return new ProxyManager(
adapter || null, // Pass adapter or null if not provided
this.proxyProcessLauncher,
this.fileSystem,
this.logger
);
}
}
```
This factory pattern allows SessionManager to create ProxyManager instances without knowing their dependencies.
## Interface Definitions
### Core External Dependencies
**Location**: `src/interfaces/external-dependencies.ts`
```typescript
// File system operations
export interface IFileSystem {
readFile(path: string, encoding?: BufferEncoding): Promise<string>;
writeFile(path: string, data: string | Buffer): Promise<void>;
exists(path: string): Promise<boolean>;
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
ensureDir(path: string): Promise<void>;
ensureDirSync(path: string): void;
pathExists(path: string): Promise<boolean>;
// ... more methods
}
// Process management
export interface IProcessManager {
spawn(command: string, args?: string[], options?: SpawnOptions): IChildProcess;
exec(command: string): Promise<{ stdout: string; stderr: string }>;
}
// Network operations
export interface INetworkManager {
createServer(): IServer;
findFreePort(): Promise<number>;
}
// Logging
export interface ILogger {
info(message: string, meta?: any): void;
error(message: string, meta?: any): void;
debug(message: string, meta?: any): void;
warn(message: string, meta?: any): void;
}
```
### Process-Specific Interfaces
**Location**: `src/interfaces/process-interfaces.ts`
```typescript
export interface IProxyProcess extends IProcess {
sessionId: string;
sendCommand(command: object): void;
waitForInitialization(timeout?: number): Promise<void>;
}
export interface IProxyProcessLauncher {
launchProxy(
scriptPath: string,
sessionId: string,
env?: NodeJS.ProcessEnv
): IProxyProcess;
}
```
## Real-World Usage
### Production Container Configuration
**Location**: `src/container/dependencies.ts`
```typescript
import { FileSystemImpl, ProcessManagerImpl, NetworkManagerImpl, ... } from '../implementations/index.js';
import { createLogger } from '../utils/logger.js';
export function createProductionDependencies(config: ContainerConfig = {}): Dependencies {
const logger = createLogger('debug-mcp', { level: config.logLevel, ... });
const environment = new ProcessEnvironment();
const fileSystem = new FileSystemImpl();
const processManager = new ProcessManagerImpl();
const networkManager = new NetworkManagerImpl();
// Process launchers
const processLauncher = new ProcessLauncherImpl(processManager);
const proxyProcessLauncher = new ProxyProcessLauncherImpl(processLauncher, processManager);
const debugTargetLauncher = new DebugTargetLauncherImpl(processLauncher, networkManager);
// Factories
const proxyManagerFactory = new ProxyManagerFactory(proxyProcessLauncher, fileSystem, logger);
const sessionStoreFactory = new SessionStoreFactory();
// Adapter registry (with dynamic loading enabled)
const adapterRegistry = new AdapterRegistry({ validateOnRegister: false, ... });
return {
fileSystem, processManager, networkManager, logger, environment,
processLauncher, proxyProcessLauncher, debugTargetLauncher,
proxyManagerFactory, sessionStoreFactory, adapterRegistry
};
}
```
### Test Container Configuration
**Location**: `tests/test-utils/helpers/test-dependencies.ts`
```typescript
export async function createTestDependencies(): Promise<Dependencies> {
const logger = createMockLogger();
const fileSystem = createMockFileSystem();
const processManager = createMockProcessManager();
const networkManager = createMockNetworkManager();
const processLauncher = new FakeProcessLauncher();
const proxyProcessLauncher = new FakeProxyProcessLauncher();
const debugTargetLauncher = new FakeDebugTargetLauncher();
const proxyManagerFactory = new MockProxyManagerFactory();
proxyManagerFactory.createFn = () => new MockProxyManager();
const sessionStoreFactory = new MockSessionStoreFactory();
return {
fileSystem, processManager, networkManager, logger,
processLauncher, proxyProcessLauncher, debugTargetLauncher,
proxyManagerFactory, sessionStoreFactory
};
}
// There is also a synchronous helper for SessionManager-specific tests:
export function createMockSessionManagerDependencies(): SessionManagerDependencies {
return {
fileSystem: createMockFileSystem(),
networkManager: createMockNetworkManager(),
logger: createMockLogger(),
proxyManagerFactory: new MockProxyManagerFactory(),
sessionStoreFactory: new MockSessionStoreFactory(),
debugTargetLauncher: createMockDebugTargetLauncher(),
environment: createMockEnvironment(),
adapterRegistry: createMockAdapterRegistry()
};
}
export function createMockFileSystem(): IFileSystem {
return {
readFile: vi.fn(),
writeFile: vi.fn(),
exists: vi.fn(),
existsSync: vi.fn(),
mkdir: vi.fn(),
readdir: vi.fn(),
stat: vi.fn(),
unlink: vi.fn(),
rmdir: vi.fn(),
ensureDir: vi.fn(),
ensureDirSync: vi.fn(),
pathExists: vi.fn(),
remove: vi.fn(),
copy: vi.fn(),
outputFile: vi.fn()
};
}
```
## Testing Benefits
### Example: Testing SessionManager
**Location**: `tests/unit/session/session-manager.test.ts`
```typescript
describe('SessionManager', () => {
let sessionManager: SessionManager;
let mockDependencies: SessionManagerDependencies;
beforeEach(() => {
// Create all mock dependencies
mockDependencies = {
fileSystem: createMockFileSystem(),
networkManager: createMockNetworkManager(),
logger: createMockLogger(),
proxyManagerFactory: createMockProxyManagerFactory(),
sessionStoreFactory: createMockSessionStoreFactory(),
debugTargetLauncher: createMockDebugTargetLauncher()
};
// Create SessionManager with mocks
sessionManager = new SessionManager(
{ logDirBase: '/tmp/test' },
mockDependencies
);
});
it('should create session directory on initialization', () => {
expect(mockDependencies.fileSystem.ensureDirSync)
.toHaveBeenCalledWith('/tmp/test');
});
it('should use network manager to find free port', async () => {
vi.mocked(mockDependencies.networkManager.findFreePort)
.mockResolvedValue(5678);
// Test will use mocked port
// ... rest of test
});
});
```
### Example: Testing with Fake Implementations
**Location**: `tests/unit/proxy/proxy-manager-lifecycle.test.ts`
```typescript
describe('ProxyManager', () => {
let proxyManager: ProxyManager;
let fakeLauncher: FakeProxyProcessLauncher;
beforeEach(() => {
// Use fake implementation instead of mock
fakeLauncher = new FakeProxyProcessLauncher();
proxyManager = new ProxyManager(
fakeLauncher, // Fake implementation
createMockFileSystem(), // Mock
createMockLogger() // Mock
);
});
it('should handle proxy messages', async () => {
// Prepare fake to simulate behavior
fakeLauncher.prepareProxy((proxy) => {
setTimeout(() => {
proxy.simulateMessage({
type: 'status',
status: 'initialized'
});
}, 100);
});
// Test uses fake behavior
await proxyManager.start(config);
// ... assertions
});
});
```
## Advanced Patterns
### Partial Dependencies
For gradual migration or optional features:
```typescript
export type PartialDependencies = Partial<IDependencies>;
export function createComponentWithDefaults(
deps: PartialDependencies
): Component {
const fullDeps = {
...createDefaultDependencies(),
...deps
};
return new Component(fullDeps as IDependencies);
}
```
### Dependency Validation
Ensure required dependencies are provided:
```typescript
constructor(dependencies: SessionManagerDependencies) {
// Validate required dependencies
if (!dependencies.logger) {
throw new Error('Logger is required');
}
if (!dependencies.fileSystem) {
throw new Error('FileSystem is required');
}
// Assign after validation
this.logger = dependencies.logger;
this.fileSystem = dependencies.fileSystem;
}
```
### Lazy Dependency Creation
For expensive dependencies:
```typescript
export class LazyProxyManagerFactory implements IProxyManagerFactory {
private instance?: IProxyManager;
create(): IProxyManager {
if (!this.instance) {
this.instance = new ProxyManager(
this.launcher,
this.fileSystem,
this.logger
);
}
return this.instance;
}
}
```
## Best Practices
1. **Define Interfaces First** - Start with the interface, not the implementation
2. **Keep Interfaces Focused** - Follow Interface Segregation Principle
3. **Use Constructor Injection** - Make dependencies explicit
4. **Avoid Service Locators** - Don't hide dependencies
5. **Create Factories for Complex Objects** - When objects need runtime parameters
6. **Test with Mocks/Fakes** - Never use real external dependencies in unit tests
7. **Document Dependencies** - Make it clear what each dependency provides
## Anti-Patterns to Avoid
### ❌ Hidden Dependencies
```typescript
// Bad - hidden dependency on global
class BadComponent {
doSomething() {
const logger = getGlobalLogger(); // Hidden dependency!
logger.info('doing something');
}
}
```
### ❌ Property Injection
```typescript
// Bad - dependencies can be changed after construction
class BadComponent {
logger?: ILogger; // Can be undefined!
doSomething() {
this.logger?.info('maybe works?');
}
}
```
### ❌ Concrete Dependencies
```typescript
// Bad - depends on concrete implementation
import { WinstonLogger } from 'winston';
class BadComponent {
constructor(private logger: WinstonLogger) {} // Tied to Winston!
}
```
## Summary
The dependency injection pattern in MCP Debug Server:
- Enables 90%+ test coverage by making everything testable
- Provides flexibility to swap implementations
- Makes the codebase more maintainable
- Documents component relationships explicitly
By following these patterns, the codebase remains modular, testable, and easy to understand.