@lanonasis/cli 2.0.7 → 3.0.1

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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * MCP Transport Manager
3
+ * Handles multiple transport types for MCP connections
4
+ */
5
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ export type TransportType = 'stdio' | 'http' | 'websocket' | 'sse';
7
+ export interface TransportConfig {
8
+ type: TransportType;
9
+ url?: string;
10
+ command?: string;
11
+ args?: string[];
12
+ headers?: Record<string, string>;
13
+ auth?: {
14
+ type: 'bearer' | 'apikey' | 'basic';
15
+ value: string;
16
+ };
17
+ reconnect?: {
18
+ enabled: boolean;
19
+ maxAttempts?: number;
20
+ delay?: number;
21
+ };
22
+ timeout?: number;
23
+ }
24
+ export interface Transport {
25
+ send(data: any): Promise<void>;
26
+ on(event: string, handler: Function): void;
27
+ off(event: string, handler: Function): void;
28
+ close(): Promise<void>;
29
+ isConnected(): boolean;
30
+ }
31
+ /**
32
+ * Transport Manager class
33
+ */
34
+ export declare class MCPTransportManager {
35
+ private transports;
36
+ private configs;
37
+ /**
38
+ * Create a transport based on configuration
39
+ */
40
+ createTransport(name: string, config: TransportConfig): Promise<Transport>;
41
+ /**
42
+ * Setup event forwarding for monitoring
43
+ */
44
+ private setupEventForwarding;
45
+ /**
46
+ * Get a transport by name
47
+ */
48
+ getTransport(name: string): Transport | undefined;
49
+ /**
50
+ * Create stdio transport helper
51
+ */
52
+ createStdioTransport(command: string, args?: string[]): Promise<StdioClientTransport>;
53
+ /**
54
+ * Create HTTP transport helper
55
+ */
56
+ createHttpTransport(url: string, headers?: Record<string, string>, auth?: TransportConfig['auth']): Promise<Transport>;
57
+ /**
58
+ * Create WebSocket transport helper
59
+ */
60
+ createWebSocketTransport(url: string, options?: {
61
+ headers?: Record<string, string>;
62
+ auth?: TransportConfig['auth'];
63
+ reconnect?: TransportConfig['reconnect'];
64
+ }): Promise<Transport>;
65
+ /**
66
+ * Close all transports
67
+ */
68
+ closeAll(): Promise<void>;
69
+ /**
70
+ * Close a specific transport
71
+ */
72
+ closeTransport(name: string): Promise<void>;
73
+ /**
74
+ * Get all transport statuses
75
+ */
76
+ getStatuses(): Record<string, boolean>;
77
+ /**
78
+ * Reconnect a transport
79
+ */
80
+ reconnect(name: string): Promise<Transport>;
81
+ }
82
+ export declare const transportManager: MCPTransportManager;
@@ -0,0 +1,434 @@
1
+ /**
2
+ * MCP Transport Manager
3
+ * Handles multiple transport types for MCP connections
4
+ */
5
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ import { EventEmitter } from 'events';
7
+ import WebSocket from 'ws';
8
+ import chalk from 'chalk';
9
+ /**
10
+ * StdIO Transport wrapper
11
+ */
12
+ class StdioTransport extends EventEmitter {
13
+ transport;
14
+ connected = false;
15
+ constructor(config) {
16
+ super();
17
+ if (!config.command) {
18
+ throw new Error('Command required for stdio transport');
19
+ }
20
+ this.transport = new StdioClientTransport({
21
+ command: config.command,
22
+ args: config.args || []
23
+ });
24
+ this.connected = true;
25
+ }
26
+ async send(data) {
27
+ if (!this.connected) {
28
+ throw new Error('Transport not connected');
29
+ }
30
+ // StdioClientTransport handles this internally
31
+ this.emit('send', data);
32
+ }
33
+ async close() {
34
+ this.connected = false;
35
+ this.removeAllListeners();
36
+ }
37
+ isConnected() {
38
+ return this.connected;
39
+ }
40
+ getInternalTransport() {
41
+ return this.transport;
42
+ }
43
+ }
44
+ /**
45
+ * HTTP Transport wrapper
46
+ */
47
+ class HttpTransport extends EventEmitter {
48
+ url;
49
+ headers;
50
+ connected = false;
51
+ auth;
52
+ constructor(config) {
53
+ super();
54
+ if (!config.url) {
55
+ throw new Error('URL required for HTTP transport');
56
+ }
57
+ this.url = config.url;
58
+ this.headers = config.headers || {};
59
+ this.auth = config.auth;
60
+ if (this.auth) {
61
+ this.setupAuthentication();
62
+ }
63
+ this.connected = true;
64
+ }
65
+ setupAuthentication() {
66
+ if (!this.auth)
67
+ return;
68
+ switch (this.auth.type) {
69
+ case 'bearer':
70
+ this.headers['Authorization'] = `Bearer ${this.auth.value}`;
71
+ break;
72
+ case 'apikey':
73
+ this.headers['X-API-Key'] = this.auth.value;
74
+ break;
75
+ case 'basic':
76
+ this.headers['Authorization'] = `Basic ${this.auth.value}`;
77
+ break;
78
+ }
79
+ }
80
+ async send(data) {
81
+ if (!this.connected) {
82
+ throw new Error('Transport not connected');
83
+ }
84
+ try {
85
+ const response = await fetch(this.url, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ ...this.headers
90
+ },
91
+ body: JSON.stringify(data)
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
95
+ }
96
+ const result = await response.json();
97
+ this.emit('message', result);
98
+ }
99
+ catch (error) {
100
+ this.emit('error', error);
101
+ throw error;
102
+ }
103
+ }
104
+ async close() {
105
+ this.connected = false;
106
+ this.removeAllListeners();
107
+ }
108
+ isConnected() {
109
+ return this.connected;
110
+ }
111
+ }
112
+ /**
113
+ * WebSocket Transport wrapper
114
+ */
115
+ class WebSocketTransport extends EventEmitter {
116
+ ws = null;
117
+ url;
118
+ connected = false;
119
+ reconnectConfig;
120
+ reconnectAttempts = 0;
121
+ reconnectTimer;
122
+ headers;
123
+ constructor(config) {
124
+ super();
125
+ if (!config.url) {
126
+ throw new Error('URL required for WebSocket transport');
127
+ }
128
+ this.url = config.url;
129
+ this.reconnectConfig = config.reconnect;
130
+ this.headers = config.headers || {};
131
+ if (config.auth) {
132
+ this.setupAuthentication(config.auth);
133
+ }
134
+ }
135
+ setupAuthentication(auth) {
136
+ if (!auth)
137
+ return;
138
+ switch (auth.type) {
139
+ case 'bearer':
140
+ this.headers['Authorization'] = `Bearer ${auth.value}`;
141
+ break;
142
+ case 'apikey':
143
+ this.headers['X-API-Key'] = auth.value;
144
+ break;
145
+ }
146
+ }
147
+ async connect() {
148
+ return new Promise((resolve, reject) => {
149
+ try {
150
+ this.ws = new WebSocket(this.url, {
151
+ headers: this.headers
152
+ });
153
+ this.ws.on('open', () => {
154
+ this.connected = true;
155
+ this.reconnectAttempts = 0;
156
+ this.emit('connected');
157
+ console.log(chalk.green(`✅ WebSocket connected to ${this.url}`));
158
+ resolve();
159
+ });
160
+ this.ws.on('message', (data) => {
161
+ try {
162
+ const message = JSON.parse(data.toString());
163
+ this.emit('message', message);
164
+ }
165
+ catch (error) {
166
+ this.emit('error', new Error('Failed to parse WebSocket message'));
167
+ }
168
+ });
169
+ this.ws.on('error', (error) => {
170
+ this.emit('error', error);
171
+ if (!this.connected) {
172
+ reject(error);
173
+ }
174
+ });
175
+ this.ws.on('close', (code, reason) => {
176
+ this.connected = false;
177
+ this.emit('disconnected', { code, reason: reason.toString() });
178
+ if (this.shouldReconnect()) {
179
+ this.scheduleReconnect();
180
+ }
181
+ });
182
+ // Set connection timeout
183
+ setTimeout(() => {
184
+ if (!this.connected) {
185
+ reject(new Error('WebSocket connection timeout'));
186
+ }
187
+ }, 10000);
188
+ }
189
+ catch (error) {
190
+ reject(error);
191
+ }
192
+ });
193
+ }
194
+ shouldReconnect() {
195
+ if (!this.reconnectConfig?.enabled)
196
+ return false;
197
+ const maxAttempts = this.reconnectConfig.maxAttempts || 5;
198
+ return this.reconnectAttempts < maxAttempts;
199
+ }
200
+ scheduleReconnect() {
201
+ const delay = this.reconnectConfig?.delay || 5000;
202
+ const backoff = Math.min(delay * Math.pow(2, this.reconnectAttempts), 30000);
203
+ this.reconnectAttempts++;
204
+ console.log(chalk.yellow(`⏳ Reconnecting WebSocket in ${backoff}ms (attempt ${this.reconnectAttempts})...`));
205
+ this.reconnectTimer = setTimeout(() => {
206
+ this.connect().catch(error => {
207
+ console.error(chalk.red('Reconnection failed:'), error);
208
+ });
209
+ }, backoff);
210
+ }
211
+ async send(data) {
212
+ if (!this.connected || !this.ws) {
213
+ throw new Error('WebSocket not connected');
214
+ }
215
+ return new Promise((resolve, reject) => {
216
+ this.ws.send(JSON.stringify(data), (error) => {
217
+ if (error) {
218
+ reject(error);
219
+ }
220
+ else {
221
+ resolve();
222
+ }
223
+ });
224
+ });
225
+ }
226
+ async close() {
227
+ if (this.reconnectTimer) {
228
+ clearTimeout(this.reconnectTimer);
229
+ }
230
+ if (this.ws) {
231
+ this.ws.close();
232
+ this.ws = null;
233
+ }
234
+ this.connected = false;
235
+ this.removeAllListeners();
236
+ }
237
+ isConnected() {
238
+ return this.connected;
239
+ }
240
+ }
241
+ /**
242
+ * Server-Sent Events Transport
243
+ */
244
+ class SSETransport extends EventEmitter {
245
+ eventSource = null;
246
+ url;
247
+ connected = false;
248
+ headers;
249
+ constructor(config) {
250
+ super();
251
+ if (!config.url) {
252
+ throw new Error('URL required for SSE transport');
253
+ }
254
+ this.url = config.url;
255
+ this.headers = config.headers || {};
256
+ }
257
+ async connect() {
258
+ // Dynamic import for EventSource
259
+ const { EventSource } = await import('eventsource');
260
+ // EventSource doesn't support headers directly
261
+ this.eventSource = new EventSource(this.url);
262
+ return new Promise((resolve, reject) => {
263
+ this.eventSource.onopen = () => {
264
+ this.connected = true;
265
+ this.emit('connected');
266
+ resolve();
267
+ };
268
+ this.eventSource.onmessage = (event) => {
269
+ try {
270
+ const data = JSON.parse(event.data);
271
+ this.emit('message', data);
272
+ }
273
+ catch (error) {
274
+ this.emit('error', new Error('Failed to parse SSE message'));
275
+ }
276
+ };
277
+ this.eventSource.onerror = (error) => {
278
+ this.emit('error', error);
279
+ if (!this.connected) {
280
+ reject(error);
281
+ }
282
+ };
283
+ });
284
+ }
285
+ async send(data) {
286
+ // SSE is receive-only, but we can send data via HTTP POST
287
+ throw new Error('SSE transport is read-only. Use HTTP for sending data.');
288
+ }
289
+ async close() {
290
+ if (this.eventSource) {
291
+ this.eventSource.close();
292
+ this.eventSource = null;
293
+ }
294
+ this.connected = false;
295
+ this.removeAllListeners();
296
+ }
297
+ isConnected() {
298
+ return this.connected;
299
+ }
300
+ }
301
+ /**
302
+ * Transport Manager class
303
+ */
304
+ export class MCPTransportManager {
305
+ transports = new Map();
306
+ configs = new Map();
307
+ /**
308
+ * Create a transport based on configuration
309
+ */
310
+ async createTransport(name, config) {
311
+ // Store config for potential reconnection
312
+ this.configs.set(name, config);
313
+ let transport;
314
+ switch (config.type) {
315
+ case 'stdio':
316
+ transport = new StdioTransport(config);
317
+ break;
318
+ case 'http':
319
+ transport = new HttpTransport(config);
320
+ break;
321
+ case 'websocket':
322
+ transport = new WebSocketTransport(config);
323
+ await transport.connect();
324
+ break;
325
+ case 'sse':
326
+ transport = new SSETransport(config);
327
+ await transport.connect();
328
+ break;
329
+ default:
330
+ throw new Error(`Unsupported transport type: ${config.type}`);
331
+ }
332
+ this.transports.set(name, transport);
333
+ // Setup event forwarding
334
+ this.setupEventForwarding(name, transport);
335
+ return transport;
336
+ }
337
+ /**
338
+ * Setup event forwarding for monitoring
339
+ */
340
+ setupEventForwarding(name, transport) {
341
+ transport.on('connected', () => {
342
+ console.log(chalk.green(`✅ Transport '${name}' connected`));
343
+ });
344
+ transport.on('disconnected', (reason) => {
345
+ console.log(chalk.yellow(`⚠️ Transport '${name}' disconnected:`, reason));
346
+ });
347
+ transport.on('error', (error) => {
348
+ console.log(chalk.red(`❌ Transport '${name}' error:`, error.message));
349
+ });
350
+ }
351
+ /**
352
+ * Get a transport by name
353
+ */
354
+ getTransport(name) {
355
+ return this.transports.get(name);
356
+ }
357
+ /**
358
+ * Create stdio transport helper
359
+ */
360
+ async createStdioTransport(command, args = []) {
361
+ const transport = new StdioTransport({
362
+ type: 'stdio',
363
+ command,
364
+ args
365
+ });
366
+ return transport.getInternalTransport();
367
+ }
368
+ /**
369
+ * Create HTTP transport helper
370
+ */
371
+ async createHttpTransport(url, headers, auth) {
372
+ return this.createTransport('http-default', {
373
+ type: 'http',
374
+ url,
375
+ headers,
376
+ auth
377
+ });
378
+ }
379
+ /**
380
+ * Create WebSocket transport helper
381
+ */
382
+ async createWebSocketTransport(url, options) {
383
+ return this.createTransport('websocket-default', {
384
+ type: 'websocket',
385
+ url,
386
+ ...options
387
+ });
388
+ }
389
+ /**
390
+ * Close all transports
391
+ */
392
+ async closeAll() {
393
+ const closePromises = Array.from(this.transports.values()).map(transport => transport.close());
394
+ await Promise.all(closePromises);
395
+ this.transports.clear();
396
+ this.configs.clear();
397
+ }
398
+ /**
399
+ * Close a specific transport
400
+ */
401
+ async closeTransport(name) {
402
+ const transport = this.transports.get(name);
403
+ if (transport) {
404
+ await transport.close();
405
+ this.transports.delete(name);
406
+ this.configs.delete(name);
407
+ }
408
+ }
409
+ /**
410
+ * Get all transport statuses
411
+ */
412
+ getStatuses() {
413
+ const statuses = {};
414
+ for (const [name, transport] of this.transports) {
415
+ statuses[name] = transport.isConnected();
416
+ }
417
+ return statuses;
418
+ }
419
+ /**
420
+ * Reconnect a transport
421
+ */
422
+ async reconnect(name) {
423
+ const config = this.configs.get(name);
424
+ if (!config) {
425
+ throw new Error(`No configuration found for transport: ${name}`);
426
+ }
427
+ // Close existing if any
428
+ await this.closeTransport(name);
429
+ // Create new transport
430
+ return this.createTransport(name, config);
431
+ }
432
+ }
433
+ // Export singleton instance
434
+ export const transportManager = new MCPTransportManager();
package/dist/utils/api.js CHANGED
@@ -15,9 +15,14 @@ export class APIClient {
15
15
  await this.config.discoverServices();
16
16
  // Use appropriate base URL based on endpoint
17
17
  const isAuthEndpoint = config.url?.includes('/auth/') || config.url?.includes('/login') || config.url?.includes('/register');
18
+ const discoveredServices = this.config.get('discoveredServices');
18
19
  config.baseURL = isAuthEndpoint ?
19
- (this.config.get('discoveredServices')?.auth_base || 'https://api.lanonasis.com/auth') :
20
+ (discoveredServices?.auth_base || 'https://api.lanonasis.com') :
20
21
  this.config.getApiUrl();
22
+ // Add project scope header for auth endpoints
23
+ if (isAuthEndpoint) {
24
+ config.headers['X-Project-Scope'] = 'lanonasis-maas';
25
+ }
21
26
  // Enhanced Authentication Support
22
27
  const token = this.config.getToken();
23
28
  const vendorKey = this.config.getVendorKey();
@@ -77,17 +82,17 @@ export class APIClient {
77
82
  }
78
83
  // Authentication - aligned with Supabase auth
79
84
  async login(email, password) {
80
- const response = await this.client.post('/api/v1/auth/login', {
85
+ const response = await this.client.post('/v1/auth/login', {
81
86
  email,
82
87
  password
83
88
  });
84
89
  return response.data;
85
90
  }
86
91
  async register(email, password, organizationName) {
87
- const response = await this.client.post('/api/v1/auth/register', {
92
+ const response = await this.client.post('/v1/auth/signup', {
88
93
  email,
89
94
  password,
90
- organization_name: organizationName
95
+ name: organizationName
91
96
  });
92
97
  return response.data;
93
98
  }
@@ -27,9 +27,9 @@ export declare class CLIConfig {
27
27
  clear(): Promise<void>;
28
28
  getConfigPath(): string;
29
29
  exists(): Promise<boolean>;
30
- get(key: string): any;
31
- set(key: string, value: any): void;
32
- setAndSave(key: string, value: any): Promise<void>;
30
+ get<T = unknown>(key: string): T;
31
+ set(key: string, value: unknown): void;
32
+ setAndSave(key: string, value: unknown): Promise<void>;
33
33
  getMCPServerPath(): string;
34
34
  getMCPServerUrl(): string;
35
35
  shouldUseRemoteMCP(): boolean;
@@ -118,6 +118,22 @@ export class CLIConfig {
118
118
  const token = this.getToken();
119
119
  if (!token)
120
120
  return false;
121
+ // Handle simple CLI tokens (format: cli_xxx_timestamp)
122
+ if (token.startsWith('cli_')) {
123
+ // Extract timestamp from CLI token
124
+ const parts = token.split('_');
125
+ if (parts.length >= 3) {
126
+ const timestamp = parseInt(parts[parts.length - 1]);
127
+ if (!isNaN(timestamp)) {
128
+ // CLI tokens are valid for 30 days
129
+ const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000;
130
+ return (Date.now() - timestamp) < thirtyDaysInMs;
131
+ }
132
+ }
133
+ // If we can't parse timestamp, assume valid (fallback)
134
+ return true;
135
+ }
136
+ // Handle JWT tokens
121
137
  try {
122
138
  const decoded = jwtDecode(token);
123
139
  const now = Date.now() / 1000;
@@ -50,6 +50,10 @@ export declare class MCPClient {
50
50
  private sseConnection;
51
51
  private wsConnection;
52
52
  constructor();
53
+ /**
54
+ * Initialize the MCP client configuration
55
+ */
56
+ init(): Promise<void>;
53
57
  /**
54
58
  * Connect to MCP server (local or remote)
55
59
  */