1+ import { MCPRegistryClient } from '@src/domains/registry/mcpRegistryClient.js' ;
12import { createServerInstallationService , getProgressTrackingService } from '@src/domains/server-management/index.js' ;
23import { GlobalOptions } from '@src/globalOptions.js' ;
34import logger from '@src/logger/logger.js' ;
45
6+ import boxen from 'boxen' ;
7+ import chalk from 'chalk' ;
58import type { Argv } from 'yargs' ;
69
7- import { backupConfig , initializeConfigContext , reloadMcpConfig , serverExists } from './utils/configUtils.js' ;
10+ import {
11+ backupConfig ,
12+ getAllServers ,
13+ initializeConfigContext ,
14+ reloadMcpConfig ,
15+ serverExists ,
16+ } from './utils/configUtils.js' ;
17+ import { InstallWizard } from './utils/installWizard.js' ;
818import { generateOperationId , parseServerNameVersion , validateVersion } from './utils/serverUtils.js' ;
919
1020export interface InstallCommandArgs extends GlobalOptions {
11- serverName : string ;
21+ serverName ? : string ;
1222 force ?: boolean ;
1323 dryRun ?: boolean ;
1424 verbose ?: boolean ;
25+ interactive ?: boolean ;
1526}
1627
1728/**
@@ -22,7 +33,7 @@ export function buildInstallCommand(yargs: Argv) {
2233 . positional ( 'serverName' , {
2334 describe : 'Server name or name@version to install' ,
2435 type : 'string' ,
25- demandOption : true ,
36+ demandOption : false ,
2637 } )
2738 . option ( 'force' , {
2839 describe : 'Force installation even if already exists' ,
@@ -34,15 +45,23 @@ export function buildInstallCommand(yargs: Argv) {
3445 type : 'boolean' ,
3546 default : false ,
3647 } )
48+ . option ( 'interactive' , {
49+ describe : 'Launch interactive wizard for guided installation' ,
50+ type : 'boolean' ,
51+ default : false ,
52+ alias : 'i' ,
53+ } )
3754 . option ( 'verbose' , {
3855 describe : 'Detailed output' ,
3956 type : 'boolean' ,
4057 default : false ,
4158 alias : 'v' ,
4259 } )
4360 . example ( [
61+ [ '$0 mcp install' , 'Launch interactive installation wizard' ] ,
4462 [ '$0 mcp install filesystem' , 'Install latest version of filesystem server' ] ,
4563 [ '$0 mcp install [email protected] ' , 'Install specific version' ] , 64+ [ '$0 mcp install filesystem --interactive' , 'Install with interactive configuration' ] ,
4665 [ '$0 mcp install filesystem --force' , 'Force reinstallation' ] ,
4766 [ '$0 mcp install filesystem --dry-run' , 'Preview installation' ] ,
4867 ] ) ;
@@ -60,6 +79,7 @@ export async function installCommand(argv: InstallCommandArgs): Promise<void> {
6079 force = false ,
6180 dryRun = false ,
6281 verbose = false ,
82+ interactive = false ,
6383 } = argv ;
6484
6585 // Initialize configuration context
@@ -69,6 +89,12 @@ export async function installCommand(argv: InstallCommandArgs): Promise<void> {
6989 logger . info ( 'Starting installation process...' ) ;
7090 }
7191
92+ // Launch interactive wizard if no server name provided or --interactive flag set
93+ if ( ! inputServerName || interactive ) {
94+ await runInteractiveInstallation ( argv ) ;
95+ return ;
96+ }
97+
7298 // Parse server name and version
7399 const { name : registryServerId , version } = parseServerNameVersion ( inputServerName ) ;
74100
@@ -268,3 +294,242 @@ function deriveLocalServerName(registryId: string): string {
268294 logger . debug ( `Derived local server name '${ sanitized } ' from registry ID '${ registryId } '` ) ;
269295 return sanitized ;
270296}
297+
298+ /**
299+ * Run interactive installation workflow
300+ */
301+ async function runInteractiveInstallation ( argv : InstallCommandArgs ) : Promise < void > {
302+ const {
303+ serverName : initialServerId ,
304+ config : configPath ,
305+ 'config-dir' : configDir ,
306+ force = false ,
307+ dryRun = false ,
308+ verbose = false ,
309+ } = argv ;
310+
311+ // Initialize configuration context
312+ initializeConfigContext ( configPath , configDir ) ;
313+
314+ // Create registry client
315+ const registryClient = new MCPRegistryClient ( {
316+ baseUrl : 'https://registry.modelcontextprotocol.io' ,
317+ timeout : 30000 ,
318+ cache : {
319+ defaultTtl : 300 ,
320+ maxSize : 100 ,
321+ cleanupInterval : 60000 ,
322+ } ,
323+ } ) ;
324+
325+ // Create wizard
326+ const wizard = new InstallWizard ( registryClient ) ;
327+
328+ // Get existing server names for conflict detection
329+ const getExistingNames = ( ) => Object . keys ( getAllServers ( ) ) ;
330+
331+ // Run wizard loop (supports installing multiple servers)
332+ let continueInstalling = true ;
333+ let currentServerId = initialServerId ;
334+
335+ try {
336+ while ( continueInstalling ) {
337+ const existingNames = getExistingNames ( ) ;
338+ const wizardResult = await wizard . run ( currentServerId , existingNames ) ;
339+
340+ if ( wizardResult . cancelled ) {
341+ console . log ( '\n❌ Installation cancelled.\n' ) ;
342+ wizard . cleanup ( ) ;
343+ process . exit ( 0 ) ;
344+ }
345+
346+ // Perform installation with collected configuration
347+ try {
348+ const registryServerId = wizardResult . serverId ;
349+ const version = wizardResult . version ;
350+ const serverName = wizardResult . localName || deriveLocalServerName ( registryServerId ) ;
351+
352+ // Use forceOverride from wizard if user selected override option
353+ const shouldForce = force || wizardResult . forceOverride || false ;
354+
355+ // Check if server already exists (early check)
356+ const serverAlreadyExists = serverExists ( serverName ) ;
357+ if ( serverAlreadyExists && ! shouldForce ) {
358+ console . error ( `\n❌ Server '${ serverName } ' already exists. Use --force to reinstall.\n` ) ;
359+ wizard . cleanup ( ) ;
360+ if ( wizardResult . installAnother ) {
361+ currentServerId = undefined ;
362+ continue ;
363+ }
364+ process . exit ( 1 ) ;
365+ }
366+
367+ // Dry run mode
368+ if ( dryRun ) {
369+ console . log ( '🔍 Dry run mode - no changes will be made\n' ) ;
370+ console . log ( `Would install: ${ serverName } ${ version ? `@${ version } ` : '' } ` ) ;
371+ console . log ( `From registry: https://registry.modelcontextprotocol.io\n` ) ;
372+ if ( wizardResult . installAnother ) {
373+ currentServerId = undefined ;
374+ continue ;
375+ }
376+ wizard . cleanup ( ) ;
377+ process . exit ( 0 ) ;
378+ }
379+
380+ // Create operation ID for tracking
381+ const operationId = generateOperationId ( ) ;
382+ const progressTracker = getProgressTrackingService ( ) ;
383+
384+ // Helper function to show step indicator
385+ const showStepIndicator = ( currentStep : number , skipClear = false ) => {
386+ if ( ! skipClear ) {
387+ console . clear ( ) ;
388+ }
389+ const steps = [ 'Search' , 'Select' , 'Configure' , 'Confirm' , 'Install' ] ;
390+ const stepBar = steps
391+ . map ( ( step , index ) => {
392+ const num = index + 1 ;
393+ if ( num < currentStep ) {
394+ return chalk . green ( `✓ ${ step } ` ) ;
395+ } else if ( num === currentStep ) {
396+ return chalk . cyan . bold ( `► ${ step } ` ) ;
397+ } else {
398+ return chalk . gray ( `○ ${ step } ` ) ;
399+ }
400+ } )
401+ . join ( ' → ' ) ;
402+ console . log ( boxen ( stepBar , { padding : { left : 2 , right : 2 , top : 0 , bottom : 0 } , borderStyle : 'round' } ) ) ;
403+ console . log ( '' ) ;
404+ } ;
405+
406+ // Show Install step indicator (clear screen before starting)
407+ showStepIndicator ( 5 , false ) ;
408+
409+ // Start progress tracking
410+ progressTracker . startOperation ( operationId , 'install' , 5 ) ;
411+
412+ try {
413+ // Get installation service
414+ const installationService = createServerInstallationService ( ) ;
415+
416+ // Update progress: Validating
417+ console . log ( chalk . cyan ( '⏳ Validating server...' ) ) ;
418+ progressTracker . updateProgress ( operationId , 1 , 'Validating server' , `Checking registry for ${ serverName } ` ) ;
419+
420+ // Create backup if replacing existing server
421+ let backupPath : string | undefined ;
422+ if ( serverAlreadyExists ) {
423+ console . log ( chalk . cyan ( '⏳ Creating backup...' ) ) ;
424+ progressTracker . updateProgress ( operationId , 2 , 'Creating backup' , `Backing up existing configuration` ) ;
425+ backupPath = backupConfig ( ) ;
426+ console . log ( chalk . gray ( ` Backup created: ${ backupPath } ` ) ) ;
427+ logger . info ( `Backup created: ${ backupPath } ` ) ;
428+
429+ // Remove the existing server before reinstalling to prevent duplicates
430+ const { removeServer } = await import ( './utils/configUtils.js' ) ;
431+ const removed = removeServer ( serverName ) ;
432+ if ( removed ) {
433+ console . log ( chalk . gray ( ` Removed existing server '${ serverName } '` ) ) ;
434+ if ( verbose ) {
435+ logger . info ( `Removed existing server '${ serverName } ' before reinstalling` ) ;
436+ }
437+ }
438+ }
439+
440+ // Update progress: Installing
441+ console . log ( chalk . cyan ( `⏳ Installing ${ serverName } ${ version ? `@${ version } ` : '' } ...` ) ) ;
442+ progressTracker . updateProgress (
443+ operationId ,
444+ 3 ,
445+ 'Installing server' ,
446+ `Installing ${ serverName } ${ version ? `@${ version } ` : '' } ` ,
447+ ) ;
448+
449+ // Perform installation
450+ const result = await installationService . installServer ( registryServerId , version , {
451+ force : shouldForce ,
452+ verbose,
453+ localServerName : serverName ,
454+ tags : wizardResult . tags ,
455+ env : wizardResult . env ,
456+ args : wizardResult . args ,
457+ } ) ;
458+
459+ // Update progress: Finalizing
460+ console . log ( chalk . cyan ( '⏳ Finalizing...' ) ) ;
461+ progressTracker . updateProgress ( operationId , 4 , 'Finalizing' , 'Verifying configuration' ) ;
462+
463+ // Update progress: Reloading
464+ console . log ( chalk . cyan ( '⏳ Reloading configuration...' ) ) ;
465+ progressTracker . updateProgress ( operationId , 5 , 'Reloading configuration' , 'Applying changes' ) ;
466+
467+ // Reload MCP configuration
468+ reloadMcpConfig ( ) ;
469+
470+ // Complete the operation
471+ const duration = result . installedAt ? Date . now ( ) - result . installedAt . getTime ( ) : 0 ;
472+ progressTracker . completeOperation ( operationId , {
473+ success : true ,
474+ operationId,
475+ duration,
476+ message : `Successfully installed ${ serverName } ` ,
477+ } ) ;
478+
479+ // Show completed step indicator with all steps marked as done (don't clear logs)
480+ console . log ( '' ) ;
481+ showStepIndicator ( 6 , true ) ; // 6 means all steps are completed, true = skip clear
482+
483+ // Report success
484+ console . log (
485+ chalk . green . bold ( `✅ Successfully installed server '${ serverName } '${ version ? ` version ${ version } ` : '' } ` ) ,
486+ ) ;
487+ if ( backupPath ) {
488+ console . log ( chalk . gray ( `📁 Backup created: ${ backupPath } ` ) ) ;
489+ }
490+ if ( result . warnings . length > 0 ) {
491+ console . log ( chalk . yellow ( '\n⚠️ Warnings:' ) ) ;
492+ result . warnings . forEach ( ( warning ) => console . log ( chalk . yellow ( ` • ${ warning } ` ) ) ) ;
493+ }
494+ } catch ( error ) {
495+ progressTracker . failOperation ( operationId , error as Error ) ;
496+ throw error ;
497+ }
498+ } catch ( error ) {
499+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
500+ console . error ( `\n❌ Installation failed: ${ errorMessage } \n` ) ;
501+ if ( error instanceof Error && error . stack ) {
502+ logger . error ( 'Installation error stack:' , error . stack ) ;
503+ }
504+
505+ if ( wizardResult . installAnother ) {
506+ const continueAfterError = await wizard . run ( undefined , getExistingNames ( ) ) ;
507+ if ( continueAfterError . cancelled ) {
508+ wizard . cleanup ( ) ;
509+ process . exit ( 1 ) ;
510+ }
511+ currentServerId = undefined ;
512+ continue ;
513+ }
514+
515+ wizard . cleanup ( ) ;
516+ process . exit ( 1 ) ;
517+ }
518+
519+ // Check if user wants to install another
520+ if ( wizardResult . installAnother ) {
521+ currentServerId = undefined ;
522+ continueInstalling = true ;
523+ } else {
524+ continueInstalling = false ;
525+ }
526+ }
527+ } finally {
528+ // Always cleanup wizard resources
529+ wizard . cleanup ( ) ;
530+ }
531+
532+ // Explicitly exit after successful completion to prevent hanging
533+ // This ensures stdin doesn't keep the process alive
534+ process . exit ( 0 ) ;
535+ }
0 commit comments