@lanonasis/cli
Version:
Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security
369 lines (368 loc) • 13.8 kB
JavaScript
/**
* Connection Manager Implementation
*
* Manages MCP server discovery, configuration, and connection lifecycle
* Implementation of the ConnectionManager interface.
*/
import { promises as fs, createWriteStream } from 'fs';
import { join, dirname, resolve } from 'path';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
/**
* Default MCP configuration
*/
const DEFAULT_MCP_CONFIG = {
localServerPath: '',
serverPort: 3000,
autoStart: true,
connectionTimeout: 10000,
retryAttempts: 3,
logLevel: 'info',
};
/**
* ConnectionManagerImpl manages MCP server discovery, configuration, and connection lifecycle
*
* This implementation automatically detects the embedded MCP server location within the CLI package,
* generates configuration files with correct server paths, and manages server processes.
*/
export class ConnectionManagerImpl {
config;
connectionStatus;
serverProcess = null;
configPath;
constructor(configPath) {
this.config = { ...DEFAULT_MCP_CONFIG };
this.configPath = configPath || join(process.cwd(), '.lanonasis', 'mcp-config.json');
this.connectionStatus = {
isConnected: false,
connectionAttempts: 0,
};
}
/**
* Initialize the connection manager by loading persisted configuration
*/
async init() {
await this.loadConfig();
}
/**
* Connect to the local embedded MCP server
*/
async connectLocal() {
try {
// Load persisted configuration first
await this.loadConfig();
// Prefer a configured path only if the file exists (ignore stale machine-specific commits)
const configuredPath = this.config.localServerPath?.trim();
let serverPath = null;
if (configuredPath) {
try {
await fs.access(configuredPath);
serverPath = configuredPath;
}
catch {
serverPath = await this.detectServerPath();
}
}
else {
serverPath = await this.detectServerPath();
}
if (!serverPath) {
return {
success: false,
error: 'Could not detect local MCP server path',
suggestions: [
'Ensure the CLI package is properly installed',
'Check that the MCP server files are present',
'Try running: lanonasis init',
],
};
}
// Update configuration with detected path
this.config.localServerPath = serverPath;
await this.saveConfig();
// Start the server if not already running
if (!this.isServerRunning()) {
const serverInstance = await this.startLocalServer();
this.connectionStatus.serverInstance = serverInstance;
}
// Verify the connection
const isConnected = await this.verifyConnection(serverPath);
if (isConnected) {
this.connectionStatus.isConnected = true;
this.connectionStatus.lastConnected = new Date();
this.connectionStatus.connectionAttempts++;
return {
success: true,
serverPath,
};
}
else {
this.connectionStatus.connectionAttempts++;
return {
success: false,
error: 'Failed to verify MCP server connection',
suggestions: [
'Check server logs for errors',
'Ensure no other process is using the port',
'Try restarting the CLI',
],
};
}
}
catch (error) {
this.connectionStatus.connectionAttempts++;
this.connectionStatus.lastError = error instanceof Error ? error.message : String(error);
return {
success: false,
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
suggestions: [
'Check your network connection',
'Verify the MCP server is installed correctly',
'Try running: lanonasis mcp diagnose',
],
};
}
}
/**
* Automatically configure the local MCP server with correct paths
*/
async autoConfigureLocalServer() {
try {
const serverPath = await this.detectServerPath();
if (!serverPath) {
return {
success: false,
error: 'Could not detect MCP server path for auto-configuration',
};
}
// Update configuration
this.config.localServerPath = serverPath;
// Ensure config directory exists
await fs.mkdir(dirname(this.configPath), { recursive: true });
// Save configuration
await this.saveConfig();
return {
success: true,
configPath: this.configPath,
serverPath,
};
}
catch (error) {
return {
success: false,
error: `Auto-configuration failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/**
* Detect the embedded MCP server path within the CLI package
*/
async detectServerPath() {
try {
// Get the current module directory
const currentDir = dirname(fileURLToPath(import.meta.url));
// Common paths to check for the MCP server
const candidatePaths = [
// Relative to current CLI source
resolve(currentDir, '../../mcp-server-entry.js'),
resolve(currentDir, '../../../dist/mcp-server-entry.js'),
// Relative to CLI package root
resolve(currentDir, '../../../mcp-server-entry.js'),
resolve(currentDir, '../../dist/mcp-server-entry.js'),
// In node_modules (if installed as dependency)
resolve(process.cwd(), 'node_modules/@lanonasis/cli/dist/mcp-server-entry.js'),
// Global installation paths
resolve(process.cwd(), 'dist/mcp-server-entry.js'),
// Development paths
resolve(process.cwd(), 'cli/dist/mcp-server-entry.js'),
resolve(process.cwd(), '../cli/dist/mcp-server-entry.js'),
];
// Check each candidate path
for (const candidatePath of candidatePaths) {
try {
await fs.access(candidatePath);
// Verify it's actually the MCP server by checking file content
const content = await fs.readFile(candidatePath, 'utf-8');
if (content.includes('mcp') || content.includes('server')) {
return candidatePath;
}
}
catch {
// Path doesn't exist or isn't accessible, continue
}
}
return null;
}
catch (error) {
console.error('Error detecting server path:', error);
return null;
}
}
/**
* Start the local MCP server process
*/
async startLocalServer() {
if (!this.config.localServerPath) {
throw new Error('No local server path configured');
}
return new Promise((resolve, reject) => {
const serverProcess = spawn('node', [this.config.localServerPath], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
PORT: this.config.serverPort?.toString() || '3000',
LOG_LEVEL: this.config.logLevel,
},
});
if (serverProcess.pid === undefined) {
reject(new Error('Failed to start local MCP server: process ID was not assigned'));
return;
}
const serverInstance = {
pid: serverProcess.pid,
port: this.config.serverPort || 3000,
status: 'starting',
startTime: new Date(),
logPath: join(dirname(this.configPath), 'mcp-server.log'),
};
// Set up logging
const logStream = createWriteStream(serverInstance.logPath, { flags: 'a' });
serverProcess.stdout?.pipe(logStream);
serverProcess.stderr?.pipe(logStream);
// Handle server startup
const startupTimeout = setTimeout(() => {
serverInstance.status = 'error';
reject(new Error('Server startup timeout'));
}, this.config.connectionTimeout);
serverProcess.on('spawn', () => {
clearTimeout(startupTimeout);
serverInstance.status = 'running';
this.serverProcess = serverProcess;
resolve(serverInstance);
});
serverProcess.on('error', (error) => {
clearTimeout(startupTimeout);
serverInstance.status = 'error';
reject(error);
});
serverProcess.on('exit', (code) => {
serverInstance.status = code === 0 ? 'stopped' : 'error';
this.serverProcess = null;
});
});
}
/**
* Verify that the MCP server connection is working
*/
async verifyConnection(serverPath) {
try {
// Simple verification - check if server path exists and is accessible
await fs.access(serverPath);
// If we have a running server instance, check if it's responsive
if (this.connectionStatus.serverInstance) {
const { status, pid } = this.connectionStatus.serverInstance;
// Explicitly check for error/stopped states
if (status === 'error' || status === 'stopped') {
return false;
}
// Only verify process for running servers
if (status === 'running') {
try {
process.kill(pid, 0); // Signal 0 checks if process exists
return true;
}
catch {
// Process doesn't exist despite status being 'running'
this.connectionStatus.serverInstance.status = 'stopped';
return false;
}
}
// Starting state is not yet ready
if (status === 'starting') {
return false;
}
}
// No server instance means we haven't started it yet
// This is okay for initial connection attempts
return true;
}
catch {
return false;
}
}
/**
* Get the current connection status
*/
getConnectionStatus() {
return { ...this.connectionStatus };
}
/**
* Stop the local MCP server if running
*/
async stopLocalServer() {
if (this.serverProcess) {
return new Promise((resolve) => {
const forceKillTimeout = setTimeout(() => {
if (this.serverProcess) {
this.serverProcess.kill('SIGKILL');
}
}, 5000);
this.serverProcess.once('exit', () => {
clearTimeout(forceKillTimeout);
this.serverProcess = null;
if (this.connectionStatus.serverInstance) {
this.connectionStatus.serverInstance.status = 'stopped';
}
this.connectionStatus.isConnected = false;
resolve();
});
this.serverProcess.kill('SIGTERM');
});
}
}
/**
* Get the current MCP configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update the MCP configuration
*/
async updateConfig(config) {
this.config = { ...this.config, ...config };
await this.saveConfig();
}
/**
* Check if the server is currently running
*/
isServerRunning() {
return (this.serverProcess !== null && this.connectionStatus.serverInstance?.status === 'running');
}
/**
* Save the current configuration to disk
*/
async saveConfig() {
try {
await fs.mkdir(dirname(this.configPath), { recursive: true });
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
}
catch (error) {
throw new Error(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load configuration from disk
*/
async loadConfig() {
try {
const configData = await fs.readFile(this.configPath, 'utf-8');
const loadedConfig = JSON.parse(configData);
this.config = { ...DEFAULT_MCP_CONFIG, ...loadedConfig };
}
catch {
// Config file doesn't exist or is invalid, use defaults
this.config = { ...DEFAULT_MCP_CONFIG };
}
}
}