This is page 9 of 9. Use http://codebase.md/portel-dev/ncp?page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .dxtignore ├── .github │ ├── FEATURE_STORY_TEMPLATE.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── mcp_server_request.yml │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── publish-mcp-registry.yml │ └── release.yml ├── .gitignore ├── .mcpbignore ├── .npmignore ├── .release-it.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── COMPLETE-IMPLEMENTATION-SUMMARY.md ├── CONTRIBUTING.md ├── CRITICAL-ISSUES-FOUND.md ├── docs │ ├── clients │ │ ├── claude-desktop.md │ │ ├── cline.md │ │ ├── continue.md │ │ ├── cursor.md │ │ ├── perplexity.md │ │ └── README.md │ ├── download-stats.md │ ├── guides │ │ ├── clipboard-security-pattern.md │ │ ├── how-it-works.md │ │ ├── mcp-prompts-for-user-interaction.md │ │ ├── mcpb-installation.md │ │ ├── ncp-registry-command.md │ │ ├── pre-release-checklist.md │ │ ├── telemetry-design.md │ │ └── testing.md │ ├── images │ │ ├── ncp-add.png │ │ ├── ncp-find.png │ │ ├── ncp-help.png │ │ ├── ncp-import.png │ │ ├── ncp-list.png │ │ └── ncp-transformation-flow.png │ ├── mcp-registry-setup.md │ ├── pr-schema-additions.ts │ └── stories │ ├── 01-dream-and-discover.md │ ├── 02-secrets-in-plain-sight.md │ ├── 03-sync-and-forget.md │ ├── 04-double-click-install.md │ ├── 05-runtime-detective.md │ └── 06-official-registry.md ├── DYNAMIC-RUNTIME-SUMMARY.md ├── EXTENSION-CONFIG-DISCOVERY.md ├── INSTALL-EXTENSION.md ├── INTERNAL-MCP-ARCHITECTURE.md ├── jest.config.js ├── LICENSE ├── MANAGEMENT-TOOLS-COMPLETE.md ├── manifest.json ├── manifest.json.backup ├── MCP-CONFIG-SCHEMA-IMPLEMENTATION-EXAMPLE.ts ├── MCP-CONFIG-SCHEMA-SIMPLE-EXAMPLE.json ├── MCP-CONFIGURATION-SCHEMA-FORMAT.json ├── MCPB-ARCHITECTURE-DECISION.md ├── NCP-EXTENSION-COMPLETE.md ├── package-lock.json ├── package.json ├── parity-between-cli-and-mcp.txt ├── PROMPTS-IMPLEMENTATION.md ├── README-COMPARISON.md ├── README.md ├── README.new.md ├── REGISTRY-INTEGRATION-COMPLETE.md ├── RELEASE-PROCESS-IMPROVEMENTS.md ├── RELEASE-SUMMARY.md ├── RELEASE.md ├── RUNTIME-DETECTION-COMPLETE.md ├── scripts │ ├── cleanup │ │ └── scan-repository.js │ └── sync-server-version.cjs ├── SECURITY.md ├── server.json ├── src │ ├── analytics │ │ ├── analytics-formatter.ts │ │ ├── log-parser.ts │ │ └── visual-formatter.ts │ ├── auth │ │ ├── oauth-device-flow.ts │ │ └── token-store.ts │ ├── cache │ │ ├── cache-patcher.ts │ │ ├── csv-cache.ts │ │ └── schema-cache.ts │ ├── cli │ │ └── index.ts │ ├── discovery │ │ ├── engine.ts │ │ ├── mcp-domain-analyzer.ts │ │ ├── rag-engine.ts │ │ ├── search-enhancer.ts │ │ └── semantic-enhancement-engine.ts │ ├── extension │ │ └── extension-init.ts │ ├── index-mcp.ts │ ├── index.ts │ ├── internal-mcps │ │ ├── internal-mcp-manager.ts │ │ ├── ncp-management.ts │ │ └── types.ts │ ├── orchestrator │ │ └── ncp-orchestrator.ts │ ├── profiles │ │ └── profile-manager.ts │ ├── server │ │ ├── mcp-prompts.ts │ │ └── mcp-server.ts │ ├── services │ │ ├── config-prompter.ts │ │ ├── config-schema-reader.ts │ │ ├── error-handler.ts │ │ ├── output-formatter.ts │ │ ├── registry-client.ts │ │ ├── tool-context-resolver.ts │ │ ├── tool-finder.ts │ │ ├── tool-schema-parser.ts │ │ └── usage-tips-generator.ts │ ├── testing │ │ ├── create-real-mcp-definitions.ts │ │ ├── dummy-mcp-server.ts │ │ ├── mcp-definitions.json │ │ ├── real-mcp-analyzer.ts │ │ ├── real-mcp-definitions.json │ │ ├── real-mcps.csv │ │ ├── setup-dummy-mcps.ts │ │ ├── setup-tiered-profiles.ts │ │ ├── test-profile.json │ │ ├── test-semantic-enhancement.ts │ │ └── verify-profile-scaling.ts │ ├── transports │ │ └── filtered-stdio-transport.ts │ └── utils │ ├── claude-desktop-importer.ts │ ├── client-importer.ts │ ├── client-registry.ts │ ├── config-manager.ts │ ├── health-monitor.ts │ ├── highlighting.ts │ ├── logger.ts │ ├── markdown-renderer.ts │ ├── mcp-error-parser.ts │ ├── mcp-wrapper.ts │ ├── ncp-paths.ts │ ├── parameter-prompter.ts │ ├── paths.ts │ ├── progress-spinner.ts │ ├── response-formatter.ts │ ├── runtime-detector.ts │ ├── schema-examples.ts │ ├── security.ts │ ├── text-utils.ts │ ├── update-checker.ts │ ├── updater.ts │ └── version.ts ├── STORY-DRIVEN-DOCUMENTATION.md ├── STORY-FIRST-WORKFLOW.md ├── test │ ├── __mocks__ │ │ ├── chalk.js │ │ ├── transformers.js │ │ ├── updater.js │ │ └── version.ts │ ├── cache-loading-focused.test.ts │ ├── cache-optimization.test.ts │ ├── cli-help-validation.sh │ ├── coverage-boost.test.ts │ ├── curated-ecosystem-validation.test.ts │ ├── discovery-engine.test.ts │ ├── discovery-fallback-focused.test.ts │ ├── ecosystem-discovery-focused.test.ts │ ├── ecosystem-discovery-validation-simple.test.ts │ ├── final-80-percent-push.test.ts │ ├── final-coverage-push.test.ts │ ├── health-integration.test.ts │ ├── health-monitor.test.ts │ ├── helpers │ │ └── mock-server-manager.ts │ ├── integration │ │ └── mcp-client-simulation.test.cjs │ ├── logger.test.ts │ ├── mcp-ecosystem-discovery.test.ts │ ├── mcp-error-parser.test.ts │ ├── mcp-immediate-response-check.js │ ├── mcp-server-protocol.test.ts │ ├── mcp-timeout-scenarios.test.ts │ ├── mcp-wrapper.test.ts │ ├── mock-mcps │ │ ├── aws-server.js │ │ ├── base-mock-server.mjs │ │ ├── brave-search-server.js │ │ ├── docker-server.js │ │ ├── filesystem-server.js │ │ ├── git-server.mjs │ │ ├── github-server.js │ │ ├── neo4j-server.js │ │ ├── notion-server.js │ │ ├── playwright-server.js │ │ ├── postgres-server.js │ │ ├── shell-server.js │ │ ├── slack-server.js │ │ └── stripe-server.js │ ├── mock-smithery-mcp │ │ ├── index.js │ │ ├── package.json │ │ └── smithery.yaml │ ├── ncp-orchestrator.test.ts │ ├── orchestrator-health-integration.test.ts │ ├── orchestrator-simple-branches.test.ts │ ├── performance-benchmark.test.ts │ ├── quick-coverage.test.ts │ ├── rag-engine.test.ts │ ├── regression-snapshot.test.ts │ ├── search-enhancer.test.ts │ ├── session-id-passthrough.test.ts │ ├── setup.ts │ ├── tool-context-resolver.test.ts │ ├── tool-schema-parser.test.ts │ ├── user-story-discovery.test.ts │ └── version-util.test.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /src/orchestrator/ncp-orchestrator.ts: -------------------------------------------------------------------------------- ```typescript /** * NCP Orchestrator - Real MCP Connections * Based on commercial NCP implementation */ import { readFileSync, existsSync } from 'fs'; import { getCacheDirectory } from '../utils/ncp-paths.js'; import { join } from 'path'; import { createHash } from 'crypto'; import ProfileManager from '../profiles/profile-manager.js'; import { logger } from '../utils/logger.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { DiscoveryEngine } from '../discovery/engine.js'; import { MCPHealthMonitor } from '../utils/health-monitor.js'; import { SearchEnhancer } from '../discovery/search-enhancer.js'; import { mcpWrapper } from '../utils/mcp-wrapper.js'; import { withFilteredOutput } from '../transports/filtered-stdio-transport.js'; import { ToolSchemaParser, ParameterInfo } from '../services/tool-schema-parser.js'; import { InternalMCPManager } from '../internal-mcps/internal-mcp-manager.js'; import { ToolContextResolver } from '../services/tool-context-resolver.js'; import type { OAuthConfig } from '../auth/oauth-device-flow.js'; import { getRuntimeForExtension, logRuntimeInfo } from '../utils/runtime-detector.js'; // Simple string similarity for tool name matching function calculateSimilarity(str1: string, str2: string): number { const s1 = str1.toLowerCase(); const s2 = str2.toLowerCase(); // Exact match if (s1 === s2) return 1.0; // Check if one contains the other if (s1.includes(s2) || s2.includes(s1)) { return 0.8; } // Simple Levenshtein-based similarity const longer = s1.length > s2.length ? s1 : s2; const shorter = s1.length > s2.length ? s2 : s1; if (longer.length === 0) return 1.0; const editDistance = levenshteinDistance(s1, s2); return (longer.length - editDistance) / longer.length; } function levenshteinDistance(str1: string, str2: string): number { const matrix: number[][] = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } import { CachePatcher } from '../cache/cache-patcher.js'; import { CSVCache, CachedTool } from '../cache/csv-cache.js'; import { spinner } from '../utils/progress-spinner.js'; interface DiscoveryResult { toolName: string; mcpName: string; confidence: number; description?: string; schema?: any; } interface ExecutionResult { success: boolean; content?: any; error?: string; } interface MCPConfig { name: string; command?: string; // Optional: for stdio transport args?: string[]; env?: Record<string, string>; url?: string; // Optional: for HTTP/SSE transport (Claude Desktop native) auth?: { type: 'oauth' | 'bearer' | 'apiKey' | 'basic'; oauth?: OAuthConfig; // OAuth 2.0 Device Flow configuration token?: string; // Bearer token or API key username?: string; // Basic auth username password?: string; // Basic auth password }; } interface Profile { name: string; description: string; mcpServers: Record<string, { command?: string; // Optional: for stdio transport args?: string[]; env?: Record<string, string>; url?: string; // Optional: for HTTP/SSE transport auth?: { type: 'oauth' | 'bearer' | 'apiKey' | 'basic'; oauth?: OAuthConfig; // OAuth 2.0 Device Flow configuration token?: string; // Bearer token or API key username?: string; // Basic auth username password?: string; // Basic auth password }; }>; metadata?: any; } interface MCPConnection { client: Client; transport: StdioClientTransport | SSEClientTransport; tools: Array<{name: string; description: string}>; serverInfo?: { name: string; title?: string; version: string; description?: string; websiteUrl?: string; }; lastUsed: number; connectTime: number; executionCount: number; } interface MCPDefinition { name: string; config: MCPConfig; tools: Array<{name: string; description: string}>; serverInfo?: { name: string; title?: string; version: string; description?: string; websiteUrl?: string; }; } export class NCPOrchestrator { private definitions: Map<string, MCPDefinition> = new Map(); private connections: Map<string, MCPConnection> = new Map(); private toolToMCP: Map<string, string> = new Map(); private allTools: Array<{ name: string; description: string; mcpName: string }> = []; private profileName: string; private readonly QUICK_PROBE_TIMEOUT = 8000; // 8 seconds - first attempt private readonly SLOW_PROBE_TIMEOUT = 30000; // 30 seconds - retry for slow MCPs private readonly CONNECTION_TIMEOUT = 10000; // 10 seconds private readonly IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes private readonly CLEANUP_INTERVAL = 60 * 1000; // Check every minute private cleanupTimer?: NodeJS.Timeout; private discovery: DiscoveryEngine; private healthMonitor: MCPHealthMonitor; private cachePatcher: CachePatcher; private csvCache: CSVCache; private showProgress: boolean; private indexingProgress: { current: number; total: number; currentMCP: string; estimatedTimeRemaining?: number } | null = null; private indexingStartTime: number = 0; private profileManager: ProfileManager | null = null; private internalMCPManager: InternalMCPManager; private forceRetry: boolean = false; /** * ⚠️ CRITICAL: Default profile MUST be 'all' - DO NOT CHANGE! * * The 'all' profile is the universal profile that contains all MCPs. * This default is used by MCPServer and all CLI commands. * * DO NOT change this to 'default' or any other name - it will break everything. */ constructor(profileName: string = 'all', showProgress: boolean = false, forceRetry: boolean = false) { this.profileName = profileName; this.discovery = new DiscoveryEngine(); this.healthMonitor = new MCPHealthMonitor(); this.cachePatcher = new CachePatcher(); this.csvCache = new CSVCache(getCacheDirectory(), profileName); this.showProgress = showProgress; this.forceRetry = forceRetry; this.internalMCPManager = new InternalMCPManager(); } private async loadProfile(): Promise<Profile | null> { try { // Create and store ProfileManager instance (reused for auto-import) if (!this.profileManager) { this.profileManager = new ProfileManager(); await this.profileManager.initialize(); // Initialize internal MCPs with ProfileManager this.internalMCPManager.initialize(this.profileManager); } const profile = await this.profileManager.getProfile(this.profileName); if (!profile) { logger.error(`Profile not found: ${this.profileName}`); return null; } return profile; } catch (error: any) { logger.error(`Failed to load profile: ${error.message}`); return null; } } async initialize(): Promise<void> { const startTime = Date.now(); this.indexingStartTime = startTime; // Debug logging if (process.env.NCP_DEBUG === 'true') { console.error(`[DEBUG ORC] Initializing with profileName: ${this.profileName}`); console.error(`[DEBUG ORC] Cache will use: ${this.csvCache ? 'csvCache exists' : 'NO CACHE'}`); } logger.info(`Initializing NCP orchestrator with profile: ${this.profileName}`); // Log runtime detection info (how NCP is running) if (process.env.NCP_DEBUG === 'true') { logRuntimeInfo(); } // Initialize progress immediately to prevent race condition // Total will be updated once we know how many MCPs need indexing this.indexingProgress = { current: 0, total: 0, currentMCP: 'initializing...' }; const profile = await this.loadProfile(); if (process.env.NCP_DEBUG === 'true') { console.error(`[DEBUG ORC] Loaded profile: ${profile ? 'YES' : 'NO'}`); if (profile) { console.error(`[DEBUG ORC] Profile MCPs: ${Object.keys(profile.mcpServers || {}).join(', ')}`); } } if (!profile) { logger.error('Failed to load profile'); this.indexingProgress = null; return; } // Initialize discovery engine first await this.discovery.initialize(); // Initialize CSV cache await this.csvCache.initialize(); // Get profile hash for cache validation const profileHash = CSVCache.hashProfile(profile.mcpServers); // Check if cache is valid const cacheValid = this.csvCache.validateCache(profileHash); const mcpConfigs: MCPConfig[] = Object.entries(profile.mcpServers).map(([name, config]) => ({ name, command: config.command, args: config.args, env: config.env || {}, url: config.url // HTTP/SSE transport support })); if (cacheValid) { // Load from cache logger.info('Loading tools from CSV cache...'); const cachedMCPCount = await this.loadFromCSVCache(mcpConfigs); } else { // Cache invalid - clear it to force full re-indexing logger.info('Cache invalid, clearing for full re-index...'); await this.csvCache.clear(); await this.csvCache.initialize(); } // Get list of MCPs that need indexing const indexedMCPs = this.csvCache.getIndexedMCPs(); const mcpsToIndex = mcpConfigs.filter(config => { const tools = profile.mcpServers[config.name]; const currentHash = CSVCache.hashProfile(tools); // Check if already indexed if (this.csvCache.isMCPIndexed(config.name, currentHash)) { return false; } // Check if failed and should retry return this.csvCache.shouldRetryFailed(config.name, this.forceRetry); }); if (mcpsToIndex.length > 0) { // Update progress tracking with actual count if (this.indexingProgress) { this.indexingProgress.total = mcpsToIndex.length; } if (this.showProgress) { const action = 'Indexing'; const cachedCount = this.csvCache.getIndexedMCPs().size; // Count only failed MCPs that are NOT being retried in this run const allFailedCount = this.csvCache.getFailedMCPsCount(); const retryingNowCount = mcpsToIndex.filter(config => { // Check if this MCP is in the failed list (being retried) return this.csvCache.isMCPFailed(config.name); }).length; const failedNotRetryingCount = allFailedCount - retryingNowCount; const totalProcessed = cachedCount + failedNotRetryingCount; const statusMsg = `${action} MCPs: ${totalProcessed}/${mcpConfigs.length}`; spinner.start(statusMsg); spinner.updateSubMessage('Initializing discovery engine...'); } // Start incremental cache writing await this.csvCache.startIncrementalWrite(profileHash); // Index only the MCPs that need it await this.discoverMCPTools(mcpsToIndex, profile, true, mcpConfigs.length); // Finalize cache await this.csvCache.finalize(); if (this.showProgress) { const successfulMCPs = this.definitions.size; const failedMCPs = this.csvCache.getFailedMCPsCount(); const totalProcessed = successfulMCPs + failedMCPs; if (failedMCPs > 0) { spinner.success(`Indexed ${this.allTools.length} tools from ${successfulMCPs} MCPs | ${failedMCPs} failed (will retry later)`); } else { spinner.success(`Indexed ${this.allTools.length} tools from ${successfulMCPs} MCPs`); } } } // Clear progress tracking once complete this.indexingProgress = null; // Add internal MCPs to discovery this.addInternalMCPsToDiscovery(); // Start cleanup timer for idle connections this.cleanupTimer = setInterval( () => this.cleanupIdleConnections(), this.CLEANUP_INTERVAL ); const externalMCPs = this.definitions.size; const internalMCPs = this.internalMCPManager.getAllInternalMCPs().length; const loadTime = Date.now() - startTime; logger.info(`🚀 NCP-OSS initialized in ${loadTime}ms with ${this.allTools.length} tools from ${externalMCPs} external + ${internalMCPs} internal MCPs`); } /** * Load cached tools from CSV */ private async loadFromCSVCache(mcpConfigs: MCPConfig[]): Promise<number> { const cachedTools = this.csvCache.loadCachedTools(); // Group tools by MCP const toolsByMCP = new Map<string, CachedTool[]>(); for (const tool of cachedTools) { if (!toolsByMCP.has(tool.mcpName)) { toolsByMCP.set(tool.mcpName, []); } toolsByMCP.get(tool.mcpName)!.push(tool); } let loadedMCPCount = 0; // Rebuild definitions and tool mappings from cache for (const config of mcpConfigs) { const mcpTools = toolsByMCP.get(config.name) || []; if (mcpTools.length === 0) continue; loadedMCPCount++; // Create definition this.definitions.set(config.name, { name: config.name, config, tools: mcpTools.map(t => ({ name: t.toolName, description: t.description, inputSchema: {} })), serverInfo: undefined }); // Add to all tools and create mappings for (const cachedTool of mcpTools) { const tool = { name: cachedTool.toolName, description: cachedTool.description, mcpName: config.name }; this.allTools.push(tool); this.toolToMCP.set(cachedTool.toolId, config.name); } // Index tools in discovery engine const discoveryTools = mcpTools.map(t => ({ id: t.toolId, name: t.toolName, description: t.description })); // Use async indexing to avoid blocking this.discovery.indexMCPTools(config.name, discoveryTools); } logger.info(`Loaded ${this.allTools.length} tools from CSV cache`); return loadedMCPCount; } private async discoverMCPTools(mcpConfigs: MCPConfig[], profile?: Profile, incrementalMode: boolean = false, totalMCPCount?: number): Promise<void> { // Only clear allTools if not in incremental mode if (!incrementalMode) { this.allTools = []; } const displayTotal = totalMCPCount || mcpConfigs.length; for (let i = 0; i < mcpConfigs.length; i++) { const config = mcpConfigs[i]; try { logger.info(`Discovering tools from MCP: ${config.name}`); if (this.showProgress) { spinner.updateSubMessage(`Connecting to ${config.name}...`); } let result; try { // First attempt with quick timeout result = await this.probeMCPTools(config, this.QUICK_PROBE_TIMEOUT); } catch (firstError: any) { // If it timed out (not connection error), retry with longer timeout if (firstError.message.includes('Probe timeout') || firstError.message.includes('timeout')) { logger.debug(`${config.name} timed out on first attempt, retrying with longer timeout...`); if (this.showProgress) { spinner.updateSubMessage(`Retrying ${config.name} (heavy initialization)...`); } // Second attempt with slow timeout for heavy MCPs result = await this.probeMCPTools(config, this.SLOW_PROBE_TIMEOUT); } else { // Not a timeout - it's a real error (connection refused, etc), don't retry throw firstError; } } // Store definition with schema fallback applied this.definitions.set(config.name, { name: config.name, config, tools: result.tools.map(tool => ({ ...tool, inputSchema: tool.inputSchema || {} })), serverInfo: result.serverInfo }); // Add to all tools and create mappings const discoveryTools = []; for (const tool of result.tools) { // Store with prefixed name for consistency with commercial version const prefixedToolName = `${config.name}:${tool.name}`; const prefixedDescription = `${config.name}: ${tool.description || 'No description available'}`; this.allTools.push({ name: prefixedToolName, description: prefixedDescription, mcpName: config.name }); // Map both formats for backward compatibility this.toolToMCP.set(tool.name, config.name); this.toolToMCP.set(prefixedToolName, config.name); // Prepare for discovery engine indexing // Pass unprefixed name and description - RAG engine will add the prefix discoveryTools.push({ id: prefixedToolName, name: tool.name, // Use unprefixed name here description: tool.description || 'No description available', // Use unprefixed description mcpServer: config.name, inputSchema: tool.inputSchema || {} }); } if (this.showProgress) { // Add time estimate to indexing sub-message for parity let timeDisplay = ''; if (this.indexingProgress?.estimatedTimeRemaining) { const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000); timeDisplay = ` (~${remainingSeconds}s remaining)`; } spinner.updateSubMessage(`Indexing ${result.tools.length} tools from ${config.name}...${timeDisplay}`); } // Index tools with discovery engine for vector search await this.discovery.indexMCPTools(config.name, discoveryTools); // Append to CSV cache incrementally (if in incremental mode) if (incrementalMode && profile) { const mcpHash = CSVCache.hashProfile(profile.mcpServers[config.name]); const cachedTools: CachedTool[] = result.tools.map(tool => ({ mcpName: config.name, toolId: `${config.name}:${tool.name}`, toolName: tool.name, description: tool.description || 'No description available', hash: this.hashString(tool.description || ''), timestamp: new Date().toISOString() })); await this.csvCache.appendMCP(config.name, cachedTools, mcpHash); } // Update indexing progress AFTER successfully appending to cache if (this.indexingProgress) { this.indexingProgress.current = i + 1; this.indexingProgress.currentMCP = config.name; // Estimate remaining time based on average time per MCP so far const elapsedTime = Date.now() - this.indexingStartTime; const averageTimePerMCP = elapsedTime / (i + 1); const remainingMCPs = mcpConfigs.length - (i + 1); this.indexingProgress.estimatedTimeRemaining = remainingMCPs * averageTimePerMCP; } if (this.showProgress) { // Calculate absolute position const cachedCount = displayTotal - mcpConfigs.length; const currentAbsolute = cachedCount + (i + 1); const percentage = Math.round((currentAbsolute / displayTotal) * 100); // Add time estimate let timeDisplay = ''; if (this.indexingProgress?.estimatedTimeRemaining) { const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000); timeDisplay = ` ~${remainingSeconds}s remaining`; } spinner.updateMessage(`Indexing MCPs: ${currentAbsolute}/${displayTotal} (${percentage}%)${timeDisplay}`); } logger.info(`Discovered ${result.tools.length} tools from ${config.name}`); } catch (error: any) { // Probe failures are expected - don't alarm users with error messages logger.debug(`Failed to discover tools from ${config.name}: ${error.message}`); // Mark MCP as failed for scheduled retry (if in incremental mode) if (incrementalMode && profile) { this.csvCache.markFailed(config.name, error); } // Update indexing progress even for failed MCPs if (this.indexingProgress) { this.indexingProgress.current = i + 1; this.indexingProgress.currentMCP = config.name; // Estimate remaining time based on average time per MCP so far const elapsedTime = Date.now() - this.indexingStartTime; const averageTimePerMCP = elapsedTime / (i + 1); const remainingMCPs = mcpConfigs.length - (i + 1); this.indexingProgress.estimatedTimeRemaining = remainingMCPs * averageTimePerMCP; } if (this.showProgress) { // Calculate absolute position const cachedCount = displayTotal - mcpConfigs.length; const currentAbsolute = cachedCount + (i + 1); const percentage = Math.round((currentAbsolute / displayTotal) * 100); // Add time estimate let timeDisplay = ''; if (this.indexingProgress?.estimatedTimeRemaining) { const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000); timeDisplay = ` ~${remainingSeconds}s remaining`; } spinner.updateMessage(`Indexing MCPs: ${currentAbsolute}/${displayTotal} (${percentage}%)${timeDisplay}`); spinner.updateSubMessage(`Skipped ${config.name} (connection failed)`); } // Update health monitor with the actual error for import feedback this.healthMonitor.markUnhealthy(config.name, error.message); } } } /** * Create appropriate transport based on config * Supports both stdio (command/args) and HTTP/SSE (url) transports * Handles OAuth authentication for HTTP/SSE connections */ private async createTransport(config: MCPConfig, env?: Record<string, string>): Promise<StdioClientTransport | SSEClientTransport> { if (config.url) { // HTTP/SSE transport (Claude Desktop native support) const url = new URL(config.url); const headers: Record<string, string> = {}; // Handle authentication if (config.auth) { const token = await this.getAuthToken(config); switch (config.auth.type) { case 'oauth': case 'bearer': headers['Authorization'] = `Bearer ${token}`; break; case 'apiKey': // API key can be in header or query param - assume header for now headers['X-API-Key'] = token; break; case 'basic': if (config.auth.username && config.auth.password) { const credentials = Buffer.from(`${config.auth.username}:${config.auth.password}`).toString('base64'); headers['Authorization'] = `Basic ${credentials}`; } break; } } // Use requestInit to add custom headers to POST requests // and eventSourceInit to add headers to the initial SSE connection const options = Object.keys(headers).length > 0 ? { requestInit: { headers }, eventSourceInit: { headers } as EventSourceInit } : undefined; return new SSEClientTransport(url, options); } if (config.command) { // stdio transport (local process) const resolvedCommand = getRuntimeForExtension(config.command); const wrappedCommand = mcpWrapper.createWrapper( config.name, resolvedCommand, config.args || [] ); return new StdioClientTransport({ command: wrappedCommand.command, args: wrappedCommand.args, env: env as Record<string, string> }); } throw new Error(`Invalid config for ${config.name}: must have either 'command' or 'url'`); } /** * Get authentication token for MCP * Handles OAuth Device Flow and token refresh */ private async getAuthToken(config: MCPConfig): Promise<string> { if (!config.auth) { throw new Error('No auth configuration provided'); } // For non-OAuth auth types, return the token directly if (config.auth.type !== 'oauth') { return config.auth.token || ''; } // OAuth flow if (!config.auth.oauth) { throw new Error('OAuth configuration missing'); } const { getTokenStore } = await import('../auth/token-store.js'); const tokenStore = getTokenStore(); // Check for existing valid token const existingToken = await tokenStore.getToken(config.name); if (existingToken) { return existingToken.access_token; } // No valid token - trigger OAuth Device Flow const { DeviceFlowAuthenticator } = await import('../auth/oauth-device-flow.js'); const authenticator = new DeviceFlowAuthenticator(config.auth.oauth); logger.info(`No valid token found for ${config.name}, starting OAuth Device Flow...`); const tokenResponse = await authenticator.authenticate(); // Store token for future use await tokenStore.storeToken(config.name, tokenResponse); return tokenResponse.access_token; } // Based on commercial NCP's probeMCPTools method private async probeMCPTools(config: MCPConfig, timeout: number = this.QUICK_PROBE_TIMEOUT): Promise<{ tools: Array<{name: string; description: string; inputSchema?: any}>; serverInfo?: { name: string; title?: string; version: string; description?: string; websiteUrl?: string; }; }> { if (!config.command && !config.url) { throw new Error(`Invalid config for ${config.name}: must have either 'command' or 'url'`); } let client: Client | null = null; let transport: StdioClientTransport | SSEClientTransport | null = null; try { // Create temporary connection for discovery const silentEnv = { ...process.env, ...(config.env || {}), MCP_SILENT: 'true', QUIET: 'true', NO_COLOR: 'true' }; transport = await this.createTransport(config, silentEnv); client = new Client( { name: 'ncp-oss', version: '1.0.0' }, { capabilities: {} } ); // Connect with timeout and filtered output await withFilteredOutput(async () => { await Promise.race([ client!.connect(transport!), new Promise((_, reject) => setTimeout(() => reject(new Error('Probe timeout')), timeout) ) ]); }); // Capture server info after connection const serverInfo = client!.getServerVersion(); // Get tool list with filtered output const response = await withFilteredOutput(async () => { return await client!.listTools(); }); const tools = response.tools.map(t => ({ name: t.name, description: t.description || '', inputSchema: t.inputSchema || {} })); // Disconnect immediately await client.close(); return { tools, serverInfo: serverInfo ? { name: serverInfo.name || config.name, title: serverInfo.title, version: serverInfo.version || 'unknown', description: serverInfo.title || serverInfo.name || undefined, websiteUrl: serverInfo.websiteUrl } : undefined }; } catch (error: any) { // Clean up on error if (client) { try { await client.close(); } catch {} } // Log full error details for debugging logger.debug(`Full error details for ${config.name}: ${JSON.stringify({ message: error.message, code: error.code, data: error.data, stack: error.stack?.split('\n')[0] })}`); throw error; } } async find(query: string, limit: number = 5, detailed: boolean = false): Promise<DiscoveryResult[]> { if (!query) { // No query = list all tools, filtered by health const healthyTools = this.allTools.filter(tool => this.healthMonitor.getHealthyMCPs([tool.mcpName]).length > 0); const results = healthyTools.slice(0, limit).map(tool => { // Extract actual tool name from prefixed format const actualToolName = tool.name.includes(':') ? tool.name.split(':', 2)[1] : tool.name; return { toolName: tool.name, // Return prefixed name mcpName: tool.mcpName, confidence: 1.0, description: detailed ? tool.description : undefined, schema: detailed ? this.getToolSchema(tool.mcpName, actualToolName) : undefined }; }); return results; } // Use battle-tested vector search from commercial NCP // DOUBLE SEARCH TECHNIQUE: Request 2x results to account for filtering disabled MCPs try { const doubleLimit = limit * 2; // Request double to account for filtered MCPs const vectorResults = await this.discovery.findRelevantTools(query, doubleLimit); // Apply universal term frequency scoring boost const adjustedResults = this.adjustScoresUniversally(query, vectorResults); // Parse and filter results const parsedResults = adjustedResults.map(result => { // Parse tool format: "mcp:tool" or just "tool" const parts = result.name.includes(':') ? result.name.split(':', 2) : [this.toolToMCP.get(result.name) || 'unknown', result.name]; const mcpName = parts[0]; const toolName = parts[1] || result.name; // Find the tool - it should be stored with prefixed name const prefixedToolName = `${mcpName}:${toolName}`; const fullTool = this.allTools.find(t => (t.name === prefixedToolName || t.name === toolName) && t.mcpName === mcpName ); return { toolName: fullTool?.name || prefixedToolName, // Return the stored (prefixed) name mcpName, confidence: result.confidence, description: detailed ? fullTool?.description : undefined, schema: detailed ? this.getToolSchema(mcpName, toolName) : undefined }; }); // HEALTH FILTERING: Remove tools from disabled MCPs const healthyResults = parsedResults.filter(result => { return this.healthMonitor.getHealthyMCPs([result.mcpName]).length > 0; }); // SORT by confidence (highest first) after our scoring adjustments const sortedResults = healthyResults.sort((a, b) => b.confidence - a.confidence); // Return up to the original limit after filtering and sorting const finalResults = sortedResults.slice(0, limit); if (healthyResults.length < parsedResults.length) { logger.debug(`Health filtering: ${parsedResults.length - healthyResults.length} tools filtered out from disabled MCPs`); } return finalResults; } catch (error: any) { logger.error(`Vector search failed: ${error.message}`); // Fallback to healthy tools only const healthyTools = this.allTools.filter(tool => this.healthMonitor.getHealthyMCPs([tool.mcpName]).length > 0); return healthyTools.slice(0, limit).map(tool => { // Extract actual tool name from prefixed format for schema lookup const actualToolName = tool.name.includes(':') ? tool.name.split(':', 2)[1] : tool.name; return { toolName: tool.name, // Return prefixed name mcpName: tool.mcpName, confidence: 0.5, description: detailed ? tool.description : undefined, schema: detailed ? this.getToolSchema(tool.mcpName, actualToolName) : undefined }; }); } } async run(toolName: string, parameters: any, meta?: Record<string, any>): Promise<ExecutionResult> { // Parse tool format: "mcp:tool" or just "tool" let mcpName: string; let actualToolName: string; if (toolName.includes(':')) { [mcpName, actualToolName] = toolName.split(':', 2); } else { actualToolName = toolName; mcpName = this.toolToMCP.get(toolName) || ''; } if (!mcpName) { const similarTools = this.findSimilarTools(toolName); let errorMessage = `Tool '${toolName}' not found.`; if (similarTools.length > 0) { errorMessage += ` Did you mean: ${similarTools.join(', ')}?`; } errorMessage += ` Use 'ncp find "${toolName}"' to search for similar tools or 'ncp find --depth 0' to list all available tools.`; return { success: false, error: errorMessage }; } // Check if this is an internal MCP if (this.internalMCPManager.isInternalMCP(mcpName)) { try { const result = await this.internalMCPManager.executeInternalTool(mcpName, actualToolName, parameters); return { success: result.success, content: result.content, error: result.error }; } catch (error: any) { logger.error(`Internal tool execution failed for ${toolName}:`, error); return { success: false, error: error.message || 'Internal tool execution failed' }; } } const definition = this.definitions.get(mcpName); if (!definition) { const availableMcps = Array.from(this.definitions.keys()).join(', '); return { success: false, error: `MCP '${mcpName}' not found. Available MCPs: ${availableMcps}. Use 'ncp find' to discover tools or check your profile configuration.` }; } try { // Get or create pooled connection const connection = await this.getOrCreateConnection(mcpName); // Validate parameters before execution const validationError = this.validateToolParameters(mcpName, actualToolName, parameters); if (validationError) { return { success: false, error: validationError }; } // Execute tool with filtered output to suppress MCP server console messages // Forward _meta transparently to support session_id and other protocol-level metadata const result = await withFilteredOutput(async () => { return await connection.client.callTool({ name: actualToolName, arguments: parameters, _meta: meta }); }); // Mark MCP as healthy on successful execution this.healthMonitor.markHealthy(mcpName); return { success: true, content: result.content }; } catch (error: any) { logger.error(`Tool execution failed for ${toolName}:`, error); // Mark MCP as unhealthy on execution failure this.healthMonitor.markUnhealthy(mcpName, error.message); return { success: false, error: this.enhanceErrorMessage(error, actualToolName, mcpName) }; } } private async getOrCreateConnection(mcpName: string): Promise<MCPConnection> { // Return existing connection if available const existing = this.connections.get(mcpName); if (existing) { existing.lastUsed = Date.now(); existing.executionCount++; return existing; } const definition = this.definitions.get(mcpName); if (!definition) { const availableMcps = Array.from(this.definitions.keys()).join(', '); throw new Error(`MCP '${mcpName}' not found. Available MCPs: ${availableMcps}. Use 'ncp find' to discover tools or check your profile configuration.`); } logger.info(`🔌 Connecting to ${mcpName} (for execution)...`); const connectStart = Date.now(); try { // Add environment variables const silentEnv = { ...process.env, ...(definition.config.env || {}), // These may still help some servers MCP_SILENT: 'true', QUIET: 'true', NO_COLOR: 'true' }; const transport = await this.createTransport(definition.config, silentEnv); const client = new Client( { name: 'ncp-oss', version: '1.0.0' }, { capabilities: {} } ); // Connect with timeout and filtered output await withFilteredOutput(async () => { await Promise.race([ client.connect(transport), new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), this.CONNECTION_TIMEOUT) ) ]); }); // Capture server info after successful connection const serverInfo = client.getServerVersion(); const connection: MCPConnection = { client, transport, tools: [], // Will be populated if needed serverInfo: serverInfo ? { name: serverInfo.name || mcpName, title: serverInfo.title, version: serverInfo.version || 'unknown', description: serverInfo.title || serverInfo.name || undefined, websiteUrl: serverInfo.websiteUrl } : undefined, lastUsed: Date.now(), connectTime: Date.now() - connectStart, executionCount: 1 }; // Store connection for reuse this.connections.set(mcpName, connection); logger.info(`✅ Connected to ${mcpName} in ${connection.connectTime}ms`); return connection; } catch (error: any) { logger.error(`❌ Failed to connect to ${mcpName}: ${error.message}`); throw error; } } /** * New optimized cache loading with profile hash validation * This is the key optimization - skips re-indexing when profile hasn't changed */ private async loadFromOptimizedCache(profile: Profile): Promise<boolean> { try { // 1. Validate cache integrity first const integrity = await this.cachePatcher.validateAndRepairCache(); if (!integrity.valid) { logger.warn('Cache integrity check failed - rebuilding required'); return false; } // 2. Check if cache is valid using profile hash validation const currentProfileHash = this.cachePatcher.generateProfileHash(profile); const cacheIsValid = await this.cachePatcher.validateCacheWithProfile(currentProfileHash); if (!cacheIsValid) { logger.info('Cache invalid or missing - profile changed'); return false; } // 3. Load tool metadata cache directly const toolMetadataCache = await this.cachePatcher.loadToolMetadataCache(); if (!toolMetadataCache.mcps || Object.keys(toolMetadataCache.mcps).length === 0) { logger.info('Tool metadata cache empty'); return false; } logger.info(`✅ Using valid cache (${Object.keys(toolMetadataCache.mcps).length} MCPs, hash: ${currentProfileHash.substring(0, 8)}...)`); // 4. Load MCPs and tools from cache directly (no re-indexing) this.allTools = []; let loadedMCPCount = 0; let loadedToolCount = 0; for (const [mcpName, mcpData] of Object.entries(toolMetadataCache.mcps)) { try { // Validate MCP data structure if (!mcpData.tools || !Array.isArray(mcpData.tools)) { logger.warn(`Skipping ${mcpName}: invalid tools data in cache`); continue; } // Check if MCP still exists in current profile if (!profile.mcpServers[mcpName]) { logger.debug(`Skipping ${mcpName}: removed from profile`); continue; } this.definitions.set(mcpName, { name: mcpName, config: { name: mcpName, ...profile.mcpServers[mcpName] }, tools: mcpData.tools.map(tool => ({ ...tool, inputSchema: tool.inputSchema || {} })), serverInfo: mcpData.serverInfo || { name: mcpName, version: '1.0.0' } }); // Build allTools array and tool mappings const discoveryTools = []; for (const tool of mcpData.tools) { try { const prefixedToolName = `${mcpName}:${tool.name}`; const prefixedDescription = tool.description.startsWith(`${mcpName}:`) ? tool.description : `${mcpName}: ${tool.description || 'No description available'}`; this.allTools.push({ name: prefixedToolName, description: prefixedDescription, mcpName: mcpName }); // Create tool mappings this.toolToMCP.set(tool.name, mcpName); this.toolToMCP.set(prefixedToolName, mcpName); discoveryTools.push({ id: prefixedToolName, name: tool.name, description: prefixedDescription, mcpServer: mcpName, inputSchema: tool.inputSchema || {} }); loadedToolCount++; } catch (toolError: any) { logger.warn(`Error loading tool ${tool.name} from ${mcpName}: ${toolError.message}`); } } // Use fast indexing (load from embeddings cache, don't regenerate) if (discoveryTools.length > 0) { // Ensure discovery engine is fully initialized before indexing await this.discovery.initialize(); await this.discovery.indexMCPToolsFromCache(mcpName, discoveryTools); loadedMCPCount++; } } catch (mcpError: any) { logger.warn(`Error loading MCP ${mcpName} from cache: ${mcpError.message}`); } } if (loadedMCPCount === 0) { logger.warn('No valid MCPs loaded from cache'); return false; } logger.info(`⚡ Loaded ${loadedToolCount} tools from ${loadedMCPCount} MCPs (optimized cache)`); return true; } catch (error: any) { logger.warn(`Optimized cache load failed: ${error.message}`); return false; } } /** * Legacy cache loading (kept for fallback) */ private async loadFromCache(profile: Profile): Promise<boolean> { try { const cacheDir = getCacheDirectory(); const cachePath = join(cacheDir, `${this.profileName}-tools.json`); if (!existsSync(cachePath)) { return false; } const content = readFileSync(cachePath, 'utf-8'); const cache = JSON.parse(content); // Use cache if less than 24 hours old if (Date.now() - cache.timestamp > 24 * 60 * 60 * 1000) { logger.info('Cache expired, will refresh tools'); return false; } logger.info(`Using cached tools (${Object.keys(cache.mcps).length} MCPs)`) // Load MCPs and tools from cache for (const [mcpName, mcpData] of Object.entries(cache.mcps)) { const data = mcpData as any; this.definitions.set(mcpName, { name: mcpName, config: { name: mcpName, ...profile.mcpServers[mcpName] }, tools: data.tools || [], serverInfo: data.serverInfo }); // Add tools to allTools and create mappings const discoveryTools = []; for (const tool of data.tools || []) { // Handle both old (unprefixed) and new (prefixed) formats in cache const isAlreadyPrefixed = tool.name.startsWith(`${mcpName}:`); const prefixedToolName = isAlreadyPrefixed ? tool.name : `${mcpName}:${tool.name}`; const actualToolName = isAlreadyPrefixed ? tool.name.substring(mcpName.length + 1) : tool.name; // Ensure description is prefixed const hasPrefixedDesc = tool.description?.startsWith(`${mcpName}: `); const prefixedDescription = hasPrefixedDesc ? tool.description : `${mcpName}: ${tool.description || 'No description available'}`; this.allTools.push({ name: prefixedToolName, description: prefixedDescription, mcpName: mcpName }); // Map both formats for backward compatibility this.toolToMCP.set(actualToolName, mcpName); this.toolToMCP.set(prefixedToolName, mcpName); // Prepare for discovery engine indexing // Pass unprefixed name - RAG engine will add the prefix discoveryTools.push({ id: prefixedToolName, name: actualToolName, // Use unprefixed name here description: prefixedDescription, mcpServer: mcpName, inputSchema: {} }); } // Index tools with discovery engine await this.discovery.indexMCPTools(mcpName, discoveryTools); } logger.info(`✅ Loaded ${this.allTools.length} tools from cache`); return true; } catch (error: any) { logger.warn(`Cache load failed: ${error.message}`); return false; } } private async saveToCache(profile: Profile): Promise<void> { try { // Use new optimized cache saving with profile hash await this.saveToOptimizedCache(profile); } catch (error: any) { logger.warn(`Cache save failed: ${error.message}`); } } /** * New optimized cache saving with profile hash and structured format */ private async saveToOptimizedCache(profile: Profile): Promise<void> { try { logger.info('💾 Saving tools to optimized cache...'); // Save all MCP definitions to tool metadata cache for (const [mcpName, definition] of this.definitions.entries()) { const mcpConfig = profile.mcpServers[mcpName]; if (mcpConfig) { await this.cachePatcher.patchAddMCP( mcpName, mcpConfig, definition.tools, definition.serverInfo ); } } // Update profile hash const profileHash = this.cachePatcher.generateProfileHash(profile); await this.cachePatcher.updateProfileHash(profileHash); logger.info(`💾 Saved ${this.allTools.length} tools to optimized cache with profile hash: ${profileHash.substring(0, 8)}...`); } catch (error: any) { logger.error(`Optimized cache save failed: ${error.message}`); throw error; } } private getToolSchema(mcpName: string, toolName: string): any { const connection = this.connections.get(mcpName); if (!connection) { // No persistent connection, try to get schema from definitions const definition = this.definitions.get(mcpName); if (!definition) return undefined; const tool = definition.tools.find(t => t.name === toolName); return tool ? (tool as any).inputSchema : undefined; } const tool = connection.tools.find(t => t.name === toolName); if (!tool) return undefined; return (tool as any).inputSchema; } /** * Check if a tool requires parameters */ toolRequiresParameters(toolIdentifier: string): boolean { const [mcpName, toolName] = toolIdentifier.split(':'); if (!mcpName || !toolName) return false; const schema = this.getToolSchema(mcpName, toolName); return ToolSchemaParser.hasRequiredParameters(schema); } /** * Get tool parameters for interactive prompting */ getToolParameters(toolIdentifier: string): ParameterInfo[] { const [mcpName, toolName] = toolIdentifier.split(':'); if (!mcpName || !toolName) return []; const schema = this.getToolSchema(mcpName, toolName); return ToolSchemaParser.parseParameters(schema); } /** * Validate tool parameters before execution */ private validateToolParameters(mcpName: string, toolName: string, parameters: any): string | null { const schema = this.getToolSchema(mcpName, toolName); if (!schema) { // No schema available, allow execution (tool may not require validation) return null; } const requiredParams = ToolSchemaParser.getRequiredParameters(schema); const missingParams: string[] = []; // Check for missing required parameters for (const param of requiredParams) { if (parameters === null || parameters === undefined || !(param.name in parameters) || parameters[param.name] === null || parameters[param.name] === undefined || parameters[param.name] === '') { missingParams.push(param.name); } } if (missingParams.length > 0) { return `Missing required parameters: ${missingParams.join(', ')}. Use 'ncp find "${mcpName}:${toolName}" --depth 2' to see parameter details.`; } return null; // Validation passed } /** * Get tool context for parameter prediction */ getToolContext(toolIdentifier: string): string { return ToolContextResolver.getContext(toolIdentifier); } /** * Find similar tool names using fuzzy matching */ private findSimilarTools(targetTool: string, maxSuggestions: number = 3): string[] { const allTools = Array.from(this.toolToMCP.keys()); const similarities = allTools.map(tool => ({ tool, similarity: calculateSimilarity(targetTool, tool) })); return similarities .filter(item => item.similarity > 0.4) // Only suggest if reasonably similar .sort((a, b) => b.similarity - a.similarity) .slice(0, maxSuggestions) .map(item => item.tool); } /** * Generate hash for each MCP configuration */ private generateConfigHashes(profile: Profile): Record<string, string> { const hashes: Record<string, string> = {}; const crypto = require('crypto'); for (const [mcpName, config] of Object.entries(profile.mcpServers)) { // Hash command + args + env + url for change detection const configString = JSON.stringify({ command: config.command, args: config.args || [], env: config.env || {}, url: config.url // Include HTTP/SSE URL in hash }); hashes[mcpName] = crypto.createHash('sha256').update(configString).digest('hex'); } return hashes; } /** * Get current indexing progress */ getIndexingProgress(): { current: number; total: number; currentMCP: string; estimatedTimeRemaining?: number } | null { return this.indexingProgress; } /** * Get MCP health status summary */ getMCPHealthStatus(): { total: number; healthy: number; unhealthy: number; mcps: Array<{name: string; healthy: boolean}> } { const allMCPs = Array.from(this.definitions.keys()); const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs); const mcpStatus = allMCPs.map(mcp => ({ name: mcp, healthy: healthyMCPs.includes(mcp) })); return { total: allMCPs.length, healthy: healthyMCPs.length, unhealthy: allMCPs.length - healthyMCPs.length, mcps: mcpStatus }; } /** * Enhance generic error messages with better context */ private enhanceErrorMessage(error: any, toolName: string, mcpName: string): string { const errorMessage = error.message || error.toString() || 'Unknown error'; // Always provide context and actionable guidance, regardless of specific error patterns let enhancedMessage = `Tool '${toolName}' failed in MCP '${mcpName}': ${errorMessage}`; // Add generic troubleshooting guidance const troubleshootingTips = [ `• Check MCP '${mcpName}' status and configuration`, `• Use 'ncp find "${mcpName}:${toolName}" --depth 2' to verify tool parameters`, `• Ensure MCP server is running and accessible` ]; enhancedMessage += `\n\nTroubleshooting:\n${troubleshootingTips.join('\n')}`; return enhancedMessage; } /** * Get all resources from active MCPs */ async getAllResources(): Promise<Array<any>> { const resources: Array<any> = []; const allMCPs = Array.from(this.definitions.keys()); const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs); for (const mcpName of healthyMCPs) { try { const mcpResources = await this.getResourcesFromMCP(mcpName); if (mcpResources && Array.isArray(mcpResources)) { // Add MCP source information to each resource with prefix const enrichedResources = mcpResources.map(resource => ({ ...resource, name: `${mcpName}:${resource.name}`, // Add MCP prefix _source: mcpName })); resources.push(...enrichedResources); } } catch (error) { logger.warn(`Failed to get resources from ${mcpName}: ${error}`); } } return resources; } /** * Get all prompts from active MCPs */ async getAllPrompts(): Promise<Array<any>> { const prompts: Array<any> = []; const allMCPs = Array.from(this.definitions.keys()); const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs); for (const mcpName of healthyMCPs) { try { const mcpPrompts = await this.getPromptsFromMCP(mcpName); if (mcpPrompts && Array.isArray(mcpPrompts)) { // Add MCP source information to each prompt with prefix const enrichedPrompts = mcpPrompts.map(prompt => ({ ...prompt, name: `${mcpName}:${prompt.name}`, // Add MCP prefix _source: mcpName })); prompts.push(...enrichedPrompts); } } catch (error) { logger.warn(`Failed to get prompts from ${mcpName}: ${error}`); } } return prompts; } /** * Get resources from a specific MCP */ private async getResourcesFromMCP(mcpName: string): Promise<Array<any>> { try { const definition = this.definitions.get(mcpName); if (!definition) { return []; } // Create temporary connection for resources request const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); const silentEnv = { ...process.env, ...(definition.config.env || {}), MCP_SILENT: 'true', QUIET: 'true', NO_COLOR: 'true' }; const transport = await this.createTransport(definition.config, silentEnv); const client = new Client( { name: 'ncp-oss-resources', version: '1.0.0' }, { capabilities: {} } ); // Connect with timeout and filtered output await withFilteredOutput(async () => { await Promise.race([ client.connect(transport), new Promise((_, reject) => setTimeout(() => reject(new Error('Resources connection timeout')), this.QUICK_PROBE_TIMEOUT) ) ]); }); // Get resources list with filtered output const response = await withFilteredOutput(async () => { return await client.listResources(); }); await client.close(); return response.resources || []; } catch (error) { logger.debug(`Resources probe failed for ${mcpName}: ${error}`); return []; } } /** * Get prompts from a specific MCP */ private async getPromptsFromMCP(mcpName: string): Promise<Array<any>> { try { const definition = this.definitions.get(mcpName); if (!definition) { return []; } // Create temporary connection for prompts request const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); const silentEnv = { ...process.env, ...(definition.config.env || {}), MCP_SILENT: 'true', QUIET: 'true', NO_COLOR: 'true' }; const transport = await this.createTransport(definition.config, silentEnv); const client = new Client( { name: 'ncp-oss-prompts', version: '1.0.0' }, { capabilities: {} } ); // Connect with timeout and filtered output await withFilteredOutput(async () => { await Promise.race([ client.connect(transport), new Promise((_, reject) => setTimeout(() => reject(new Error('Prompts connection timeout')), this.QUICK_PROBE_TIMEOUT) ) ]); }); // Get prompts list with filtered output const response = await withFilteredOutput(async () => { return await client.listPrompts(); }); await client.close(); return response.prompts || []; } catch (error) { logger.debug(`Prompts probe failed for ${mcpName}: ${error}`); return []; } } /** * Clean up idle connections (like commercial version) */ private async cleanupIdleConnections(): Promise<void> { const now = Date.now(); const toDisconnect: string[] = []; for (const [name, connection] of this.connections) { const idleTime = now - connection.lastUsed; if (idleTime > this.IDLE_TIMEOUT) { logger.info(`🧹 Disconnecting idle MCP: ${name} (idle for ${Math.round(idleTime / 1000)}s)`); toDisconnect.push(name); } } // Disconnect idle connections for (const name of toDisconnect) { await this.disconnectMCP(name); } } /** * Disconnect a specific MCP */ private async disconnectMCP(mcpName: string): Promise<void> { const connection = this.connections.get(mcpName); if (!connection) return; try { await connection.client.close(); this.connections.delete(mcpName); logger.debug(`Disconnected ${mcpName}`); } catch (error) { logger.error(`Error disconnecting ${mcpName}:`, error); } } async cleanup(): Promise<void> { logger.info('Shutting down NCP Orchestrator...'); // Stop cleanup timer if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } // Finalize cache if it's being written if (this.csvCache) { try { await this.csvCache.finalize(); } catch (error) { // Ignore finalize errors } } // Stop progress spinner if active if (this.showProgress) { const { spinner } = await import('../utils/progress-spinner.js'); spinner.stop(); } // Close any active connections for (const connection of this.connections.values()) { try { await connection.client.close(); } catch (error) { // Ignore cleanup errors } } this.connections.clear(); logger.info('NCP orchestrator cleanup completed'); } /** * Get server descriptions for all configured MCPs */ getServerDescriptions(): Record<string, string> { const descriptions: Record<string, string> = {}; // From active connections for (const [mcpName, connection] of this.connections) { if (connection.serverInfo?.description) { descriptions[mcpName] = connection.serverInfo.description; } else if (connection.serverInfo?.title) { descriptions[mcpName] = connection.serverInfo.title; } } // From cached definitions for (const [mcpName, definition] of this.definitions) { if (!descriptions[mcpName] && definition.serverInfo?.description) { descriptions[mcpName] = definition.serverInfo.description; } else if (!descriptions[mcpName] && definition.serverInfo?.title) { descriptions[mcpName] = definition.serverInfo.title; } } return descriptions; } /** * Apply universal term frequency scoring boost with action word weighting * Uses SearchEnhancer for clean, extensible term classification and semantic mapping */ private adjustScoresUniversally(query: string, results: any[]): any[] { const queryTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 2); // Skip very short terms return results.map(result => { const toolName = result.name.toLowerCase(); const toolDescription = (result.description || '').toLowerCase(); let nameBoost = 0; let descBoost = 0; // Process each query term with SearchEnhancer classification for (const term of queryTerms) { const termType = SearchEnhancer.classifyTerm(term); const weight = SearchEnhancer.getTypeWeights(termType); // Apply scoring based on term type if (toolName.includes(term)) { nameBoost += weight.name; } if (toolDescription.includes(term)) { descBoost += weight.desc; } // Apply semantic action matching for ACTION terms if (termType === 'ACTION') { const semantics = SearchEnhancer.getActionSemantics(term); for (const semanticMatch of semantics) { if (toolName.includes(semanticMatch)) { nameBoost += weight.name * 1.2; // 120% of full action weight for semantic matches (boosted) } if (toolDescription.includes(semanticMatch)) { descBoost += weight.desc * 1.2; } } // Apply intent penalties for conflicting actions const penalty = SearchEnhancer.getIntentPenalty(term, toolName); nameBoost -= penalty; } } // Apply diminishing returns to prevent excessive stacking const baseWeight = 0.15; // Base weight for diminishing returns calculation const finalNameBoost = nameBoost > 0 ? nameBoost * Math.pow(0.8, Math.max(0, nameBoost / baseWeight - 1)) : 0; const finalDescBoost = descBoost > 0 ? descBoost * Math.pow(0.8, Math.max(0, descBoost / (baseWeight / 2) - 1)) : 0; const totalBoost = 1 + finalNameBoost + finalDescBoost; return { ...result, confidence: result.confidence * totalBoost }; }); } /** * Trigger auto-import from MCP client * Called by MCPServer after it receives clientInfo from initialize request */ async triggerAutoImport(clientName: string): Promise<void> { if (!this.profileManager) { // ProfileManager not initialized yet, skip auto-import logger.warn('ProfileManager not initialized, skipping auto-import'); return; } try { await this.profileManager.tryAutoImportFromClient(clientName); } catch (error: any) { logger.error(`Auto-import failed: ${error.message}`); } } /** * Add internal MCPs to tool discovery * Called after external MCPs are indexed */ private addInternalMCPsToDiscovery(): void { const internalMCPs = this.internalMCPManager.getAllInternalMCPs(); for (const mcp of internalMCPs) { // Add to definitions (for consistency with external MCPs) this.definitions.set(mcp.name, { name: mcp.name, config: { name: mcp.name, command: 'internal', args: [] }, tools: mcp.tools.map(t => ({ name: t.name, description: t.description })), serverInfo: { name: mcp.name, version: '1.0.0', description: mcp.description } }); // Add tools to allTools and discovery for (const tool of mcp.tools) { const toolId = `${mcp.name}:${tool.name}`; // Add to allTools this.allTools.push({ name: tool.name, description: tool.description, mcpName: mcp.name }); // Add to toolToMCP mapping this.toolToMCP.set(toolId, mcp.name); } // Index in discovery engine const discoveryTools = mcp.tools.map(t => ({ id: `${mcp.name}:${t.name}`, name: t.name, description: t.description })); this.discovery.indexMCPTools(mcp.name, discoveryTools); logger.info(`Added internal MCP "${mcp.name}" with ${mcp.tools.length} tools`); } } /** * Get the ProfileManager instance * Used by MCP server for management operations (add/remove MCPs) */ getProfileManager(): ProfileManager | null { return this.profileManager; } /** * Hash a string for change detection */ private hashString(str: string): string { return createHash('sha256').update(str).digest('hex'); } } export default NCPOrchestrator; ``` -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { ProfileManager } from '../profiles/profile-manager.js'; import { MCPServer } from '../server/mcp-server.js'; import { ConfigManager } from '../utils/config-manager.js'; import { formatCommandDisplay } from '../utils/security.js'; import { TextUtils } from '../utils/text-utils.js'; import { OutputFormatter } from '../services/output-formatter.js'; import { ErrorHandler } from '../services/error-handler.js'; import { CachePatcher } from '../cache/cache-patcher.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { mcpWrapper } from '../utils/mcp-wrapper.js'; import { withFilteredOutput } from '../transports/filtered-stdio-transport.js'; import { UpdateChecker } from '../utils/update-checker.js'; import { setOverrideWorkingDirectory } from '../utils/ncp-paths.js'; import { ConfigSchemaReader } from '../services/config-schema-reader.js'; import { ConfigPrompter } from '../services/config-prompter.js'; import { SchemaCache } from '../cache/schema-cache.js'; import { getCacheDirectory } from '../utils/ncp-paths.js'; // Check for no-color flag early const noColor = process.argv.includes('--no-color') || process.env.NO_COLOR === 'true'; if (noColor) { chalk.level = 0; // Disable colors globally } else { // Ensure colors are enabled for TTY and when FORCE_COLOR is set if (process.env.FORCE_COLOR || process.stdout?.isTTY) { chalk.level = 3; // Full color support } } // Fuzzy matching helper for finding similar names function findSimilarNames(target: string, availableNames: string[], maxSuggestions = 3): string[] { const targetLower = target.toLowerCase(); // Score each name based on similarity const scored = availableNames.map(name => { const nameLower = name.toLowerCase(); let score = 0; // Exact match gets highest score if (nameLower === targetLower) score += 100; // Contains target or target contains name if (nameLower.includes(targetLower)) score += 50; if (targetLower.includes(nameLower)) score += 50; // First few characters match const minLen = Math.min(targetLower.length, nameLower.length); for (let i = 0; i < minLen && i < 3; i++) { if (targetLower[i] === nameLower[i]) score += 10; } // Similar length bonus const lengthDiff = Math.abs(targetLower.length - nameLower.length); if (lengthDiff <= 2) score += 5; return { name, score }; }); // Filter out low scores and sort by score return scored .filter(item => item.score > 0) .sort((a, b) => b.score - a.score) .slice(0, maxSuggestions) .map(item => item.name); } // Enhanced remove validation helper async function validateRemoveCommand(name: string, manager: ProfileManager, profiles: string[]): Promise<{ mcpExists: boolean; suggestions: string[]; allMCPs: string[]; }> { const allMCPs = new Set<string>(); // Collect all MCP names from specified profiles for (const profileName of profiles) { const profile = await manager.getProfile(profileName); if (profile?.mcpServers) { Object.keys(profile.mcpServers).forEach(mcpName => allMCPs.add(mcpName)); } } const mcpList = Array.from(allMCPs); const mcpExists = mcpList.includes(name); let suggestions: string[] = []; if (!mcpExists && mcpList.length > 0) { suggestions = findSimilarNames(name, mcpList); } return { mcpExists, suggestions, allMCPs: mcpList }; } // Simple validation helper for ADD command async function validateAddCommand(name: string, command: string, args: any[]): Promise<{ message: string; suggestions: Array<{ command: string; description: string }> }> { const suggestions: Array<{ command: string; description: string }> = []; const fullCommand = `${command} ${args.join(' ')}`.trim(); // Basic command format validation and helpful tips if (command === 'npx' && args.length > 0) { // Clean up the command format - avoid duplication const cleanedArgs = args.filter(arg => arg !== '-y' || args.indexOf(arg) === 0); suggestions.push({ command: fullCommand, description: 'NPM package execution - health monitor will validate if package exists and starts correctly' }); } else if (command.startsWith('/') || command.startsWith('./') || command.includes('\\')) { suggestions.push({ command: fullCommand, description: 'Local executable - health monitor will validate if command works' }); } else if (command.includes('@') && !command.startsWith('npx')) { suggestions.push({ command: `npx -y ${fullCommand}`, description: 'Consider using npx for npm packages' }); } else { // Show the command as provided suggestions.push({ command: fullCommand, description: 'Custom command - health monitor will validate functionality' }); } return { message: chalk.dim('💡 MCP will be validated by health monitor after adding'), suggestions }; } // Simple emoji support detection for cross-platform compatibility const supportsEmoji = () => { // Windows Command Prompt and PowerShell often don't support emojis well if (process.platform === 'win32') { // Check if it's Windows Terminal (supports emojis) vs cmd/powershell return process.env.WT_SESSION || process.env.TERM_PROGRAM === 'vscode'; } // macOS and Linux terminals generally support emojis return true; }; const getIcon = (emoji: string, fallback: string) => supportsEmoji() ? emoji : fallback; // Configure OutputFormatter OutputFormatter.configure({ noColor: !!noColor, emoji: !!supportsEmoji() }); // Use centralized version utility import { version } from '../utils/version.js'; // Discovery function for single MCP - extracted from NCPOrchestrator.probeMCPTools async function discoverSingleMCP(name: string, command: string, args: string[] = [], env: Record<string, string> = {}): Promise<{ tools: Array<{name: string; description: string; inputSchema?: any}>; serverInfo?: { name: string; title?: string; version: string; description?: string; websiteUrl?: string; }; configurationSchema?: any; }> { const config = { name, command, args, env }; if (!config.command) { throw new Error(`Invalid config for ${config.name}`); } let client: Client | null = null; let transport: StdioClientTransport | null = null; const DISCOVERY_TIMEOUT = 8000; // 8 seconds try { // Create wrapper command for discovery phase const wrappedCommand = mcpWrapper.createWrapper( config.name, config.command, config.args || [] ); // Create temporary connection for discovery const silentEnv = { ...process.env, ...(config.env || {}), MCP_SILENT: 'true', QUIET: 'true', NO_COLOR: 'true' }; transport = new StdioClientTransport({ command: wrappedCommand.command, args: wrappedCommand.args, env: silentEnv as Record<string, string> }); client = new Client( { name: 'ncp-oss', version: '1.0.0' }, { capabilities: {} } ); // Connect with timeout and filtered output await withFilteredOutput(async () => { await Promise.race([ client!.connect(transport!), new Promise((_, reject) => setTimeout(() => reject(new Error('Discovery timeout')), DISCOVERY_TIMEOUT) ) ]); }); // Capture server info after connection const serverInfo = client!.getServerVersion(); // Capture configuration schema if available // TODO: Once MCP SDK is updated to support top-level configurationSchema, // also check for it directly. For now, check experimental capabilities. const serverCapabilities = client!.getServerCapabilities(); const configurationSchema = (serverCapabilities as any)?.experimental?.configurationSchema; // Get tool list with filtered output const response = await withFilteredOutput(async () => { return await client!.listTools(); }); const tools = response.tools.map(t => ({ name: t.name, description: t.description || '', inputSchema: t.inputSchema || {} })); // Disconnect immediately await client.close(); return { tools, serverInfo: serverInfo ? { name: serverInfo.name || config.name, title: serverInfo.title, version: serverInfo.version || 'unknown', description: serverInfo.title || serverInfo.name || undefined, websiteUrl: serverInfo.websiteUrl } : undefined, configurationSchema }; } catch (error: any) { // Clean up connections try { if (client) { await client.close(); } } catch (closeError) { // Ignore close errors } throw new Error(`Failed to discover tools from ${config.name}: ${error.message}`); } } const program = new Command(); // Set version program.version(version, '-v, --version', 'output the current version'); // Custom help configuration with colors and enhanced content program .name('ncp') .description(` ${chalk.bold.white('Natural Context Provider')} ${chalk.dim('v' + version)} - ${chalk.cyan('1 MCP to rule them all')} ${chalk.dim('Orchestrates multiple MCP servers through a unified interface for AI assistants.')} ${chalk.dim('Reduces cognitive load and clutter, saving tokens and speeding up AI interactions.')} ${chalk.dim('Enables smart tool discovery across all configured servers with vector similarity search.')}`) .option('--profile <name>', 'Profile to use (default: all)') .option('--working-dir <path>', 'Working directory for profile resolution (overrides current directory)') .option('--force-retry', 'Force retry all failed MCPs immediately (ignores scheduled retry times)') .option('--no-color', 'Disable colored output'); // Configure help with enhanced formatting, Quick Start, and examples program.configureHelp({ sortSubcommands: true, formatHelp: (cmd, helper) => { // Calculate proper padding based on actual command names and options separately const allCommands = cmd.commands.filter((cmd: any) => !cmd.hidden); const maxCmdLength = allCommands.length > 0 ? Math.max(...allCommands.map(cmd => cmd.name().length)) : 0; const maxOptionLength = cmd.options.length > 0 ? Math.max(...cmd.options.map(option => option.flags.length)) : 0; const cmdPad = maxCmdLength + 4; // Add extra space for command alignment const optionPad = maxOptionLength + 4; // Add extra space for option alignment const helpWidth = helper.helpWidth || 80; function formatItem(term: string, description?: string, padding?: number): string { if (description) { const pad = padding || cmdPad; return term.padEnd(pad) + description; } return term; } // Add description first let output = cmd.description() + '\n\n'; // Then usage and config info output += `${chalk.bold.white('Usage:')} ${cmd.name()} [options] [command]\n`; output += `${chalk.yellow('NCP config files:')} ~/.ncp/profiles/\n\n`; // Options if (cmd.options.length) { output += chalk.bold.white('Options:') + '\n'; cmd.options.forEach(option => { // Calculate padding based on raw flags, not styled version const rawPadding = ' ' + option.flags; const paddedRaw = rawPadding.padEnd(optionPad + 2); const styledFlags = chalk.cyan(option.flags); const description = chalk.white(option.description); output += ' ' + styledFlags + ' '.repeat(paddedRaw.length - rawPadding.length) + description + '\n'; }); output += '\n'; } // Commands const commands = cmd.commands.filter((cmd: any) => !cmd.hidden); if (commands.length) { output += chalk.bold.white('Commands:') + '\n'; commands.sort((a, b) => a.name().localeCompare(b.name())); commands.forEach(cmd => { // Group commands by category with enhanced styling const managementCommands = ['add', 'remove', 'import', 'list', 'config']; const discoveryCommands = ['find']; const executionCommands = ['run']; let cmdName = cmd.name(); let styledCmdName = cmdName; if (managementCommands.includes(cmd.name())) { styledCmdName = chalk.cyan(cmd.name()); } else if (discoveryCommands.includes(cmd.name())) { styledCmdName = chalk.green.bold(cmd.name()); } else if (executionCommands.includes(cmd.name())) { styledCmdName = chalk.yellow.bold(cmd.name()); } // Calculate padding based on raw command name, not styled version const rawPadding = ' ' + cmdName; const paddedRaw = rawPadding.padEnd(cmdPad + 2); // Use cmdPad + 2 for consistency const description = chalk.white(cmd.description()); output += ' ' + styledCmdName + ' '.repeat(paddedRaw.length - rawPadding.length) + description + '\n'; }); } return output; } }); // Add help command program .command('help [command]') .description('Show help for NCP or a specific command') .action((command) => { if (command) { const cmd = program.commands.find(cmd => cmd.name() === command); if (cmd) { cmd.help(); } else { console.log(`Unknown command: ${command}`); program.help(); } } else { program.help(); } }); // Add Quick Start and Examples after all commands are defined program.addHelpText('after', ` ${chalk.bold.white('Quick Start:')} ${chalk.cyan('1a')} Import existing MCPs: ${chalk.green('ncp config import')} ${chalk.dim('(copy JSON first)')} ${chalk.cyan('1b')} Or add manually: ${chalk.green('ncp add <name> <command>')} ${chalk.cyan('2')} Configure NCP in AI client settings ${chalk.bold.white('Examples:')} $ ${chalk.yellow('ncp config import config.json')} ${chalk.dim(' # Import from file')} $ ${chalk.yellow('ncp add filesystem npx @modelcontextprotocol/server-filesystem /tmp')} $ ${chalk.yellow('ncp find "file operations"')} $ ${chalk.yellow('ncp run filesystem:read_file --params \'{"path": "/tmp/example.txt"}\'')} $ ${chalk.yellow('ncp list --depth 1')}`); // Check if we should run as MCP server // MCP server mode: default when no CLI commands are provided, or when --profile is specified const profileIndex = process.argv.indexOf('--profile'); const hasCommands = process.argv.includes('find') || process.argv.includes('add') || process.argv.includes('list') || process.argv.includes('remove') || process.argv.includes('run') || process.argv.includes('config') || process.argv.includes('help') || process.argv.includes('--help') || process.argv.includes('-h') || process.argv.includes('--version') || process.argv.includes('-v') || process.argv.includes('import') || process.argv.includes('analytics') || process.argv.includes('visual') || process.argv.includes('update') || process.argv.includes('repair'); // Default to MCP server mode when no CLI commands are provided // This ensures compatibility with Claude Desktop and other MCP clients that expect server mode by default const shouldRunAsServer = !hasCommands; if (shouldRunAsServer) { // Handle --working-dir parameter for MCP server mode const workingDirIndex = process.argv.indexOf('--working-dir'); if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) { const workingDirValue = process.argv[workingDirIndex + 1]; setOverrideWorkingDirectory(workingDirValue); } // Running as MCP server: ncp (defaults to 'all' profile) or ncp --profile <name> // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE to 'default' or anything else! const profileName = profileIndex !== -1 ? (process.argv[profileIndex + 1] || 'all') : 'all'; // Debug logging for integration tests if (process.env.NCP_DEBUG === 'true') { console.error(`[DEBUG] profileIndex: ${profileIndex}`); console.error(`[DEBUG] process.argv: ${process.argv.join(' ')}`); console.error(`[DEBUG] Selected profile: ${profileName}`); } const server = new MCPServer(profileName); server.run().catch(console.error); } else { // Handle --working-dir parameter for CLI mode const workingDirIndex = process.argv.indexOf('--working-dir'); if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) { const workingDirValue = process.argv[workingDirIndex + 1]; setOverrideWorkingDirectory(workingDirValue); } // Running as CLI tool // Add MCP command program .command('add <name> <command> [args...]') .description('Add an MCP server to a profile') .option('--profile <names...>', 'Profile(s) to add to (can specify multiple, default: all)') .option('--env <vars...>', 'Environment variables (KEY=value)') .action(async (name, command, args, options) => { console.log(`\n${chalk.blue(`📦 Adding MCP server: ${chalk.bold(name)}`)}`); const manager = new ProfileManager(); await manager.initialize(); // Show helpful guidance without hard validation const guidance = await validateAddCommand(name, command, args); console.log(guidance.message); if (guidance.suggestions.length > 0) { console.log('\n📋 Command validation:'); guidance.suggestions.forEach((suggestion, index) => { if (index === 0) { // Main command console.log(` ${chalk.cyan(suggestion.command)}`); console.log(` ${chalk.dim(suggestion.description)}`); } else { // Alternative suggestions console.log(chalk.dim(`\n💡 Alternative: ${suggestion.command}`)); console.log(chalk.dim(` ${suggestion.description}`)); } }); console.log(''); } // Parse environment variables const env: Record<string, string> = {}; if (options.env) { console.log(chalk.dim('🔧 Processing environment variables...')); for (const envVar of options.env) { const [key, value] = envVar.split('='); if (key && value) { env[key] = value; console.log(chalk.dim(` ${key}=${formatCommandDisplay(value)}`)); } else { console.log(chalk.yellow(`⚠️ Invalid environment variable format: ${envVar}`)); } } } const config = { command, args: args || [], ...(Object.keys(env).length > 0 && { env }) }; // Show what will be added // Determine which profiles to add to // ⚠️ CRITICAL: Default MUST be ['all'] - DO NOT CHANGE! const profiles = options.profile || ['all']; console.log('\n📋 Profile configuration:'); console.log(` ${chalk.cyan('Target profiles:')} ${profiles.join(', ')}`); if (Object.keys(env).length > 0) { console.log(` ${chalk.cyan('Environment variables:')} ${Object.keys(env).length} configured`); Object.entries(env).forEach(([key, value]) => { console.log(chalk.dim(` ${key}=${formatCommandDisplay(value)}`)); }); } console.log(''); // spacing // Initialize schema services const schemaReader = new ConfigSchemaReader(); const configPrompter = new ConfigPrompter(); const schemaCache = new SchemaCache(getCacheDirectory()); // Try to discover and detect configuration requirements BEFORE adding to profile console.log(chalk.dim('🔍 Discovering tools and configuration requirements...')); const discoveryStart = Date.now(); let discoveryResult: Awaited<ReturnType<typeof discoverSingleMCP>> | null = null; let finalConfig = { ...config }; let detectedSchema: any = null; try { discoveryResult = await discoverSingleMCP(name, command, args, env); const discoveryTime = Date.now() - discoveryStart; console.log(`${chalk.green('✅')} Found ${discoveryResult.tools.length} tools in ${discoveryTime}ms`); // Two-tier configuration detection strategy: // Tier 1: MCP Protocol configurationSchema (from server capabilities) // Tier 2: Error parsing (fallback - happens on failure below) // Tier 1: Check for MCP protocol schema if (discoveryResult.configurationSchema) { detectedSchema = schemaReader.readSchema({ protocolVersion: '1.0', capabilities: {}, serverInfo: { name, version: '1.0' }, configurationSchema: discoveryResult.configurationSchema }); if (detectedSchema) { console.log(chalk.dim(' Configuration schema detected (MCP protocol)')); } } // Apply detected schema if we have one with required config if (detectedSchema && schemaReader.hasRequiredConfig(detectedSchema)) { console.log(chalk.cyan('\n📋 Configuration required')); // Prompt for configuration const promptedConfig = await configPrompter.promptForConfig(detectedSchema, name); // Merge prompted config with existing config finalConfig = { command: config.command, args: [...(config.args || []), ...(promptedConfig.arguments || [])], env: { ...(config.env || {}), ...(promptedConfig.environmentVariables || {}) } }; // Display summary configPrompter.displaySummary(promptedConfig, name); // Cache schema for future use schemaCache.save(name, detectedSchema); console.log(chalk.dim('✓ Configuration schema cached')); } } catch (discoveryError: any) { console.log(`${chalk.yellow('⚠️')} Discovery failed: ${discoveryError.message}`); console.log(chalk.dim(' Proceeding with manual configuration...')); // Tier 3: Error parsing would happen here in future enhancement // Continue with manual config - error will be saved to profile } // Initialize cache patcher const cachePatcher = new CachePatcher(); for (const profileName of profiles) { try { // 1. Update profile with final configuration await manager.addMCPToProfile(profileName, name, finalConfig); console.log(`\n${OutputFormatter.success(`Added ${name} to profile: ${profileName}`)}`); // 2. Update cache if we have discovery results if (discoveryResult) { if (discoveryResult.tools.length > 0) { console.log(chalk.dim(' Tools discovered:')); // Show first few tools const toolsToShow = discoveryResult.tools.slice(0, 3); toolsToShow.forEach(tool => { const shortDesc = tool.description?.length > 50 ? tool.description.substring(0, 50) + '...' : tool.description; console.log(chalk.dim(` • ${tool.name}: ${shortDesc}`)); }); if (discoveryResult.tools.length > 3) { console.log(chalk.dim(` • ... and ${discoveryResult.tools.length - 3} more`)); } } // 3. Patch tool metadata cache with final config await cachePatcher.patchAddMCP(name, finalConfig, discoveryResult.tools, discoveryResult.serverInfo); // 4. Update profile hash const profile = await manager.getProfile(profileName); const profileHash = cachePatcher.generateProfileHash(profile); await cachePatcher.updateProfileHash(profileHash); console.log(`${chalk.green('✅')} Cache updated for ${name}`); } else { console.log(chalk.dim(' Profile updated, but cache not built. Run "ncp find <query>" to build cache later.')); } } catch (error: any) { const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('profile', 'add', `${name} to ${profileName}`)); console.log('\n' + ErrorHandler.formatForConsole(errorResult)); } } console.log(chalk.dim('\n💡 Next steps:')); console.log(chalk.dim(' •') + ' View profiles: ' + chalk.cyan('ncp list')); console.log(chalk.dim(' •') + ' Test discovery: ' + chalk.cyan('ncp find <query>')); }); // Lightweight function to read MCP info from cache without full orchestrator initialization async function loadMCPInfoFromCache(mcpDescriptions: Record<string, string>, mcpToolCounts: Record<string, number>, mcpVersions: Record<string, string>): Promise<boolean> { const { readFileSync, existsSync } = await import('fs'); const { getCacheDirectory } = await import('../utils/ncp-paths.js'); const { join } = await import('path'); const cacheDir = getCacheDirectory(); const cachePath = join(cacheDir, 'all-tools.json'); if (!existsSync(cachePath)) { return false; // No cache available } try { const cacheContent = readFileSync(cachePath, 'utf-8'); const cache = JSON.parse(cacheContent); // Extract server info and tool counts from cache for (const [mcpName, mcpData] of Object.entries(cache.mcps || {})) { const data = mcpData as any; // Extract server description (without version) if (data.serverInfo?.description && data.serverInfo.description !== mcpName) { mcpDescriptions[mcpName] = data.serverInfo.description; } else if (data.serverInfo?.title) { mcpDescriptions[mcpName] = data.serverInfo.title; } // Extract version separately if (data.serverInfo?.version && data.serverInfo.version !== 'unknown') { mcpVersions[mcpName] = data.serverInfo.version; } // Count tools if (data.tools && Array.isArray(data.tools)) { mcpToolCounts[mcpName] = data.tools.length; } } return true; // Cache was successfully loaded } catch (error) { // Ignore cache reading errors - will just show without descriptions return false; } } // List command program .command('list [filter]') .description('List all profiles and their MCPs with intelligent filtering') .option('--limit <number>', 'Maximum number of items to show (default: 20)') .option('--page <number>', 'Page number for pagination (default: 1)') .option('--depth <number>', 'Display depth: 0=profiles only, 1=profiles+MCPs+description, 2=profiles+MCPs+description+tools (default: 2)') .option('--search <query>', 'Search in MCP names and descriptions') .option('--profile <name>', 'Show only specific profile') .option('--sort <field>', 'Sort by: name, tools, profiles (default: name)', 'name') .option('--non-empty', 'Show only profiles with configured MCPs') .action(async (filter, options) => { const limit = parseInt(options.limit || '20'); const page = parseInt(options.page || '1'); const depth = parseInt(options.depth || '2'); const manager = new ProfileManager(); await manager.initialize(); let profiles = manager.listProfiles(); if (profiles.length === 0) { console.log(chalk.yellow('📋 No profiles configured')); console.log(chalk.dim('💡 Use: ncp add <name> <command> to add an MCP server')); return; } // Apply profile filtering first if (options.profile) { const targetProfile = options.profile.toLowerCase(); profiles = profiles.filter(p => p.toLowerCase() === targetProfile); if (profiles.length === 0) { console.log(chalk.yellow(`⚠️ Profile "${options.profile}" not found`)); // Suggest similar profiles const allProfiles = manager.listProfiles(); const suggestions = findSimilarNames(options.profile, allProfiles); if (suggestions.length > 0) { console.log(chalk.yellow('\n💡 Did you mean:')); suggestions.forEach((suggestion, index) => { console.log(` ${index + 1}. ${chalk.cyan(suggestion)}`); }); } else { console.log(chalk.yellow('\n📋 Available profiles:')); allProfiles.forEach((profile, index) => { console.log(` ${index + 1}. ${chalk.cyan(profile)}`); }); } return; } } // Initialize orchestrator to get MCP descriptions and tool counts if needed let orchestrator; let mcpDescriptions: Record<string, string> = {}; let mcpToolCounts: Record<string, number> = {}; let mcpVersions: Record<string, string> = {}; if (depth >= 1) { try { // Lightweight cache reading - no orchestrator initialization needed const cacheLoaded = await loadMCPInfoFromCache(mcpDescriptions, mcpToolCounts, mcpVersions); if (!cacheLoaded) { // Show helpful message about building cache console.log(chalk.dim('💡 No MCP cache found. Use `ncp find <query>` to discover tools and build cache.')); } } catch (error) { // If cache reading fails, continue without descriptions console.log(chalk.dim('Note: Could not load MCP descriptions and tool counts')); } } // Collect and filter data first const profileData: Array<{ name: string; mcps: Record<string, any>; filteredMcps: Record<string, any>; originalCount: number; filteredCount: number; }> = []; for (const profileName of profiles) { const mcps = await manager.getProfileMCPs(profileName) || {}; let filteredMcps = mcps; // Apply MCP filtering if (filter || options.search) { const query = filter || options.search; const queryLower = query.toLowerCase(); filteredMcps = Object.fromEntries( Object.entries(mcps).filter(([mcpName, config]) => { const description = mcpDescriptions[mcpName] || mcpName; return ( mcpName.toLowerCase().includes(queryLower) || description.toLowerCase().includes(queryLower) ); }) ); } // Apply non-empty filter if (options.nonEmpty && Object.keys(filteredMcps).length === 0) { continue; // Skip empty profiles when --non-empty is used } profileData.push({ name: profileName, mcps, filteredMcps, originalCount: Object.keys(mcps).length, filteredCount: Object.keys(filteredMcps).length }); } // Check if filtering returned no results if (profileData.length === 0) { const queryInfo = filter || options.search; console.log(chalk.yellow(`⚠️ No MCPs found${queryInfo ? ` matching "${queryInfo}"` : ''}`)); // Suggest available MCPs if search was used if (queryInfo) { const allMcps = new Set<string>(); for (const profile of manager.listProfiles()) { const mcps = await manager.getProfileMCPs(profile); if (mcps) { Object.keys(mcps).forEach(mcp => allMcps.add(mcp)); } } if (allMcps.size > 0) { const suggestions = findSimilarNames(queryInfo, Array.from(allMcps)); if (suggestions.length > 0) { console.log(chalk.yellow('\n💡 Did you mean:')); suggestions.forEach((suggestion, index) => { console.log(` ${index + 1}. ${chalk.cyan(suggestion)}`); }); } else { console.log(chalk.yellow('\n📋 Available MCPs:')); Array.from(allMcps).slice(0, 10).forEach((mcp, index) => { console.log(` ${index + 1}. ${chalk.cyan(mcp)}`); }); } } } return; } // Sort profiles if requested if (options.sort !== 'name') { profileData.sort((a, b) => { switch (options.sort) { case 'tools': return b.filteredCount - a.filteredCount; case 'profiles': return a.name.localeCompare(b.name); default: return a.name.localeCompare(b.name); } }); } // Display results console.log(''); console.log(chalk.bold.white('Profiles ▶ MCPs')); if (filter || options.search) { console.log(chalk.dim(`🔍 Filtered by: "${filter || options.search}"`)); } console.log(''); let totalMCPs = 0; for (const data of profileData) { const { name: profileName, filteredMcps, filteredCount } = data; totalMCPs += filteredCount; // Profile header with count const countBadge = filteredCount > 0 ? chalk.green(`${filteredCount} MCPs`) : chalk.dim('empty'); console.log(`📦 ${chalk.bold.white(profileName)}`, chalk.dim(`(${countBadge})`)); // Depth 0: profiles only - skip MCP details if (depth === 0) { // Already showing profile, nothing more needed } else if (filteredMcps && Object.keys(filteredMcps).length > 0) { const mcpEntries = Object.entries(filteredMcps); mcpEntries.forEach(([mcpName, config], index) => { const isLast = index === mcpEntries.length - 1; const connector = isLast ? '└──' : '├──'; const indent = isLast ? ' ' : '│ '; // MCP name with tool count (following profile count style) const toolCount = mcpToolCounts[mcpName]; const versionPart = mcpVersions[mcpName] ? chalk.magenta(`v${mcpVersions[mcpName]}`) : ''; // If not in cache, it means MCP hasn't connected successfully const toolPart = toolCount !== undefined ? chalk.green(`${toolCount} tools`) : chalk.gray('not available'); const badge = versionPart && toolPart ? chalk.dim(` (${versionPart} | ${toolPart})`) : versionPart ? chalk.dim(` (${versionPart})`) : toolPart ? chalk.dim(` (${toolPart})`) : ''; console.log(` ${connector} ${chalk.bold.cyanBright(mcpName)}${badge}`); // Depth 1+: Show description if available and meaningful if (depth >= 1 && mcpDescriptions[mcpName]) { const description = mcpDescriptions[mcpName]; // Skip descriptions that just repeat the MCP name (no value added) if (description.toLowerCase() !== mcpName.toLowerCase()) { console.log(` ${indent} ${chalk.white(description)}`); } } // Depth 2: Show command with reverse colors and text wrapping if (depth >= 2) { const commandText = formatCommandDisplay(config.command, config.args); const maxWidth = process.stdout.columns ? process.stdout.columns - 6 : 80; // Leave space for indentation const wrappedLines = TextUtils.wrapTextWithBackground(commandText, maxWidth, ` ${indent} `, (text: string) => chalk.bgGray.black(text)); console.log(wrappedLines); } }); } else if (depth > 0) { console.log(chalk.dim(' └── (empty)')); } console.log(''); } // No cleanup needed for lightweight approach }); // Helper function to format find command output with consistent color scheme function formatFindOutput(text: string): string { return text // Tool names in headers: # **toolname** -> bold light blue .replace(/^# \*\*([^*]+)\*\*/gm, (match, toolName) => chalk.bold.cyanBright(toolName)) // Parameters: ### param: type (optional) - description .replace(/^### ([^:]+): (.+)$/gm, (match, param, rest) => { // Handle: type (optional) - description const optionalDescMatch = rest.match(/^(.+?)\s+\*\(optional\)\*\s*-\s*(.+)$/); if (optionalDescMatch) { return `${chalk.yellow(param)}: ${chalk.cyan(optionalDescMatch[1])} ${chalk.dim('(optional)')} - ${chalk.white(optionalDescMatch[2])}`; } // Handle: type - description const descMatch = rest.match(/^(.+?)\s*-\s*(.+)$/); if (descMatch) { return `${chalk.yellow(param)}: ${chalk.cyan(descMatch[1])} - ${chalk.white(descMatch[2])}`; } // Handle: type (optional) const optionalMatch = rest.match(/^(.+)\s+\*\(optional\)\*$/); if (optionalMatch) { return `${chalk.yellow(param)}: ${chalk.cyan(optionalMatch[1])} ${chalk.dim('(optional)')}`; } // Handle: type only return `${chalk.yellow(param)}: ${chalk.cyan(rest)}`; }) // Parameter descriptions: #### description -> dim .replace(/^#### (.+)$/gm, (match, desc) => chalk.dim(desc)) // Separators: --- -> dim .replace(/^---$/gm, chalk.dim('---')) // Bold text in general: **text** -> bold for tool names in lists .replace(/\*\*([^*]+)\*\*/g, (match, text) => { // Check if it's a tool name (contains colon) if (text.includes(':')) { return chalk.bold.cyanBright(text); } else { // MCP name or other bold text return chalk.bold(text); } }) // [no parameters] -> dim .replace(/\*\[no parameters\]\*/g, chalk.dim('[no parameters]')) // Italic text: *text* -> dim for tips .replace(/\*([^*\[]+)\*/g, (match, text) => chalk.dim(text)) // Confidence percentages: (XX% match) -> green percentage .replace(/\((\d+)% match\)/g, (match, percentage) => chalk.dim(`(${chalk.green(percentage + '%')} match)`)) // Header search results - make query bold white .replace(/Found tools for "([^"]+)"/g, (match, query) => `Found tools for ${chalk.bold.white(`"${query}"`)}`) // No results message .replace(/❌ No tools found for "([^"]+)"/g, (match, query) => `❌ No tools found for ${chalk.bold.white(`"${query}"`)}`) // Usage tips .replace(/^💡 (.+)$/gm, (match, tip) => `💡 ${chalk.white(tip)}`); } // Remove command program .command('remove <name>') .description('Remove an MCP server from profiles') .option('--profile <names...>', 'Profile(s) to remove from (can specify multiple, default: all)') .action(async (name, options) => { console.log(chalk.blue(`🗑️ Removing MCP server: ${chalk.bold(name)}`)); const manager = new ProfileManager(); await manager.initialize(); // ⚠️ CRITICAL: Default MUST be ['all'] - DO NOT CHANGE! const profiles = options.profile || ['all']; // Validate if MCP exists and get suggestions const validation = await validateRemoveCommand(name, manager, profiles); if (!validation.mcpExists) { console.log(chalk.yellow(`⚠️ MCP "${name}" not found in specified profiles`)); if (validation.suggestions.length > 0) { console.log(chalk.yellow('\n💡 Did you mean:')); validation.suggestions.forEach((suggestion, index) => { console.log(` ${index + 1}. ${chalk.cyan(suggestion)}`); }); console.log(chalk.yellow('\n💡 Use the exact name from the list above')); } else if (validation.allMCPs.length > 0) { console.log(chalk.yellow('\n📋 Available MCPs in these profiles:')); validation.allMCPs.forEach((mcp, index) => { console.log(` ${index + 1}. ${chalk.cyan(mcp)}`); }); } else { console.log(chalk.dim('\n📋 No MCPs found in specified profiles')); console.log(chalk.dim('💡 Use \'ncp list\' to see all configured MCPs')); } console.log(chalk.yellow('\n⚠️ No changes made')); return; } // MCP exists, proceed with removal console.log(chalk.green('✅ MCP found, proceeding with removal...\n')); // Initialize cache patcher const cachePatcher = new CachePatcher(); for (const profileName of profiles) { try { // 1. Remove from profile await manager.removeMCPFromProfile(profileName, name); console.log(OutputFormatter.success(`Removed ${name} from profile: ${profileName}`)); // 2. Clean up caches console.log(chalk.dim('🔧 Cleaning up caches...')); try { // Remove from tool metadata cache await cachePatcher.patchRemoveMCP(name); // Remove from embeddings cache await cachePatcher.patchRemoveEmbeddings(name); // Update profile hash const profile = await manager.getProfile(profileName); if (profile) { const profileHash = cachePatcher.generateProfileHash(profile); await cachePatcher.updateProfileHash(profileHash); } console.log(`${chalk.green('✅')} Cache cleaned for ${name}`); } catch (cacheError: any) { console.log(`${chalk.yellow('⚠️')} Could not clean cache: ${cacheError.message}`); console.log(chalk.dim(' Profile updated successfully. Cache will rebuild on next discovery.')); } } catch (error: any) { const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('profile', 'remove', `${name} from ${profileName}`)); console.log('\n' + ErrorHandler.formatForConsole(errorResult)); } } }); // Config command group const configCmd = program .command('config') .description('Manage NCP configuration (import, validate, edit)'); configCmd .command('import [file]') .description('Import MCP configurations from file or clipboard') .option('--profile <name>', 'Target profile (default: all)') .option('--dry-run', 'Show what would be imported without actually importing') .action(async (file, options) => { try { const manager = new ConfigManager(); await manager.importConfig(file, options.profile, options.dryRun); } catch (error: any) { const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('config', 'import', file || 'clipboard')); console.log('\n' + ErrorHandler.formatForConsole(errorResult)); process.exit(1); } }); configCmd .command('edit') .description('Open config directory in default editor') .action(async () => { const manager = new ConfigManager(); await manager.editConfig(); }); configCmd .command('validate') .description('Validate current configuration') .action(async () => { const manager = new ConfigManager(); await manager.validateConfig(); }); configCmd .command('location') .description('Show configuration file locations') .action(async () => { const manager = new ConfigManager(); await manager.showConfigLocations(); }); // Repair command - fix failed MCPs interactively program .command('repair') .description('Interactively configure failed MCPs') .option('--profile <name>', 'Profile to repair (default: all)') .action(async (options) => { try { // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE! const profileName = options.profile || program.getOptionValue('profile') || 'all'; console.log(chalk.bold('\n🔧 MCP Repair Tool\n')); // Load failed MCPs from both sources const { getCacheDirectory } = await import('../utils/ncp-paths.js'); const { CSVCache } = await import('../cache/csv-cache.js'); const { MCPErrorParser } = await import('../utils/mcp-error-parser.js'); const { ProfileManager } = await import('../profiles/profile-manager.js'); const { MCPWrapper } = await import('../utils/mcp-wrapper.js'); const { healthMonitor } = await import('../utils/health-monitor.js'); const { readFileSync, existsSync } = await import('fs'); const { Client } = await import('@modelcontextprotocol/sdk/client/index.js'); const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js'); const cache = new CSVCache(getCacheDirectory(), profileName); await cache.initialize(); // Load profile first to know which MCPs to check const profileManager = new ProfileManager(); await profileManager.initialize(); const profile = await profileManager.getProfile(profileName); if (!profile) { console.log(chalk.red(`❌ Profile '${profileName}' not found`)); return; } // Merge failed MCPs from both sources const failedMCPs = new Map<string, { errorMessage: string; attemptCount: number; source: 'cache' | 'health' | 'both'; lastAttempt: string; }>(); // Add from CSV cache const cacheMetadata = (cache as any).metadata; if (cacheMetadata?.failedMCPs) { for (const [mcpName, failedInfo] of cacheMetadata.failedMCPs) { failedMCPs.set(mcpName, { errorMessage: failedInfo.errorMessage, attemptCount: failedInfo.attemptCount, source: 'cache', lastAttempt: failedInfo.lastAttempt }); } } // Add from health monitor (unhealthy or disabled) const healthReport = healthMonitor.generateHealthReport(); for (const health of healthReport.details) { if (health.status === 'unhealthy' || health.status === 'disabled') { // Only include if it's in the current profile if (profile.mcpServers[health.name]) { const existing = failedMCPs.get(health.name); if (existing) { // Already in cache, mark as both existing.source = 'both'; } else { // Only in health monitor failedMCPs.set(health.name, { errorMessage: health.lastError || 'Unknown error', attemptCount: health.errorCount, source: 'health', lastAttempt: health.lastCheck }); } } } } if (failedMCPs.size === 0) { console.log(chalk.green('✅ No failed MCPs! Everything is working.')); return; } console.log(chalk.yellow(`Found ${failedMCPs.size} failed MCPs\n`)); console.log(chalk.dim('This tool will help you configure them interactively.\n')); const errorParser = new MCPErrorParser(); const mcpWrapper = new MCPWrapper(); const prompts = (await import('prompts')).default; let fixedCount = 0; let skippedCount = 0; let stillFailingCount = 0; // Iterate through failed MCPs for (const [mcpName, failedInfo] of failedMCPs) { console.log(chalk.cyan(`\n📦 ${mcpName}`)); console.log(chalk.dim(` Last error: ${failedInfo.errorMessage}`)); console.log(chalk.dim(` Failed ${failedInfo.attemptCount} time(s)`)); // Show source of failure detection const sourceLabel = failedInfo.source === 'both' ? 'indexing & runtime' : failedInfo.source === 'cache' ? 'indexing' : 'runtime'; console.log(chalk.dim(` Detected during: ${sourceLabel}`)); // Ask if user wants to fix this MCP const { shouldFix } = await prompts({ type: 'confirm', name: 'shouldFix', message: `Try to fix ${mcpName}?`, initial: true }); if (!shouldFix) { skippedCount++; continue; } // Get current MCP config first (needed for all tiers) const currentConfig = profile.mcpServers[mcpName]; if (!currentConfig) { console.log(chalk.red(` ❌ MCP not found in profile`)); skippedCount++; continue; } // Skip HTTP/SSE MCPs (they don't have command/args to repair) if (currentConfig.url && !currentConfig.command) { console.log(chalk.yellow(` ⚠️ Skipping HTTP/SSE MCP (remote connector)`)); skippedCount++; continue; } // Two-tier configuration detection (same as ncp add): // Tier 1: Cached schema (from previous successful add or MCP protocol) // Tier 2: Error parsing (fallback) let detectedSchema: any = null; // Check for cached schema const schemaCache = new SchemaCache(getCacheDirectory()); detectedSchema = schemaCache.get(mcpName); if (detectedSchema) { console.log(chalk.dim(` ✓ Using cached configuration schema`)); } // If we have a schema, use schema-based prompting if (detectedSchema) { const schemaReader = new ConfigSchemaReader(); const configPrompter = new ConfigPrompter(); if (schemaReader.hasRequiredConfig(detectedSchema)) { console.log(chalk.cyan(`\n 📋 Configuration required`)); const promptedConfig = await configPrompter.promptForConfig(detectedSchema, mcpName); // Update config with prompted values const updatedConfig = { command: currentConfig.command, args: [...(currentConfig.args || []), ...(promptedConfig.arguments || [])], env: { ...(currentConfig.env || {}), ...(promptedConfig.environmentVariables || {}) } }; // Save updated config await profileManager.addMCPToProfile(profileName, mcpName, updatedConfig); console.log(chalk.green(`\n ✅ Configuration updated for ${mcpName}`)); fixedCount++; continue; } } // Tier 3: Fallback to error parsing if no schema available const logPath = mcpWrapper.getLogFile(mcpName); let stderr = ''; if (existsSync(logPath)) { const logContent = readFileSync(logPath, 'utf-8'); // Extract stderr lines const stderrLines = logContent.split('\n').filter(line => line.includes('[STDERR]')); stderr = stderrLines.map(line => line.replace(/\[STDERR\]\s*/, '')).join('\n'); } else { stderr = failedInfo.errorMessage; } // Parse errors to detect configuration needs const configNeeds = errorParser.parseError(mcpName, stderr, 1); if (configNeeds.length === 0) { console.log(chalk.yellow(` ⚠️ Could not detect specific configuration needs`)); console.log(chalk.dim(` Check logs manually: ${logPath}`)); skippedCount++; continue; } // Check if it's a missing package const packageMissing = configNeeds.find(n => n.type === 'package_missing'); if (packageMissing) { console.log(chalk.red(` ❌ Package not found on npm - cannot fix`)); console.log(chalk.dim(` ${packageMissing.extractedFrom}`)); skippedCount++; continue; } console.log(chalk.yellow(`\n Found ${configNeeds.length} configuration need(s):`)); for (const need of configNeeds) { console.log(chalk.dim(` • ${need.description}`)); } // Collect new configuration from user const newEnv = { ...(currentConfig.env || {}) }; const newArgs = [...(currentConfig.args || [])]; for (const need of configNeeds) { if (need.type === 'api_key' || need.type === 'env_var') { const { value } = await prompts({ type: need.sensitive ? 'password' : 'text', name: 'value', message: need.prompt, validate: (val: string) => val.length > 0 ? true : 'Value required' }); if (!value) { console.log(chalk.yellow(` Skipped ${mcpName}`)); skippedCount++; continue; } newEnv[need.variable] = value; } else if (need.type === 'command_arg') { const { value } = await prompts({ type: 'text', name: 'value', message: need.prompt, validate: (val: string) => val.length > 0 ? true : 'Value required' }); if (!value) { console.log(chalk.yellow(` Skipped ${mcpName}`)); skippedCount++; continue; } newArgs.push(value); } } // Test MCP with new configuration console.log(chalk.dim(`\n Testing ${mcpName} with new configuration...`)); const testConfig = { name: mcpName, command: currentConfig.command, args: newArgs, env: newEnv }; try { // Create wrapper command const wrappedCommand = mcpWrapper.createWrapper( testConfig.name, testConfig.command || '', // Should never be undefined after HTTP/SSE check testConfig.args || [] ); // Test connection with 30 second timeout const transport = new StdioClientTransport({ command: wrappedCommand.command, args: wrappedCommand.args, env: { ...process.env, ...(testConfig.env || {}), MCP_SILENT: 'true', QUIET: 'true' } }); const client = new Client({ name: 'ncp-repair-test', version: '1.0.0' }, { capabilities: {} }); await client.connect(transport); // Try to list tools const result = await Promise.race([ client.listTools(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Test timeout')), 30000) ) ]); await client.close(); console.log(chalk.green(` ✅ Success! Found ${result.tools.length} tools`)); // Update profile with new configuration profile.mcpServers[mcpName] = { command: testConfig.command, args: newArgs, env: newEnv }; profile.metadata.modified = new Date().toISOString(); await profileManager.saveProfile(profile); // Remove from both failure tracking systems if (cacheMetadata?.failedMCPs) { cacheMetadata.failedMCPs.delete(mcpName); (cache as any).saveMetadata(); } // Clear from health monitor and mark as healthy await healthMonitor.enableMCP(mcpName); healthMonitor.markHealthy(mcpName); await healthMonitor.saveHealth(); fixedCount++; } catch (error: any) { console.log(chalk.red(` ❌ Still failing: ${error.message}`)); stillFailingCount++; } } // Final report console.log(chalk.bold('\n📊 Repair Summary\n')); console.log(chalk.green(`✅ Fixed: ${fixedCount}`)); console.log(chalk.yellow(`⏭️ Skipped: ${skippedCount}`)); console.log(chalk.red(`❌ Still failing: ${stillFailingCount}`)); if (fixedCount > 0) { console.log(chalk.dim('\n💡 Run "ncp find --force-retry" to re-index fixed MCPs')); } } catch (error: any) { console.error(chalk.red('\n❌ Repair command failed:'), error.message); console.error(chalk.dim(error.stack)); process.exit(1); } }); // Find command (CLI-optimized version for fast discovery) program .command('find [query]') .description('Find tools matching a query or list all tools') .option('--limit <number>', 'Maximum number of results (default: 5)') .option('--page <number>', 'Page number (default: 1)') .option('--depth <number>', 'Display depth: 0=overview, 1=tools, 2=details (default: 2)') .option('--confidence_threshold <number>', 'Minimum confidence level (0.0-1.0, default: 0.3). Examples: 0.1=show all, 0.5=strict, 0.7=very precise') .action(async (query, options) => { // Add newline after command before any output console.log(); // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE! const profileName = program.getOptionValue('profile') || 'all'; const forceRetry = program.getOptionValue('forceRetry') || false; // Use MCPServer for rich formatted output const { MCPServer } = await import('../server/mcp-server.js'); const server = new MCPServer(profileName, true, forceRetry); // Enable progress + force retry flag // Setup graceful shutdown on Ctrl+C const gracefulShutdown = async () => { process.stdout.write('\n\n💾 Saving progress...'); try { await server.cleanup(); process.stdout.write('\r\u001B[K✅ Progress saved\n'); } catch (error) { process.stdout.write('\r\u001B[K❌ Error saving progress\n'); } process.exit(0); }; process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); await server.initialize(); // For CLI usage, wait for indexing to complete before searching await server.waitForInitialization(); const limit = parseInt(options.limit || '5'); const page = parseInt(options.page || '1'); const depth = parseInt(options.depth || '2'); const confidence_threshold = options.confidence_threshold ? parseFloat(options.confidence_threshold) : undefined; const result = await server.handleFind( { jsonrpc: '2.0', id: 'cli', method: 'tools/call' }, { description: query || '', limit, page, depth, confidence_threshold } ); const formattedOutput = formatFindOutput(result.result.content[0].text); console.log(formattedOutput); await server.cleanup(); }); // Analytics command group const analyticsCmd = program .command('analytics') .description('View NCP usage analytics and performance metrics'); analyticsCmd .command('dashboard') .description('Show comprehensive analytics dashboard') .option('--period <days>', 'Show data for last N days (e.g., --period 7)') .option('--from <date>', 'Start date (YYYY-MM-DD format)') .option('--to <date>', 'End date (YYYY-MM-DD format)') .option('--today', 'Show only today\'s data') .option('--visual', 'Enhanced visual dashboard with charts and graphs') .action(async (options) => { const { NCPLogParser } = await import('../analytics/log-parser.js'); console.log(chalk.dim('📊 Analyzing NCP usage data...')); const parser = new NCPLogParser(); // Parse time range options const parseOptions: any = {}; if (options.today) { parseOptions.today = true; } else if (options.period) { parseOptions.period = parseInt(options.period); } else if (options.from || options.to) { if (options.from) parseOptions.from = new Date(options.from); if (options.to) parseOptions.to = new Date(options.to); } const report = await parser.parseAllLogs(parseOptions); if (report.totalSessions === 0) { console.log(chalk.yellow('📊 No analytics data available for the specified time range')); console.log(chalk.dim('💡 Try a different time range or check if MCPs have been used through NCP')); return; } if (options.visual) { const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js'); const dashboard = await VisualAnalyticsFormatter.formatVisualDashboard(report); console.log(dashboard); } else { const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js'); const dashboard = AnalyticsFormatter.formatDashboard(report); console.log(dashboard); } }); analyticsCmd .command('performance') .description('Show performance-focused analytics') .option('--period <days>', 'Show data for last N days (e.g., --period 7)') .option('--from <date>', 'Start date (YYYY-MM-DD format)') .option('--to <date>', 'End date (YYYY-MM-DD format)') .option('--today', 'Show only today\'s data') .option('--visual', 'Enhanced visual performance report with gauges and charts') .action(async (options) => { const { NCPLogParser } = await import('../analytics/log-parser.js'); console.log(chalk.dim('⚡ Analyzing performance metrics...')); const parser = new NCPLogParser(); // Parse time range options const parseOptions: any = {}; if (options.today) { parseOptions.today = true; } else if (options.period) { parseOptions.period = parseInt(options.period); } else if (options.from || options.to) { if (options.from) parseOptions.from = new Date(options.from); if (options.to) parseOptions.to = new Date(options.to); } const report = await parser.parseAllLogs(parseOptions); if (report.totalSessions === 0) { console.log(chalk.yellow('📊 No performance data available for the specified time range')); return; } if (options.visual) { const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js'); const performance = await VisualAnalyticsFormatter.formatVisualPerformance(report); console.log(performance); } else { const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js'); const performance = AnalyticsFormatter.formatPerformanceReport(report); console.log(performance); } }); analyticsCmd .command('visual') .description('Show enhanced visual analytics with charts and graphs') .option('--period <days>', 'Show data for last N days (e.g., --period 7)') .option('--from <date>', 'Start date (YYYY-MM-DD format)') .option('--to <date>', 'End date (YYYY-MM-DD format)') .option('--today', 'Show only today\'s data') .action(async (options) => { const { NCPLogParser } = await import('../analytics/log-parser.js'); const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js'); console.log(chalk.dim('🎨 Generating visual analytics...')); const parser = new NCPLogParser(); // Parse time range options const parseOptions: any = {}; if (options.today) { parseOptions.today = true; } else if (options.period) { parseOptions.period = parseInt(options.period); } else if (options.from || options.to) { if (options.from) parseOptions.from = new Date(options.from); if (options.to) parseOptions.to = new Date(options.to); } const report = await parser.parseAllLogs(parseOptions); if (report.totalSessions === 0) { console.log(chalk.yellow('📊 No analytics data available for the specified time range')); console.log(chalk.dim('💡 Try a different time range or check if MCPs have been used through NCP')); return; } const dashboard = await VisualAnalyticsFormatter.formatVisualDashboard(report); console.log(dashboard); }); analyticsCmd .command('export') .description('Export analytics data to CSV') .option('--output <file>', 'Output file (default: ncp-analytics.csv)') .option('--period <days>', 'Export data for last N days (e.g., --period 7)') .option('--from <date>', 'Start date (YYYY-MM-DD format)') .option('--to <date>', 'End date (YYYY-MM-DD format)') .option('--today', 'Export only today\'s data') .action(async (options) => { const { NCPLogParser } = await import('../analytics/log-parser.js'); const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js'); const { writeFileSync } = await import('fs'); console.log(chalk.dim('📊 Generating analytics export...')); const parser = new NCPLogParser(); // Parse time range options const parseOptions: any = {}; if (options.today) { parseOptions.today = true; } else if (options.period) { parseOptions.period = parseInt(options.period); } else if (options.from || options.to) { if (options.from) parseOptions.from = new Date(options.from); if (options.to) parseOptions.to = new Date(options.to); } const report = await parser.parseAllLogs(parseOptions); if (report.totalSessions === 0) { console.log(chalk.yellow('📊 No data to export for the specified time range')); return; } const csv = AnalyticsFormatter.formatCSV(report); const filename = options.output || 'ncp-analytics.csv'; writeFileSync(filename, csv, 'utf-8'); console.log(chalk.green(`✅ Analytics exported to ${filename}`)); console.log(chalk.dim(`📊 Exported ${report.totalSessions} sessions across ${report.uniqueMCPs} MCPs`)); }); // Run command (existing functionality) program .command('run <tool>') .description('Run a specific tool') .option('--params <json>', 'Tool parameters as JSON string (optional - will prompt interactively if not provided)') .option('--no-prompt', 'Skip interactive prompting for missing parameters') .option('--output-format <format>', 'Output format: auto (smart rendering), json (raw JSON)', 'auto') .option('-y, --yes', 'Automatically answer yes to prompts (e.g., open media files)') .configureHelp({ formatHelp: (cmd, helper) => { const indent = ' '; let output = '\n'; // Header first - context before syntax output += chalk.bold.white('NCP Run Command') + ' - ' + chalk.cyan('Direct MCP Tool Execution') + '\n\n'; output += chalk.dim('Execute MCP tools with intelligent parameter prompting and rich media support.') + '\n'; output += chalk.dim('Automatically handles parameter collection, validation, and response formatting.') + '\n\n'; // Then usage output += chalk.bold.white('Usage:') + ' ' + helper.commandUsage(cmd) + '\n\n'; const visibleOptions = helper.visibleOptions(cmd); if (visibleOptions.length) { output += chalk.bold.white('Options:') + '\n'; visibleOptions.forEach(option => { const flags = option.flags; const description = helper.optionDescription(option); const paddingNeeded = Math.max(0, 42 - flags.length); const padding = ' '.repeat(paddingNeeded); output += indent + chalk.cyan(flags) + padding + ' ' + chalk.white(description) + '\n'; }); output += '\n'; } // Examples section output += chalk.bold.white('Examples:') + '\n'; output += chalk.dim(' Basic execution:') + '\n'; output += indent + chalk.yellow('ncp run memory:create_entities') + chalk.gray(' # Interactive parameter prompting') + '\n'; output += indent + chalk.yellow('ncp run memory:create_entities --params \'{"entities":["item1"]}\'') + '\n\n'; output += chalk.dim(' Output control:') + '\n'; output += indent + chalk.yellow('ncp run tool --output-format json') + chalk.gray(' # Raw JSON output') + '\n'; output += indent + chalk.yellow('ncp run tool -y') + chalk.gray(' # Auto-open media files') + '\n\n'; output += chalk.dim(' Non-interactive:') + '\n'; output += indent + chalk.yellow('ncp run tool --no-prompt --params \'{}\'') + chalk.gray(' # Scripting/automation') + '\n\n'; // Media support note output += chalk.bold.white('Media Support:') + '\n'; output += chalk.dim(' • Images and audio are displayed with metadata') + '\n'; output += chalk.dim(' • Use') + chalk.cyan(' -y ') + chalk.dim('to auto-open media in default applications') + '\n'; output += chalk.dim(' • Without') + chalk.cyan(' -y') + chalk.dim(', prompts before opening media files') + '\n\n'; return output; } }) .action(async (tool, options) => { // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE! const profileName = program.getOptionValue('profile') || 'all'; const { NCPOrchestrator } = await import('../orchestrator/ncp-orchestrator.js'); const orchestrator = new NCPOrchestrator(profileName, false); // Silent indexing for run command await orchestrator.initialize(); // If tool doesn't contain a colon, try to find matching tools first if (!tool.includes(':')) { console.log(chalk.dim(`🔍 Searching for tools matching "${tool}"...`)); try { const matchingTools = await orchestrator.find(tool, 5, false); if (matchingTools.length === 0) { console.log('\n' + OutputFormatter.error(`No tools found matching "${tool}"`)); console.log(chalk.yellow('💡 Try \'ncp find\' to explore all available tools')); await orchestrator.cleanup(); return; } if (matchingTools.length === 1) { // Only one match, use it automatically const matchedTool = matchingTools[0]; tool = matchedTool.toolName; console.log(chalk.green(`✅ Found exact match: ${tool}`)); } else { // Multiple matches, show them and ask user to be more specific console.log(chalk.yellow(`Found ${matchingTools.length} matching tools:`)); matchingTools.forEach((match, index) => { const confidence = Math.round(match.confidence * 100); console.log(` ${index + 1}. ${chalk.cyan(match.toolName)} (${confidence}% match)`); if (match.description) { console.log(` ${chalk.dim(match.description)}`); } }); console.log(chalk.yellow('\n💡 Please specify the exact tool name from the list above')); console.log(chalk.yellow(`💡 Example: ncp run ${matchingTools[0].toolName}`)); await orchestrator.cleanup(); return; } } catch (error: any) { console.log('\n' + OutputFormatter.error(`Error searching for tools: ${error.message}`)); await orchestrator.cleanup(); return; } } // Check if parameters are provided let parameters = {}; if (options.params) { parameters = JSON.parse(options.params); } else { // Get tool schema and parameters const toolParams = orchestrator.getToolParameters(tool); if (toolParams && toolParams.length > 0) { const requiredParams = toolParams.filter(p => p.required); if (requiredParams.length > 0 && options.prompt !== false) { // Interactive prompting for parameters (default behavior) const { ParameterPrompter } = await import('../utils/parameter-prompter.js'); const { ParameterPredictor } = await import('../server/mcp-server.js'); const prompter = new ParameterPrompter(); const predictor = new ParameterPredictor(); const toolContext = orchestrator.getToolContext(tool); try { parameters = await prompter.promptForParameters(tool, toolParams, predictor, toolContext); prompter.close(); } catch (error) { prompter.close(); console.log('\n' + OutputFormatter.error('Error during parameter input')); await orchestrator.cleanup(); return; } } else if (requiredParams.length > 0 && options.prompt === false) { console.log('\n' + OutputFormatter.error('This tool requires parameters')); console.log(chalk.yellow(`💡 Use: ncp run ${tool} --params '{"param": "value"}'`)); console.log(chalk.yellow(`💡 Or use: ncp find "${tool}" --depth 2 to see required parameters`)); console.log(chalk.yellow(`💡 Or remove --no-prompt to use interactive prompting`)); await orchestrator.cleanup(); return; } } } console.log(OutputFormatter.running(tool) + '\n'); const result = await orchestrator.run(tool, parameters); if (result.success) { // Check if the content indicates an actual error despite "success" status const contentStr = JSON.stringify(result.content); const isActualError = contentStr.includes('"type":"text"') && (contentStr.includes('Error:') || contentStr.includes('not found') || contentStr.includes('Unknown tool')); if (isActualError) { const errorText = result.content?.[0]?.text || 'Unknown error occurred'; let suggestions: string[] = []; if (errorText.includes('not configured') || errorText.includes('Unknown tool')) { // Extract the query from the tool name for vector search const [mcpName, toolName] = tool.split(':'); // Try multiple search strategies to find the best matches let similarTools: any[] = []; try { // Strategy 1: Search with both MCP context and tool name for better domain matching if (toolName && mcpName) { const contextualQuery = `${mcpName} ${toolName}`; similarTools = await orchestrator.find(contextualQuery, 3, false); } // Strategy 2: If no results, try just the tool name if (similarTools.length === 0 && toolName) { similarTools = await orchestrator.find(toolName, 3, false); } // Strategy 3: If still no results, try just the MCP name (domain search) if (similarTools.length === 0) { similarTools = await orchestrator.find(mcpName, 3, false); } if (similarTools.length > 0) { suggestions.push('💡 Did you mean:'); similarTools.forEach(similar => { const confidence = Math.round(similar.confidence * 100); suggestions.push(` • ${similar.toolName} (${confidence}% match)`); }); } } catch (error: any) { // Fallback to basic suggestions if vector search fails suggestions = ['Try \'ncp find <keyword>\' to discover similar tools']; } } const context = ErrorHandler.createContext('mcp', 'run', tool, suggestions); const errorResult = ErrorHandler.handle(errorText, context); console.log('\n' + ErrorHandler.formatForConsole(errorResult)); } else { console.log(OutputFormatter.success('Tool execution completed')); // Respect user's output format choice if (options.outputFormat === 'json') { // Raw JSON output - test different formatters to pick the best const { formatJson } = await import('../utils/highlighting.js'); console.log(formatJson(result.content, 'cli-highlight')); // Let's test this one } else { // Smart response formatting (default) const { ResponseFormatter } = await import('../utils/response-formatter.js'); // Check if this is text content that should be formatted naturally const isTextResponse = Array.isArray(result.content) && result.content.every((item: any) => item?.type === 'text'); if (isTextResponse || (result.content?.[0]?.type === 'text' && result.content.length === 1)) { // Format as natural text with proper newlines console.log(ResponseFormatter.format(result.content, true, options.yes)); } else if (ResponseFormatter.isPureData(result.content)) { // Pure data - use JSON formatting const { formatJson } = await import('../utils/highlighting.js'); console.log(formatJson(result.content, 'cli-highlight')); } else { // Mixed content or unknown - use smart formatter console.log(ResponseFormatter.format(result.content, true, options.yes)); } } } } else { // Check if this is a tool not found error and provide "did you mean" suggestions const errorMessage = result.error || 'Unknown error occurred'; let suggestions: string[] = []; if (errorMessage.includes('not configured') || errorMessage.includes('Unknown tool')) { // Extract the query from the tool name for vector search const [mcpName, toolName] = tool.split(':'); // Try multiple search strategies to find the best matches let similarTools: any[] = []; try { // Strategy 1: Search with both MCP context and tool name for better domain matching if (toolName && mcpName) { const contextualQuery = `${mcpName} ${toolName}`; similarTools = await orchestrator.find(contextualQuery, 3, false); } // Strategy 2: If no results, try just the tool name if (similarTools.length === 0 && toolName) { similarTools = await orchestrator.find(toolName, 3, false); } // Strategy 3: If still no results, try just the MCP name (domain search) if (similarTools.length === 0) { similarTools = await orchestrator.find(mcpName, 3, false); } if (similarTools.length > 0) { suggestions.push('💡 Did you mean:'); similarTools.forEach(similar => { const confidence = Math.round(similar.confidence * 100); suggestions.push(` • ${similar.toolName} (${confidence}% match)`); }); } } catch (error: any) { // Fallback to basic suggestions if vector search fails suggestions = ['Try \'ncp find <keyword>\' to discover similar tools']; } } const context = ErrorHandler.createContext('mcp', 'run', tool, suggestions); const errorResult = ErrorHandler.handle(errorMessage, context); console.log('\n' + ErrorHandler.formatForConsole(errorResult)); } await orchestrator.cleanup(); }); // Auth command program .command('auth <mcp>') .description('Authenticate an MCP server using OAuth Device Flow') .option('--profile <name>', 'Profile to use (default: all)') .configureHelp({ formatHelp: () => { let output = '\n'; output += chalk.bold.white('NCP Auth Command') + ' - ' + chalk.cyan('OAuth Authentication') + '\n\n'; output += chalk.dim('Authenticate an MCP server that requires OAuth 2.0 Device Flow authentication.') + '\n'; output += chalk.dim('Tokens are securely stored and automatically refreshed.') + '\n\n'; output += chalk.bold.white('Usage:') + '\n'; output += ' ' + chalk.yellow('ncp auth <mcp>') + ' # Authenticate an MCP server\n'; output += ' ' + chalk.yellow('ncp auth <mcp> --profile <name>') + ' # Authenticate for specific profile\n\n'; output += chalk.bold.white('Examples:') + '\n'; output += ' ' + chalk.gray('$ ') + chalk.yellow('ncp auth github') + '\n'; output += ' ' + chalk.gray('$ ') + chalk.yellow('ncp auth my-api --profile production') + '\n\n'; return output; } }) .action(async (mcpName, options) => { try { const profileName = options.profile || 'all'; // Load profile const manager = new ProfileManager(); await manager.initialize(); const profile = await manager.getProfile(profileName); if (!profile) { console.error(chalk.red(`❌ Profile '${profileName}' not found`)); process.exit(1); } // Check if MCP exists in profile const mcpConfig = profile.mcpServers[mcpName]; if (!mcpConfig) { console.error(chalk.red(`❌ MCP '${mcpName}' not found in profile '${profileName}'`)); // Suggest similar MCPs const availableMCPs = Object.keys(profile.mcpServers); if (availableMCPs.length > 0) { const suggestions = findSimilarNames(mcpName, availableMCPs); if (suggestions.length > 0) { console.log(chalk.yellow('\n💡 Did you mean:')); suggestions.forEach((suggestion, index) => { console.log(` ${index + 1}. ${chalk.cyan(suggestion)}`); }); } } process.exit(1); } // Check if MCP has OAuth configuration if (!mcpConfig.auth || mcpConfig.auth.type !== 'oauth' || !mcpConfig.auth.oauth) { console.error(chalk.red(`❌ MCP '${mcpName}' does not have OAuth configuration`)); console.log(chalk.yellow('\n💡 To add OAuth configuration, edit your profile configuration file:')); console.log(chalk.dim(' Add "auth": { "type": "oauth", "oauth": { ... } } to the MCP configuration')); process.exit(1); } // Perform OAuth Device Flow const { DeviceFlowAuthenticator } = await import('../auth/oauth-device-flow.js'); const { getTokenStore } = await import('../auth/token-store.js'); const authenticator = new DeviceFlowAuthenticator(mcpConfig.auth.oauth); const tokenStore = getTokenStore(); console.log(chalk.blue(`🔐 Starting OAuth Device Flow for '${mcpName}'...\n`)); try { const tokenResponse = await authenticator.authenticate(); // Store the token await tokenStore.storeToken(mcpName, tokenResponse); console.log(chalk.green(`✅ Successfully authenticated '${mcpName}'!`)); console.log(chalk.dim(` Token expires: ${new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()}`)); console.log(chalk.dim(` Token stored securely in: ~/.ncp/tokens/${mcpName}.token`)); } catch (error: any) { console.error(chalk.red(`❌ Authentication failed: ${error.message}`)); process.exit(1); } } catch (error: any) { console.error(chalk.red(`❌ Error: ${error.message}`)); process.exit(1); } }); // Update command program .command('update') .description('Update NCP to the latest version') .option('--check', 'Check for updates without installing') .configureHelp({ formatHelp: () => { let output = '\n'; output += chalk.bold.white('NCP Update Command') + ' - ' + chalk.cyan('Version Management') + '\n\n'; output += chalk.dim('Keep NCP up to date with the latest features and bug fixes.') + '\n\n'; output += chalk.bold.white('Usage:') + '\n'; output += ' ' + chalk.yellow('ncp update') + ' # Update to latest version\n'; output += ' ' + chalk.yellow('ncp update --check') + ' # Check for updates without installing\n\n'; output += chalk.bold.white('Examples:') + '\n'; output += ' ' + chalk.gray('$ ') + chalk.yellow('ncp update --check') + '\n'; output += ' ' + chalk.gray('$ ') + chalk.yellow('ncp update') + '\n\n'; return output; } }) .action(async (options) => { try { const updateChecker = new UpdateChecker(); if (options.check) { // Check for updates only console.log(chalk.blue('🔍 Checking for updates...')); const result = await updateChecker.checkForUpdates(true); if (result.hasUpdate) { console.log(chalk.yellow('📦 Update Available!')); console.log(chalk.dim(` Current: ${result.currentVersion}`)); console.log(chalk.green(` Latest: ${result.latestVersion}`)); console.log(); console.log(chalk.cyan(' Run: ncp update')); } else { console.log(chalk.green('✅ You are using the latest version!')); console.log(chalk.dim(` Version: ${result.currentVersion}`)); } } else { // Perform update const result = await updateChecker.checkForUpdates(true); if (result.hasUpdate) { console.log(chalk.yellow('📦 Update Available!')); console.log(chalk.dim(` Current: ${result.currentVersion}`)); console.log(chalk.green(` Latest: ${result.latestVersion}`)); console.log(); const success = await updateChecker.performUpdate(); if (!success) { process.exit(1); } } else { console.log(chalk.green('✅ You are already using the latest version!')); console.log(chalk.dim(` Version: ${result.currentVersion}`)); } } } catch (error) { console.error(chalk.red('❌ Failed to check for updates:'), error); process.exit(1); } }); // Check for updates on CLI startup (non-intrusive) // Temporarily disabled - causing hangs in some environments // TODO: Re-enable with proper timeout handling // (async () => { // try { // const updateChecker = new UpdateChecker(); // await updateChecker.showUpdateNotification(); // } catch { // // Silently fail - don't interrupt normal CLI usage // } // })(); program.parse(); } ```