@lanonasis/cli 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,41 +54,60 @@ export function mcpCommands(program) {
54
54
  });
55
55
  // Connect command
56
56
  mcp.command('connect')
57
- .description('Connect to MCP server (local or remote)')
57
+ .description('Connect to MCP server (local, remote, or WebSocket)')
58
58
  .option('-l, --local', 'Connect to local MCP server')
59
59
  .option('-r, --remote', 'Connect to remote MCP server (api.lanonasis.com)')
60
+ .option('-w, --websocket', 'Connect using WebSocket mode for enterprise users')
60
61
  .option('-s, --server <path>', 'Local MCP server path')
61
- .option('-u, --url <url>', 'Remote MCP server URL')
62
+ .option('-u, --url <url>', 'Remote/WebSocket server URL')
62
63
  .action(async (options) => {
63
64
  const spinner = ora('Connecting to MCP server...').start();
64
65
  const config = new CLIConfig();
65
66
  try {
66
- // Determine connection mode
67
- let useRemote = options.remote;
68
- if (!options.local && !options.remote) {
67
+ let connectionMode;
68
+ // Determine connection mode - WebSocket takes precedence over remote and local
69
+ if (options.websocket) {
70
+ connectionMode = 'websocket';
71
+ }
72
+ else if (options.remote) {
73
+ connectionMode = 'remote';
74
+ }
75
+ else if (options.local) {
76
+ connectionMode = 'local';
77
+ }
78
+ else {
69
79
  // Default to remote if authenticated, otherwise local
70
- useRemote = !!config.get('token');
80
+ connectionMode = !!config.get('token') ? 'remote' : 'local';
71
81
  }
72
- // Save preference
73
- config.set('mcpUseRemote', useRemote);
82
+ // Save preferences
83
+ config.set('mcpConnectionMode', connectionMode);
74
84
  if (options.server) {
75
85
  config.set('mcpServerPath', options.server);
76
86
  }
77
87
  if (options.url) {
78
- config.set('mcpServerUrl', options.url);
88
+ if (connectionMode === 'websocket') {
89
+ config.set('mcpWebSocketUrl', options.url);
90
+ }
91
+ else {
92
+ config.set('mcpServerUrl', options.url);
93
+ }
79
94
  }
80
95
  const client = getMCPClient();
81
96
  const connected = await client.connect({
82
- useRemote,
97
+ connectionMode,
83
98
  serverPath: options.server,
84
99
  serverUrl: options.url
85
100
  });
86
101
  if (connected) {
87
- spinner.succeed(chalk.green(`Connected to ${useRemote ? 'remote' : 'local'} MCP server`));
88
- if (useRemote) {
102
+ spinner.succeed(chalk.green(`Connected to MCP server in ${connectionMode} mode`));
103
+ if (connectionMode === 'remote') {
89
104
  console.log(chalk.cyan('ℹ️ Using remote MCP via api.lanonasis.com'));
90
105
  console.log(chalk.cyan('📡 SSE endpoint active for real-time updates'));
91
106
  }
107
+ else if (connectionMode === 'websocket') {
108
+ console.log(chalk.cyan('ℹ️ Using enterprise WebSocket MCP server'));
109
+ console.log(chalk.cyan('📡 WebSocket connection active with auto-reconnect'));
110
+ }
92
111
  }
93
112
  else {
94
113
  spinner.fail('Failed to connect to MCP server');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,519 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { CLIConfig } from './utils/config.js';
6
+ // MCP Protocol Compliance: Redirect all console output to stderr
7
+ // This prevents stdout pollution which breaks JSON-RPC communication
8
+ const originalConsoleError = console.error;
9
+ // Silent mode for MCP protocol compliance
10
+ const isSilentMode = process.env.LANONASIS_SILENT === 'true' || process.argv.includes('--silent');
11
+ if (isSilentMode) {
12
+ // Completely silence all output except JSON-RPC
13
+ console.log = () => { };
14
+ console.error = () => { };
15
+ console.warn = () => { };
16
+ console.info = () => { };
17
+ console.debug = () => { };
18
+ }
19
+ else {
20
+ // Redirect to stderr for debugging
21
+ console.log = (...args) => originalConsoleError('[MCP-LOG]', ...args);
22
+ console.error = (...args) => originalConsoleError('[MCP-ERROR]', ...args);
23
+ console.warn = (...args) => originalConsoleError('[MCP-WARN]', ...args);
24
+ console.info = (...args) => originalConsoleError('[MCP-INFO]', ...args);
25
+ }
26
+ // Disable colors and verbose output for MCP protocol compliance
27
+ process.env.FORCE_COLOR = '0';
28
+ process.env.DEBUG = '';
29
+ process.env.NODE_ENV = process.env.NODE_ENV || 'production';
30
+ class LanonasisMCPServer {
31
+ server;
32
+ config;
33
+ constructor() {
34
+ this.config = new CLIConfig();
35
+ this.server = new Server({
36
+ name: 'lanonasis-mcp-server',
37
+ version: '1.3.0',
38
+ }, {
39
+ capabilities: {
40
+ tools: {},
41
+ resources: {},
42
+ },
43
+ });
44
+ this.setupHandlers();
45
+ }
46
+ setupHandlers() {
47
+ // List available tools - Comprehensive MCP toolset matching legacy CLI
48
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
49
+ return {
50
+ tools: [
51
+ // Memory Management Tools
52
+ {
53
+ name: 'create_memory',
54
+ description: 'Create a new memory entry with vector embedding',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ title: { type: 'string', description: 'Memory title' },
59
+ content: { type: 'string', description: 'Memory content' },
60
+ memory_type: { type: 'string', description: 'Type of memory', enum: ['context', 'project', 'knowledge', 'reference', 'personal', 'workflow'] },
61
+ tags: { type: 'array', items: { type: 'string' }, description: 'Memory tags' },
62
+ topic_id: { type: 'string', description: 'Topic ID for organization' }
63
+ },
64
+ required: ['title', 'content']
65
+ }
66
+ },
67
+ {
68
+ name: 'search_memories',
69
+ description: 'Search through memories with semantic vector search',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ query: { type: 'string', description: 'Search query' },
74
+ memory_type: { type: 'string', description: 'Filter by memory type' },
75
+ limit: { type: 'number', description: 'Maximum results to return', default: 10 },
76
+ threshold: { type: 'number', description: 'Similarity threshold (0.0-1.0)', default: 0.7 },
77
+ tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' }
78
+ },
79
+ required: ['query']
80
+ }
81
+ },
82
+ {
83
+ name: 'get_memory',
84
+ description: 'Get a specific memory by ID',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ id: { type: 'string', description: 'Memory ID' }
89
+ },
90
+ required: ['id']
91
+ }
92
+ },
93
+ {
94
+ name: 'update_memory',
95
+ description: 'Update an existing memory',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ id: { type: 'string', description: 'Memory ID' },
100
+ title: { type: 'string', description: 'Memory title' },
101
+ content: { type: 'string', description: 'Memory content' },
102
+ memory_type: { type: 'string', description: 'Type of memory' },
103
+ tags: { type: 'array', items: { type: 'string' }, description: 'Memory tags' }
104
+ },
105
+ required: ['id']
106
+ }
107
+ },
108
+ {
109
+ name: 'delete_memory',
110
+ description: 'Delete a memory by ID',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ id: { type: 'string', description: 'Memory ID' }
115
+ },
116
+ required: ['id']
117
+ }
118
+ },
119
+ {
120
+ name: 'list_memories',
121
+ description: 'List memories with pagination and filters',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ limit: { type: 'number', description: 'Number of memories to return', default: 20 },
126
+ offset: { type: 'number', description: 'Offset for pagination', default: 0 },
127
+ memory_type: { type: 'string', description: 'Filter by memory type' },
128
+ tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' }
129
+ }
130
+ }
131
+ },
132
+ // API Key Management Tools
133
+ {
134
+ name: 'create_api_key',
135
+ description: 'Create a new API key',
136
+ inputSchema: {
137
+ type: 'object',
138
+ properties: {
139
+ name: { type: 'string', description: 'API key name' },
140
+ description: { type: 'string', description: 'API key description' },
141
+ project_id: { type: 'string', description: 'Project ID' },
142
+ access_level: { type: 'string', description: 'Access level', enum: ['public', 'authenticated', 'team', 'admin', 'enterprise'] },
143
+ expires_in_days: { type: 'number', description: 'Expiration in days', default: 365 }
144
+ },
145
+ required: ['name']
146
+ }
147
+ },
148
+ {
149
+ name: 'list_api_keys',
150
+ description: 'List API keys',
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ project_id: { type: 'string', description: 'Filter by project ID' },
155
+ active_only: { type: 'boolean', description: 'Show only active keys', default: true }
156
+ }
157
+ }
158
+ },
159
+ {
160
+ name: 'rotate_api_key',
161
+ description: 'Rotate an API key',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ key_id: { type: 'string', description: 'API key ID to rotate' }
166
+ },
167
+ required: ['key_id']
168
+ }
169
+ },
170
+ {
171
+ name: 'delete_api_key',
172
+ description: 'Delete an API key',
173
+ inputSchema: {
174
+ type: 'object',
175
+ properties: {
176
+ key_id: { type: 'string', description: 'API key ID to delete' }
177
+ },
178
+ required: ['key_id']
179
+ }
180
+ },
181
+ // Project Management Tools
182
+ {
183
+ name: 'create_project',
184
+ description: 'Create a new project',
185
+ inputSchema: {
186
+ type: 'object',
187
+ properties: {
188
+ name: { type: 'string', description: 'Project name' },
189
+ description: { type: 'string', description: 'Project description' },
190
+ organization_id: { type: 'string', description: 'Organization ID' }
191
+ },
192
+ required: ['name']
193
+ }
194
+ },
195
+ {
196
+ name: 'list_projects',
197
+ description: 'List projects',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ organization_id: { type: 'string', description: 'Filter by organization ID' }
202
+ }
203
+ }
204
+ },
205
+ // Organization Management Tools
206
+ {
207
+ name: 'get_organization_info',
208
+ description: 'Get organization information',
209
+ inputSchema: {
210
+ type: 'object',
211
+ properties: {}
212
+ }
213
+ },
214
+ // Authentication Tools
215
+ {
216
+ name: 'get_auth_status',
217
+ description: 'Get authentication status',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {}
221
+ }
222
+ },
223
+ // Configuration Tools
224
+ {
225
+ name: 'get_config',
226
+ description: 'Get configuration settings',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: {
230
+ key: { type: 'string', description: 'Specific config key to retrieve' }
231
+ }
232
+ }
233
+ },
234
+ {
235
+ name: 'set_config',
236
+ description: 'Set configuration setting',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ key: { type: 'string', description: 'Configuration key' },
241
+ value: { type: 'string', description: 'Configuration value' }
242
+ },
243
+ required: ['key', 'value']
244
+ }
245
+ },
246
+ // Health and Status Tools
247
+ {
248
+ name: 'get_health_status',
249
+ description: 'Get system health status',
250
+ inputSchema: {
251
+ type: 'object',
252
+ properties: {}
253
+ }
254
+ }
255
+ ]
256
+ };
257
+ });
258
+ // Handle tool calls - Comprehensive implementation
259
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
+ const { name, arguments: args } = request.params;
261
+ try {
262
+ const apiKey = process.env.LANONASIS_API_KEY;
263
+ const apiUrl = process.env.LANONASIS_API_URL || 'https://api.lanonasis.com';
264
+ if (!apiKey) {
265
+ return {
266
+ content: [
267
+ {
268
+ type: 'text',
269
+ text: 'Error: LANONASIS_API_KEY environment variable is required'
270
+ }
271
+ ]
272
+ };
273
+ }
274
+ const headers = {
275
+ 'Authorization': `Bearer ${apiKey}`,
276
+ 'Content-Type': 'application/json',
277
+ 'User-Agent': 'lanonasis-mcp-server/1.3.0'
278
+ };
279
+ switch (name) {
280
+ // Memory Management Tools
281
+ case 'create_memory': {
282
+ const response = await fetch(`${apiUrl}/api/memory`, {
283
+ method: 'POST',
284
+ headers,
285
+ body: JSON.stringify(args)
286
+ });
287
+ if (!response.ok) {
288
+ const errorText = await response.text();
289
+ throw new Error(`Memory creation failed: ${response.status} ${response.statusText} - ${errorText}`);
290
+ }
291
+ const result = await response.json();
292
+ return {
293
+ content: [
294
+ {
295
+ type: 'text',
296
+ text: `✅ Memory created successfully:\n${JSON.stringify(result, null, 2)}`
297
+ }
298
+ ]
299
+ };
300
+ }
301
+ case 'search_memories': {
302
+ const queryParams = new URLSearchParams();
303
+ if (args.query)
304
+ queryParams.append('query', String(args.query));
305
+ if (args.memory_type)
306
+ queryParams.append('memory_type', String(args.memory_type));
307
+ if (args.limit)
308
+ queryParams.append('limit', args.limit.toString());
309
+ if (args.threshold)
310
+ queryParams.append('threshold', args.threshold.toString());
311
+ if (args.tags && Array.isArray(args.tags)) {
312
+ args.tags.forEach((tag) => queryParams.append('tags', String(tag)));
313
+ }
314
+ const response = await fetch(`${apiUrl}/api/memory/search?${queryParams}`, {
315
+ method: 'GET',
316
+ headers
317
+ });
318
+ if (!response.ok) {
319
+ const errorText = await response.text();
320
+ throw new Error(`Memory search failed: ${response.status} ${response.statusText} - ${errorText}`);
321
+ }
322
+ const result = await response.json();
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: `🔍 Search results (${result.length || 0} found):\n${JSON.stringify(result, null, 2)}`
328
+ }
329
+ ]
330
+ };
331
+ }
332
+ case 'get_memory': {
333
+ const response = await fetch(`${apiUrl}/api/memory/${args.id}`, {
334
+ method: 'GET',
335
+ headers
336
+ });
337
+ if (!response.ok) {
338
+ const errorText = await response.text();
339
+ throw new Error(`Memory retrieval failed: ${response.status} ${response.statusText} - ${errorText}`);
340
+ }
341
+ const result = await response.json();
342
+ return {
343
+ content: [
344
+ {
345
+ type: 'text',
346
+ text: `📄 Memory details:\n${JSON.stringify(result, null, 2)}`
347
+ }
348
+ ]
349
+ };
350
+ }
351
+ case 'update_memory': {
352
+ const { id, ...updateData } = args;
353
+ const response = await fetch(`${apiUrl}/api/memory/${id}`, {
354
+ method: 'PUT',
355
+ headers,
356
+ body: JSON.stringify(updateData)
357
+ });
358
+ if (!response.ok) {
359
+ const errorText = await response.text();
360
+ throw new Error(`Memory update failed: ${response.status} ${response.statusText} - ${errorText}`);
361
+ }
362
+ const result = await response.json();
363
+ return {
364
+ content: [
365
+ {
366
+ type: 'text',
367
+ text: `✏️ Memory updated successfully:\n${JSON.stringify(result, null, 2)}`
368
+ }
369
+ ]
370
+ };
371
+ }
372
+ case 'delete_memory': {
373
+ const response = await fetch(`${apiUrl}/api/memory/${args.id}`, {
374
+ method: 'DELETE',
375
+ headers
376
+ });
377
+ if (!response.ok) {
378
+ const errorText = await response.text();
379
+ throw new Error(`Memory deletion failed: ${response.status} ${response.statusText} - ${errorText}`);
380
+ }
381
+ return {
382
+ content: [
383
+ {
384
+ type: 'text',
385
+ text: `🗑️ Memory deleted successfully (ID: ${args.id})`
386
+ }
387
+ ]
388
+ };
389
+ }
390
+ case 'list_memories': {
391
+ const queryParams = new URLSearchParams();
392
+ if (args.limit)
393
+ queryParams.append('limit', args.limit.toString());
394
+ if (args.offset)
395
+ queryParams.append('offset', args.offset.toString());
396
+ if (args.memory_type)
397
+ queryParams.append('memory_type', String(args.memory_type));
398
+ if (args.tags && Array.isArray(args.tags)) {
399
+ args.tags.forEach((tag) => queryParams.append('tags', String(tag)));
400
+ }
401
+ const response = await fetch(`${apiUrl}/api/memory?${queryParams}`, {
402
+ method: 'GET',
403
+ headers
404
+ });
405
+ if (!response.ok) {
406
+ const errorText = await response.text();
407
+ throw new Error(`Memory listing failed: ${response.status} ${response.statusText} - ${errorText}`);
408
+ }
409
+ const result = await response.json();
410
+ return {
411
+ content: [
412
+ {
413
+ type: 'text',
414
+ text: `📋 Memory list (${result.length || 0} items):\n${JSON.stringify(result, null, 2)}`
415
+ }
416
+ ]
417
+ };
418
+ }
419
+ // API Key Management Tools
420
+ case 'create_api_key': {
421
+ const response = await fetch(`${apiUrl}/api/api-keys`, {
422
+ method: 'POST',
423
+ headers,
424
+ body: JSON.stringify(args)
425
+ });
426
+ if (!response.ok) {
427
+ const errorText = await response.text();
428
+ throw new Error(`API key creation failed: ${response.status} ${response.statusText} - ${errorText}`);
429
+ }
430
+ const result = await response.json();
431
+ return {
432
+ content: [
433
+ {
434
+ type: 'text',
435
+ text: `🔑 API key created successfully:\n${JSON.stringify(result, null, 2)}`
436
+ }
437
+ ]
438
+ };
439
+ }
440
+ case 'list_api_keys': {
441
+ const queryParams = new URLSearchParams();
442
+ if (args.project_id)
443
+ queryParams.append('project_id', String(args.project_id));
444
+ if (args.active_only !== undefined)
445
+ queryParams.append('active_only', args.active_only.toString());
446
+ const response = await fetch(`${apiUrl}/api/api-keys?${queryParams}`, {
447
+ method: 'GET',
448
+ headers
449
+ });
450
+ if (!response.ok) {
451
+ const errorText = await response.text();
452
+ throw new Error(`API key listing failed: ${response.status} ${response.statusText} - ${errorText}`);
453
+ }
454
+ const result = await response.json();
455
+ return {
456
+ content: [
457
+ {
458
+ type: 'text',
459
+ text: `🔑 API keys (${result.length || 0} found):\n${JSON.stringify(result, null, 2)}`
460
+ }
461
+ ]
462
+ };
463
+ }
464
+ case 'get_health_status': {
465
+ const response = await fetch(`${apiUrl}/api/health`, {
466
+ method: 'GET',
467
+ headers
468
+ });
469
+ if (!response.ok) {
470
+ const errorText = await response.text();
471
+ throw new Error(`Health check failed: ${response.status} ${response.statusText} - ${errorText}`);
472
+ }
473
+ const result = await response.json();
474
+ return {
475
+ content: [
476
+ {
477
+ type: 'text',
478
+ text: `💚 System health status:\n${JSON.stringify(result, null, 2)}`
479
+ }
480
+ ]
481
+ };
482
+ }
483
+ default:
484
+ return {
485
+ content: [
486
+ {
487
+ type: 'text',
488
+ text: `❌ Unknown tool: ${name}. Available tools: create_memory, search_memories, get_memory, update_memory, delete_memory, list_memories, create_api_key, list_api_keys, get_health_status`
489
+ }
490
+ ]
491
+ };
492
+ }
493
+ }
494
+ catch (error) {
495
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
496
+ return {
497
+ content: [
498
+ {
499
+ type: 'text',
500
+ text: `❌ Error: ${errorMessage}`
501
+ }
502
+ ]
503
+ };
504
+ }
505
+ });
506
+ }
507
+ async run() {
508
+ const transport = new StdioServerTransport();
509
+ await this.server.connect(transport);
510
+ // Log to stderr that server is ready
511
+ console.error('[MCP-INFO] Lanonasis MCP Server started and ready');
512
+ }
513
+ }
514
+ // Start the server
515
+ const server = new LanonasisMCPServer();
516
+ server.run().catch((error) => {
517
+ console.error('[MCP-ERROR] Failed to start server:', error);
518
+ process.exit(1);
519
+ });
@@ -2,12 +2,53 @@ interface MCPConnectionOptions {
2
2
  serverPath?: string;
3
3
  serverUrl?: string;
4
4
  useRemote?: boolean;
5
+ useWebSocket?: boolean;
6
+ connectionMode?: 'local' | 'remote' | 'websocket';
7
+ }
8
+ /**
9
+ * Interface for MCP tool arguments
10
+ */
11
+ interface MCPToolArgs {
12
+ [key: string]: unknown;
13
+ }
14
+ /**
15
+ * Interface for MCP tool response
16
+ */
17
+ export interface MCPToolResponse {
18
+ result?: unknown;
19
+ error?: {
20
+ code: number;
21
+ message: string;
22
+ };
23
+ id?: string;
24
+ title?: string;
25
+ memory_type?: string;
26
+ length?: number;
27
+ forEach?: (callback: (item: any, index: number) => void) => void;
28
+ code?: number;
29
+ message?: string;
30
+ response?: any;
31
+ }
32
+ /**
33
+ * Interface for MCP WebSocket messages
34
+ */
35
+ export interface MCPWebSocketMessage {
36
+ id: number;
37
+ method?: string;
38
+ params?: Record<string, unknown>;
39
+ result?: Record<string, unknown>;
40
+ error?: {
41
+ code: number;
42
+ message: string;
43
+ data?: unknown;
44
+ };
5
45
  }
6
46
  export declare class MCPClient {
7
47
  private client;
8
48
  private config;
9
49
  private isConnected;
10
50
  private sseConnection;
51
+ private wsConnection;
11
52
  constructor();
12
53
  /**
13
54
  * Connect to MCP server (local or remote)
@@ -17,6 +58,14 @@ export declare class MCPClient {
17
58
  * Initialize SSE connection for real-time updates
18
59
  */
19
60
  private initializeSSE;
61
+ /**
62
+ * Initialize WebSocket connection for enterprise MCP server
63
+ */
64
+ private initializeWebSocket;
65
+ /**
66
+ * Send a message over the WebSocket connection
67
+ */
68
+ private sendWebSocketMessage;
20
69
  /**
21
70
  * Disconnect from MCP server
22
71
  */
@@ -24,7 +73,7 @@ export declare class MCPClient {
24
73
  /**
25
74
  * Call an MCP tool
26
75
  */
27
- callTool(toolName: string, args: any): Promise<any>;
76
+ callTool(toolName: string, args: MCPToolArgs): Promise<MCPToolResponse>;
28
77
  /**
29
78
  * Call remote tool via REST API with MCP interface
30
79
  */
@@ -5,6 +5,7 @@ import { CLIConfig } from './config.js';
5
5
  import * as path from 'path';
6
6
  import { EventSource } from 'eventsource';
7
7
  import { fileURLToPath } from 'url';
8
+ import WebSocket from 'ws';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
10
11
  export class MCPClient {
@@ -12,6 +13,7 @@ export class MCPClient {
12
13
  config;
13
14
  isConnected = false;
14
15
  sseConnection = null;
16
+ wsConnection = null;
15
17
  constructor() {
16
18
  this.config = new CLIConfig();
17
19
  }
@@ -20,34 +22,59 @@ export class MCPClient {
20
22
  */
21
23
  async connect(options = {}) {
22
24
  try {
23
- const useRemote = options.useRemote ?? this.config.get('mcpUseRemote') ?? false;
24
- if (useRemote) {
25
- // For remote MCP, we'll use the REST API with MCP-style interface
26
- const serverUrl = options.serverUrl ?? this.config.get('mcpServerUrl') ?? 'https://api.lanonasis.com';
27
- console.log(chalk.cyan(`Connecting to remote MCP server at ${serverUrl}...`));
28
- // Initialize SSE connection for real-time updates
29
- await this.initializeSSE(serverUrl);
30
- this.isConnected = true;
31
- return true;
32
- }
33
- else {
34
- // Local MCP server connection
35
- const serverPath = options.serverPath ?? this.config.get('mcpServerPath') ?? path.join(__dirname, '../../../../onasis-gateway/mcp-server/server.js');
36
- console.log(chalk.cyan(`Connecting to local MCP server at ${serverPath}...`));
37
- const transport = new StdioClientTransport({
38
- command: 'node',
39
- args: [serverPath]
40
- });
41
- this.client = new Client({
42
- name: '@lanonasis/cli',
43
- version: '1.0.0'
44
- }, {
45
- capabilities: {}
46
- });
47
- await this.client.connect(transport);
48
- this.isConnected = true;
49
- console.log(chalk.green('✓ Connected to MCP server'));
50
- return true;
25
+ // Determine connection mode with priority to explicit mode option
26
+ const connectionMode = options.connectionMode ??
27
+ (options.useWebSocket ? 'websocket' :
28
+ options.useRemote ? 'remote' :
29
+ this.config.get('mcpConnectionMode') ??
30
+ this.config.get('mcpUseRemote') ? 'remote' : 'local');
31
+ let wsUrl;
32
+ let serverUrl;
33
+ let serverPath;
34
+ switch (connectionMode) {
35
+ case 'websocket':
36
+ // WebSocket connection mode for enterprise users
37
+ wsUrl = options.serverUrl ??
38
+ this.config.get('mcpWebSocketUrl') ??
39
+ 'ws://localhost:8081/mcp/ws';
40
+ console.log(chalk.cyan(`Connecting to WebSocket MCP server at ${wsUrl}...`));
41
+ // Initialize WebSocket connection
42
+ await this.initializeWebSocket(wsUrl);
43
+ this.isConnected = true;
44
+ return true;
45
+ case 'remote':
46
+ // For remote MCP, we'll use the REST API with MCP-style interface
47
+ serverUrl = options.serverUrl ??
48
+ this.config.get('mcpServerUrl') ??
49
+ 'https://api.lanonasis.com';
50
+ console.log(chalk.cyan(`Connecting to remote MCP server at ${serverUrl}...`));
51
+ // Initialize SSE connection for real-time updates
52
+ await this.initializeSSE(serverUrl);
53
+ this.isConnected = true;
54
+ return true;
55
+ case 'local':
56
+ default:
57
+ {
58
+ // Local MCP server connection
59
+ serverPath = options.serverPath ??
60
+ this.config.get('mcpServerPath') ??
61
+ path.join(__dirname, '../../../../onasis-gateway/mcp-server/server.js');
62
+ console.log(chalk.cyan(`Connecting to local MCP server at ${serverPath}...`));
63
+ const localTransport = new StdioClientTransport({
64
+ command: 'node',
65
+ args: [serverPath]
66
+ });
67
+ this.client = new Client({
68
+ name: '@lanonasis/cli',
69
+ version: '1.0.0'
70
+ }, {
71
+ capabilities: {}
72
+ });
73
+ await this.client.connect(localTransport);
74
+ }
75
+ this.isConnected = true;
76
+ console.log(chalk.green('✓ Connected to MCP server'));
77
+ return true;
51
78
  }
52
79
  }
53
80
  catch (error) {
@@ -79,6 +106,87 @@ export class MCPClient {
79
106
  };
80
107
  }
81
108
  }
109
+ /**
110
+ * Initialize WebSocket connection for enterprise MCP server
111
+ */
112
+ async initializeWebSocket(wsUrl) {
113
+ const token = this.config.get('token');
114
+ if (!token) {
115
+ throw new Error('API key required for WebSocket mode. Set LANONASIS_API_KEY or login first.');
116
+ }
117
+ return new Promise((resolve, reject) => {
118
+ try {
119
+ // Close existing connection if any
120
+ if (this.wsConnection) {
121
+ this.wsConnection.close();
122
+ this.wsConnection = null;
123
+ }
124
+ // Create new WebSocket connection with authentication
125
+ this.wsConnection = new WebSocket(wsUrl, {
126
+ headers: {
127
+ 'Authorization': `Bearer ${token}`,
128
+ 'X-API-Key': token
129
+ }
130
+ });
131
+ this.wsConnection.on('open', () => {
132
+ console.log(chalk.green('✅ Connected to MCP WebSocket server'));
133
+ // Send initialization message
134
+ this.sendWebSocketMessage({
135
+ id: 1,
136
+ method: 'initialize',
137
+ params: {
138
+ protocolVersion: '2024-11-05',
139
+ capabilities: {
140
+ tools: ['memory_management', 'workflow_orchestration']
141
+ },
142
+ clientInfo: {
143
+ name: '@lanonasis/cli',
144
+ version: '1.1.0'
145
+ }
146
+ }
147
+ });
148
+ resolve();
149
+ });
150
+ this.wsConnection.on('message', (data) => {
151
+ try {
152
+ const message = JSON.parse(data.toString());
153
+ console.log(chalk.blue('📡 MCP message:'), message.id, message.method || 'response');
154
+ }
155
+ catch (error) {
156
+ console.error('Failed to parse WebSocket message:', error);
157
+ }
158
+ });
159
+ this.wsConnection.on('error', (error) => {
160
+ console.error(chalk.red('WebSocket error:'), error);
161
+ reject(error);
162
+ });
163
+ this.wsConnection.on('close', (code, reason) => {
164
+ console.log(chalk.yellow(`WebSocket connection closed (${code}): ${reason}`));
165
+ // Auto-reconnect after delay
166
+ setTimeout(() => {
167
+ if (this.isConnected) {
168
+ console.log(chalk.blue('🔄 Attempting to reconnect to WebSocket...'));
169
+ this.initializeWebSocket(wsUrl).catch(err => {
170
+ console.error('Failed to reconnect:', err);
171
+ });
172
+ }
173
+ }, 5000);
174
+ });
175
+ }
176
+ catch (error) {
177
+ reject(error);
178
+ }
179
+ });
180
+ }
181
+ /**
182
+ * Send a message over the WebSocket connection
183
+ */
184
+ sendWebSocketMessage(message) {
185
+ if (!this.wsConnection) {
186
+ throw new Error('WebSocket not connected');
187
+ }
188
+ this.wsConnection.send(JSON.stringify(message));
189
+ }
82
190
  /**
83
191
  * Disconnect from MCP server
84
192
  */
@@ -115,7 +223,12 @@ export class MCPClient {
115
223
  name: toolName,
116
224
  arguments: args
117
225
  });
118
- return result;
226
+ // Convert the SDK result to our expected MCPToolResponse format
227
+ return {
228
+ result: result,
229
+ code: 200,
230
+ message: 'Success'
231
+ };
119
232
  }
120
233
  catch (error) {
121
234
  throw new Error(`MCP tool call failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -177,7 +290,8 @@ export class MCPClient {
177
290
  // Handle dynamic endpoint for memory operations that need ID
178
291
  let endpoint = mapping.endpoint;
179
292
  if (endpoint.includes('{id}') && args.memory_id) {
180
- endpoint = endpoint.replace('{id}', args.memory_id);
293
+ // Ensure memory_id is treated as a string for replacement
294
+ endpoint = endpoint.replace('{id}', String(args.memory_id));
181
295
  }
182
296
  const response = await axios({
183
297
  method: mapping.method,
@@ -192,7 +306,11 @@ export class MCPClient {
192
306
  return response.data;
193
307
  }
194
308
  catch (error) {
195
- throw new Error(`Remote tool call failed: ${error.response?.data?.error || error.message}`);
309
+ // Safely handle errors with type checking
310
+ const errorObj = error;
311
+ const errorMsg = errorObj.response?.data?.error ||
312
+ (errorObj.message ? errorObj.message : 'Unknown error');
313
+ throw new Error(`Remote tool call failed: ${errorMsg}`);
196
314
  }
197
315
  }
198
316
  /**
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "description": "LanOnasis Enterprise CLI - Memory as a Service, API Key Management, and Infrastructure Orchestration",
5
5
  "main": "dist/index-simple.js",
6
6
  "bin": {
7
7
  "lanonasis": "dist/index-simple.js",
8
+ "lanonasis-mcp-server": "dist/mcp-server.js",
8
9
  "memory": "dist/index-simple.js",
9
10
  "maas": "dist/index-simple.js"
10
11
  },
@@ -40,6 +41,7 @@
40
41
  "dependencies": {
41
42
  "@modelcontextprotocol/sdk": "^1.17.0",
42
43
  "@types/eventsource": "^1.1.15",
44
+ "@types/ws": "^8.18.1",
43
45
  "axios": "^1.11.0",
44
46
  "chalk": "^5.4.1",
45
47
  "cli-table3": "^0.6.5",
@@ -52,7 +54,8 @@
52
54
  "open": "^10.2.0",
53
55
  "ora": "^8.2.0",
54
56
  "table": "^6.9.0",
55
- "word-wrap": "^1.2.5"
57
+ "word-wrap": "^1.2.5",
58
+ "ws": "^8.18.3"
56
59
  },
57
60
  "devDependencies": {
58
61
  "@types/inquirer": "^9.0.7",