@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,696 @@
1
+ /**
2
+ * Lanonasis MCP Server Implementation
3
+ * Provides MCP protocol access to Lanonasis MaaS functionality
4
+ */
5
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { CLIConfig } from '../../utils/config.js';
8
+ import { APIClient } from '../../utils/api.js';
9
+ import chalk from 'chalk';
10
+ export class LanonasisMCPServer {
11
+ server;
12
+ config;
13
+ apiClient;
14
+ transport = null;
15
+ options;
16
+ constructor(options = {}) {
17
+ this.options = options;
18
+ // Initialize server with metadata
19
+ this.server = new Server({
20
+ name: options.name || "lanonasis-maas-server",
21
+ version: options.version || "3.0.1"
22
+ }, {
23
+ capabilities: {
24
+ tools: {},
25
+ resources: {},
26
+ prompts: {}
27
+ }
28
+ });
29
+ // Initialize config and API client
30
+ this.config = new CLIConfig();
31
+ this.apiClient = new APIClient();
32
+ // Register all tools, resources, and prompts
33
+ this.registerTools();
34
+ this.registerResources();
35
+ this.registerPrompts();
36
+ // Setup error handling
37
+ this.setupErrorHandling();
38
+ }
39
+ /**
40
+ * Initialize the server
41
+ */
42
+ async initialize() {
43
+ // Initialize configuration
44
+ await this.config.init();
45
+ // Override with options if provided
46
+ if (this.options.apiUrl) {
47
+ await this.config.setApiUrl(this.options.apiUrl);
48
+ }
49
+ if (this.options.token) {
50
+ await this.config.setToken(this.options.token);
51
+ }
52
+ // Initialize API client with config
53
+ const apiUrl = this.config.getApiUrl();
54
+ const token = this.config.getToken();
55
+ if (apiUrl) {
56
+ this.apiClient = new APIClient();
57
+ // APIClient will use the config internally
58
+ }
59
+ if (this.options.verbose) {
60
+ console.log(chalk.cyan('šŸš€ Lanonasis MCP Server initialized'));
61
+ console.log(chalk.gray(`API URL: ${apiUrl}`));
62
+ console.log(chalk.gray(`Authenticated: ${token ? 'Yes' : 'No'}`));
63
+ }
64
+ }
65
+ /**
66
+ * Register MCP tools
67
+ */
68
+ registerTools() {
69
+ // Memory operations
70
+ this.server.setRequestHandler({ method: 'tools/list' }, async () => ({
71
+ tools: [
72
+ // Memory tools
73
+ {
74
+ name: 'memory_create',
75
+ description: 'Create a new memory entry',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ title: {
80
+ type: 'string',
81
+ description: 'Memory title'
82
+ },
83
+ content: {
84
+ type: 'string',
85
+ description: 'Memory content'
86
+ },
87
+ memory_type: {
88
+ type: 'string',
89
+ enum: ['context', 'reference', 'note'],
90
+ default: 'context',
91
+ description: 'Type of memory'
92
+ },
93
+ tags: {
94
+ type: 'array',
95
+ items: { type: 'string' },
96
+ description: 'Optional tags for the memory'
97
+ }
98
+ },
99
+ required: ['title', 'content']
100
+ }
101
+ },
102
+ {
103
+ name: 'memory_search',
104
+ description: 'Search memories using semantic search',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ query: {
109
+ type: 'string',
110
+ description: 'Search query'
111
+ },
112
+ limit: {
113
+ type: 'number',
114
+ default: 10,
115
+ description: 'Maximum number of results'
116
+ },
117
+ threshold: {
118
+ type: 'number',
119
+ minimum: 0,
120
+ maximum: 1,
121
+ default: 0.7,
122
+ description: 'Similarity threshold'
123
+ }
124
+ },
125
+ required: ['query']
126
+ }
127
+ },
128
+ {
129
+ name: 'memory_list',
130
+ description: 'List all memory entries',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ limit: {
135
+ type: 'number',
136
+ default: 20,
137
+ description: 'Maximum number of results'
138
+ },
139
+ offset: {
140
+ type: 'number',
141
+ default: 0,
142
+ description: 'Pagination offset'
143
+ },
144
+ topic_id: {
145
+ type: 'string',
146
+ description: 'Filter by topic ID'
147
+ }
148
+ }
149
+ }
150
+ },
151
+ {
152
+ name: 'memory_get',
153
+ description: 'Get a specific memory by ID',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {
157
+ memory_id: {
158
+ type: 'string',
159
+ description: 'Memory ID'
160
+ }
161
+ },
162
+ required: ['memory_id']
163
+ }
164
+ },
165
+ {
166
+ name: 'memory_update',
167
+ description: 'Update an existing memory',
168
+ inputSchema: {
169
+ type: 'object',
170
+ properties: {
171
+ memory_id: {
172
+ type: 'string',
173
+ description: 'Memory ID'
174
+ },
175
+ title: {
176
+ type: 'string',
177
+ description: 'New title (optional)'
178
+ },
179
+ content: {
180
+ type: 'string',
181
+ description: 'New content (optional)'
182
+ },
183
+ tags: {
184
+ type: 'array',
185
+ items: { type: 'string' },
186
+ description: 'New tags (optional)'
187
+ }
188
+ },
189
+ required: ['memory_id']
190
+ }
191
+ },
192
+ {
193
+ name: 'memory_delete',
194
+ description: 'Delete a memory',
195
+ inputSchema: {
196
+ type: 'object',
197
+ properties: {
198
+ memory_id: {
199
+ type: 'string',
200
+ description: 'Memory ID'
201
+ }
202
+ },
203
+ required: ['memory_id']
204
+ }
205
+ },
206
+ // Topic tools
207
+ {
208
+ name: 'topic_create',
209
+ description: 'Create a new topic',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ name: {
214
+ type: 'string',
215
+ description: 'Topic name'
216
+ },
217
+ description: {
218
+ type: 'string',
219
+ description: 'Topic description'
220
+ }
221
+ },
222
+ required: ['name']
223
+ }
224
+ },
225
+ {
226
+ name: 'topic_list',
227
+ description: 'List all topics',
228
+ inputSchema: {
229
+ type: 'object',
230
+ properties: {
231
+ limit: {
232
+ type: 'number',
233
+ default: 20
234
+ }
235
+ }
236
+ }
237
+ },
238
+ // API Key tools
239
+ {
240
+ name: 'apikey_create',
241
+ description: 'Create a new API key',
242
+ inputSchema: {
243
+ type: 'object',
244
+ properties: {
245
+ name: {
246
+ type: 'string',
247
+ description: 'API key name'
248
+ },
249
+ permissions: {
250
+ type: 'array',
251
+ items: {
252
+ type: 'string',
253
+ enum: ['read', 'write', 'delete']
254
+ },
255
+ default: ['read']
256
+ }
257
+ },
258
+ required: ['name']
259
+ }
260
+ },
261
+ {
262
+ name: 'apikey_list',
263
+ description: 'List all API keys',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {}
267
+ }
268
+ },
269
+ // System tools
270
+ {
271
+ name: 'system_health',
272
+ description: 'Check system health status',
273
+ inputSchema: {
274
+ type: 'object',
275
+ properties: {
276
+ verbose: {
277
+ type: 'boolean',
278
+ default: false,
279
+ description: 'Include detailed diagnostics'
280
+ }
281
+ }
282
+ }
283
+ },
284
+ {
285
+ name: 'system_config',
286
+ description: 'Get or update system configuration',
287
+ inputSchema: {
288
+ type: 'object',
289
+ properties: {
290
+ action: {
291
+ type: 'string',
292
+ enum: ['get', 'set'],
293
+ default: 'get'
294
+ },
295
+ key: {
296
+ type: 'string',
297
+ description: 'Configuration key'
298
+ },
299
+ value: {
300
+ type: 'string',
301
+ description: 'Configuration value (for set action)'
302
+ }
303
+ }
304
+ }
305
+ }
306
+ ]
307
+ }));
308
+ // Tool call handler
309
+ this.server.setRequestHandler({ method: 'tools/call' }, async (request) => {
310
+ const { name, arguments: args } = request.params;
311
+ try {
312
+ const result = await this.handleToolCall(name, args);
313
+ return {
314
+ content: [
315
+ {
316
+ type: 'text',
317
+ text: JSON.stringify(result, null, 2)
318
+ }
319
+ ]
320
+ };
321
+ }
322
+ catch (error) {
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
328
+ }
329
+ ],
330
+ isError: true
331
+ };
332
+ }
333
+ });
334
+ }
335
+ /**
336
+ * Register MCP resources
337
+ */
338
+ registerResources() {
339
+ this.server.setRequestHandler({ method: 'resources/list' }, async () => ({
340
+ resources: [
341
+ {
342
+ uri: 'memory://recent',
343
+ name: 'Recent Memories',
344
+ description: 'List of recently created or updated memories',
345
+ mimeType: 'application/json'
346
+ },
347
+ {
348
+ uri: 'memory://search',
349
+ name: 'Memory Search',
350
+ description: 'Search interface for memories',
351
+ mimeType: 'application/json'
352
+ },
353
+ {
354
+ uri: 'config://current',
355
+ name: 'Current Configuration',
356
+ description: 'Current CLI configuration settings',
357
+ mimeType: 'application/json'
358
+ },
359
+ {
360
+ uri: 'stats://usage',
361
+ name: 'Usage Statistics',
362
+ description: 'Memory usage and API statistics',
363
+ mimeType: 'application/json'
364
+ }
365
+ ]
366
+ }));
367
+ this.server.setRequestHandler({ method: 'resources/read' }, async (request) => {
368
+ const { uri } = request.params;
369
+ try {
370
+ const content = await this.handleResourceRead(uri);
371
+ return {
372
+ contents: [
373
+ {
374
+ uri,
375
+ mimeType: 'application/json',
376
+ text: JSON.stringify(content, null, 2)
377
+ }
378
+ ]
379
+ };
380
+ }
381
+ catch (error) {
382
+ throw new Error(`Failed to read resource ${uri}: ${error}`);
383
+ }
384
+ });
385
+ }
386
+ /**
387
+ * Register MCP prompts
388
+ */
389
+ registerPrompts() {
390
+ this.server.setRequestHandler({ method: 'prompts/list' }, async () => ({
391
+ prompts: [
392
+ {
393
+ name: 'create_memory',
394
+ description: 'Interactive prompt to create a new memory',
395
+ arguments: [
396
+ {
397
+ name: 'title',
398
+ description: 'Initial title for the memory',
399
+ required: false
400
+ }
401
+ ]
402
+ },
403
+ {
404
+ name: 'search_memories',
405
+ description: 'Interactive prompt to search memories',
406
+ arguments: [
407
+ {
408
+ name: 'query',
409
+ description: 'Initial search query',
410
+ required: false
411
+ }
412
+ ]
413
+ },
414
+ {
415
+ name: 'organize_memories',
416
+ description: 'Interactive prompt to organize memories into topics',
417
+ arguments: []
418
+ }
419
+ ]
420
+ }));
421
+ this.server.setRequestHandler({ method: 'prompts/get' }, async (request) => {
422
+ const { name, arguments: args } = request.params;
423
+ const prompts = {
424
+ create_memory: {
425
+ description: 'Create a new memory entry',
426
+ messages: [
427
+ {
428
+ role: 'user',
429
+ content: {
430
+ type: 'text',
431
+ text: `Please provide the following information for the new memory:
432
+
433
+ Title: ${args?.title || '[Enter a descriptive title]'}
434
+ Content: [Enter the memory content]
435
+ Type: [context/reference/note]
436
+ Tags: [Optional comma-separated tags]`
437
+ }
438
+ }
439
+ ]
440
+ },
441
+ search_memories: {
442
+ description: 'Search through your memories',
443
+ messages: [
444
+ {
445
+ role: 'user',
446
+ content: {
447
+ type: 'text',
448
+ text: `What would you like to search for in your memories?
449
+
450
+ Query: ${args?.query || '[Enter search terms]'}
451
+ Limit: [Number of results, default 10]
452
+ Threshold: [Similarity threshold 0-1, default 0.7]`
453
+ }
454
+ }
455
+ ]
456
+ },
457
+ organize_memories: {
458
+ description: 'Organize memories into topics',
459
+ messages: [
460
+ {
461
+ role: 'user',
462
+ content: {
463
+ type: 'text',
464
+ text: `Let's organize your memories into topics.
465
+
466
+ Would you like to:
467
+ 1. Create a new topic
468
+ 2. Move memories to existing topics
469
+ 3. Review uncategorized memories
470
+ 4. Merge similar topics
471
+
472
+ Please choose an option (1-4):`
473
+ }
474
+ }
475
+ ]
476
+ }
477
+ };
478
+ const prompt = prompts[name];
479
+ if (!prompt) {
480
+ throw new Error(`Unknown prompt: ${name}`);
481
+ }
482
+ return prompt;
483
+ });
484
+ }
485
+ /**
486
+ * Handle tool calls
487
+ */
488
+ async handleToolCall(name, args) {
489
+ // Ensure we're initialized
490
+ if (!this.apiClient) {
491
+ await this.initialize();
492
+ }
493
+ switch (name) {
494
+ // Memory operations
495
+ case 'memory_create':
496
+ return await this.apiClient.createMemory(args);
497
+ case 'memory_search':
498
+ return await this.apiClient.searchMemories(args.query, {
499
+ limit: args.limit,
500
+ threshold: args.threshold
501
+ });
502
+ case 'memory_list':
503
+ return await this.apiClient.getMemories({
504
+ limit: args.limit,
505
+ offset: args.offset,
506
+ topic_id: args.topic_id
507
+ });
508
+ case 'memory_get':
509
+ return await this.apiClient.getMemory(args.memory_id);
510
+ case 'memory_update':
511
+ return await this.apiClient.updateMemory(args.memory_id, args);
512
+ case 'memory_delete':
513
+ return await this.apiClient.deleteMemory(args.memory_id);
514
+ // Topic operations
515
+ case 'topic_create':
516
+ return await this.apiClient.createTopic(args);
517
+ case 'topic_list':
518
+ return await this.apiClient.getTopics();
519
+ // API Key operations
520
+ case 'apikey_create':
521
+ // API keys not directly supported in current APIClient
522
+ return { error: 'API key creation not yet implemented' };
523
+ case 'apikey_list':
524
+ // API keys not directly supported in current APIClient
525
+ return { error: 'API key listing not yet implemented' };
526
+ // System operations
527
+ case 'system_health':
528
+ return await this.handleSystemHealth(args.verbose);
529
+ case 'system_config':
530
+ return await this.handleSystemConfig(args);
531
+ default:
532
+ throw new Error(`Unknown tool: ${name}`);
533
+ }
534
+ }
535
+ /**
536
+ * Handle resource reads
537
+ */
538
+ async handleResourceRead(uri) {
539
+ const [protocol, path] = uri.split('://');
540
+ switch (protocol) {
541
+ case 'memory':
542
+ if (path === 'recent') {
543
+ return await this.apiClient.getMemories({ limit: 10 });
544
+ }
545
+ else if (path === 'search') {
546
+ return {
547
+ message: 'Use memory_search tool to search memories',
548
+ example: { query: 'your search query', limit: 10 }
549
+ };
550
+ }
551
+ break;
552
+ case 'config':
553
+ if (path === 'current') {
554
+ await this.config.init();
555
+ return {
556
+ apiUrl: this.config.getApiUrl(),
557
+ authenticated: await this.config.isAuthenticated(),
558
+ user: await this.config.getCurrentUser()
559
+ };
560
+ }
561
+ break;
562
+ case 'stats':
563
+ if (path === 'usage') {
564
+ // TODO: Implement actual stats collection
565
+ return {
566
+ totalMemories: 0,
567
+ totalTopics: 0,
568
+ apiCallsToday: 0,
569
+ lastSync: new Date().toISOString()
570
+ };
571
+ }
572
+ break;
573
+ }
574
+ throw new Error(`Unknown resource: ${uri}`);
575
+ }
576
+ /**
577
+ * Handle system health check
578
+ */
579
+ async handleSystemHealth(verbose) {
580
+ const health = {
581
+ status: 'healthy',
582
+ server: this.options.name || 'lanonasis-maas-server',
583
+ version: this.options.version || '3.0.1',
584
+ timestamp: new Date().toISOString()
585
+ };
586
+ if (verbose) {
587
+ health.api = {
588
+ url: this.config.getApiUrl(),
589
+ authenticated: await this.config.isAuthenticated()
590
+ };
591
+ try {
592
+ const apiHealth = await this.apiClient.getHealth();
593
+ health.api.status = apiHealth.status;
594
+ health.api.version = apiHealth.version;
595
+ }
596
+ catch (error) {
597
+ health.api.status = 'error';
598
+ health.api.error = error instanceof Error ? error.message : 'Unknown error';
599
+ }
600
+ }
601
+ return health;
602
+ }
603
+ /**
604
+ * Handle system configuration
605
+ */
606
+ async handleSystemConfig(args) {
607
+ if (args.action === 'get') {
608
+ if (args.key) {
609
+ return { [args.key]: this.config.get(args.key) };
610
+ }
611
+ else {
612
+ // Return all config
613
+ await this.config.init();
614
+ return {
615
+ apiUrl: this.config.getApiUrl(),
616
+ mcpServerUrl: this.config.get('mcpServerUrl'),
617
+ mcpUseRemote: this.config.get('mcpUseRemote')
618
+ };
619
+ }
620
+ }
621
+ else if (args.action === 'set') {
622
+ if (!args.key || !args.value) {
623
+ throw new Error('Key and value required for set action');
624
+ }
625
+ this.config.set(args.key, args.value);
626
+ await this.config.save();
627
+ return {
628
+ success: true,
629
+ message: `Set ${args.key} to ${args.value}`
630
+ };
631
+ }
632
+ throw new Error('Invalid action');
633
+ }
634
+ /**
635
+ * Setup error handling
636
+ */
637
+ setupErrorHandling() {
638
+ process.on('SIGINT', async () => {
639
+ console.log(chalk.yellow('\nāš ļø Shutting down MCP server...'));
640
+ await this.stop();
641
+ process.exit(0);
642
+ });
643
+ process.on('uncaughtException', (error) => {
644
+ console.error(chalk.red('Uncaught exception:'), error);
645
+ process.exit(1);
646
+ });
647
+ process.on('unhandledRejection', (reason, promise) => {
648
+ console.error(chalk.red('Unhandled rejection at:'), promise, 'reason:', reason);
649
+ });
650
+ }
651
+ /**
652
+ * Start the server
653
+ */
654
+ async start() {
655
+ await this.initialize();
656
+ // Create and connect transport
657
+ this.transport = new StdioServerTransport();
658
+ await this.server.connect(this.transport);
659
+ if (this.options.verbose) {
660
+ console.log(chalk.green('āœ… Lanonasis MCP Server started'));
661
+ console.log(chalk.gray('Waiting for client connections...'));
662
+ }
663
+ // Keep the process alive
664
+ process.stdin.resume();
665
+ }
666
+ /**
667
+ * Stop the server
668
+ */
669
+ async stop() {
670
+ if (this.transport) {
671
+ await this.server.close();
672
+ this.transport = null;
673
+ }
674
+ if (this.options.verbose) {
675
+ console.log(chalk.gray('MCP Server stopped'));
676
+ }
677
+ }
678
+ /**
679
+ * Get server instance (for testing)
680
+ */
681
+ getServer() {
682
+ return this.server;
683
+ }
684
+ }
685
+ // CLI entry point
686
+ if (import.meta.url === `file://${process.argv[1]}`) {
687
+ const server = new LanonasisMCPServer({
688
+ verbose: process.argv.includes('--verbose'),
689
+ apiUrl: process.env.LANONASIS_API_URL,
690
+ token: process.env.LANONASIS_TOKEN
691
+ });
692
+ server.start().catch((error) => {
693
+ console.error(chalk.red('Failed to start server:'), error);
694
+ process.exit(1);
695
+ });
696
+ }