#
tokens: 48653/50000 2/189 files (page 12/12)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 12 of 12. Use http://codebase.md/portel-dev/ncp?lines=true&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
   1 | /**
   2 |  * NCP Orchestrator - Real MCP Connections
   3 |  * Based on commercial NCP implementation
   4 |  */
   5 | 
   6 | import { readFileSync, existsSync } from 'fs';
   7 | import { getCacheDirectory } from '../utils/ncp-paths.js';
   8 | import { join } from 'path';
   9 | import { createHash } from 'crypto';
  10 | import ProfileManager from '../profiles/profile-manager.js';
  11 | import { logger } from '../utils/logger.js';
  12 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  13 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
  14 | import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
  15 | import { DiscoveryEngine } from '../discovery/engine.js';
  16 | import { MCPHealthMonitor } from '../utils/health-monitor.js';
  17 | import { SearchEnhancer } from '../discovery/search-enhancer.js';
  18 | import { mcpWrapper } from '../utils/mcp-wrapper.js';
  19 | import { withFilteredOutput } from '../transports/filtered-stdio-transport.js';
  20 | import { ToolSchemaParser, ParameterInfo } from '../services/tool-schema-parser.js';
  21 | import { InternalMCPManager } from '../internal-mcps/internal-mcp-manager.js';
  22 | import { ToolContextResolver } from '../services/tool-context-resolver.js';
  23 | import type { OAuthConfig } from '../auth/oauth-device-flow.js';
  24 | import { getRuntimeForExtension, logRuntimeInfo } from '../utils/runtime-detector.js';
  25 | // Simple string similarity for tool name matching
  26 | function calculateSimilarity(str1: string, str2: string): number {
  27 |   const s1 = str1.toLowerCase();
  28 |   const s2 = str2.toLowerCase();
  29 | 
  30 |   // Exact match
  31 |   if (s1 === s2) return 1.0;
  32 | 
  33 |   // Check if one contains the other
  34 |   if (s1.includes(s2) || s2.includes(s1)) {
  35 |     return 0.8;
  36 |   }
  37 | 
  38 |   // Simple Levenshtein-based similarity
  39 |   const longer = s1.length > s2.length ? s1 : s2;
  40 |   const shorter = s1.length > s2.length ? s2 : s1;
  41 | 
  42 |   if (longer.length === 0) return 1.0;
  43 | 
  44 |   const editDistance = levenshteinDistance(s1, s2);
  45 |   return (longer.length - editDistance) / longer.length;
  46 | }
  47 | 
  48 | function levenshteinDistance(str1: string, str2: string): number {
  49 |   const matrix: number[][] = [];
  50 | 
  51 |   for (let i = 0; i <= str2.length; i++) {
  52 |     matrix[i] = [i];
  53 |   }
  54 | 
  55 |   for (let j = 0; j <= str1.length; j++) {
  56 |     matrix[0][j] = j;
  57 |   }
  58 | 
  59 |   for (let i = 1; i <= str2.length; i++) {
  60 |     for (let j = 1; j <= str1.length; j++) {
  61 |       if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
  62 |         matrix[i][j] = matrix[i - 1][j - 1];
  63 |       } else {
  64 |         matrix[i][j] = Math.min(
  65 |           matrix[i - 1][j - 1] + 1,
  66 |           matrix[i][j - 1] + 1,
  67 |           matrix[i - 1][j] + 1
  68 |         );
  69 |       }
  70 |     }
  71 |   }
  72 | 
  73 |   return matrix[str2.length][str1.length];
  74 | }
  75 | import { CachePatcher } from '../cache/cache-patcher.js';
  76 | import { CSVCache, CachedTool } from '../cache/csv-cache.js';
  77 | import { spinner } from '../utils/progress-spinner.js';
  78 | 
  79 | interface DiscoveryResult {
  80 |   toolName: string;
  81 |   mcpName: string;
  82 |   confidence: number;
  83 |   description?: string;
  84 |   schema?: any;
  85 | }
  86 | 
  87 | interface ExecutionResult {
  88 |   success: boolean;
  89 |   content?: any;
  90 |   error?: string;
  91 | }
  92 | 
  93 | interface MCPConfig {
  94 |   name: string;
  95 |   command?: string;  // Optional: for stdio transport
  96 |   args?: string[];
  97 |   env?: Record<string, string>;
  98 |   url?: string;  // Optional: for HTTP/SSE transport (Claude Desktop native)
  99 |   auth?: {
 100 |     type: 'oauth' | 'bearer' | 'apiKey' | 'basic';
 101 |     oauth?: OAuthConfig;  // OAuth 2.0 Device Flow configuration
 102 |     token?: string;       // Bearer token or API key
 103 |     username?: string;    // Basic auth username
 104 |     password?: string;    // Basic auth password
 105 |   };
 106 | }
 107 | 
 108 | interface Profile {
 109 |   name: string;
 110 |   description: string;
 111 |   mcpServers: Record<string, {
 112 |     command?: string;  // Optional: for stdio transport
 113 |     args?: string[];
 114 |     env?: Record<string, string>;
 115 |     url?: string;  // Optional: for HTTP/SSE transport
 116 |     auth?: {
 117 |       type: 'oauth' | 'bearer' | 'apiKey' | 'basic';
 118 |       oauth?: OAuthConfig;  // OAuth 2.0 Device Flow configuration
 119 |       token?: string;       // Bearer token or API key
 120 |       username?: string;    // Basic auth username
 121 |       password?: string;    // Basic auth password
 122 |     };
 123 |   }>;
 124 |   metadata?: any;
 125 | }
 126 | 
 127 | interface MCPConnection {
 128 |   client: Client;
 129 |   transport: StdioClientTransport | SSEClientTransport;
 130 |   tools: Array<{name: string; description: string}>;
 131 |   serverInfo?: {
 132 |     name: string;
 133 |     title?: string;
 134 |     version: string;
 135 |     description?: string;
 136 |     websiteUrl?: string;
 137 |   };
 138 |   lastUsed: number;
 139 |   connectTime: number;
 140 |   executionCount: number;
 141 | }
 142 | 
 143 | interface MCPDefinition {
 144 |   name: string;
 145 |   config: MCPConfig;
 146 |   tools: Array<{name: string; description: string}>;
 147 |   serverInfo?: {
 148 |     name: string;
 149 |     title?: string;
 150 |     version: string;
 151 |     description?: string;
 152 |     websiteUrl?: string;
 153 |   };
 154 | }
 155 | 
 156 | export class NCPOrchestrator {
 157 |   private definitions: Map<string, MCPDefinition> = new Map();
 158 |   private connections: Map<string, MCPConnection> = new Map();
 159 |   private toolToMCP: Map<string, string> = new Map();
 160 |   private allTools: Array<{ name: string; description: string; mcpName: string }> = [];
 161 |   private profileName: string;
 162 |   private readonly QUICK_PROBE_TIMEOUT = 8000; // 8 seconds - first attempt
 163 |   private readonly SLOW_PROBE_TIMEOUT = 30000; // 30 seconds - retry for slow MCPs
 164 |   private readonly CONNECTION_TIMEOUT = 10000; // 10 seconds
 165 |   private readonly IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
 166 |   private readonly CLEANUP_INTERVAL = 60 * 1000; // Check every minute
 167 |   private cleanupTimer?: NodeJS.Timeout;
 168 |   private discovery: DiscoveryEngine;
 169 |   private healthMonitor: MCPHealthMonitor;
 170 |   private cachePatcher: CachePatcher;
 171 |   private csvCache: CSVCache;
 172 |   private showProgress: boolean;
 173 |   private indexingProgress: { current: number; total: number; currentMCP: string; estimatedTimeRemaining?: number } | null = null;
 174 |   private indexingStartTime: number = 0;
 175 |   private profileManager: ProfileManager | null = null;
 176 |   private internalMCPManager: InternalMCPManager;
 177 | 
 178 |   private forceRetry: boolean = false;
 179 | 
 180 |   /**
 181 |    * ⚠️ CRITICAL: Default profile MUST be 'all' - DO NOT CHANGE!
 182 |    *
 183 |    * The 'all' profile is the universal profile that contains all MCPs.
 184 |    * This default is used by MCPServer and all CLI commands.
 185 |    *
 186 |    * DO NOT change this to 'default' or any other name - it will break everything.
 187 |    */
 188 |   constructor(profileName: string = 'all', showProgress: boolean = false, forceRetry: boolean = false) {
 189 |     this.profileName = profileName;
 190 |     this.discovery = new DiscoveryEngine();
 191 |     this.healthMonitor = new MCPHealthMonitor();
 192 |     this.cachePatcher = new CachePatcher();
 193 |     this.csvCache = new CSVCache(getCacheDirectory(), profileName);
 194 |     this.showProgress = showProgress;
 195 |     this.forceRetry = forceRetry;
 196 |     this.internalMCPManager = new InternalMCPManager();
 197 |   }
 198 | 
 199 |   private async loadProfile(): Promise<Profile | null> {
 200 |     try {
 201 |       // Create and store ProfileManager instance (reused for auto-import)
 202 |       if (!this.profileManager) {
 203 |         this.profileManager = new ProfileManager();
 204 |         await this.profileManager.initialize();
 205 | 
 206 |         // Initialize internal MCPs with ProfileManager
 207 |         this.internalMCPManager.initialize(this.profileManager);
 208 |       }
 209 | 
 210 |       const profile = await this.profileManager.getProfile(this.profileName);
 211 | 
 212 |       if (!profile) {
 213 |         logger.error(`Profile not found: ${this.profileName}`);
 214 |         return null;
 215 |       }
 216 | 
 217 |       return profile;
 218 |     } catch (error: any) {
 219 |       logger.error(`Failed to load profile: ${error.message}`);
 220 |       return null;
 221 |     }
 222 |   }
 223 | 
 224 |   async initialize(): Promise<void> {
 225 |     const startTime = Date.now();
 226 |     this.indexingStartTime = startTime;
 227 | 
 228 |     // Debug logging
 229 |     if (process.env.NCP_DEBUG === 'true') {
 230 |       console.error(`[DEBUG ORC] Initializing with profileName: ${this.profileName}`);
 231 |       console.error(`[DEBUG ORC] Cache will use: ${this.csvCache ? 'csvCache exists' : 'NO CACHE'}`);
 232 |     }
 233 | 
 234 |     logger.info(`Initializing NCP orchestrator with profile: ${this.profileName}`);
 235 | 
 236 |     // Log runtime detection info (how NCP is running)
 237 |     if (process.env.NCP_DEBUG === 'true') {
 238 |       logRuntimeInfo();
 239 |     }
 240 | 
 241 |     // Initialize progress immediately to prevent race condition
 242 |     // Total will be updated once we know how many MCPs need indexing
 243 |     this.indexingProgress = {
 244 |       current: 0,
 245 |       total: 0,
 246 |       currentMCP: 'initializing...'
 247 |     };
 248 | 
 249 |     const profile = await this.loadProfile();
 250 | 
 251 |     if (process.env.NCP_DEBUG === 'true') {
 252 |       console.error(`[DEBUG ORC] Loaded profile: ${profile ? 'YES' : 'NO'}`);
 253 |       if (profile) {
 254 |         console.error(`[DEBUG ORC] Profile MCPs: ${Object.keys(profile.mcpServers || {}).join(', ')}`);
 255 |       }
 256 |     }
 257 | 
 258 |     if (!profile) {
 259 |       logger.error('Failed to load profile');
 260 |       this.indexingProgress = null;
 261 |       return;
 262 |     }
 263 | 
 264 |     // Initialize discovery engine first
 265 |     await this.discovery.initialize();
 266 | 
 267 |     // Initialize CSV cache
 268 |     await this.csvCache.initialize();
 269 | 
 270 |     // Get profile hash for cache validation
 271 |     const profileHash = CSVCache.hashProfile(profile.mcpServers);
 272 | 
 273 |     // Check if cache is valid
 274 |     const cacheValid = this.csvCache.validateCache(profileHash);
 275 | 
 276 |     const mcpConfigs: MCPConfig[] = Object.entries(profile.mcpServers).map(([name, config]) => ({
 277 |       name,
 278 |       command: config.command,
 279 |       args: config.args,
 280 |       env: config.env || {},
 281 |       url: config.url  // HTTP/SSE transport support
 282 |     }));
 283 | 
 284 |     if (cacheValid) {
 285 |       // Load from cache
 286 |       logger.info('Loading tools from CSV cache...');
 287 |       const cachedMCPCount = await this.loadFromCSVCache(mcpConfigs);
 288 |     } else {
 289 |       // Cache invalid - clear it to force full re-indexing
 290 |       logger.info('Cache invalid, clearing for full re-index...');
 291 |       await this.csvCache.clear();
 292 |       await this.csvCache.initialize();
 293 |     }
 294 | 
 295 |     // Get list of MCPs that need indexing
 296 |     const indexedMCPs = this.csvCache.getIndexedMCPs();
 297 |     const mcpsToIndex = mcpConfigs.filter(config => {
 298 |       const tools = profile.mcpServers[config.name];
 299 |       const currentHash = CSVCache.hashProfile(tools);
 300 | 
 301 |       // Check if already indexed
 302 |       if (this.csvCache.isMCPIndexed(config.name, currentHash)) {
 303 |         return false;
 304 |       }
 305 | 
 306 |       // Check if failed and should retry
 307 |       return this.csvCache.shouldRetryFailed(config.name, this.forceRetry);
 308 |     });
 309 | 
 310 |     if (mcpsToIndex.length > 0) {
 311 |       // Update progress tracking with actual count
 312 |       if (this.indexingProgress) {
 313 |         this.indexingProgress.total = mcpsToIndex.length;
 314 |       }
 315 | 
 316 |       if (this.showProgress) {
 317 |         const action = 'Indexing';
 318 |         const cachedCount = this.csvCache.getIndexedMCPs().size;
 319 | 
 320 |         // Count only failed MCPs that are NOT being retried in this run
 321 |         const allFailedCount = this.csvCache.getFailedMCPsCount();
 322 |         const retryingNowCount = mcpsToIndex.filter(config => {
 323 |           // Check if this MCP is in the failed list (being retried)
 324 |           return this.csvCache.isMCPFailed(config.name);
 325 |         }).length;
 326 |         const failedNotRetryingCount = allFailedCount - retryingNowCount;
 327 | 
 328 |         const totalProcessed = cachedCount + failedNotRetryingCount;
 329 |         const statusMsg = `${action} MCPs: ${totalProcessed}/${mcpConfigs.length}`;
 330 |         spinner.start(statusMsg);
 331 |         spinner.updateSubMessage('Initializing discovery engine...');
 332 |       }
 333 | 
 334 |       // Start incremental cache writing
 335 |       await this.csvCache.startIncrementalWrite(profileHash);
 336 | 
 337 |       // Index only the MCPs that need it
 338 |       await this.discoverMCPTools(mcpsToIndex, profile, true, mcpConfigs.length);
 339 | 
 340 |       // Finalize cache
 341 |       await this.csvCache.finalize();
 342 | 
 343 |       if (this.showProgress) {
 344 |         const successfulMCPs = this.definitions.size;
 345 |         const failedMCPs = this.csvCache.getFailedMCPsCount();
 346 |         const totalProcessed = successfulMCPs + failedMCPs;
 347 | 
 348 |         if (failedMCPs > 0) {
 349 |           spinner.success(`Indexed ${this.allTools.length} tools from ${successfulMCPs} MCPs | ${failedMCPs} failed (will retry later)`);
 350 |         } else {
 351 |           spinner.success(`Indexed ${this.allTools.length} tools from ${successfulMCPs} MCPs`);
 352 |         }
 353 |       }
 354 |     }
 355 | 
 356 |     // Clear progress tracking once complete
 357 |     this.indexingProgress = null;
 358 | 
 359 |     // Add internal MCPs to discovery
 360 |     this.addInternalMCPsToDiscovery();
 361 | 
 362 |     // Start cleanup timer for idle connections
 363 |     this.cleanupTimer = setInterval(
 364 |       () => this.cleanupIdleConnections(),
 365 |       this.CLEANUP_INTERVAL
 366 |     );
 367 | 
 368 |     const externalMCPs = this.definitions.size;
 369 |     const internalMCPs = this.internalMCPManager.getAllInternalMCPs().length;
 370 |     const loadTime = Date.now() - startTime;
 371 |     logger.info(`🚀 NCP-OSS initialized in ${loadTime}ms with ${this.allTools.length} tools from ${externalMCPs} external + ${internalMCPs} internal MCPs`);
 372 |   }
 373 | 
 374 |   /**
 375 |    * Load cached tools from CSV
 376 |    */
 377 |   private async loadFromCSVCache(mcpConfigs: MCPConfig[]): Promise<number> {
 378 |     const cachedTools = this.csvCache.loadCachedTools();
 379 | 
 380 |     // Group tools by MCP
 381 |     const toolsByMCP = new Map<string, CachedTool[]>();
 382 |     for (const tool of cachedTools) {
 383 |       if (!toolsByMCP.has(tool.mcpName)) {
 384 |         toolsByMCP.set(tool.mcpName, []);
 385 |       }
 386 |       toolsByMCP.get(tool.mcpName)!.push(tool);
 387 |     }
 388 | 
 389 |     let loadedMCPCount = 0;
 390 | 
 391 |     // Rebuild definitions and tool mappings from cache
 392 |     for (const config of mcpConfigs) {
 393 |       const mcpTools = toolsByMCP.get(config.name) || [];
 394 |       if (mcpTools.length === 0) continue;
 395 | 
 396 |       loadedMCPCount++;
 397 | 
 398 |       // Create definition
 399 |       this.definitions.set(config.name, {
 400 |         name: config.name,
 401 |         config,
 402 |         tools: mcpTools.map(t => ({
 403 |           name: t.toolName,
 404 |           description: t.description,
 405 |           inputSchema: {}
 406 |         })),
 407 |         serverInfo: undefined
 408 |       });
 409 | 
 410 |       // Add to all tools and create mappings
 411 |       for (const cachedTool of mcpTools) {
 412 |         const tool = {
 413 |           name: cachedTool.toolName,
 414 |           description: cachedTool.description,
 415 |           mcpName: config.name
 416 |         };
 417 |         this.allTools.push(tool);
 418 |         this.toolToMCP.set(cachedTool.toolId, config.name);
 419 |       }
 420 | 
 421 |       // Index tools in discovery engine
 422 |       const discoveryTools = mcpTools.map(t => ({
 423 |         id: t.toolId,
 424 |         name: t.toolName,
 425 |         description: t.description
 426 |       }));
 427 | 
 428 |       // Use async indexing to avoid blocking
 429 |       this.discovery.indexMCPTools(config.name, discoveryTools);
 430 |     }
 431 | 
 432 |     logger.info(`Loaded ${this.allTools.length} tools from CSV cache`);
 433 |     return loadedMCPCount;
 434 |   }
 435 | 
 436 |   private async discoverMCPTools(mcpConfigs: MCPConfig[], profile?: Profile, incrementalMode: boolean = false, totalMCPCount?: number): Promise<void> {
 437 |     // Only clear allTools if not in incremental mode
 438 |     if (!incrementalMode) {
 439 |       this.allTools = [];
 440 |     }
 441 | 
 442 |     const displayTotal = totalMCPCount || mcpConfigs.length;
 443 | 
 444 |     for (let i = 0; i < mcpConfigs.length; i++) {
 445 |       const config = mcpConfigs[i];
 446 |       try {
 447 |         logger.info(`Discovering tools from MCP: ${config.name}`);
 448 | 
 449 |         if (this.showProgress) {
 450 |           spinner.updateSubMessage(`Connecting to ${config.name}...`);
 451 |         }
 452 | 
 453 |         let result;
 454 |         try {
 455 |           // First attempt with quick timeout
 456 |           result = await this.probeMCPTools(config, this.QUICK_PROBE_TIMEOUT);
 457 |         } catch (firstError: any) {
 458 |           // If it timed out (not connection error), retry with longer timeout
 459 |           if (firstError.message.includes('Probe timeout') || firstError.message.includes('timeout')) {
 460 |             logger.debug(`${config.name} timed out on first attempt, retrying with longer timeout...`);
 461 |             if (this.showProgress) {
 462 |               spinner.updateSubMessage(`Retrying ${config.name} (heavy initialization)...`);
 463 |             }
 464 |             // Second attempt with slow timeout for heavy MCPs
 465 |             result = await this.probeMCPTools(config, this.SLOW_PROBE_TIMEOUT);
 466 |           } else {
 467 |             // Not a timeout - it's a real error (connection refused, etc), don't retry
 468 |             throw firstError;
 469 |           }
 470 |         }
 471 | 
 472 |         // Store definition with schema fallback applied
 473 |         this.definitions.set(config.name, {
 474 |           name: config.name,
 475 |           config,
 476 |           tools: result.tools.map(tool => ({
 477 |             ...tool,
 478 |             inputSchema: tool.inputSchema || {}
 479 |           })),
 480 |           serverInfo: result.serverInfo
 481 |         });
 482 | 
 483 |         // Add to all tools and create mappings
 484 |         const discoveryTools = [];
 485 |         for (const tool of result.tools) {
 486 |           // Store with prefixed name for consistency with commercial version
 487 |           const prefixedToolName = `${config.name}:${tool.name}`;
 488 |           const prefixedDescription = `${config.name}: ${tool.description || 'No description available'}`;
 489 | 
 490 |           this.allTools.push({
 491 |             name: prefixedToolName,
 492 |             description: prefixedDescription,
 493 |             mcpName: config.name
 494 |           });
 495 | 
 496 |           // Map both formats for backward compatibility
 497 |           this.toolToMCP.set(tool.name, config.name);
 498 |           this.toolToMCP.set(prefixedToolName, config.name);
 499 | 
 500 |           // Prepare for discovery engine indexing
 501 |           // Pass unprefixed name and description - RAG engine will add the prefix
 502 |           discoveryTools.push({
 503 |             id: prefixedToolName,
 504 |             name: tool.name,  // Use unprefixed name here
 505 |             description: tool.description || 'No description available',  // Use unprefixed description
 506 |             mcpServer: config.name,
 507 |             inputSchema: tool.inputSchema || {}
 508 |           });
 509 |         }
 510 | 
 511 |         if (this.showProgress) {
 512 |           // Add time estimate to indexing sub-message for parity
 513 |           let timeDisplay = '';
 514 |           if (this.indexingProgress?.estimatedTimeRemaining) {
 515 |             const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000);
 516 |             timeDisplay = ` (~${remainingSeconds}s remaining)`;
 517 |           }
 518 |           spinner.updateSubMessage(`Indexing ${result.tools.length} tools from ${config.name}...${timeDisplay}`);
 519 |         }
 520 | 
 521 |         // Index tools with discovery engine for vector search
 522 |         await this.discovery.indexMCPTools(config.name, discoveryTools);
 523 | 
 524 |         // Append to CSV cache incrementally (if in incremental mode)
 525 |         if (incrementalMode && profile) {
 526 |           const mcpHash = CSVCache.hashProfile(profile.mcpServers[config.name]);
 527 |           const cachedTools: CachedTool[] = result.tools.map(tool => ({
 528 |             mcpName: config.name,
 529 |             toolId: `${config.name}:${tool.name}`,
 530 |             toolName: tool.name,
 531 |             description: tool.description || 'No description available',
 532 |             hash: this.hashString(tool.description || ''),
 533 |             timestamp: new Date().toISOString()
 534 |           }));
 535 | 
 536 |           await this.csvCache.appendMCP(config.name, cachedTools, mcpHash);
 537 |         }
 538 | 
 539 |         // Update indexing progress AFTER successfully appending to cache
 540 |         if (this.indexingProgress) {
 541 |           this.indexingProgress.current = i + 1;
 542 |           this.indexingProgress.currentMCP = config.name;
 543 | 
 544 |           // Estimate remaining time based on average time per MCP so far
 545 |           const elapsedTime = Date.now() - this.indexingStartTime;
 546 |           const averageTimePerMCP = elapsedTime / (i + 1);
 547 |           const remainingMCPs = mcpConfigs.length - (i + 1);
 548 |           this.indexingProgress.estimatedTimeRemaining = remainingMCPs * averageTimePerMCP;
 549 |         }
 550 | 
 551 |         if (this.showProgress) {
 552 |           // Calculate absolute position
 553 |           const cachedCount = displayTotal - mcpConfigs.length;
 554 |           const currentAbsolute = cachedCount + (i + 1);
 555 |           const percentage = Math.round((currentAbsolute / displayTotal) * 100);
 556 | 
 557 |           // Add time estimate
 558 |           let timeDisplay = '';
 559 |           if (this.indexingProgress?.estimatedTimeRemaining) {
 560 |             const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000);
 561 |             timeDisplay = ` ~${remainingSeconds}s remaining`;
 562 |           }
 563 | 
 564 |           spinner.updateMessage(`Indexing MCPs: ${currentAbsolute}/${displayTotal} (${percentage}%)${timeDisplay}`);
 565 |         }
 566 | 
 567 |         logger.info(`Discovered ${result.tools.length} tools from ${config.name}`);
 568 |       } catch (error: any) {
 569 |         // Probe failures are expected - don't alarm users with error messages
 570 |         logger.debug(`Failed to discover tools from ${config.name}: ${error.message}`);
 571 | 
 572 |         // Mark MCP as failed for scheduled retry (if in incremental mode)
 573 |         if (incrementalMode && profile) {
 574 |           this.csvCache.markFailed(config.name, error);
 575 |         }
 576 | 
 577 |         // Update indexing progress even for failed MCPs
 578 |         if (this.indexingProgress) {
 579 |           this.indexingProgress.current = i + 1;
 580 |           this.indexingProgress.currentMCP = config.name;
 581 | 
 582 |           // Estimate remaining time based on average time per MCP so far
 583 |           const elapsedTime = Date.now() - this.indexingStartTime;
 584 |           const averageTimePerMCP = elapsedTime / (i + 1);
 585 |           const remainingMCPs = mcpConfigs.length - (i + 1);
 586 |           this.indexingProgress.estimatedTimeRemaining = remainingMCPs * averageTimePerMCP;
 587 |         }
 588 | 
 589 |         if (this.showProgress) {
 590 |           // Calculate absolute position
 591 |           const cachedCount = displayTotal - mcpConfigs.length;
 592 |           const currentAbsolute = cachedCount + (i + 1);
 593 |           const percentage = Math.round((currentAbsolute / displayTotal) * 100);
 594 | 
 595 |           // Add time estimate
 596 |           let timeDisplay = '';
 597 |           if (this.indexingProgress?.estimatedTimeRemaining) {
 598 |             const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000);
 599 |             timeDisplay = ` ~${remainingSeconds}s remaining`;
 600 |           }
 601 | 
 602 |           spinner.updateMessage(`Indexing MCPs: ${currentAbsolute}/${displayTotal} (${percentage}%)${timeDisplay}`);
 603 |           spinner.updateSubMessage(`Skipped ${config.name} (connection failed)`);
 604 |         }
 605 | 
 606 |         // Update health monitor with the actual error for import feedback
 607 |         this.healthMonitor.markUnhealthy(config.name, error.message);
 608 |       }
 609 |     }
 610 |   }
 611 | 
 612 |   /**
 613 |    * Create appropriate transport based on config
 614 |    * Supports both stdio (command/args) and HTTP/SSE (url) transports
 615 |    * Handles OAuth authentication for HTTP/SSE connections
 616 |    */
 617 |   private async createTransport(config: MCPConfig, env?: Record<string, string>): Promise<StdioClientTransport | SSEClientTransport> {
 618 |     if (config.url) {
 619 |       // HTTP/SSE transport (Claude Desktop native support)
 620 |       const url = new URL(config.url);
 621 |       const headers: Record<string, string> = {};
 622 | 
 623 |       // Handle authentication
 624 |       if (config.auth) {
 625 |         const token = await this.getAuthToken(config);
 626 | 
 627 |         switch (config.auth.type) {
 628 |           case 'oauth':
 629 |           case 'bearer':
 630 |             headers['Authorization'] = `Bearer ${token}`;
 631 |             break;
 632 |           case 'apiKey':
 633 |             // API key can be in header or query param - assume header for now
 634 |             headers['X-API-Key'] = token;
 635 |             break;
 636 |           case 'basic':
 637 |             if (config.auth.username && config.auth.password) {
 638 |               const credentials = Buffer.from(`${config.auth.username}:${config.auth.password}`).toString('base64');
 639 |               headers['Authorization'] = `Basic ${credentials}`;
 640 |             }
 641 |             break;
 642 |         }
 643 |       }
 644 | 
 645 |       // Use requestInit to add custom headers to POST requests
 646 |       // and eventSourceInit to add headers to the initial SSE connection
 647 |       const options = Object.keys(headers).length > 0 ? {
 648 |         requestInit: { headers },
 649 |         eventSourceInit: { headers } as EventSourceInit
 650 |       } : undefined;
 651 | 
 652 |       return new SSEClientTransport(url, options);
 653 |     }
 654 | 
 655 |     if (config.command) {
 656 |       // stdio transport (local process)
 657 |       const resolvedCommand = getRuntimeForExtension(config.command);
 658 |       const wrappedCommand = mcpWrapper.createWrapper(
 659 |         config.name,
 660 |         resolvedCommand,
 661 |         config.args || []
 662 |       );
 663 | 
 664 |       return new StdioClientTransport({
 665 |         command: wrappedCommand.command,
 666 |         args: wrappedCommand.args,
 667 |         env: env as Record<string, string>
 668 |       });
 669 |     }
 670 | 
 671 |     throw new Error(`Invalid config for ${config.name}: must have either 'command' or 'url'`);
 672 |   }
 673 | 
 674 |   /**
 675 |    * Get authentication token for MCP
 676 |    * Handles OAuth Device Flow and token refresh
 677 |    */
 678 |   private async getAuthToken(config: MCPConfig): Promise<string> {
 679 |     if (!config.auth) {
 680 |       throw new Error('No auth configuration provided');
 681 |     }
 682 | 
 683 |     // For non-OAuth auth types, return the token directly
 684 |     if (config.auth.type !== 'oauth') {
 685 |       return config.auth.token || '';
 686 |     }
 687 | 
 688 |     // OAuth flow
 689 |     if (!config.auth.oauth) {
 690 |       throw new Error('OAuth configuration missing');
 691 |     }
 692 | 
 693 |     const { getTokenStore } = await import('../auth/token-store.js');
 694 |     const tokenStore = getTokenStore();
 695 | 
 696 |     // Check for existing valid token
 697 |     const existingToken = await tokenStore.getToken(config.name);
 698 |     if (existingToken) {
 699 |       return existingToken.access_token;
 700 |     }
 701 | 
 702 |     // No valid token - trigger OAuth Device Flow
 703 |     const { DeviceFlowAuthenticator } = await import('../auth/oauth-device-flow.js');
 704 |     const authenticator = new DeviceFlowAuthenticator(config.auth.oauth);
 705 | 
 706 |     logger.info(`No valid token found for ${config.name}, starting OAuth Device Flow...`);
 707 |     const tokenResponse = await authenticator.authenticate();
 708 | 
 709 |     // Store token for future use
 710 |     await tokenStore.storeToken(config.name, tokenResponse);
 711 | 
 712 |     return tokenResponse.access_token;
 713 |   }
 714 | 
 715 |   // Based on commercial NCP's probeMCPTools method
 716 |   private async probeMCPTools(config: MCPConfig, timeout: number = this.QUICK_PROBE_TIMEOUT): Promise<{
 717 |     tools: Array<{name: string; description: string; inputSchema?: any}>;
 718 |     serverInfo?: {
 719 |       name: string;
 720 |       title?: string;
 721 |       version: string;
 722 |       description?: string;
 723 |       websiteUrl?: string;
 724 |     };
 725 |   }> {
 726 |     if (!config.command && !config.url) {
 727 |       throw new Error(`Invalid config for ${config.name}: must have either 'command' or 'url'`);
 728 |     }
 729 | 
 730 |     let client: Client | null = null;
 731 |     let transport: StdioClientTransport | SSEClientTransport | null = null;
 732 | 
 733 |     try {
 734 |       // Create temporary connection for discovery
 735 |       const silentEnv = {
 736 |         ...process.env,
 737 |         ...(config.env || {}),
 738 |         MCP_SILENT: 'true',
 739 |         QUIET: 'true',
 740 |         NO_COLOR: 'true'
 741 |       };
 742 | 
 743 |       transport = await this.createTransport(config, silentEnv);
 744 | 
 745 |       client = new Client(
 746 |         { name: 'ncp-oss', version: '1.0.0' },
 747 |         { capabilities: {} }
 748 |       );
 749 | 
 750 |       // Connect with timeout and filtered output
 751 |       await withFilteredOutput(async () => {
 752 |         await Promise.race([
 753 |           client!.connect(transport!),
 754 |           new Promise((_, reject) =>
 755 |             setTimeout(() => reject(new Error('Probe timeout')), timeout)
 756 |           )
 757 |         ]);
 758 |       });
 759 | 
 760 |       // Capture server info after connection
 761 |       const serverInfo = client!.getServerVersion();
 762 | 
 763 |       // Get tool list with filtered output
 764 |       const response = await withFilteredOutput(async () => {
 765 |         return await client!.listTools();
 766 |       });
 767 |       const tools = response.tools.map(t => ({
 768 |         name: t.name,
 769 |         description: t.description || '',
 770 |         inputSchema: t.inputSchema || {}
 771 |       }));
 772 | 
 773 |       // Disconnect immediately
 774 |       await client.close();
 775 | 
 776 |       return {
 777 |         tools,
 778 |         serverInfo: serverInfo ? {
 779 |           name: serverInfo.name || config.name,
 780 |           title: serverInfo.title,
 781 |           version: serverInfo.version || 'unknown',
 782 |           description: serverInfo.title || serverInfo.name || undefined,
 783 |           websiteUrl: serverInfo.websiteUrl
 784 |         } : undefined
 785 |       };
 786 | 
 787 |     } catch (error: any) {
 788 |       // Clean up on error
 789 |       if (client) {
 790 |         try { await client.close(); } catch {}
 791 |       }
 792 | 
 793 |       // Log full error details for debugging
 794 |       logger.debug(`Full error details for ${config.name}: ${JSON.stringify({
 795 |         message: error.message,
 796 |         code: error.code,
 797 |         data: error.data,
 798 |         stack: error.stack?.split('\n')[0]
 799 |       })}`);
 800 | 
 801 |       throw error;
 802 |     }
 803 |   }
 804 | 
 805 |   async find(query: string, limit: number = 5, detailed: boolean = false): Promise<DiscoveryResult[]> {
 806 |     if (!query) {
 807 |       // No query = list all tools, filtered by health
 808 |       const healthyTools = this.allTools.filter(tool => this.healthMonitor.getHealthyMCPs([tool.mcpName]).length > 0);
 809 |       const results = healthyTools.slice(0, limit).map(tool => {
 810 |         // Extract actual tool name from prefixed format
 811 |         const actualToolName = tool.name.includes(':') ? tool.name.split(':', 2)[1] : tool.name;
 812 |         return {
 813 |           toolName: tool.name, // Return prefixed name
 814 |           mcpName: tool.mcpName,
 815 |           confidence: 1.0,
 816 |           description: detailed ? tool.description : undefined,
 817 |           schema: detailed ? this.getToolSchema(tool.mcpName, actualToolName) : undefined
 818 |         };
 819 |       });
 820 |       return results;
 821 |     }
 822 | 
 823 |     // Use battle-tested vector search from commercial NCP
 824 |     // DOUBLE SEARCH TECHNIQUE: Request 2x results to account for filtering disabled MCPs
 825 |     try {
 826 |       const doubleLimit = limit * 2; // Request double to account for filtered MCPs
 827 |       const vectorResults = await this.discovery.findRelevantTools(query, doubleLimit);
 828 | 
 829 |       // Apply universal term frequency scoring boost
 830 |       const adjustedResults = this.adjustScoresUniversally(query, vectorResults);
 831 | 
 832 |       // Parse and filter results
 833 |       const parsedResults = adjustedResults.map(result => {
 834 |         // Parse tool format: "mcp:tool" or just "tool"
 835 |         const parts = result.name.includes(':') ? result.name.split(':', 2) : [this.toolToMCP.get(result.name) || 'unknown', result.name];
 836 |         const mcpName = parts[0];
 837 |         const toolName = parts[1] || result.name;
 838 | 
 839 |         // Find the tool - it should be stored with prefixed name
 840 |         const prefixedToolName = `${mcpName}:${toolName}`;
 841 |         const fullTool = this.allTools.find(t =>
 842 |           (t.name === prefixedToolName || t.name === toolName) && t.mcpName === mcpName
 843 |         );
 844 |         return {
 845 |           toolName: fullTool?.name || prefixedToolName, // Return the stored (prefixed) name
 846 |           mcpName,
 847 |           confidence: result.confidence,
 848 |           description: detailed ? fullTool?.description : undefined,
 849 |           schema: detailed ? this.getToolSchema(mcpName, toolName) : undefined
 850 |         };
 851 |       });
 852 | 
 853 |       // HEALTH FILTERING: Remove tools from disabled MCPs
 854 |       const healthyResults = parsedResults.filter(result => {
 855 |         return this.healthMonitor.getHealthyMCPs([result.mcpName]).length > 0;
 856 |       });
 857 | 
 858 |       // SORT by confidence (highest first) after our scoring adjustments
 859 |       const sortedResults = healthyResults.sort((a, b) => b.confidence - a.confidence);
 860 | 
 861 |       // Return up to the original limit after filtering and sorting
 862 |       const finalResults = sortedResults.slice(0, limit);
 863 | 
 864 |       if (healthyResults.length < parsedResults.length) {
 865 |         logger.debug(`Health filtering: ${parsedResults.length - healthyResults.length} tools filtered out from disabled MCPs`);
 866 |       }
 867 | 
 868 |       return finalResults;
 869 | 
 870 |     } catch (error: any) {
 871 |       logger.error(`Vector search failed: ${error.message}`);
 872 | 
 873 |       // Fallback to healthy tools only
 874 |       const healthyTools = this.allTools.filter(tool => this.healthMonitor.getHealthyMCPs([tool.mcpName]).length > 0);
 875 |       return healthyTools.slice(0, limit).map(tool => {
 876 |         // Extract actual tool name from prefixed format for schema lookup
 877 |         const actualToolName = tool.name.includes(':') ? tool.name.split(':', 2)[1] : tool.name;
 878 |         return {
 879 |           toolName: tool.name, // Return prefixed name
 880 |           mcpName: tool.mcpName,
 881 |           confidence: 0.5,
 882 |           description: detailed ? tool.description : undefined,
 883 |           schema: detailed ? this.getToolSchema(tool.mcpName, actualToolName) : undefined
 884 |         };
 885 |       });
 886 |     }
 887 |   }
 888 | 
 889 |   async run(toolName: string, parameters: any, meta?: Record<string, any>): Promise<ExecutionResult> {
 890 |     // Parse tool format: "mcp:tool" or just "tool"
 891 |     let mcpName: string;
 892 |     let actualToolName: string;
 893 | 
 894 |     if (toolName.includes(':')) {
 895 |       [mcpName, actualToolName] = toolName.split(':', 2);
 896 |     } else {
 897 |       actualToolName = toolName;
 898 |       mcpName = this.toolToMCP.get(toolName) || '';
 899 |     }
 900 | 
 901 |     if (!mcpName) {
 902 |       const similarTools = this.findSimilarTools(toolName);
 903 |       let errorMessage = `Tool '${toolName}' not found.`;
 904 | 
 905 |       if (similarTools.length > 0) {
 906 |         errorMessage += ` Did you mean: ${similarTools.join(', ')}?`;
 907 |       }
 908 | 
 909 |       errorMessage += ` Use 'ncp find "${toolName}"' to search for similar tools or 'ncp find --depth 0' to list all available tools.`;
 910 | 
 911 |       return {
 912 |         success: false,
 913 |         error: errorMessage
 914 |       };
 915 |     }
 916 | 
 917 |     // Check if this is an internal MCP
 918 |     if (this.internalMCPManager.isInternalMCP(mcpName)) {
 919 |       try {
 920 |         const result = await this.internalMCPManager.executeInternalTool(mcpName, actualToolName, parameters);
 921 |         return {
 922 |           success: result.success,
 923 |           content: result.content,
 924 |           error: result.error
 925 |         };
 926 |       } catch (error: any) {
 927 |         logger.error(`Internal tool execution failed for ${toolName}:`, error);
 928 |         return {
 929 |           success: false,
 930 |           error: error.message || 'Internal tool execution failed'
 931 |         };
 932 |       }
 933 |     }
 934 | 
 935 |     const definition = this.definitions.get(mcpName);
 936 |     if (!definition) {
 937 |       const availableMcps = Array.from(this.definitions.keys()).join(', ');
 938 |       return {
 939 |         success: false,
 940 |         error: `MCP '${mcpName}' not found. Available MCPs: ${availableMcps}. Use 'ncp find' to discover tools or check your profile configuration.`
 941 |       };
 942 |     }
 943 | 
 944 |     try {
 945 |       // Get or create pooled connection
 946 |       const connection = await this.getOrCreateConnection(mcpName);
 947 | 
 948 |       // Validate parameters before execution
 949 |       const validationError = this.validateToolParameters(mcpName, actualToolName, parameters);
 950 |       if (validationError) {
 951 |         return {
 952 |           success: false,
 953 |           error: validationError
 954 |         };
 955 |       }
 956 | 
 957 |       // Execute tool with filtered output to suppress MCP server console messages
 958 |       // Forward _meta transparently to support session_id and other protocol-level metadata
 959 |       const result = await withFilteredOutput(async () => {
 960 |         return await connection.client.callTool({
 961 |           name: actualToolName,
 962 |           arguments: parameters,
 963 |           _meta: meta
 964 |         });
 965 |       });
 966 | 
 967 |       // Mark MCP as healthy on successful execution
 968 |       this.healthMonitor.markHealthy(mcpName);
 969 | 
 970 |       return {
 971 |         success: true,
 972 |         content: result.content
 973 |       };
 974 | 
 975 |     } catch (error: any) {
 976 |       logger.error(`Tool execution failed for ${toolName}:`, error);
 977 | 
 978 |       // Mark MCP as unhealthy on execution failure
 979 |       this.healthMonitor.markUnhealthy(mcpName, error.message);
 980 | 
 981 |       return {
 982 |         success: false,
 983 |         error: this.enhanceErrorMessage(error, actualToolName, mcpName)
 984 |       };
 985 |     }
 986 |   }
 987 | 
 988 |   private async getOrCreateConnection(mcpName: string): Promise<MCPConnection> {
 989 |     // Return existing connection if available
 990 |     const existing = this.connections.get(mcpName);
 991 |     if (existing) {
 992 |       existing.lastUsed = Date.now();
 993 |       existing.executionCount++;
 994 |       return existing;
 995 |     }
 996 | 
 997 |     const definition = this.definitions.get(mcpName);
 998 |     if (!definition) {
 999 |       const availableMcps = Array.from(this.definitions.keys()).join(', ');
1000 |       throw new Error(`MCP '${mcpName}' not found. Available MCPs: ${availableMcps}. Use 'ncp find' to discover tools or check your profile configuration.`);
1001 |     }
1002 | 
1003 |     logger.info(`🔌 Connecting to ${mcpName} (for execution)...`);
1004 |     const connectStart = Date.now();
1005 | 
1006 |     try {
1007 |       // Add environment variables
1008 |       const silentEnv = {
1009 |         ...process.env,
1010 |         ...(definition.config.env || {}),
1011 |         // These may still help some servers
1012 |         MCP_SILENT: 'true',
1013 |         QUIET: 'true',
1014 |         NO_COLOR: 'true'
1015 |       };
1016 | 
1017 |       const transport = await this.createTransport(definition.config, silentEnv);
1018 | 
1019 |       const client = new Client(
1020 |         { name: 'ncp-oss', version: '1.0.0' },
1021 |         { capabilities: {} }
1022 |       );
1023 | 
1024 |       // Connect with timeout and filtered output
1025 |       await withFilteredOutput(async () => {
1026 |         await Promise.race([
1027 |           client.connect(transport),
1028 |           new Promise((_, reject) =>
1029 |             setTimeout(() => reject(new Error('Connection timeout')), this.CONNECTION_TIMEOUT)
1030 |           )
1031 |         ]);
1032 |       });
1033 | 
1034 |       // Capture server info after successful connection
1035 |       const serverInfo = client.getServerVersion();
1036 | 
1037 |       const connection: MCPConnection = {
1038 |         client,
1039 |         transport,
1040 |         tools: [], // Will be populated if needed
1041 |         serverInfo: serverInfo ? {
1042 |           name: serverInfo.name || mcpName,
1043 |           title: serverInfo.title,
1044 |           version: serverInfo.version || 'unknown',
1045 |           description: serverInfo.title || serverInfo.name || undefined,
1046 |           websiteUrl: serverInfo.websiteUrl
1047 |         } : undefined,
1048 |         lastUsed: Date.now(),
1049 |         connectTime: Date.now() - connectStart,
1050 |         executionCount: 1
1051 |       };
1052 | 
1053 |       // Store connection for reuse
1054 |       this.connections.set(mcpName, connection);
1055 |       logger.info(`✅ Connected to ${mcpName} in ${connection.connectTime}ms`);
1056 | 
1057 |       return connection;
1058 |     } catch (error: any) {
1059 |       logger.error(`❌ Failed to connect to ${mcpName}: ${error.message}`);
1060 |       throw error;
1061 |     }
1062 |   }
1063 | 
1064 |   /**
1065 |    * New optimized cache loading with profile hash validation
1066 |    * This is the key optimization - skips re-indexing when profile hasn't changed
1067 |    */
1068 |   private async loadFromOptimizedCache(profile: Profile): Promise<boolean> {
1069 |     try {
1070 |       // 1. Validate cache integrity first
1071 |       const integrity = await this.cachePatcher.validateAndRepairCache();
1072 |       if (!integrity.valid) {
1073 |         logger.warn('Cache integrity check failed - rebuilding required');
1074 |         return false;
1075 |       }
1076 | 
1077 |       // 2. Check if cache is valid using profile hash validation
1078 |       const currentProfileHash = this.cachePatcher.generateProfileHash(profile);
1079 |       const cacheIsValid = await this.cachePatcher.validateCacheWithProfile(currentProfileHash);
1080 | 
1081 |       if (!cacheIsValid) {
1082 |         logger.info('Cache invalid or missing - profile changed');
1083 |         return false;
1084 |       }
1085 | 
1086 |       // 3. Load tool metadata cache directly
1087 |       const toolMetadataCache = await this.cachePatcher.loadToolMetadataCache();
1088 | 
1089 |       if (!toolMetadataCache.mcps || Object.keys(toolMetadataCache.mcps).length === 0) {
1090 |         logger.info('Tool metadata cache empty');
1091 |         return false;
1092 |       }
1093 | 
1094 |       logger.info(`✅ Using valid cache (${Object.keys(toolMetadataCache.mcps).length} MCPs, hash: ${currentProfileHash.substring(0, 8)}...)`);
1095 | 
1096 |       // 4. Load MCPs and tools from cache directly (no re-indexing)
1097 |       this.allTools = [];
1098 |       let loadedMCPCount = 0;
1099 |       let loadedToolCount = 0;
1100 | 
1101 |       for (const [mcpName, mcpData] of Object.entries(toolMetadataCache.mcps)) {
1102 |         try {
1103 |           // Validate MCP data structure
1104 |           if (!mcpData.tools || !Array.isArray(mcpData.tools)) {
1105 |             logger.warn(`Skipping ${mcpName}: invalid tools data in cache`);
1106 |             continue;
1107 |           }
1108 | 
1109 |           // Check if MCP still exists in current profile
1110 |           if (!profile.mcpServers[mcpName]) {
1111 |             logger.debug(`Skipping ${mcpName}: removed from profile`);
1112 |             continue;
1113 |           }
1114 | 
1115 |           this.definitions.set(mcpName, {
1116 |             name: mcpName,
1117 |             config: {
1118 |               name: mcpName,
1119 |               ...profile.mcpServers[mcpName]
1120 |             },
1121 |             tools: mcpData.tools.map(tool => ({
1122 |               ...tool,
1123 |               inputSchema: tool.inputSchema || {}
1124 |             })),
1125 |             serverInfo: mcpData.serverInfo || { name: mcpName, version: '1.0.0' }
1126 |           });
1127 | 
1128 |           // Build allTools array and tool mappings
1129 |           const discoveryTools = [];
1130 |           for (const tool of mcpData.tools) {
1131 |             try {
1132 |               const prefixedToolName = `${mcpName}:${tool.name}`;
1133 |               const prefixedDescription = tool.description.startsWith(`${mcpName}:`)
1134 |                 ? tool.description
1135 |                 : `${mcpName}: ${tool.description || 'No description available'}`;
1136 | 
1137 |               this.allTools.push({
1138 |                 name: prefixedToolName,
1139 |                 description: prefixedDescription,
1140 |                 mcpName: mcpName
1141 |               });
1142 | 
1143 |               // Create tool mappings
1144 |               this.toolToMCP.set(tool.name, mcpName);
1145 |               this.toolToMCP.set(prefixedToolName, mcpName);
1146 | 
1147 |               discoveryTools.push({
1148 |                 id: prefixedToolName,
1149 |                 name: tool.name,
1150 |                 description: prefixedDescription,
1151 |                 mcpServer: mcpName,
1152 |                 inputSchema: tool.inputSchema || {}
1153 |               });
1154 | 
1155 |               loadedToolCount++;
1156 |             } catch (toolError: any) {
1157 |               logger.warn(`Error loading tool ${tool.name} from ${mcpName}: ${toolError.message}`);
1158 |             }
1159 |           }
1160 | 
1161 |           // Use fast indexing (load from embeddings cache, don't regenerate)
1162 |           if (discoveryTools.length > 0) {
1163 |             // Ensure discovery engine is fully initialized before indexing
1164 |             await this.discovery.initialize();
1165 |             await this.discovery.indexMCPToolsFromCache(mcpName, discoveryTools);
1166 |             loadedMCPCount++;
1167 |           }
1168 | 
1169 |         } catch (mcpError: any) {
1170 |           logger.warn(`Error loading MCP ${mcpName} from cache: ${mcpError.message}`);
1171 |         }
1172 |       }
1173 | 
1174 |       if (loadedMCPCount === 0) {
1175 |         logger.warn('No valid MCPs loaded from cache');
1176 |         return false;
1177 |       }
1178 | 
1179 |       logger.info(`⚡ Loaded ${loadedToolCount} tools from ${loadedMCPCount} MCPs (optimized cache)`);
1180 |       return true;
1181 | 
1182 |     } catch (error: any) {
1183 |       logger.warn(`Optimized cache load failed: ${error.message}`);
1184 |       return false;
1185 |     }
1186 |   }
1187 | 
1188 |   /**
1189 |    * Legacy cache loading (kept for fallback)
1190 |    */
1191 |   private async loadFromCache(profile: Profile): Promise<boolean> {
1192 |     try {
1193 |       const cacheDir = getCacheDirectory();
1194 |       const cachePath = join(cacheDir, `${this.profileName}-tools.json`);
1195 | 
1196 |       if (!existsSync(cachePath)) {
1197 |         return false;
1198 |       }
1199 | 
1200 |       const content = readFileSync(cachePath, 'utf-8');
1201 |       const cache = JSON.parse(content);
1202 | 
1203 |       // Use cache if less than 24 hours old
1204 |       if (Date.now() - cache.timestamp > 24 * 60 * 60 * 1000) {
1205 |         logger.info('Cache expired, will refresh tools');
1206 |         return false;
1207 |       }
1208 | 
1209 |       logger.info(`Using cached tools (${Object.keys(cache.mcps).length} MCPs)`)
1210 | 
1211 |       // Load MCPs and tools from cache
1212 |       for (const [mcpName, mcpData] of Object.entries(cache.mcps)) {
1213 |         const data = mcpData as any;
1214 | 
1215 |         this.definitions.set(mcpName, {
1216 |           name: mcpName,
1217 |           config: {
1218 |             name: mcpName,
1219 |             ...profile.mcpServers[mcpName]
1220 |           },
1221 |           tools: data.tools || [],
1222 |           serverInfo: data.serverInfo
1223 |         });
1224 | 
1225 |         // Add tools to allTools and create mappings
1226 |         const discoveryTools = [];
1227 |         for (const tool of data.tools || []) {
1228 |           // Handle both old (unprefixed) and new (prefixed) formats in cache
1229 |           const isAlreadyPrefixed = tool.name.startsWith(`${mcpName}:`);
1230 |           const prefixedToolName = isAlreadyPrefixed ? tool.name : `${mcpName}:${tool.name}`;
1231 |           const actualToolName = isAlreadyPrefixed ? tool.name.substring(mcpName.length + 1) : tool.name;
1232 | 
1233 |           // Ensure description is prefixed
1234 |           const hasPrefixedDesc = tool.description?.startsWith(`${mcpName}: `);
1235 |           const prefixedDescription = hasPrefixedDesc ? tool.description : `${mcpName}: ${tool.description || 'No description available'}`;
1236 | 
1237 |           this.allTools.push({
1238 |             name: prefixedToolName,
1239 |             description: prefixedDescription,
1240 |             mcpName: mcpName
1241 |           });
1242 | 
1243 |           // Map both formats for backward compatibility
1244 |           this.toolToMCP.set(actualToolName, mcpName);
1245 |           this.toolToMCP.set(prefixedToolName, mcpName);
1246 | 
1247 |           // Prepare for discovery engine indexing
1248 |           // Pass unprefixed name - RAG engine will add the prefix
1249 |           discoveryTools.push({
1250 |             id: prefixedToolName,
1251 |             name: actualToolName,  // Use unprefixed name here
1252 |             description: prefixedDescription,
1253 |             mcpServer: mcpName,
1254 |             inputSchema: {}
1255 |           });
1256 |         }
1257 | 
1258 |         // Index tools with discovery engine
1259 |         await this.discovery.indexMCPTools(mcpName, discoveryTools);
1260 |       }
1261 | 
1262 |       logger.info(`✅ Loaded ${this.allTools.length} tools from cache`);
1263 |       return true;
1264 | 
1265 |     } catch (error: any) {
1266 |       logger.warn(`Cache load failed: ${error.message}`);
1267 |       return false;
1268 |     }
1269 |   }
1270 | 
1271 |   private async saveToCache(profile: Profile): Promise<void> {
1272 |     try {
1273 |       // Use new optimized cache saving with profile hash
1274 |       await this.saveToOptimizedCache(profile);
1275 | 
1276 |     } catch (error: any) {
1277 |       logger.warn(`Cache save failed: ${error.message}`);
1278 |     }
1279 |   }
1280 | 
1281 |   /**
1282 |    * New optimized cache saving with profile hash and structured format
1283 |    */
1284 |   private async saveToOptimizedCache(profile: Profile): Promise<void> {
1285 |     try {
1286 |       logger.info('💾 Saving tools to optimized cache...');
1287 | 
1288 |       // Save all MCP definitions to tool metadata cache
1289 |       for (const [mcpName, definition] of this.definitions.entries()) {
1290 |         const mcpConfig = profile.mcpServers[mcpName];
1291 |         if (mcpConfig) {
1292 |           await this.cachePatcher.patchAddMCP(
1293 |             mcpName,
1294 |             mcpConfig,
1295 |             definition.tools,
1296 |             definition.serverInfo
1297 |           );
1298 |         }
1299 |       }
1300 | 
1301 |       // Update profile hash
1302 |       const profileHash = this.cachePatcher.generateProfileHash(profile);
1303 |       await this.cachePatcher.updateProfileHash(profileHash);
1304 | 
1305 |       logger.info(`💾 Saved ${this.allTools.length} tools to optimized cache with profile hash: ${profileHash.substring(0, 8)}...`);
1306 | 
1307 |     } catch (error: any) {
1308 |       logger.error(`Optimized cache save failed: ${error.message}`);
1309 |       throw error;
1310 |     }
1311 |   }
1312 | 
1313 |   private getToolSchema(mcpName: string, toolName: string): any {
1314 |     const connection = this.connections.get(mcpName);
1315 |     if (!connection) {
1316 |       // No persistent connection, try to get schema from definitions
1317 |       const definition = this.definitions.get(mcpName);
1318 |       if (!definition) return undefined;
1319 | 
1320 |       const tool = definition.tools.find(t => t.name === toolName);
1321 |       return tool ? (tool as any).inputSchema : undefined;
1322 |     }
1323 | 
1324 |     const tool = connection.tools.find(t => t.name === toolName);
1325 |     if (!tool) return undefined;
1326 | 
1327 |     return (tool as any).inputSchema;
1328 |   }
1329 | 
1330 |   /**
1331 |    * Check if a tool requires parameters
1332 |    */
1333 |   toolRequiresParameters(toolIdentifier: string): boolean {
1334 |     const [mcpName, toolName] = toolIdentifier.split(':');
1335 |     if (!mcpName || !toolName) return false;
1336 | 
1337 |     const schema = this.getToolSchema(mcpName, toolName);
1338 |     return ToolSchemaParser.hasRequiredParameters(schema);
1339 |   }
1340 | 
1341 |   /**
1342 |    * Get tool parameters for interactive prompting
1343 |    */
1344 |   getToolParameters(toolIdentifier: string): ParameterInfo[] {
1345 |     const [mcpName, toolName] = toolIdentifier.split(':');
1346 |     if (!mcpName || !toolName) return [];
1347 | 
1348 |     const schema = this.getToolSchema(mcpName, toolName);
1349 |     return ToolSchemaParser.parseParameters(schema);
1350 |   }
1351 | 
1352 |   /**
1353 |    * Validate tool parameters before execution
1354 |    */
1355 |   private validateToolParameters(mcpName: string, toolName: string, parameters: any): string | null {
1356 |     const schema = this.getToolSchema(mcpName, toolName);
1357 |     if (!schema) {
1358 |       // No schema available, allow execution (tool may not require validation)
1359 |       return null;
1360 |     }
1361 | 
1362 |     const requiredParams = ToolSchemaParser.getRequiredParameters(schema);
1363 |     const missingParams: string[] = [];
1364 | 
1365 |     // Check for missing required parameters
1366 |     for (const param of requiredParams) {
1367 |       if (parameters === null || parameters === undefined || !(param.name in parameters) || parameters[param.name] === null || parameters[param.name] === undefined || parameters[param.name] === '') {
1368 |         missingParams.push(param.name);
1369 |       }
1370 |     }
1371 | 
1372 |     if (missingParams.length > 0) {
1373 |       return `Missing required parameters: ${missingParams.join(', ')}. Use 'ncp find "${mcpName}:${toolName}" --depth 2' to see parameter details.`;
1374 |     }
1375 | 
1376 |     return null; // Validation passed
1377 |   }
1378 | 
1379 |   /**
1380 |    * Get tool context for parameter prediction
1381 |    */
1382 |   getToolContext(toolIdentifier: string): string {
1383 |     return ToolContextResolver.getContext(toolIdentifier);
1384 |   }
1385 | 
1386 |   /**
1387 |    * Find similar tool names using fuzzy matching
1388 |    */
1389 |   private findSimilarTools(targetTool: string, maxSuggestions: number = 3): string[] {
1390 |     const allTools = Array.from(this.toolToMCP.keys());
1391 |     const similarities = allTools.map(tool => ({
1392 |       tool,
1393 |       similarity: calculateSimilarity(targetTool, tool)
1394 |     }));
1395 | 
1396 |     return similarities
1397 |       .filter(item => item.similarity > 0.4) // Only suggest if reasonably similar
1398 |       .sort((a, b) => b.similarity - a.similarity)
1399 |       .slice(0, maxSuggestions)
1400 |       .map(item => item.tool);
1401 |   }
1402 | 
1403 |   /**
1404 |    * Generate hash for each MCP configuration
1405 |    */
1406 |   private generateConfigHashes(profile: Profile): Record<string, string> {
1407 |     const hashes: Record<string, string> = {};
1408 |     const crypto = require('crypto');
1409 | 
1410 |     for (const [mcpName, config] of Object.entries(profile.mcpServers)) {
1411 |       // Hash command + args + env + url for change detection
1412 |       const configString = JSON.stringify({
1413 |         command: config.command,
1414 |         args: config.args || [],
1415 |         env: config.env || {},
1416 |         url: config.url  // Include HTTP/SSE URL in hash
1417 |       });
1418 | 
1419 |       hashes[mcpName] = crypto.createHash('sha256').update(configString).digest('hex');
1420 |     }
1421 | 
1422 |     return hashes;
1423 |   }
1424 | 
1425 |   /**
1426 |    * Get current indexing progress
1427 |    */
1428 |   getIndexingProgress(): { current: number; total: number; currentMCP: string; estimatedTimeRemaining?: number } | null {
1429 |     return this.indexingProgress;
1430 |   }
1431 | 
1432 |   /**
1433 |    * Get MCP health status summary
1434 |    */
1435 |   getMCPHealthStatus(): { total: number; healthy: number; unhealthy: number; mcps: Array<{name: string; healthy: boolean}> } {
1436 |     const allMCPs = Array.from(this.definitions.keys());
1437 |     const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs);
1438 | 
1439 |     const mcpStatus = allMCPs.map(mcp => ({
1440 |       name: mcp,
1441 |       healthy: healthyMCPs.includes(mcp)
1442 |     }));
1443 | 
1444 |     return {
1445 |       total: allMCPs.length,
1446 |       healthy: healthyMCPs.length,
1447 |       unhealthy: allMCPs.length - healthyMCPs.length,
1448 |       mcps: mcpStatus
1449 |     };
1450 |   }
1451 | 
1452 |   /**
1453 |    * Enhance generic error messages with better context
1454 |    */
1455 |   private enhanceErrorMessage(error: any, toolName: string, mcpName: string): string {
1456 |     const errorMessage = error.message || error.toString() || 'Unknown error';
1457 | 
1458 |     // Always provide context and actionable guidance, regardless of specific error patterns
1459 |     let enhancedMessage = `Tool '${toolName}' failed in MCP '${mcpName}': ${errorMessage}`;
1460 | 
1461 |     // Add generic troubleshooting guidance
1462 |     const troubleshootingTips = [
1463 |       `• Check MCP '${mcpName}' status and configuration`,
1464 |       `• Use 'ncp find "${mcpName}:${toolName}" --depth 2' to verify tool parameters`,
1465 |       `• Ensure MCP server is running and accessible`
1466 |     ];
1467 | 
1468 |     enhancedMessage += `\n\nTroubleshooting:\n${troubleshootingTips.join('\n')}`;
1469 | 
1470 |     return enhancedMessage;
1471 |   }
1472 | 
1473 |   /**
1474 |    * Get all resources from active MCPs
1475 |    */
1476 |   async getAllResources(): Promise<Array<any>> {
1477 |     const resources: Array<any> = [];
1478 |     const allMCPs = Array.from(this.definitions.keys());
1479 |     const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs);
1480 | 
1481 |     for (const mcpName of healthyMCPs) {
1482 |       try {
1483 |         const mcpResources = await this.getResourcesFromMCP(mcpName);
1484 |         if (mcpResources && Array.isArray(mcpResources)) {
1485 |           // Add MCP source information to each resource with prefix
1486 |           const enrichedResources = mcpResources.map(resource => ({
1487 |             ...resource,
1488 |             name: `${mcpName}:${resource.name}`, // Add MCP prefix
1489 |             _source: mcpName
1490 |           }));
1491 |           resources.push(...enrichedResources);
1492 |         }
1493 |       } catch (error) {
1494 |         logger.warn(`Failed to get resources from ${mcpName}: ${error}`);
1495 |       }
1496 |     }
1497 | 
1498 |     return resources;
1499 |   }
1500 | 
1501 |   /**
1502 |    * Get all prompts from active MCPs
1503 |    */
1504 |   async getAllPrompts(): Promise<Array<any>> {
1505 |     const prompts: Array<any> = [];
1506 |     const allMCPs = Array.from(this.definitions.keys());
1507 |     const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs);
1508 | 
1509 |     for (const mcpName of healthyMCPs) {
1510 |       try {
1511 |         const mcpPrompts = await this.getPromptsFromMCP(mcpName);
1512 |         if (mcpPrompts && Array.isArray(mcpPrompts)) {
1513 |           // Add MCP source information to each prompt with prefix
1514 |           const enrichedPrompts = mcpPrompts.map(prompt => ({
1515 |             ...prompt,
1516 |             name: `${mcpName}:${prompt.name}`, // Add MCP prefix
1517 |             _source: mcpName
1518 |           }));
1519 |           prompts.push(...enrichedPrompts);
1520 |         }
1521 |       } catch (error) {
1522 |         logger.warn(`Failed to get prompts from ${mcpName}: ${error}`);
1523 |       }
1524 |     }
1525 | 
1526 |     return prompts;
1527 |   }
1528 | 
1529 |   /**
1530 |    * Get resources from a specific MCP
1531 |    */
1532 |   private async getResourcesFromMCP(mcpName: string): Promise<Array<any>> {
1533 |     try {
1534 |       const definition = this.definitions.get(mcpName);
1535 |       if (!definition) {
1536 |         return [];
1537 |       }
1538 | 
1539 |       // Create temporary connection for resources request
1540 |       const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
1541 | 
1542 |       const silentEnv = {
1543 |         ...process.env,
1544 |         ...(definition.config.env || {}),
1545 |         MCP_SILENT: 'true',
1546 |         QUIET: 'true',
1547 |         NO_COLOR: 'true'
1548 |       };
1549 | 
1550 |       const transport = await this.createTransport(definition.config, silentEnv);
1551 | 
1552 |       const client = new Client(
1553 |         { name: 'ncp-oss-resources', version: '1.0.0' },
1554 |         { capabilities: {} }
1555 |       );
1556 | 
1557 |       // Connect with timeout and filtered output
1558 |       await withFilteredOutput(async () => {
1559 |         await Promise.race([
1560 |           client.connect(transport),
1561 |           new Promise((_, reject) =>
1562 |             setTimeout(() => reject(new Error('Resources connection timeout')), this.QUICK_PROBE_TIMEOUT)
1563 |           )
1564 |         ]);
1565 |       });
1566 | 
1567 |       // Get resources list with filtered output
1568 |       const response = await withFilteredOutput(async () => {
1569 |         return await client.listResources();
1570 |       });
1571 |       await client.close();
1572 | 
1573 |       return response.resources || [];
1574 | 
1575 |     } catch (error) {
1576 |       logger.debug(`Resources probe failed for ${mcpName}: ${error}`);
1577 |       return [];
1578 |     }
1579 |   }
1580 | 
1581 |   /**
1582 |    * Get prompts from a specific MCP
1583 |    */
1584 |   private async getPromptsFromMCP(mcpName: string): Promise<Array<any>> {
1585 |     try {
1586 |       const definition = this.definitions.get(mcpName);
1587 |       if (!definition) {
1588 |         return [];
1589 |       }
1590 | 
1591 |       // Create temporary connection for prompts request
1592 |       const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
1593 | 
1594 |       const silentEnv = {
1595 |         ...process.env,
1596 |         ...(definition.config.env || {}),
1597 |         MCP_SILENT: 'true',
1598 |         QUIET: 'true',
1599 |         NO_COLOR: 'true'
1600 |       };
1601 | 
1602 |       const transport = await this.createTransport(definition.config, silentEnv);
1603 | 
1604 |       const client = new Client(
1605 |         { name: 'ncp-oss-prompts', version: '1.0.0' },
1606 |         { capabilities: {} }
1607 |       );
1608 | 
1609 |       // Connect with timeout and filtered output
1610 |       await withFilteredOutput(async () => {
1611 |         await Promise.race([
1612 |           client.connect(transport),
1613 |           new Promise((_, reject) =>
1614 |             setTimeout(() => reject(new Error('Prompts connection timeout')), this.QUICK_PROBE_TIMEOUT)
1615 |           )
1616 |         ]);
1617 |       });
1618 | 
1619 |       // Get prompts list with filtered output
1620 |       const response = await withFilteredOutput(async () => {
1621 |         return await client.listPrompts();
1622 |       });
1623 |       await client.close();
1624 | 
1625 |       return response.prompts || [];
1626 | 
1627 |     } catch (error) {
1628 |       logger.debug(`Prompts probe failed for ${mcpName}: ${error}`);
1629 |       return [];
1630 |     }
1631 |   }
1632 | 
1633 |   /**
1634 |    * Clean up idle connections (like commercial version)
1635 |    */
1636 |   private async cleanupIdleConnections(): Promise<void> {
1637 |     const now = Date.now();
1638 |     const toDisconnect: string[] = [];
1639 | 
1640 |     for (const [name, connection] of this.connections) {
1641 |       const idleTime = now - connection.lastUsed;
1642 | 
1643 |       if (idleTime > this.IDLE_TIMEOUT) {
1644 |         logger.info(`🧹 Disconnecting idle MCP: ${name} (idle for ${Math.round(idleTime / 1000)}s)`);
1645 |         toDisconnect.push(name);
1646 |       }
1647 |     }
1648 | 
1649 |     // Disconnect idle connections
1650 |     for (const name of toDisconnect) {
1651 |       await this.disconnectMCP(name);
1652 |     }
1653 |   }
1654 | 
1655 |   /**
1656 |    * Disconnect a specific MCP
1657 |    */
1658 |   private async disconnectMCP(mcpName: string): Promise<void> {
1659 |     const connection = this.connections.get(mcpName);
1660 |     if (!connection) return;
1661 | 
1662 |     try {
1663 |       await connection.client.close();
1664 |       this.connections.delete(mcpName);
1665 |       logger.debug(`Disconnected ${mcpName}`);
1666 |     } catch (error) {
1667 |       logger.error(`Error disconnecting ${mcpName}:`, error);
1668 |     }
1669 |   }
1670 | 
1671 |   async cleanup(): Promise<void> {
1672 |     logger.info('Shutting down NCP Orchestrator...');
1673 | 
1674 |     // Stop cleanup timer
1675 |     if (this.cleanupTimer) {
1676 |       clearInterval(this.cleanupTimer);
1677 |     }
1678 | 
1679 |     // Finalize cache if it's being written
1680 |     if (this.csvCache) {
1681 |       try {
1682 |         await this.csvCache.finalize();
1683 |       } catch (error) {
1684 |         // Ignore finalize errors
1685 |       }
1686 |     }
1687 | 
1688 |     // Stop progress spinner if active
1689 |     if (this.showProgress) {
1690 |       const { spinner } = await import('../utils/progress-spinner.js');
1691 |       spinner.stop();
1692 |     }
1693 | 
1694 |     // Close any active connections
1695 |     for (const connection of this.connections.values()) {
1696 |       try {
1697 |         await connection.client.close();
1698 |       } catch (error) {
1699 |         // Ignore cleanup errors
1700 |       }
1701 |     }
1702 | 
1703 |     this.connections.clear();
1704 |     logger.info('NCP orchestrator cleanup completed');
1705 |   }
1706 | 
1707 |   /**
1708 |    * Get server descriptions for all configured MCPs
1709 |    */
1710 |   getServerDescriptions(): Record<string, string> {
1711 |     const descriptions: Record<string, string> = {};
1712 | 
1713 |     // From active connections
1714 |     for (const [mcpName, connection] of this.connections) {
1715 |       if (connection.serverInfo?.description) {
1716 |         descriptions[mcpName] = connection.serverInfo.description;
1717 |       } else if (connection.serverInfo?.title) {
1718 |         descriptions[mcpName] = connection.serverInfo.title;
1719 |       }
1720 |     }
1721 | 
1722 |     // From cached definitions
1723 |     for (const [mcpName, definition] of this.definitions) {
1724 |       if (!descriptions[mcpName] && definition.serverInfo?.description) {
1725 |         descriptions[mcpName] = definition.serverInfo.description;
1726 |       } else if (!descriptions[mcpName] && definition.serverInfo?.title) {
1727 |         descriptions[mcpName] = definition.serverInfo.title;
1728 |       }
1729 |     }
1730 | 
1731 |     return descriptions;
1732 |   }
1733 | 
1734 |   /**
1735 |    * Apply universal term frequency scoring boost with action word weighting
1736 |    * Uses SearchEnhancer for clean, extensible term classification and semantic mapping
1737 |    */
1738 |   private adjustScoresUniversally(query: string, results: any[]): any[] {
1739 |     const queryTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 2); // Skip very short terms
1740 | 
1741 |     return results.map(result => {
1742 |       const toolName = result.name.toLowerCase();
1743 |       const toolDescription = (result.description || '').toLowerCase();
1744 | 
1745 |       let nameBoost = 0;
1746 |       let descBoost = 0;
1747 | 
1748 |       // Process each query term with SearchEnhancer classification
1749 |       for (const term of queryTerms) {
1750 |         const termType = SearchEnhancer.classifyTerm(term);
1751 |         const weight = SearchEnhancer.getTypeWeights(termType);
1752 | 
1753 |         // Apply scoring based on term type
1754 |         if (toolName.includes(term)) {
1755 |           nameBoost += weight.name;
1756 |         }
1757 |         if (toolDescription.includes(term)) {
1758 |           descBoost += weight.desc;
1759 |         }
1760 | 
1761 |         // Apply semantic action matching for ACTION terms
1762 |         if (termType === 'ACTION') {
1763 |           const semantics = SearchEnhancer.getActionSemantics(term);
1764 |           for (const semanticMatch of semantics) {
1765 |             if (toolName.includes(semanticMatch)) {
1766 |               nameBoost += weight.name * 1.2; // 120% of full action weight for semantic matches (boosted)
1767 |             }
1768 |             if (toolDescription.includes(semanticMatch)) {
1769 |               descBoost += weight.desc * 1.2;
1770 |             }
1771 |           }
1772 | 
1773 |           // Apply intent penalties for conflicting actions
1774 |           const penalty = SearchEnhancer.getIntentPenalty(term, toolName);
1775 |           nameBoost -= penalty;
1776 |         }
1777 |       }
1778 | 
1779 |       // Apply diminishing returns to prevent excessive stacking
1780 |       const baseWeight = 0.15; // Base weight for diminishing returns calculation
1781 |       const finalNameBoost = nameBoost > 0 ? nameBoost * Math.pow(0.8, Math.max(0, nameBoost / baseWeight - 1)) : 0;
1782 |       const finalDescBoost = descBoost > 0 ? descBoost * Math.pow(0.8, Math.max(0, descBoost / (baseWeight / 2) - 1)) : 0;
1783 | 
1784 |       const totalBoost = 1 + finalNameBoost + finalDescBoost;
1785 | 
1786 |       return {
1787 |         ...result,
1788 |         confidence: result.confidence * totalBoost
1789 |       };
1790 |     });
1791 |   }
1792 | 
1793 |   /**
1794 |    * Trigger auto-import from MCP client
1795 |    * Called by MCPServer after it receives clientInfo from initialize request
1796 |    */
1797 |   async triggerAutoImport(clientName: string): Promise<void> {
1798 |     if (!this.profileManager) {
1799 |       // ProfileManager not initialized yet, skip auto-import
1800 |       logger.warn('ProfileManager not initialized, skipping auto-import');
1801 |       return;
1802 |     }
1803 | 
1804 |     try {
1805 |       await this.profileManager.tryAutoImportFromClient(clientName);
1806 |     } catch (error: any) {
1807 |       logger.error(`Auto-import failed: ${error.message}`);
1808 |     }
1809 |   }
1810 | 
1811 |   /**
1812 |    * Add internal MCPs to tool discovery
1813 |    * Called after external MCPs are indexed
1814 |    */
1815 |   private addInternalMCPsToDiscovery(): void {
1816 |     const internalMCPs = this.internalMCPManager.getAllInternalMCPs();
1817 | 
1818 |     for (const mcp of internalMCPs) {
1819 |       // Add to definitions (for consistency with external MCPs)
1820 |       this.definitions.set(mcp.name, {
1821 |         name: mcp.name,
1822 |         config: {
1823 |           name: mcp.name,
1824 |           command: 'internal',
1825 |           args: []
1826 |         },
1827 |         tools: mcp.tools.map(t => ({ name: t.name, description: t.description })),
1828 |         serverInfo: {
1829 |           name: mcp.name,
1830 |           version: '1.0.0',
1831 |           description: mcp.description
1832 |         }
1833 |       });
1834 | 
1835 |       // Add tools to allTools and discovery
1836 |       for (const tool of mcp.tools) {
1837 |         const toolId = `${mcp.name}:${tool.name}`;
1838 | 
1839 |         // Add to allTools
1840 |         this.allTools.push({
1841 |           name: tool.name,
1842 |           description: tool.description,
1843 |           mcpName: mcp.name
1844 |         });
1845 | 
1846 |         // Add to toolToMCP mapping
1847 |         this.toolToMCP.set(toolId, mcp.name);
1848 |       }
1849 | 
1850 |       // Index in discovery engine
1851 |       const discoveryTools = mcp.tools.map(t => ({
1852 |         id: `${mcp.name}:${t.name}`,
1853 |         name: t.name,
1854 |         description: t.description
1855 |       }));
1856 | 
1857 |       this.discovery.indexMCPTools(mcp.name, discoveryTools);
1858 | 
1859 |       logger.info(`Added internal MCP "${mcp.name}" with ${mcp.tools.length} tools`);
1860 |     }
1861 |   }
1862 | 
1863 |   /**
1864 |    * Get the ProfileManager instance
1865 |    * Used by MCP server for management operations (add/remove MCPs)
1866 |    */
1867 |   getProfileManager(): ProfileManager | null {
1868 |     return this.profileManager;
1869 |   }
1870 | 
1871 |   /**
1872 |    * Hash a string for change detection
1873 |    */
1874 |   private hashString(str: string): string {
1875 |     return createHash('sha256').update(str).digest('hex');
1876 |   }
1877 | }
1878 | 
1879 | export default NCPOrchestrator;
```

--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | #!/usr/bin/env node
   2 | 
   3 | import { Command } from 'commander';
   4 | import chalk from 'chalk';
   5 | import { readFileSync } from 'fs';
   6 | import { fileURLToPath } from 'url';
   7 | import { dirname, join } from 'path';
   8 | import { ProfileManager } from '../profiles/profile-manager.js';
   9 | import { MCPServer } from '../server/mcp-server.js';
  10 | import { ConfigManager } from '../utils/config-manager.js';
  11 | import { formatCommandDisplay } from '../utils/security.js';
  12 | import { TextUtils } from '../utils/text-utils.js';
  13 | import { OutputFormatter } from '../services/output-formatter.js';
  14 | import { ErrorHandler } from '../services/error-handler.js';
  15 | import { CachePatcher } from '../cache/cache-patcher.js';
  16 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  17 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
  18 | import { mcpWrapper } from '../utils/mcp-wrapper.js';
  19 | import { withFilteredOutput } from '../transports/filtered-stdio-transport.js';
  20 | import { UpdateChecker } from '../utils/update-checker.js';
  21 | import { setOverrideWorkingDirectory } from '../utils/ncp-paths.js';
  22 | import { ConfigSchemaReader } from '../services/config-schema-reader.js';
  23 | import { ConfigPrompter } from '../services/config-prompter.js';
  24 | import { SchemaCache } from '../cache/schema-cache.js';
  25 | import { getCacheDirectory } from '../utils/ncp-paths.js';
  26 | 
  27 | // Check for no-color flag early
  28 | const noColor = process.argv.includes('--no-color') || process.env.NO_COLOR === 'true';
  29 | if (noColor) {
  30 |   chalk.level = 0; // Disable colors globally
  31 | } else {
  32 |   // Ensure colors are enabled for TTY and when FORCE_COLOR is set
  33 |   if (process.env.FORCE_COLOR || process.stdout?.isTTY) {
  34 |     chalk.level = 3; // Full color support
  35 |   }
  36 | }
  37 | 
  38 | // Fuzzy matching helper for finding similar names
  39 | function findSimilarNames(target: string, availableNames: string[], maxSuggestions = 3): string[] {
  40 |   const targetLower = target.toLowerCase();
  41 | 
  42 |   // Score each name based on similarity
  43 |   const scored = availableNames.map(name => {
  44 |     const nameLower = name.toLowerCase();
  45 |     let score = 0;
  46 | 
  47 |     // Exact match gets highest score
  48 |     if (nameLower === targetLower) score += 100;
  49 | 
  50 |     // Contains target or target contains name
  51 |     if (nameLower.includes(targetLower)) score += 50;
  52 |     if (targetLower.includes(nameLower)) score += 50;
  53 | 
  54 |     // First few characters match
  55 |     const minLen = Math.min(targetLower.length, nameLower.length);
  56 |     for (let i = 0; i < minLen && i < 3; i++) {
  57 |       if (targetLower[i] === nameLower[i]) score += 10;
  58 |     }
  59 | 
  60 |     // Similar length bonus
  61 |     const lengthDiff = Math.abs(targetLower.length - nameLower.length);
  62 |     if (lengthDiff <= 2) score += 5;
  63 | 
  64 |     return { name, score };
  65 |   });
  66 | 
  67 |   // Filter out low scores and sort by score
  68 |   return scored
  69 |     .filter(item => item.score > 0)
  70 |     .sort((a, b) => b.score - a.score)
  71 |     .slice(0, maxSuggestions)
  72 |     .map(item => item.name);
  73 | }
  74 | 
  75 | // Enhanced remove validation helper
  76 | async function validateRemoveCommand(name: string, manager: ProfileManager, profiles: string[]): Promise<{
  77 |   mcpExists: boolean;
  78 |   suggestions: string[];
  79 |   allMCPs: string[];
  80 | }> {
  81 |   const allMCPs = new Set<string>();
  82 | 
  83 |   // Collect all MCP names from specified profiles
  84 |   for (const profileName of profiles) {
  85 |     const profile = await manager.getProfile(profileName);
  86 |     if (profile?.mcpServers) {
  87 |       Object.keys(profile.mcpServers).forEach(mcpName => allMCPs.add(mcpName));
  88 |     }
  89 |   }
  90 | 
  91 |   const mcpList = Array.from(allMCPs);
  92 |   const mcpExists = mcpList.includes(name);
  93 | 
  94 |   let suggestions: string[] = [];
  95 |   if (!mcpExists && mcpList.length > 0) {
  96 |     suggestions = findSimilarNames(name, mcpList);
  97 |   }
  98 | 
  99 |   return {
 100 |     mcpExists,
 101 |     suggestions,
 102 |     allMCPs: mcpList
 103 |   };
 104 | }
 105 | 
 106 | // Simple validation helper for ADD command
 107 | async function validateAddCommand(name: string, command: string, args: any[]): Promise<{
 108 |   message: string;
 109 |   suggestions: Array<{ command: string; description: string }>
 110 | }> {
 111 |   const suggestions: Array<{ command: string; description: string }> = [];
 112 | 
 113 |   const fullCommand = `${command} ${args.join(' ')}`.trim();
 114 | 
 115 |   // Basic command format validation and helpful tips
 116 |   if (command === 'npx' && args.length > 0) {
 117 |     // Clean up the command format - avoid duplication
 118 |     const cleanedArgs = args.filter(arg => arg !== '-y' || args.indexOf(arg) === 0);
 119 |     suggestions.push({
 120 |       command: fullCommand,
 121 |       description: 'NPM package execution - health monitor will validate if package exists and starts correctly'
 122 |     });
 123 |   } else if (command.startsWith('/') || command.startsWith('./') || command.includes('\\')) {
 124 |     suggestions.push({
 125 |       command: fullCommand,
 126 |       description: 'Local executable - health monitor will validate if command works'
 127 |     });
 128 |   } else if (command.includes('@') && !command.startsWith('npx')) {
 129 |     suggestions.push({
 130 |       command: `npx -y ${fullCommand}`,
 131 |       description: 'Consider using npx for npm packages'
 132 |     });
 133 |   } else {
 134 |     // Show the command as provided
 135 |     suggestions.push({
 136 |       command: fullCommand,
 137 |       description: 'Custom command - health monitor will validate functionality'
 138 |     });
 139 |   }
 140 | 
 141 |   return {
 142 |     message: chalk.dim('💡 MCP will be validated by health monitor after adding'),
 143 |     suggestions
 144 |   };
 145 | }
 146 | 
 147 | // Simple emoji support detection for cross-platform compatibility
 148 | const supportsEmoji = () => {
 149 |   // Windows Command Prompt and PowerShell often don't support emojis well
 150 |   if (process.platform === 'win32') {
 151 |     // Check if it's Windows Terminal (supports emojis) vs cmd/powershell
 152 |     return process.env.WT_SESSION || process.env.TERM_PROGRAM === 'vscode';
 153 |   }
 154 |   // macOS and Linux terminals generally support emojis
 155 |   return true;
 156 | };
 157 | 
 158 | const getIcon = (emoji: string, fallback: string) =>
 159 |   supportsEmoji() ? emoji : fallback;
 160 | 
 161 | // Configure OutputFormatter
 162 | OutputFormatter.configure({ noColor: !!noColor, emoji: !!supportsEmoji() });
 163 | 
 164 | // Use centralized version utility
 165 | import { version } from '../utils/version.js';
 166 | 
 167 | // Discovery function for single MCP - extracted from NCPOrchestrator.probeMCPTools
 168 | async function discoverSingleMCP(name: string, command: string, args: string[] = [], env: Record<string, string> = {}): Promise<{
 169 |   tools: Array<{name: string; description: string; inputSchema?: any}>;
 170 |   serverInfo?: {
 171 |     name: string;
 172 |     title?: string;
 173 |     version: string;
 174 |     description?: string;
 175 |     websiteUrl?: string;
 176 |   };
 177 |   configurationSchema?: any;
 178 | }> {
 179 |   const config = { name, command, args, env };
 180 | 
 181 |   if (!config.command) {
 182 |     throw new Error(`Invalid config for ${config.name}`);
 183 |   }
 184 | 
 185 |   let client: Client | null = null;
 186 |   let transport: StdioClientTransport | null = null;
 187 |   const DISCOVERY_TIMEOUT = 8000; // 8 seconds
 188 | 
 189 |   try {
 190 |     // Create wrapper command for discovery phase
 191 |     const wrappedCommand = mcpWrapper.createWrapper(
 192 |       config.name,
 193 |       config.command,
 194 |       config.args || []
 195 |     );
 196 | 
 197 |     // Create temporary connection for discovery
 198 |     const silentEnv = {
 199 |       ...process.env,
 200 |       ...(config.env || {}),
 201 |       MCP_SILENT: 'true',
 202 |       QUIET: 'true',
 203 |       NO_COLOR: 'true'
 204 |     };
 205 | 
 206 |     transport = new StdioClientTransport({
 207 |       command: wrappedCommand.command,
 208 |       args: wrappedCommand.args,
 209 |       env: silentEnv as Record<string, string>
 210 |     });
 211 | 
 212 |     client = new Client(
 213 |       { name: 'ncp-oss', version: '1.0.0' },
 214 |       { capabilities: {} }
 215 |     );
 216 | 
 217 |     // Connect with timeout and filtered output
 218 |     await withFilteredOutput(async () => {
 219 |       await Promise.race([
 220 |         client!.connect(transport!),
 221 |         new Promise((_, reject) =>
 222 |           setTimeout(() => reject(new Error('Discovery timeout')), DISCOVERY_TIMEOUT)
 223 |         )
 224 |       ]);
 225 |     });
 226 | 
 227 |     // Capture server info after connection
 228 |     const serverInfo = client!.getServerVersion();
 229 | 
 230 |     // Capture configuration schema if available
 231 |     // TODO: Once MCP SDK is updated to support top-level configurationSchema,
 232 |     // also check for it directly. For now, check experimental capabilities.
 233 |     const serverCapabilities = client!.getServerCapabilities();
 234 |     const configurationSchema = (serverCapabilities as any)?.experimental?.configurationSchema;
 235 | 
 236 |     // Get tool list with filtered output
 237 |     const response = await withFilteredOutput(async () => {
 238 |       return await client!.listTools();
 239 |     });
 240 | 
 241 |     const tools = response.tools.map(t => ({
 242 |       name: t.name,
 243 |       description: t.description || '',
 244 |       inputSchema: t.inputSchema || {}
 245 |     }));
 246 | 
 247 |     // Disconnect immediately
 248 |     await client.close();
 249 | 
 250 |     return {
 251 |       tools,
 252 |       serverInfo: serverInfo ? {
 253 |         name: serverInfo.name || config.name,
 254 |         title: serverInfo.title,
 255 |         version: serverInfo.version || 'unknown',
 256 |         description: serverInfo.title || serverInfo.name || undefined,
 257 |         websiteUrl: serverInfo.websiteUrl
 258 |       } : undefined,
 259 |       configurationSchema
 260 |     };
 261 | 
 262 |   } catch (error: any) {
 263 |     // Clean up connections
 264 |     try {
 265 |       if (client) {
 266 |         await client.close();
 267 |       }
 268 |     } catch (closeError) {
 269 |       // Ignore close errors
 270 |     }
 271 | 
 272 |     throw new Error(`Failed to discover tools from ${config.name}: ${error.message}`);
 273 |   }
 274 | }
 275 | 
 276 | const program = new Command();
 277 | 
 278 | 
 279 | // Set version
 280 | program.version(version, '-v, --version', 'output the current version');
 281 | 
 282 | 
 283 | // Custom help configuration with colors and enhanced content
 284 | program
 285 |   .name('ncp')
 286 |   .description(`
 287 | ${chalk.bold.white('Natural Context Provider')} ${chalk.dim('v' + version)} - ${chalk.cyan('1 MCP to rule them all')}
 288 | ${chalk.dim('Orchestrates multiple MCP servers through a unified interface for AI assistants.')}
 289 | ${chalk.dim('Reduces cognitive load and clutter, saving tokens and speeding up AI interactions.')}
 290 | ${chalk.dim('Enables smart tool discovery across all configured servers with vector similarity search.')}`)
 291 |   .option('--profile <name>', 'Profile to use (default: all)')
 292 |   .option('--working-dir <path>', 'Working directory for profile resolution (overrides current directory)')
 293 |   .option('--force-retry', 'Force retry all failed MCPs immediately (ignores scheduled retry times)')
 294 |   .option('--no-color', 'Disable colored output');
 295 | 
 296 | 
 297 | // Configure help with enhanced formatting, Quick Start, and examples
 298 | program.configureHelp({
 299 |   sortSubcommands: true,
 300 |   formatHelp: (cmd, helper) => {
 301 |     // Calculate proper padding based on actual command names and options separately
 302 |     const allCommands = cmd.commands.filter((cmd: any) => !cmd.hidden);
 303 |     const maxCmdLength = allCommands.length > 0 ? Math.max(...allCommands.map(cmd => cmd.name().length)) : 0;
 304 |     const maxOptionLength = cmd.options.length > 0 ? Math.max(...cmd.options.map(option => option.flags.length)) : 0;
 305 | 
 306 |     const cmdPad = maxCmdLength + 4; // Add extra space for command alignment
 307 |     const optionPad = maxOptionLength + 4; // Add extra space for option alignment
 308 |     const helpWidth = helper.helpWidth || 80;
 309 | 
 310 |     function formatItem(term: string, description?: string, padding?: number): string {
 311 |       if (description) {
 312 |         const pad = padding || cmdPad;
 313 |         return term.padEnd(pad) + description;
 314 |       }
 315 |       return term;
 316 |     }
 317 | 
 318 |     // Add description first
 319 |     let output = cmd.description() + '\n\n';
 320 | 
 321 |     // Then usage and config info
 322 |     output += `${chalk.bold.white('Usage:')} ${cmd.name()} [options] [command]\n`;
 323 |     output += `${chalk.yellow('NCP config files:')} ~/.ncp/profiles/\n\n`;
 324 | 
 325 |     // Options
 326 |     if (cmd.options.length) {
 327 |       output += chalk.bold.white('Options:') + '\n';
 328 |       cmd.options.forEach(option => {
 329 |         // Calculate padding based on raw flags, not styled version
 330 |         const rawPadding = '  ' + option.flags;
 331 |         const paddedRaw = rawPadding.padEnd(optionPad + 2);
 332 |         const styledFlags = chalk.cyan(option.flags);
 333 |         const description = chalk.white(option.description);
 334 | 
 335 |         output += '  ' + styledFlags + ' '.repeat(paddedRaw.length - rawPadding.length) + description + '\n';
 336 |       });
 337 |       output += '\n';
 338 |     }
 339 | 
 340 |     // Commands
 341 |     const commands = cmd.commands.filter((cmd: any) => !cmd.hidden);
 342 |     if (commands.length) {
 343 |       output += chalk.bold.white('Commands:') + '\n';
 344 |       commands.sort((a, b) => a.name().localeCompare(b.name()));
 345 | 
 346 |       commands.forEach(cmd => {
 347 |         // Group commands by category with enhanced styling
 348 |         const managementCommands = ['add', 'remove', 'import', 'list', 'config'];
 349 |         const discoveryCommands = ['find'];
 350 |         const executionCommands = ['run'];
 351 | 
 352 |         let cmdName = cmd.name();
 353 |         let styledCmdName = cmdName;
 354 |         if (managementCommands.includes(cmd.name())) {
 355 |           styledCmdName = chalk.cyan(cmd.name());
 356 |         } else if (discoveryCommands.includes(cmd.name())) {
 357 |           styledCmdName = chalk.green.bold(cmd.name());
 358 |         } else if (executionCommands.includes(cmd.name())) {
 359 |           styledCmdName = chalk.yellow.bold(cmd.name());
 360 |         }
 361 | 
 362 |         // Calculate padding based on raw command name, not styled version
 363 |         const rawPadding = '  ' + cmdName;
 364 |         const paddedRaw = rawPadding.padEnd(cmdPad + 2);  // Use cmdPad + 2 for consistency
 365 |         const description = chalk.white(cmd.description());
 366 | 
 367 |         output += '  ' + styledCmdName + ' '.repeat(paddedRaw.length - rawPadding.length) + description + '\n';
 368 |       });
 369 |     }
 370 | 
 371 |     return output;
 372 |   }
 373 | });
 374 | 
 375 | 
 376 | // Add help command
 377 | program
 378 |   .command('help [command]')
 379 |   .description('Show help for NCP or a specific command')
 380 |   .action((command) => {
 381 |     if (command) {
 382 |       const cmd = program.commands.find(cmd => cmd.name() === command);
 383 |       if (cmd) {
 384 |         cmd.help();
 385 |       } else {
 386 |         console.log(`Unknown command: ${command}`);
 387 |         program.help();
 388 |       }
 389 |     } else {
 390 |       program.help();
 391 |     }
 392 |   });
 393 | 
 394 | // Add Quick Start and Examples after all commands are defined
 395 | program.addHelpText('after', `
 396 | ${chalk.bold.white('Quick Start:')}
 397 |   ${chalk.cyan('1a')} Import existing MCPs: ${chalk.green('ncp config import')} ${chalk.dim('(copy JSON first)')}
 398 |   ${chalk.cyan('1b')} Or add manually: ${chalk.green('ncp add <name> <command>')}
 399 |   ${chalk.cyan('2')} Configure NCP in AI client settings
 400 | 
 401 | ${chalk.bold.white('Examples:')}
 402 |   $ ${chalk.yellow('ncp config import config.json')} ${chalk.dim('              # Import from file')}
 403 |   $ ${chalk.yellow('ncp add filesystem npx @modelcontextprotocol/server-filesystem /tmp')}
 404 |   $ ${chalk.yellow('ncp find "file operations"')}
 405 |   $ ${chalk.yellow('ncp run filesystem:read_file --params \'{"path": "/tmp/example.txt"}\'')}
 406 |   $ ${chalk.yellow('ncp list --depth 1')}`);
 407 | 
 408 | // Check if we should run as MCP server
 409 | // MCP server mode: default when no CLI commands are provided, or when --profile is specified
 410 | const profileIndex = process.argv.indexOf('--profile');
 411 | const hasCommands = process.argv.includes('find') ||
 412 |   process.argv.includes('add') ||
 413 |   process.argv.includes('list') ||
 414 |   process.argv.includes('remove') ||
 415 |   process.argv.includes('run') ||
 416 |   process.argv.includes('config') ||
 417 |   process.argv.includes('help') ||
 418 |   process.argv.includes('--help') ||
 419 |   process.argv.includes('-h') ||
 420 |   process.argv.includes('--version') ||
 421 |   process.argv.includes('-v') ||
 422 |   process.argv.includes('import') ||
 423 |   process.argv.includes('analytics') ||
 424 |   process.argv.includes('visual') ||
 425 |   process.argv.includes('update') ||
 426 |   process.argv.includes('repair');
 427 | 
 428 | // Default to MCP server mode when no CLI commands are provided
 429 | // This ensures compatibility with Claude Desktop and other MCP clients that expect server mode by default
 430 | const shouldRunAsServer = !hasCommands;
 431 | 
 432 | if (shouldRunAsServer) {
 433 |   // Handle --working-dir parameter for MCP server mode
 434 |   const workingDirIndex = process.argv.indexOf('--working-dir');
 435 |   if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) {
 436 |     const workingDirValue = process.argv[workingDirIndex + 1];
 437 |     setOverrideWorkingDirectory(workingDirValue);
 438 |   }
 439 | 
 440 |   // Running as MCP server: ncp (defaults to 'all' profile) or ncp --profile <name>
 441 |   // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE to 'default' or anything else!
 442 |   const profileName = profileIndex !== -1 ? (process.argv[profileIndex + 1] || 'all') : 'all';
 443 | 
 444 |   // Debug logging for integration tests
 445 |   if (process.env.NCP_DEBUG === 'true') {
 446 |     console.error(`[DEBUG] profileIndex: ${profileIndex}`);
 447 |     console.error(`[DEBUG] process.argv: ${process.argv.join(' ')}`);
 448 |     console.error(`[DEBUG] Selected profile: ${profileName}`);
 449 |   }
 450 | 
 451 |   const server = new MCPServer(profileName);
 452 |   server.run().catch(console.error);
 453 | } else {
 454 |   // Handle --working-dir parameter for CLI mode
 455 |   const workingDirIndex = process.argv.indexOf('--working-dir');
 456 |   if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) {
 457 |     const workingDirValue = process.argv[workingDirIndex + 1];
 458 |     setOverrideWorkingDirectory(workingDirValue);
 459 |   }
 460 | 
 461 |   // Running as CLI tool
 462 | 
 463 | // Add MCP command
 464 | program
 465 |   .command('add <name> <command> [args...]')
 466 |   .description('Add an MCP server to a profile')
 467 |   .option('--profile <names...>', 'Profile(s) to add to (can specify multiple, default: all)')
 468 |   .option('--env <vars...>', 'Environment variables (KEY=value)')
 469 |   .action(async (name, command, args, options) => {
 470 |     console.log(`\n${chalk.blue(`📦 Adding MCP server: ${chalk.bold(name)}`)}`);
 471 | 
 472 |     const manager = new ProfileManager();
 473 |     await manager.initialize();
 474 | 
 475 |     // Show helpful guidance without hard validation
 476 |     const guidance = await validateAddCommand(name, command, args);
 477 |     console.log(guidance.message);
 478 |     if (guidance.suggestions.length > 0) {
 479 |       console.log('\n📋 Command validation:');
 480 |       guidance.suggestions.forEach((suggestion, index) => {
 481 |         if (index === 0) {
 482 |           // Main command
 483 |           console.log(`   ${chalk.cyan(suggestion.command)}`);
 484 |           console.log(`   ${chalk.dim(suggestion.description)}`);
 485 |         } else {
 486 |           // Alternative suggestions
 487 |           console.log(chalk.dim(`\n💡 Alternative: ${suggestion.command}`));
 488 |           console.log(chalk.dim(`   ${suggestion.description}`));
 489 |         }
 490 |       });
 491 |       console.log('');
 492 |     }
 493 | 
 494 |     // Parse environment variables
 495 |     const env: Record<string, string> = {};
 496 |     if (options.env) {
 497 |       console.log(chalk.dim('🔧 Processing environment variables...'));
 498 |       for (const envVar of options.env) {
 499 |         const [key, value] = envVar.split('=');
 500 |         if (key && value) {
 501 |           env[key] = value;
 502 |           console.log(chalk.dim(`   ${key}=${formatCommandDisplay(value)}`));
 503 |         } else {
 504 |           console.log(chalk.yellow(`⚠️  Invalid environment variable format: ${envVar}`));
 505 |         }
 506 |       }
 507 |     }
 508 | 
 509 |     const config = {
 510 |       command,
 511 |       args: args || [],
 512 |       ...(Object.keys(env).length > 0 && { env })
 513 |     };
 514 | 
 515 |     // Show what will be added
 516 |     // Determine which profiles to add to
 517 |     // ⚠️ CRITICAL: Default MUST be ['all'] - DO NOT CHANGE!
 518 |     const profiles = options.profile || ['all'];
 519 | 
 520 |     console.log('\n📋 Profile configuration:');
 521 |     console.log(`   ${chalk.cyan('Target profiles:')} ${profiles.join(', ')}`);
 522 |     if (Object.keys(env).length > 0) {
 523 |       console.log(`   ${chalk.cyan('Environment variables:')} ${Object.keys(env).length} configured`);
 524 |       Object.entries(env).forEach(([key, value]) => {
 525 |         console.log(chalk.dim(`     ${key}=${formatCommandDisplay(value)}`));
 526 |       });
 527 |     }
 528 | 
 529 |     console.log(''); // spacing
 530 | 
 531 |     // Initialize schema services
 532 |     const schemaReader = new ConfigSchemaReader();
 533 |     const configPrompter = new ConfigPrompter();
 534 |     const schemaCache = new SchemaCache(getCacheDirectory());
 535 | 
 536 |     // Try to discover and detect configuration requirements BEFORE adding to profile
 537 |     console.log(chalk.dim('🔍 Discovering tools and configuration requirements...'));
 538 |     const discoveryStart = Date.now();
 539 | 
 540 |     let discoveryResult: Awaited<ReturnType<typeof discoverSingleMCP>> | null = null;
 541 |     let finalConfig = { ...config };
 542 |     let detectedSchema: any = null;
 543 | 
 544 |     try {
 545 |       discoveryResult = await discoverSingleMCP(name, command, args, env);
 546 |       const discoveryTime = Date.now() - discoveryStart;
 547 | 
 548 |       console.log(`${chalk.green('✅')} Found ${discoveryResult.tools.length} tools in ${discoveryTime}ms`);
 549 | 
 550 |       // Two-tier configuration detection strategy:
 551 |       // Tier 1: MCP Protocol configurationSchema (from server capabilities)
 552 |       // Tier 2: Error parsing (fallback - happens on failure below)
 553 | 
 554 |       // Tier 1: Check for MCP protocol schema
 555 |       if (discoveryResult.configurationSchema) {
 556 |         detectedSchema = schemaReader.readSchema({
 557 |           protocolVersion: '1.0',
 558 |           capabilities: {},
 559 |           serverInfo: { name, version: '1.0' },
 560 |           configurationSchema: discoveryResult.configurationSchema
 561 |         });
 562 |         if (detectedSchema) {
 563 |           console.log(chalk.dim('   Configuration schema detected (MCP protocol)'));
 564 |         }
 565 |       }
 566 | 
 567 |       // Apply detected schema if we have one with required config
 568 |       if (detectedSchema && schemaReader.hasRequiredConfig(detectedSchema)) {
 569 |         console.log(chalk.cyan('\n📋 Configuration required'));
 570 | 
 571 |         // Prompt for configuration
 572 |         const promptedConfig = await configPrompter.promptForConfig(detectedSchema, name);
 573 | 
 574 |         // Merge prompted config with existing config
 575 |         finalConfig = {
 576 |           command: config.command,
 577 |           args: [...(config.args || []), ...(promptedConfig.arguments || [])],
 578 |           env: { ...(config.env || {}), ...(promptedConfig.environmentVariables || {}) }
 579 |         };
 580 | 
 581 |         // Display summary
 582 |         configPrompter.displaySummary(promptedConfig, name);
 583 | 
 584 |         // Cache schema for future use
 585 |         schemaCache.save(name, detectedSchema);
 586 |         console.log(chalk.dim('✓ Configuration schema cached'));
 587 |       }
 588 |     } catch (discoveryError: any) {
 589 |       console.log(`${chalk.yellow('⚠️')} Discovery failed: ${discoveryError.message}`);
 590 |       console.log(chalk.dim('   Proceeding with manual configuration...'));
 591 |       // Tier 3: Error parsing would happen here in future enhancement
 592 |       // Continue with manual config - error will be saved to profile
 593 |     }
 594 | 
 595 |     // Initialize cache patcher
 596 |     const cachePatcher = new CachePatcher();
 597 | 
 598 |     for (const profileName of profiles) {
 599 |       try {
 600 |         // 1. Update profile with final configuration
 601 |         await manager.addMCPToProfile(profileName, name, finalConfig);
 602 |         console.log(`\n${OutputFormatter.success(`Added ${name} to profile: ${profileName}`)}`);
 603 | 
 604 |         // 2. Update cache if we have discovery results
 605 |         if (discoveryResult) {
 606 |           if (discoveryResult.tools.length > 0) {
 607 |             console.log(chalk.dim('   Tools discovered:'));
 608 |             // Show first few tools
 609 |             const toolsToShow = discoveryResult.tools.slice(0, 3);
 610 |             toolsToShow.forEach(tool => {
 611 |               const shortDesc = tool.description?.length > 50
 612 |                 ? tool.description.substring(0, 50) + '...'
 613 |                 : tool.description;
 614 |               console.log(chalk.dim(`   • ${tool.name}: ${shortDesc}`));
 615 |             });
 616 |             if (discoveryResult.tools.length > 3) {
 617 |               console.log(chalk.dim(`   • ... and ${discoveryResult.tools.length - 3} more`));
 618 |             }
 619 |           }
 620 | 
 621 |           // 3. Patch tool metadata cache with final config
 622 |           await cachePatcher.patchAddMCP(name, finalConfig, discoveryResult.tools, discoveryResult.serverInfo);
 623 | 
 624 |           // 4. Update profile hash
 625 |           const profile = await manager.getProfile(profileName);
 626 |           const profileHash = cachePatcher.generateProfileHash(profile);
 627 |           await cachePatcher.updateProfileHash(profileHash);
 628 | 
 629 |           console.log(`${chalk.green('✅')} Cache updated for ${name}`);
 630 |         } else {
 631 |           console.log(chalk.dim('   Profile updated, but cache not built. Run "ncp find <query>" to build cache later.'));
 632 |         }
 633 | 
 634 |       } catch (error: any) {
 635 |         const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('profile', 'add', `${name} to ${profileName}`));
 636 |         console.log('\n' + ErrorHandler.formatForConsole(errorResult));
 637 |       }
 638 |     }
 639 | 
 640 |     console.log(chalk.dim('\n💡 Next steps:'));
 641 |     console.log(chalk.dim('  •') + ' View profiles: ' + chalk.cyan('ncp list'));
 642 |     console.log(chalk.dim('  •') + ' Test discovery: ' + chalk.cyan('ncp find <query>'));
 643 |   });
 644 | 
 645 | 
 646 | // Lightweight function to read MCP info from cache without full orchestrator initialization
 647 | async function loadMCPInfoFromCache(mcpDescriptions: Record<string, string>, mcpToolCounts: Record<string, number>, mcpVersions: Record<string, string>): Promise<boolean> {
 648 |   const { readFileSync, existsSync } = await import('fs');
 649 |   const { getCacheDirectory } = await import('../utils/ncp-paths.js');
 650 |   const { join } = await import('path');
 651 | 
 652 |   const cacheDir = getCacheDirectory();
 653 |   const cachePath = join(cacheDir, 'all-tools.json');
 654 | 
 655 |   if (!existsSync(cachePath)) {
 656 |     return false; // No cache available
 657 |   }
 658 | 
 659 |   try {
 660 |     const cacheContent = readFileSync(cachePath, 'utf-8');
 661 |     const cache = JSON.parse(cacheContent);
 662 | 
 663 |     // Extract server info and tool counts from cache
 664 |     for (const [mcpName, mcpData] of Object.entries(cache.mcps || {})) {
 665 |       const data = mcpData as any;
 666 | 
 667 |       // Extract server description (without version)
 668 |       if (data.serverInfo?.description && data.serverInfo.description !== mcpName) {
 669 |         mcpDescriptions[mcpName] = data.serverInfo.description;
 670 |       } else if (data.serverInfo?.title) {
 671 |         mcpDescriptions[mcpName] = data.serverInfo.title;
 672 |       }
 673 | 
 674 |       // Extract version separately
 675 |       if (data.serverInfo?.version && data.serverInfo.version !== 'unknown') {
 676 |         mcpVersions[mcpName] = data.serverInfo.version;
 677 |       }
 678 | 
 679 |       // Count tools
 680 |       if (data.tools && Array.isArray(data.tools)) {
 681 |         mcpToolCounts[mcpName] = data.tools.length;
 682 |       }
 683 |     }
 684 |     return true; // Cache was successfully loaded
 685 |   } catch (error) {
 686 |     // Ignore cache reading errors - will just show without descriptions
 687 |     return false;
 688 |   }
 689 | }
 690 | 
 691 | 
 692 | // List command
 693 | program
 694 |   .command('list [filter]')
 695 |   .description('List all profiles and their MCPs with intelligent filtering')
 696 |   .option('--limit <number>', 'Maximum number of items to show (default: 20)')
 697 |   .option('--page <number>', 'Page number for pagination (default: 1)')
 698 |   .option('--depth <number>', 'Display depth: 0=profiles only, 1=profiles+MCPs+description, 2=profiles+MCPs+description+tools (default: 2)')
 699 |   .option('--search <query>', 'Search in MCP names and descriptions')
 700 |   .option('--profile <name>', 'Show only specific profile')
 701 |   .option('--sort <field>', 'Sort by: name, tools, profiles (default: name)', 'name')
 702 |   .option('--non-empty', 'Show only profiles with configured MCPs')
 703 |   .action(async (filter, options) => {
 704 |     const limit = parseInt(options.limit || '20');
 705 |     const page = parseInt(options.page || '1');
 706 |     const depth = parseInt(options.depth || '2');
 707 | 
 708 |     const manager = new ProfileManager();
 709 |     await manager.initialize();
 710 | 
 711 |     let profiles = manager.listProfiles();
 712 | 
 713 |     if (profiles.length === 0) {
 714 |       console.log(chalk.yellow('📋 No profiles configured'));
 715 |       console.log(chalk.dim('💡 Use: ncp add <name> <command> to add an MCP server'));
 716 |       return;
 717 |     }
 718 | 
 719 |     // Apply profile filtering first
 720 |     if (options.profile) {
 721 |       const targetProfile = options.profile.toLowerCase();
 722 |       profiles = profiles.filter(p => p.toLowerCase() === targetProfile);
 723 | 
 724 |       if (profiles.length === 0) {
 725 |         console.log(chalk.yellow(`⚠️  Profile "${options.profile}" not found`));
 726 | 
 727 |         // Suggest similar profiles
 728 |         const allProfiles = manager.listProfiles();
 729 |         const suggestions = findSimilarNames(options.profile, allProfiles);
 730 |         if (suggestions.length > 0) {
 731 |           console.log(chalk.yellow('\n💡 Did you mean:'));
 732 |           suggestions.forEach((suggestion, index) => {
 733 |             console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
 734 |           });
 735 |         } else {
 736 |           console.log(chalk.yellow('\n📋 Available profiles:'));
 737 |           allProfiles.forEach((profile, index) => {
 738 |             console.log(`  ${index + 1}. ${chalk.cyan(profile)}`);
 739 |           });
 740 |         }
 741 |         return;
 742 |       }
 743 |     }
 744 | 
 745 |     // Initialize orchestrator to get MCP descriptions and tool counts if needed
 746 |     let orchestrator;
 747 |     let mcpDescriptions: Record<string, string> = {};
 748 |     let mcpToolCounts: Record<string, number> = {};
 749 |     let mcpVersions: Record<string, string> = {};
 750 | 
 751 |     if (depth >= 1) {
 752 |       try {
 753 |         // Lightweight cache reading - no orchestrator initialization needed
 754 |         const cacheLoaded = await loadMCPInfoFromCache(mcpDescriptions, mcpToolCounts, mcpVersions);
 755 | 
 756 |         if (!cacheLoaded) {
 757 |           // Show helpful message about building cache
 758 |           console.log(chalk.dim('💡 No MCP cache found. Use `ncp find <query>` to discover tools and build cache.'));
 759 |         }
 760 |       } catch (error) {
 761 |         // If cache reading fails, continue without descriptions
 762 |         console.log(chalk.dim('Note: Could not load MCP descriptions and tool counts'));
 763 |       }
 764 |     }
 765 | 
 766 |     // Collect and filter data first
 767 |     const profileData: Array<{
 768 |       name: string;
 769 |       mcps: Record<string, any>;
 770 |       filteredMcps: Record<string, any>;
 771 |       originalCount: number;
 772 |       filteredCount: number;
 773 |     }> = [];
 774 | 
 775 |     for (const profileName of profiles) {
 776 |       const mcps = await manager.getProfileMCPs(profileName) || {};
 777 |       let filteredMcps = mcps;
 778 | 
 779 |       // Apply MCP filtering
 780 |       if (filter || options.search) {
 781 |         const query = filter || options.search;
 782 |         const queryLower = query.toLowerCase();
 783 | 
 784 |         filteredMcps = Object.fromEntries(
 785 |           Object.entries(mcps).filter(([mcpName, config]) => {
 786 |             const description = mcpDescriptions[mcpName] || mcpName;
 787 |             return (
 788 |               mcpName.toLowerCase().includes(queryLower) ||
 789 |               description.toLowerCase().includes(queryLower)
 790 |             );
 791 |           })
 792 |         );
 793 |       }
 794 | 
 795 |       // Apply non-empty filter
 796 |       if (options.nonEmpty && Object.keys(filteredMcps).length === 0) {
 797 |         continue; // Skip empty profiles when --non-empty is used
 798 |       }
 799 | 
 800 |       profileData.push({
 801 |         name: profileName,
 802 |         mcps,
 803 |         filteredMcps,
 804 |         originalCount: Object.keys(mcps).length,
 805 |         filteredCount: Object.keys(filteredMcps).length
 806 |       });
 807 |     }
 808 | 
 809 |     // Check if filtering returned no results
 810 |     if (profileData.length === 0) {
 811 |       const queryInfo = filter || options.search;
 812 |       console.log(chalk.yellow(`⚠️  No MCPs found${queryInfo ? ` matching "${queryInfo}"` : ''}`));
 813 | 
 814 |       // Suggest available MCPs if search was used
 815 |       if (queryInfo) {
 816 |         const allMcps = new Set<string>();
 817 |         for (const profile of manager.listProfiles()) {
 818 |           const mcps = await manager.getProfileMCPs(profile);
 819 |           if (mcps) {
 820 |             Object.keys(mcps).forEach(mcp => allMcps.add(mcp));
 821 |           }
 822 |         }
 823 | 
 824 |         if (allMcps.size > 0) {
 825 |           const suggestions = findSimilarNames(queryInfo, Array.from(allMcps));
 826 |           if (suggestions.length > 0) {
 827 |             console.log(chalk.yellow('\n💡 Did you mean:'));
 828 |             suggestions.forEach((suggestion, index) => {
 829 |               console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
 830 |             });
 831 |           } else {
 832 |             console.log(chalk.yellow('\n📋 Available MCPs:'));
 833 |             Array.from(allMcps).slice(0, 10).forEach((mcp, index) => {
 834 |               console.log(`  ${index + 1}. ${chalk.cyan(mcp)}`);
 835 |             });
 836 |           }
 837 |         }
 838 |       }
 839 |       return;
 840 |     }
 841 | 
 842 |     // Sort profiles if requested
 843 |     if (options.sort !== 'name') {
 844 |       profileData.sort((a, b) => {
 845 |         switch (options.sort) {
 846 |           case 'tools':
 847 |             return b.filteredCount - a.filteredCount;
 848 |           case 'profiles':
 849 |             return a.name.localeCompare(b.name);
 850 |           default:
 851 |             return a.name.localeCompare(b.name);
 852 |         }
 853 |       });
 854 |     }
 855 | 
 856 |     // Display results
 857 |     console.log('');
 858 |     console.log(chalk.bold.white('Profiles ▶ MCPs'));
 859 |     if (filter || options.search) {
 860 |       console.log(chalk.dim(`🔍 Filtered by: "${filter || options.search}"`));
 861 |     }
 862 |     console.log('');
 863 | 
 864 |     let totalMCPs = 0;
 865 | 
 866 |     for (const data of profileData) {
 867 |       const { name: profileName, filteredMcps, filteredCount } = data;
 868 |       totalMCPs += filteredCount;
 869 | 
 870 |       // Profile header with count
 871 |       const countBadge = filteredCount > 0 ? chalk.green(`${filteredCount} MCPs`) : chalk.dim('empty');
 872 |       console.log(`📦 ${chalk.bold.white(profileName)}`, chalk.dim(`(${countBadge})`));
 873 | 
 874 |       // Depth 0: profiles only - skip MCP details
 875 |       if (depth === 0) {
 876 |         // Already showing profile, nothing more needed
 877 |       } else if (filteredMcps && Object.keys(filteredMcps).length > 0) {
 878 |         const mcpEntries = Object.entries(filteredMcps);
 879 |         mcpEntries.forEach(([mcpName, config], index) => {
 880 |           const isLast = index === mcpEntries.length - 1;
 881 |           const connector = isLast ? '└──' : '├──';
 882 |           const indent = isLast ? '   ' : '│  ';
 883 | 
 884 |           // MCP name with tool count (following profile count style)
 885 |           const toolCount = mcpToolCounts[mcpName];
 886 |           const versionPart = mcpVersions[mcpName] ? chalk.magenta(`v${mcpVersions[mcpName]}`) : '';
 887 | 
 888 |           // If not in cache, it means MCP hasn't connected successfully
 889 |           const toolPart = toolCount !== undefined ? chalk.green(`${toolCount} tools`) : chalk.gray('not available');
 890 | 
 891 |           const badge = versionPart && toolPart ? chalk.dim(` (${versionPart} | ${toolPart})`) :
 892 |                        versionPart ? chalk.dim(` (${versionPart})`) :
 893 |                        toolPart ? chalk.dim(` (${toolPart})`) : '';
 894 |           console.log(`  ${connector} ${chalk.bold.cyanBright(mcpName)}${badge}`);
 895 | 
 896 |           // Depth 1+: Show description if available and meaningful
 897 |           if (depth >= 1 && mcpDescriptions[mcpName]) {
 898 |             const description = mcpDescriptions[mcpName];
 899 |             // Skip descriptions that just repeat the MCP name (no value added)
 900 |             if (description.toLowerCase() !== mcpName.toLowerCase()) {
 901 |               console.log(`  ${indent} ${chalk.white(description)}`);
 902 |             }
 903 |           }
 904 | 
 905 |           // Depth 2: Show command with reverse colors and text wrapping
 906 |           if (depth >= 2) {
 907 |             const commandText = formatCommandDisplay(config.command, config.args);
 908 |             const maxWidth = process.stdout.columns ? process.stdout.columns - 6 : 80; // Leave space for indentation
 909 |             const wrappedLines = TextUtils.wrapTextWithBackground(commandText, maxWidth, `  ${indent} `, (text: string) => chalk.bgGray.black(text));
 910 |             console.log(wrappedLines);
 911 |           }
 912 |         });
 913 |       } else if (depth > 0) {
 914 |         console.log(chalk.dim('  └── (empty)'));
 915 |       }
 916 |       console.log('');
 917 |     }
 918 | 
 919 |     // No cleanup needed for lightweight approach
 920 | 
 921 |   });
 922 | 
 923 | 
 924 | // Helper function to format find command output with consistent color scheme
 925 | function formatFindOutput(text: string): string {
 926 |   return text
 927 |     // Tool names in headers: # **toolname** -> bold light blue
 928 |     .replace(/^# \*\*([^*]+)\*\*/gm, (match, toolName) => chalk.bold.cyanBright(toolName))
 929 |     // Parameters: ### param: type (optional) - description
 930 |     .replace(/^### ([^:]+): (.+)$/gm, (match, param, rest) => {
 931 |       // Handle: type (optional) - description
 932 |       const optionalDescMatch = rest.match(/^(.+?)\s+\*\(optional\)\*\s*-\s*(.+)$/);
 933 |       if (optionalDescMatch) {
 934 |         return `${chalk.yellow(param)}: ${chalk.cyan(optionalDescMatch[1])} ${chalk.dim('(optional)')} - ${chalk.white(optionalDescMatch[2])}`;
 935 |       }
 936 | 
 937 |       // Handle: type - description
 938 |       const descMatch = rest.match(/^(.+?)\s*-\s*(.+)$/);
 939 |       if (descMatch) {
 940 |         return `${chalk.yellow(param)}: ${chalk.cyan(descMatch[1])} - ${chalk.white(descMatch[2])}`;
 941 |       }
 942 | 
 943 |       // Handle: type (optional)
 944 |       const optionalMatch = rest.match(/^(.+)\s+\*\(optional\)\*$/);
 945 |       if (optionalMatch) {
 946 |         return `${chalk.yellow(param)}: ${chalk.cyan(optionalMatch[1])} ${chalk.dim('(optional)')}`;
 947 |       }
 948 | 
 949 |       // Handle: type only
 950 |       return `${chalk.yellow(param)}: ${chalk.cyan(rest)}`;
 951 |     })
 952 |     // Parameter descriptions: #### description -> dim
 953 |     .replace(/^#### (.+)$/gm, (match, desc) => chalk.dim(desc))
 954 |     // Separators: --- -> dim
 955 |     .replace(/^---$/gm, chalk.dim('---'))
 956 |     // Bold text in general: **text** -> bold for tool names in lists
 957 |     .replace(/\*\*([^*]+)\*\*/g, (match, text) => {
 958 |       // Check if it's a tool name (contains colon)
 959 |       if (text.includes(':')) {
 960 |         return chalk.bold.cyanBright(text);
 961 |       } else {
 962 |         // MCP name or other bold text
 963 |         return chalk.bold(text);
 964 |       }
 965 |     })
 966 |     // [no parameters] -> dim
 967 |     .replace(/\*\[no parameters\]\*/g, chalk.dim('[no parameters]'))
 968 |     // Italic text: *text* -> dim for tips
 969 |     .replace(/\*([^*\[]+)\*/g, (match, text) => chalk.dim(text))
 970 |     // Confidence percentages: (XX% match) -> green percentage
 971 |     .replace(/\((\d+)% match\)/g, (match, percentage) => chalk.dim(`(${chalk.green(percentage + '%')} match)`))
 972 |     // Header search results - make query bold white
 973 |     .replace(/Found tools for "([^"]+)"/g, (match, query) => `Found tools for ${chalk.bold.white(`"${query}"`)}`)
 974 |     // No results message
 975 |     .replace(/❌ No tools found for "([^"]+)"/g, (match, query) => `❌ No tools found for ${chalk.bold.white(`"${query}"`)}`)
 976 |     // Usage tips
 977 |     .replace(/^💡 (.+)$/gm, (match, tip) => `💡 ${chalk.white(tip)}`);
 978 | }
 979 | 
 980 | 
 981 | 
 982 | // Remove command
 983 | program
 984 |   .command('remove <name>')
 985 |   .description('Remove an MCP server from profiles')
 986 |   .option('--profile <names...>', 'Profile(s) to remove from (can specify multiple, default: all)')
 987 |   .action(async (name, options) => {
 988 |     console.log(chalk.blue(`🗑️  Removing MCP server: ${chalk.bold(name)}`));
 989 | 
 990 |     const manager = new ProfileManager();
 991 |     await manager.initialize();
 992 | 
 993 |     // ⚠️ CRITICAL: Default MUST be ['all'] - DO NOT CHANGE!
 994 |     const profiles = options.profile || ['all'];
 995 | 
 996 |     // Validate if MCP exists and get suggestions
 997 |     const validation = await validateRemoveCommand(name, manager, profiles);
 998 | 
 999 |     if (!validation.mcpExists) {
1000 |       console.log(chalk.yellow(`⚠️  MCP "${name}" not found in specified profiles`));
1001 | 
1002 |       if (validation.suggestions.length > 0) {
1003 |         console.log(chalk.yellow('\n💡 Did you mean:'));
1004 |         validation.suggestions.forEach((suggestion, index) => {
1005 |           console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
1006 |         });
1007 |         console.log(chalk.yellow('\n💡 Use the exact name from the list above'));
1008 |       } else if (validation.allMCPs.length > 0) {
1009 |         console.log(chalk.yellow('\n📋 Available MCPs in these profiles:'));
1010 |         validation.allMCPs.forEach((mcp, index) => {
1011 |           console.log(`  ${index + 1}. ${chalk.cyan(mcp)}`);
1012 |         });
1013 |       } else {
1014 |         console.log(chalk.dim('\n📋 No MCPs found in specified profiles'));
1015 |         console.log(chalk.dim('💡 Use \'ncp list\' to see all configured MCPs'));
1016 |       }
1017 | 
1018 |       console.log(chalk.yellow('\n⚠️  No changes made'));
1019 |       return;
1020 |     }
1021 | 
1022 |     // MCP exists, proceed with removal
1023 |     console.log(chalk.green('✅ MCP found, proceeding with removal...\n'));
1024 | 
1025 |     // Initialize cache patcher
1026 |     const cachePatcher = new CachePatcher();
1027 | 
1028 |     for (const profileName of profiles) {
1029 |       try {
1030 |         // 1. Remove from profile
1031 |         await manager.removeMCPFromProfile(profileName, name);
1032 |         console.log(OutputFormatter.success(`Removed ${name} from profile: ${profileName}`));
1033 | 
1034 |         // 2. Clean up caches
1035 |         console.log(chalk.dim('🔧 Cleaning up caches...'));
1036 | 
1037 |         try {
1038 |           // Remove from tool metadata cache
1039 |           await cachePatcher.patchRemoveMCP(name);
1040 | 
1041 |           // Remove from embeddings cache
1042 |           await cachePatcher.patchRemoveEmbeddings(name);
1043 | 
1044 |           // Update profile hash
1045 |           const profile = await manager.getProfile(profileName);
1046 |           if (profile) {
1047 |             const profileHash = cachePatcher.generateProfileHash(profile);
1048 |             await cachePatcher.updateProfileHash(profileHash);
1049 |           }
1050 | 
1051 |           console.log(`${chalk.green('✅')} Cache cleaned for ${name}`);
1052 | 
1053 |         } catch (cacheError: any) {
1054 |           console.log(`${chalk.yellow('⚠️')} Could not clean cache: ${cacheError.message}`);
1055 |           console.log(chalk.dim('   Profile updated successfully. Cache will rebuild on next discovery.'));
1056 |         }
1057 | 
1058 |       } catch (error: any) {
1059 |         const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('profile', 'remove', `${name} from ${profileName}`));
1060 |         console.log('\n' + ErrorHandler.formatForConsole(errorResult));
1061 |       }
1062 |     }
1063 |   });
1064 | 
1065 | // Config command group
1066 | const configCmd = program
1067 |   .command('config')
1068 |   .description('Manage NCP configuration (import, validate, edit)');
1069 | 
1070 | configCmd
1071 |   .command('import [file]')
1072 |   .description('Import MCP configurations from file or clipboard')
1073 |   .option('--profile <name>', 'Target profile (default: all)')
1074 |   .option('--dry-run', 'Show what would be imported without actually importing')
1075 |   .action(async (file, options) => {
1076 |     try {
1077 |       const manager = new ConfigManager();
1078 |       await manager.importConfig(file, options.profile, options.dryRun);
1079 |     } catch (error: any) {
1080 |       const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('config', 'import', file || 'clipboard'));
1081 |       console.log('\n' + ErrorHandler.formatForConsole(errorResult));
1082 |       process.exit(1);
1083 |     }
1084 |   });
1085 | 
1086 | configCmd
1087 |   .command('edit')
1088 |   .description('Open config directory in default editor')
1089 |   .action(async () => {
1090 |     const manager = new ConfigManager();
1091 |     await manager.editConfig();
1092 |   });
1093 | 
1094 | configCmd
1095 |   .command('validate')
1096 |   .description('Validate current configuration')
1097 |   .action(async () => {
1098 |     const manager = new ConfigManager();
1099 |     await manager.validateConfig();
1100 |   });
1101 | 
1102 | configCmd
1103 |   .command('location')
1104 |   .description('Show configuration file locations')
1105 |   .action(async () => {
1106 |     const manager = new ConfigManager();
1107 |     await manager.showConfigLocations();
1108 |   });
1109 | 
1110 | 
1111 | // Repair command - fix failed MCPs interactively
1112 | program
1113 |   .command('repair')
1114 |   .description('Interactively configure failed MCPs')
1115 |   .option('--profile <name>', 'Profile to repair (default: all)')
1116 |   .action(async (options) => {
1117 |     try {
1118 |       // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE!
1119 |       const profileName = options.profile || program.getOptionValue('profile') || 'all';
1120 | 
1121 |       console.log(chalk.bold('\n🔧 MCP Repair Tool\n'));
1122 | 
1123 |     // Load failed MCPs from both sources
1124 |     const { getCacheDirectory } = await import('../utils/ncp-paths.js');
1125 |     const { CSVCache } = await import('../cache/csv-cache.js');
1126 |     const { MCPErrorParser } = await import('../utils/mcp-error-parser.js');
1127 |     const { ProfileManager } = await import('../profiles/profile-manager.js');
1128 |     const { MCPWrapper } = await import('../utils/mcp-wrapper.js');
1129 |     const { healthMonitor } = await import('../utils/health-monitor.js');
1130 |     const { readFileSync, existsSync } = await import('fs');
1131 |     const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
1132 |     const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
1133 | 
1134 |     const cache = new CSVCache(getCacheDirectory(), profileName);
1135 |     await cache.initialize();
1136 | 
1137 |     // Load profile first to know which MCPs to check
1138 |     const profileManager = new ProfileManager();
1139 |     await profileManager.initialize();
1140 |     const profile = await profileManager.getProfile(profileName);
1141 | 
1142 |     if (!profile) {
1143 |       console.log(chalk.red(`❌ Profile '${profileName}' not found`));
1144 |       return;
1145 |     }
1146 | 
1147 |     // Merge failed MCPs from both sources
1148 |     const failedMCPs = new Map<string, {
1149 |       errorMessage: string;
1150 |       attemptCount: number;
1151 |       source: 'cache' | 'health' | 'both';
1152 |       lastAttempt: string;
1153 |     }>();
1154 | 
1155 |     // Add from CSV cache
1156 |     const cacheMetadata = (cache as any).metadata;
1157 |     if (cacheMetadata?.failedMCPs) {
1158 |       for (const [mcpName, failedInfo] of cacheMetadata.failedMCPs) {
1159 |         failedMCPs.set(mcpName, {
1160 |           errorMessage: failedInfo.errorMessage,
1161 |           attemptCount: failedInfo.attemptCount,
1162 |           source: 'cache',
1163 |           lastAttempt: failedInfo.lastAttempt
1164 |         });
1165 |       }
1166 |     }
1167 | 
1168 |     // Add from health monitor (unhealthy or disabled)
1169 |     const healthReport = healthMonitor.generateHealthReport();
1170 |     for (const health of healthReport.details) {
1171 |       if (health.status === 'unhealthy' || health.status === 'disabled') {
1172 |         // Only include if it's in the current profile
1173 |         if (profile.mcpServers[health.name]) {
1174 |           const existing = failedMCPs.get(health.name);
1175 |           if (existing) {
1176 |             // Already in cache, mark as both
1177 |             existing.source = 'both';
1178 |           } else {
1179 |             // Only in health monitor
1180 |             failedMCPs.set(health.name, {
1181 |               errorMessage: health.lastError || 'Unknown error',
1182 |               attemptCount: health.errorCount,
1183 |               source: 'health',
1184 |               lastAttempt: health.lastCheck
1185 |             });
1186 |           }
1187 |         }
1188 |       }
1189 |     }
1190 | 
1191 |     if (failedMCPs.size === 0) {
1192 |       console.log(chalk.green('✅ No failed MCPs! Everything is working.'));
1193 |       return;
1194 |     }
1195 | 
1196 |     console.log(chalk.yellow(`Found ${failedMCPs.size} failed MCPs\n`));
1197 |     console.log(chalk.dim('This tool will help you configure them interactively.\n'));
1198 | 
1199 |     const errorParser = new MCPErrorParser();
1200 |     const mcpWrapper = new MCPWrapper();
1201 |     const prompts = (await import('prompts')).default;
1202 | 
1203 |     let fixedCount = 0;
1204 |     let skippedCount = 0;
1205 |     let stillFailingCount = 0;
1206 | 
1207 |     // Iterate through failed MCPs
1208 |     for (const [mcpName, failedInfo] of failedMCPs) {
1209 |       console.log(chalk.cyan(`\n📦 ${mcpName}`));
1210 |       console.log(chalk.dim(`   Last error: ${failedInfo.errorMessage}`));
1211 |       console.log(chalk.dim(`   Failed ${failedInfo.attemptCount} time(s)`));
1212 | 
1213 |       // Show source of failure detection
1214 |       const sourceLabel = failedInfo.source === 'both'
1215 |         ? 'indexing & runtime'
1216 |         : failedInfo.source === 'cache'
1217 |           ? 'indexing'
1218 |           : 'runtime';
1219 |       console.log(chalk.dim(`   Detected during: ${sourceLabel}`));
1220 | 
1221 |       // Ask if user wants to fix this MCP
1222 |       const { shouldFix } = await prompts({
1223 |         type: 'confirm',
1224 |         name: 'shouldFix',
1225 |         message: `Try to fix ${mcpName}?`,
1226 |         initial: true
1227 |       });
1228 | 
1229 |       if (!shouldFix) {
1230 |         skippedCount++;
1231 |         continue;
1232 |       }
1233 | 
1234 |       // Get current MCP config first (needed for all tiers)
1235 |       const currentConfig = profile.mcpServers[mcpName];
1236 |       if (!currentConfig) {
1237 |         console.log(chalk.red(`   ❌ MCP not found in profile`));
1238 |         skippedCount++;
1239 |         continue;
1240 |       }
1241 | 
1242 |       // Skip HTTP/SSE MCPs (they don't have command/args to repair)
1243 |       if (currentConfig.url && !currentConfig.command) {
1244 |         console.log(chalk.yellow(`   ⚠️  Skipping HTTP/SSE MCP (remote connector)`));
1245 |         skippedCount++;
1246 |         continue;
1247 |       }
1248 | 
1249 |       // Two-tier configuration detection (same as ncp add):
1250 |       // Tier 1: Cached schema (from previous successful add or MCP protocol)
1251 |       // Tier 2: Error parsing (fallback)
1252 | 
1253 |       let detectedSchema: any = null;
1254 | 
1255 |       // Check for cached schema
1256 |       const schemaCache = new SchemaCache(getCacheDirectory());
1257 |       detectedSchema = schemaCache.get(mcpName);
1258 | 
1259 |       if (detectedSchema) {
1260 |         console.log(chalk.dim(`   ✓ Using cached configuration schema`));
1261 |       }
1262 | 
1263 |       // If we have a schema, use schema-based prompting
1264 |       if (detectedSchema) {
1265 |         const schemaReader = new ConfigSchemaReader();
1266 |         const configPrompter = new ConfigPrompter();
1267 | 
1268 |         if (schemaReader.hasRequiredConfig(detectedSchema)) {
1269 |           console.log(chalk.cyan(`\n   📋 Configuration required`));
1270 | 
1271 |           const promptedConfig = await configPrompter.promptForConfig(detectedSchema, mcpName);
1272 | 
1273 |           // Update config with prompted values
1274 |           const updatedConfig = {
1275 |             command: currentConfig.command,
1276 |             args: [...(currentConfig.args || []), ...(promptedConfig.arguments || [])],
1277 |             env: { ...(currentConfig.env || {}), ...(promptedConfig.environmentVariables || {}) }
1278 |           };
1279 | 
1280 |           // Save updated config
1281 |           await profileManager.addMCPToProfile(profileName, mcpName, updatedConfig);
1282 | 
1283 |           console.log(chalk.green(`\n   ✅ Configuration updated for ${mcpName}`));
1284 |           fixedCount++;
1285 |           continue;
1286 |         }
1287 |       }
1288 | 
1289 |       // Tier 3: Fallback to error parsing if no schema available
1290 |       const logPath = mcpWrapper.getLogFile(mcpName);
1291 |       let stderr = '';
1292 | 
1293 |       if (existsSync(logPath)) {
1294 |         const logContent = readFileSync(logPath, 'utf-8');
1295 |         // Extract stderr lines
1296 |         const stderrLines = logContent.split('\n').filter(line => line.includes('[STDERR]'));
1297 |         stderr = stderrLines.map(line => line.replace(/\[STDERR\]\s*/, '')).join('\n');
1298 |       } else {
1299 |         stderr = failedInfo.errorMessage;
1300 |       }
1301 | 
1302 |       // Parse errors to detect configuration needs
1303 |       const configNeeds = errorParser.parseError(mcpName, stderr, 1);
1304 | 
1305 |       if (configNeeds.length === 0) {
1306 |         console.log(chalk.yellow(`   ⚠️  Could not detect specific configuration needs`));
1307 |         console.log(chalk.dim(`   Check logs manually: ${logPath}`));
1308 |         skippedCount++;
1309 |         continue;
1310 |       }
1311 | 
1312 |       // Check if it's a missing package
1313 |       const packageMissing = configNeeds.find(n => n.type === 'package_missing');
1314 |       if (packageMissing) {
1315 |         console.log(chalk.red(`   ❌ Package not found on npm - cannot fix`));
1316 |         console.log(chalk.dim(`      ${packageMissing.extractedFrom}`));
1317 |         skippedCount++;
1318 |         continue;
1319 |       }
1320 | 
1321 |       console.log(chalk.yellow(`\n   Found ${configNeeds.length} configuration need(s):`));
1322 |       for (const need of configNeeds) {
1323 |         console.log(chalk.dim(`   • ${need.description}`));
1324 |       }
1325 | 
1326 |       // Collect new configuration from user
1327 |       const newEnv = { ...(currentConfig.env || {}) };
1328 |       const newArgs = [...(currentConfig.args || [])];
1329 | 
1330 |       for (const need of configNeeds) {
1331 |         if (need.type === 'api_key' || need.type === 'env_var') {
1332 |           const { value } = await prompts({
1333 |             type: need.sensitive ? 'password' : 'text',
1334 |             name: 'value',
1335 |             message: need.prompt,
1336 |             validate: (val: string) => val.length > 0 ? true : 'Value required'
1337 |           });
1338 | 
1339 |           if (!value) {
1340 |             console.log(chalk.yellow(`   Skipped ${mcpName}`));
1341 |             skippedCount++;
1342 |             continue;
1343 |           }
1344 | 
1345 |           newEnv[need.variable] = value;
1346 |         } else if (need.type === 'command_arg') {
1347 |           const { value } = await prompts({
1348 |             type: 'text',
1349 |             name: 'value',
1350 |             message: need.prompt,
1351 |             validate: (val: string) => val.length > 0 ? true : 'Value required'
1352 |           });
1353 | 
1354 |           if (!value) {
1355 |             console.log(chalk.yellow(`   Skipped ${mcpName}`));
1356 |             skippedCount++;
1357 |             continue;
1358 |           }
1359 | 
1360 |           newArgs.push(value);
1361 |         }
1362 |       }
1363 | 
1364 |       // Test MCP with new configuration
1365 |       console.log(chalk.dim(`\n   Testing ${mcpName} with new configuration...`));
1366 | 
1367 |       const testConfig = {
1368 |         name: mcpName,
1369 |         command: currentConfig.command,
1370 |         args: newArgs,
1371 |         env: newEnv
1372 |       };
1373 | 
1374 |       try {
1375 |         // Create wrapper command
1376 |         const wrappedCommand = mcpWrapper.createWrapper(
1377 |           testConfig.name,
1378 |           testConfig.command || '',  // Should never be undefined after HTTP/SSE check
1379 |           testConfig.args || []
1380 |         );
1381 | 
1382 |         // Test connection with 30 second timeout
1383 |         const transport = new StdioClientTransport({
1384 |           command: wrappedCommand.command,
1385 |           args: wrappedCommand.args,
1386 |           env: {
1387 |             ...process.env,
1388 |             ...(testConfig.env || {}),
1389 |             MCP_SILENT: 'true',
1390 |             QUIET: 'true'
1391 |           }
1392 |         });
1393 | 
1394 |         const client = new Client({
1395 |           name: 'ncp-repair-test',
1396 |           version: '1.0.0'
1397 |         }, {
1398 |           capabilities: {}
1399 |         });
1400 | 
1401 |         await client.connect(transport);
1402 | 
1403 |         // Try to list tools
1404 |         const result = await Promise.race([
1405 |           client.listTools(),
1406 |           new Promise<never>((_, reject) =>
1407 |             setTimeout(() => reject(new Error('Test timeout')), 30000)
1408 |           )
1409 |         ]);
1410 | 
1411 |         await client.close();
1412 | 
1413 |         console.log(chalk.green(`   ✅ Success! Found ${result.tools.length} tools`));
1414 | 
1415 |         // Update profile with new configuration
1416 |         profile.mcpServers[mcpName] = {
1417 |           command: testConfig.command,
1418 |           args: newArgs,
1419 |           env: newEnv
1420 |         };
1421 |         profile.metadata.modified = new Date().toISOString();
1422 | 
1423 |         await profileManager.saveProfile(profile);
1424 | 
1425 |         // Remove from both failure tracking systems
1426 |         if (cacheMetadata?.failedMCPs) {
1427 |           cacheMetadata.failedMCPs.delete(mcpName);
1428 |           (cache as any).saveMetadata();
1429 |         }
1430 | 
1431 |         // Clear from health monitor and mark as healthy
1432 |         await healthMonitor.enableMCP(mcpName);
1433 |         healthMonitor.markHealthy(mcpName);
1434 |         await healthMonitor.saveHealth();
1435 | 
1436 |         fixedCount++;
1437 |       } catch (error: any) {
1438 |         console.log(chalk.red(`   ❌ Still failing: ${error.message}`));
1439 |         stillFailingCount++;
1440 |       }
1441 |     }
1442 | 
1443 |     // Final report
1444 |     console.log(chalk.bold('\n📊 Repair Summary\n'));
1445 |     console.log(chalk.green(`✅ Fixed: ${fixedCount}`));
1446 |     console.log(chalk.yellow(`⏭️  Skipped: ${skippedCount}`));
1447 |     console.log(chalk.red(`❌ Still failing: ${stillFailingCount}`));
1448 | 
1449 |     if (fixedCount > 0) {
1450 |       console.log(chalk.dim('\n💡 Run "ncp find --force-retry" to re-index fixed MCPs'));
1451 |     }
1452 |     } catch (error: any) {
1453 |       console.error(chalk.red('\n❌ Repair command failed:'), error.message);
1454 |       console.error(chalk.dim(error.stack));
1455 |       process.exit(1);
1456 |     }
1457 |   });
1458 | 
1459 | // Find command (CLI-optimized version for fast discovery)
1460 | program
1461 |   .command('find [query]')
1462 |   .description('Find tools matching a query or list all tools')
1463 |   .option('--limit <number>', 'Maximum number of results (default: 5)')
1464 |   .option('--page <number>', 'Page number (default: 1)')
1465 |   .option('--depth <number>', 'Display depth: 0=overview, 1=tools, 2=details (default: 2)')
1466 |   .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')
1467 |   .action(async (query, options) => {
1468 |     // Add newline after command before any output
1469 |     console.log();
1470 | 
1471 |     // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE!
1472 |     const profileName = program.getOptionValue('profile') || 'all';
1473 |     const forceRetry = program.getOptionValue('forceRetry') || false;
1474 | 
1475 |     // Use MCPServer for rich formatted output
1476 |     const { MCPServer } = await import('../server/mcp-server.js');
1477 |     const server = new MCPServer(profileName, true, forceRetry); // Enable progress + force retry flag
1478 | 
1479 |     // Setup graceful shutdown on Ctrl+C
1480 |     const gracefulShutdown = async () => {
1481 |       process.stdout.write('\n\n💾 Saving progress...');
1482 |       try {
1483 |         await server.cleanup();
1484 |         process.stdout.write('\r\u001B[K✅ Progress saved\n');
1485 |       } catch (error) {
1486 |         process.stdout.write('\r\u001B[K❌ Error saving progress\n');
1487 |       }
1488 |       process.exit(0);
1489 |     };
1490 | 
1491 |     process.on('SIGINT', gracefulShutdown);
1492 |     process.on('SIGTERM', gracefulShutdown);
1493 | 
1494 |     await server.initialize();
1495 | 
1496 |     // For CLI usage, wait for indexing to complete before searching
1497 |     await server.waitForInitialization();
1498 | 
1499 |     const limit = parseInt(options.limit || '5');
1500 |     const page = parseInt(options.page || '1');
1501 |     const depth = parseInt(options.depth || '2');
1502 |     const confidence_threshold = options.confidence_threshold ? parseFloat(options.confidence_threshold) : undefined;
1503 | 
1504 |     const result = await server.handleFind(
1505 |       { jsonrpc: '2.0', id: 'cli', method: 'tools/call' },
1506 |       { description: query || '', limit, page, depth, confidence_threshold }
1507 |     );
1508 | 
1509 |     const formattedOutput = formatFindOutput(result.result.content[0].text);
1510 |     console.log(formattedOutput);
1511 |     await server.cleanup();
1512 |   });
1513 | 
1514 | // Analytics command group
1515 | const analyticsCmd = program
1516 |   .command('analytics')
1517 |   .description('View NCP usage analytics and performance metrics');
1518 | 
1519 | analyticsCmd
1520 |   .command('dashboard')
1521 |   .description('Show comprehensive analytics dashboard')
1522 |   .option('--period <days>', 'Show data for last N days (e.g., --period 7)')
1523 |   .option('--from <date>', 'Start date (YYYY-MM-DD format)')
1524 |   .option('--to <date>', 'End date (YYYY-MM-DD format)')
1525 |   .option('--today', 'Show only today\'s data')
1526 |   .option('--visual', 'Enhanced visual dashboard with charts and graphs')
1527 |   .action(async (options) => {
1528 |     const { NCPLogParser } = await import('../analytics/log-parser.js');
1529 | 
1530 |     console.log(chalk.dim('📊 Analyzing NCP usage data...'));
1531 | 
1532 |     const parser = new NCPLogParser();
1533 | 
1534 |     // Parse time range options
1535 |     const parseOptions: any = {};
1536 | 
1537 |     if (options.today) {
1538 |       parseOptions.today = true;
1539 |     } else if (options.period) {
1540 |       parseOptions.period = parseInt(options.period);
1541 |     } else if (options.from || options.to) {
1542 |       if (options.from) parseOptions.from = new Date(options.from);
1543 |       if (options.to) parseOptions.to = new Date(options.to);
1544 |     }
1545 | 
1546 |     const report = await parser.parseAllLogs(parseOptions);
1547 | 
1548 |     if (report.totalSessions === 0) {
1549 |       console.log(chalk.yellow('📊 No analytics data available for the specified time range'));
1550 |       console.log(chalk.dim('💡 Try a different time range or check if MCPs have been used through NCP'));
1551 |       return;
1552 |     }
1553 | 
1554 |     if (options.visual) {
1555 |       const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js');
1556 |       const dashboard = await VisualAnalyticsFormatter.formatVisualDashboard(report);
1557 |       console.log(dashboard);
1558 |     } else {
1559 |       const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js');
1560 |       const dashboard = AnalyticsFormatter.formatDashboard(report);
1561 |       console.log(dashboard);
1562 |     }
1563 |   });
1564 | 
1565 | analyticsCmd
1566 |   .command('performance')
1567 |   .description('Show performance-focused analytics')
1568 |   .option('--period <days>', 'Show data for last N days (e.g., --period 7)')
1569 |   .option('--from <date>', 'Start date (YYYY-MM-DD format)')
1570 |   .option('--to <date>', 'End date (YYYY-MM-DD format)')
1571 |   .option('--today', 'Show only today\'s data')
1572 |   .option('--visual', 'Enhanced visual performance report with gauges and charts')
1573 |   .action(async (options) => {
1574 |     const { NCPLogParser } = await import('../analytics/log-parser.js');
1575 | 
1576 |     console.log(chalk.dim('⚡ Analyzing performance metrics...'));
1577 | 
1578 |     const parser = new NCPLogParser();
1579 | 
1580 |     // Parse time range options
1581 |     const parseOptions: any = {};
1582 |     if (options.today) {
1583 |       parseOptions.today = true;
1584 |     } else if (options.period) {
1585 |       parseOptions.period = parseInt(options.period);
1586 |     } else if (options.from || options.to) {
1587 |       if (options.from) parseOptions.from = new Date(options.from);
1588 |       if (options.to) parseOptions.to = new Date(options.to);
1589 |     }
1590 | 
1591 |     const report = await parser.parseAllLogs(parseOptions);
1592 | 
1593 |     if (report.totalSessions === 0) {
1594 |       console.log(chalk.yellow('📊 No performance data available for the specified time range'));
1595 |       return;
1596 |     }
1597 | 
1598 |     if (options.visual) {
1599 |       const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js');
1600 |       const performance = await VisualAnalyticsFormatter.formatVisualPerformance(report);
1601 |       console.log(performance);
1602 |     } else {
1603 |       const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js');
1604 |       const performance = AnalyticsFormatter.formatPerformanceReport(report);
1605 |       console.log(performance);
1606 |     }
1607 |   });
1608 | 
1609 | analyticsCmd
1610 |   .command('visual')
1611 |   .description('Show enhanced visual analytics with charts and graphs')
1612 |   .option('--period <days>', 'Show data for last N days (e.g., --period 7)')
1613 |   .option('--from <date>', 'Start date (YYYY-MM-DD format)')
1614 |   .option('--to <date>', 'End date (YYYY-MM-DD format)')
1615 |   .option('--today', 'Show only today\'s data')
1616 |   .action(async (options) => {
1617 |     const { NCPLogParser } = await import('../analytics/log-parser.js');
1618 |     const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js');
1619 | 
1620 |     console.log(chalk.dim('🎨 Generating visual analytics...'));
1621 | 
1622 |     const parser = new NCPLogParser();
1623 | 
1624 |     // Parse time range options
1625 |     const parseOptions: any = {};
1626 |     if (options.today) {
1627 |       parseOptions.today = true;
1628 |     } else if (options.period) {
1629 |       parseOptions.period = parseInt(options.period);
1630 |     } else if (options.from || options.to) {
1631 |       if (options.from) parseOptions.from = new Date(options.from);
1632 |       if (options.to) parseOptions.to = new Date(options.to);
1633 |     }
1634 | 
1635 |     const report = await parser.parseAllLogs(parseOptions);
1636 | 
1637 |     if (report.totalSessions === 0) {
1638 |       console.log(chalk.yellow('📊 No analytics data available for the specified time range'));
1639 |       console.log(chalk.dim('💡 Try a different time range or check if MCPs have been used through NCP'));
1640 |       return;
1641 |     }
1642 | 
1643 |     const dashboard = await VisualAnalyticsFormatter.formatVisualDashboard(report);
1644 |     console.log(dashboard);
1645 |   });
1646 | 
1647 | analyticsCmd
1648 |   .command('export')
1649 |   .description('Export analytics data to CSV')
1650 |   .option('--output <file>', 'Output file (default: ncp-analytics.csv)')
1651 |   .option('--period <days>', 'Export data for last N days (e.g., --period 7)')
1652 |   .option('--from <date>', 'Start date (YYYY-MM-DD format)')
1653 |   .option('--to <date>', 'End date (YYYY-MM-DD format)')
1654 |   .option('--today', 'Export only today\'s data')
1655 |   .action(async (options) => {
1656 |     const { NCPLogParser } = await import('../analytics/log-parser.js');
1657 |     const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js');
1658 |     const { writeFileSync } = await import('fs');
1659 | 
1660 |     console.log(chalk.dim('📊 Generating analytics export...'));
1661 | 
1662 |     const parser = new NCPLogParser();
1663 | 
1664 |     // Parse time range options
1665 |     const parseOptions: any = {};
1666 |     if (options.today) {
1667 |       parseOptions.today = true;
1668 |     } else if (options.period) {
1669 |       parseOptions.period = parseInt(options.period);
1670 |     } else if (options.from || options.to) {
1671 |       if (options.from) parseOptions.from = new Date(options.from);
1672 |       if (options.to) parseOptions.to = new Date(options.to);
1673 |     }
1674 | 
1675 |     const report = await parser.parseAllLogs(parseOptions);
1676 | 
1677 |     if (report.totalSessions === 0) {
1678 |       console.log(chalk.yellow('📊 No data to export for the specified time range'));
1679 |       return;
1680 |     }
1681 | 
1682 |     const csv = AnalyticsFormatter.formatCSV(report);
1683 |     const filename = options.output || 'ncp-analytics.csv';
1684 | 
1685 |     writeFileSync(filename, csv, 'utf-8');
1686 |     console.log(chalk.green(`✅ Analytics exported to ${filename}`));
1687 |     console.log(chalk.dim(`📊 Exported ${report.totalSessions} sessions across ${report.uniqueMCPs} MCPs`));
1688 |   });
1689 | 
1690 | // Run command (existing functionality)
1691 | program
1692 |   .command('run <tool>')
1693 | 
1694 |   .description('Run a specific tool')
1695 |   .option('--params <json>', 'Tool parameters as JSON string (optional - will prompt interactively if not provided)')
1696 |   .option('--no-prompt', 'Skip interactive prompting for missing parameters')
1697 |   .option('--output-format <format>', 'Output format: auto (smart rendering), json (raw JSON)', 'auto')
1698 |   .option('-y, --yes', 'Automatically answer yes to prompts (e.g., open media files)')
1699 |   .configureHelp({
1700 |     formatHelp: (cmd, helper) => {
1701 |       const indent = '  ';
1702 |       let output = '\n';
1703 | 
1704 |       // Header first - context before syntax
1705 |       output += chalk.bold.white('NCP Run Command') + ' - ' + chalk.cyan('Direct MCP Tool Execution') + '\n\n';
1706 |       output += chalk.dim('Execute MCP tools with intelligent parameter prompting and rich media support.') + '\n';
1707 |       output += chalk.dim('Automatically handles parameter collection, validation, and response formatting.') + '\n\n';
1708 | 
1709 |       // Then usage
1710 |       output += chalk.bold.white('Usage:') + ' ' + helper.commandUsage(cmd) + '\n\n';
1711 | 
1712 |       const visibleOptions = helper.visibleOptions(cmd);
1713 |       if (visibleOptions.length) {
1714 |         output += chalk.bold.white('Options:') + '\n';
1715 |         visibleOptions.forEach(option => {
1716 |           const flags = option.flags;
1717 |           const description = helper.optionDescription(option);
1718 |           const paddingNeeded = Math.max(0, 42 - flags.length);
1719 |           const padding = ' '.repeat(paddingNeeded);
1720 |           output += indent + chalk.cyan(flags) + padding + ' ' + chalk.white(description) + '\n';
1721 |         });
1722 |         output += '\n';
1723 |       }
1724 | 
1725 |       // Examples section
1726 |       output += chalk.bold.white('Examples:') + '\n';
1727 |       output += chalk.dim('  Basic execution:') + '\n';
1728 |       output += indent + chalk.yellow('ncp run memory:create_entities') + chalk.gray('  # Interactive parameter prompting') + '\n';
1729 |       output += indent + chalk.yellow('ncp run memory:create_entities --params \'{"entities":["item1"]}\'') + '\n\n';
1730 | 
1731 |       output += chalk.dim('  Output control:') + '\n';
1732 |       output += indent + chalk.yellow('ncp run tool --output-format json') + chalk.gray('  # Raw JSON output') + '\n';
1733 |       output += indent + chalk.yellow('ncp run tool -y') + chalk.gray('  # Auto-open media files') + '\n\n';
1734 | 
1735 |       output += chalk.dim('  Non-interactive:') + '\n';
1736 |       output += indent + chalk.yellow('ncp run tool --no-prompt --params \'{}\'') + chalk.gray('  # Scripting/automation') + '\n\n';
1737 | 
1738 |       // Media support note
1739 |       output += chalk.bold.white('Media Support:') + '\n';
1740 |       output += chalk.dim('  • Images and audio are displayed with metadata') + '\n';
1741 |       output += chalk.dim('  • Use') + chalk.cyan(' -y ') + chalk.dim('to auto-open media in default applications') + '\n';
1742 |       output += chalk.dim('  • Without') + chalk.cyan(' -y') + chalk.dim(', prompts before opening media files') + '\n\n';
1743 | 
1744 |       return output;
1745 |     }
1746 |   })
1747 |   .action(async (tool, options) => {
1748 |     // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE!
1749 |     const profileName = program.getOptionValue('profile') || 'all';
1750 | 
1751 |     const { NCPOrchestrator } = await import('../orchestrator/ncp-orchestrator.js');
1752 |     const orchestrator = new NCPOrchestrator(profileName, false); // Silent indexing for run command
1753 | 
1754 |     await orchestrator.initialize();
1755 | 
1756 |     // If tool doesn't contain a colon, try to find matching tools first
1757 |     if (!tool.includes(':')) {
1758 |       console.log(chalk.dim(`🔍 Searching for tools matching "${tool}"...`));
1759 | 
1760 |       try {
1761 |         const matchingTools = await orchestrator.find(tool, 5, false);
1762 | 
1763 |         if (matchingTools.length === 0) {
1764 |           console.log('\n' + OutputFormatter.error(`No tools found matching "${tool}"`));
1765 |           console.log(chalk.yellow('💡 Try \'ncp find\' to explore all available tools'));
1766 |           await orchestrator.cleanup();
1767 |           return;
1768 |         }
1769 | 
1770 |         if (matchingTools.length === 1) {
1771 |           // Only one match, use it automatically
1772 |           const matchedTool = matchingTools[0];
1773 |           tool = matchedTool.toolName;
1774 |           console.log(chalk.green(`✅ Found exact match: ${tool}`));
1775 |         } else {
1776 |           // Multiple matches, show them and ask user to be more specific
1777 |           console.log(chalk.yellow(`Found ${matchingTools.length} matching tools:`));
1778 |           matchingTools.forEach((match, index) => {
1779 |             const confidence = Math.round(match.confidence * 100);
1780 |             console.log(`  ${index + 1}. ${chalk.cyan(match.toolName)} (${confidence}% match)`);
1781 |             if (match.description) {
1782 |               console.log(`     ${chalk.dim(match.description)}`);
1783 |             }
1784 |           });
1785 |           console.log(chalk.yellow('\n💡 Please specify the exact tool name from the list above'));
1786 |           console.log(chalk.yellow(`💡 Example: ncp run ${matchingTools[0].toolName}`));
1787 |           await orchestrator.cleanup();
1788 |           return;
1789 |         }
1790 |       } catch (error: any) {
1791 |         console.log('\n' + OutputFormatter.error(`Error searching for tools: ${error.message}`));
1792 |         await orchestrator.cleanup();
1793 |         return;
1794 |       }
1795 |     }
1796 | 
1797 |     // Check if parameters are provided
1798 |     let parameters = {};
1799 |     if (options.params) {
1800 |       parameters = JSON.parse(options.params);
1801 |     } else {
1802 |       // Get tool schema and parameters
1803 |       const toolParams = orchestrator.getToolParameters(tool);
1804 | 
1805 |       if (toolParams && toolParams.length > 0) {
1806 |         const requiredParams = toolParams.filter(p => p.required);
1807 | 
1808 |         if (requiredParams.length > 0 && options.prompt !== false) {
1809 |           // Interactive prompting for parameters (default behavior)
1810 |           const { ParameterPrompter } = await import('../utils/parameter-prompter.js');
1811 |           const { ParameterPredictor } = await import('../server/mcp-server.js');
1812 | 
1813 |           const prompter = new ParameterPrompter();
1814 |           const predictor = new ParameterPredictor();
1815 |           const toolContext = orchestrator.getToolContext(tool);
1816 | 
1817 |           try {
1818 |             parameters = await prompter.promptForParameters(tool, toolParams, predictor, toolContext);
1819 |             prompter.close();
1820 |           } catch (error) {
1821 |             prompter.close();
1822 |             console.log('\n' + OutputFormatter.error('Error during parameter input'));
1823 |             await orchestrator.cleanup();
1824 |             return;
1825 |           }
1826 |         } else if (requiredParams.length > 0 && options.prompt === false) {
1827 |           console.log('\n' + OutputFormatter.error('This tool requires parameters'));
1828 |           console.log(chalk.yellow(`💡 Use: ncp run ${tool} --params '{"param": "value"}'`));
1829 |           console.log(chalk.yellow(`💡 Or use: ncp find "${tool}" --depth 2 to see required parameters`));
1830 |           console.log(chalk.yellow(`💡 Or remove --no-prompt to use interactive prompting`));
1831 |           await orchestrator.cleanup();
1832 |           return;
1833 |         }
1834 |       }
1835 |     }
1836 | 
1837 |     console.log(OutputFormatter.running(tool) + '\n');
1838 | 
1839 |     const result = await orchestrator.run(tool, parameters);
1840 | 
1841 |     if (result.success) {
1842 |       // Check if the content indicates an actual error despite "success" status
1843 |       const contentStr = JSON.stringify(result.content);
1844 |       const isActualError = contentStr.includes('"type":"text"') &&
1845 |                            (contentStr.includes('Error:') || contentStr.includes('not found') || contentStr.includes('Unknown tool'));
1846 | 
1847 |       if (isActualError) {
1848 |         const errorText = result.content?.[0]?.text || 'Unknown error occurred';
1849 |         let suggestions: string[] = [];
1850 | 
1851 |         if (errorText.includes('not configured') || errorText.includes('Unknown tool')) {
1852 |           // Extract the query from the tool name for vector search
1853 |           const [mcpName, toolName] = tool.split(':');
1854 | 
1855 |           // Try multiple search strategies to find the best matches
1856 |           let similarTools: any[] = [];
1857 | 
1858 |           try {
1859 |             // Strategy 1: Search with both MCP context and tool name for better domain matching
1860 |             if (toolName && mcpName) {
1861 |               const contextualQuery = `${mcpName} ${toolName}`;
1862 |               similarTools = await orchestrator.find(contextualQuery, 3, false);
1863 |             }
1864 | 
1865 |             // Strategy 2: If no results, try just the tool name
1866 |             if (similarTools.length === 0 && toolName) {
1867 |               similarTools = await orchestrator.find(toolName, 3, false);
1868 |             }
1869 | 
1870 |             // Strategy 3: If still no results, try just the MCP name (domain search)
1871 |             if (similarTools.length === 0) {
1872 |               similarTools = await orchestrator.find(mcpName, 3, false);
1873 |             }
1874 | 
1875 |             if (similarTools.length > 0) {
1876 |               suggestions.push('💡 Did you mean:');
1877 |               similarTools.forEach(similar => {
1878 |                 const confidence = Math.round(similar.confidence * 100);
1879 |                 suggestions.push(`  • ${similar.toolName} (${confidence}% match)`);
1880 |               });
1881 |             }
1882 |           } catch (error: any) {
1883 |             // Fallback to basic suggestions if vector search fails
1884 |             suggestions = ['Try \'ncp find <keyword>\' to discover similar tools'];
1885 |           }
1886 |         }
1887 | 
1888 |         const context = ErrorHandler.createContext('mcp', 'run', tool, suggestions);
1889 |         const errorResult = ErrorHandler.handle(errorText, context);
1890 |         console.log('\n' + ErrorHandler.formatForConsole(errorResult));
1891 |       } else {
1892 |         console.log(OutputFormatter.success('Tool execution completed'));
1893 | 
1894 |         // Respect user's output format choice
1895 |         if (options.outputFormat === 'json') {
1896 |           // Raw JSON output - test different formatters to pick the best
1897 |           const { formatJson } = await import('../utils/highlighting.js');
1898 |           console.log(formatJson(result.content, 'cli-highlight')); // Let's test this one
1899 |         } else {
1900 |           // Smart response formatting (default)
1901 |           const { ResponseFormatter } = await import('../utils/response-formatter.js');
1902 | 
1903 |           // Check if this is text content that should be formatted naturally
1904 |           const isTextResponse = Array.isArray(result.content) &&
1905 |                                 result.content.every((item: any) => item?.type === 'text');
1906 | 
1907 |           if (isTextResponse || (result.content?.[0]?.type === 'text' && result.content.length === 1)) {
1908 |             // Format as natural text with proper newlines
1909 |             console.log(ResponseFormatter.format(result.content, true, options.yes));
1910 |           } else if (ResponseFormatter.isPureData(result.content)) {
1911 |             // Pure data - use JSON formatting
1912 |             const { formatJson } = await import('../utils/highlighting.js');
1913 |             console.log(formatJson(result.content, 'cli-highlight'));
1914 |           } else {
1915 |             // Mixed content or unknown - use smart formatter
1916 |             console.log(ResponseFormatter.format(result.content, true, options.yes));
1917 |           }
1918 |         }
1919 |       }
1920 |     } else {
1921 |       // Check if this is a tool not found error and provide "did you mean" suggestions
1922 |       const errorMessage = result.error || 'Unknown error occurred';
1923 |       let suggestions: string[] = [];
1924 | 
1925 |       if (errorMessage.includes('not configured') || errorMessage.includes('Unknown tool')) {
1926 |         // Extract the query from the tool name for vector search
1927 |         const [mcpName, toolName] = tool.split(':');
1928 | 
1929 |         // Try multiple search strategies to find the best matches
1930 |         let similarTools: any[] = [];
1931 | 
1932 |         try {
1933 |           // Strategy 1: Search with both MCP context and tool name for better domain matching
1934 |           if (toolName && mcpName) {
1935 |             const contextualQuery = `${mcpName} ${toolName}`;
1936 |             similarTools = await orchestrator.find(contextualQuery, 3, false);
1937 |           }
1938 | 
1939 |           // Strategy 2: If no results, try just the tool name
1940 |           if (similarTools.length === 0 && toolName) {
1941 |             similarTools = await orchestrator.find(toolName, 3, false);
1942 |           }
1943 | 
1944 |           // Strategy 3: If still no results, try just the MCP name (domain search)
1945 |           if (similarTools.length === 0) {
1946 |             similarTools = await orchestrator.find(mcpName, 3, false);
1947 |           }
1948 |           if (similarTools.length > 0) {
1949 |             suggestions.push('💡 Did you mean:');
1950 |             similarTools.forEach(similar => {
1951 |               const confidence = Math.round(similar.confidence * 100);
1952 |               suggestions.push(`  • ${similar.toolName} (${confidence}% match)`);
1953 |             });
1954 |           }
1955 |         } catch (error: any) {
1956 |           // Fallback to basic suggestions if vector search fails
1957 |           suggestions = ['Try \'ncp find <keyword>\' to discover similar tools'];
1958 |         }
1959 |       }
1960 | 
1961 |       const context = ErrorHandler.createContext('mcp', 'run', tool, suggestions);
1962 |       const errorResult = ErrorHandler.handle(errorMessage, context);
1963 |       console.log('\n' + ErrorHandler.formatForConsole(errorResult));
1964 |     }
1965 | 
1966 |     await orchestrator.cleanup();
1967 |   });
1968 | 
1969 | // Auth command
1970 | program
1971 |   .command('auth <mcp>')
1972 |   .description('Authenticate an MCP server using OAuth Device Flow')
1973 |   .option('--profile <name>', 'Profile to use (default: all)')
1974 |   .configureHelp({
1975 |     formatHelp: () => {
1976 |       let output = '\n';
1977 |       output += chalk.bold.white('NCP Auth Command') + ' - ' + chalk.cyan('OAuth Authentication') + '\n\n';
1978 |       output += chalk.dim('Authenticate an MCP server that requires OAuth 2.0 Device Flow authentication.') + '\n';
1979 |       output += chalk.dim('Tokens are securely stored and automatically refreshed.') + '\n\n';
1980 |       output += chalk.bold.white('Usage:') + '\n';
1981 |       output += '  ' + chalk.yellow('ncp auth <mcp>') + '          # Authenticate an MCP server\n';
1982 |       output += '  ' + chalk.yellow('ncp auth <mcp> --profile <name>') + '   # Authenticate for specific profile\n\n';
1983 |       output += chalk.bold.white('Examples:') + '\n';
1984 |       output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp auth github') + '\n';
1985 |       output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp auth my-api --profile production') + '\n\n';
1986 |       return output;
1987 |     }
1988 |   })
1989 |   .action(async (mcpName, options) => {
1990 |     try {
1991 |       const profileName = options.profile || 'all';
1992 | 
1993 |       // Load profile
1994 |       const manager = new ProfileManager();
1995 |       await manager.initialize();
1996 | 
1997 |       const profile = await manager.getProfile(profileName);
1998 |       if (!profile) {
1999 |         console.error(chalk.red(`❌ Profile '${profileName}' not found`));
2000 |         process.exit(1);
2001 |       }
2002 | 
2003 |       // Check if MCP exists in profile
2004 |       const mcpConfig = profile.mcpServers[mcpName];
2005 |       if (!mcpConfig) {
2006 |         console.error(chalk.red(`❌ MCP '${mcpName}' not found in profile '${profileName}'`));
2007 | 
2008 |         // Suggest similar MCPs
2009 |         const availableMCPs = Object.keys(profile.mcpServers);
2010 |         if (availableMCPs.length > 0) {
2011 |           const suggestions = findSimilarNames(mcpName, availableMCPs);
2012 |           if (suggestions.length > 0) {
2013 |             console.log(chalk.yellow('\n💡 Did you mean:'));
2014 |             suggestions.forEach((suggestion, index) => {
2015 |               console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
2016 |             });
2017 |           }
2018 |         }
2019 |         process.exit(1);
2020 |       }
2021 | 
2022 |       // Check if MCP has OAuth configuration
2023 |       if (!mcpConfig.auth || mcpConfig.auth.type !== 'oauth' || !mcpConfig.auth.oauth) {
2024 |         console.error(chalk.red(`❌ MCP '${mcpName}' does not have OAuth configuration`));
2025 |         console.log(chalk.yellow('\n💡 To add OAuth configuration, edit your profile configuration file:'));
2026 |         console.log(chalk.dim('   Add "auth": { "type": "oauth", "oauth": { ... } } to the MCP configuration'));
2027 |         process.exit(1);
2028 |       }
2029 | 
2030 |       // Perform OAuth Device Flow
2031 |       const { DeviceFlowAuthenticator } = await import('../auth/oauth-device-flow.js');
2032 |       const { getTokenStore } = await import('../auth/token-store.js');
2033 | 
2034 |       const authenticator = new DeviceFlowAuthenticator(mcpConfig.auth.oauth);
2035 |       const tokenStore = getTokenStore();
2036 | 
2037 |       console.log(chalk.blue(`🔐 Starting OAuth Device Flow for '${mcpName}'...\n`));
2038 | 
2039 |       try {
2040 |         const tokenResponse = await authenticator.authenticate();
2041 | 
2042 |         // Store the token
2043 |         await tokenStore.storeToken(mcpName, tokenResponse);
2044 | 
2045 |         console.log(chalk.green(`✅ Successfully authenticated '${mcpName}'!`));
2046 |         console.log(chalk.dim(`   Token expires: ${new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()}`));
2047 |         console.log(chalk.dim(`   Token stored securely in: ~/.ncp/tokens/${mcpName}.token`));
2048 |       } catch (error: any) {
2049 |         console.error(chalk.red(`❌ Authentication failed: ${error.message}`));
2050 |         process.exit(1);
2051 |       }
2052 |     } catch (error: any) {
2053 |       console.error(chalk.red(`❌ Error: ${error.message}`));
2054 |       process.exit(1);
2055 |     }
2056 |   });
2057 | 
2058 | // Update command
2059 | program
2060 |   .command('update')
2061 |   .description('Update NCP to the latest version')
2062 |   .option('--check', 'Check for updates without installing')
2063 |   .configureHelp({
2064 |     formatHelp: () => {
2065 |       let output = '\n';
2066 |       output += chalk.bold.white('NCP Update Command') + ' - ' + chalk.cyan('Version Management') + '\n\n';
2067 |       output += chalk.dim('Keep NCP up to date with the latest features and bug fixes.') + '\n\n';
2068 |       output += chalk.bold.white('Usage:') + '\n';
2069 |       output += '  ' + chalk.yellow('ncp update') + '          # Update to latest version\n';
2070 |       output += '  ' + chalk.yellow('ncp update --check') + '   # Check for updates without installing\n\n';
2071 |       output += chalk.bold.white('Examples:') + '\n';
2072 |       output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp update --check') + '\n';
2073 |       output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp update') + '\n\n';
2074 |       return output;
2075 |     }
2076 |   })
2077 |   .action(async (options) => {
2078 |     try {
2079 |       const updateChecker = new UpdateChecker();
2080 | 
2081 |       if (options.check) {
2082 |         // Check for updates only
2083 |         console.log(chalk.blue('🔍 Checking for updates...'));
2084 |         const result = await updateChecker.checkForUpdates(true);
2085 | 
2086 |         if (result.hasUpdate) {
2087 |           console.log(chalk.yellow('📦 Update Available!'));
2088 |           console.log(chalk.dim(`   Current: ${result.currentVersion}`));
2089 |           console.log(chalk.green(`   Latest:  ${result.latestVersion}`));
2090 |           console.log();
2091 |           console.log(chalk.cyan('   Run: ncp update'));
2092 |         } else {
2093 |           console.log(chalk.green('✅ You are using the latest version!'));
2094 |           console.log(chalk.dim(`   Version: ${result.currentVersion}`));
2095 |         }
2096 |       } else {
2097 |         // Perform update
2098 |         const result = await updateChecker.checkForUpdates(true);
2099 | 
2100 |         if (result.hasUpdate) {
2101 |           console.log(chalk.yellow('📦 Update Available!'));
2102 |           console.log(chalk.dim(`   Current: ${result.currentVersion}`));
2103 |           console.log(chalk.green(`   Latest:  ${result.latestVersion}`));
2104 |           console.log();
2105 | 
2106 |           const success = await updateChecker.performUpdate();
2107 |           if (!success) {
2108 |             process.exit(1);
2109 |           }
2110 |         } else {
2111 |           console.log(chalk.green('✅ You are already using the latest version!'));
2112 |           console.log(chalk.dim(`   Version: ${result.currentVersion}`));
2113 |         }
2114 |       }
2115 |     } catch (error) {
2116 |       console.error(chalk.red('❌ Failed to check for updates:'), error);
2117 |       process.exit(1);
2118 |     }
2119 |   });
2120 | 
2121 | // Check for updates on CLI startup (non-intrusive)
2122 | // Temporarily disabled - causing hangs in some environments
2123 | // TODO: Re-enable with proper timeout handling
2124 | // (async () => {
2125 | //   try {
2126 | //     const updateChecker = new UpdateChecker();
2127 | //     await updateChecker.showUpdateNotification();
2128 | //   } catch {
2129 | //     // Silently fail - don't interrupt normal CLI usage
2130 | //   }
2131 | // })();
2132 | 
2133 | program.parse();
2134 | }
```
Page 12/12FirstPrevNextLast