@lanonasis/cli 1.4.1 → 1.5.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.
@@ -5,7 +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
+ import { output } from './output.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
  export class MCPClient {
@@ -13,7 +13,7 @@ export class MCPClient {
13
13
  config;
14
14
  isConnected = false;
15
15
  sseConnection = null;
16
- wsConnection = null;
16
+ wsClient = null;
17
17
  constructor() {
18
18
  this.config = new CLIConfig();
19
19
  }
@@ -22,67 +22,81 @@ export class MCPClient {
22
22
  */
23
23
  async connect(options = {}) {
24
24
  try {
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
25
+ // Support new mode parameter or fallback to legacy useRemote
26
+ const mode = options.mode ?? (options.useRemote ? 'remote' : 'local');
27
+ const useRemote = mode !== 'local';
28
+ if (useRemote) {
29
+ if (mode === 'websocket') {
30
+ // WebSocket MCP connection for enterprise users
31
+ const wsUrl = options.serverUrl ?? 'wss://mcp.lanonasis.com/mcp';
32
+ if (!output.isSilent()) {
33
+ output.log(chalk.cyan(`Connecting to WebSocket MCP server at ${wsUrl}...`));
34
+ }
35
+ // Initialize WebSocket MCP connection
42
36
  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}...`));
37
+ }
38
+ else {
39
+ // SSE MCP connection for regular users
40
+ const serverUrl = options.serverUrl ?? this.config.get('mcpServerUrl') ?? 'https://api.lanonasis.com';
41
+ if (!output.isSilent()) {
42
+ output.log(chalk.cyan(`Connecting to remote MCP server at ${serverUrl}...`));
43
+ }
51
44
  // Initialize SSE connection for real-time updates
52
45
  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;
46
+ }
47
+ this.isConnected = true;
48
+ return true;
49
+ }
50
+ else {
51
+ // Local MCP server connection
52
+ const serverPath = options.serverPath ?? this.config.get('mcpServerPath') ?? path.join(__dirname, '../../../../onasis-gateway/mcp-server/server.js');
53
+ if (!output.isSilent()) {
54
+ output.log(chalk.cyan(`Connecting to local MCP server at ${serverPath}...`));
55
+ }
56
+ const transport = new StdioClientTransport({
57
+ command: 'node',
58
+ args: [serverPath]
59
+ });
60
+ this.client = new Client({
61
+ name: '@lanonasis/cli',
62
+ version: '1.0.0'
63
+ }, {
64
+ capabilities: {}
65
+ });
66
+ await this.client.connect(transport);
67
+ this.isConnected = true;
68
+ if (!output.isSilent()) {
69
+ output.log(chalk.green('✓ Connected to MCP server'));
70
+ }
71
+ return true;
78
72
  }
79
73
  }
80
74
  catch (error) {
81
- console.error(chalk.red('Failed to connect to MCP server:'), error);
75
+ if (!output.isSilent()) {
76
+ console.error(chalk.red('Failed to connect to MCP server:'), error);
77
+ }
82
78
  this.isConnected = false;
83
79
  return false;
84
80
  }
85
81
  }
82
+ /**
83
+ * Initialize WebSocket MCP connection for enterprise users
84
+ */
85
+ async initializeWebSocket(wsUrl) {
86
+ const { WebSocketMCPClient } = await import('./websocket-mcp-client.js');
87
+ const apiKey = this.config.get('token');
88
+ if (!apiKey) {
89
+ throw new Error('API key required for WebSocket MCP connection');
90
+ }
91
+ this.wsClient = new WebSocketMCPClient({
92
+ url: wsUrl,
93
+ apiKey: apiKey
94
+ });
95
+ await this.wsClient.connect();
96
+ if (!output.isSilent()) {
97
+ output.log(chalk.green('✅ WebSocket MCP connection established'));
98
+ }
99
+ }
86
100
  /**
87
101
  * Initialize SSE connection for real-time updates
88
102
  */
@@ -95,97 +109,20 @@ export class MCPClient {
95
109
  this.sseConnection.onmessage = (event) => {
96
110
  try {
97
111
  const data = JSON.parse(event.data);
98
- console.log(chalk.blue('📡 Real-time update:'), data.type);
112
+ if (!output.isSilent() && process.env.CLI_VERBOSE === 'true') {
113
+ output.log(chalk.blue('📡 Real-time update:'), data.type);
114
+ }
99
115
  }
100
- catch {
116
+ catch (error) {
101
117
  // Ignore parse errors
102
118
  }
103
119
  };
104
- this.sseConnection.onerror = () => {
105
- console.error(chalk.yellow('⚠️ SSE connection error (will retry)'));
106
- };
107
- }
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;
120
+ this.sseConnection.onerror = (error) => {
121
+ if (!output.isSilent() && process.env.CLI_VERBOSE === 'true') {
122
+ console.error(chalk.yellow('⚠️ SSE connection error (will retry)'));
123
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');
124
+ };
187
125
  }
188
- this.wsConnection.send(JSON.stringify(message));
189
126
  }
190
127
  /**
191
128
  * Disconnect from MCP server
@@ -223,12 +160,7 @@ export class MCPClient {
223
160
  name: toolName,
224
161
  arguments: args
225
162
  });
226
- // Convert the SDK result to our expected MCPToolResponse format
227
- return {
228
- result: result,
229
- code: 200,
230
- message: 'Success'
231
- };
163
+ return result;
232
164
  }
233
165
  catch (error) {
234
166
  throw new Error(`MCP tool call failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -258,21 +190,20 @@ export class MCPClient {
258
190
  },
259
191
  'memory_get_memory': {
260
192
  method: 'GET',
261
- endpoint: '/api/v1/memory/{id}',
193
+ endpoint: `/api/v1/memory/${args.memory_id}`,
262
194
  transform: () => undefined
263
195
  },
264
196
  'memory_update_memory': {
265
197
  method: 'PUT',
266
- endpoint: '/api/v1/memory/{id}',
198
+ endpoint: `/api/v1/memory/${args.memory_id}`,
267
199
  transform: (args) => {
268
- const data = { ...args };
269
- delete data.memory_id;
200
+ const { memory_id, ...data } = args;
270
201
  return data;
271
202
  }
272
203
  },
273
204
  'memory_delete_memory': {
274
205
  method: 'DELETE',
275
- endpoint: '/api/v1/memory/{id}',
206
+ endpoint: `/api/v1/memory/${args.memory_id}`,
276
207
  transform: () => undefined
277
208
  },
278
209
  'memory_list_memories': {
@@ -287,15 +218,9 @@ export class MCPClient {
287
218
  }
288
219
  try {
289
220
  const axios = (await import('axios')).default;
290
- // Handle dynamic endpoint for memory operations that need ID
291
- let endpoint = mapping.endpoint;
292
- if (endpoint.includes('{id}') && args.memory_id) {
293
- // Ensure memory_id is treated as a string for replacement
294
- endpoint = endpoint.replace('{id}', String(args.memory_id));
295
- }
296
221
  const response = await axios({
297
222
  method: mapping.method,
298
- url: `${apiUrl}${endpoint}`,
223
+ url: `${apiUrl}${mapping.endpoint}`,
299
224
  headers: {
300
225
  'Authorization': `Bearer ${token}`,
301
226
  'Content-Type': 'application/json'
@@ -306,11 +231,7 @@ export class MCPClient {
306
231
  return response.data;
307
232
  }
308
233
  catch (error) {
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}`);
234
+ throw new Error(`Remote tool call failed: ${error.response?.data?.error || error.message}`);
314
235
  }
315
236
  }
316
237
  /**
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, beforeEach } from 'bun:test';
2
+ import { MCPClient } from './mcp-client';
3
+ describe('MCPClient', () => {
4
+ let client;
5
+ beforeEach(() => {
6
+ client = new MCPClient();
7
+ });
8
+ describe('constructor', () => {
9
+ it('should create client instance', () => {
10
+ expect(client).toBeDefined();
11
+ expect(client.isConnected()).toBe(false);
12
+ });
13
+ });
14
+ describe('connection modes', () => {
15
+ it('should support local connection mode', async () => {
16
+ const result = await client.connect({ mode: 'local' });
17
+ // Local mode may fail without actual server, but should not throw
18
+ expect(typeof result).toBe('boolean');
19
+ });
20
+ it('should support remote connection mode', async () => {
21
+ const result = await client.connect({ mode: 'remote' });
22
+ // Remote mode may fail without credentials, but should not throw
23
+ expect(typeof result).toBe('boolean');
24
+ });
25
+ it('should support websocket connection mode', async () => {
26
+ const result = await client.connect({ mode: 'websocket' });
27
+ // WebSocket mode may fail without server, but should not throw
28
+ expect(typeof result).toBe('boolean');
29
+ });
30
+ });
31
+ describe('connection options', () => {
32
+ it('should handle server path option', async () => {
33
+ const options = {
34
+ mode: 'local',
35
+ serverPath: '/custom/path/to/server'
36
+ };
37
+ const result = await client.connect(options);
38
+ expect(typeof result).toBe('boolean');
39
+ });
40
+ it('should handle server URL option', async () => {
41
+ const options = {
42
+ mode: 'websocket',
43
+ serverUrl: 'ws://custom.server.com:8081'
44
+ };
45
+ const result = await client.connect(options);
46
+ expect(typeof result).toBe('boolean');
47
+ });
48
+ });
49
+ describe('tool operations', () => {
50
+ it('should handle list tools request', async () => {
51
+ // Mock successful connection first
52
+ client.isConnected = () => true;
53
+ try {
54
+ const tools = await client.listTools();
55
+ expect(Array.isArray(tools)).toBe(true);
56
+ }
57
+ catch (error) {
58
+ // Expected if no actual connection
59
+ expect(error).toBeDefined();
60
+ }
61
+ });
62
+ it('should handle call tool request', async () => {
63
+ // Mock successful connection first
64
+ client.isConnected = () => true;
65
+ try {
66
+ const result = await client.callTool('test_tool', { param: 'value' });
67
+ expect(result).toBeDefined();
68
+ }
69
+ catch (error) {
70
+ // Expected if no actual connection
71
+ expect(error).toBeDefined();
72
+ }
73
+ });
74
+ });
75
+ describe('memory operations', () => {
76
+ it('should handle create memory request', async () => {
77
+ // Mock successful connection first
78
+ client.isConnected = () => true;
79
+ try {
80
+ const result = await client.createMemory({
81
+ title: 'Test Memory',
82
+ content: 'Test content',
83
+ tags: ['test']
84
+ });
85
+ expect(result).toBeDefined();
86
+ }
87
+ catch (error) {
88
+ // Expected if no actual connection
89
+ expect(error).toBeDefined();
90
+ }
91
+ });
92
+ it('should handle search memories request', async () => {
93
+ // Mock successful connection first
94
+ client.isConnected = () => true;
95
+ try {
96
+ const result = await client.searchMemories({
97
+ query: 'test query',
98
+ limit: 10
99
+ });
100
+ expect(result).toBeDefined();
101
+ }
102
+ catch (error) {
103
+ // Expected if no actual connection
104
+ expect(error).toBeDefined();
105
+ }
106
+ });
107
+ });
108
+ describe('error handling', () => {
109
+ it('should handle connection failures gracefully', async () => {
110
+ const result = await client.connect({
111
+ mode: 'websocket',
112
+ serverUrl: 'ws://nonexistent.server:9999'
113
+ });
114
+ expect(result).toBe(false);
115
+ });
116
+ it('should handle invalid connection modes', async () => {
117
+ try {
118
+ await client.connect({ mode: 'invalid' });
119
+ }
120
+ catch (error) {
121
+ expect(error).toBeDefined();
122
+ }
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,23 @@
1
+ export interface OutputOptions {
2
+ format?: string;
3
+ silent?: boolean;
4
+ json?: boolean;
5
+ }
6
+ export declare class OutputManager {
7
+ private static instance;
8
+ private options;
9
+ static getInstance(): OutputManager;
10
+ setOptions(options: OutputOptions): void;
11
+ isJsonOutput(): boolean;
12
+ isSilent(): boolean;
13
+ log(...args: any[]): void;
14
+ error(...args: any[]): void;
15
+ json(data: any): void;
16
+ table(data: any[]): void;
17
+ success(message: string): void;
18
+ warning(message: string): void;
19
+ info(message: string): void;
20
+ }
21
+ export declare const output: OutputManager;
22
+ export declare function showOutput(format?: string): boolean;
23
+ export declare function formatOutput(data: any, format?: string): string;
@@ -0,0 +1,97 @@
1
+ import chalk from 'chalk';
2
+ export class OutputManager {
3
+ static instance;
4
+ options = {};
5
+ static getInstance() {
6
+ if (!OutputManager.instance) {
7
+ OutputManager.instance = new OutputManager();
8
+ }
9
+ return OutputManager.instance;
10
+ }
11
+ setOptions(options) {
12
+ this.options = { ...this.options, ...options };
13
+ }
14
+ isJsonOutput() {
15
+ return this.options.format === 'json' ||
16
+ this.options.json === true ||
17
+ process.env.CLI_OUTPUT_FORMAT === 'json';
18
+ }
19
+ isSilent() {
20
+ return this.options.silent === true ||
21
+ this.isJsonOutput() ||
22
+ process.env.CLI_SILENT === 'true';
23
+ }
24
+ log(...args) {
25
+ if (!this.isSilent()) {
26
+ console.log(...args);
27
+ }
28
+ }
29
+ error(...args) {
30
+ if (this.isJsonOutput()) {
31
+ console.error(JSON.stringify({
32
+ error: true,
33
+ message: args.join(' ').replace(/\x1b\[[0-9;]*m/g, '') // Strip ANSI codes
34
+ }));
35
+ }
36
+ else {
37
+ console.error(...args);
38
+ }
39
+ }
40
+ json(data) {
41
+ console.log(JSON.stringify(data, null, 2));
42
+ }
43
+ table(data) {
44
+ if (this.isJsonOutput()) {
45
+ this.json(data);
46
+ }
47
+ else {
48
+ console.table(data);
49
+ }
50
+ }
51
+ success(message) {
52
+ if (this.isJsonOutput()) {
53
+ this.json({ success: true, message });
54
+ }
55
+ else {
56
+ this.log(chalk.green('✓'), message);
57
+ }
58
+ }
59
+ warning(message) {
60
+ if (this.isJsonOutput()) {
61
+ this.json({ warning: true, message });
62
+ }
63
+ else {
64
+ this.log(chalk.yellow('⚠️'), message);
65
+ }
66
+ }
67
+ info(message) {
68
+ if (!this.isSilent()) {
69
+ if (this.isJsonOutput()) {
70
+ this.json({ info: true, message });
71
+ }
72
+ else {
73
+ this.log(chalk.blue('ℹ'), message);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ export const output = OutputManager.getInstance();
79
+ // Helper to conditionally show output
80
+ export function showOutput(format) {
81
+ return format !== 'json' && process.env.CLI_OUTPUT_FORMAT !== 'json';
82
+ }
83
+ // Helper to format output based on format
84
+ export function formatOutput(data, format) {
85
+ const outputFormat = format || process.env.CLI_OUTPUT_FORMAT || 'table';
86
+ switch (outputFormat) {
87
+ case 'json':
88
+ return JSON.stringify(data, null, 2);
89
+ case 'yaml':
90
+ // Simple YAML-like format
91
+ return Object.entries(data)
92
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
93
+ .join('\n');
94
+ default:
95
+ return data;
96
+ }
97
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * WebSocket MCP Client for Enterprise Connections
3
+ * Connects to mcp.lanonasis.com WebSocket server for real-time MCP operations
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ export interface MCPParams {
7
+ [key: string]: unknown;
8
+ }
9
+ export interface MCPResult {
10
+ [key: string]: unknown;
11
+ }
12
+ export interface MCPError {
13
+ code: number;
14
+ message: string;
15
+ data?: unknown;
16
+ }
17
+ export interface MCPMessage {
18
+ id?: string;
19
+ type: 'request' | 'response' | 'notification' | 'error';
20
+ method?: string;
21
+ params?: MCPParams;
22
+ result?: MCPResult;
23
+ error?: MCPError;
24
+ }
25
+ export interface WebSocketMCPClientOptions {
26
+ url?: string;
27
+ apiKey: string;
28
+ reconnectInterval?: number;
29
+ maxReconnectAttempts?: number;
30
+ timeout?: number;
31
+ }
32
+ export declare class WebSocketMCPClient extends EventEmitter {
33
+ private ws;
34
+ private url;
35
+ private apiKey;
36
+ private reconnectInterval;
37
+ private maxReconnectAttempts;
38
+ private timeout;
39
+ private reconnectAttempts;
40
+ private isConnected;
41
+ private messageId;
42
+ private pendingRequests;
43
+ constructor(options: WebSocketMCPClientOptions);
44
+ connect(): Promise<void>;
45
+ private handleMessage;
46
+ sendRequest(method: string, params?: MCPParams): Promise<MCPResult>;
47
+ listTools(): Promise<MCPResult>;
48
+ callTool(name: string, arguments_?: MCPParams): Promise<MCPResult>;
49
+ deleteMemory(id: string): Promise<MCPResult>;
50
+ searchMemories(args: MCPParams): Promise<MCPResult>;
51
+ listResources(): Promise<MCPResult>;
52
+ getMemories(query?: string): Promise<MCPResult>;
53
+ disconnect(): void;
54
+ getConnectionStatus(): {
55
+ connected: boolean;
56
+ url: string;
57
+ reconnectAttempts: number;
58
+ pendingRequests: number;
59
+ };
60
+ }