@lanonasis/cli
Version:
Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security
1,601 lines โข 63.1 kB
JavaScript
/**
* Lanonasis MCP Server Implementation
* Provides MCP protocol access to Lanonasis MaaS functionality
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CLIConfig } from '../../utils/config.js';
import { APIClient } from '../../utils/api.js';
import chalk from 'chalk';
export class LanonasisMCPServer {
server;
config;
apiClient;
transport = null;
options;
// Connection pool management
connectionPool = new Map();
maxConnections = 10;
connectionCleanupInterval = null;
// Transport protocol management
supportedTransports = ['stdio', 'websocket', 'http'];
transportFailures = new Map();
enableFallback = true;
constructor(options = {}) {
this.options = options;
// Initialize transport settings
this.enableFallback = options.enableTransportFallback !== false; // Default to true
// Initialize server with metadata
this.server = new Server({
name: options.name || "lanonasis-maas-server",
version: options.version || "3.0.1"
}, {
capabilities: {
tools: {},
resources: {},
prompts: {}
}
});
// Initialize config and API client
this.config = new CLIConfig();
this.apiClient = new APIClient();
// Note: registerTools is now async and called in initialize()
// Setup error handling
this.setupErrorHandling();
}
extractMemoryIdentifier(args) {
const id = typeof args.id === 'string' ? args.id.trim() : '';
const legacyId = typeof args.memory_id === 'string' ? args.memory_id.trim() : '';
const resolved = id || legacyId;
if (!resolved) {
throw new Error('Memory ID is required');
}
return resolved;
}
/**
* Initialize the server
*/
async initialize() {
// Initialize configuration
await this.config.init();
// Override with options if provided
if (this.options.apiUrl) {
await this.config.setApiUrl(this.options.apiUrl);
}
if (this.options.token) {
await this.config.setToken(this.options.token);
}
// Initialize API client with config
const apiUrl = this.config.getApiUrl();
const token = this.config.getToken();
if (apiUrl) {
this.apiClient = new APIClient();
// APIClient will use the config internally
}
// Register tools, resources, and prompts after config is loaded
await this.registerTools();
await this.registerResources();
await this.registerPrompts();
// Start connection cleanup monitoring
this.startConnectionCleanup();
if (this.options.verbose) {
console.log(chalk.cyan('๐ Lanonasis MCP Server initialized'));
console.log(chalk.gray(`API URL: ${apiUrl}`));
console.log(chalk.gray(`Authenticated: ${token ? 'Yes' : 'No'}`));
console.log(chalk.gray(`Max connections: ${this.maxConnections}`));
}
}
/**
* Register MCP tools
*/
async registerTools() {
// Import request schemas dynamically for ES modules
const { ListToolsRequestSchema, CallToolRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// Memory tools
{
name: 'memory_create',
description: 'Create a new memory entry',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Memory title'
},
content: {
type: 'string',
description: 'Memory content'
},
memory_type: {
type: 'string',
enum: ['context', 'reference', 'note'],
default: 'context',
description: 'Type of memory'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Optional tags for the memory'
}
},
required: ['title', 'content']
}
},
{
name: 'memory_search',
description: 'Search memories using semantic search',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
default: 10,
description: 'Maximum number of results'
},
threshold: {
type: 'number',
minimum: 0,
maximum: 1,
default: 0.55,
description: 'Similarity threshold'
}
},
required: ['query']
}
},
{
name: 'memory_list',
description: 'List all memory entries',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
default: 20,
description: 'Maximum number of results'
},
offset: {
type: 'number',
default: 0,
description: 'Pagination offset'
},
topic_id: {
type: 'string',
description: 'Filter by topic ID'
}
}
}
},
{
name: 'memory_get',
description: 'Get a specific memory by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Memory ID or displayed prefix'
},
memory_id: {
type: 'string',
description: 'Legacy alias for memory ID or displayed prefix'
}
},
anyOf: [
{ required: ['id'] },
{ required: ['memory_id'] }
]
}
},
{
name: 'memory_update',
description: 'Update an existing memory',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Memory ID or displayed prefix'
},
memory_id: {
type: 'string',
description: 'Legacy alias for memory ID or displayed prefix'
},
title: {
type: 'string',
description: 'New title (optional)'
},
content: {
type: 'string',
description: 'New content (optional)'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'New tags (optional)'
}
},
anyOf: [
{ required: ['id'] },
{ required: ['memory_id'] }
]
}
},
{
name: 'memory_delete',
description: 'Delete a memory',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Memory ID or displayed prefix'
},
memory_id: {
type: 'string',
description: 'Legacy alias for memory ID or displayed prefix'
}
},
anyOf: [
{ required: ['id'] },
{ required: ['memory_id'] }
]
}
},
// Topic tools
{
name: 'topic_create',
description: 'Create a new topic',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Topic name'
},
description: {
type: 'string',
description: 'Topic description'
}
},
required: ['name']
}
},
{
name: 'topic_list',
description: 'List all topics',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
default: 20
}
}
}
},
// API Key tools
{
name: 'apikey_create',
description: 'Create a new API key',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'API key name'
},
permissions: {
type: 'array',
items: {
type: 'string',
enum: ['read', 'write', 'delete']
},
default: ['read']
}
},
required: ['name']
}
},
{
name: 'apikey_list',
description: 'List all API keys',
inputSchema: {
type: 'object',
properties: {}
}
},
// System tools
{
name: 'system_health',
description: 'Check system health status',
inputSchema: {
type: 'object',
properties: {
verbose: {
type: 'boolean',
default: false,
description: 'Include detailed diagnostics'
}
}
}
},
{
name: 'system_config',
description: 'Get or update system configuration',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['get', 'set'],
default: 'get'
},
key: {
type: 'string',
description: 'Configuration key'
},
value: {
type: 'string',
description: 'Configuration value (for set action)'
}
}
}
},
// Connection management tools
{
name: 'connection_stats',
description: 'Get connection pool statistics',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'connection_auth_status',
description: 'Get authentication status for all connections',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'connection_validate_auth',
description: 'Validate authentication for a specific connection',
inputSchema: {
type: 'object',
properties: {
clientId: {
type: 'string',
description: 'Client ID to validate'
}
},
required: ['clientId']
}
},
// Transport management tools
{
name: 'transport_status',
description: 'Get transport protocol status and statistics',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'transport_test',
description: 'Test availability of a specific transport',
inputSchema: {
type: 'object',
properties: {
transport: {
type: 'string',
enum: ['stdio', 'websocket', 'http'],
description: 'Transport to test'
}
},
required: ['transport']
}
},
{
name: 'transport_reset_failures',
description: 'Reset failure count for a transport',
inputSchema: {
type: 'object',
properties: {
transport: {
type: 'string',
enum: ['stdio', 'websocket', 'http'],
description: 'Transport to reset (optional, resets all if not specified)'
}
}
}
}
]
}));
// Tool call handler (CallToolRequestSchema already imported above)
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Generate or extract client ID for connection tracking
const clientId = this.extractClientId(request) || this.generateClientId();
// Authenticate the connection before processing the request
try {
await this.authenticateRequest(request, clientId);
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Authentication Error: ${error instanceof Error ? error.message : 'Authentication failed'}`
}
],
isError: true,
task: {
taskId: `${clientId}-${Date.now()}`,
status: 'failed',
ttl: null,
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
statusMessage: 'Authentication failed'
}
};
}
this.updateConnectionActivity(clientId);
try {
const result = await this.handleToolCall(name, args, clientId);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
],
task: {
taskId: `${clientId}-${Date.now()}`,
status: 'completed',
ttl: null,
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString()
}
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
}
],
isError: true,
task: {
taskId: `${clientId}-${Date.now()}`,
status: 'failed',
ttl: null,
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
statusMessage: error instanceof Error ? error.message : 'Unknown error'
}
};
}
});
}
/**
* Register MCP resources
*/
async registerResources() {
const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'memory://recent',
name: 'Recent Memories',
description: 'List of recently created or updated memories',
mimeType: 'application/json'
},
{
uri: 'memory://search',
name: 'Memory Search',
description: 'Search interface for memories',
mimeType: 'application/json'
},
{
uri: 'config://current',
name: 'Current Configuration',
description: 'Current CLI configuration settings',
mimeType: 'application/json'
},
{
uri: 'stats://usage',
name: 'Usage Statistics',
description: 'Memory usage and API statistics',
mimeType: 'application/json'
},
{
uri: 'connections://pool',
name: 'Connection Pool',
description: 'Current connection pool status and statistics',
mimeType: 'application/json'
},
{
uri: 'transport://status',
name: 'Transport Status',
description: 'Transport protocol status and failure statistics',
mimeType: 'application/json'
}
]
}));
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
const content = await this.handleResourceRead(uri);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(content, null, 2)
}
]
};
}
catch (error) {
throw new Error(`Failed to read resource ${uri}: ${error}`);
}
});
}
/**
* Register MCP prompts
*/
async registerPrompts() {
const { ListPromptsRequestSchema, GetPromptRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: 'create_memory',
description: 'Interactive prompt to create a new memory',
arguments: [
{
name: 'title',
description: 'Initial title for the memory',
required: false
}
]
},
{
name: 'search_memories',
description: 'Interactive prompt to search memories',
arguments: [
{
name: 'query',
description: 'Initial search query',
required: false
}
]
},
{
name: 'organize_memories',
description: 'Interactive prompt to organize memories into topics',
arguments: []
}
]
}));
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const prompts = {
create_memory: {
description: 'Create a new memory entry',
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please provide the following information for the new memory:
Title: ${args?.title || '[Enter a descriptive title]'}
Content: [Enter the memory content]
Type: [context/reference/note]
Tags: [Optional comma-separated tags]`
}
}
]
},
search_memories: {
description: 'Search through your memories',
messages: [
{
role: 'user',
content: {
type: 'text',
text: `What would you like to search for in your memories?
Query: ${args?.query || '[Enter search terms]'}
Limit: [Number of results, default 10]
Threshold: [Similarity threshold 0-1, default 0.55]`
}
}
]
},
organize_memories: {
description: 'Organize memories into topics',
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Let's organize your memories into topics.
Would you like to:
1. Create a new topic
2. Move memories to existing topics
3. Review uncategorized memories
4. Merge similar topics
Please choose an option (1-4):`
}
}
]
}
};
const prompt = prompts[name];
if (!prompt) {
throw new Error(`Unknown prompt: ${name}`);
}
return prompt;
});
} /**
/**
* Handle tool calls
*/
async handleToolCall(name, args, _clientId) {
// Ensure we're initialized
if (!this.apiClient) {
await this.initialize();
}
switch (name) {
// Memory operations
case 'memory_create':
return await this.apiClient.createMemory(args);
case 'memory_search':
return await this.apiClient.searchMemories(args.query, {
limit: args.limit,
threshold: args.threshold
});
case 'memory_list':
return await this.apiClient.getMemories({
limit: args.limit,
offset: args.offset,
topic_id: args.topic_id
});
case 'memory_get':
return await this.apiClient.getMemory(this.extractMemoryIdentifier(args));
case 'memory_update':
const memoryId = this.extractMemoryIdentifier(args);
const { id: _id, memory_id: _legacyId, ...updateArgs } = args;
return await this.apiClient.updateMemory(memoryId, updateArgs);
case 'memory_delete':
return await this.apiClient.deleteMemory(this.extractMemoryIdentifier(args));
// Topic operations
case 'topic_create':
return await this.apiClient.createTopic(args);
case 'topic_list':
return await this.apiClient.getTopics();
// API Key operations
case 'apikey_create':
// API keys not directly supported in current APIClient
return { error: 'API key creation not yet implemented' };
case 'apikey_list':
// API keys not directly supported in current APIClient
return { error: 'API key listing not yet implemented' };
// System operations
case 'system_health':
return await this.handleSystemHealth(args.verbose);
case 'system_config':
return await this.handleSystemConfig(args);
// Connection management operations
case 'connection_stats':
return this.getConnectionPoolStats();
case 'connection_auth_status':
return this.getAuthenticationStatus();
case 'connection_validate_auth':
if (!args.clientId) {
throw new Error('clientId is required');
}
return {
clientId: args.clientId,
authenticated: this.validateConnectionAuth(args.clientId),
connection: this.getConnection(args.clientId) ? {
transport: this.getConnection(args.clientId).transport,
connectedAt: this.getConnection(args.clientId).connectedAt.toISOString(),
lastActivity: this.getConnection(args.clientId).lastActivity.toISOString()
} : null
};
// Transport management operations
case 'transport_status':
return this.getTransportStatus();
case 'transport_test': {
if (!args.transport) {
throw new Error('transport is required');
}
const isAvailable = await this.checkTransportAvailability(args.transport);
return {
transport: args.transport,
available: isAvailable,
tested_at: new Date().toISOString()
};
}
case 'transport_reset_failures':
if (args.transport) {
this.transportFailures.delete(args.transport);
return {
success: true,
message: `Reset failures for ${args.transport} transport`
};
}
else {
this.transportFailures.clear();
return {
success: true,
message: 'Reset failures for all transports'
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
/**
* Handle resource reads
*/
async handleResourceRead(uri) {
const [protocol, path] = uri.split('://');
switch (protocol) {
case 'memory':
if (path === 'recent') {
return await this.apiClient.getMemories({ limit: 10 });
}
else if (path === 'search') {
return {
message: 'Use memory_search tool to search memories',
example: { query: 'your search query', limit: 10 }
};
}
break;
case 'config':
if (path === 'current') {
await this.config.init();
return {
apiUrl: this.config.getApiUrl(),
authenticated: await this.config.isAuthenticated(),
user: await this.config.getCurrentUser()
};
}
break;
case 'stats':
if (path === 'usage') {
// TODO: Implement actual stats collection
return {
totalMemories: 0,
totalTopics: 0,
apiCallsToday: 0,
lastSync: new Date().toISOString()
};
}
break;
case 'connections':
if (path === 'pool') {
return {
...this.getConnectionPoolStats(),
connections: Array.from(this.connectionPool.values()).map(conn => ({
clientId: conn.clientId,
connectedAt: conn.connectedAt.toISOString(),
lastActivity: conn.lastActivity.toISOString(),
transport: conn.transport,
authenticated: conn.authenticated,
uptime: Date.now() - conn.connectedAt.getTime(),
clientInfo: conn.clientInfo
}))
};
}
break;
case 'transport':
if (path === 'status') {
return this.getTransportStatus();
}
break;
}
throw new Error(`Unknown resource: ${uri}`);
}
/**
* Handle system health check
*/
async handleSystemHealth(verbose) {
const health = {
status: 'healthy',
server: this.options.name || 'lanonasis-maas-server',
version: this.options.version || '3.0.1',
timestamp: new Date().toISOString(),
connections: this.getConnectionPoolStats(),
authentication: this.getAuthenticationStatus()
};
if (verbose) {
health.api = {
url: this.config.getApiUrl(),
authenticated: await this.config.isAuthenticated()
};
try {
const apiHealth = await this.apiClient.getHealth();
health.api.status = apiHealth.status;
health.api.version = apiHealth.version;
}
catch (error) {
health.api.status = 'error';
health.api.error = error instanceof Error ? error.message : 'Unknown error';
}
// Include detailed connection information
health.connectionDetails = Array.from(this.connectionPool.values()).map(conn => ({
clientId: conn.clientId,
transport: conn.transport,
authenticated: conn.authenticated,
connectedAt: conn.connectedAt.toISOString(),
lastActivity: conn.lastActivity.toISOString(),
uptime: Date.now() - conn.connectedAt.getTime(),
clientInfo: conn.clientInfo
}));
}
return health;
}
/**
* Handle system configuration
*/
async handleSystemConfig(args) {
if (args.action === 'get') {
if (args.key) {
return { [args.key]: this.config.get(args.key) };
}
else {
// Return all config
await this.config.init();
return {
apiUrl: this.config.getApiUrl(),
mcpServerUrl: this.config.get('mcpServerUrl'),
mcpUseRemote: this.config.get('mcpUseRemote'),
maxConnections: this.maxConnections,
transport: this.getTransportStatus()
};
}
}
else if (args.action === 'set') {
if (!args.key || !args.value) {
throw new Error('Key and value required for set action');
}
// Handle special configuration keys
if (args.key === 'maxConnections') {
const newMax = parseInt(args.value);
if (isNaN(newMax) || newMax < 1 || newMax > 100) {
throw new Error('maxConnections must be a number between 1 and 100');
}
this.maxConnections = newMax;
return {
success: true,
message: `Set ${args.key} to ${args.value}`
};
}
this.config.set(args.key, args.value);
await this.config.save();
return {
success: true,
message: `Set ${args.key} to ${args.value}`
};
}
throw new Error('Invalid action');
} /**
* Connection pool management methods
*/
/**
* Add a new connection to the pool
*/
addConnection(clientId, transport, clientInfo) {
// Check if we've reached the maximum number of connections
if (this.connectionPool.size >= this.maxConnections) {
if (this.options.verbose) {
console.log(chalk.yellow(`โ ๏ธ Maximum connections (${this.maxConnections}) reached, rejecting new connection`));
}
return false;
}
const connection = {
clientId,
connectedAt: new Date(),
lastActivity: new Date(),
transport,
authenticated: false,
clientInfo
};
this.connectionPool.set(clientId, connection);
if (this.options.verbose) {
console.log(chalk.cyan(`โ
Added connection ${clientId} (${transport}) - Total: ${this.connectionPool.size}`));
}
return true;
}
/**
* Remove a connection from the pool
*/
removeConnection(clientId) {
const connection = this.connectionPool.get(clientId);
if (connection) {
this.connectionPool.delete(clientId);
if (this.options.verbose) {
const uptime = Date.now() - connection.connectedAt.getTime();
console.log(chalk.gray(`๐ Removed connection ${clientId} (uptime: ${Math.round(uptime / 1000)}s) - Total: ${this.connectionPool.size}`));
}
}
}
/**
* Update connection activity timestamp
*/
updateConnectionActivity(clientId) {
const connection = this.connectionPool.get(clientId);
if (connection) {
connection.lastActivity = new Date();
}
}
/**
* Mark connection as authenticated
*/
authenticateConnection(clientId) {
const connection = this.connectionPool.get(clientId);
if (connection) {
connection.authenticated = true;
if (this.options.verbose) {
console.log(chalk.green(`๐ Connection ${clientId} authenticated`));
}
}
}
/**
* Get connection pool statistics
*/
getConnectionPoolStats() {
const stats = {
totalConnections: this.connectionPool.size,
activeConnections: 0,
authenticatedConnections: 0,
connectionsByTransport: {}
};
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
this.connectionPool.forEach(connection => {
// Count as active if there was activity in the last 5 minutes
if (connection.lastActivity.getTime() > fiveMinutesAgo) {
stats.activeConnections++;
}
if (connection.authenticated) {
stats.authenticatedConnections++;
}
stats.connectionsByTransport[connection.transport] =
(stats.connectionsByTransport[connection.transport] || 0) + 1;
});
return stats;
}
/**
* Start connection cleanup monitoring
*/
startConnectionCleanup() {
// Clean up stale connections every 2 minutes
this.connectionCleanupInterval = setInterval(() => {
this.cleanupStaleConnections();
}, 2 * 60 * 1000);
}
/**
* Stop connection cleanup monitoring
*/
stopConnectionCleanup() {
if (this.connectionCleanupInterval) {
clearInterval(this.connectionCleanupInterval);
this.connectionCleanupInterval = null;
}
}
/**
* Clean up stale connections (no activity for 10 minutes)
*/
cleanupStaleConnections() {
const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
const staleConnections = [];
this.connectionPool.forEach((connection, clientId) => {
if (connection.lastActivity.getTime() < tenMinutesAgo) {
staleConnections.push(clientId);
}
});
for (let i = 0; i < staleConnections.length; i++) {
const clientId = staleConnections[i];
this.removeConnection(clientId);
if (this.options.verbose) {
console.log(chalk.yellow(`๐งน Cleaned up stale connection: ${clientId}`));
}
}
}
/**
* Generate unique client ID for new connections
*/
generateClientId() {
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Extract client ID from request headers or metadata
*/
extractClientId(request) {
// Try to extract client ID from request metadata
// This would depend on the MCP transport implementation
return request.meta?.clientId || null;
}
/**
* Authenticate incoming MCP request
*/
async authenticateRequest(request, clientId) {
// Check if connection already exists and is authenticated
const existingConnection = this.getConnection(clientId);
if (existingConnection && existingConnection.authenticated) {
return; // Already authenticated
}
// Extract authentication information from request
const authInfo = this.extractAuthInfo(request);
if (!authInfo.token && !authInfo.vendorKey) {
// For stdio connections, use the CLI's stored credentials
if (this.isStdioConnection(request)) {
const isAuthenticated = await this.validateStoredCredentials();
if (isAuthenticated) {
this.ensureConnectionExists(clientId, 'stdio');
this.authenticateConnection(clientId);
return;
}
}
throw new Error('Authentication required. No valid credentials provided.');
}
// Validate provided credentials
const isValid = await this.validateCredentials(authInfo.token, authInfo.vendorKey);
if (!isValid) {
throw new Error('Invalid credentials provided.');
}
// Add/update connection in pool and mark as authenticated
const transport = this.determineTransport(request);
this.ensureConnectionExists(clientId, transport, authInfo.clientInfo);
this.authenticateConnection(clientId);
}
/**
* Extract authentication information from request
*/
extractAuthInfo(request) {
// Try to extract from various possible locations
const headers = request.headers || {};
const meta = request.meta || {};
const params = request.params || {};
return {
token: headers.authorization?.replace('Bearer ', '') ||
headers['x-auth-token'] ||
meta.token ||
params.token,
vendorKey: headers['x-api-key'] ||
headers['x-vendor-key'] ||
meta.vendorKey ||
params.vendorKey,
clientInfo: {
name: headers['x-client-name'] || meta.clientName || 'unknown',
version: headers['x-client-version'] || meta.clientVersion || '1.0.0'
}
};
}
/**
* Check if this is a stdio connection
*/
isStdioConnection(request) {
// Stdio connections typically don't have HTTP-style headers
return !request.headers || Object.keys(request.headers).length === 0;
}
/**
* Determine transport type from request
*/
determineTransport(request) {
if (this.isStdioConnection(request)) {
return 'stdio';
}
const headers = request.headers || {};
if (headers.upgrade === 'websocket' || headers.connection?.includes('Upgrade')) {
return 'websocket';
}
return 'http';
}
/**
* Ensure connection exists in pool
*/
ensureConnectionExists(clientId, transport, clientInfo) {
if (!this.connectionPool.has(clientId)) {
const success = this.addConnection(clientId, transport, clientInfo);
if (!success) {
throw new Error('Maximum connections reached. Please try again later.');
}
}
}
/**
* Validate stored CLI credentials
*/
async validateStoredCredentials() {
try {
return await this.config.validateStoredCredentials();
}
catch (error) {
if (this.options.verbose) {
console.log(chalk.yellow(`โ ๏ธ Stored credentials validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
return false;
}
}
/**
* Validate provided credentials against the API
*/
async validateCredentials(token, vendorKey) {
if (!token && !vendorKey) {
return false;
}
try {
// Import axios dynamically to avoid circular dependency
const axios = (await import('axios')).default;
// Ensure service discovery is done
await this.config.discoverServices();
const authBase = this.config.getDiscoveredApiUrl();
const headers = {
'X-Project-Scope': 'lanonasis-maas'
};
if (vendorKey) {
headers['X-API-Key'] = vendorKey;
headers['X-Auth-Method'] = 'vendor_key';
}
else if (token) {
headers['Authorization'] = `Bearer ${token}`;
headers['X-Auth-Method'] = 'jwt';
}
const normalizedBase = authBase.replace(/\/$/, '');
const endpoints = [
`${normalizedBase}/health`,
`${normalizedBase}/api/v1/health`
];
let lastError;
for (const endpoint of endpoints) {
try {
await axios.get(endpoint, {
headers,
timeout: 10000
});
return true;
}
catch (error) {
lastError = error;
}
}
if (lastError instanceof Error) {
throw lastError;
}
throw new Error('Auth health endpoints unreachable');
}
catch (error) {
if (this.options.verbose) {
console.log(chalk.yellow(`โ ๏ธ Credential validation failed: ${error.response?.status || error.message}`));
}
return false;
}
}
/**
* Validate connection authentication status
*/
validateConnectionAuth(clientId) {
const connection = this.getConnection(clientId);
return connection ? connection.authenticated : false;
}
/**
* Get authentication status for all connections
*/
getAuthenticationStatus() {
const stats = this.getConnectionPoolStats();
return {
totalConnections: stats.totalConnections,
authenticatedConnections: stats.authenticatedConnections,
unauthenticatedConnections: stats.totalConnections - stats.authenticatedConnections
};
}
/**
* Transport protocol management methods
*/
/**
* Check if a transport is available and working
*/
async checkTransportAvailability(transport) {
try {
switch (transport) {
case 'stdio':
// Stdio is always available if we can start the process
return true;
case 'websocket':
// Check if WebSocket server can be started
return await this.testWebSocketAvailability();
case 'http':
// Check if HTTP server can be started
return await this.testHttpAvailability();
default:
return false;
}
}
catch (error) {
this.recordTransportFailure(transport, error);
return false;
}
}
/**
* Test WebSocket transport availability
*/
async testWebSocketAvailability() {
try {
// This would typically involve checking if we can bind to a WebSocket port
// For now, we'll assume it's available unless we have recorded failures
const failures = this.transportFailures.get('websocket');
if (failures && failures.count > 3) {
const timeSinceLastFailure = Date.now() - failures.lastFailure.getTime();
// Don't retry for 5 minutes after multiple failures
if (timeSinceLastFailure < 5 * 60 * 1000) {
return false;
}
}
return true;
}
catch {
return false;
}
}
/**
* Test HTTP transport availability
*/
async testHttpAvailability() {
try {
// This would typically involve checking if we can bind to an HTTP port
// For now, we'll assume it's available unless we have recorded failures
const failures = this.transportFailures.get('http');
if (failures && failures.count > 3) {
const timeSinceLastFailure = Date.now() - failures.lastFailure.getTime();
// Don't retry for 5 minutes after multiple failures
if (timeSinceLastFailure < 5 * 60 * 1000) {
return false;
}
}
return true;
}
catch {
return false;
}
}
/**
* Record a transport failure
*/
recordTransportFailure(transport, error) {
const existing = this.transportFailures.get(transport);
const failure = {
count: existing ? existing.count + 1 : 1,
lastFailure: new Date()
};
this.transportFailures.set(transport, failure);
if (this.options.verbose) {
console.log(chalk.yellow(`โ ๏ธ Transport ${transport} failure #${failure.count}: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
/**
* Get the best available transport
*/
async getBestAvailableTransport() {
const preferred = this.options.preferredTransport || 'stdio';
// Try preferred transport first
if (await this.checkTransportAvailability(preferred)) {
return preferred;
}
if (!this.enableFallback) {
return null;
}
// Try other transports in order of preference
const fallbackOrder = this.supportedTransports.filter(t => t !== preferred);
for (const transport of fallbackOrder) {
if (await this.checkTransportAvailability(transport)) {
if (this.options.verbose) {
console.log(chalk.cyan(`๐ Falling back to ${transport} transport`));
}
return transport;
}
}
return null;
}
/**
* Handle transport-specific errors with clear messages
*/
handleTransportError(transport, error) {
this.recordTransportFailure(transport, error);
const baseMessage = `${transport.toUpperCase()} transport failed`;
let specificMessage = '';
let troubleshooting = '';
switch (transport) {
case 'stdio':
if (error.code === 'ENOENT') {
specificMessage = 'Server executable not found';
troubleshooting = 'Ensure the MCP server is installed and accessible';
}
else if (error.code === 'EACCES') {
specificMessage = 'Permission denied';
troubleshooting = 'Check file permissions for the MCP server executable';
}
else {
specificMessage = 'Process communication failed';
troubleshooting = 'Check if the server process can be started';
}
break;
case 'websocket':
if (error.code === 'EADDRINUSE') {
specificMessage = 'WebSocket port already in use';
troubleshooting = 'Try a different port or stop the conflicting service';
}
else if (error.code === 'ECONNREFUSED') {
specificMessage = 'WebSocket connection refused';
troubleshooting = 'Check if the WebSocket server is running and accessible';
}
else if (error.code === 'ENOTFOUND') {
specificMessage = 'WebSocket server not found';
troubleshooting = 'Verify the WebSocket server URL is correct';
}
else {
specificMessage = 'WebSocket connection failed';
troubleshooting = 'Check network connectivity and firewall settings';
}
break;
case 'http':
if (error.code === 'EADDRINUSE') {
specificMessage = 'HTTP port already in use';
troubleshooting = 'Try a different port or stop the conflicting service';
}
else if (error.code === 'ECONNREFUSED') {
specificMessage = 'HTTP connection refused';
troubleshooting = 'Check if the HTTP server is running and accessible';
}
else if (error.response?.status) {
specificMessage = `HTTP ${error.response.status} error`;
troubleshooting = 'Check server status and authentication';
}
else {
specificMessage = 'HTTP connection failed';
troubleshooting = 'Check network connectivity and server availability';
}
break;
}
const fullMessage = `${baseMessage}: ${specificMessage}. ${troubleshooting}`;
return new Error(fullMessage);
}
/**
* Attempt to start server with transport fallback
*/
async startWithTransportFallback() {
const availableTransport = await this.getBestAvailableTransport();
if (!availableTransport) {
const failureMessages = Array.from(this.transportFailures.entries())
.map(([transport, failure]) => `${transport}: ${failure.count} failures`)
.join(', ');
throw new Error(`No available transports. All transports have failed: ${failureMessages}. ` +
'Please check your configuration and network connectivity.');
}
try {
await this.startTransport(availableTransport);
return availableTransport;
}
catch (error) {
const transportError = this.handleTransportError(availableTransport, error);
if (this.enableFallback && this.supportedTransports.length > 1) {
// Try next available transport
const nextTransport = await this.getBestAvailableTransport();
if (nextTransport && nextTransport !== availableTransport) {
console.log(chalk.yellow(`โ ๏ธ ${transportError.message}`));
console.log(chalk.cyan(`๐ Attempting fallback to ${nextTransport} transport...`));
try {
await this.startTransport(nextTransport);
return nextTransport;
}
catch (fallbackError) {
const fallbackTransportError = this.handleTransportError(nextTransport, fallbackError);
throw new Error(`Primary transport failed: ${transportError.message}. Fallback also failed: ${fallbackTransportError.message}`);
}
}
}
throw transportError;
}
}
/**
* Start a specific transport
*/
async startTransport(transport) {
switch (transport) {
case 'stdio':
this.transport = new StdioServerTransport();
await this.server.connect(this.transport);
break;
case 'websocket':
// WebSocket transport would be implemented here
// For now, we'll simulate it
throw new Error('WebSocket transport not yet implemented');
case 'http':
// HTTP transport would be implemented here
// For now, we'll simulate it
throw new Error('HTTP transport not yet implemented');
default:
throw new Error(`Unsupported transport: ${transport}`);
}
}
/**
* Get transport status and statistics
*/
getTransportStatus() {
const failures = {};
this.transportFailures.forEach((failure, transport) => {
failures[transport] = {
count: failure.count,
lastFailure: failure.lastFailure.toISOString()
};
});
return {
supportedTransports: this.supportedTransports,
preferredTransport: this.options.preferredTransport || 'stdio',
enableFallback: this.enableFallback,
transportFailures: failures
};
}
/**
* Check if connection limit allows new connections
*/
canAcceptNewConnection() {
return this.connectionPool.size < this.maxConnections;
}
/**
* Get connection by client ID
*/
getConnection(clientId) {
return this.connectionPool.get(clientId);
}
/**
* Setup error handling
*/
setupErrorHandling() {
process.on('SIGINT', async () => {
console.log(chalk.yellow('\nโ ๏ธ Shutting down MCP server...'));
await this.stop();
process.exit(0);
});
process.on('uncaughtException', (error) => {
console.error(chalk.red('Uncaught exception:'), error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error(chalk.red('Unhandled rejection at:'), promise, 'reason:', reason);
});
}
/**
* Start the server
*/
async start() {
await this.initialize();
try {
// Start server with transport fallback
const activeTransport = await this.startWithTransportFallback();
// Add the initial connection to the pool
const initialClientId = this.generateClientId();
this.addConnection(initialClientId, activeTransport, {
name: `${activeTransport}-client`,
version: '1.0.0'
});
if (this.options.verbose) {
console.log(chalk.green('โ
Lanonasis MCP Server started'));
console.log(chalk.gray(`Active transport: ${activeTransport}`));
console.log(chalk.gray('Waiting for client connections...'));
if (this.enableFallback) {
console.log(chalk.gray('Transport fallback: enabled'));
}
}
// Keep the process alive
process.stdin.resume();
}
catch (error) {
console.error(chalk.red('โ Failed to start MCP Server:'));
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
if (this.options.verbose) {
console.log(chalk.yellow('\n๐ง Troubleshooting tips:'));
console.log(chalk.cyan('โข Check if all required dependencies are installed'));
console.log(chalk.cyan('โข Verify network connectivity and firewall settings'));
console.log(chalk.cyan('โข Try enabling transport fallback: --enable-fallback'));
console.log(chalk.cyan('โข Use --verbose for detailed error information'));
const transportStatus = this.getTransportStatus();
if (Object.keys(transportStatus.transportFailures).length > 0) {
console.log(chalk.yellow('\n๐ Transport failure history:'));
for (const [transport, failure] of Object.entries(transportStatus.transportFailures)) {
console.log(chalk.gray(` ${transport}: ${failure.count} failures (last: ${failure.lastFailure})`));
}
}
}
throw error;
}
}
/**
* Start the server in stdio mode for external MCP clients (Claude Desktop, Cursor, etc.)
* This is the primary entry point for `lanonasis mcp start`
*/
async startStdio() {
await this.initialize();
try {
// Use API key from options if provided (from LANONASIS_API_KEY env var)
if (this.options.apiKey) {
// Set the API key for authentication
await this.config.setVendorKey(this.options.apiKey);
}
// Create stdio transport
this.transport = new StdioServerTransport();
await this.server.connect(this.transport);
// Log to stderr since stdout is for MCP protocol
console.error(chalk.cyan('๐ Lanonasis MCP Server started (stdio mode)'));
console.error(chalk.gray(`Backend: ${this.config.getApiUrl()}`));
console.error(chalk.gray(`Tools: 18+ available`));
console.error(chalk.gray('Ready for MCP client connections...'));
// Keep the process alive
process.stdin.resume();
}
catch (error) {
console.error(chalk.red('โ Failed to start MCP Server:'));
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
throw error;
}
}
/**
* Stop the server
*/
async stop() {
// Stop connection cleanup
this.stopConnectionCleanup();
// Clear all connections
this.connectionPool.clear();
if (this.transport) {
await this.server.close();
this.transport = null;
}
if (this.options.verbose) {
console.log(chalk.gray('MCP Server stopped'));
}
}
/**
* Get server instance (for testing)
*/
getServer() {
return this.server;
}
/**
* Get connection pool for testing/monitoring
*/
getConnectionPool() {
return new Map(this.connectionPool);
}
}
// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new LanonasisMCPServer({
verbose: process.argv.includes('--verbose'),
apiUrl: process.env.LANONASIS_API_URL,
token: process.env.LANONASIS_TOKEN
});
server.start().catch((error) => {
console.error(chalk.red('Failed to start server:'), error);
process.exit(1);
});
}