@lanonasis/cli
Version:
Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security
435 lines (434 loc) • 13 kB
JavaScript
/**
* MCP Transport Manager
* Handles multiple transport types for MCP connections
*/
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { EventEmitter } from 'events';
import WebSocket from 'ws';
import chalk from 'chalk';
/**
* StdIO Transport wrapper
*/
class StdioTransport extends EventEmitter {
transport;
connected = false;
constructor(config) {
super();
if (!config.command) {
throw new Error('Command required for stdio transport');
}
this.transport = new StdioClientTransport({
command: config.command,
args: config.args || []
});
this.connected = true;
}
async send(data) {
if (!this.connected) {
throw new Error('Transport not connected');
}
// StdioClientTransport handles this internally
this.emit('send', data);
}
async close() {
this.connected = false;
this.removeAllListeners();
}
isConnected() {
return this.connected;
}
getInternalTransport() {
return this.transport;
}
}
/**
* HTTP Transport wrapper
*/
class HttpTransport extends EventEmitter {
url;
headers;
connected = false;
auth;
constructor(config) {
super();
if (!config.url) {
throw new Error('URL required for HTTP transport');
}
this.url = config.url;
this.headers = config.headers || {};
this.auth = config.auth;
if (this.auth) {
this.setupAuthentication();
}
this.connected = true;
}
setupAuthentication() {
if (!this.auth)
return;
switch (this.auth.type) {
case 'bearer':
this.headers['Authorization'] = `Bearer ${this.auth.value}`;
break;
case 'apikey':
this.headers['X-API-Key'] = this.auth.value;
break;
case 'basic':
this.headers['Authorization'] = `Basic ${this.auth.value}`;
break;
}
}
async send(data) {
if (!this.connected) {
throw new Error('Transport not connected');
}
try {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.headers
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}
const result = await response.json();
this.emit('message', result);
}
catch (error) {
this.emit('error', error);
throw error;
}
}
async close() {
this.connected = false;
this.removeAllListeners();
}
isConnected() {
return this.connected;
}
}
/**
* WebSocket Transport wrapper
*/
class WebSocketTransport extends EventEmitter {
ws = null;
url;
connected = false;
reconnectConfig;
reconnectAttempts = 0;
reconnectTimer;
headers;
constructor(config) {
super();
if (!config.url) {
throw new Error('URL required for WebSocket transport');
}
this.url = config.url;
this.reconnectConfig = config.reconnect;
this.headers = config.headers || {};
if (config.auth) {
this.setupAuthentication(config.auth);
}
}
setupAuthentication(auth) {
if (!auth)
return;
switch (auth.type) {
case 'bearer':
this.headers['Authorization'] = `Bearer ${auth.value}`;
break;
case 'apikey':
this.headers['X-API-Key'] = auth.value;
break;
}
}
async connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url, {
headers: this.headers
});
this.ws.on('open', () => {
this.connected = true;
this.reconnectAttempts = 0;
this.emit('connected');
console.log(chalk.green(`✅ WebSocket connected to ${this.url}`));
resolve();
});
this.ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.emit('message', message);
}
catch {
this.emit('error', new Error('Failed to parse WebSocket message'));
}
});
this.ws.on('error', (error) => {
this.emit('error', error);
if (!this.connected) {
reject(error);
}
});
this.ws.on('close', (code, reason) => {
this.connected = false;
this.emit('disconnected', { code, reason: reason.toString() });
if (this.shouldReconnect()) {
this.scheduleReconnect();
}
});
// Set connection timeout
setTimeout(() => {
if (!this.connected) {
reject(new Error('WebSocket connection timeout'));
}
}, 10000);
}
catch (error) {
reject(error);
}
});
}
shouldReconnect() {
if (!this.reconnectConfig?.enabled)
return false;
const maxAttempts = this.reconnectConfig.maxAttempts || 5;
return this.reconnectAttempts < maxAttempts;
}
scheduleReconnect() {
const delay = this.reconnectConfig?.delay || 5000;
const backoff = Math.min(delay * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
console.log(chalk.yellow(`⏳ Reconnecting WebSocket in ${backoff}ms (attempt ${this.reconnectAttempts})...`));
this.reconnectTimer = setTimeout(() => {
this.connect().catch(error => {
console.error(chalk.red('Reconnection failed:'), error);
});
}, backoff);
}
async send(data) {
if (!this.connected || !this.ws) {
throw new Error('WebSocket not connected');
}
return new Promise((resolve, reject) => {
this.ws.send(JSON.stringify(data), (error) => {
if (error) {
reject(error);
}
else {
resolve();
}
});
});
}
async close() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.connected = false;
this.removeAllListeners();
}
isConnected() {
return this.connected;
}
}
/**
* Server-Sent Events Transport
*/
class SSETransport extends EventEmitter {
eventSource = null;
url;
connected = false;
headers;
constructor(config) {
super();
if (!config.url) {
throw new Error('URL required for SSE transport');
}
this.url = config.url;
this.headers = config.headers || {};
}
async connect() {
// Dynamic import for EventSource
const { EventSource } = await import('eventsource');
// EventSource doesn't support headers directly
this.eventSource = new EventSource(this.url);
return new Promise((resolve, reject) => {
this.eventSource.onopen = () => {
this.connected = true;
this.emit('connected');
resolve();
};
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit('message', data);
}
catch {
this.emit('error', new Error('Failed to parse SSE message'));
}
};
this.eventSource.onerror = (error) => {
this.emit('error', error);
if (!this.connected) {
reject(error);
}
};
});
}
async send(_data) {
// SSE is receive-only, but we can send data via HTTP POST
throw new Error('SSE transport is read-only. Use HTTP for sending data.');
}
async close() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.connected = false;
this.removeAllListeners();
}
isConnected() {
return this.connected;
}
}
/**
* Transport Manager class
*/
export class MCPTransportManager {
transports = new Map();
configs = new Map();
/**
* Create a transport based on configuration
*/
async createTransport(name, config) {
// Store config for potential reconnection
this.configs.set(name, config);
let transport;
switch (config.type) {
case 'stdio':
transport = new StdioTransport(config);
break;
case 'http':
transport = new HttpTransport(config);
break;
case 'websocket':
transport = new WebSocketTransport(config);
await transport.connect();
break;
case 'sse':
transport = new SSETransport(config);
await transport.connect();
break;
default:
throw new Error(`Unsupported transport type: ${config.type}`);
}
this.transports.set(name, transport);
// Setup event forwarding
this.setupEventForwarding(name, transport);
return transport;
}
/**
* Setup event forwarding for monitoring
*/
setupEventForwarding(name, transport) {
transport.on('connected', () => {
console.log(chalk.green(`✅ Transport '${name}' connected`));
});
transport.on('disconnected', (reason) => {
console.log(chalk.yellow(`⚠️ Transport '${name}' disconnected:`, reason));
});
transport.on('error', (error) => {
console.log(chalk.red(`❌ Transport '${name}' error:`, error.message));
});
}
/**
* Get a transport by name
*/
getTransport(name) {
return this.transports.get(name);
}
/**
* Create stdio transport helper
*/
async createStdioTransport(command, args = []) {
const transport = new StdioTransport({
type: 'stdio',
command,
args
});
return transport.getInternalTransport();
}
/**
* Create HTTP transport helper
*/
async createHttpTransport(url, headers, auth) {
return this.createTransport('http-default', {
type: 'http',
url,
headers,
auth
});
}
/**
* Create WebSocket transport helper
*/
async createWebSocketTransport(url, options) {
return this.createTransport('websocket-default', {
type: 'websocket',
url,
...options
});
}
/**
* Close all transports
*/
async closeAll() {
const closePromises = Array.from(this.transports.values()).map(transport => transport.close());
await Promise.all(closePromises);
this.transports.clear();
this.configs.clear();
}
/**
* Close a specific transport
*/
async closeTransport(name) {
const transport = this.transports.get(name);
if (transport) {
await transport.close();
this.transports.delete(name);
this.configs.delete(name);
}
}
/**
* Get all transport statuses
*/
getStatuses() {
const statuses = {};
this.transports.forEach((transport, name) => {
statuses[name] = transport.isConnected();
});
return statuses;
}
/**
* Reconnect a transport
*/
async reconnect(name) {
const config = this.configs.get(name);
if (!config) {
throw new Error(`No configuration found for transport: ${name}`);
}
// Close existing if any
await this.closeTransport(name);
// Create new transport
return this.createTransport(name, config);
}
}
// Export singleton instance
export const transportManager = new MCPTransportManager();