reloaderoo
Version:
Hot-reload your MCP servers without restarting your AI coding assistant. Works excellently with VSCode MCP, well with Claude Code. A transparent development proxy for the Model Context Protocol that enables seamless server restarts during development.
197 lines • 9.79 kB
JavaScript
/**
* Proxy command implementation
*
* Maintains backward compatibility with existing MCP server functionality
*/
import { Command } from 'commander';
import { existsSync, accessSync, constants } from 'fs';
import { resolve, isAbsolute } from 'path';
import { MCPProxy } from '../../mcp-proxy.js';
import { Config, validateCommand, getEnvironmentConfig } from '../../config.js';
import { logger } from '../../mcp-logger.js';
/**
* Validate directory path and check accessibility
*/
function validateDirectory(path, name) {
const absPath = isAbsolute(path) ? path : resolve(path);
if (!existsSync(absPath)) {
return { valid: false, error: `${name} does not exist: ${absPath}` };
}
try {
accessSync(absPath, constants.R_OK | constants.W_OK);
return { valid: true };
}
catch {
return { valid: false, error: `${name} is not readable/writable: ${absPath}` };
}
}
/**
* Format duration for human-readable output
*/
function formatDuration(ms) {
if (ms < 1000)
return `${ms}ms`;
if (ms < 60000)
return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}
/**
* Create the proxy command
*/
export function createProxyCommand() {
const proxy = new Command('proxy')
.description('Run as MCP proxy server (default behavior)')
.usage('[options] -- <child-command> [child-args...]')
.addHelpText('after', `
Examples:
$ reloaderoo proxy -- node server.js
$ reloaderoo proxy --log-level debug -- python mcp_server.py --port 8080
`)
.option('-w, --working-dir <directory>', 'Working directory for the child process', process.cwd())
.option('-l, --log-level <level>', 'Log level (debug, info, notice, warning, error, critical)', 'info')
.option('-f, --log-file <path>', 'Custom log file path (logs to stderr by default)')
.option('-t, --restart-timeout <ms>', 'Timeout for restart operations in milliseconds', '30000')
.option('-m, --max-restarts <number>', 'Maximum number of restart attempts (0-10)', '3')
.option('-d, --restart-delay <ms>', 'Delay between restart attempts in milliseconds', '1000')
.option('-q, --quiet', 'Suppress non-essential output')
.option('--no-auto-restart', 'Disable automatic restart on crashes')
.option('--debug', 'Enable debug mode with verbose logging')
.option('--dry-run', 'Validate configuration without starting proxy')
.action(async (options) => {
try {
// Handle debug mode
if (options.debug) {
options.logLevel = 'debug';
}
// Create configuration
const config = new Config();
// Load environment configuration first
const envConfig = getEnvironmentConfig();
if (Object.keys(envConfig).length > 0 && !options.quiet) {
process.stderr.write('Loaded configuration from environment variables\n');
}
// Parse child command using pass-through syntax (-- child-command [args...])
const dashIndex = process.argv.indexOf('--');
if (dashIndex === -1 || dashIndex >= process.argv.length - 1) {
process.stderr.write('Error: Child command is required\n');
process.stderr.write('Use: reloaderoo proxy [options] -- <command> [args...]\n');
process.stderr.write('Example: reloaderoo proxy -- node server.js\n');
process.stderr.write('Try: reloaderoo proxy --help\n');
process.exit(1);
}
const childCommand = process.argv[dashIndex + 1];
const childArgs = process.argv.slice(dashIndex + 2);
// Validate child command
const cmdValidation = validateCommand(childCommand);
if (!cmdValidation.valid) {
process.stderr.write(`Error: ${cmdValidation.error}\n`);
process.exit(1);
}
// Validate working directory
const dirValidation = validateDirectory(options.workingDir, 'Working directory');
if (!dirValidation.valid) {
process.stderr.write(`Error: ${dirValidation.error}\n`);
process.exit(1);
}
// Validate numeric options
const restartTimeout = parseInt(options.restartTimeout);
if (isNaN(restartTimeout) || restartTimeout < 1000 || restartTimeout > 300000) {
process.stderr.write('Error: --restart-timeout must be between 1000 and 300000\n');
process.exit(1);
}
const maxRestarts = parseInt(options.maxRestarts);
if (isNaN(maxRestarts) || maxRestarts < 0 || maxRestarts > 10) {
process.stderr.write('Error: --max-restarts must be between 0 and 10\n');
process.exit(1);
}
const restartDelay = parseInt(options.restartDelay);
if (isNaN(restartDelay) || restartDelay < 0 || restartDelay > 60000) {
process.stderr.write('Error: --restart-delay must be between 0 and 60000\n');
process.exit(1);
}
// Validate log level
const validLogLevels = ['debug', 'info', 'notice', 'warning', 'error', 'critical'];
if (!validLogLevels.includes(options.logLevel)) {
process.stderr.write(`Error: Invalid log level '${options.logLevel}'\n`);
process.stderr.write(`Valid levels: ${validLogLevels.join(', ')}\n`);
process.exit(1);
}
// Build proxy configuration
const proxyConfig = {
childCommand: cmdValidation.path || childCommand,
childArgs,
workingDirectory: resolve(options.workingDir),
environment: process.env,
restartLimit: maxRestarts,
operationTimeout: restartTimeout,
logLevel: options.logLevel,
autoRestart: options.autoRestart !== false,
restartDelay
};
// Configure logging
logger.setLevel(proxyConfig.logLevel);
// Set custom log file if provided via CLI or environment
const logFile = options.logFile || envConfig.logFile;
if (logFile) {
logger.setLogFile(logFile);
if (!options.quiet) {
process.stderr.write(`Logging to file: ${logFile}\n`);
}
}
// Dry run mode - validate and exit
if (options.dryRun) {
const validation = config.validateConfig(proxyConfig);
if (!options.quiet) {
process.stderr.write('\n=== Configuration Validation ===\n');
process.stderr.write(`Valid: ${validation.valid ? 'Yes' : 'No'}\n`);
if (validation.errors.length > 0) {
process.stderr.write('\nErrors:\n');
validation.errors.forEach(err => process.stderr.write(` - ${err}\n`));
}
if (validation.warnings.length > 0) {
process.stderr.write('\nWarnings:\n');
validation.warnings.forEach(warn => process.stderr.write(` - ${warn}\n`));
}
if (validation.valid) {
process.stderr.write('\nConfiguration:\n');
process.stderr.write(` Child Command: ${proxyConfig.childCommand}\n`);
process.stderr.write(` Child Args: ${proxyConfig.childArgs.join(' ') || '(none)'}\n`);
process.stderr.write(` Working Dir: ${proxyConfig.workingDirectory}\n`);
process.stderr.write(` Log Level: ${proxyConfig.logLevel}\n`);
process.stderr.write(` Auto Restart: ${proxyConfig.autoRestart}\n`);
process.stderr.write(` Max Restarts: ${proxyConfig.restartLimit}\n`);
process.stderr.write(` Restart Delay: ${formatDuration(proxyConfig.restartDelay)}\n`);
process.stderr.write(` Operation Timeout: ${formatDuration(proxyConfig.operationTimeout)}\n`);
}
}
process.exit(validation.valid ? 0 : 1);
}
// Start the proxy
if (!options.quiet) {
process.stderr.write('Starting reloaderoo MCP proxy server...\n');
process.stderr.write(`Child: ${childCommand} ${childArgs.join(' ')}\n`);
process.stderr.write(`Working Directory: ${proxyConfig.workingDirectory}\n`);
process.stderr.write('\n💡 For CLI tools and debugging, use: reloaderoo --help or reloaderoo inspect --help\n');
}
const proxyInstance = new MCPProxy(proxyConfig);
// Handle graceful shutdown
const shutdown = async (signal) => {
if (!options.quiet) {
process.stderr.write(`\nReceived ${signal}, shutting down gracefully...\n`);
}
await proxyInstance.stop();
process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// Start proxy
await proxyInstance.start();
}
catch (error) {
process.stderr.write(`Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
process.exit(1);
}
});
return proxy;
}
//# sourceMappingURL=proxy.js.map