#
tokens: 35791/50000 2/189 files (page 9/9)
lines: off (toggle) GitHub
raw markdown copy
This is page 9 of 9. Use http://codebase.md/portel-dev/ncp?page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .dxtignore
├── .github
│   ├── FEATURE_STORY_TEMPLATE.md
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── mcp_server_request.yml
│   ├── pull_request_template.md
│   └── workflows
│       ├── ci.yml
│       ├── publish-mcp-registry.yml
│       └── release.yml
├── .gitignore
├── .mcpbignore
├── .npmignore
├── .release-it.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COMPLETE-IMPLEMENTATION-SUMMARY.md
├── CONTRIBUTING.md
├── CRITICAL-ISSUES-FOUND.md
├── docs
│   ├── clients
│   │   ├── claude-desktop.md
│   │   ├── cline.md
│   │   ├── continue.md
│   │   ├── cursor.md
│   │   ├── perplexity.md
│   │   └── README.md
│   ├── download-stats.md
│   ├── guides
│   │   ├── clipboard-security-pattern.md
│   │   ├── how-it-works.md
│   │   ├── mcp-prompts-for-user-interaction.md
│   │   ├── mcpb-installation.md
│   │   ├── ncp-registry-command.md
│   │   ├── pre-release-checklist.md
│   │   ├── telemetry-design.md
│   │   └── testing.md
│   ├── images
│   │   ├── ncp-add.png
│   │   ├── ncp-find.png
│   │   ├── ncp-help.png
│   │   ├── ncp-import.png
│   │   ├── ncp-list.png
│   │   └── ncp-transformation-flow.png
│   ├── mcp-registry-setup.md
│   ├── pr-schema-additions.ts
│   └── stories
│       ├── 01-dream-and-discover.md
│       ├── 02-secrets-in-plain-sight.md
│       ├── 03-sync-and-forget.md
│       ├── 04-double-click-install.md
│       ├── 05-runtime-detective.md
│       └── 06-official-registry.md
├── DYNAMIC-RUNTIME-SUMMARY.md
├── EXTENSION-CONFIG-DISCOVERY.md
├── INSTALL-EXTENSION.md
├── INTERNAL-MCP-ARCHITECTURE.md
├── jest.config.js
├── LICENSE
├── MANAGEMENT-TOOLS-COMPLETE.md
├── manifest.json
├── manifest.json.backup
├── MCP-CONFIG-SCHEMA-IMPLEMENTATION-EXAMPLE.ts
├── MCP-CONFIG-SCHEMA-SIMPLE-EXAMPLE.json
├── MCP-CONFIGURATION-SCHEMA-FORMAT.json
├── MCPB-ARCHITECTURE-DECISION.md
├── NCP-EXTENSION-COMPLETE.md
├── package-lock.json
├── package.json
├── parity-between-cli-and-mcp.txt
├── PROMPTS-IMPLEMENTATION.md
├── README-COMPARISON.md
├── README.md
├── README.new.md
├── REGISTRY-INTEGRATION-COMPLETE.md
├── RELEASE-PROCESS-IMPROVEMENTS.md
├── RELEASE-SUMMARY.md
├── RELEASE.md
├── RUNTIME-DETECTION-COMPLETE.md
├── scripts
│   ├── cleanup
│   │   └── scan-repository.js
│   └── sync-server-version.cjs
├── SECURITY.md
├── server.json
├── src
│   ├── analytics
│   │   ├── analytics-formatter.ts
│   │   ├── log-parser.ts
│   │   └── visual-formatter.ts
│   ├── auth
│   │   ├── oauth-device-flow.ts
│   │   └── token-store.ts
│   ├── cache
│   │   ├── cache-patcher.ts
│   │   ├── csv-cache.ts
│   │   └── schema-cache.ts
│   ├── cli
│   │   └── index.ts
│   ├── discovery
│   │   ├── engine.ts
│   │   ├── mcp-domain-analyzer.ts
│   │   ├── rag-engine.ts
│   │   ├── search-enhancer.ts
│   │   └── semantic-enhancement-engine.ts
│   ├── extension
│   │   └── extension-init.ts
│   ├── index-mcp.ts
│   ├── index.ts
│   ├── internal-mcps
│   │   ├── internal-mcp-manager.ts
│   │   ├── ncp-management.ts
│   │   └── types.ts
│   ├── orchestrator
│   │   └── ncp-orchestrator.ts
│   ├── profiles
│   │   └── profile-manager.ts
│   ├── server
│   │   ├── mcp-prompts.ts
│   │   └── mcp-server.ts
│   ├── services
│   │   ├── config-prompter.ts
│   │   ├── config-schema-reader.ts
│   │   ├── error-handler.ts
│   │   ├── output-formatter.ts
│   │   ├── registry-client.ts
│   │   ├── tool-context-resolver.ts
│   │   ├── tool-finder.ts
│   │   ├── tool-schema-parser.ts
│   │   └── usage-tips-generator.ts
│   ├── testing
│   │   ├── create-real-mcp-definitions.ts
│   │   ├── dummy-mcp-server.ts
│   │   ├── mcp-definitions.json
│   │   ├── real-mcp-analyzer.ts
│   │   ├── real-mcp-definitions.json
│   │   ├── real-mcps.csv
│   │   ├── setup-dummy-mcps.ts
│   │   ├── setup-tiered-profiles.ts
│   │   ├── test-profile.json
│   │   ├── test-semantic-enhancement.ts
│   │   └── verify-profile-scaling.ts
│   ├── transports
│   │   └── filtered-stdio-transport.ts
│   └── utils
│       ├── claude-desktop-importer.ts
│       ├── client-importer.ts
│       ├── client-registry.ts
│       ├── config-manager.ts
│       ├── health-monitor.ts
│       ├── highlighting.ts
│       ├── logger.ts
│       ├── markdown-renderer.ts
│       ├── mcp-error-parser.ts
│       ├── mcp-wrapper.ts
│       ├── ncp-paths.ts
│       ├── parameter-prompter.ts
│       ├── paths.ts
│       ├── progress-spinner.ts
│       ├── response-formatter.ts
│       ├── runtime-detector.ts
│       ├── schema-examples.ts
│       ├── security.ts
│       ├── text-utils.ts
│       ├── update-checker.ts
│       ├── updater.ts
│       └── version.ts
├── STORY-DRIVEN-DOCUMENTATION.md
├── STORY-FIRST-WORKFLOW.md
├── test
│   ├── __mocks__
│   │   ├── chalk.js
│   │   ├── transformers.js
│   │   ├── updater.js
│   │   └── version.ts
│   ├── cache-loading-focused.test.ts
│   ├── cache-optimization.test.ts
│   ├── cli-help-validation.sh
│   ├── coverage-boost.test.ts
│   ├── curated-ecosystem-validation.test.ts
│   ├── discovery-engine.test.ts
│   ├── discovery-fallback-focused.test.ts
│   ├── ecosystem-discovery-focused.test.ts
│   ├── ecosystem-discovery-validation-simple.test.ts
│   ├── final-80-percent-push.test.ts
│   ├── final-coverage-push.test.ts
│   ├── health-integration.test.ts
│   ├── health-monitor.test.ts
│   ├── helpers
│   │   └── mock-server-manager.ts
│   ├── integration
│   │   └── mcp-client-simulation.test.cjs
│   ├── logger.test.ts
│   ├── mcp-ecosystem-discovery.test.ts
│   ├── mcp-error-parser.test.ts
│   ├── mcp-immediate-response-check.js
│   ├── mcp-server-protocol.test.ts
│   ├── mcp-timeout-scenarios.test.ts
│   ├── mcp-wrapper.test.ts
│   ├── mock-mcps
│   │   ├── aws-server.js
│   │   ├── base-mock-server.mjs
│   │   ├── brave-search-server.js
│   │   ├── docker-server.js
│   │   ├── filesystem-server.js
│   │   ├── git-server.mjs
│   │   ├── github-server.js
│   │   ├── neo4j-server.js
│   │   ├── notion-server.js
│   │   ├── playwright-server.js
│   │   ├── postgres-server.js
│   │   ├── shell-server.js
│   │   ├── slack-server.js
│   │   └── stripe-server.js
│   ├── mock-smithery-mcp
│   │   ├── index.js
│   │   ├── package.json
│   │   └── smithery.yaml
│   ├── ncp-orchestrator.test.ts
│   ├── orchestrator-health-integration.test.ts
│   ├── orchestrator-simple-branches.test.ts
│   ├── performance-benchmark.test.ts
│   ├── quick-coverage.test.ts
│   ├── rag-engine.test.ts
│   ├── regression-snapshot.test.ts
│   ├── search-enhancer.test.ts
│   ├── session-id-passthrough.test.ts
│   ├── setup.ts
│   ├── tool-context-resolver.test.ts
│   ├── tool-schema-parser.test.ts
│   ├── user-story-discovery.test.ts
│   └── version-util.test.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/orchestrator/ncp-orchestrator.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * NCP Orchestrator - Real MCP Connections
 * Based on commercial NCP implementation
 */

import { readFileSync, existsSync } from 'fs';
import { getCacheDirectory } from '../utils/ncp-paths.js';
import { join } from 'path';
import { createHash } from 'crypto';
import ProfileManager from '../profiles/profile-manager.js';
import { logger } from '../utils/logger.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { DiscoveryEngine } from '../discovery/engine.js';
import { MCPHealthMonitor } from '../utils/health-monitor.js';
import { SearchEnhancer } from '../discovery/search-enhancer.js';
import { mcpWrapper } from '../utils/mcp-wrapper.js';
import { withFilteredOutput } from '../transports/filtered-stdio-transport.js';
import { ToolSchemaParser, ParameterInfo } from '../services/tool-schema-parser.js';
import { InternalMCPManager } from '../internal-mcps/internal-mcp-manager.js';
import { ToolContextResolver } from '../services/tool-context-resolver.js';
import type { OAuthConfig } from '../auth/oauth-device-flow.js';
import { getRuntimeForExtension, logRuntimeInfo } from '../utils/runtime-detector.js';
// Simple string similarity for tool name matching
function calculateSimilarity(str1: string, str2: string): number {
  const s1 = str1.toLowerCase();
  const s2 = str2.toLowerCase();

  // Exact match
  if (s1 === s2) return 1.0;

  // Check if one contains the other
  if (s1.includes(s2) || s2.includes(s1)) {
    return 0.8;
  }

  // Simple Levenshtein-based similarity
  const longer = s1.length > s2.length ? s1 : s2;
  const shorter = s1.length > s2.length ? s2 : s1;

  if (longer.length === 0) return 1.0;

  const editDistance = levenshteinDistance(s1, s2);
  return (longer.length - editDistance) / longer.length;
}

function levenshteinDistance(str1: string, str2: string): number {
  const matrix: number[][] = [];

  for (let i = 0; i <= str2.length; i++) {
    matrix[i] = [i];
  }

  for (let j = 0; j <= str1.length; j++) {
    matrix[0][j] = j;
  }

  for (let i = 1; i <= str2.length; i++) {
    for (let j = 1; j <= str1.length; j++) {
      if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
        matrix[i][j] = matrix[i - 1][j - 1];
      } else {
        matrix[i][j] = Math.min(
          matrix[i - 1][j - 1] + 1,
          matrix[i][j - 1] + 1,
          matrix[i - 1][j] + 1
        );
      }
    }
  }

  return matrix[str2.length][str1.length];
}
import { CachePatcher } from '../cache/cache-patcher.js';
import { CSVCache, CachedTool } from '../cache/csv-cache.js';
import { spinner } from '../utils/progress-spinner.js';

interface DiscoveryResult {
  toolName: string;
  mcpName: string;
  confidence: number;
  description?: string;
  schema?: any;
}

interface ExecutionResult {
  success: boolean;
  content?: any;
  error?: string;
}

interface MCPConfig {
  name: string;
  command?: string;  // Optional: for stdio transport
  args?: string[];
  env?: Record<string, string>;
  url?: string;  // Optional: for HTTP/SSE transport (Claude Desktop native)
  auth?: {
    type: 'oauth' | 'bearer' | 'apiKey' | 'basic';
    oauth?: OAuthConfig;  // OAuth 2.0 Device Flow configuration
    token?: string;       // Bearer token or API key
    username?: string;    // Basic auth username
    password?: string;    // Basic auth password
  };
}

interface Profile {
  name: string;
  description: string;
  mcpServers: Record<string, {
    command?: string;  // Optional: for stdio transport
    args?: string[];
    env?: Record<string, string>;
    url?: string;  // Optional: for HTTP/SSE transport
    auth?: {
      type: 'oauth' | 'bearer' | 'apiKey' | 'basic';
      oauth?: OAuthConfig;  // OAuth 2.0 Device Flow configuration
      token?: string;       // Bearer token or API key
      username?: string;    // Basic auth username
      password?: string;    // Basic auth password
    };
  }>;
  metadata?: any;
}

interface MCPConnection {
  client: Client;
  transport: StdioClientTransport | SSEClientTransport;
  tools: Array<{name: string; description: string}>;
  serverInfo?: {
    name: string;
    title?: string;
    version: string;
    description?: string;
    websiteUrl?: string;
  };
  lastUsed: number;
  connectTime: number;
  executionCount: number;
}

interface MCPDefinition {
  name: string;
  config: MCPConfig;
  tools: Array<{name: string; description: string}>;
  serverInfo?: {
    name: string;
    title?: string;
    version: string;
    description?: string;
    websiteUrl?: string;
  };
}

export class NCPOrchestrator {
  private definitions: Map<string, MCPDefinition> = new Map();
  private connections: Map<string, MCPConnection> = new Map();
  private toolToMCP: Map<string, string> = new Map();
  private allTools: Array<{ name: string; description: string; mcpName: string }> = [];
  private profileName: string;
  private readonly QUICK_PROBE_TIMEOUT = 8000; // 8 seconds - first attempt
  private readonly SLOW_PROBE_TIMEOUT = 30000; // 30 seconds - retry for slow MCPs
  private readonly CONNECTION_TIMEOUT = 10000; // 10 seconds
  private readonly IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
  private readonly CLEANUP_INTERVAL = 60 * 1000; // Check every minute
  private cleanupTimer?: NodeJS.Timeout;
  private discovery: DiscoveryEngine;
  private healthMonitor: MCPHealthMonitor;
  private cachePatcher: CachePatcher;
  private csvCache: CSVCache;
  private showProgress: boolean;
  private indexingProgress: { current: number; total: number; currentMCP: string; estimatedTimeRemaining?: number } | null = null;
  private indexingStartTime: number = 0;
  private profileManager: ProfileManager | null = null;
  private internalMCPManager: InternalMCPManager;

  private forceRetry: boolean = false;

  /**
   * ⚠️ CRITICAL: Default profile MUST be 'all' - DO NOT CHANGE!
   *
   * The 'all' profile is the universal profile that contains all MCPs.
   * This default is used by MCPServer and all CLI commands.
   *
   * DO NOT change this to 'default' or any other name - it will break everything.
   */
  constructor(profileName: string = 'all', showProgress: boolean = false, forceRetry: boolean = false) {
    this.profileName = profileName;
    this.discovery = new DiscoveryEngine();
    this.healthMonitor = new MCPHealthMonitor();
    this.cachePatcher = new CachePatcher();
    this.csvCache = new CSVCache(getCacheDirectory(), profileName);
    this.showProgress = showProgress;
    this.forceRetry = forceRetry;
    this.internalMCPManager = new InternalMCPManager();
  }

  private async loadProfile(): Promise<Profile | null> {
    try {
      // Create and store ProfileManager instance (reused for auto-import)
      if (!this.profileManager) {
        this.profileManager = new ProfileManager();
        await this.profileManager.initialize();

        // Initialize internal MCPs with ProfileManager
        this.internalMCPManager.initialize(this.profileManager);
      }

      const profile = await this.profileManager.getProfile(this.profileName);

      if (!profile) {
        logger.error(`Profile not found: ${this.profileName}`);
        return null;
      }

      return profile;
    } catch (error: any) {
      logger.error(`Failed to load profile: ${error.message}`);
      return null;
    }
  }

  async initialize(): Promise<void> {
    const startTime = Date.now();
    this.indexingStartTime = startTime;

    // Debug logging
    if (process.env.NCP_DEBUG === 'true') {
      console.error(`[DEBUG ORC] Initializing with profileName: ${this.profileName}`);
      console.error(`[DEBUG ORC] Cache will use: ${this.csvCache ? 'csvCache exists' : 'NO CACHE'}`);
    }

    logger.info(`Initializing NCP orchestrator with profile: ${this.profileName}`);

    // Log runtime detection info (how NCP is running)
    if (process.env.NCP_DEBUG === 'true') {
      logRuntimeInfo();
    }

    // Initialize progress immediately to prevent race condition
    // Total will be updated once we know how many MCPs need indexing
    this.indexingProgress = {
      current: 0,
      total: 0,
      currentMCP: 'initializing...'
    };

    const profile = await this.loadProfile();

    if (process.env.NCP_DEBUG === 'true') {
      console.error(`[DEBUG ORC] Loaded profile: ${profile ? 'YES' : 'NO'}`);
      if (profile) {
        console.error(`[DEBUG ORC] Profile MCPs: ${Object.keys(profile.mcpServers || {}).join(', ')}`);
      }
    }

    if (!profile) {
      logger.error('Failed to load profile');
      this.indexingProgress = null;
      return;
    }

    // Initialize discovery engine first
    await this.discovery.initialize();

    // Initialize CSV cache
    await this.csvCache.initialize();

    // Get profile hash for cache validation
    const profileHash = CSVCache.hashProfile(profile.mcpServers);

    // Check if cache is valid
    const cacheValid = this.csvCache.validateCache(profileHash);

    const mcpConfigs: MCPConfig[] = Object.entries(profile.mcpServers).map(([name, config]) => ({
      name,
      command: config.command,
      args: config.args,
      env: config.env || {},
      url: config.url  // HTTP/SSE transport support
    }));

    if (cacheValid) {
      // Load from cache
      logger.info('Loading tools from CSV cache...');
      const cachedMCPCount = await this.loadFromCSVCache(mcpConfigs);
    } else {
      // Cache invalid - clear it to force full re-indexing
      logger.info('Cache invalid, clearing for full re-index...');
      await this.csvCache.clear();
      await this.csvCache.initialize();
    }

    // Get list of MCPs that need indexing
    const indexedMCPs = this.csvCache.getIndexedMCPs();
    const mcpsToIndex = mcpConfigs.filter(config => {
      const tools = profile.mcpServers[config.name];
      const currentHash = CSVCache.hashProfile(tools);

      // Check if already indexed
      if (this.csvCache.isMCPIndexed(config.name, currentHash)) {
        return false;
      }

      // Check if failed and should retry
      return this.csvCache.shouldRetryFailed(config.name, this.forceRetry);
    });

    if (mcpsToIndex.length > 0) {
      // Update progress tracking with actual count
      if (this.indexingProgress) {
        this.indexingProgress.total = mcpsToIndex.length;
      }

      if (this.showProgress) {
        const action = 'Indexing';
        const cachedCount = this.csvCache.getIndexedMCPs().size;

        // Count only failed MCPs that are NOT being retried in this run
        const allFailedCount = this.csvCache.getFailedMCPsCount();
        const retryingNowCount = mcpsToIndex.filter(config => {
          // Check if this MCP is in the failed list (being retried)
          return this.csvCache.isMCPFailed(config.name);
        }).length;
        const failedNotRetryingCount = allFailedCount - retryingNowCount;

        const totalProcessed = cachedCount + failedNotRetryingCount;
        const statusMsg = `${action} MCPs: ${totalProcessed}/${mcpConfigs.length}`;
        spinner.start(statusMsg);
        spinner.updateSubMessage('Initializing discovery engine...');
      }

      // Start incremental cache writing
      await this.csvCache.startIncrementalWrite(profileHash);

      // Index only the MCPs that need it
      await this.discoverMCPTools(mcpsToIndex, profile, true, mcpConfigs.length);

      // Finalize cache
      await this.csvCache.finalize();

      if (this.showProgress) {
        const successfulMCPs = this.definitions.size;
        const failedMCPs = this.csvCache.getFailedMCPsCount();
        const totalProcessed = successfulMCPs + failedMCPs;

        if (failedMCPs > 0) {
          spinner.success(`Indexed ${this.allTools.length} tools from ${successfulMCPs} MCPs | ${failedMCPs} failed (will retry later)`);
        } else {
          spinner.success(`Indexed ${this.allTools.length} tools from ${successfulMCPs} MCPs`);
        }
      }
    }

    // Clear progress tracking once complete
    this.indexingProgress = null;

    // Add internal MCPs to discovery
    this.addInternalMCPsToDiscovery();

    // Start cleanup timer for idle connections
    this.cleanupTimer = setInterval(
      () => this.cleanupIdleConnections(),
      this.CLEANUP_INTERVAL
    );

    const externalMCPs = this.definitions.size;
    const internalMCPs = this.internalMCPManager.getAllInternalMCPs().length;
    const loadTime = Date.now() - startTime;
    logger.info(`🚀 NCP-OSS initialized in ${loadTime}ms with ${this.allTools.length} tools from ${externalMCPs} external + ${internalMCPs} internal MCPs`);
  }

  /**
   * Load cached tools from CSV
   */
  private async loadFromCSVCache(mcpConfigs: MCPConfig[]): Promise<number> {
    const cachedTools = this.csvCache.loadCachedTools();

    // Group tools by MCP
    const toolsByMCP = new Map<string, CachedTool[]>();
    for (const tool of cachedTools) {
      if (!toolsByMCP.has(tool.mcpName)) {
        toolsByMCP.set(tool.mcpName, []);
      }
      toolsByMCP.get(tool.mcpName)!.push(tool);
    }

    let loadedMCPCount = 0;

    // Rebuild definitions and tool mappings from cache
    for (const config of mcpConfigs) {
      const mcpTools = toolsByMCP.get(config.name) || [];
      if (mcpTools.length === 0) continue;

      loadedMCPCount++;

      // Create definition
      this.definitions.set(config.name, {
        name: config.name,
        config,
        tools: mcpTools.map(t => ({
          name: t.toolName,
          description: t.description,
          inputSchema: {}
        })),
        serverInfo: undefined
      });

      // Add to all tools and create mappings
      for (const cachedTool of mcpTools) {
        const tool = {
          name: cachedTool.toolName,
          description: cachedTool.description,
          mcpName: config.name
        };
        this.allTools.push(tool);
        this.toolToMCP.set(cachedTool.toolId, config.name);
      }

      // Index tools in discovery engine
      const discoveryTools = mcpTools.map(t => ({
        id: t.toolId,
        name: t.toolName,
        description: t.description
      }));

      // Use async indexing to avoid blocking
      this.discovery.indexMCPTools(config.name, discoveryTools);
    }

    logger.info(`Loaded ${this.allTools.length} tools from CSV cache`);
    return loadedMCPCount;
  }

  private async discoverMCPTools(mcpConfigs: MCPConfig[], profile?: Profile, incrementalMode: boolean = false, totalMCPCount?: number): Promise<void> {
    // Only clear allTools if not in incremental mode
    if (!incrementalMode) {
      this.allTools = [];
    }

    const displayTotal = totalMCPCount || mcpConfigs.length;

    for (let i = 0; i < mcpConfigs.length; i++) {
      const config = mcpConfigs[i];
      try {
        logger.info(`Discovering tools from MCP: ${config.name}`);

        if (this.showProgress) {
          spinner.updateSubMessage(`Connecting to ${config.name}...`);
        }

        let result;
        try {
          // First attempt with quick timeout
          result = await this.probeMCPTools(config, this.QUICK_PROBE_TIMEOUT);
        } catch (firstError: any) {
          // If it timed out (not connection error), retry with longer timeout
          if (firstError.message.includes('Probe timeout') || firstError.message.includes('timeout')) {
            logger.debug(`${config.name} timed out on first attempt, retrying with longer timeout...`);
            if (this.showProgress) {
              spinner.updateSubMessage(`Retrying ${config.name} (heavy initialization)...`);
            }
            // Second attempt with slow timeout for heavy MCPs
            result = await this.probeMCPTools(config, this.SLOW_PROBE_TIMEOUT);
          } else {
            // Not a timeout - it's a real error (connection refused, etc), don't retry
            throw firstError;
          }
        }

        // Store definition with schema fallback applied
        this.definitions.set(config.name, {
          name: config.name,
          config,
          tools: result.tools.map(tool => ({
            ...tool,
            inputSchema: tool.inputSchema || {}
          })),
          serverInfo: result.serverInfo
        });

        // Add to all tools and create mappings
        const discoveryTools = [];
        for (const tool of result.tools) {
          // Store with prefixed name for consistency with commercial version
          const prefixedToolName = `${config.name}:${tool.name}`;
          const prefixedDescription = `${config.name}: ${tool.description || 'No description available'}`;

          this.allTools.push({
            name: prefixedToolName,
            description: prefixedDescription,
            mcpName: config.name
          });

          // Map both formats for backward compatibility
          this.toolToMCP.set(tool.name, config.name);
          this.toolToMCP.set(prefixedToolName, config.name);

          // Prepare for discovery engine indexing
          // Pass unprefixed name and description - RAG engine will add the prefix
          discoveryTools.push({
            id: prefixedToolName,
            name: tool.name,  // Use unprefixed name here
            description: tool.description || 'No description available',  // Use unprefixed description
            mcpServer: config.name,
            inputSchema: tool.inputSchema || {}
          });
        }

        if (this.showProgress) {
          // Add time estimate to indexing sub-message for parity
          let timeDisplay = '';
          if (this.indexingProgress?.estimatedTimeRemaining) {
            const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000);
            timeDisplay = ` (~${remainingSeconds}s remaining)`;
          }
          spinner.updateSubMessage(`Indexing ${result.tools.length} tools from ${config.name}...${timeDisplay}`);
        }

        // Index tools with discovery engine for vector search
        await this.discovery.indexMCPTools(config.name, discoveryTools);

        // Append to CSV cache incrementally (if in incremental mode)
        if (incrementalMode && profile) {
          const mcpHash = CSVCache.hashProfile(profile.mcpServers[config.name]);
          const cachedTools: CachedTool[] = result.tools.map(tool => ({
            mcpName: config.name,
            toolId: `${config.name}:${tool.name}`,
            toolName: tool.name,
            description: tool.description || 'No description available',
            hash: this.hashString(tool.description || ''),
            timestamp: new Date().toISOString()
          }));

          await this.csvCache.appendMCP(config.name, cachedTools, mcpHash);
        }

        // Update indexing progress AFTER successfully appending to cache
        if (this.indexingProgress) {
          this.indexingProgress.current = i + 1;
          this.indexingProgress.currentMCP = config.name;

          // Estimate remaining time based on average time per MCP so far
          const elapsedTime = Date.now() - this.indexingStartTime;
          const averageTimePerMCP = elapsedTime / (i + 1);
          const remainingMCPs = mcpConfigs.length - (i + 1);
          this.indexingProgress.estimatedTimeRemaining = remainingMCPs * averageTimePerMCP;
        }

        if (this.showProgress) {
          // Calculate absolute position
          const cachedCount = displayTotal - mcpConfigs.length;
          const currentAbsolute = cachedCount + (i + 1);
          const percentage = Math.round((currentAbsolute / displayTotal) * 100);

          // Add time estimate
          let timeDisplay = '';
          if (this.indexingProgress?.estimatedTimeRemaining) {
            const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000);
            timeDisplay = ` ~${remainingSeconds}s remaining`;
          }

          spinner.updateMessage(`Indexing MCPs: ${currentAbsolute}/${displayTotal} (${percentage}%)${timeDisplay}`);
        }

        logger.info(`Discovered ${result.tools.length} tools from ${config.name}`);
      } catch (error: any) {
        // Probe failures are expected - don't alarm users with error messages
        logger.debug(`Failed to discover tools from ${config.name}: ${error.message}`);

        // Mark MCP as failed for scheduled retry (if in incremental mode)
        if (incrementalMode && profile) {
          this.csvCache.markFailed(config.name, error);
        }

        // Update indexing progress even for failed MCPs
        if (this.indexingProgress) {
          this.indexingProgress.current = i + 1;
          this.indexingProgress.currentMCP = config.name;

          // Estimate remaining time based on average time per MCP so far
          const elapsedTime = Date.now() - this.indexingStartTime;
          const averageTimePerMCP = elapsedTime / (i + 1);
          const remainingMCPs = mcpConfigs.length - (i + 1);
          this.indexingProgress.estimatedTimeRemaining = remainingMCPs * averageTimePerMCP;
        }

        if (this.showProgress) {
          // Calculate absolute position
          const cachedCount = displayTotal - mcpConfigs.length;
          const currentAbsolute = cachedCount + (i + 1);
          const percentage = Math.round((currentAbsolute / displayTotal) * 100);

          // Add time estimate
          let timeDisplay = '';
          if (this.indexingProgress?.estimatedTimeRemaining) {
            const remainingSeconds = Math.ceil(this.indexingProgress.estimatedTimeRemaining / 1000);
            timeDisplay = ` ~${remainingSeconds}s remaining`;
          }

          spinner.updateMessage(`Indexing MCPs: ${currentAbsolute}/${displayTotal} (${percentage}%)${timeDisplay}`);
          spinner.updateSubMessage(`Skipped ${config.name} (connection failed)`);
        }

        // Update health monitor with the actual error for import feedback
        this.healthMonitor.markUnhealthy(config.name, error.message);
      }
    }
  }

  /**
   * Create appropriate transport based on config
   * Supports both stdio (command/args) and HTTP/SSE (url) transports
   * Handles OAuth authentication for HTTP/SSE connections
   */
  private async createTransport(config: MCPConfig, env?: Record<string, string>): Promise<StdioClientTransport | SSEClientTransport> {
    if (config.url) {
      // HTTP/SSE transport (Claude Desktop native support)
      const url = new URL(config.url);
      const headers: Record<string, string> = {};

      // Handle authentication
      if (config.auth) {
        const token = await this.getAuthToken(config);

        switch (config.auth.type) {
          case 'oauth':
          case 'bearer':
            headers['Authorization'] = `Bearer ${token}`;
            break;
          case 'apiKey':
            // API key can be in header or query param - assume header for now
            headers['X-API-Key'] = token;
            break;
          case 'basic':
            if (config.auth.username && config.auth.password) {
              const credentials = Buffer.from(`${config.auth.username}:${config.auth.password}`).toString('base64');
              headers['Authorization'] = `Basic ${credentials}`;
            }
            break;
        }
      }

      // Use requestInit to add custom headers to POST requests
      // and eventSourceInit to add headers to the initial SSE connection
      const options = Object.keys(headers).length > 0 ? {
        requestInit: { headers },
        eventSourceInit: { headers } as EventSourceInit
      } : undefined;

      return new SSEClientTransport(url, options);
    }

    if (config.command) {
      // stdio transport (local process)
      const resolvedCommand = getRuntimeForExtension(config.command);
      const wrappedCommand = mcpWrapper.createWrapper(
        config.name,
        resolvedCommand,
        config.args || []
      );

      return new StdioClientTransport({
        command: wrappedCommand.command,
        args: wrappedCommand.args,
        env: env as Record<string, string>
      });
    }

    throw new Error(`Invalid config for ${config.name}: must have either 'command' or 'url'`);
  }

  /**
   * Get authentication token for MCP
   * Handles OAuth Device Flow and token refresh
   */
  private async getAuthToken(config: MCPConfig): Promise<string> {
    if (!config.auth) {
      throw new Error('No auth configuration provided');
    }

    // For non-OAuth auth types, return the token directly
    if (config.auth.type !== 'oauth') {
      return config.auth.token || '';
    }

    // OAuth flow
    if (!config.auth.oauth) {
      throw new Error('OAuth configuration missing');
    }

    const { getTokenStore } = await import('../auth/token-store.js');
    const tokenStore = getTokenStore();

    // Check for existing valid token
    const existingToken = await tokenStore.getToken(config.name);
    if (existingToken) {
      return existingToken.access_token;
    }

    // No valid token - trigger OAuth Device Flow
    const { DeviceFlowAuthenticator } = await import('../auth/oauth-device-flow.js');
    const authenticator = new DeviceFlowAuthenticator(config.auth.oauth);

    logger.info(`No valid token found for ${config.name}, starting OAuth Device Flow...`);
    const tokenResponse = await authenticator.authenticate();

    // Store token for future use
    await tokenStore.storeToken(config.name, tokenResponse);

    return tokenResponse.access_token;
  }

  // Based on commercial NCP's probeMCPTools method
  private async probeMCPTools(config: MCPConfig, timeout: number = this.QUICK_PROBE_TIMEOUT): Promise<{
    tools: Array<{name: string; description: string; inputSchema?: any}>;
    serverInfo?: {
      name: string;
      title?: string;
      version: string;
      description?: string;
      websiteUrl?: string;
    };
  }> {
    if (!config.command && !config.url) {
      throw new Error(`Invalid config for ${config.name}: must have either 'command' or 'url'`);
    }

    let client: Client | null = null;
    let transport: StdioClientTransport | SSEClientTransport | null = null;

    try {
      // Create temporary connection for discovery
      const silentEnv = {
        ...process.env,
        ...(config.env || {}),
        MCP_SILENT: 'true',
        QUIET: 'true',
        NO_COLOR: 'true'
      };

      transport = await this.createTransport(config, silentEnv);

      client = new Client(
        { name: 'ncp-oss', version: '1.0.0' },
        { capabilities: {} }
      );

      // Connect with timeout and filtered output
      await withFilteredOutput(async () => {
        await Promise.race([
          client!.connect(transport!),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Probe timeout')), timeout)
          )
        ]);
      });

      // Capture server info after connection
      const serverInfo = client!.getServerVersion();

      // Get tool list with filtered output
      const response = await withFilteredOutput(async () => {
        return await client!.listTools();
      });
      const tools = response.tools.map(t => ({
        name: t.name,
        description: t.description || '',
        inputSchema: t.inputSchema || {}
      }));

      // Disconnect immediately
      await client.close();

      return {
        tools,
        serverInfo: serverInfo ? {
          name: serverInfo.name || config.name,
          title: serverInfo.title,
          version: serverInfo.version || 'unknown',
          description: serverInfo.title || serverInfo.name || undefined,
          websiteUrl: serverInfo.websiteUrl
        } : undefined
      };

    } catch (error: any) {
      // Clean up on error
      if (client) {
        try { await client.close(); } catch {}
      }

      // Log full error details for debugging
      logger.debug(`Full error details for ${config.name}: ${JSON.stringify({
        message: error.message,
        code: error.code,
        data: error.data,
        stack: error.stack?.split('\n')[0]
      })}`);

      throw error;
    }
  }

  async find(query: string, limit: number = 5, detailed: boolean = false): Promise<DiscoveryResult[]> {
    if (!query) {
      // No query = list all tools, filtered by health
      const healthyTools = this.allTools.filter(tool => this.healthMonitor.getHealthyMCPs([tool.mcpName]).length > 0);
      const results = healthyTools.slice(0, limit).map(tool => {
        // Extract actual tool name from prefixed format
        const actualToolName = tool.name.includes(':') ? tool.name.split(':', 2)[1] : tool.name;
        return {
          toolName: tool.name, // Return prefixed name
          mcpName: tool.mcpName,
          confidence: 1.0,
          description: detailed ? tool.description : undefined,
          schema: detailed ? this.getToolSchema(tool.mcpName, actualToolName) : undefined
        };
      });
      return results;
    }

    // Use battle-tested vector search from commercial NCP
    // DOUBLE SEARCH TECHNIQUE: Request 2x results to account for filtering disabled MCPs
    try {
      const doubleLimit = limit * 2; // Request double to account for filtered MCPs
      const vectorResults = await this.discovery.findRelevantTools(query, doubleLimit);

      // Apply universal term frequency scoring boost
      const adjustedResults = this.adjustScoresUniversally(query, vectorResults);

      // Parse and filter results
      const parsedResults = adjustedResults.map(result => {
        // Parse tool format: "mcp:tool" or just "tool"
        const parts = result.name.includes(':') ? result.name.split(':', 2) : [this.toolToMCP.get(result.name) || 'unknown', result.name];
        const mcpName = parts[0];
        const toolName = parts[1] || result.name;

        // Find the tool - it should be stored with prefixed name
        const prefixedToolName = `${mcpName}:${toolName}`;
        const fullTool = this.allTools.find(t =>
          (t.name === prefixedToolName || t.name === toolName) && t.mcpName === mcpName
        );
        return {
          toolName: fullTool?.name || prefixedToolName, // Return the stored (prefixed) name
          mcpName,
          confidence: result.confidence,
          description: detailed ? fullTool?.description : undefined,
          schema: detailed ? this.getToolSchema(mcpName, toolName) : undefined
        };
      });

      // HEALTH FILTERING: Remove tools from disabled MCPs
      const healthyResults = parsedResults.filter(result => {
        return this.healthMonitor.getHealthyMCPs([result.mcpName]).length > 0;
      });

      // SORT by confidence (highest first) after our scoring adjustments
      const sortedResults = healthyResults.sort((a, b) => b.confidence - a.confidence);

      // Return up to the original limit after filtering and sorting
      const finalResults = sortedResults.slice(0, limit);

      if (healthyResults.length < parsedResults.length) {
        logger.debug(`Health filtering: ${parsedResults.length - healthyResults.length} tools filtered out from disabled MCPs`);
      }

      return finalResults;

    } catch (error: any) {
      logger.error(`Vector search failed: ${error.message}`);

      // Fallback to healthy tools only
      const healthyTools = this.allTools.filter(tool => this.healthMonitor.getHealthyMCPs([tool.mcpName]).length > 0);
      return healthyTools.slice(0, limit).map(tool => {
        // Extract actual tool name from prefixed format for schema lookup
        const actualToolName = tool.name.includes(':') ? tool.name.split(':', 2)[1] : tool.name;
        return {
          toolName: tool.name, // Return prefixed name
          mcpName: tool.mcpName,
          confidence: 0.5,
          description: detailed ? tool.description : undefined,
          schema: detailed ? this.getToolSchema(tool.mcpName, actualToolName) : undefined
        };
      });
    }
  }

  async run(toolName: string, parameters: any, meta?: Record<string, any>): Promise<ExecutionResult> {
    // Parse tool format: "mcp:tool" or just "tool"
    let mcpName: string;
    let actualToolName: string;

    if (toolName.includes(':')) {
      [mcpName, actualToolName] = toolName.split(':', 2);
    } else {
      actualToolName = toolName;
      mcpName = this.toolToMCP.get(toolName) || '';
    }

    if (!mcpName) {
      const similarTools = this.findSimilarTools(toolName);
      let errorMessage = `Tool '${toolName}' not found.`;

      if (similarTools.length > 0) {
        errorMessage += ` Did you mean: ${similarTools.join(', ')}?`;
      }

      errorMessage += ` Use 'ncp find "${toolName}"' to search for similar tools or 'ncp find --depth 0' to list all available tools.`;

      return {
        success: false,
        error: errorMessage
      };
    }

    // Check if this is an internal MCP
    if (this.internalMCPManager.isInternalMCP(mcpName)) {
      try {
        const result = await this.internalMCPManager.executeInternalTool(mcpName, actualToolName, parameters);
        return {
          success: result.success,
          content: result.content,
          error: result.error
        };
      } catch (error: any) {
        logger.error(`Internal tool execution failed for ${toolName}:`, error);
        return {
          success: false,
          error: error.message || 'Internal tool execution failed'
        };
      }
    }

    const definition = this.definitions.get(mcpName);
    if (!definition) {
      const availableMcps = Array.from(this.definitions.keys()).join(', ');
      return {
        success: false,
        error: `MCP '${mcpName}' not found. Available MCPs: ${availableMcps}. Use 'ncp find' to discover tools or check your profile configuration.`
      };
    }

    try {
      // Get or create pooled connection
      const connection = await this.getOrCreateConnection(mcpName);

      // Validate parameters before execution
      const validationError = this.validateToolParameters(mcpName, actualToolName, parameters);
      if (validationError) {
        return {
          success: false,
          error: validationError
        };
      }

      // Execute tool with filtered output to suppress MCP server console messages
      // Forward _meta transparently to support session_id and other protocol-level metadata
      const result = await withFilteredOutput(async () => {
        return await connection.client.callTool({
          name: actualToolName,
          arguments: parameters,
          _meta: meta
        });
      });

      // Mark MCP as healthy on successful execution
      this.healthMonitor.markHealthy(mcpName);

      return {
        success: true,
        content: result.content
      };

    } catch (error: any) {
      logger.error(`Tool execution failed for ${toolName}:`, error);

      // Mark MCP as unhealthy on execution failure
      this.healthMonitor.markUnhealthy(mcpName, error.message);

      return {
        success: false,
        error: this.enhanceErrorMessage(error, actualToolName, mcpName)
      };
    }
  }

  private async getOrCreateConnection(mcpName: string): Promise<MCPConnection> {
    // Return existing connection if available
    const existing = this.connections.get(mcpName);
    if (existing) {
      existing.lastUsed = Date.now();
      existing.executionCount++;
      return existing;
    }

    const definition = this.definitions.get(mcpName);
    if (!definition) {
      const availableMcps = Array.from(this.definitions.keys()).join(', ');
      throw new Error(`MCP '${mcpName}' not found. Available MCPs: ${availableMcps}. Use 'ncp find' to discover tools or check your profile configuration.`);
    }

    logger.info(`🔌 Connecting to ${mcpName} (for execution)...`);
    const connectStart = Date.now();

    try {
      // Add environment variables
      const silentEnv = {
        ...process.env,
        ...(definition.config.env || {}),
        // These may still help some servers
        MCP_SILENT: 'true',
        QUIET: 'true',
        NO_COLOR: 'true'
      };

      const transport = await this.createTransport(definition.config, silentEnv);

      const client = new Client(
        { name: 'ncp-oss', version: '1.0.0' },
        { capabilities: {} }
      );

      // Connect with timeout and filtered output
      await withFilteredOutput(async () => {
        await Promise.race([
          client.connect(transport),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Connection timeout')), this.CONNECTION_TIMEOUT)
          )
        ]);
      });

      // Capture server info after successful connection
      const serverInfo = client.getServerVersion();

      const connection: MCPConnection = {
        client,
        transport,
        tools: [], // Will be populated if needed
        serverInfo: serverInfo ? {
          name: serverInfo.name || mcpName,
          title: serverInfo.title,
          version: serverInfo.version || 'unknown',
          description: serverInfo.title || serverInfo.name || undefined,
          websiteUrl: serverInfo.websiteUrl
        } : undefined,
        lastUsed: Date.now(),
        connectTime: Date.now() - connectStart,
        executionCount: 1
      };

      // Store connection for reuse
      this.connections.set(mcpName, connection);
      logger.info(`✅ Connected to ${mcpName} in ${connection.connectTime}ms`);

      return connection;
    } catch (error: any) {
      logger.error(`❌ Failed to connect to ${mcpName}: ${error.message}`);
      throw error;
    }
  }

  /**
   * New optimized cache loading with profile hash validation
   * This is the key optimization - skips re-indexing when profile hasn't changed
   */
  private async loadFromOptimizedCache(profile: Profile): Promise<boolean> {
    try {
      // 1. Validate cache integrity first
      const integrity = await this.cachePatcher.validateAndRepairCache();
      if (!integrity.valid) {
        logger.warn('Cache integrity check failed - rebuilding required');
        return false;
      }

      // 2. Check if cache is valid using profile hash validation
      const currentProfileHash = this.cachePatcher.generateProfileHash(profile);
      const cacheIsValid = await this.cachePatcher.validateCacheWithProfile(currentProfileHash);

      if (!cacheIsValid) {
        logger.info('Cache invalid or missing - profile changed');
        return false;
      }

      // 3. Load tool metadata cache directly
      const toolMetadataCache = await this.cachePatcher.loadToolMetadataCache();

      if (!toolMetadataCache.mcps || Object.keys(toolMetadataCache.mcps).length === 0) {
        logger.info('Tool metadata cache empty');
        return false;
      }

      logger.info(`✅ Using valid cache (${Object.keys(toolMetadataCache.mcps).length} MCPs, hash: ${currentProfileHash.substring(0, 8)}...)`);

      // 4. Load MCPs and tools from cache directly (no re-indexing)
      this.allTools = [];
      let loadedMCPCount = 0;
      let loadedToolCount = 0;

      for (const [mcpName, mcpData] of Object.entries(toolMetadataCache.mcps)) {
        try {
          // Validate MCP data structure
          if (!mcpData.tools || !Array.isArray(mcpData.tools)) {
            logger.warn(`Skipping ${mcpName}: invalid tools data in cache`);
            continue;
          }

          // Check if MCP still exists in current profile
          if (!profile.mcpServers[mcpName]) {
            logger.debug(`Skipping ${mcpName}: removed from profile`);
            continue;
          }

          this.definitions.set(mcpName, {
            name: mcpName,
            config: {
              name: mcpName,
              ...profile.mcpServers[mcpName]
            },
            tools: mcpData.tools.map(tool => ({
              ...tool,
              inputSchema: tool.inputSchema || {}
            })),
            serverInfo: mcpData.serverInfo || { name: mcpName, version: '1.0.0' }
          });

          // Build allTools array and tool mappings
          const discoveryTools = [];
          for (const tool of mcpData.tools) {
            try {
              const prefixedToolName = `${mcpName}:${tool.name}`;
              const prefixedDescription = tool.description.startsWith(`${mcpName}:`)
                ? tool.description
                : `${mcpName}: ${tool.description || 'No description available'}`;

              this.allTools.push({
                name: prefixedToolName,
                description: prefixedDescription,
                mcpName: mcpName
              });

              // Create tool mappings
              this.toolToMCP.set(tool.name, mcpName);
              this.toolToMCP.set(prefixedToolName, mcpName);

              discoveryTools.push({
                id: prefixedToolName,
                name: tool.name,
                description: prefixedDescription,
                mcpServer: mcpName,
                inputSchema: tool.inputSchema || {}
              });

              loadedToolCount++;
            } catch (toolError: any) {
              logger.warn(`Error loading tool ${tool.name} from ${mcpName}: ${toolError.message}`);
            }
          }

          // Use fast indexing (load from embeddings cache, don't regenerate)
          if (discoveryTools.length > 0) {
            // Ensure discovery engine is fully initialized before indexing
            await this.discovery.initialize();
            await this.discovery.indexMCPToolsFromCache(mcpName, discoveryTools);
            loadedMCPCount++;
          }

        } catch (mcpError: any) {
          logger.warn(`Error loading MCP ${mcpName} from cache: ${mcpError.message}`);
        }
      }

      if (loadedMCPCount === 0) {
        logger.warn('No valid MCPs loaded from cache');
        return false;
      }

      logger.info(`⚡ Loaded ${loadedToolCount} tools from ${loadedMCPCount} MCPs (optimized cache)`);
      return true;

    } catch (error: any) {
      logger.warn(`Optimized cache load failed: ${error.message}`);
      return false;
    }
  }

  /**
   * Legacy cache loading (kept for fallback)
   */
  private async loadFromCache(profile: Profile): Promise<boolean> {
    try {
      const cacheDir = getCacheDirectory();
      const cachePath = join(cacheDir, `${this.profileName}-tools.json`);

      if (!existsSync(cachePath)) {
        return false;
      }

      const content = readFileSync(cachePath, 'utf-8');
      const cache = JSON.parse(content);

      // Use cache if less than 24 hours old
      if (Date.now() - cache.timestamp > 24 * 60 * 60 * 1000) {
        logger.info('Cache expired, will refresh tools');
        return false;
      }

      logger.info(`Using cached tools (${Object.keys(cache.mcps).length} MCPs)`)

      // Load MCPs and tools from cache
      for (const [mcpName, mcpData] of Object.entries(cache.mcps)) {
        const data = mcpData as any;

        this.definitions.set(mcpName, {
          name: mcpName,
          config: {
            name: mcpName,
            ...profile.mcpServers[mcpName]
          },
          tools: data.tools || [],
          serverInfo: data.serverInfo
        });

        // Add tools to allTools and create mappings
        const discoveryTools = [];
        for (const tool of data.tools || []) {
          // Handle both old (unprefixed) and new (prefixed) formats in cache
          const isAlreadyPrefixed = tool.name.startsWith(`${mcpName}:`);
          const prefixedToolName = isAlreadyPrefixed ? tool.name : `${mcpName}:${tool.name}`;
          const actualToolName = isAlreadyPrefixed ? tool.name.substring(mcpName.length + 1) : tool.name;

          // Ensure description is prefixed
          const hasPrefixedDesc = tool.description?.startsWith(`${mcpName}: `);
          const prefixedDescription = hasPrefixedDesc ? tool.description : `${mcpName}: ${tool.description || 'No description available'}`;

          this.allTools.push({
            name: prefixedToolName,
            description: prefixedDescription,
            mcpName: mcpName
          });

          // Map both formats for backward compatibility
          this.toolToMCP.set(actualToolName, mcpName);
          this.toolToMCP.set(prefixedToolName, mcpName);

          // Prepare for discovery engine indexing
          // Pass unprefixed name - RAG engine will add the prefix
          discoveryTools.push({
            id: prefixedToolName,
            name: actualToolName,  // Use unprefixed name here
            description: prefixedDescription,
            mcpServer: mcpName,
            inputSchema: {}
          });
        }

        // Index tools with discovery engine
        await this.discovery.indexMCPTools(mcpName, discoveryTools);
      }

      logger.info(`✅ Loaded ${this.allTools.length} tools from cache`);
      return true;

    } catch (error: any) {
      logger.warn(`Cache load failed: ${error.message}`);
      return false;
    }
  }

  private async saveToCache(profile: Profile): Promise<void> {
    try {
      // Use new optimized cache saving with profile hash
      await this.saveToOptimizedCache(profile);

    } catch (error: any) {
      logger.warn(`Cache save failed: ${error.message}`);
    }
  }

  /**
   * New optimized cache saving with profile hash and structured format
   */
  private async saveToOptimizedCache(profile: Profile): Promise<void> {
    try {
      logger.info('💾 Saving tools to optimized cache...');

      // Save all MCP definitions to tool metadata cache
      for (const [mcpName, definition] of this.definitions.entries()) {
        const mcpConfig = profile.mcpServers[mcpName];
        if (mcpConfig) {
          await this.cachePatcher.patchAddMCP(
            mcpName,
            mcpConfig,
            definition.tools,
            definition.serverInfo
          );
        }
      }

      // Update profile hash
      const profileHash = this.cachePatcher.generateProfileHash(profile);
      await this.cachePatcher.updateProfileHash(profileHash);

      logger.info(`💾 Saved ${this.allTools.length} tools to optimized cache with profile hash: ${profileHash.substring(0, 8)}...`);

    } catch (error: any) {
      logger.error(`Optimized cache save failed: ${error.message}`);
      throw error;
    }
  }

  private getToolSchema(mcpName: string, toolName: string): any {
    const connection = this.connections.get(mcpName);
    if (!connection) {
      // No persistent connection, try to get schema from definitions
      const definition = this.definitions.get(mcpName);
      if (!definition) return undefined;

      const tool = definition.tools.find(t => t.name === toolName);
      return tool ? (tool as any).inputSchema : undefined;
    }

    const tool = connection.tools.find(t => t.name === toolName);
    if (!tool) return undefined;

    return (tool as any).inputSchema;
  }

  /**
   * Check if a tool requires parameters
   */
  toolRequiresParameters(toolIdentifier: string): boolean {
    const [mcpName, toolName] = toolIdentifier.split(':');
    if (!mcpName || !toolName) return false;

    const schema = this.getToolSchema(mcpName, toolName);
    return ToolSchemaParser.hasRequiredParameters(schema);
  }

  /**
   * Get tool parameters for interactive prompting
   */
  getToolParameters(toolIdentifier: string): ParameterInfo[] {
    const [mcpName, toolName] = toolIdentifier.split(':');
    if (!mcpName || !toolName) return [];

    const schema = this.getToolSchema(mcpName, toolName);
    return ToolSchemaParser.parseParameters(schema);
  }

  /**
   * Validate tool parameters before execution
   */
  private validateToolParameters(mcpName: string, toolName: string, parameters: any): string | null {
    const schema = this.getToolSchema(mcpName, toolName);
    if (!schema) {
      // No schema available, allow execution (tool may not require validation)
      return null;
    }

    const requiredParams = ToolSchemaParser.getRequiredParameters(schema);
    const missingParams: string[] = [];

    // Check for missing required parameters
    for (const param of requiredParams) {
      if (parameters === null || parameters === undefined || !(param.name in parameters) || parameters[param.name] === null || parameters[param.name] === undefined || parameters[param.name] === '') {
        missingParams.push(param.name);
      }
    }

    if (missingParams.length > 0) {
      return `Missing required parameters: ${missingParams.join(', ')}. Use 'ncp find "${mcpName}:${toolName}" --depth 2' to see parameter details.`;
    }

    return null; // Validation passed
  }

  /**
   * Get tool context for parameter prediction
   */
  getToolContext(toolIdentifier: string): string {
    return ToolContextResolver.getContext(toolIdentifier);
  }

  /**
   * Find similar tool names using fuzzy matching
   */
  private findSimilarTools(targetTool: string, maxSuggestions: number = 3): string[] {
    const allTools = Array.from(this.toolToMCP.keys());
    const similarities = allTools.map(tool => ({
      tool,
      similarity: calculateSimilarity(targetTool, tool)
    }));

    return similarities
      .filter(item => item.similarity > 0.4) // Only suggest if reasonably similar
      .sort((a, b) => b.similarity - a.similarity)
      .slice(0, maxSuggestions)
      .map(item => item.tool);
  }

  /**
   * Generate hash for each MCP configuration
   */
  private generateConfigHashes(profile: Profile): Record<string, string> {
    const hashes: Record<string, string> = {};
    const crypto = require('crypto');

    for (const [mcpName, config] of Object.entries(profile.mcpServers)) {
      // Hash command + args + env + url for change detection
      const configString = JSON.stringify({
        command: config.command,
        args: config.args || [],
        env: config.env || {},
        url: config.url  // Include HTTP/SSE URL in hash
      });

      hashes[mcpName] = crypto.createHash('sha256').update(configString).digest('hex');
    }

    return hashes;
  }

  /**
   * Get current indexing progress
   */
  getIndexingProgress(): { current: number; total: number; currentMCP: string; estimatedTimeRemaining?: number } | null {
    return this.indexingProgress;
  }

  /**
   * Get MCP health status summary
   */
  getMCPHealthStatus(): { total: number; healthy: number; unhealthy: number; mcps: Array<{name: string; healthy: boolean}> } {
    const allMCPs = Array.from(this.definitions.keys());
    const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs);

    const mcpStatus = allMCPs.map(mcp => ({
      name: mcp,
      healthy: healthyMCPs.includes(mcp)
    }));

    return {
      total: allMCPs.length,
      healthy: healthyMCPs.length,
      unhealthy: allMCPs.length - healthyMCPs.length,
      mcps: mcpStatus
    };
  }

  /**
   * Enhance generic error messages with better context
   */
  private enhanceErrorMessage(error: any, toolName: string, mcpName: string): string {
    const errorMessage = error.message || error.toString() || 'Unknown error';

    // Always provide context and actionable guidance, regardless of specific error patterns
    let enhancedMessage = `Tool '${toolName}' failed in MCP '${mcpName}': ${errorMessage}`;

    // Add generic troubleshooting guidance
    const troubleshootingTips = [
      `• Check MCP '${mcpName}' status and configuration`,
      `• Use 'ncp find "${mcpName}:${toolName}" --depth 2' to verify tool parameters`,
      `• Ensure MCP server is running and accessible`
    ];

    enhancedMessage += `\n\nTroubleshooting:\n${troubleshootingTips.join('\n')}`;

    return enhancedMessage;
  }

  /**
   * Get all resources from active MCPs
   */
  async getAllResources(): Promise<Array<any>> {
    const resources: Array<any> = [];
    const allMCPs = Array.from(this.definitions.keys());
    const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs);

    for (const mcpName of healthyMCPs) {
      try {
        const mcpResources = await this.getResourcesFromMCP(mcpName);
        if (mcpResources && Array.isArray(mcpResources)) {
          // Add MCP source information to each resource with prefix
          const enrichedResources = mcpResources.map(resource => ({
            ...resource,
            name: `${mcpName}:${resource.name}`, // Add MCP prefix
            _source: mcpName
          }));
          resources.push(...enrichedResources);
        }
      } catch (error) {
        logger.warn(`Failed to get resources from ${mcpName}: ${error}`);
      }
    }

    return resources;
  }

  /**
   * Get all prompts from active MCPs
   */
  async getAllPrompts(): Promise<Array<any>> {
    const prompts: Array<any> = [];
    const allMCPs = Array.from(this.definitions.keys());
    const healthyMCPs = this.healthMonitor.getHealthyMCPs(allMCPs);

    for (const mcpName of healthyMCPs) {
      try {
        const mcpPrompts = await this.getPromptsFromMCP(mcpName);
        if (mcpPrompts && Array.isArray(mcpPrompts)) {
          // Add MCP source information to each prompt with prefix
          const enrichedPrompts = mcpPrompts.map(prompt => ({
            ...prompt,
            name: `${mcpName}:${prompt.name}`, // Add MCP prefix
            _source: mcpName
          }));
          prompts.push(...enrichedPrompts);
        }
      } catch (error) {
        logger.warn(`Failed to get prompts from ${mcpName}: ${error}`);
      }
    }

    return prompts;
  }

  /**
   * Get resources from a specific MCP
   */
  private async getResourcesFromMCP(mcpName: string): Promise<Array<any>> {
    try {
      const definition = this.definitions.get(mcpName);
      if (!definition) {
        return [];
      }

      // Create temporary connection for resources request
      const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');

      const silentEnv = {
        ...process.env,
        ...(definition.config.env || {}),
        MCP_SILENT: 'true',
        QUIET: 'true',
        NO_COLOR: 'true'
      };

      const transport = await this.createTransport(definition.config, silentEnv);

      const client = new Client(
        { name: 'ncp-oss-resources', version: '1.0.0' },
        { capabilities: {} }
      );

      // Connect with timeout and filtered output
      await withFilteredOutput(async () => {
        await Promise.race([
          client.connect(transport),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Resources connection timeout')), this.QUICK_PROBE_TIMEOUT)
          )
        ]);
      });

      // Get resources list with filtered output
      const response = await withFilteredOutput(async () => {
        return await client.listResources();
      });
      await client.close();

      return response.resources || [];

    } catch (error) {
      logger.debug(`Resources probe failed for ${mcpName}: ${error}`);
      return [];
    }
  }

  /**
   * Get prompts from a specific MCP
   */
  private async getPromptsFromMCP(mcpName: string): Promise<Array<any>> {
    try {
      const definition = this.definitions.get(mcpName);
      if (!definition) {
        return [];
      }

      // Create temporary connection for prompts request
      const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');

      const silentEnv = {
        ...process.env,
        ...(definition.config.env || {}),
        MCP_SILENT: 'true',
        QUIET: 'true',
        NO_COLOR: 'true'
      };

      const transport = await this.createTransport(definition.config, silentEnv);

      const client = new Client(
        { name: 'ncp-oss-prompts', version: '1.0.0' },
        { capabilities: {} }
      );

      // Connect with timeout and filtered output
      await withFilteredOutput(async () => {
        await Promise.race([
          client.connect(transport),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Prompts connection timeout')), this.QUICK_PROBE_TIMEOUT)
          )
        ]);
      });

      // Get prompts list with filtered output
      const response = await withFilteredOutput(async () => {
        return await client.listPrompts();
      });
      await client.close();

      return response.prompts || [];

    } catch (error) {
      logger.debug(`Prompts probe failed for ${mcpName}: ${error}`);
      return [];
    }
  }

  /**
   * Clean up idle connections (like commercial version)
   */
  private async cleanupIdleConnections(): Promise<void> {
    const now = Date.now();
    const toDisconnect: string[] = [];

    for (const [name, connection] of this.connections) {
      const idleTime = now - connection.lastUsed;

      if (idleTime > this.IDLE_TIMEOUT) {
        logger.info(`🧹 Disconnecting idle MCP: ${name} (idle for ${Math.round(idleTime / 1000)}s)`);
        toDisconnect.push(name);
      }
    }

    // Disconnect idle connections
    for (const name of toDisconnect) {
      await this.disconnectMCP(name);
    }
  }

  /**
   * Disconnect a specific MCP
   */
  private async disconnectMCP(mcpName: string): Promise<void> {
    const connection = this.connections.get(mcpName);
    if (!connection) return;

    try {
      await connection.client.close();
      this.connections.delete(mcpName);
      logger.debug(`Disconnected ${mcpName}`);
    } catch (error) {
      logger.error(`Error disconnecting ${mcpName}:`, error);
    }
  }

  async cleanup(): Promise<void> {
    logger.info('Shutting down NCP Orchestrator...');

    // Stop cleanup timer
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer);
    }

    // Finalize cache if it's being written
    if (this.csvCache) {
      try {
        await this.csvCache.finalize();
      } catch (error) {
        // Ignore finalize errors
      }
    }

    // Stop progress spinner if active
    if (this.showProgress) {
      const { spinner } = await import('../utils/progress-spinner.js');
      spinner.stop();
    }

    // Close any active connections
    for (const connection of this.connections.values()) {
      try {
        await connection.client.close();
      } catch (error) {
        // Ignore cleanup errors
      }
    }

    this.connections.clear();
    logger.info('NCP orchestrator cleanup completed');
  }

  /**
   * Get server descriptions for all configured MCPs
   */
  getServerDescriptions(): Record<string, string> {
    const descriptions: Record<string, string> = {};

    // From active connections
    for (const [mcpName, connection] of this.connections) {
      if (connection.serverInfo?.description) {
        descriptions[mcpName] = connection.serverInfo.description;
      } else if (connection.serverInfo?.title) {
        descriptions[mcpName] = connection.serverInfo.title;
      }
    }

    // From cached definitions
    for (const [mcpName, definition] of this.definitions) {
      if (!descriptions[mcpName] && definition.serverInfo?.description) {
        descriptions[mcpName] = definition.serverInfo.description;
      } else if (!descriptions[mcpName] && definition.serverInfo?.title) {
        descriptions[mcpName] = definition.serverInfo.title;
      }
    }

    return descriptions;
  }

  /**
   * Apply universal term frequency scoring boost with action word weighting
   * Uses SearchEnhancer for clean, extensible term classification and semantic mapping
   */
  private adjustScoresUniversally(query: string, results: any[]): any[] {
    const queryTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 2); // Skip very short terms

    return results.map(result => {
      const toolName = result.name.toLowerCase();
      const toolDescription = (result.description || '').toLowerCase();

      let nameBoost = 0;
      let descBoost = 0;

      // Process each query term with SearchEnhancer classification
      for (const term of queryTerms) {
        const termType = SearchEnhancer.classifyTerm(term);
        const weight = SearchEnhancer.getTypeWeights(termType);

        // Apply scoring based on term type
        if (toolName.includes(term)) {
          nameBoost += weight.name;
        }
        if (toolDescription.includes(term)) {
          descBoost += weight.desc;
        }

        // Apply semantic action matching for ACTION terms
        if (termType === 'ACTION') {
          const semantics = SearchEnhancer.getActionSemantics(term);
          for (const semanticMatch of semantics) {
            if (toolName.includes(semanticMatch)) {
              nameBoost += weight.name * 1.2; // 120% of full action weight for semantic matches (boosted)
            }
            if (toolDescription.includes(semanticMatch)) {
              descBoost += weight.desc * 1.2;
            }
          }

          // Apply intent penalties for conflicting actions
          const penalty = SearchEnhancer.getIntentPenalty(term, toolName);
          nameBoost -= penalty;
        }
      }

      // Apply diminishing returns to prevent excessive stacking
      const baseWeight = 0.15; // Base weight for diminishing returns calculation
      const finalNameBoost = nameBoost > 0 ? nameBoost * Math.pow(0.8, Math.max(0, nameBoost / baseWeight - 1)) : 0;
      const finalDescBoost = descBoost > 0 ? descBoost * Math.pow(0.8, Math.max(0, descBoost / (baseWeight / 2) - 1)) : 0;

      const totalBoost = 1 + finalNameBoost + finalDescBoost;

      return {
        ...result,
        confidence: result.confidence * totalBoost
      };
    });
  }

  /**
   * Trigger auto-import from MCP client
   * Called by MCPServer after it receives clientInfo from initialize request
   */
  async triggerAutoImport(clientName: string): Promise<void> {
    if (!this.profileManager) {
      // ProfileManager not initialized yet, skip auto-import
      logger.warn('ProfileManager not initialized, skipping auto-import');
      return;
    }

    try {
      await this.profileManager.tryAutoImportFromClient(clientName);
    } catch (error: any) {
      logger.error(`Auto-import failed: ${error.message}`);
    }
  }

  /**
   * Add internal MCPs to tool discovery
   * Called after external MCPs are indexed
   */
  private addInternalMCPsToDiscovery(): void {
    const internalMCPs = this.internalMCPManager.getAllInternalMCPs();

    for (const mcp of internalMCPs) {
      // Add to definitions (for consistency with external MCPs)
      this.definitions.set(mcp.name, {
        name: mcp.name,
        config: {
          name: mcp.name,
          command: 'internal',
          args: []
        },
        tools: mcp.tools.map(t => ({ name: t.name, description: t.description })),
        serverInfo: {
          name: mcp.name,
          version: '1.0.0',
          description: mcp.description
        }
      });

      // Add tools to allTools and discovery
      for (const tool of mcp.tools) {
        const toolId = `${mcp.name}:${tool.name}`;

        // Add to allTools
        this.allTools.push({
          name: tool.name,
          description: tool.description,
          mcpName: mcp.name
        });

        // Add to toolToMCP mapping
        this.toolToMCP.set(toolId, mcp.name);
      }

      // Index in discovery engine
      const discoveryTools = mcp.tools.map(t => ({
        id: `${mcp.name}:${t.name}`,
        name: t.name,
        description: t.description
      }));

      this.discovery.indexMCPTools(mcp.name, discoveryTools);

      logger.info(`Added internal MCP "${mcp.name}" with ${mcp.tools.length} tools`);
    }
  }

  /**
   * Get the ProfileManager instance
   * Used by MCP server for management operations (add/remove MCPs)
   */
  getProfileManager(): ProfileManager | null {
    return this.profileManager;
  }

  /**
   * Hash a string for change detection
   */
  private hashString(str: string): string {
    return createHash('sha256').update(str).digest('hex');
  }
}

export default NCPOrchestrator;
```

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

```typescript
#!/usr/bin/env node

import { Command } from 'commander';
import chalk from 'chalk';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { ProfileManager } from '../profiles/profile-manager.js';
import { MCPServer } from '../server/mcp-server.js';
import { ConfigManager } from '../utils/config-manager.js';
import { formatCommandDisplay } from '../utils/security.js';
import { TextUtils } from '../utils/text-utils.js';
import { OutputFormatter } from '../services/output-formatter.js';
import { ErrorHandler } from '../services/error-handler.js';
import { CachePatcher } from '../cache/cache-patcher.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { mcpWrapper } from '../utils/mcp-wrapper.js';
import { withFilteredOutput } from '../transports/filtered-stdio-transport.js';
import { UpdateChecker } from '../utils/update-checker.js';
import { setOverrideWorkingDirectory } from '../utils/ncp-paths.js';
import { ConfigSchemaReader } from '../services/config-schema-reader.js';
import { ConfigPrompter } from '../services/config-prompter.js';
import { SchemaCache } from '../cache/schema-cache.js';
import { getCacheDirectory } from '../utils/ncp-paths.js';

// Check for no-color flag early
const noColor = process.argv.includes('--no-color') || process.env.NO_COLOR === 'true';
if (noColor) {
  chalk.level = 0; // Disable colors globally
} else {
  // Ensure colors are enabled for TTY and when FORCE_COLOR is set
  if (process.env.FORCE_COLOR || process.stdout?.isTTY) {
    chalk.level = 3; // Full color support
  }
}

// Fuzzy matching helper for finding similar names
function findSimilarNames(target: string, availableNames: string[], maxSuggestions = 3): string[] {
  const targetLower = target.toLowerCase();

  // Score each name based on similarity
  const scored = availableNames.map(name => {
    const nameLower = name.toLowerCase();
    let score = 0;

    // Exact match gets highest score
    if (nameLower === targetLower) score += 100;

    // Contains target or target contains name
    if (nameLower.includes(targetLower)) score += 50;
    if (targetLower.includes(nameLower)) score += 50;

    // First few characters match
    const minLen = Math.min(targetLower.length, nameLower.length);
    for (let i = 0; i < minLen && i < 3; i++) {
      if (targetLower[i] === nameLower[i]) score += 10;
    }

    // Similar length bonus
    const lengthDiff = Math.abs(targetLower.length - nameLower.length);
    if (lengthDiff <= 2) score += 5;

    return { name, score };
  });

  // Filter out low scores and sort by score
  return scored
    .filter(item => item.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, maxSuggestions)
    .map(item => item.name);
}

// Enhanced remove validation helper
async function validateRemoveCommand(name: string, manager: ProfileManager, profiles: string[]): Promise<{
  mcpExists: boolean;
  suggestions: string[];
  allMCPs: string[];
}> {
  const allMCPs = new Set<string>();

  // Collect all MCP names from specified profiles
  for (const profileName of profiles) {
    const profile = await manager.getProfile(profileName);
    if (profile?.mcpServers) {
      Object.keys(profile.mcpServers).forEach(mcpName => allMCPs.add(mcpName));
    }
  }

  const mcpList = Array.from(allMCPs);
  const mcpExists = mcpList.includes(name);

  let suggestions: string[] = [];
  if (!mcpExists && mcpList.length > 0) {
    suggestions = findSimilarNames(name, mcpList);
  }

  return {
    mcpExists,
    suggestions,
    allMCPs: mcpList
  };
}

// Simple validation helper for ADD command
async function validateAddCommand(name: string, command: string, args: any[]): Promise<{
  message: string;
  suggestions: Array<{ command: string; description: string }>
}> {
  const suggestions: Array<{ command: string; description: string }> = [];

  const fullCommand = `${command} ${args.join(' ')}`.trim();

  // Basic command format validation and helpful tips
  if (command === 'npx' && args.length > 0) {
    // Clean up the command format - avoid duplication
    const cleanedArgs = args.filter(arg => arg !== '-y' || args.indexOf(arg) === 0);
    suggestions.push({
      command: fullCommand,
      description: 'NPM package execution - health monitor will validate if package exists and starts correctly'
    });
  } else if (command.startsWith('/') || command.startsWith('./') || command.includes('\\')) {
    suggestions.push({
      command: fullCommand,
      description: 'Local executable - health monitor will validate if command works'
    });
  } else if (command.includes('@') && !command.startsWith('npx')) {
    suggestions.push({
      command: `npx -y ${fullCommand}`,
      description: 'Consider using npx for npm packages'
    });
  } else {
    // Show the command as provided
    suggestions.push({
      command: fullCommand,
      description: 'Custom command - health monitor will validate functionality'
    });
  }

  return {
    message: chalk.dim('💡 MCP will be validated by health monitor after adding'),
    suggestions
  };
}

// Simple emoji support detection for cross-platform compatibility
const supportsEmoji = () => {
  // Windows Command Prompt and PowerShell often don't support emojis well
  if (process.platform === 'win32') {
    // Check if it's Windows Terminal (supports emojis) vs cmd/powershell
    return process.env.WT_SESSION || process.env.TERM_PROGRAM === 'vscode';
  }
  // macOS and Linux terminals generally support emojis
  return true;
};

const getIcon = (emoji: string, fallback: string) =>
  supportsEmoji() ? emoji : fallback;

// Configure OutputFormatter
OutputFormatter.configure({ noColor: !!noColor, emoji: !!supportsEmoji() });

// Use centralized version utility
import { version } from '../utils/version.js';

// Discovery function for single MCP - extracted from NCPOrchestrator.probeMCPTools
async function discoverSingleMCP(name: string, command: string, args: string[] = [], env: Record<string, string> = {}): Promise<{
  tools: Array<{name: string; description: string; inputSchema?: any}>;
  serverInfo?: {
    name: string;
    title?: string;
    version: string;
    description?: string;
    websiteUrl?: string;
  };
  configurationSchema?: any;
}> {
  const config = { name, command, args, env };

  if (!config.command) {
    throw new Error(`Invalid config for ${config.name}`);
  }

  let client: Client | null = null;
  let transport: StdioClientTransport | null = null;
  const DISCOVERY_TIMEOUT = 8000; // 8 seconds

  try {
    // Create wrapper command for discovery phase
    const wrappedCommand = mcpWrapper.createWrapper(
      config.name,
      config.command,
      config.args || []
    );

    // Create temporary connection for discovery
    const silentEnv = {
      ...process.env,
      ...(config.env || {}),
      MCP_SILENT: 'true',
      QUIET: 'true',
      NO_COLOR: 'true'
    };

    transport = new StdioClientTransport({
      command: wrappedCommand.command,
      args: wrappedCommand.args,
      env: silentEnv as Record<string, string>
    });

    client = new Client(
      { name: 'ncp-oss', version: '1.0.0' },
      { capabilities: {} }
    );

    // Connect with timeout and filtered output
    await withFilteredOutput(async () => {
      await Promise.race([
        client!.connect(transport!),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Discovery timeout')), DISCOVERY_TIMEOUT)
        )
      ]);
    });

    // Capture server info after connection
    const serverInfo = client!.getServerVersion();

    // Capture configuration schema if available
    // TODO: Once MCP SDK is updated to support top-level configurationSchema,
    // also check for it directly. For now, check experimental capabilities.
    const serverCapabilities = client!.getServerCapabilities();
    const configurationSchema = (serverCapabilities as any)?.experimental?.configurationSchema;

    // Get tool list with filtered output
    const response = await withFilteredOutput(async () => {
      return await client!.listTools();
    });

    const tools = response.tools.map(t => ({
      name: t.name,
      description: t.description || '',
      inputSchema: t.inputSchema || {}
    }));

    // Disconnect immediately
    await client.close();

    return {
      tools,
      serverInfo: serverInfo ? {
        name: serverInfo.name || config.name,
        title: serverInfo.title,
        version: serverInfo.version || 'unknown',
        description: serverInfo.title || serverInfo.name || undefined,
        websiteUrl: serverInfo.websiteUrl
      } : undefined,
      configurationSchema
    };

  } catch (error: any) {
    // Clean up connections
    try {
      if (client) {
        await client.close();
      }
    } catch (closeError) {
      // Ignore close errors
    }

    throw new Error(`Failed to discover tools from ${config.name}: ${error.message}`);
  }
}

const program = new Command();


// Set version
program.version(version, '-v, --version', 'output the current version');


// Custom help configuration with colors and enhanced content
program
  .name('ncp')
  .description(`
${chalk.bold.white('Natural Context Provider')} ${chalk.dim('v' + version)} - ${chalk.cyan('1 MCP to rule them all')}
${chalk.dim('Orchestrates multiple MCP servers through a unified interface for AI assistants.')}
${chalk.dim('Reduces cognitive load and clutter, saving tokens and speeding up AI interactions.')}
${chalk.dim('Enables smart tool discovery across all configured servers with vector similarity search.')}`)
  .option('--profile <name>', 'Profile to use (default: all)')
  .option('--working-dir <path>', 'Working directory for profile resolution (overrides current directory)')
  .option('--force-retry', 'Force retry all failed MCPs immediately (ignores scheduled retry times)')
  .option('--no-color', 'Disable colored output');


// Configure help with enhanced formatting, Quick Start, and examples
program.configureHelp({
  sortSubcommands: true,
  formatHelp: (cmd, helper) => {
    // Calculate proper padding based on actual command names and options separately
    const allCommands = cmd.commands.filter((cmd: any) => !cmd.hidden);
    const maxCmdLength = allCommands.length > 0 ? Math.max(...allCommands.map(cmd => cmd.name().length)) : 0;
    const maxOptionLength = cmd.options.length > 0 ? Math.max(...cmd.options.map(option => option.flags.length)) : 0;

    const cmdPad = maxCmdLength + 4; // Add extra space for command alignment
    const optionPad = maxOptionLength + 4; // Add extra space for option alignment
    const helpWidth = helper.helpWidth || 80;

    function formatItem(term: string, description?: string, padding?: number): string {
      if (description) {
        const pad = padding || cmdPad;
        return term.padEnd(pad) + description;
      }
      return term;
    }

    // Add description first
    let output = cmd.description() + '\n\n';

    // Then usage and config info
    output += `${chalk.bold.white('Usage:')} ${cmd.name()} [options] [command]\n`;
    output += `${chalk.yellow('NCP config files:')} ~/.ncp/profiles/\n\n`;

    // Options
    if (cmd.options.length) {
      output += chalk.bold.white('Options:') + '\n';
      cmd.options.forEach(option => {
        // Calculate padding based on raw flags, not styled version
        const rawPadding = '  ' + option.flags;
        const paddedRaw = rawPadding.padEnd(optionPad + 2);
        const styledFlags = chalk.cyan(option.flags);
        const description = chalk.white(option.description);

        output += '  ' + styledFlags + ' '.repeat(paddedRaw.length - rawPadding.length) + description + '\n';
      });
      output += '\n';
    }

    // Commands
    const commands = cmd.commands.filter((cmd: any) => !cmd.hidden);
    if (commands.length) {
      output += chalk.bold.white('Commands:') + '\n';
      commands.sort((a, b) => a.name().localeCompare(b.name()));

      commands.forEach(cmd => {
        // Group commands by category with enhanced styling
        const managementCommands = ['add', 'remove', 'import', 'list', 'config'];
        const discoveryCommands = ['find'];
        const executionCommands = ['run'];

        let cmdName = cmd.name();
        let styledCmdName = cmdName;
        if (managementCommands.includes(cmd.name())) {
          styledCmdName = chalk.cyan(cmd.name());
        } else if (discoveryCommands.includes(cmd.name())) {
          styledCmdName = chalk.green.bold(cmd.name());
        } else if (executionCommands.includes(cmd.name())) {
          styledCmdName = chalk.yellow.bold(cmd.name());
        }

        // Calculate padding based on raw command name, not styled version
        const rawPadding = '  ' + cmdName;
        const paddedRaw = rawPadding.padEnd(cmdPad + 2);  // Use cmdPad + 2 for consistency
        const description = chalk.white(cmd.description());

        output += '  ' + styledCmdName + ' '.repeat(paddedRaw.length - rawPadding.length) + description + '\n';
      });
    }

    return output;
  }
});


// Add help command
program
  .command('help [command]')
  .description('Show help for NCP or a specific command')
  .action((command) => {
    if (command) {
      const cmd = program.commands.find(cmd => cmd.name() === command);
      if (cmd) {
        cmd.help();
      } else {
        console.log(`Unknown command: ${command}`);
        program.help();
      }
    } else {
      program.help();
    }
  });

// Add Quick Start and Examples after all commands are defined
program.addHelpText('after', `
${chalk.bold.white('Quick Start:')}
  ${chalk.cyan('1a')} Import existing MCPs: ${chalk.green('ncp config import')} ${chalk.dim('(copy JSON first)')}
  ${chalk.cyan('1b')} Or add manually: ${chalk.green('ncp add <name> <command>')}
  ${chalk.cyan('2')} Configure NCP in AI client settings

${chalk.bold.white('Examples:')}
  $ ${chalk.yellow('ncp config import config.json')} ${chalk.dim('              # Import from file')}
  $ ${chalk.yellow('ncp add filesystem npx @modelcontextprotocol/server-filesystem /tmp')}
  $ ${chalk.yellow('ncp find "file operations"')}
  $ ${chalk.yellow('ncp run filesystem:read_file --params \'{"path": "/tmp/example.txt"}\'')}
  $ ${chalk.yellow('ncp list --depth 1')}`);

// Check if we should run as MCP server
// MCP server mode: default when no CLI commands are provided, or when --profile is specified
const profileIndex = process.argv.indexOf('--profile');
const hasCommands = process.argv.includes('find') ||
  process.argv.includes('add') ||
  process.argv.includes('list') ||
  process.argv.includes('remove') ||
  process.argv.includes('run') ||
  process.argv.includes('config') ||
  process.argv.includes('help') ||
  process.argv.includes('--help') ||
  process.argv.includes('-h') ||
  process.argv.includes('--version') ||
  process.argv.includes('-v') ||
  process.argv.includes('import') ||
  process.argv.includes('analytics') ||
  process.argv.includes('visual') ||
  process.argv.includes('update') ||
  process.argv.includes('repair');

// Default to MCP server mode when no CLI commands are provided
// This ensures compatibility with Claude Desktop and other MCP clients that expect server mode by default
const shouldRunAsServer = !hasCommands;

if (shouldRunAsServer) {
  // Handle --working-dir parameter for MCP server mode
  const workingDirIndex = process.argv.indexOf('--working-dir');
  if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) {
    const workingDirValue = process.argv[workingDirIndex + 1];
    setOverrideWorkingDirectory(workingDirValue);
  }

  // Running as MCP server: ncp (defaults to 'all' profile) or ncp --profile <name>
  // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE to 'default' or anything else!
  const profileName = profileIndex !== -1 ? (process.argv[profileIndex + 1] || 'all') : 'all';

  // Debug logging for integration tests
  if (process.env.NCP_DEBUG === 'true') {
    console.error(`[DEBUG] profileIndex: ${profileIndex}`);
    console.error(`[DEBUG] process.argv: ${process.argv.join(' ')}`);
    console.error(`[DEBUG] Selected profile: ${profileName}`);
  }

  const server = new MCPServer(profileName);
  server.run().catch(console.error);
} else {
  // Handle --working-dir parameter for CLI mode
  const workingDirIndex = process.argv.indexOf('--working-dir');
  if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) {
    const workingDirValue = process.argv[workingDirIndex + 1];
    setOverrideWorkingDirectory(workingDirValue);
  }

  // Running as CLI tool

// Add MCP command
program
  .command('add <name> <command> [args...]')
  .description('Add an MCP server to a profile')
  .option('--profile <names...>', 'Profile(s) to add to (can specify multiple, default: all)')
  .option('--env <vars...>', 'Environment variables (KEY=value)')
  .action(async (name, command, args, options) => {
    console.log(`\n${chalk.blue(`📦 Adding MCP server: ${chalk.bold(name)}`)}`);

    const manager = new ProfileManager();
    await manager.initialize();

    // Show helpful guidance without hard validation
    const guidance = await validateAddCommand(name, command, args);
    console.log(guidance.message);
    if (guidance.suggestions.length > 0) {
      console.log('\n📋 Command validation:');
      guidance.suggestions.forEach((suggestion, index) => {
        if (index === 0) {
          // Main command
          console.log(`   ${chalk.cyan(suggestion.command)}`);
          console.log(`   ${chalk.dim(suggestion.description)}`);
        } else {
          // Alternative suggestions
          console.log(chalk.dim(`\n💡 Alternative: ${suggestion.command}`));
          console.log(chalk.dim(`   ${suggestion.description}`));
        }
      });
      console.log('');
    }

    // Parse environment variables
    const env: Record<string, string> = {};
    if (options.env) {
      console.log(chalk.dim('🔧 Processing environment variables...'));
      for (const envVar of options.env) {
        const [key, value] = envVar.split('=');
        if (key && value) {
          env[key] = value;
          console.log(chalk.dim(`   ${key}=${formatCommandDisplay(value)}`));
        } else {
          console.log(chalk.yellow(`⚠️  Invalid environment variable format: ${envVar}`));
        }
      }
    }

    const config = {
      command,
      args: args || [],
      ...(Object.keys(env).length > 0 && { env })
    };

    // Show what will be added
    // Determine which profiles to add to
    // ⚠️ CRITICAL: Default MUST be ['all'] - DO NOT CHANGE!
    const profiles = options.profile || ['all'];

    console.log('\n📋 Profile configuration:');
    console.log(`   ${chalk.cyan('Target profiles:')} ${profiles.join(', ')}`);
    if (Object.keys(env).length > 0) {
      console.log(`   ${chalk.cyan('Environment variables:')} ${Object.keys(env).length} configured`);
      Object.entries(env).forEach(([key, value]) => {
        console.log(chalk.dim(`     ${key}=${formatCommandDisplay(value)}`));
      });
    }

    console.log(''); // spacing

    // Initialize schema services
    const schemaReader = new ConfigSchemaReader();
    const configPrompter = new ConfigPrompter();
    const schemaCache = new SchemaCache(getCacheDirectory());

    // Try to discover and detect configuration requirements BEFORE adding to profile
    console.log(chalk.dim('🔍 Discovering tools and configuration requirements...'));
    const discoveryStart = Date.now();

    let discoveryResult: Awaited<ReturnType<typeof discoverSingleMCP>> | null = null;
    let finalConfig = { ...config };
    let detectedSchema: any = null;

    try {
      discoveryResult = await discoverSingleMCP(name, command, args, env);
      const discoveryTime = Date.now() - discoveryStart;

      console.log(`${chalk.green('✅')} Found ${discoveryResult.tools.length} tools in ${discoveryTime}ms`);

      // Two-tier configuration detection strategy:
      // Tier 1: MCP Protocol configurationSchema (from server capabilities)
      // Tier 2: Error parsing (fallback - happens on failure below)

      // Tier 1: Check for MCP protocol schema
      if (discoveryResult.configurationSchema) {
        detectedSchema = schemaReader.readSchema({
          protocolVersion: '1.0',
          capabilities: {},
          serverInfo: { name, version: '1.0' },
          configurationSchema: discoveryResult.configurationSchema
        });
        if (detectedSchema) {
          console.log(chalk.dim('   Configuration schema detected (MCP protocol)'));
        }
      }

      // Apply detected schema if we have one with required config
      if (detectedSchema && schemaReader.hasRequiredConfig(detectedSchema)) {
        console.log(chalk.cyan('\n📋 Configuration required'));

        // Prompt for configuration
        const promptedConfig = await configPrompter.promptForConfig(detectedSchema, name);

        // Merge prompted config with existing config
        finalConfig = {
          command: config.command,
          args: [...(config.args || []), ...(promptedConfig.arguments || [])],
          env: { ...(config.env || {}), ...(promptedConfig.environmentVariables || {}) }
        };

        // Display summary
        configPrompter.displaySummary(promptedConfig, name);

        // Cache schema for future use
        schemaCache.save(name, detectedSchema);
        console.log(chalk.dim('✓ Configuration schema cached'));
      }
    } catch (discoveryError: any) {
      console.log(`${chalk.yellow('⚠️')} Discovery failed: ${discoveryError.message}`);
      console.log(chalk.dim('   Proceeding with manual configuration...'));
      // Tier 3: Error parsing would happen here in future enhancement
      // Continue with manual config - error will be saved to profile
    }

    // Initialize cache patcher
    const cachePatcher = new CachePatcher();

    for (const profileName of profiles) {
      try {
        // 1. Update profile with final configuration
        await manager.addMCPToProfile(profileName, name, finalConfig);
        console.log(`\n${OutputFormatter.success(`Added ${name} to profile: ${profileName}`)}`);

        // 2. Update cache if we have discovery results
        if (discoveryResult) {
          if (discoveryResult.tools.length > 0) {
            console.log(chalk.dim('   Tools discovered:'));
            // Show first few tools
            const toolsToShow = discoveryResult.tools.slice(0, 3);
            toolsToShow.forEach(tool => {
              const shortDesc = tool.description?.length > 50
                ? tool.description.substring(0, 50) + '...'
                : tool.description;
              console.log(chalk.dim(`   • ${tool.name}: ${shortDesc}`));
            });
            if (discoveryResult.tools.length > 3) {
              console.log(chalk.dim(`   • ... and ${discoveryResult.tools.length - 3} more`));
            }
          }

          // 3. Patch tool metadata cache with final config
          await cachePatcher.patchAddMCP(name, finalConfig, discoveryResult.tools, discoveryResult.serverInfo);

          // 4. Update profile hash
          const profile = await manager.getProfile(profileName);
          const profileHash = cachePatcher.generateProfileHash(profile);
          await cachePatcher.updateProfileHash(profileHash);

          console.log(`${chalk.green('✅')} Cache updated for ${name}`);
        } else {
          console.log(chalk.dim('   Profile updated, but cache not built. Run "ncp find <query>" to build cache later.'));
        }

      } catch (error: any) {
        const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('profile', 'add', `${name} to ${profileName}`));
        console.log('\n' + ErrorHandler.formatForConsole(errorResult));
      }
    }

    console.log(chalk.dim('\n💡 Next steps:'));
    console.log(chalk.dim('  •') + ' View profiles: ' + chalk.cyan('ncp list'));
    console.log(chalk.dim('  •') + ' Test discovery: ' + chalk.cyan('ncp find <query>'));
  });


// Lightweight function to read MCP info from cache without full orchestrator initialization
async function loadMCPInfoFromCache(mcpDescriptions: Record<string, string>, mcpToolCounts: Record<string, number>, mcpVersions: Record<string, string>): Promise<boolean> {
  const { readFileSync, existsSync } = await import('fs');
  const { getCacheDirectory } = await import('../utils/ncp-paths.js');
  const { join } = await import('path');

  const cacheDir = getCacheDirectory();
  const cachePath = join(cacheDir, 'all-tools.json');

  if (!existsSync(cachePath)) {
    return false; // No cache available
  }

  try {
    const cacheContent = readFileSync(cachePath, 'utf-8');
    const cache = JSON.parse(cacheContent);

    // Extract server info and tool counts from cache
    for (const [mcpName, mcpData] of Object.entries(cache.mcps || {})) {
      const data = mcpData as any;

      // Extract server description (without version)
      if (data.serverInfo?.description && data.serverInfo.description !== mcpName) {
        mcpDescriptions[mcpName] = data.serverInfo.description;
      } else if (data.serverInfo?.title) {
        mcpDescriptions[mcpName] = data.serverInfo.title;
      }

      // Extract version separately
      if (data.serverInfo?.version && data.serverInfo.version !== 'unknown') {
        mcpVersions[mcpName] = data.serverInfo.version;
      }

      // Count tools
      if (data.tools && Array.isArray(data.tools)) {
        mcpToolCounts[mcpName] = data.tools.length;
      }
    }
    return true; // Cache was successfully loaded
  } catch (error) {
    // Ignore cache reading errors - will just show without descriptions
    return false;
  }
}


// List command
program
  .command('list [filter]')
  .description('List all profiles and their MCPs with intelligent filtering')
  .option('--limit <number>', 'Maximum number of items to show (default: 20)')
  .option('--page <number>', 'Page number for pagination (default: 1)')
  .option('--depth <number>', 'Display depth: 0=profiles only, 1=profiles+MCPs+description, 2=profiles+MCPs+description+tools (default: 2)')
  .option('--search <query>', 'Search in MCP names and descriptions')
  .option('--profile <name>', 'Show only specific profile')
  .option('--sort <field>', 'Sort by: name, tools, profiles (default: name)', 'name')
  .option('--non-empty', 'Show only profiles with configured MCPs')
  .action(async (filter, options) => {
    const limit = parseInt(options.limit || '20');
    const page = parseInt(options.page || '1');
    const depth = parseInt(options.depth || '2');

    const manager = new ProfileManager();
    await manager.initialize();

    let profiles = manager.listProfiles();

    if (profiles.length === 0) {
      console.log(chalk.yellow('📋 No profiles configured'));
      console.log(chalk.dim('💡 Use: ncp add <name> <command> to add an MCP server'));
      return;
    }

    // Apply profile filtering first
    if (options.profile) {
      const targetProfile = options.profile.toLowerCase();
      profiles = profiles.filter(p => p.toLowerCase() === targetProfile);

      if (profiles.length === 0) {
        console.log(chalk.yellow(`⚠️  Profile "${options.profile}" not found`));

        // Suggest similar profiles
        const allProfiles = manager.listProfiles();
        const suggestions = findSimilarNames(options.profile, allProfiles);
        if (suggestions.length > 0) {
          console.log(chalk.yellow('\n💡 Did you mean:'));
          suggestions.forEach((suggestion, index) => {
            console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
          });
        } else {
          console.log(chalk.yellow('\n📋 Available profiles:'));
          allProfiles.forEach((profile, index) => {
            console.log(`  ${index + 1}. ${chalk.cyan(profile)}`);
          });
        }
        return;
      }
    }

    // Initialize orchestrator to get MCP descriptions and tool counts if needed
    let orchestrator;
    let mcpDescriptions: Record<string, string> = {};
    let mcpToolCounts: Record<string, number> = {};
    let mcpVersions: Record<string, string> = {};

    if (depth >= 1) {
      try {
        // Lightweight cache reading - no orchestrator initialization needed
        const cacheLoaded = await loadMCPInfoFromCache(mcpDescriptions, mcpToolCounts, mcpVersions);

        if (!cacheLoaded) {
          // Show helpful message about building cache
          console.log(chalk.dim('💡 No MCP cache found. Use `ncp find <query>` to discover tools and build cache.'));
        }
      } catch (error) {
        // If cache reading fails, continue without descriptions
        console.log(chalk.dim('Note: Could not load MCP descriptions and tool counts'));
      }
    }

    // Collect and filter data first
    const profileData: Array<{
      name: string;
      mcps: Record<string, any>;
      filteredMcps: Record<string, any>;
      originalCount: number;
      filteredCount: number;
    }> = [];

    for (const profileName of profiles) {
      const mcps = await manager.getProfileMCPs(profileName) || {};
      let filteredMcps = mcps;

      // Apply MCP filtering
      if (filter || options.search) {
        const query = filter || options.search;
        const queryLower = query.toLowerCase();

        filteredMcps = Object.fromEntries(
          Object.entries(mcps).filter(([mcpName, config]) => {
            const description = mcpDescriptions[mcpName] || mcpName;
            return (
              mcpName.toLowerCase().includes(queryLower) ||
              description.toLowerCase().includes(queryLower)
            );
          })
        );
      }

      // Apply non-empty filter
      if (options.nonEmpty && Object.keys(filteredMcps).length === 0) {
        continue; // Skip empty profiles when --non-empty is used
      }

      profileData.push({
        name: profileName,
        mcps,
        filteredMcps,
        originalCount: Object.keys(mcps).length,
        filteredCount: Object.keys(filteredMcps).length
      });
    }

    // Check if filtering returned no results
    if (profileData.length === 0) {
      const queryInfo = filter || options.search;
      console.log(chalk.yellow(`⚠️  No MCPs found${queryInfo ? ` matching "${queryInfo}"` : ''}`));

      // Suggest available MCPs if search was used
      if (queryInfo) {
        const allMcps = new Set<string>();
        for (const profile of manager.listProfiles()) {
          const mcps = await manager.getProfileMCPs(profile);
          if (mcps) {
            Object.keys(mcps).forEach(mcp => allMcps.add(mcp));
          }
        }

        if (allMcps.size > 0) {
          const suggestions = findSimilarNames(queryInfo, Array.from(allMcps));
          if (suggestions.length > 0) {
            console.log(chalk.yellow('\n💡 Did you mean:'));
            suggestions.forEach((suggestion, index) => {
              console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
            });
          } else {
            console.log(chalk.yellow('\n📋 Available MCPs:'));
            Array.from(allMcps).slice(0, 10).forEach((mcp, index) => {
              console.log(`  ${index + 1}. ${chalk.cyan(mcp)}`);
            });
          }
        }
      }
      return;
    }

    // Sort profiles if requested
    if (options.sort !== 'name') {
      profileData.sort((a, b) => {
        switch (options.sort) {
          case 'tools':
            return b.filteredCount - a.filteredCount;
          case 'profiles':
            return a.name.localeCompare(b.name);
          default:
            return a.name.localeCompare(b.name);
        }
      });
    }

    // Display results
    console.log('');
    console.log(chalk.bold.white('Profiles ▶ MCPs'));
    if (filter || options.search) {
      console.log(chalk.dim(`🔍 Filtered by: "${filter || options.search}"`));
    }
    console.log('');

    let totalMCPs = 0;

    for (const data of profileData) {
      const { name: profileName, filteredMcps, filteredCount } = data;
      totalMCPs += filteredCount;

      // Profile header with count
      const countBadge = filteredCount > 0 ? chalk.green(`${filteredCount} MCPs`) : chalk.dim('empty');
      console.log(`📦 ${chalk.bold.white(profileName)}`, chalk.dim(`(${countBadge})`));

      // Depth 0: profiles only - skip MCP details
      if (depth === 0) {
        // Already showing profile, nothing more needed
      } else if (filteredMcps && Object.keys(filteredMcps).length > 0) {
        const mcpEntries = Object.entries(filteredMcps);
        mcpEntries.forEach(([mcpName, config], index) => {
          const isLast = index === mcpEntries.length - 1;
          const connector = isLast ? '└──' : '├──';
          const indent = isLast ? '   ' : '│  ';

          // MCP name with tool count (following profile count style)
          const toolCount = mcpToolCounts[mcpName];
          const versionPart = mcpVersions[mcpName] ? chalk.magenta(`v${mcpVersions[mcpName]}`) : '';

          // If not in cache, it means MCP hasn't connected successfully
          const toolPart = toolCount !== undefined ? chalk.green(`${toolCount} tools`) : chalk.gray('not available');

          const badge = versionPart && toolPart ? chalk.dim(` (${versionPart} | ${toolPart})`) :
                       versionPart ? chalk.dim(` (${versionPart})`) :
                       toolPart ? chalk.dim(` (${toolPart})`) : '';
          console.log(`  ${connector} ${chalk.bold.cyanBright(mcpName)}${badge}`);

          // Depth 1+: Show description if available and meaningful
          if (depth >= 1 && mcpDescriptions[mcpName]) {
            const description = mcpDescriptions[mcpName];
            // Skip descriptions that just repeat the MCP name (no value added)
            if (description.toLowerCase() !== mcpName.toLowerCase()) {
              console.log(`  ${indent} ${chalk.white(description)}`);
            }
          }

          // Depth 2: Show command with reverse colors and text wrapping
          if (depth >= 2) {
            const commandText = formatCommandDisplay(config.command, config.args);
            const maxWidth = process.stdout.columns ? process.stdout.columns - 6 : 80; // Leave space for indentation
            const wrappedLines = TextUtils.wrapTextWithBackground(commandText, maxWidth, `  ${indent} `, (text: string) => chalk.bgGray.black(text));
            console.log(wrappedLines);
          }
        });
      } else if (depth > 0) {
        console.log(chalk.dim('  └── (empty)'));
      }
      console.log('');
    }

    // No cleanup needed for lightweight approach

  });


// Helper function to format find command output with consistent color scheme
function formatFindOutput(text: string): string {
  return text
    // Tool names in headers: # **toolname** -> bold light blue
    .replace(/^# \*\*([^*]+)\*\*/gm, (match, toolName) => chalk.bold.cyanBright(toolName))
    // Parameters: ### param: type (optional) - description
    .replace(/^### ([^:]+): (.+)$/gm, (match, param, rest) => {
      // Handle: type (optional) - description
      const optionalDescMatch = rest.match(/^(.+?)\s+\*\(optional\)\*\s*-\s*(.+)$/);
      if (optionalDescMatch) {
        return `${chalk.yellow(param)}: ${chalk.cyan(optionalDescMatch[1])} ${chalk.dim('(optional)')} - ${chalk.white(optionalDescMatch[2])}`;
      }

      // Handle: type - description
      const descMatch = rest.match(/^(.+?)\s*-\s*(.+)$/);
      if (descMatch) {
        return `${chalk.yellow(param)}: ${chalk.cyan(descMatch[1])} - ${chalk.white(descMatch[2])}`;
      }

      // Handle: type (optional)
      const optionalMatch = rest.match(/^(.+)\s+\*\(optional\)\*$/);
      if (optionalMatch) {
        return `${chalk.yellow(param)}: ${chalk.cyan(optionalMatch[1])} ${chalk.dim('(optional)')}`;
      }

      // Handle: type only
      return `${chalk.yellow(param)}: ${chalk.cyan(rest)}`;
    })
    // Parameter descriptions: #### description -> dim
    .replace(/^#### (.+)$/gm, (match, desc) => chalk.dim(desc))
    // Separators: --- -> dim
    .replace(/^---$/gm, chalk.dim('---'))
    // Bold text in general: **text** -> bold for tool names in lists
    .replace(/\*\*([^*]+)\*\*/g, (match, text) => {
      // Check if it's a tool name (contains colon)
      if (text.includes(':')) {
        return chalk.bold.cyanBright(text);
      } else {
        // MCP name or other bold text
        return chalk.bold(text);
      }
    })
    // [no parameters] -> dim
    .replace(/\*\[no parameters\]\*/g, chalk.dim('[no parameters]'))
    // Italic text: *text* -> dim for tips
    .replace(/\*([^*\[]+)\*/g, (match, text) => chalk.dim(text))
    // Confidence percentages: (XX% match) -> green percentage
    .replace(/\((\d+)% match\)/g, (match, percentage) => chalk.dim(`(${chalk.green(percentage + '%')} match)`))
    // Header search results - make query bold white
    .replace(/Found tools for "([^"]+)"/g, (match, query) => `Found tools for ${chalk.bold.white(`"${query}"`)}`)
    // No results message
    .replace(/❌ No tools found for "([^"]+)"/g, (match, query) => `❌ No tools found for ${chalk.bold.white(`"${query}"`)}`)
    // Usage tips
    .replace(/^💡 (.+)$/gm, (match, tip) => `💡 ${chalk.white(tip)}`);
}



// Remove command
program
  .command('remove <name>')
  .description('Remove an MCP server from profiles')
  .option('--profile <names...>', 'Profile(s) to remove from (can specify multiple, default: all)')
  .action(async (name, options) => {
    console.log(chalk.blue(`🗑️  Removing MCP server: ${chalk.bold(name)}`));

    const manager = new ProfileManager();
    await manager.initialize();

    // ⚠️ CRITICAL: Default MUST be ['all'] - DO NOT CHANGE!
    const profiles = options.profile || ['all'];

    // Validate if MCP exists and get suggestions
    const validation = await validateRemoveCommand(name, manager, profiles);

    if (!validation.mcpExists) {
      console.log(chalk.yellow(`⚠️  MCP "${name}" not found in specified profiles`));

      if (validation.suggestions.length > 0) {
        console.log(chalk.yellow('\n💡 Did you mean:'));
        validation.suggestions.forEach((suggestion, index) => {
          console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
        });
        console.log(chalk.yellow('\n💡 Use the exact name from the list above'));
      } else if (validation.allMCPs.length > 0) {
        console.log(chalk.yellow('\n📋 Available MCPs in these profiles:'));
        validation.allMCPs.forEach((mcp, index) => {
          console.log(`  ${index + 1}. ${chalk.cyan(mcp)}`);
        });
      } else {
        console.log(chalk.dim('\n📋 No MCPs found in specified profiles'));
        console.log(chalk.dim('💡 Use \'ncp list\' to see all configured MCPs'));
      }

      console.log(chalk.yellow('\n⚠️  No changes made'));
      return;
    }

    // MCP exists, proceed with removal
    console.log(chalk.green('✅ MCP found, proceeding with removal...\n'));

    // Initialize cache patcher
    const cachePatcher = new CachePatcher();

    for (const profileName of profiles) {
      try {
        // 1. Remove from profile
        await manager.removeMCPFromProfile(profileName, name);
        console.log(OutputFormatter.success(`Removed ${name} from profile: ${profileName}`));

        // 2. Clean up caches
        console.log(chalk.dim('🔧 Cleaning up caches...'));

        try {
          // Remove from tool metadata cache
          await cachePatcher.patchRemoveMCP(name);

          // Remove from embeddings cache
          await cachePatcher.patchRemoveEmbeddings(name);

          // Update profile hash
          const profile = await manager.getProfile(profileName);
          if (profile) {
            const profileHash = cachePatcher.generateProfileHash(profile);
            await cachePatcher.updateProfileHash(profileHash);
          }

          console.log(`${chalk.green('✅')} Cache cleaned for ${name}`);

        } catch (cacheError: any) {
          console.log(`${chalk.yellow('⚠️')} Could not clean cache: ${cacheError.message}`);
          console.log(chalk.dim('   Profile updated successfully. Cache will rebuild on next discovery.'));
        }

      } catch (error: any) {
        const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('profile', 'remove', `${name} from ${profileName}`));
        console.log('\n' + ErrorHandler.formatForConsole(errorResult));
      }
    }
  });

// Config command group
const configCmd = program
  .command('config')
  .description('Manage NCP configuration (import, validate, edit)');

configCmd
  .command('import [file]')
  .description('Import MCP configurations from file or clipboard')
  .option('--profile <name>', 'Target profile (default: all)')
  .option('--dry-run', 'Show what would be imported without actually importing')
  .action(async (file, options) => {
    try {
      const manager = new ConfigManager();
      await manager.importConfig(file, options.profile, options.dryRun);
    } catch (error: any) {
      const errorResult = ErrorHandler.handle(error, ErrorHandler.createContext('config', 'import', file || 'clipboard'));
      console.log('\n' + ErrorHandler.formatForConsole(errorResult));
      process.exit(1);
    }
  });

configCmd
  .command('edit')
  .description('Open config directory in default editor')
  .action(async () => {
    const manager = new ConfigManager();
    await manager.editConfig();
  });

configCmd
  .command('validate')
  .description('Validate current configuration')
  .action(async () => {
    const manager = new ConfigManager();
    await manager.validateConfig();
  });

configCmd
  .command('location')
  .description('Show configuration file locations')
  .action(async () => {
    const manager = new ConfigManager();
    await manager.showConfigLocations();
  });


// Repair command - fix failed MCPs interactively
program
  .command('repair')
  .description('Interactively configure failed MCPs')
  .option('--profile <name>', 'Profile to repair (default: all)')
  .action(async (options) => {
    try {
      // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE!
      const profileName = options.profile || program.getOptionValue('profile') || 'all';

      console.log(chalk.bold('\n🔧 MCP Repair Tool\n'));

    // Load failed MCPs from both sources
    const { getCacheDirectory } = await import('../utils/ncp-paths.js');
    const { CSVCache } = await import('../cache/csv-cache.js');
    const { MCPErrorParser } = await import('../utils/mcp-error-parser.js');
    const { ProfileManager } = await import('../profiles/profile-manager.js');
    const { MCPWrapper } = await import('../utils/mcp-wrapper.js');
    const { healthMonitor } = await import('../utils/health-monitor.js');
    const { readFileSync, existsSync } = await import('fs');
    const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
    const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');

    const cache = new CSVCache(getCacheDirectory(), profileName);
    await cache.initialize();

    // Load profile first to know which MCPs to check
    const profileManager = new ProfileManager();
    await profileManager.initialize();
    const profile = await profileManager.getProfile(profileName);

    if (!profile) {
      console.log(chalk.red(`❌ Profile '${profileName}' not found`));
      return;
    }

    // Merge failed MCPs from both sources
    const failedMCPs = new Map<string, {
      errorMessage: string;
      attemptCount: number;
      source: 'cache' | 'health' | 'both';
      lastAttempt: string;
    }>();

    // Add from CSV cache
    const cacheMetadata = (cache as any).metadata;
    if (cacheMetadata?.failedMCPs) {
      for (const [mcpName, failedInfo] of cacheMetadata.failedMCPs) {
        failedMCPs.set(mcpName, {
          errorMessage: failedInfo.errorMessage,
          attemptCount: failedInfo.attemptCount,
          source: 'cache',
          lastAttempt: failedInfo.lastAttempt
        });
      }
    }

    // Add from health monitor (unhealthy or disabled)
    const healthReport = healthMonitor.generateHealthReport();
    for (const health of healthReport.details) {
      if (health.status === 'unhealthy' || health.status === 'disabled') {
        // Only include if it's in the current profile
        if (profile.mcpServers[health.name]) {
          const existing = failedMCPs.get(health.name);
          if (existing) {
            // Already in cache, mark as both
            existing.source = 'both';
          } else {
            // Only in health monitor
            failedMCPs.set(health.name, {
              errorMessage: health.lastError || 'Unknown error',
              attemptCount: health.errorCount,
              source: 'health',
              lastAttempt: health.lastCheck
            });
          }
        }
      }
    }

    if (failedMCPs.size === 0) {
      console.log(chalk.green('✅ No failed MCPs! Everything is working.'));
      return;
    }

    console.log(chalk.yellow(`Found ${failedMCPs.size} failed MCPs\n`));
    console.log(chalk.dim('This tool will help you configure them interactively.\n'));

    const errorParser = new MCPErrorParser();
    const mcpWrapper = new MCPWrapper();
    const prompts = (await import('prompts')).default;

    let fixedCount = 0;
    let skippedCount = 0;
    let stillFailingCount = 0;

    // Iterate through failed MCPs
    for (const [mcpName, failedInfo] of failedMCPs) {
      console.log(chalk.cyan(`\n📦 ${mcpName}`));
      console.log(chalk.dim(`   Last error: ${failedInfo.errorMessage}`));
      console.log(chalk.dim(`   Failed ${failedInfo.attemptCount} time(s)`));

      // Show source of failure detection
      const sourceLabel = failedInfo.source === 'both'
        ? 'indexing & runtime'
        : failedInfo.source === 'cache'
          ? 'indexing'
          : 'runtime';
      console.log(chalk.dim(`   Detected during: ${sourceLabel}`));

      // Ask if user wants to fix this MCP
      const { shouldFix } = await prompts({
        type: 'confirm',
        name: 'shouldFix',
        message: `Try to fix ${mcpName}?`,
        initial: true
      });

      if (!shouldFix) {
        skippedCount++;
        continue;
      }

      // Get current MCP config first (needed for all tiers)
      const currentConfig = profile.mcpServers[mcpName];
      if (!currentConfig) {
        console.log(chalk.red(`   ❌ MCP not found in profile`));
        skippedCount++;
        continue;
      }

      // Skip HTTP/SSE MCPs (they don't have command/args to repair)
      if (currentConfig.url && !currentConfig.command) {
        console.log(chalk.yellow(`   ⚠️  Skipping HTTP/SSE MCP (remote connector)`));
        skippedCount++;
        continue;
      }

      // Two-tier configuration detection (same as ncp add):
      // Tier 1: Cached schema (from previous successful add or MCP protocol)
      // Tier 2: Error parsing (fallback)

      let detectedSchema: any = null;

      // Check for cached schema
      const schemaCache = new SchemaCache(getCacheDirectory());
      detectedSchema = schemaCache.get(mcpName);

      if (detectedSchema) {
        console.log(chalk.dim(`   ✓ Using cached configuration schema`));
      }

      // If we have a schema, use schema-based prompting
      if (detectedSchema) {
        const schemaReader = new ConfigSchemaReader();
        const configPrompter = new ConfigPrompter();

        if (schemaReader.hasRequiredConfig(detectedSchema)) {
          console.log(chalk.cyan(`\n   📋 Configuration required`));

          const promptedConfig = await configPrompter.promptForConfig(detectedSchema, mcpName);

          // Update config with prompted values
          const updatedConfig = {
            command: currentConfig.command,
            args: [...(currentConfig.args || []), ...(promptedConfig.arguments || [])],
            env: { ...(currentConfig.env || {}), ...(promptedConfig.environmentVariables || {}) }
          };

          // Save updated config
          await profileManager.addMCPToProfile(profileName, mcpName, updatedConfig);

          console.log(chalk.green(`\n   ✅ Configuration updated for ${mcpName}`));
          fixedCount++;
          continue;
        }
      }

      // Tier 3: Fallback to error parsing if no schema available
      const logPath = mcpWrapper.getLogFile(mcpName);
      let stderr = '';

      if (existsSync(logPath)) {
        const logContent = readFileSync(logPath, 'utf-8');
        // Extract stderr lines
        const stderrLines = logContent.split('\n').filter(line => line.includes('[STDERR]'));
        stderr = stderrLines.map(line => line.replace(/\[STDERR\]\s*/, '')).join('\n');
      } else {
        stderr = failedInfo.errorMessage;
      }

      // Parse errors to detect configuration needs
      const configNeeds = errorParser.parseError(mcpName, stderr, 1);

      if (configNeeds.length === 0) {
        console.log(chalk.yellow(`   ⚠️  Could not detect specific configuration needs`));
        console.log(chalk.dim(`   Check logs manually: ${logPath}`));
        skippedCount++;
        continue;
      }

      // Check if it's a missing package
      const packageMissing = configNeeds.find(n => n.type === 'package_missing');
      if (packageMissing) {
        console.log(chalk.red(`   ❌ Package not found on npm - cannot fix`));
        console.log(chalk.dim(`      ${packageMissing.extractedFrom}`));
        skippedCount++;
        continue;
      }

      console.log(chalk.yellow(`\n   Found ${configNeeds.length} configuration need(s):`));
      for (const need of configNeeds) {
        console.log(chalk.dim(`   • ${need.description}`));
      }

      // Collect new configuration from user
      const newEnv = { ...(currentConfig.env || {}) };
      const newArgs = [...(currentConfig.args || [])];

      for (const need of configNeeds) {
        if (need.type === 'api_key' || need.type === 'env_var') {
          const { value } = await prompts({
            type: need.sensitive ? 'password' : 'text',
            name: 'value',
            message: need.prompt,
            validate: (val: string) => val.length > 0 ? true : 'Value required'
          });

          if (!value) {
            console.log(chalk.yellow(`   Skipped ${mcpName}`));
            skippedCount++;
            continue;
          }

          newEnv[need.variable] = value;
        } else if (need.type === 'command_arg') {
          const { value } = await prompts({
            type: 'text',
            name: 'value',
            message: need.prompt,
            validate: (val: string) => val.length > 0 ? true : 'Value required'
          });

          if (!value) {
            console.log(chalk.yellow(`   Skipped ${mcpName}`));
            skippedCount++;
            continue;
          }

          newArgs.push(value);
        }
      }

      // Test MCP with new configuration
      console.log(chalk.dim(`\n   Testing ${mcpName} with new configuration...`));

      const testConfig = {
        name: mcpName,
        command: currentConfig.command,
        args: newArgs,
        env: newEnv
      };

      try {
        // Create wrapper command
        const wrappedCommand = mcpWrapper.createWrapper(
          testConfig.name,
          testConfig.command || '',  // Should never be undefined after HTTP/SSE check
          testConfig.args || []
        );

        // Test connection with 30 second timeout
        const transport = new StdioClientTransport({
          command: wrappedCommand.command,
          args: wrappedCommand.args,
          env: {
            ...process.env,
            ...(testConfig.env || {}),
            MCP_SILENT: 'true',
            QUIET: 'true'
          }
        });

        const client = new Client({
          name: 'ncp-repair-test',
          version: '1.0.0'
        }, {
          capabilities: {}
        });

        await client.connect(transport);

        // Try to list tools
        const result = await Promise.race([
          client.listTools(),
          new Promise<never>((_, reject) =>
            setTimeout(() => reject(new Error('Test timeout')), 30000)
          )
        ]);

        await client.close();

        console.log(chalk.green(`   ✅ Success! Found ${result.tools.length} tools`));

        // Update profile with new configuration
        profile.mcpServers[mcpName] = {
          command: testConfig.command,
          args: newArgs,
          env: newEnv
        };
        profile.metadata.modified = new Date().toISOString();

        await profileManager.saveProfile(profile);

        // Remove from both failure tracking systems
        if (cacheMetadata?.failedMCPs) {
          cacheMetadata.failedMCPs.delete(mcpName);
          (cache as any).saveMetadata();
        }

        // Clear from health monitor and mark as healthy
        await healthMonitor.enableMCP(mcpName);
        healthMonitor.markHealthy(mcpName);
        await healthMonitor.saveHealth();

        fixedCount++;
      } catch (error: any) {
        console.log(chalk.red(`   ❌ Still failing: ${error.message}`));
        stillFailingCount++;
      }
    }

    // Final report
    console.log(chalk.bold('\n📊 Repair Summary\n'));
    console.log(chalk.green(`✅ Fixed: ${fixedCount}`));
    console.log(chalk.yellow(`⏭️  Skipped: ${skippedCount}`));
    console.log(chalk.red(`❌ Still failing: ${stillFailingCount}`));

    if (fixedCount > 0) {
      console.log(chalk.dim('\n💡 Run "ncp find --force-retry" to re-index fixed MCPs'));
    }
    } catch (error: any) {
      console.error(chalk.red('\n❌ Repair command failed:'), error.message);
      console.error(chalk.dim(error.stack));
      process.exit(1);
    }
  });

// Find command (CLI-optimized version for fast discovery)
program
  .command('find [query]')
  .description('Find tools matching a query or list all tools')
  .option('--limit <number>', 'Maximum number of results (default: 5)')
  .option('--page <number>', 'Page number (default: 1)')
  .option('--depth <number>', 'Display depth: 0=overview, 1=tools, 2=details (default: 2)')
  .option('--confidence_threshold <number>', 'Minimum confidence level (0.0-1.0, default: 0.3). Examples: 0.1=show all, 0.5=strict, 0.7=very precise')
  .action(async (query, options) => {
    // Add newline after command before any output
    console.log();

    // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE!
    const profileName = program.getOptionValue('profile') || 'all';
    const forceRetry = program.getOptionValue('forceRetry') || false;

    // Use MCPServer for rich formatted output
    const { MCPServer } = await import('../server/mcp-server.js');
    const server = new MCPServer(profileName, true, forceRetry); // Enable progress + force retry flag

    // Setup graceful shutdown on Ctrl+C
    const gracefulShutdown = async () => {
      process.stdout.write('\n\n💾 Saving progress...');
      try {
        await server.cleanup();
        process.stdout.write('\r\u001B[K✅ Progress saved\n');
      } catch (error) {
        process.stdout.write('\r\u001B[K❌ Error saving progress\n');
      }
      process.exit(0);
    };

    process.on('SIGINT', gracefulShutdown);
    process.on('SIGTERM', gracefulShutdown);

    await server.initialize();

    // For CLI usage, wait for indexing to complete before searching
    await server.waitForInitialization();

    const limit = parseInt(options.limit || '5');
    const page = parseInt(options.page || '1');
    const depth = parseInt(options.depth || '2');
    const confidence_threshold = options.confidence_threshold ? parseFloat(options.confidence_threshold) : undefined;

    const result = await server.handleFind(
      { jsonrpc: '2.0', id: 'cli', method: 'tools/call' },
      { description: query || '', limit, page, depth, confidence_threshold }
    );

    const formattedOutput = formatFindOutput(result.result.content[0].text);
    console.log(formattedOutput);
    await server.cleanup();
  });

// Analytics command group
const analyticsCmd = program
  .command('analytics')
  .description('View NCP usage analytics and performance metrics');

analyticsCmd
  .command('dashboard')
  .description('Show comprehensive analytics dashboard')
  .option('--period <days>', 'Show data for last N days (e.g., --period 7)')
  .option('--from <date>', 'Start date (YYYY-MM-DD format)')
  .option('--to <date>', 'End date (YYYY-MM-DD format)')
  .option('--today', 'Show only today\'s data')
  .option('--visual', 'Enhanced visual dashboard with charts and graphs')
  .action(async (options) => {
    const { NCPLogParser } = await import('../analytics/log-parser.js');

    console.log(chalk.dim('📊 Analyzing NCP usage data...'));

    const parser = new NCPLogParser();

    // Parse time range options
    const parseOptions: any = {};

    if (options.today) {
      parseOptions.today = true;
    } else if (options.period) {
      parseOptions.period = parseInt(options.period);
    } else if (options.from || options.to) {
      if (options.from) parseOptions.from = new Date(options.from);
      if (options.to) parseOptions.to = new Date(options.to);
    }

    const report = await parser.parseAllLogs(parseOptions);

    if (report.totalSessions === 0) {
      console.log(chalk.yellow('📊 No analytics data available for the specified time range'));
      console.log(chalk.dim('💡 Try a different time range or check if MCPs have been used through NCP'));
      return;
    }

    if (options.visual) {
      const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js');
      const dashboard = await VisualAnalyticsFormatter.formatVisualDashboard(report);
      console.log(dashboard);
    } else {
      const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js');
      const dashboard = AnalyticsFormatter.formatDashboard(report);
      console.log(dashboard);
    }
  });

analyticsCmd
  .command('performance')
  .description('Show performance-focused analytics')
  .option('--period <days>', 'Show data for last N days (e.g., --period 7)')
  .option('--from <date>', 'Start date (YYYY-MM-DD format)')
  .option('--to <date>', 'End date (YYYY-MM-DD format)')
  .option('--today', 'Show only today\'s data')
  .option('--visual', 'Enhanced visual performance report with gauges and charts')
  .action(async (options) => {
    const { NCPLogParser } = await import('../analytics/log-parser.js');

    console.log(chalk.dim('⚡ Analyzing performance metrics...'));

    const parser = new NCPLogParser();

    // Parse time range options
    const parseOptions: any = {};
    if (options.today) {
      parseOptions.today = true;
    } else if (options.period) {
      parseOptions.period = parseInt(options.period);
    } else if (options.from || options.to) {
      if (options.from) parseOptions.from = new Date(options.from);
      if (options.to) parseOptions.to = new Date(options.to);
    }

    const report = await parser.parseAllLogs(parseOptions);

    if (report.totalSessions === 0) {
      console.log(chalk.yellow('📊 No performance data available for the specified time range'));
      return;
    }

    if (options.visual) {
      const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js');
      const performance = await VisualAnalyticsFormatter.formatVisualPerformance(report);
      console.log(performance);
    } else {
      const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js');
      const performance = AnalyticsFormatter.formatPerformanceReport(report);
      console.log(performance);
    }
  });

analyticsCmd
  .command('visual')
  .description('Show enhanced visual analytics with charts and graphs')
  .option('--period <days>', 'Show data for last N days (e.g., --period 7)')
  .option('--from <date>', 'Start date (YYYY-MM-DD format)')
  .option('--to <date>', 'End date (YYYY-MM-DD format)')
  .option('--today', 'Show only today\'s data')
  .action(async (options) => {
    const { NCPLogParser } = await import('../analytics/log-parser.js');
    const { VisualAnalyticsFormatter } = await import('../analytics/visual-formatter.js');

    console.log(chalk.dim('🎨 Generating visual analytics...'));

    const parser = new NCPLogParser();

    // Parse time range options
    const parseOptions: any = {};
    if (options.today) {
      parseOptions.today = true;
    } else if (options.period) {
      parseOptions.period = parseInt(options.period);
    } else if (options.from || options.to) {
      if (options.from) parseOptions.from = new Date(options.from);
      if (options.to) parseOptions.to = new Date(options.to);
    }

    const report = await parser.parseAllLogs(parseOptions);

    if (report.totalSessions === 0) {
      console.log(chalk.yellow('📊 No analytics data available for the specified time range'));
      console.log(chalk.dim('💡 Try a different time range or check if MCPs have been used through NCP'));
      return;
    }

    const dashboard = await VisualAnalyticsFormatter.formatVisualDashboard(report);
    console.log(dashboard);
  });

analyticsCmd
  .command('export')
  .description('Export analytics data to CSV')
  .option('--output <file>', 'Output file (default: ncp-analytics.csv)')
  .option('--period <days>', 'Export data for last N days (e.g., --period 7)')
  .option('--from <date>', 'Start date (YYYY-MM-DD format)')
  .option('--to <date>', 'End date (YYYY-MM-DD format)')
  .option('--today', 'Export only today\'s data')
  .action(async (options) => {
    const { NCPLogParser } = await import('../analytics/log-parser.js');
    const { AnalyticsFormatter } = await import('../analytics/analytics-formatter.js');
    const { writeFileSync } = await import('fs');

    console.log(chalk.dim('📊 Generating analytics export...'));

    const parser = new NCPLogParser();

    // Parse time range options
    const parseOptions: any = {};
    if (options.today) {
      parseOptions.today = true;
    } else if (options.period) {
      parseOptions.period = parseInt(options.period);
    } else if (options.from || options.to) {
      if (options.from) parseOptions.from = new Date(options.from);
      if (options.to) parseOptions.to = new Date(options.to);
    }

    const report = await parser.parseAllLogs(parseOptions);

    if (report.totalSessions === 0) {
      console.log(chalk.yellow('📊 No data to export for the specified time range'));
      return;
    }

    const csv = AnalyticsFormatter.formatCSV(report);
    const filename = options.output || 'ncp-analytics.csv';

    writeFileSync(filename, csv, 'utf-8');
    console.log(chalk.green(`✅ Analytics exported to ${filename}`));
    console.log(chalk.dim(`📊 Exported ${report.totalSessions} sessions across ${report.uniqueMCPs} MCPs`));
  });

// Run command (existing functionality)
program
  .command('run <tool>')

  .description('Run a specific tool')
  .option('--params <json>', 'Tool parameters as JSON string (optional - will prompt interactively if not provided)')
  .option('--no-prompt', 'Skip interactive prompting for missing parameters')
  .option('--output-format <format>', 'Output format: auto (smart rendering), json (raw JSON)', 'auto')
  .option('-y, --yes', 'Automatically answer yes to prompts (e.g., open media files)')
  .configureHelp({
    formatHelp: (cmd, helper) => {
      const indent = '  ';
      let output = '\n';

      // Header first - context before syntax
      output += chalk.bold.white('NCP Run Command') + ' - ' + chalk.cyan('Direct MCP Tool Execution') + '\n\n';
      output += chalk.dim('Execute MCP tools with intelligent parameter prompting and rich media support.') + '\n';
      output += chalk.dim('Automatically handles parameter collection, validation, and response formatting.') + '\n\n';

      // Then usage
      output += chalk.bold.white('Usage:') + ' ' + helper.commandUsage(cmd) + '\n\n';

      const visibleOptions = helper.visibleOptions(cmd);
      if (visibleOptions.length) {
        output += chalk.bold.white('Options:') + '\n';
        visibleOptions.forEach(option => {
          const flags = option.flags;
          const description = helper.optionDescription(option);
          const paddingNeeded = Math.max(0, 42 - flags.length);
          const padding = ' '.repeat(paddingNeeded);
          output += indent + chalk.cyan(flags) + padding + ' ' + chalk.white(description) + '\n';
        });
        output += '\n';
      }

      // Examples section
      output += chalk.bold.white('Examples:') + '\n';
      output += chalk.dim('  Basic execution:') + '\n';
      output += indent + chalk.yellow('ncp run memory:create_entities') + chalk.gray('  # Interactive parameter prompting') + '\n';
      output += indent + chalk.yellow('ncp run memory:create_entities --params \'{"entities":["item1"]}\'') + '\n\n';

      output += chalk.dim('  Output control:') + '\n';
      output += indent + chalk.yellow('ncp run tool --output-format json') + chalk.gray('  # Raw JSON output') + '\n';
      output += indent + chalk.yellow('ncp run tool -y') + chalk.gray('  # Auto-open media files') + '\n\n';

      output += chalk.dim('  Non-interactive:') + '\n';
      output += indent + chalk.yellow('ncp run tool --no-prompt --params \'{}\'') + chalk.gray('  # Scripting/automation') + '\n\n';

      // Media support note
      output += chalk.bold.white('Media Support:') + '\n';
      output += chalk.dim('  • Images and audio are displayed with metadata') + '\n';
      output += chalk.dim('  • Use') + chalk.cyan(' -y ') + chalk.dim('to auto-open media in default applications') + '\n';
      output += chalk.dim('  • Without') + chalk.cyan(' -y') + chalk.dim(', prompts before opening media files') + '\n\n';

      return output;
    }
  })
  .action(async (tool, options) => {
    // ⚠️ CRITICAL: Default MUST be 'all' - DO NOT CHANGE!
    const profileName = program.getOptionValue('profile') || 'all';

    const { NCPOrchestrator } = await import('../orchestrator/ncp-orchestrator.js');
    const orchestrator = new NCPOrchestrator(profileName, false); // Silent indexing for run command

    await orchestrator.initialize();

    // If tool doesn't contain a colon, try to find matching tools first
    if (!tool.includes(':')) {
      console.log(chalk.dim(`🔍 Searching for tools matching "${tool}"...`));

      try {
        const matchingTools = await orchestrator.find(tool, 5, false);

        if (matchingTools.length === 0) {
          console.log('\n' + OutputFormatter.error(`No tools found matching "${tool}"`));
          console.log(chalk.yellow('💡 Try \'ncp find\' to explore all available tools'));
          await orchestrator.cleanup();
          return;
        }

        if (matchingTools.length === 1) {
          // Only one match, use it automatically
          const matchedTool = matchingTools[0];
          tool = matchedTool.toolName;
          console.log(chalk.green(`✅ Found exact match: ${tool}`));
        } else {
          // Multiple matches, show them and ask user to be more specific
          console.log(chalk.yellow(`Found ${matchingTools.length} matching tools:`));
          matchingTools.forEach((match, index) => {
            const confidence = Math.round(match.confidence * 100);
            console.log(`  ${index + 1}. ${chalk.cyan(match.toolName)} (${confidence}% match)`);
            if (match.description) {
              console.log(`     ${chalk.dim(match.description)}`);
            }
          });
          console.log(chalk.yellow('\n💡 Please specify the exact tool name from the list above'));
          console.log(chalk.yellow(`💡 Example: ncp run ${matchingTools[0].toolName}`));
          await orchestrator.cleanup();
          return;
        }
      } catch (error: any) {
        console.log('\n' + OutputFormatter.error(`Error searching for tools: ${error.message}`));
        await orchestrator.cleanup();
        return;
      }
    }

    // Check if parameters are provided
    let parameters = {};
    if (options.params) {
      parameters = JSON.parse(options.params);
    } else {
      // Get tool schema and parameters
      const toolParams = orchestrator.getToolParameters(tool);

      if (toolParams && toolParams.length > 0) {
        const requiredParams = toolParams.filter(p => p.required);

        if (requiredParams.length > 0 && options.prompt !== false) {
          // Interactive prompting for parameters (default behavior)
          const { ParameterPrompter } = await import('../utils/parameter-prompter.js');
          const { ParameterPredictor } = await import('../server/mcp-server.js');

          const prompter = new ParameterPrompter();
          const predictor = new ParameterPredictor();
          const toolContext = orchestrator.getToolContext(tool);

          try {
            parameters = await prompter.promptForParameters(tool, toolParams, predictor, toolContext);
            prompter.close();
          } catch (error) {
            prompter.close();
            console.log('\n' + OutputFormatter.error('Error during parameter input'));
            await orchestrator.cleanup();
            return;
          }
        } else if (requiredParams.length > 0 && options.prompt === false) {
          console.log('\n' + OutputFormatter.error('This tool requires parameters'));
          console.log(chalk.yellow(`💡 Use: ncp run ${tool} --params '{"param": "value"}'`));
          console.log(chalk.yellow(`💡 Or use: ncp find "${tool}" --depth 2 to see required parameters`));
          console.log(chalk.yellow(`💡 Or remove --no-prompt to use interactive prompting`));
          await orchestrator.cleanup();
          return;
        }
      }
    }

    console.log(OutputFormatter.running(tool) + '\n');

    const result = await orchestrator.run(tool, parameters);

    if (result.success) {
      // Check if the content indicates an actual error despite "success" status
      const contentStr = JSON.stringify(result.content);
      const isActualError = contentStr.includes('"type":"text"') &&
                           (contentStr.includes('Error:') || contentStr.includes('not found') || contentStr.includes('Unknown tool'));

      if (isActualError) {
        const errorText = result.content?.[0]?.text || 'Unknown error occurred';
        let suggestions: string[] = [];

        if (errorText.includes('not configured') || errorText.includes('Unknown tool')) {
          // Extract the query from the tool name for vector search
          const [mcpName, toolName] = tool.split(':');

          // Try multiple search strategies to find the best matches
          let similarTools: any[] = [];

          try {
            // Strategy 1: Search with both MCP context and tool name for better domain matching
            if (toolName && mcpName) {
              const contextualQuery = `${mcpName} ${toolName}`;
              similarTools = await orchestrator.find(contextualQuery, 3, false);
            }

            // Strategy 2: If no results, try just the tool name
            if (similarTools.length === 0 && toolName) {
              similarTools = await orchestrator.find(toolName, 3, false);
            }

            // Strategy 3: If still no results, try just the MCP name (domain search)
            if (similarTools.length === 0) {
              similarTools = await orchestrator.find(mcpName, 3, false);
            }

            if (similarTools.length > 0) {
              suggestions.push('💡 Did you mean:');
              similarTools.forEach(similar => {
                const confidence = Math.round(similar.confidence * 100);
                suggestions.push(`  • ${similar.toolName} (${confidence}% match)`);
              });
            }
          } catch (error: any) {
            // Fallback to basic suggestions if vector search fails
            suggestions = ['Try \'ncp find <keyword>\' to discover similar tools'];
          }
        }

        const context = ErrorHandler.createContext('mcp', 'run', tool, suggestions);
        const errorResult = ErrorHandler.handle(errorText, context);
        console.log('\n' + ErrorHandler.formatForConsole(errorResult));
      } else {
        console.log(OutputFormatter.success('Tool execution completed'));

        // Respect user's output format choice
        if (options.outputFormat === 'json') {
          // Raw JSON output - test different formatters to pick the best
          const { formatJson } = await import('../utils/highlighting.js');
          console.log(formatJson(result.content, 'cli-highlight')); // Let's test this one
        } else {
          // Smart response formatting (default)
          const { ResponseFormatter } = await import('../utils/response-formatter.js');

          // Check if this is text content that should be formatted naturally
          const isTextResponse = Array.isArray(result.content) &&
                                result.content.every((item: any) => item?.type === 'text');

          if (isTextResponse || (result.content?.[0]?.type === 'text' && result.content.length === 1)) {
            // Format as natural text with proper newlines
            console.log(ResponseFormatter.format(result.content, true, options.yes));
          } else if (ResponseFormatter.isPureData(result.content)) {
            // Pure data - use JSON formatting
            const { formatJson } = await import('../utils/highlighting.js');
            console.log(formatJson(result.content, 'cli-highlight'));
          } else {
            // Mixed content or unknown - use smart formatter
            console.log(ResponseFormatter.format(result.content, true, options.yes));
          }
        }
      }
    } else {
      // Check if this is a tool not found error and provide "did you mean" suggestions
      const errorMessage = result.error || 'Unknown error occurred';
      let suggestions: string[] = [];

      if (errorMessage.includes('not configured') || errorMessage.includes('Unknown tool')) {
        // Extract the query from the tool name for vector search
        const [mcpName, toolName] = tool.split(':');

        // Try multiple search strategies to find the best matches
        let similarTools: any[] = [];

        try {
          // Strategy 1: Search with both MCP context and tool name for better domain matching
          if (toolName && mcpName) {
            const contextualQuery = `${mcpName} ${toolName}`;
            similarTools = await orchestrator.find(contextualQuery, 3, false);
          }

          // Strategy 2: If no results, try just the tool name
          if (similarTools.length === 0 && toolName) {
            similarTools = await orchestrator.find(toolName, 3, false);
          }

          // Strategy 3: If still no results, try just the MCP name (domain search)
          if (similarTools.length === 0) {
            similarTools = await orchestrator.find(mcpName, 3, false);
          }
          if (similarTools.length > 0) {
            suggestions.push('💡 Did you mean:');
            similarTools.forEach(similar => {
              const confidence = Math.round(similar.confidence * 100);
              suggestions.push(`  • ${similar.toolName} (${confidence}% match)`);
            });
          }
        } catch (error: any) {
          // Fallback to basic suggestions if vector search fails
          suggestions = ['Try \'ncp find <keyword>\' to discover similar tools'];
        }
      }

      const context = ErrorHandler.createContext('mcp', 'run', tool, suggestions);
      const errorResult = ErrorHandler.handle(errorMessage, context);
      console.log('\n' + ErrorHandler.formatForConsole(errorResult));
    }

    await orchestrator.cleanup();
  });

// Auth command
program
  .command('auth <mcp>')
  .description('Authenticate an MCP server using OAuth Device Flow')
  .option('--profile <name>', 'Profile to use (default: all)')
  .configureHelp({
    formatHelp: () => {
      let output = '\n';
      output += chalk.bold.white('NCP Auth Command') + ' - ' + chalk.cyan('OAuth Authentication') + '\n\n';
      output += chalk.dim('Authenticate an MCP server that requires OAuth 2.0 Device Flow authentication.') + '\n';
      output += chalk.dim('Tokens are securely stored and automatically refreshed.') + '\n\n';
      output += chalk.bold.white('Usage:') + '\n';
      output += '  ' + chalk.yellow('ncp auth <mcp>') + '          # Authenticate an MCP server\n';
      output += '  ' + chalk.yellow('ncp auth <mcp> --profile <name>') + '   # Authenticate for specific profile\n\n';
      output += chalk.bold.white('Examples:') + '\n';
      output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp auth github') + '\n';
      output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp auth my-api --profile production') + '\n\n';
      return output;
    }
  })
  .action(async (mcpName, options) => {
    try {
      const profileName = options.profile || 'all';

      // Load profile
      const manager = new ProfileManager();
      await manager.initialize();

      const profile = await manager.getProfile(profileName);
      if (!profile) {
        console.error(chalk.red(`❌ Profile '${profileName}' not found`));
        process.exit(1);
      }

      // Check if MCP exists in profile
      const mcpConfig = profile.mcpServers[mcpName];
      if (!mcpConfig) {
        console.error(chalk.red(`❌ MCP '${mcpName}' not found in profile '${profileName}'`));

        // Suggest similar MCPs
        const availableMCPs = Object.keys(profile.mcpServers);
        if (availableMCPs.length > 0) {
          const suggestions = findSimilarNames(mcpName, availableMCPs);
          if (suggestions.length > 0) {
            console.log(chalk.yellow('\n💡 Did you mean:'));
            suggestions.forEach((suggestion, index) => {
              console.log(`  ${index + 1}. ${chalk.cyan(suggestion)}`);
            });
          }
        }
        process.exit(1);
      }

      // Check if MCP has OAuth configuration
      if (!mcpConfig.auth || mcpConfig.auth.type !== 'oauth' || !mcpConfig.auth.oauth) {
        console.error(chalk.red(`❌ MCP '${mcpName}' does not have OAuth configuration`));
        console.log(chalk.yellow('\n💡 To add OAuth configuration, edit your profile configuration file:'));
        console.log(chalk.dim('   Add "auth": { "type": "oauth", "oauth": { ... } } to the MCP configuration'));
        process.exit(1);
      }

      // Perform OAuth Device Flow
      const { DeviceFlowAuthenticator } = await import('../auth/oauth-device-flow.js');
      const { getTokenStore } = await import('../auth/token-store.js');

      const authenticator = new DeviceFlowAuthenticator(mcpConfig.auth.oauth);
      const tokenStore = getTokenStore();

      console.log(chalk.blue(`🔐 Starting OAuth Device Flow for '${mcpName}'...\n`));

      try {
        const tokenResponse = await authenticator.authenticate();

        // Store the token
        await tokenStore.storeToken(mcpName, tokenResponse);

        console.log(chalk.green(`✅ Successfully authenticated '${mcpName}'!`));
        console.log(chalk.dim(`   Token expires: ${new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()}`));
        console.log(chalk.dim(`   Token stored securely in: ~/.ncp/tokens/${mcpName}.token`));
      } catch (error: any) {
        console.error(chalk.red(`❌ Authentication failed: ${error.message}`));
        process.exit(1);
      }
    } catch (error: any) {
      console.error(chalk.red(`❌ Error: ${error.message}`));
      process.exit(1);
    }
  });

// Update command
program
  .command('update')
  .description('Update NCP to the latest version')
  .option('--check', 'Check for updates without installing')
  .configureHelp({
    formatHelp: () => {
      let output = '\n';
      output += chalk.bold.white('NCP Update Command') + ' - ' + chalk.cyan('Version Management') + '\n\n';
      output += chalk.dim('Keep NCP up to date with the latest features and bug fixes.') + '\n\n';
      output += chalk.bold.white('Usage:') + '\n';
      output += '  ' + chalk.yellow('ncp update') + '          # Update to latest version\n';
      output += '  ' + chalk.yellow('ncp update --check') + '   # Check for updates without installing\n\n';
      output += chalk.bold.white('Examples:') + '\n';
      output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp update --check') + '\n';
      output += '  ' + chalk.gray('$ ') + chalk.yellow('ncp update') + '\n\n';
      return output;
    }
  })
  .action(async (options) => {
    try {
      const updateChecker = new UpdateChecker();

      if (options.check) {
        // Check for updates only
        console.log(chalk.blue('🔍 Checking for updates...'));
        const result = await updateChecker.checkForUpdates(true);

        if (result.hasUpdate) {
          console.log(chalk.yellow('📦 Update Available!'));
          console.log(chalk.dim(`   Current: ${result.currentVersion}`));
          console.log(chalk.green(`   Latest:  ${result.latestVersion}`));
          console.log();
          console.log(chalk.cyan('   Run: ncp update'));
        } else {
          console.log(chalk.green('✅ You are using the latest version!'));
          console.log(chalk.dim(`   Version: ${result.currentVersion}`));
        }
      } else {
        // Perform update
        const result = await updateChecker.checkForUpdates(true);

        if (result.hasUpdate) {
          console.log(chalk.yellow('📦 Update Available!'));
          console.log(chalk.dim(`   Current: ${result.currentVersion}`));
          console.log(chalk.green(`   Latest:  ${result.latestVersion}`));
          console.log();

          const success = await updateChecker.performUpdate();
          if (!success) {
            process.exit(1);
          }
        } else {
          console.log(chalk.green('✅ You are already using the latest version!'));
          console.log(chalk.dim(`   Version: ${result.currentVersion}`));
        }
      }
    } catch (error) {
      console.error(chalk.red('❌ Failed to check for updates:'), error);
      process.exit(1);
    }
  });

// Check for updates on CLI startup (non-intrusive)
// Temporarily disabled - causing hangs in some environments
// TODO: Re-enable with proper timeout handling
// (async () => {
//   try {
//     const updateChecker = new UpdateChecker();
//     await updateChecker.showUpdateNotification();
//   } catch {
//     // Silently fail - don't interrupt normal CLI usage
//   }
// })();

program.parse();
}
```
Page 9/9FirstPrevNextLast