@mcp-use/cli 1.0.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.
@@ -0,0 +1,1024 @@
1
+ import { ChatOpenAI } from '@langchain/openai';
2
+ import { ChatAnthropic } from '@langchain/anthropic';
3
+ import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
4
+ import { ChatMistralAI } from '@langchain/mistralai';
5
+ import { SecureStorage } from './storage.js';
6
+ export class CommandHandler {
7
+ constructor() {
8
+ Object.defineProperty(this, "currentLLMConfig", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: null
13
+ });
14
+ Object.defineProperty(this, "sessionApiKeys", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: {}
19
+ });
20
+ Object.defineProperty(this, "persistentConfig", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
26
+ Object.defineProperty(this, "sessionServers", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: {}
31
+ });
32
+ Object.defineProperty(this, "availableModels", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: {
37
+ openai: [
38
+ 'gpt-4o',
39
+ 'gpt-4o-mini',
40
+ 'gpt-4-turbo',
41
+ 'gpt-4',
42
+ 'gpt-3.5-turbo'
43
+ ],
44
+ anthropic: [
45
+ 'claude-3-5-sonnet-20241022',
46
+ 'claude-3-5-haiku-20241022',
47
+ 'claude-3-opus-20240229',
48
+ 'claude-3-sonnet-20240229',
49
+ 'claude-3-haiku-20240307'
50
+ ],
51
+ google: [
52
+ 'gemini-1.5-pro',
53
+ 'gemini-1.5-flash',
54
+ 'gemini-pro'
55
+ ],
56
+ mistral: [
57
+ 'mistral-large-latest',
58
+ 'mistral-medium-latest',
59
+ 'mistral-small-latest'
60
+ ]
61
+ }
62
+ });
63
+ // Load persistent config
64
+ this.persistentConfig = SecureStorage.loadConfig();
65
+ // Auto-detect available provider and set default
66
+ this.initializeDefaultProvider();
67
+ }
68
+ initializeDefaultProvider() {
69
+ // First, try to load the last used model from persistent config
70
+ if (this.persistentConfig.lastModel) {
71
+ const lastModel = this.persistentConfig.lastModel;
72
+ const envVar = {
73
+ openai: 'OPENAI_API_KEY',
74
+ anthropic: 'ANTHROPIC_API_KEY',
75
+ google: 'GOOGLE_API_KEY',
76
+ mistral: 'MISTRAL_API_KEY'
77
+ }[lastModel.provider];
78
+ // Check if we still have the API key for the last used model
79
+ if (envVar && this.getApiKey(envVar)) {
80
+ this.currentLLMConfig = {
81
+ provider: lastModel.provider,
82
+ model: lastModel.model,
83
+ temperature: lastModel.temperature || 0.7,
84
+ maxTokens: lastModel.maxTokens
85
+ };
86
+ return;
87
+ }
88
+ }
89
+ const providers = [
90
+ { name: 'openai', envVar: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini' },
91
+ { name: 'anthropic', envVar: 'ANTHROPIC_API_KEY', defaultModel: 'claude-3-5-sonnet-20241022' },
92
+ { name: 'google', envVar: 'GOOGLE_API_KEY', defaultModel: 'gemini-1.5-pro' },
93
+ { name: 'mistral', envVar: 'MISTRAL_API_KEY', defaultModel: 'mistral-large-latest' }
94
+ ];
95
+ // Find first available provider
96
+ for (const provider of providers) {
97
+ if (this.getApiKey(provider.envVar)) {
98
+ this.currentLLMConfig = {
99
+ provider: provider.name,
100
+ model: provider.defaultModel,
101
+ temperature: 0.7
102
+ };
103
+ return;
104
+ }
105
+ }
106
+ // No provider found - leave null, will show setup message
107
+ this.currentLLMConfig = null;
108
+ }
109
+ getAvailableProviders() {
110
+ const providers = [];
111
+ if (this.getApiKey('OPENAI_API_KEY'))
112
+ providers.push('openai');
113
+ if (this.getApiKey('ANTHROPIC_API_KEY'))
114
+ providers.push('anthropic');
115
+ if (this.getApiKey('GOOGLE_API_KEY'))
116
+ providers.push('google');
117
+ if (this.getApiKey('MISTRAL_API_KEY'))
118
+ providers.push('mistral');
119
+ return providers;
120
+ }
121
+ getApiKey(envVar) {
122
+ // Check session keys first, then persistent storage, then environment variables
123
+ return this.sessionApiKeys[envVar] || this.persistentConfig.apiKeys[envVar] || process.env[envVar];
124
+ }
125
+ isAnyProviderAvailable() {
126
+ return this.getAvailableProviders().length > 0;
127
+ }
128
+ async handleCommand(input) {
129
+ const parts = input.trim().split(/\s+/);
130
+ const command = parts[0];
131
+ const args = parts.slice(1);
132
+ switch (command) {
133
+ case '/help':
134
+ return this.handleHelp();
135
+ case '/model':
136
+ return this.handleModel(args);
137
+ case '/models':
138
+ return this.handleListModels(args);
139
+ case '/status':
140
+ return this.handleStatus();
141
+ case '/config':
142
+ return this.handleConfig(args);
143
+ case '/setkey':
144
+ return this.handleSetKey(args);
145
+ case '/clearkeys':
146
+ return this.handleClearKeys();
147
+ case '/server':
148
+ return this.handleServer(args);
149
+ case '/servers':
150
+ return this.handleListServers();
151
+ case '/tools':
152
+ return this.handleListTools();
153
+ case '/test-server':
154
+ return this.handleTestServer(args);
155
+ default:
156
+ return {
157
+ type: 'error',
158
+ message: `Unknown command: ${command}. Type /help for available commands.`
159
+ };
160
+ }
161
+ }
162
+ handleHelp() {
163
+ const helpText = `
164
+ Available slash commands:
165
+
166
+ šŸ¤– Get Started:
167
+ /model <provider> <model> - Choose your LLM (CLI handles API key setup)
168
+ /models [provider] - List available models for a provider
169
+
170
+ šŸ”Œ MCP Servers:
171
+ /server add - Configure a new server (auto-connects)
172
+ /server connect <name> - Connect to a configured server by name
173
+ /server disconnect <name> - Disconnect from a connected server
174
+ /servers - List servers and their connection status
175
+ /tools - Show available tools from connected servers
176
+ /test-server <name> - Test if a server package can be started
177
+ /status - Show current configuration
178
+
179
+ šŸ”‘ API Keys (automatic):
180
+ /setkey <provider> <key> - Set API key manually (stored securely)
181
+ /clearkeys - Clear all stored API keys
182
+
183
+ āš™ļø Configuration:
184
+ /config temp <value> - Set temperature (0.0-2.0)
185
+ /config tokens <value> - Set max tokens
186
+ /help - Show this help
187
+
188
+ šŸ“‹ Quick Start Examples:
189
+ /model openai gpt-4o-mini
190
+ /server add # Interactive server setup
191
+ /servers
192
+ /config temp 0.5
193
+ `.trim();
194
+ return {
195
+ type: 'info',
196
+ message: helpText
197
+ };
198
+ }
199
+ handleModel(args) {
200
+ if (args.length < 2) {
201
+ const availableProviders = this.getAvailableProviders();
202
+ if (availableProviders.length === 0) {
203
+ return {
204
+ type: 'info',
205
+ message: 'Usage: /model <provider> <model>\n\nPopular models to try:\n• /model openai gpt-4o-mini\n• /model anthropic claude-3-5-sonnet-20241022\n• /model google gemini-1.5-pro\n\nThe CLI will prompt for your API key when needed.\nUse /models to see all available options.'
206
+ };
207
+ }
208
+ return {
209
+ type: 'error',
210
+ message: `Usage: /model <provider> <model>\nExample: /model openai gpt-4o\n\nAvailable providers: ${availableProviders.join(', ')}`
211
+ };
212
+ }
213
+ const provider = args[0];
214
+ const model = args[1];
215
+ if (!provider || !this.availableModels[provider]) {
216
+ return {
217
+ type: 'error',
218
+ message: `Unknown provider: ${provider}\nAvailable providers: ${Object.keys(this.availableModels).join(', ')}`
219
+ };
220
+ }
221
+ // Check if the provider's API key is available
222
+ const availableProviders = this.getAvailableProviders();
223
+ if (!availableProviders.includes(provider)) {
224
+ const envVarMap = {
225
+ openai: 'OPENAI_API_KEY',
226
+ anthropic: 'ANTHROPIC_API_KEY',
227
+ google: 'GOOGLE_API_KEY',
228
+ mistral: 'MISTRAL_API_KEY'
229
+ };
230
+ // Prompt for API key instead of showing error
231
+ return {
232
+ type: 'prompt_key',
233
+ message: `Please enter your ${provider.toUpperCase()} API key:`,
234
+ data: {
235
+ provider,
236
+ model,
237
+ envVar: envVarMap[provider]
238
+ }
239
+ };
240
+ }
241
+ if (!model || !this.availableModels[provider].includes(model)) {
242
+ return {
243
+ type: 'error',
244
+ message: `Model ${model} not available for ${provider}\nUse /models ${provider} to see available models`
245
+ };
246
+ }
247
+ this.currentLLMConfig = {
248
+ provider,
249
+ model,
250
+ temperature: this.currentLLMConfig?.temperature || 0.7,
251
+ maxTokens: this.currentLLMConfig?.maxTokens
252
+ };
253
+ // Save the current model to persistent storage
254
+ this.persistentConfig.lastModel = this.currentLLMConfig;
255
+ SecureStorage.saveConfig(this.persistentConfig);
256
+ return {
257
+ type: 'success',
258
+ message: `āœ… Switched to ${provider} ${model}`,
259
+ data: { llmConfig: this.currentLLMConfig }
260
+ };
261
+ }
262
+ handleListModels(args) {
263
+ if (args.length === 0) {
264
+ let modelList = 'šŸ“‹ Available models by provider:\n\n';
265
+ Object.entries(this.availableModels).forEach(([provider, models]) => {
266
+ modelList += `šŸ”ø ${provider}:\n`;
267
+ models.forEach(model => {
268
+ const current = provider === this.currentLLMConfig?.provider && model === this.currentLLMConfig?.model ? ' ← current' : '';
269
+ modelList += ` • ${model}${current}\n`;
270
+ });
271
+ modelList += '\n';
272
+ });
273
+ return {
274
+ type: 'info',
275
+ message: modelList.trim()
276
+ };
277
+ }
278
+ const provider = args[0];
279
+ if (!this.availableModels[provider]) {
280
+ return {
281
+ type: 'error',
282
+ message: `Unknown provider: ${provider}\nAvailable providers: ${Object.keys(this.availableModels).join(', ')}`
283
+ };
284
+ }
285
+ let modelList = `šŸ“‹ Available ${provider} models:\n\n`;
286
+ this.availableModels[provider].forEach(model => {
287
+ const current = provider === this.currentLLMConfig?.provider && model === this.currentLLMConfig?.model ? ' ← current' : '';
288
+ modelList += `• ${model}${current}\n`;
289
+ });
290
+ return {
291
+ type: 'info',
292
+ message: modelList.trim()
293
+ };
294
+ }
295
+ handleStatus() {
296
+ const availableProviders = this.getAvailableProviders();
297
+ let statusText = 'šŸ¤– Current Configuration:\n\n';
298
+ // API Keys status
299
+ statusText += 'šŸ”‘ API Keys:\n';
300
+ const allProviders = ['openai', 'anthropic', 'google', 'mistral'];
301
+ allProviders.forEach(provider => {
302
+ const envVarMap = {
303
+ openai: 'OPENAI_API_KEY',
304
+ anthropic: 'ANTHROPIC_API_KEY',
305
+ google: 'GOOGLE_API_KEY',
306
+ mistral: 'MISTRAL_API_KEY'
307
+ };
308
+ const envVar = envVarMap[provider];
309
+ const hasEnvKey = !!process.env[envVar];
310
+ const hasSessionKey = !!this.sessionApiKeys[envVar];
311
+ const hasPersistentKey = !!this.persistentConfig.apiKeys[envVar];
312
+ if (hasSessionKey) {
313
+ const key = this.sessionApiKeys[envVar];
314
+ if (key) {
315
+ const maskedKey = this.maskApiKey(key);
316
+ statusText += ` • ${provider}: ${maskedKey} (session)\n`;
317
+ }
318
+ }
319
+ else if (hasPersistentKey) {
320
+ const key = this.persistentConfig.apiKeys[envVar];
321
+ if (key) {
322
+ const maskedKey = this.maskApiKey(key);
323
+ statusText += ` • ${provider}: ${maskedKey} (stored)\n`;
324
+ }
325
+ }
326
+ else if (hasEnvKey) {
327
+ const key = process.env[envVar];
328
+ if (key) {
329
+ const maskedKey = this.maskApiKey(key);
330
+ statusText += ` • ${provider}: ${maskedKey} (env)\n`;
331
+ }
332
+ }
333
+ else {
334
+ statusText += ` • ${provider}: āŒ not set\n`;
335
+ }
336
+ });
337
+ statusText += '\n';
338
+ // Current model
339
+ if (!this.currentLLMConfig) {
340
+ if (availableProviders.length === 0) {
341
+ statusText += 'āš ļø No model selected\n';
342
+ statusText += '\nChoose a model to get started:\n';
343
+ statusText += '• /model openai gpt-4o-mini\n';
344
+ statusText += '• /model anthropic claude-3-5-sonnet-20241022\n';
345
+ statusText += '• /model google gemini-1.5-pro\n';
346
+ statusText += '\nThe CLI will help you set up API keys when needed.';
347
+ }
348
+ else {
349
+ statusText += 'āš ļø No model selected\n';
350
+ statusText += `\nAvailable providers: ${availableProviders.join(', ')}\n`;
351
+ statusText += 'Use /model <provider> <model> to get started';
352
+ }
353
+ }
354
+ else {
355
+ const config = this.currentLLMConfig;
356
+ statusText += `šŸŽÆ Active Model:\n`;
357
+ statusText += ` Provider: ${config.provider}\n`;
358
+ statusText += ` Model: ${config.model}\n`;
359
+ statusText += ` Temperature: ${config.temperature || 0.7}\n`;
360
+ statusText += ` Max Tokens: ${config.maxTokens || 'default'}\n`;
361
+ statusText += '\nUse /model to switch models or /config to adjust settings';
362
+ }
363
+ return {
364
+ type: 'info',
365
+ message: statusText
366
+ };
367
+ }
368
+ handleConfig(args) {
369
+ if (args.length < 2) {
370
+ return {
371
+ type: 'error',
372
+ message: 'Usage: /config <setting> <value>\nAvailable settings: temp, tokens'
373
+ };
374
+ }
375
+ const setting = args[0];
376
+ const value = args[1];
377
+ if (!value) {
378
+ return {
379
+ type: 'error',
380
+ message: 'Value is required'
381
+ };
382
+ }
383
+ if (!this.currentLLMConfig) {
384
+ return {
385
+ type: 'error',
386
+ message: 'No model configured. Use /model to select a provider and model first.'
387
+ };
388
+ }
389
+ switch (setting) {
390
+ case 'temp':
391
+ case 'temperature':
392
+ const temp = parseFloat(value);
393
+ if (isNaN(temp) || temp < 0 || temp > 2) {
394
+ return {
395
+ type: 'error',
396
+ message: 'Temperature must be a number between 0.0 and 2.0'
397
+ };
398
+ }
399
+ this.currentLLMConfig.temperature = temp;
400
+ return {
401
+ type: 'success',
402
+ message: `āœ… Temperature set to ${temp}`,
403
+ data: { llmConfig: this.currentLLMConfig }
404
+ };
405
+ case 'tokens':
406
+ case 'max-tokens':
407
+ const tokens = parseInt(value);
408
+ if (isNaN(tokens) || tokens < 1) {
409
+ return {
410
+ type: 'error',
411
+ message: 'Max tokens must be a positive integer'
412
+ };
413
+ }
414
+ this.currentLLMConfig.maxTokens = tokens;
415
+ return {
416
+ type: 'success',
417
+ message: `āœ… Max tokens set to ${tokens}`,
418
+ data: { llmConfig: this.currentLLMConfig }
419
+ };
420
+ default:
421
+ return {
422
+ type: 'error',
423
+ message: `Unknown setting: ${setting}\nAvailable settings: temp, tokens`
424
+ };
425
+ }
426
+ }
427
+ handleSetKey(args) {
428
+ if (args.length < 2) {
429
+ return {
430
+ type: 'error',
431
+ message: 'Usage: /setkey <provider> <api_key>\n\nSupported providers: openai, anthropic, google, mistral\n\nExample:\n/setkey openai sk-1234567890abcdef...'
432
+ };
433
+ }
434
+ const provider = args[0]?.toLowerCase();
435
+ const apiKey = args[1];
436
+ if (!provider || !apiKey) {
437
+ return {
438
+ type: 'error',
439
+ message: 'Both provider and API key are required'
440
+ };
441
+ }
442
+ // Validate provider
443
+ const validProviders = ['openai', 'anthropic', 'google', 'mistral'];
444
+ if (!validProviders.includes(provider)) {
445
+ return {
446
+ type: 'error',
447
+ message: `Invalid provider: ${provider}\nSupported providers: ${validProviders.join(', ')}`
448
+ };
449
+ }
450
+ // Basic API key validation
451
+ const validationResult = this.validateApiKey(provider, apiKey);
452
+ if (!validationResult.valid) {
453
+ return {
454
+ type: 'error',
455
+ message: validationResult.message
456
+ };
457
+ }
458
+ // Map provider to environment variable name
459
+ const envVarMap = {
460
+ openai: 'OPENAI_API_KEY',
461
+ anthropic: 'ANTHROPIC_API_KEY',
462
+ google: 'GOOGLE_API_KEY',
463
+ mistral: 'MISTRAL_API_KEY'
464
+ };
465
+ const envVar = envVarMap[provider];
466
+ // Store the API key in persistent storage
467
+ this.persistentConfig.apiKeys[envVar] = apiKey;
468
+ SecureStorage.saveConfig(this.persistentConfig);
469
+ // Check if we can auto-select this provider
470
+ const shouldAutoSelect = !this.currentLLMConfig;
471
+ if (shouldAutoSelect) {
472
+ const defaultModels = {
473
+ openai: 'gpt-4o-mini',
474
+ anthropic: 'claude-3-5-sonnet-20241022',
475
+ google: 'gemini-1.5-pro',
476
+ mistral: 'mistral-large-latest'
477
+ };
478
+ this.currentLLMConfig = {
479
+ provider: provider,
480
+ model: defaultModels[provider],
481
+ temperature: 0.7
482
+ };
483
+ }
484
+ const maskedKey = this.maskApiKey(apiKey);
485
+ let message = `āœ… ${provider} API key set (${maskedKey})`;
486
+ if (shouldAutoSelect) {
487
+ message += `\nšŸ¤– Auto-selected ${this.currentLLMConfig.provider}/${this.currentLLMConfig.model}`;
488
+ }
489
+ return {
490
+ type: 'success',
491
+ message,
492
+ data: shouldAutoSelect ? { llmConfig: this.currentLLMConfig } : undefined
493
+ };
494
+ }
495
+ validateApiKey(provider, apiKey) {
496
+ if (!apiKey || apiKey.trim().length === 0) {
497
+ return { valid: false, message: 'API key cannot be empty' };
498
+ }
499
+ // Basic format validation for each provider
500
+ switch (provider) {
501
+ case 'openai':
502
+ if (!apiKey.startsWith('sk-') || apiKey.length < 20) {
503
+ return { valid: false, message: 'OpenAI API keys should start with "sk-" and be at least 20 characters long' };
504
+ }
505
+ break;
506
+ case 'anthropic':
507
+ if (!apiKey.startsWith('ant_') && !apiKey.startsWith('sk-ant-') || apiKey.length < 20) {
508
+ return { valid: false, message: 'Anthropic API keys should start with "ant_" or "sk-ant-" and be at least 20 characters long' };
509
+ }
510
+ break;
511
+ case 'google':
512
+ if (apiKey.length < 20) {
513
+ return { valid: false, message: 'Google API keys should be at least 20 characters long' };
514
+ }
515
+ break;
516
+ case 'mistral':
517
+ if (apiKey.length < 20) {
518
+ return { valid: false, message: 'Mistral API keys should be at least 20 characters long' };
519
+ }
520
+ break;
521
+ }
522
+ return { valid: true, message: '' };
523
+ }
524
+ maskApiKey(apiKey) {
525
+ if (apiKey.length <= 8) {
526
+ return '*'.repeat(apiKey.length);
527
+ }
528
+ const start = apiKey.substring(0, 4);
529
+ const end = apiKey.substring(apiKey.length - 4);
530
+ const middle = '*'.repeat(Math.min(12, apiKey.length - 8));
531
+ return `${start}${middle}${end}`;
532
+ }
533
+ createLLM() {
534
+ if (!this.currentLLMConfig) {
535
+ throw new Error('No LLM configured. Use /model command to select a provider and model.');
536
+ }
537
+ const config = this.currentLLMConfig;
538
+ const baseConfig = {
539
+ temperature: config.temperature || 0.7,
540
+ maxTokens: config.maxTokens
541
+ };
542
+ switch (config.provider) {
543
+ case 'openai':
544
+ const openaiKey = this.getApiKey('OPENAI_API_KEY');
545
+ if (!openaiKey) {
546
+ throw new Error('OPENAI_API_KEY is required for OpenAI models. Use /setkey openai <your-key>');
547
+ }
548
+ return new ChatOpenAI({
549
+ modelName: config.model,
550
+ openAIApiKey: openaiKey,
551
+ ...baseConfig
552
+ });
553
+ case 'anthropic':
554
+ const anthropicKey = this.getApiKey('ANTHROPIC_API_KEY');
555
+ if (!anthropicKey) {
556
+ throw new Error('ANTHROPIC_API_KEY is required for Anthropic models. Use /setkey anthropic <your-key>');
557
+ }
558
+ return new ChatAnthropic({
559
+ modelName: config.model,
560
+ anthropicApiKey: anthropicKey,
561
+ ...baseConfig
562
+ });
563
+ case 'google':
564
+ const googleKey = this.getApiKey('GOOGLE_API_KEY');
565
+ if (!googleKey) {
566
+ throw new Error('GOOGLE_API_KEY is required for Google models. Use /setkey google <your-key>');
567
+ }
568
+ return new ChatGoogleGenerativeAI({
569
+ model: config.model,
570
+ apiKey: googleKey,
571
+ ...baseConfig
572
+ });
573
+ case 'mistral':
574
+ const mistralKey = this.getApiKey('MISTRAL_API_KEY');
575
+ if (!mistralKey) {
576
+ throw new Error('MISTRAL_API_KEY is required for Mistral models. Use /setkey mistral <your-key>');
577
+ }
578
+ return new ChatMistralAI({
579
+ modelName: config.model,
580
+ apiKey: mistralKey,
581
+ ...baseConfig
582
+ });
583
+ default:
584
+ throw new Error(`Unsupported provider: ${config.provider}`);
585
+ }
586
+ }
587
+ getCurrentConfig() {
588
+ return this.currentLLMConfig ? { ...this.currentLLMConfig } : null;
589
+ }
590
+ getCurrentStoredConfig() {
591
+ return this.persistentConfig;
592
+ }
593
+ getSessionServers() {
594
+ return this.sessionServers;
595
+ }
596
+ handleListTools() {
597
+ return {
598
+ type: 'info',
599
+ message: 'šŸ”§ Checking available MCP tools...\n\nThis command will show tools available from connected MCP servers.\nNote: This requires the MCP service to provide tool listing functionality.',
600
+ data: { checkTools: true }
601
+ };
602
+ }
603
+ handleTestServer(args) {
604
+ if (args.length === 0) {
605
+ const configuredServers = Object.keys(this.persistentConfig.mcpServers || {});
606
+ if (configuredServers.length === 0) {
607
+ return {
608
+ type: 'error',
609
+ message: 'No servers configured to test.\n\nUsage: /test-server <server_name>\n\nUse /server add to configure servers first.'
610
+ };
611
+ }
612
+ return {
613
+ type: 'info',
614
+ message: `Usage: /test-server <server_name>\n\nConfigured servers: ${configuredServers.join(', ')}\n\nThis command will test if the server package can be started manually.`
615
+ };
616
+ }
617
+ const serverName = args[0];
618
+ if (!serverName) {
619
+ return {
620
+ type: 'error',
621
+ message: 'Server name is required.\n\nUsage: /test-server <server_name>'
622
+ };
623
+ }
624
+ const serverConfig = this.persistentConfig.mcpServers?.[serverName];
625
+ if (!serverConfig) {
626
+ const configuredServers = Object.keys(this.persistentConfig.mcpServers || {});
627
+ return {
628
+ type: 'error',
629
+ message: `Server "${serverName}" is not configured.\n\nConfigured servers: ${configuredServers.length > 0 ? configuredServers.join(', ') : 'none'}`
630
+ };
631
+ }
632
+ const command = serverConfig.command;
633
+ const args_str = serverConfig.args ? serverConfig.args.join(' ') : '';
634
+ const full_command = `${command} ${args_str}`.trim();
635
+ return {
636
+ type: 'info',
637
+ message: `🧪 Testing server "${serverName}"...\n\nCommand: ${full_command}\n\nāš ļø Note: This will attempt to run the server command manually.\nCheck the console for output and errors.\n\nšŸ’” Try running this command manually in your terminal:\n${full_command}`,
638
+ data: { testServer: true, serverName, command: full_command }
639
+ };
640
+ }
641
+ isCommand(input) {
642
+ return input.trim().startsWith('/');
643
+ }
644
+ handleClearKeys() {
645
+ // Clear both session and persistent keys
646
+ this.sessionApiKeys = {};
647
+ this.persistentConfig.apiKeys = {};
648
+ this.persistentConfig.lastModel = undefined;
649
+ // Clear the current LLM config since we have no keys
650
+ this.currentLLMConfig = null;
651
+ // Save the cleared config
652
+ SecureStorage.saveConfig(this.persistentConfig);
653
+ return {
654
+ type: 'success',
655
+ message: 'āœ… All API keys cleared from storage.\n\nUse /setkey or /model to set up a new provider.',
656
+ data: { llmConfig: null }
657
+ };
658
+ }
659
+ // Method to handle server configuration input
660
+ handleServerConfigInput(input, step, serverConfig) {
661
+ const config = serverConfig || {};
662
+ switch (step) {
663
+ case 'name_or_json':
664
+ // Check if input looks like JSON
665
+ const trimmedInput = input.trim();
666
+ if (trimmedInput.startsWith('{') && trimmedInput.includes('mcpServers')) {
667
+ try {
668
+ const parsedConfig = JSON.parse(trimmedInput);
669
+ // Validate JSON structure
670
+ if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') {
671
+ return {
672
+ type: 'error',
673
+ message: 'Invalid JSON format. Expected format:\n{\n "mcpServers": {\n "servername": {\n "command": "...",\n "args": [...]\n }\n }\n}'
674
+ };
675
+ }
676
+ const servers = parsedConfig.mcpServers;
677
+ const serverNames = Object.keys(servers);
678
+ if (serverNames.length === 0) {
679
+ return {
680
+ type: 'error',
681
+ message: 'No servers found in JSON configuration.'
682
+ };
683
+ }
684
+ // Check for conflicts with existing servers
685
+ const existingServers = this.persistentConfig.mcpServers || {};
686
+ const conflicts = serverNames.filter(name => existingServers[name]);
687
+ if (conflicts.length > 0) {
688
+ return {
689
+ type: 'error',
690
+ message: `Server(s) already exist: ${conflicts.join(', ')}. Please use different names.`
691
+ };
692
+ }
693
+ // Validate each server config
694
+ for (const [name, serverConfig] of Object.entries(servers)) {
695
+ const server = serverConfig;
696
+ if (!server.command || typeof server.command !== 'string') {
697
+ return {
698
+ type: 'error',
699
+ message: `Server "${name}" missing required "command" field.`
700
+ };
701
+ }
702
+ }
703
+ // All validation passed, save the servers
704
+ if (!this.persistentConfig.mcpServers) {
705
+ this.persistentConfig.mcpServers = {};
706
+ }
707
+ // Add all servers from JSON
708
+ Object.assign(this.persistentConfig.mcpServers, servers);
709
+ SecureStorage.saveConfig(this.persistentConfig);
710
+ // Auto-connect all newly configured servers
711
+ Object.assign(this.sessionServers, servers);
712
+ const addedCount = serverNames.length;
713
+ const serverList = serverNames.map(name => `• ${name}`).join('\n');
714
+ return {
715
+ type: 'success',
716
+ message: `āœ… Configured and connected ${addedCount} server(s)!\n\n${serverList}\n\nšŸ”„ Agent will be reinitialized with these servers - attempting to establish connections...\nUse /tools to verify the server tools are available.`,
717
+ data: { serversAdded: true, serverConnected: true, serverNames }
718
+ };
719
+ }
720
+ catch (error) {
721
+ return {
722
+ type: 'error',
723
+ message: `Invalid JSON format: ${error instanceof Error ? error.message : 'Parse error'}\n\nPlease check your JSON syntax and try again.`
724
+ };
725
+ }
726
+ }
727
+ // Not JSON, treat as server name for interactive setup
728
+ if (!trimmedInput) {
729
+ return {
730
+ type: 'error',
731
+ message: 'Server name cannot be empty.'
732
+ };
733
+ }
734
+ // Check if server name already exists
735
+ if (this.persistentConfig.mcpServers?.[trimmedInput]) {
736
+ return {
737
+ type: 'error',
738
+ message: `Server "${trimmedInput}" already exists. Use a different name.`
739
+ };
740
+ }
741
+ config.name = trimmedInput;
742
+ return {
743
+ type: 'prompt_server_config',
744
+ message: `Server name: ${config.name}\n\nEnter the command to run this server (e.g., "npx", "node", "python"):`,
745
+ data: { step: 'command', config }
746
+ };
747
+ case 'name':
748
+ if (!input.trim()) {
749
+ return {
750
+ type: 'error',
751
+ message: 'Server name cannot be empty.'
752
+ };
753
+ }
754
+ // Check if server name already exists
755
+ if (this.persistentConfig.mcpServers?.[input.trim()]) {
756
+ return {
757
+ type: 'error',
758
+ message: `Server "${input.trim()}" already exists. Use a different name.`
759
+ };
760
+ }
761
+ config.name = input.trim();
762
+ return {
763
+ type: 'prompt_server_config',
764
+ message: `Server name: ${config.name}\n\nEnter the command to run this server (e.g., "npx", "node", "python"):`,
765
+ data: { step: 'command', config }
766
+ };
767
+ case 'command':
768
+ if (!input.trim()) {
769
+ return {
770
+ type: 'error',
771
+ message: 'Command cannot be empty.'
772
+ };
773
+ }
774
+ config.command = input.trim();
775
+ return {
776
+ type: 'prompt_server_config',
777
+ message: `Server name: ${config.name}\nCommand: ${config.command}\n\nEnter arguments (space-separated, or press Enter for none):\nExample: "-y @modelcontextprotocol/server-filesystem /tmp"`,
778
+ data: { step: 'args', config }
779
+ };
780
+ case 'args':
781
+ config.args = input.trim() ? input.trim().split(/\s+/) : [];
782
+ return {
783
+ type: 'prompt_server_config',
784
+ message: `Server name: ${config.name}\nCommand: ${config.command}\nArgs: ${config.args.length > 0 ? config.args.join(' ') : 'none'}\n\nEnter environment variables (KEY=VALUE format, one per line, or press Enter for none):\nExample: "DEBUG=1" or press Enter to skip:`,
785
+ data: { step: 'env', config }
786
+ };
787
+ case 'env':
788
+ config.env = {};
789
+ if (input.trim()) {
790
+ const envLines = input.trim().split('\n');
791
+ for (const line of envLines) {
792
+ const [key, ...valueParts] = line.split('=');
793
+ if (key && valueParts.length > 0) {
794
+ config.env[key.trim()] = valueParts.join('=').trim();
795
+ }
796
+ }
797
+ }
798
+ return {
799
+ type: 'prompt_server_config',
800
+ message: `Server Configuration Summary:\n\nName: ${config.name}\nCommand: ${config.command}\nArgs: ${config.args.length > 0 ? config.args.join(' ') : 'none'}\nEnv: ${Object.keys(config.env).length > 0 ? Object.entries(config.env).map(([k, v]) => `${k}=${v}`).join(', ') : 'none'}\n\nConfirm to add this server? (y/n):`,
801
+ data: { step: 'confirm', config }
802
+ };
803
+ case 'confirm':
804
+ if (input.trim().toLowerCase() === 'y' || input.trim().toLowerCase() === 'yes') {
805
+ const serverConfig = {
806
+ command: config.command,
807
+ args: config.args,
808
+ env: config.env
809
+ };
810
+ // Add server to persistent configuration (servers are configured but not automatically connected)
811
+ if (!this.persistentConfig.mcpServers) {
812
+ this.persistentConfig.mcpServers = {};
813
+ }
814
+ this.persistentConfig.mcpServers[config.name] = serverConfig;
815
+ // Save configuration
816
+ SecureStorage.saveConfig(this.persistentConfig);
817
+ // Auto-connect the newly configured server
818
+ this.sessionServers[config.name] = serverConfig;
819
+ return {
820
+ type: 'success',
821
+ message: `āœ… Server "${config.name}" configured and connected!\n\nšŸ”„ Agent will be reinitialized with this server - attempting to establish connection...\nUse /tools to verify the server tools are available.`,
822
+ data: { serverAdded: true, serverConnected: true, serverName: config.name }
823
+ };
824
+ }
825
+ else if (input.trim().toLowerCase() === 'n' || input.trim().toLowerCase() === 'no') {
826
+ return {
827
+ type: 'info',
828
+ message: 'Server configuration cancelled.'
829
+ };
830
+ }
831
+ else {
832
+ return {
833
+ type: 'error',
834
+ message: 'Please enter "y" for yes or "n" for no.'
835
+ };
836
+ }
837
+ default:
838
+ return {
839
+ type: 'error',
840
+ message: 'Invalid server configuration step.'
841
+ };
842
+ }
843
+ }
844
+ handleServer(args) {
845
+ if (args.length === 0) {
846
+ return {
847
+ type: 'info',
848
+ message: 'Server management commands:\n\n/server add - Configure a new server (stored but not connected)\n/server connect <name> - Connect to a configured server by name\n/server disconnect <name> - Disconnect from a connected server\n/servers - List configured servers and connection status\n\nUse /server <command> for specific help.'
849
+ };
850
+ }
851
+ if (args[0] === 'add') {
852
+ return {
853
+ type: 'prompt_server_config',
854
+ message: 'Let\'s configure a new MCP server!\n\nYou can either:\n1. Enter a server name for interactive setup\n2. Paste a complete JSON configuration\n\nExample JSON:\n{\n "mcpServers": {\n "myserver": {\n "command": "npx",\n "args": ["-y", "@example/server"]\n }\n }\n}\n\nEnter server name or paste JSON:',
855
+ data: { step: 'name_or_json' }
856
+ };
857
+ }
858
+ if (args[0] === 'connect') {
859
+ if (args.length < 2) {
860
+ const configuredServers = Object.keys({ ...this.persistentConfig.mcpServers });
861
+ if (configuredServers.length === 0) {
862
+ return {
863
+ type: 'error',
864
+ message: 'No servers configured. Use /server add to configure servers first.\n\nUsage: /server connect <server_name>'
865
+ };
866
+ }
867
+ return {
868
+ type: 'error',
869
+ message: `Usage: /server connect <server_name>\n\nConfigured servers: ${configuredServers.join(', ')}`
870
+ };
871
+ }
872
+ const serverName = args[1];
873
+ if (!serverName) {
874
+ return {
875
+ type: 'error',
876
+ message: 'Server name is required.\n\nUsage: /server connect <server_name>'
877
+ };
878
+ }
879
+ return this.handleConnectServer(serverName);
880
+ }
881
+ if (args[0] === 'disconnect') {
882
+ if (args.length < 2) {
883
+ const connectedServers = Object.keys(this.sessionServers);
884
+ if (connectedServers.length === 0) {
885
+ return {
886
+ type: 'info',
887
+ message: 'No servers currently connected.\n\nUsage: /server disconnect <server_name>'
888
+ };
889
+ }
890
+ return {
891
+ type: 'error',
892
+ message: `Usage: /server disconnect <server_name>\n\nConnected servers: ${connectedServers.join(', ')}`
893
+ };
894
+ }
895
+ const serverName = args[1];
896
+ if (!serverName) {
897
+ return {
898
+ type: 'error',
899
+ message: 'Server name is required.\n\nUsage: /server disconnect <server_name>'
900
+ };
901
+ }
902
+ return this.handleDisconnectServer(serverName);
903
+ }
904
+ return {
905
+ type: 'error',
906
+ message: 'Usage: /server <command>\n\nCommands:\n add - Configure server\n connect <name> - Connect to server\n disconnect <name> - Disconnect server\n\nExample: /server connect airbnb'
907
+ };
908
+ }
909
+ handleListServers() {
910
+ const persistentServers = this.persistentConfig.mcpServers || {};
911
+ const connectedServers = this.sessionServers || {};
912
+ let serverList = 'šŸ“‹ MCP Server Status:\n\n';
913
+ // Built-in filesystem server (always connected)
914
+ serverList += 'šŸ”ø filesystem:\n';
915
+ serverList += ' Status: 🟢 Connected (built-in)\n';
916
+ serverList += ' Command: npx\n';
917
+ serverList += ' Args: -y @modelcontextprotocol/server-filesystem /tmp\n\n';
918
+ // Custom configured servers
919
+ const configuredServerNames = Object.keys(persistentServers);
920
+ if (configuredServerNames.length === 0) {
921
+ serverList += 'No custom servers configured.\n\nUse /server add to configure servers, then /server connect <name> to connect.';
922
+ }
923
+ else {
924
+ for (const [name, config] of Object.entries(persistentServers)) {
925
+ const isConnected = connectedServers[name] !== undefined;
926
+ const status = isConnected ? '🟢 Connected' : 'šŸ”“ Disconnected';
927
+ const action = isConnected ? '/server disconnect' : '/server connect';
928
+ serverList += `šŸ”ø ${name}:\n`;
929
+ serverList += ` Status: ${status}\n`;
930
+ serverList += ` Command: ${config.command}\n`;
931
+ if (config.args && config.args.length > 0) {
932
+ serverList += ` Args: ${config.args.join(' ')}\n`;
933
+ }
934
+ if (config.env && Object.keys(config.env).length > 0) {
935
+ serverList += ` Env: ${Object.entries(config.env).map(([k, v]) => `${k}=${v}`).join(', ')}\n`;
936
+ }
937
+ serverList += ` Action: ${action} ${name}\n\n`;
938
+ }
939
+ }
940
+ return {
941
+ type: 'info',
942
+ message: serverList.trim()
943
+ };
944
+ }
945
+ handleConnectServer(serverName) {
946
+ // Check if server is configured
947
+ const configuredServer = this.persistentConfig.mcpServers?.[serverName];
948
+ if (!configuredServer) {
949
+ const availableServers = Object.keys(this.persistentConfig.mcpServers || {});
950
+ return {
951
+ type: 'error',
952
+ message: `Server "${serverName}" is not configured.\n\nConfigured servers: ${availableServers.length > 0 ? availableServers.join(', ') : 'none'}\n\nUse /server add to configure new servers.`
953
+ };
954
+ }
955
+ // Check if already connected (in session servers)
956
+ if (this.sessionServers[serverName]) {
957
+ return {
958
+ type: 'info',
959
+ message: `āœ… Server "${serverName}" is already connected.`
960
+ };
961
+ }
962
+ // Connect the server (add to session servers)
963
+ this.sessionServers[serverName] = configuredServer;
964
+ return {
965
+ type: 'success',
966
+ message: `āœ… Connected to server "${serverName}"!\n\nšŸ”„ Agent will be reinitialized with this server - attempting to establish connection...\nUse /tools to verify the server tools are available.`,
967
+ data: { serverConnected: true, serverName }
968
+ };
969
+ }
970
+ handleDisconnectServer(serverName) {
971
+ // Check if server is connected
972
+ if (!this.sessionServers[serverName]) {
973
+ const connectedServers = Object.keys(this.sessionServers);
974
+ return {
975
+ type: 'error',
976
+ message: `Server "${serverName}" is not connected.\n\nConnected servers: ${connectedServers.length > 0 ? connectedServers.join(', ') : 'none'}`
977
+ };
978
+ }
979
+ // Disconnect the server (remove from session servers)
980
+ delete this.sessionServers[serverName];
981
+ return {
982
+ type: 'success',
983
+ message: `āœ… Disconnected from server "${serverName}".\n\nšŸ”„ Agent will be reinitialized without this server.`,
984
+ data: { serverDisconnected: true, serverName }
985
+ };
986
+ }
987
+ // Method to handle API key input and complete model selection
988
+ handleApiKeyInput(apiKey, provider, model) {
989
+ // Validate the API key
990
+ const validationResult = this.validateApiKey(provider, apiKey);
991
+ if (!validationResult.valid) {
992
+ return {
993
+ type: 'error',
994
+ message: validationResult.message
995
+ };
996
+ }
997
+ // Map provider to environment variable name
998
+ const envVarMap = {
999
+ openai: 'OPENAI_API_KEY',
1000
+ anthropic: 'ANTHROPIC_API_KEY',
1001
+ google: 'GOOGLE_API_KEY',
1002
+ mistral: 'MISTRAL_API_KEY'
1003
+ };
1004
+ const envVar = envVarMap[provider];
1005
+ // Store the API key in persistent storage
1006
+ this.persistentConfig.apiKeys[envVar] = apiKey;
1007
+ // Set the model configuration
1008
+ this.currentLLMConfig = {
1009
+ provider: provider,
1010
+ model,
1011
+ temperature: this.currentLLMConfig?.temperature || 0.7,
1012
+ maxTokens: this.currentLLMConfig?.maxTokens
1013
+ };
1014
+ // Save both the API key and the current model to persistent storage
1015
+ this.persistentConfig.lastModel = this.currentLLMConfig;
1016
+ SecureStorage.saveConfig(this.persistentConfig);
1017
+ const maskedKey = this.maskApiKey(apiKey);
1018
+ return {
1019
+ type: 'success',
1020
+ message: `āœ… ${provider} API key set (${maskedKey})\nšŸ¤– Switched to ${provider}/${model}`,
1021
+ data: { llmConfig: this.currentLLMConfig }
1022
+ };
1023
+ }
1024
+ }