#
tokens: 47908/50000 14/189 files (page 5/12)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 12. Use http://codebase.md/portel-dev/ncp?lines=true&page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/src/utils/highlighting.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import chalk from 'chalk';
  2 | import { highlight as cliHighlight } from 'cli-highlight';
  3 | import prettyjson from 'prettyjson';
  4 | import colorizer from 'json-colorizer';
  5 | 
  6 | /**
  7 |  * Comprehensive color highlighting utilities for NCP
  8 |  * Handles JSON-RPC, CLI output, tool responses, and interactive elements
  9 |  */
 10 | export class HighlightingUtils {
 11 |   /**
 12 |    * Highlight JSON with beautiful syntax colors
 13 |    * Uses multiple highlighting engines with fallbacks
 14 |    */
 15 |   static formatJson(json: any, style: 'cli-highlight' | 'prettyjson' | 'colorizer' | 'auto' = 'auto'): string {
 16 |     const jsonString = JSON.stringify(json, null, 2);
 17 | 
 18 |     try {
 19 |       if (style === 'prettyjson') {
 20 |         return prettyjson.render(json, {
 21 |           keysColor: 'blue',
 22 |           dashColor: 'grey',
 23 |           stringColor: 'green',
 24 |           numberColor: 'yellow'
 25 |         });
 26 |       }
 27 | 
 28 |       if (style === 'colorizer') {
 29 |         return (colorizer as any)(jsonString, {
 30 |           pretty: true,
 31 |           colors: {
 32 |             BRACE: 'gray',
 33 |             BRACKET: 'gray',
 34 |             COLON: 'gray',
 35 |             COMMA: 'gray',
 36 |             STRING_KEY: 'blue',
 37 |             STRING_LITERAL: 'green',
 38 |             NUMBER_LITERAL: 'yellow',
 39 |             BOOLEAN_LITERAL: 'cyan',
 40 |             NULL_LITERAL: 'red'
 41 |           }
 42 |         });
 43 |       }
 44 | 
 45 |       if (style === 'cli-highlight' || style === 'auto') {
 46 |         return cliHighlight(jsonString, {
 47 |           language: 'json',
 48 |           theme: {
 49 |             keyword: chalk.blue,
 50 |             string: chalk.green,
 51 |             number: chalk.yellow,
 52 |             literal: chalk.cyan
 53 |           }
 54 |         });
 55 |       }
 56 | 
 57 |     } catch (error) {
 58 |       // Try fallback methods
 59 |       if (style !== 'colorizer') {
 60 |         try {
 61 |           return (colorizer as any)(jsonString, { pretty: true });
 62 |         } catch {}
 63 |       }
 64 | 
 65 |       if (style !== 'prettyjson') {
 66 |         try {
 67 |           return prettyjson.render(json);
 68 |         } catch {}
 69 |       }
 70 | 
 71 |       // Final fallback - basic JSON with manual coloring
 72 |       return HighlightingUtils.manualJsonHighlight(jsonString);
 73 |     }
 74 | 
 75 |     return jsonString;
 76 |   }
 77 | 
 78 |   /**
 79 |    * Manual JSON highlighting as final fallback
 80 |    */
 81 |   private static manualJsonHighlight(jsonString: string): string {
 82 |     return jsonString
 83 |       .replace(/"([^"]+)":/g, chalk.blue('"$1"') + chalk.gray(':'))
 84 |       .replace(/: "([^"]+)"/g, ': ' + chalk.green('"$1"'))
 85 |       .replace(/: (\d+)/g, ': ' + chalk.yellow('$1'))
 86 |       .replace(/: (true|false|null)/g, ': ' + chalk.cyan('$1'))
 87 |       .replace(/[{}]/g, chalk.gray('$&'))
 88 |       .replace(/[\[\]]/g, chalk.gray('$&'))
 89 |       .replace(/,/g, chalk.gray(','));
 90 |   }
 91 | 
 92 |   /**
 93 |    * Create a bordered JSON display with syntax highlighting
 94 |    */
 95 |   static createJsonBox(json: any, title?: string): string {
 96 |     const highlighted = this.formatJson(json);
 97 |     const lines = highlighted.split('\n');
 98 | 
 99 |     const maxLength = Math.max(...lines.map(line => this.stripAnsi(line).length));
100 |     const boxWidth = Math.max(maxLength + 4, 45);
101 | 
102 |     let output = '';
103 | 
104 |     if (title) {
105 |       output += chalk.blue(`📋 ${title}:\n`);
106 |     }
107 | 
108 |     output += chalk.gray('┌' + '─'.repeat(boxWidth - 2) + '┐\n');
109 | 
110 |     lines.forEach(line => {
111 |       const stripped = this.stripAnsi(line);
112 |       const padding = ' '.repeat(boxWidth - stripped.length - 4);
113 |       output += chalk.gray(`│ `) + line + padding + chalk.gray(` │\n`);
114 |     });
115 | 
116 |     output += chalk.gray('└' + '─'.repeat(boxWidth - 2) + '┘');
117 | 
118 |     return output;
119 |   }
120 | 
121 |   /**
122 |    * Highlight JSON-RPC responses with beautiful formatting
123 |    */
124 |   static formatJsonRpc(response: any): string {
125 |     if (response.error) {
126 |       return this.createJsonBox(response, chalk.red('JSON-RPC Error'));
127 |     }
128 | 
129 |     if (response.result) {
130 |       return this.createJsonBox(response, chalk.green('JSON-RPC Response'));
131 |     }
132 | 
133 |     return this.createJsonBox(response, 'JSON-RPC');
134 |   }
135 | 
136 |   /**
137 |    * Format tool discovery results with confidence-based colors
138 |    */
139 |   static formatToolResult(tool: any, index: number): string {
140 |     const confidence = parseFloat(tool.confidence || '0');
141 |     let confidenceColor = chalk.red;
142 | 
143 |     if (confidence >= 70) confidenceColor = chalk.green;
144 |     else if (confidence >= 50) confidenceColor = chalk.yellow;
145 |     else if (confidence >= 30) confidenceColor = chalk.hex('#FFA500');
146 | 
147 |     let result = chalk.cyan(`${index}. `) +
148 |                   chalk.bold(tool.name) +
149 |                   chalk.gray(` (${tool.source || 'unknown'})\n`) +
150 |                   `   Confidence: ` + confidenceColor(`${confidence}%\n`) +
151 |                   `   Command: ` + chalk.dim(tool.command || 'unknown');
152 | 
153 |     if (tool.description) {
154 |       result += `\n   ` + chalk.gray(tool.description.substring(0, 100) + '...');
155 |     }
156 | 
157 |     return result;
158 |   }
159 | 
160 |   /**
161 |    * Format profile tree with beautiful colors
162 |    */
163 |   static formatProfileTree(profileName: string, mcps: any[]): string {
164 |     let output = chalk.blue(`📦 ${profileName}\n`);
165 | 
166 |     if (mcps.length === 0) {
167 |       output += chalk.gray('  └── (empty)');
168 |       return output;
169 |     }
170 | 
171 |     mcps.forEach((mcp, index) => {
172 |       const isLast = index === mcps.length - 1;
173 |       const connector = isLast ? '└──' : '├──';
174 | 
175 |       output += chalk.gray(`  ${connector} `) + chalk.cyan(mcp.name) + '\n';
176 | 
177 |       if (mcp.command) {
178 |         const subConnector = isLast ? '    ' : '  │ ';
179 |         output += chalk.gray(subConnector + '└── ') + chalk.dim(mcp.command);
180 |         if (mcp.args && mcp.args.length > 0) {
181 |           output += chalk.dim(' ' + mcp.args.join(' '));
182 |         }
183 |         if (index < mcps.length - 1) output += '\n';
184 |       }
185 |     });
186 | 
187 |     return output;
188 |   }
189 | 
190 |   /**
191 |    * Format status messages with appropriate colors
192 |    */
193 |   static formatStatus(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): string {
194 |     const icons = {
195 |       success: '✅',
196 |       error: '❌',
197 |       warning: '⚠️',
198 |       info: 'ℹ️'
199 |     };
200 | 
201 |     const colors = {
202 |       success: chalk.green,
203 |       error: chalk.red,
204 |       warning: chalk.yellow,
205 |       info: chalk.blue
206 |     };
207 | 
208 |     return colors[type](`${icons[type]} ${message}`);
209 |   }
210 | 
211 |   /**
212 |    * Create animated progress indicator
213 |    */
214 |   static createProgressBar(current: number, total: number, width: number = 30): string {
215 |     const percentage = Math.round((current / total) * 100);
216 |     const filled = Math.round((current / total) * width);
217 |     const empty = width - filled;
218 | 
219 |     const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
220 | 
221 |     return `[${bar}] ${chalk.cyan(percentage + '%')} (${current}/${total})`;
222 |   }
223 | 
224 |   /**
225 |    * Format code blocks with syntax highlighting
226 |    */
227 |   static formatCode(code: string, language?: string): string {
228 |     try {
229 |       return cliHighlight(code, { language: language || 'javascript', theme: 'default' });
230 |     } catch (error) {
231 |       return code;
232 |     }
233 |   }
234 | 
235 |   /**
236 |    * Format markdown with basic styling
237 |    */
238 |   static formatMarkdown(markdown: string): string {
239 |     return markdown
240 |       .replace(/^# (.*)/gm, chalk.bold.blue('# $1'))
241 |       .replace(/^## (.*)/gm, chalk.bold.cyan('## $1'))
242 |       .replace(/^### (.*)/gm, chalk.bold.yellow('### $1'))
243 |       .replace(/\*\*(.*?)\*\*/g, chalk.bold('$1'))
244 |       .replace(/\*(.*?)\*/g, chalk.italic('$1'))
245 |       .replace(/`(.*?)`/g, chalk.gray.bgBlack(' $1 '));
246 |   }
247 | 
248 |   /**
249 |    * Highlight configuration values
250 |    */
251 |   static formatConfigValue(key: string, value: any): string {
252 |     let formattedValue = '';
253 | 
254 |     if (typeof value === 'string') {
255 |       formattedValue = chalk.green(`"${value}"`);
256 |     } else if (typeof value === 'number') {
257 |       formattedValue = chalk.yellow(value.toString());
258 |     } else if (typeof value === 'boolean') {
259 |       formattedValue = value ? chalk.green('true') : chalk.red('false');
260 |     } else if (Array.isArray(value)) {
261 |       formattedValue = chalk.magenta(`[${value.length} items]`);
262 |     } else if (typeof value === 'object' && value !== null) {
263 |       formattedValue = chalk.magenta(`{object}`);
264 |     } else {
265 |       formattedValue = chalk.gray('null');
266 |     }
267 | 
268 |     return chalk.cyan(key) + chalk.white(': ') + formattedValue;
269 |   }
270 | 
271 |   /**
272 |    * Create a separator line
273 |    */
274 |   static createSeparator(char: string = '─', length: number = 50): string {
275 |     return chalk.gray(char.repeat(length));
276 |   }
277 | 
278 |   /**
279 |    * Format table-like data
280 |    */
281 |   static formatTable(headers: string[], rows: string[][]): string {
282 |     const columnWidths = headers.map((header, i) =>
283 |       Math.max(header.length, ...rows.map(row => (row[i] || '').length))
284 |     );
285 | 
286 |     let output = '';
287 | 
288 |     // Header
289 |     output += headers.map((header, i) =>
290 |       chalk.bold.blue(header.padEnd(columnWidths[i]))
291 |     ).join(' │ ') + '\n';
292 | 
293 |     // Separator
294 |     output += columnWidths.map(width =>
295 |       chalk.gray('─'.repeat(width))
296 |     ).join('─┼─') + '\n';
297 | 
298 |     // Rows
299 |     rows.forEach(row => {
300 |       output += row.map((cell, i) =>
301 |         (cell || '').padEnd(columnWidths[i])
302 |       ).join(' │ ') + '\n';
303 |     });
304 | 
305 |     return output;
306 |   }
307 | 
308 |   /**
309 |    * Strip ANSI escape codes from string (for length calculations)
310 |    */
311 |   private static stripAnsi(str: string): string {
312 |     return str.replace(/\x1b\[[0-9;]*m/g, '');
313 |   }
314 | }
315 | 
316 | /**
317 |  * Convenience exports for common highlighting patterns
318 |  */
319 | export const highlightUtils = HighlightingUtils;
320 | export const formatJson = HighlightingUtils.formatJson;
321 | export const formatJsonRpc = HighlightingUtils.formatJsonRpc;
322 | export const formatStatus = HighlightingUtils.formatStatus;
323 | export const createJsonBox = HighlightingUtils.createJsonBox;
```

--------------------------------------------------------------------------------
/src/discovery/search-enhancer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Search Enhancement System
  3 |  * Maps action words to semantic equivalents and categorizes terms for intelligent ranking
  4 |  */
  5 | 
  6 | interface ActionSemanticMapping {
  7 |   [action: string]: string[];
  8 | }
  9 | 
 10 | interface TermTypeMapping {
 11 |   [type: string]: string[];
 12 | }
 13 | 
 14 | interface ScoringWeights {
 15 |   [type: string]: {
 16 |     name: number;
 17 |     desc: number;
 18 |   };
 19 | }
 20 | 
 21 | export class SearchEnhancer {
 22 |   /**
 23 |    * Semantic action mappings for enhanced intent matching
 24 |    * Maps indirect actions to their direct equivalents
 25 |    */
 26 |   private static readonly ACTION_SEMANTIC: ActionSemanticMapping = {
 27 |     // Write/Create actions
 28 |     'save': ['write', 'create', 'store', 'edit', 'modify', 'update'],
 29 |     'make': ['create', 'write', 'add'],
 30 |     'store': ['write', 'save', 'put'],
 31 |     'put': ['write', 'store', 'add'],
 32 |     'insert': ['add', 'write', 'create'],
 33 | 
 34 |     // Read/Retrieve actions
 35 |     'load': ['read', 'get', 'open'],
 36 |     'show': ['view', 'display', 'read'],
 37 |     'fetch': ['get', 'retrieve', 'read'],
 38 |     'retrieve': ['get', 'fetch', 'read'],
 39 |     'display': ['show', 'view', 'read'],
 40 | 
 41 |     // Modify/Update actions
 42 |     'modify': ['edit', 'update', 'change'],
 43 |     'alter': ['edit', 'modify', 'update'],
 44 |     'patch': ['edit', 'update', 'modify'],
 45 |     'change': ['edit', 'modify', 'update'],
 46 | 
 47 |     // Delete/Remove actions
 48 |     'remove': ['delete', 'clear', 'drop'],
 49 |     'clear': ['delete', 'remove', 'drop'],
 50 |     'destroy': ['delete', 'remove', 'clear'],
 51 |     'drop': ['delete', 'remove', 'clear'],
 52 | 
 53 |     // Search/Query actions
 54 |     'find': ['search', 'query', 'get'],
 55 |     'lookup': ['find', 'search', 'get'],
 56 |     'query': ['search', 'find', 'get'],
 57 |     'filter': ['search', 'find', 'query'],
 58 | 
 59 |     // Execute/Run actions
 60 |     'execute': ['run', 'start', 'launch'],
 61 |     'launch': ['run', 'start', 'execute'],
 62 |     'invoke': ['run', 'execute', 'call'],
 63 |     'trigger': ['run', 'execute', 'start']
 64 |   };
 65 | 
 66 |   /**
 67 |    * Term type classification for differentiated scoring
 68 |    * Categorizes query terms by their semantic role
 69 |    */
 70 |   private static readonly TERM_TYPES: TermTypeMapping = {
 71 |     ACTION: [
 72 |       // Primary actions
 73 |       'save', 'write', 'create', 'make', 'add', 'insert', 'store', 'put',
 74 |       'read', 'get', 'load', 'open', 'view', 'show', 'fetch', 'retrieve',
 75 |       'edit', 'update', 'modify', 'change', 'alter', 'patch',
 76 |       'delete', 'remove', 'clear', 'drop', 'destroy',
 77 |       'list', 'find', 'search', 'query', 'filter', 'lookup',
 78 |       'run', 'execute', 'start', 'stop', 'restart', 'launch', 'invoke',
 79 | 
 80 |       // Extended actions
 81 |       'copy', 'move', 'rename', 'duplicate', 'clone',
 82 |       'upload', 'download', 'sync', 'backup', 'restore',
 83 |       'import', 'export', 'convert', 'transform', 'process',
 84 |       'validate', 'verify', 'check', 'test', 'monitor'
 85 |     ],
 86 | 
 87 |     OBJECT: [
 88 |       // File/Document objects
 89 |       'file', 'files', 'document', 'documents', 'data', 'content',
 90 |       'folder', 'directory', 'directories', 'path', 'paths',
 91 |       'image', 'images', 'video', 'videos', 'audio', 'media',
 92 | 
 93 |       // Data objects
 94 |       'record', 'records', 'entry', 'entries', 'item', 'items',
 95 |       'database', 'table', 'tables', 'collection', 'dataset',
 96 |       'user', 'users', 'account', 'accounts', 'profile', 'profiles',
 97 | 
 98 |       // System objects
 99 |       'process', 'processes', 'service', 'services', 'application', 'apps',
100 |       'server', 'servers', 'connection', 'connections', 'session', 'sessions',
101 |       'config', 'configuration', 'settings', 'preferences', 'options'
102 |     ],
103 | 
104 |     MODIFIER: [
105 |       // Format modifiers
106 |       'text', 'binary', 'json', 'xml', 'csv', 'html', 'markdown', 'pdf',
107 |       'yaml', 'toml', 'ini', 'config', 'log', 'tmp', 'temp',
108 | 
109 |       // Size modifiers
110 |       'large', 'small', 'big', 'tiny', 'huge', 'mini', 'massive',
111 | 
112 |       // State modifiers
113 |       'new', 'old', 'existing', 'current', 'active', 'inactive',
114 |       'enabled', 'disabled', 'public', 'private', 'hidden', 'visible',
115 | 
116 |       // Quality modifiers
117 |       'empty', 'full', 'partial', 'complete', 'broken', 'valid', 'invalid'
118 |     ],
119 | 
120 |     SCOPE: [
121 |       // Quantity scope
122 |       'all', 'some', 'none', 'every', 'each', 'any',
123 |       'multiple', 'single', 'one', 'many', 'few', 'several',
124 | 
125 |       // Processing scope
126 |       'batch', 'bulk', 'individual', 'group', 'mass',
127 |       'recursive', 'nested', 'deep', 'shallow',
128 | 
129 |       // Range scope
130 |       'first', 'last', 'next', 'previous', 'recent', 'latest'
131 |     ]
132 |   };
133 | 
134 |   /**
135 |    * Scoring weights for different term types
136 |    * Higher weights indicate more important semantic roles
137 |    */
138 |   private static readonly SCORING_WEIGHTS: ScoringWeights = {
139 |     ACTION: { name: 0.7, desc: 0.35 },    // Highest weight - intent is critical
140 |     OBJECT: { name: 0.2, desc: 0.1 },     // Medium weight - what we're acting on
141 |     MODIFIER: { name: 0.05, desc: 0.025 }, // Low weight - how we're acting
142 |     SCOPE: { name: 0.03, desc: 0.015 }    // Lowest weight - scale of action
143 |   };
144 | 
145 |   /**
146 |    * Get semantic mappings for an action word
147 |    */
148 |   static getActionSemantics(action: string): string[] {
149 |     return this.ACTION_SEMANTIC[action.toLowerCase()] || [];
150 |   }
151 | 
152 |   /**
153 |    * Classify a term by its semantic type
154 |    */
155 |   static classifyTerm(term: string): string {
156 |     const lowerTerm = term.toLowerCase();
157 | 
158 |     for (const [type, terms] of Object.entries(this.TERM_TYPES)) {
159 |       if (terms.includes(lowerTerm)) {
160 |         return type;
161 |       }
162 |     }
163 | 
164 |     return 'OTHER';
165 |   }
166 | 
167 |   /**
168 |    * Get scoring weights for a term type
169 |    */
170 |   static getTypeWeights(termType: string): { name: number; desc: number } {
171 |     return this.SCORING_WEIGHTS[termType] || { name: 0.15, desc: 0.075 };
172 |   }
173 | 
174 |   /**
175 |    * Get all action words for a specific category
176 |    */
177 |   static getActionsByCategory(category: 'write' | 'read' | 'modify' | 'delete' | 'search' | 'execute'): string[] {
178 |     const actions = this.TERM_TYPES.ACTION;
179 |     const categoryMappings = {
180 |       write: ['save', 'write', 'create', 'make', 'add', 'insert', 'store', 'put'],
181 |       read: ['read', 'get', 'load', 'open', 'view', 'show', 'fetch', 'retrieve'],
182 |       modify: ['edit', 'update', 'modify', 'change', 'alter', 'patch'],
183 |       delete: ['delete', 'remove', 'clear', 'drop', 'destroy'],
184 |       search: ['list', 'find', 'search', 'query', 'filter', 'lookup'],
185 |       execute: ['run', 'execute', 'start', 'stop', 'restart', 'launch', 'invoke']
186 |     };
187 | 
188 |     return categoryMappings[category] || [];
189 |   }
190 | 
191 |   /**
192 |    * Add new action semantic mapping (for extensibility)
193 |    */
194 |   static addActionSemantic(action: string, semantics: string[]): void {
195 |     this.ACTION_SEMANTIC[action.toLowerCase()] = semantics;
196 |   }
197 | 
198 |   /**
199 |    * Add terms to a type category (for extensibility)
200 |    */
201 |   static addTermsToType(type: string, terms: string[]): void {
202 |     if (!this.TERM_TYPES[type]) {
203 |       this.TERM_TYPES[type] = [];
204 |     }
205 |     this.TERM_TYPES[type].push(...terms.map(t => t.toLowerCase()));
206 |   }
207 | 
208 |   /**
209 |    * Update scoring weights for a term type (for tuning)
210 |    */
211 |   static updateTypeWeights(type: string, nameWeight: number, descWeight: number): void {
212 |     this.SCORING_WEIGHTS[type] = { name: nameWeight, desc: descWeight };
213 |   }
214 | 
215 |   /**
216 |    * Calculate intent penalty for conflicting actions
217 |    */
218 |   static getIntentPenalty(actionTerm: string, toolName: string): number {
219 |     const lowerToolName = toolName.toLowerCase();
220 | 
221 |     // Penalize read-only tools when user wants to save/write
222 |     if ((actionTerm === 'save' || actionTerm === 'write') &&
223 |         (lowerToolName.includes('read') && !lowerToolName.includes('write') && !lowerToolName.includes('edit'))) {
224 |       return 0.3; // Penalty for read-only tools when intent is save/write
225 |     }
226 | 
227 |     // Penalize write tools when user wants to read
228 |     if (actionTerm === 'read' &&
229 |         (lowerToolName.includes('write') && !lowerToolName.includes('read'))) {
230 |       return 0.2; // Penalty for write-only tools when intent is read
231 |     }
232 | 
233 |     // Penalize delete tools when user wants to create/add
234 |     if ((actionTerm === 'create' || actionTerm === 'add') &&
235 |         lowerToolName.includes('delete')) {
236 |       return 0.3; // Penalty for delete tools when intent is create
237 |     }
238 | 
239 |     return 0; // No penalty
240 |   }
241 | 
242 |   /**
243 |    * Get debug information for a query
244 |    */
245 |   static analyzeQuery(query: string): {
246 |     terms: string[];
247 |     classifications: { [term: string]: string };
248 |     actionSemantics: { [action: string]: string[] };
249 |     weights: { [term: string]: { name: number; desc: number } };
250 |   } {
251 |     const terms = query.toLowerCase().split(/\s+/).filter(term => term.length > 2);
252 |     const classifications: { [term: string]: string } = {};
253 |     const actionSemantics: { [action: string]: string[] } = {};
254 |     const weights: { [term: string]: { name: number; desc: number } } = {};
255 | 
256 |     for (const term of terms) {
257 |       const type = this.classifyTerm(term);
258 |       classifications[term] = type;
259 |       weights[term] = this.getTypeWeights(type);
260 | 
261 |       if (type === 'ACTION') {
262 |         const semantics = this.getActionSemantics(term);
263 |         if (semantics.length > 0) {
264 |           actionSemantics[term] = semantics;
265 |         }
266 |       }
267 |     }
268 | 
269 |     return { terms, classifications, actionSemantics, weights };
270 |   }
271 | 
272 |   /**
273 |    * Get all available term types (for documentation)
274 |    */
275 |   static getAllTermTypes(): string[] {
276 |     return Object.keys(this.TERM_TYPES).sort();
277 |   }
278 | 
279 |   /**
280 |    * Get all action words (for documentation)
281 |    */
282 |   static getAllActions(): string[] {
283 |     return Object.keys(this.ACTION_SEMANTIC).sort();
284 |   }
285 | }
286 | 
287 | export default SearchEnhancer;
```

--------------------------------------------------------------------------------
/PROMPTS-IMPLEMENTATION.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Prompts Implementation Summary
  2 | 
  3 | ## ✅ **What We've Implemented**
  4 | 
  5 | You asked: "Can we pop up dialog boxes for user approval even when using .mcpb?"
  6 | 
  7 | **Answer: YES!** We've implemented MCP protocol's **prompts capability** that works in Claude Desktop, even with .mcpb bundles.
  8 | 
  9 | ---
 10 | 
 11 | ## 🎯 **How It Works**
 12 | 
 13 | ### **The Flow**
 14 | 
 15 | 1. **AI wants to do something** (e.g., add GitHub MCP)
 16 | 2. **NCP triggers a prompt** (shows dialog to user)
 17 | 3. **User approves/declines** (clicks YES/NO or provides input)
 18 | 4. **NCP gets response** (executes or cancels based on user choice)
 19 | 
 20 | ### **Works Everywhere**
 21 | 
 22 | | Environment | Prompt Display |
 23 | |-------------|----------------|
 24 | | **.mcpb in Claude Desktop** | ✅ Native dialog boxes |
 25 | | **npm in Claude Desktop** | ✅ Native dialog boxes |
 26 | | **VS Code** | ✅ Quick pick / input box |
 27 | | **Cursor** | ✅ IDE notifications |
 28 | 
 29 | ---
 30 | 
 31 | ## 📁 **Files Created/Modified**
 32 | 
 33 | ### **New Files**
 34 | 
 35 | 1. **`src/server/mcp-prompts.ts`** - Prompt definitions and generators
 36 |    - Defines 4 prompt types (confirm_add, confirm_remove, configure, approve_dangerous)
 37 |    - Generates user-friendly prompt messages
 38 |    - Parses user responses
 39 | 
 40 | 2. **`docs/guides/mcp-prompts-for-user-interaction.md`** - Complete documentation
 41 |    - Architecture diagrams
 42 |    - Usage examples
 43 |    - Implementation roadmap
 44 | 
 45 | ### **Modified Files**
 46 | 
 47 | 1. **`src/server/mcp-server.ts`**
 48 |    - Added `prompts: {}` to capabilities
 49 |    - Updated `handleListPrompts()` to include NCP_PROMPTS
 50 |    - Implemented `handleGetPrompt()` for prompt generation
 51 |    - Added `prompts/get` to request router
 52 | 
 53 | ---
 54 | 
 55 | ## 🛠️ **Available Prompts**
 56 | 
 57 | ### **1. confirm_add_mcp**
 58 | **Purpose:** Ask user before adding new MCP server
 59 | 
 60 | **Parameters:**
 61 | - `mcp_name` - Name of the MCP to add
 62 | - `command` - Command to execute
 63 | - `args` - Command arguments (optional)
 64 | - `profile` - Target profile (default: 'all')
 65 | 
 66 | **User sees:**
 67 | ```
 68 | Do you want to add the MCP server "github" to profile "all"?
 69 | 
 70 | Command: npx -y @modelcontextprotocol/server-github
 71 | 
 72 | This will allow Claude to access the tools provided by this MCP server.
 73 | 
 74 | Please respond with YES to confirm or NO to cancel.
 75 | 
 76 | [ YES ]  [ NO ]
 77 | ```
 78 | 
 79 | ---
 80 | 
 81 | ### **2. confirm_remove_mcp**
 82 | **Purpose:** Ask user before removing MCP server
 83 | 
 84 | **Parameters:**
 85 | - `mcp_name` - Name of the MCP to remove
 86 | - `profile` - Profile to remove from (default: 'all')
 87 | 
 88 | **User sees:**
 89 | ```
 90 | Do you want to remove the MCP server "github" from profile "all"?
 91 | 
 92 | This will remove access to all tools provided by this MCP server.
 93 | 
 94 | Please respond with YES to confirm or NO to cancel.
 95 | 
 96 | [ YES ]  [ NO ]
 97 | ```
 98 | 
 99 | ---
100 | 
101 | ### **3. configure_mcp**
102 | **Purpose:** Collect configuration input from user
103 | 
104 | **Parameters:**
105 | - `mcp_name` - Name of the MCP being configured
106 | - `config_type` - Type of configuration
107 | - `description` - What to ask for
108 | 
109 | **User sees:**
110 | ```
111 | Configuration needed for "github":
112 | 
113 | GitHub Personal Access Token (for repository access)
114 | 
115 | Please provide the required value.
116 | 
117 | [ Input: _________________ ]
118 | ```
119 | 
120 | ---
121 | 
122 | ### **4. approve_dangerous_operation**
123 | **Purpose:** Get approval for risky operations
124 | 
125 | **Parameters:**
126 | - `operation` - Description of operation
127 | - `impact` - Potential impact description
128 | 
129 | **User sees:**
130 | ```
131 | ⚠️  Dangerous Operation
132 | 
133 | Remove all MCP servers from profile 'all'
134 | 
135 | Potential Impact:
136 | - All configured MCPs will be removed
137 | - Claude will lose access to all tools
138 | - Configuration will need to be rebuilt
139 | 
140 | Do you want to proceed?
141 | 
142 | [ YES ]  [ NO ]
143 | ```
144 | 
145 | ---
146 | 
147 | ## 🚀 **Next Steps to Complete Implementation**
148 | 
149 | ### **Phase 1: Foundation** ✅ DONE
150 | - [x] Prompts capability enabled
151 | - [x] Prompt definitions created
152 | - [x] Prompt handlers implemented
153 | - [x] Documentation written
154 | 
155 | ### **Phase 2: Add Management Tools** (TODO)
156 | 
157 | Add these new tools that USE the prompts:
158 | 
159 | ```typescript
160 | // 1. add_mcp tool
161 | {
162 |   name: 'add_mcp',
163 |   description: 'Add a new MCP server (requires user approval)',
164 |   inputSchema: {
165 |     type: 'object',
166 |     properties: {
167 |       mcp_name: { type: 'string' },
168 |       command: { type: 'string' },
169 |       args: { type: 'array', items: { type: 'string' } },
170 |       env: { type: 'object' }
171 |     },
172 |     required: ['mcp_name', 'command']
173 |   }
174 | }
175 | 
176 | // Implementation pseudo-code:
177 | async function handleAddMCP(args) {
178 |   // 1. Show prompt to user
179 |   const confirmed = await showPrompt('confirm_add_mcp', args);
180 | 
181 |   // 2. If user approved, add MCP
182 |   if (confirmed) {
183 |     await profileManager.addMCPToProfile(args.profile, args.mcp_name, {
184 |       command: args.command,
185 |       args: args.args,
186 |       env: args.env
187 |     });
188 |     return { success: true };
189 |   }
190 | 
191 |   return { success: false, message: 'User cancelled' };
192 | }
193 | 
194 | // 2. remove_mcp tool
195 | {
196 |   name: 'remove_mcp',
197 |   description: 'Remove an MCP server (requires user approval)',
198 |   inputSchema: {
199 |     type: 'object',
200 |     properties: {
201 |       mcp_name: { type: 'string' },
202 |       profile: { type: 'string', default: 'all' }
203 |     },
204 |     required: ['mcp_name']
205 |   }
206 | }
207 | 
208 | // 3. configure_env tool
209 | {
210 |   name: 'configure_env',
211 |   description: 'Configure environment variables for MCP (collects input)',
212 |   inputSchema: {
213 |     type: 'object',
214 |     properties: {
215 |       mcp_name: { type: 'string' },
216 |       var_name: { type: 'string' },
217 |       description: { type: 'string' }
218 |     },
219 |     required: ['mcp_name', 'var_name']
220 |   }
221 | }
222 | ```
223 | 
224 | ---
225 | 
226 | ## 🎬 **Example User Experience**
227 | 
228 | ### **Scenario: User asks AI to add GitHub MCP**
229 | 
230 | **User:** "Add the GitHub MCP server so you can access my repositories"
231 | 
232 | **Claude (AI) thinks:**
233 | ```
234 | I need to add the GitHub MCP server. Let me use the add_mcp tool.
235 | ```
236 | 
237 | **Claude calls tool:**
238 | ```json
239 | {
240 |   "name": "add_mcp",
241 |   "arguments": {
242 |     "mcp_name": "github",
243 |     "command": "npx",
244 |     "args": ["-y", "@modelcontextprotocol/server-github"]
245 |   }
246 | }
247 | ```
248 | 
249 | **NCP shows prompt to user:**
250 | ```
251 | ┌────────────────────────────────────────────────┐
252 | │  Do you want to add the MCP server "github"   │
253 | │  to profile "all"?                             │
254 | │                                                 │
255 | │  Command: npx -y @modelcontextprotocol/        │
256 | │           server-github                         │
257 | │                                                 │
258 | │  This will allow Claude to access the tools    │
259 | │  provided by this MCP server.                  │
260 | │                                                 │
261 | │              [ YES ]    [ NO ]                 │
262 | └────────────────────────────────────────────────┘
263 | ```
264 | 
265 | **User clicks: YES**
266 | 
267 | **NCP adds the MCP and returns:**
268 | ```json
269 | {
270 |   "success": true,
271 |   "message": "MCP server 'github' added successfully",
272 |   "tools_count": 5,
273 |   "tools": ["create_issue", "get_repository", "search_code", "create_pr", "list_issues"]
274 | }
275 | ```
276 | 
277 | **Claude tells user:**
278 | ```
279 | ✅ I've successfully added the GitHub MCP server!
280 | 
281 | I now have access to 5 new tools for working with GitHub:
282 | - Create issues
283 | - Get repository information
284 | - Search code
285 | - Create pull requests
286 | - List issues
287 | 
288 | You can now ask me to interact with your GitHub repositories!
289 | ```
290 | 
291 | ---
292 | 
293 | ## 🔒 **Security Benefits**
294 | 
295 | | Without Prompts | With Prompts |
296 | |-----------------|--------------|
297 | | ❌ AI modifies config freely | ✅ User approves every change |
298 | | ❌ No transparency | ✅ User sees exact command |
299 | | ❌ Hard to undo mistakes | ✅ Prevent mistakes before they happen |
300 | | ❌ User feels out of control | ✅ User stays in control |
301 | 
302 | ---
303 | 
304 | ## 🧪 **Testing**
305 | 
306 | ### **Test Prompts Capability**
307 | 
308 | ```bash
309 | # 1. List available prompts
310 | echo '{"jsonrpc":"2.0","id":1,"method":"prompts/list","params":{}}' | npx ncp
311 | 
312 | # Expected: Returns NCP_PROMPTS array
313 | ```
314 | 
315 | ### **Test Specific Prompt**
316 | 
317 | ```bash
318 | # 2. Get add_mcp confirmation prompt
319 | echo '{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"confirm_add_mcp","arguments":{"mcp_name":"test","command":"echo","args":["hello"]}}}' | npx ncp
320 | 
321 | # Expected: Returns prompt messages for user
322 | ```
323 | 
324 | ### **Test in Claude Desktop**
325 | 
326 | 1. Start NCP as MCP server (via .mcpb or npm)
327 | 2. Say to Claude: "What prompts do you have available?"
328 | 3. Claude will call `prompts/list` and show the 4 prompts
329 | 4. Say: "Show me the add MCP confirmation"
330 | 5. Claude will call `prompts/get` and generate the message
331 | 
332 | ---
333 | 
334 | ## 📊 **Implementation Status**
335 | 
336 | | Component | Status | File |
337 | |-----------|--------|------|
338 | | Prompts capability | ✅ Done | `src/server/mcp-server.ts:197` |
339 | | Prompt definitions | ✅ Done | `src/server/mcp-prompts.ts` |
340 | | handleListPrompts | ✅ Done | `src/server/mcp-server.ts:747` |
341 | | handleGetPrompt | ✅ Done | `src/server/mcp-server.ts:777` |
342 | | Message generators | ✅ Done | `src/server/mcp-prompts.ts` |
343 | | Documentation | ✅ Done | `docs/guides/mcp-prompts-for-user-interaction.md` |
344 | | Management tools (add/remove) | ⏳ TODO | Need to implement |
345 | | Prompt response parsing | ⏳ TODO | Need to integrate |
346 | 
347 | ---
348 | 
349 | ## 💡 **Key Insight**
350 | 
351 | **The foundation is complete!** We have:
352 | - ✅ Prompts capability enabled
353 | - ✅ 4 prompts defined and ready
354 | - ✅ Prompt generation working
355 | - ✅ Full documentation
356 | 
357 | **What's left:** Add the actual management tools (`add_mcp`, `remove_mcp`) that USE these prompts.
358 | 
359 | **This answers your question:** YES, you can pop up dialog boxes for user approval, even in .mcpb bundles! The MCP protocol makes this possible through the prompts capability. 🎉
360 | 
361 | ---
362 | 
363 | ## 🔗 **Related Files**
364 | 
365 | - **Implementation:** `src/server/mcp-prompts.ts`
366 | - **Integration:** `src/server/mcp-server.ts`
367 | - **Documentation:** `docs/guides/mcp-prompts-for-user-interaction.md`
368 | - **MCP Spec:** https://modelcontextprotocol.io/docs/concepts/prompts
369 | 
370 | ---
371 | 
372 | **Ready to add management tools when you want to proceed with Phase 2!** 🚀
373 | 
```

--------------------------------------------------------------------------------
/test/search-enhancer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { SearchEnhancer } from '../src/discovery/search-enhancer';
  2 | 
  3 | describe('SearchEnhancer', () => {
  4 |   describe('Action Semantic Mapping', () => {
  5 |     test('should get semantic mappings for save action', () => {
  6 |       const semantics = SearchEnhancer.getActionSemantics('save');
  7 |       expect(semantics).toContain('write');
  8 |       expect(semantics).toContain('create');
  9 |       expect(semantics).toContain('store');
 10 |       expect(semantics).toContain('edit');
 11 |       expect(semantics).toContain('modify');
 12 |       expect(semantics).toContain('update');
 13 |     });
 14 | 
 15 |     test('should get semantic mappings for load action', () => {
 16 |       const semantics = SearchEnhancer.getActionSemantics('load');
 17 |       expect(semantics).toContain('read');
 18 |       expect(semantics).toContain('get');
 19 |       expect(semantics).toContain('open');
 20 |     });
 21 | 
 22 |     test('should return empty array for unknown action', () => {
 23 |       const semantics = SearchEnhancer.getActionSemantics('unknownaction');
 24 |       expect(semantics).toEqual([]);
 25 |     });
 26 | 
 27 |     test('should handle case-insensitive action words', () => {
 28 |       const semantics1 = SearchEnhancer.getActionSemantics('SAVE');
 29 |       const semantics2 = SearchEnhancer.getActionSemantics('save');
 30 |       expect(semantics1).toEqual(semantics2);
 31 |     });
 32 |   });
 33 | 
 34 |   describe('Term Classification', () => {
 35 |     test('should classify action terms correctly', () => {
 36 |       expect(SearchEnhancer.classifyTerm('save')).toBe('ACTION');
 37 |       expect(SearchEnhancer.classifyTerm('write')).toBe('ACTION');
 38 |       expect(SearchEnhancer.classifyTerm('read')).toBe('ACTION');
 39 |       expect(SearchEnhancer.classifyTerm('delete')).toBe('ACTION');
 40 |     });
 41 | 
 42 |     test('should classify object terms correctly', () => {
 43 |       expect(SearchEnhancer.classifyTerm('file')).toBe('OBJECT');
 44 |       expect(SearchEnhancer.classifyTerm('document')).toBe('OBJECT');
 45 |       expect(SearchEnhancer.classifyTerm('database')).toBe('OBJECT');
 46 |       expect(SearchEnhancer.classifyTerm('user')).toBe('OBJECT');
 47 |     });
 48 | 
 49 |     test('should classify modifier terms correctly', () => {
 50 |       expect(SearchEnhancer.classifyTerm('text')).toBe('MODIFIER');
 51 |       expect(SearchEnhancer.classifyTerm('json')).toBe('MODIFIER');
 52 |       expect(SearchEnhancer.classifyTerm('large')).toBe('MODIFIER');
 53 |       expect(SearchEnhancer.classifyTerm('new')).toBe('MODIFIER');
 54 |     });
 55 | 
 56 |     test('should classify scope terms correctly', () => {
 57 |       expect(SearchEnhancer.classifyTerm('all')).toBe('SCOPE');
 58 |       expect(SearchEnhancer.classifyTerm('multiple')).toBe('SCOPE');
 59 |       expect(SearchEnhancer.classifyTerm('batch')).toBe('SCOPE');
 60 |       expect(SearchEnhancer.classifyTerm('recursive')).toBe('SCOPE');
 61 |     });
 62 | 
 63 |     test('should return OTHER for unrecognized terms', () => {
 64 |       expect(SearchEnhancer.classifyTerm('xyz')).toBe('OTHER');
 65 |       expect(SearchEnhancer.classifyTerm('randomword')).toBe('OTHER');
 66 |     });
 67 |   });
 68 | 
 69 |   describe('Type Weights', () => {
 70 |     test('should return correct weights for ACTION type', () => {
 71 |       const weights = SearchEnhancer.getTypeWeights('ACTION');
 72 |       expect(weights.name).toBe(0.7);
 73 |       expect(weights.desc).toBe(0.35);
 74 |     });
 75 | 
 76 |     test('should return correct weights for OBJECT type', () => {
 77 |       const weights = SearchEnhancer.getTypeWeights('OBJECT');
 78 |       expect(weights.name).toBe(0.2);
 79 |       expect(weights.desc).toBe(0.1);
 80 |     });
 81 | 
 82 |     test('should return correct weights for MODIFIER type', () => {
 83 |       const weights = SearchEnhancer.getTypeWeights('MODIFIER');
 84 |       expect(weights.name).toBe(0.05);
 85 |       expect(weights.desc).toBe(0.025);
 86 |     });
 87 | 
 88 |     test('should return correct weights for SCOPE type', () => {
 89 |       const weights = SearchEnhancer.getTypeWeights('SCOPE');
 90 |       expect(weights.name).toBe(0.03);
 91 |       expect(weights.desc).toBe(0.015);
 92 |     });
 93 | 
 94 |     test('should return default weights for unknown type', () => {
 95 |       const weights = SearchEnhancer.getTypeWeights('UNKNOWN');
 96 |       expect(weights.name).toBe(0.15);
 97 |       expect(weights.desc).toBe(0.075);
 98 |     });
 99 |   });
100 | 
101 |   describe('Intent Penalty', () => {
102 |     test('should penalize read-only tools when intent is save', () => {
103 |       const penalty = SearchEnhancer.getIntentPenalty('save', 'read_file');
104 |       expect(penalty).toBe(0.3);
105 |     });
106 | 
107 |     test('should penalize read-only tools when intent is write', () => {
108 |       const penalty = SearchEnhancer.getIntentPenalty('write', 'read_text_file');
109 |       expect(penalty).toBe(0.3);
110 |     });
111 | 
112 |     test('should not penalize tools with both read and write capabilities', () => {
113 |       const penalty = SearchEnhancer.getIntentPenalty('save', 'read_write_file');
114 |       expect(penalty).toBe(0);
115 |     });
116 | 
117 |     test('should not penalize tools with edit capability', () => {
118 |       const penalty = SearchEnhancer.getIntentPenalty('save', 'edit_file');
119 |       expect(penalty).toBe(0);
120 |     });
121 | 
122 |     test('should penalize write-only tools when intent is read', () => {
123 |       const penalty = SearchEnhancer.getIntentPenalty('read', 'write_file');
124 |       expect(penalty).toBe(0.2);
125 |     });
126 | 
127 |     test('should penalize delete tools when intent is create', () => {
128 |       const penalty = SearchEnhancer.getIntentPenalty('create', 'delete_file');
129 |       expect(penalty).toBe(0.3);
130 |     });
131 | 
132 |     test('should penalize delete tools when intent is add', () => {
133 |       const penalty = SearchEnhancer.getIntentPenalty('add', 'delete_record');
134 |       expect(penalty).toBe(0.3);
135 |     });
136 | 
137 |     test('should return no penalty for aligned operations', () => {
138 |       const penalty1 = SearchEnhancer.getIntentPenalty('save', 'write_file');
139 |       const penalty2 = SearchEnhancer.getIntentPenalty('read', 'read_file');
140 |       const penalty3 = SearchEnhancer.getIntentPenalty('delete', 'delete_file');
141 | 
142 |       expect(penalty1).toBe(0);
143 |       expect(penalty2).toBe(0);
144 |       expect(penalty3).toBe(0);
145 |     });
146 |   });
147 | 
148 |   describe('Query Analysis', () => {
149 |     test('should analyze query and provide comprehensive information', () => {
150 |       const analysis = SearchEnhancer.analyzeQuery('save text file');
151 | 
152 |       expect(analysis.terms).toEqual(['save', 'text', 'file']);
153 |       expect(analysis.classifications['save']).toBe('ACTION');
154 |       expect(analysis.classifications['text']).toBe('MODIFIER');
155 |       expect(analysis.classifications['file']).toBe('OBJECT');
156 | 
157 |       expect(analysis.actionSemantics['save']).toBeDefined();
158 |       expect(analysis.actionSemantics['save']).toContain('write');
159 | 
160 |       expect(analysis.weights['save'].name).toBe(0.7);
161 |       expect(analysis.weights['text'].name).toBe(0.05);
162 |       expect(analysis.weights['file'].name).toBe(0.2);
163 |     });
164 | 
165 |     test('should filter short terms in query analysis', () => {
166 |       const analysis = SearchEnhancer.analyzeQuery('save a to file');
167 | 
168 |       // 'a' and 'to' should be filtered out (length <= 2)
169 |       expect(analysis.terms).toEqual(['save', 'file']);
170 |       expect(analysis.terms).not.toContain('a');
171 |       expect(analysis.terms).not.toContain('to');
172 |     });
173 |   });
174 | 
175 |   describe('Actions by Category', () => {
176 |     test('should get write category actions', () => {
177 |       const actions = SearchEnhancer.getActionsByCategory('write');
178 |       expect(actions).toContain('save');
179 |       expect(actions).toContain('write');
180 |       expect(actions).toContain('create');
181 |       expect(actions).toContain('store');
182 |     });
183 | 
184 |     test('should get read category actions', () => {
185 |       const actions = SearchEnhancer.getActionsByCategory('read');
186 |       expect(actions).toContain('read');
187 |       expect(actions).toContain('get');
188 |       expect(actions).toContain('load');
189 |       expect(actions).toContain('fetch');
190 |     });
191 | 
192 |     test('should get delete category actions', () => {
193 |       const actions = SearchEnhancer.getActionsByCategory('delete');
194 |       expect(actions).toContain('delete');
195 |       expect(actions).toContain('remove');
196 |       expect(actions).toContain('clear');
197 |       expect(actions).toContain('drop');
198 |     });
199 |   });
200 | 
201 |   describe('Extensibility Methods', () => {
202 |     test('should add new action semantic mapping', () => {
203 |       SearchEnhancer.addActionSemantic('custom', ['test1', 'test2']);
204 |       const semantics = SearchEnhancer.getActionSemantics('custom');
205 |       expect(semantics).toEqual(['test1', 'test2']);
206 |     });
207 | 
208 |     test('should add terms to type category', () => {
209 |       SearchEnhancer.addTermsToType('CUSTOM_TYPE', ['term1', 'term2']);
210 |       expect(SearchEnhancer.classifyTerm('term1')).toBe('CUSTOM_TYPE');
211 |       expect(SearchEnhancer.classifyTerm('term2')).toBe('CUSTOM_TYPE');
212 |     });
213 | 
214 |     test('should update type weights', () => {
215 |       SearchEnhancer.updateTypeWeights('CUSTOM_TYPE', 0.5, 0.25);
216 |       const weights = SearchEnhancer.getTypeWeights('CUSTOM_TYPE');
217 |       expect(weights.name).toBe(0.5);
218 |       expect(weights.desc).toBe(0.25);
219 |     });
220 |   });
221 | 
222 |   describe('Utility Methods', () => {
223 |     test('should get all term types', () => {
224 |       const types = SearchEnhancer.getAllTermTypes();
225 |       expect(types).toContain('ACTION');
226 |       expect(types).toContain('OBJECT');
227 |       expect(types).toContain('MODIFIER');
228 |       expect(types).toContain('SCOPE');
229 |       // Check if array is sorted
230 |       const sorted = [...types].sort();
231 |       expect(types).toEqual(sorted);
232 |     });
233 | 
234 |     test('should get all actions', () => {
235 |       const actions = SearchEnhancer.getAllActions();
236 |       expect(actions).toContain('save');
237 |       expect(actions).toContain('load');
238 |       expect(actions).toContain('modify');
239 |       // Check if array is sorted
240 |       const sorted = [...actions].sort();
241 |       expect(actions).toEqual(sorted);
242 |     });
243 |   });
244 | });
```

--------------------------------------------------------------------------------
/README-COMPARISON.md:
--------------------------------------------------------------------------------

```markdown
  1 | # README Comparison: Old vs New (Story-First)
  2 | 
  3 | ## 🎯 **What Changed?**
  4 | 
  5 | ### **Structure Transformation**
  6 | 
  7 | **Old README (610 lines):**
  8 | ```
  9 | 1. Badges
 10 | 2. Title
 11 | 3. Feature description (technical)
 12 | 4. MCP Paradox section
 13 | 5. Toy analogy
 14 | 6. Before/After comparison
 15 | 7. Prerequisites
 16 | 8. Installation (2 long sections)
 17 | 9. Test drive
 18 | 10. Alternative installation
 19 | 11. Why it matters
 20 | 12. Manual setup
 21 | 13. Popular MCPs
 22 | 14. Client configurations
 23 | 15. Advanced features
 24 | 16. Troubleshooting
 25 | 17. Deep dive link
 26 | 18. Contributing
 27 | 19. License
 28 | ```
 29 | 
 30 | **New README (Story-First, ~350 lines):**
 31 | ```
 32 | 1. Badges
 33 | 2. Title
 34 | 3. ONE LINE hook (instead of paragraph)
 35 | 4. The Problem (concise)
 36 | 5. **THE SIX STORIES** (with reading times)
 37 | 6. Quick Start (2 options, super clear)
 38 | 7. The Difference (numbers table)
 39 | 8. Learn More (organized links)
 40 | 9. Testimonials
 41 | 10. Philosophy
 42 | 11. [Expandable] Full documentation
 43 |     - Installation
 44 |     - Test drive
 45 |     - Project config
 46 |     - Advanced features
 47 |     - Troubleshooting
 48 |     - Popular MCPs
 49 |     - Contributing
 50 | 12. License
 51 | ```
 52 | 
 53 | ---
 54 | 
 55 | ## 💡 **Key Improvements**
 56 | 
 57 | ### **1. Immediate Hook**
 58 | 
 59 | **Old (Technical):**
 60 | > "NCP transforms N scattered MCP servers into 1 intelligent orchestrator. Your AI sees just 2 simple tools instead of 50+ complex ones..."
 61 | 
 62 | **New (Story):**
 63 | > "Your AI doesn't see your 50 tools. It dreams of the perfect tool, and NCP finds it instantly."
 64 | 
 65 | **Why better:** One sentence. No jargon. Instantly understood.
 66 | 
 67 | ---
 68 | 
 69 | ### **2. Problem Statement**
 70 | 
 71 | **Old:**
 72 | - 4 paragraphs with analogies (toys, buffet, poet)
 73 | - Great content but too much upfront
 74 | 
 75 | **New:**
 76 | - 4 bullet points
 77 | - Problem → Why it matters → Done
 78 | - Analogies moved to stories
 79 | 
 80 | **Why better:** Respects reader's time. They can deep-dive via stories if interested.
 81 | 
 82 | ---
 83 | 
 84 | ### **3. Core Innovation: The Six Stories**
 85 | 
 86 | **Old:**
 87 | - Features described inline as you read
 88 | - Technical explanations mixed with benefits
 89 | - Hard to find specific information later
 90 | 
 91 | **New:**
 92 | - **Six named stories** at top (like a table of contents)
 93 | - Each story: Problem + Solution + Result (one line)
 94 | - Reading time shown (2 min each)
 95 | - Full stories in separate pages
 96 | 
 97 | **Why better:**
 98 | - **Scannable:** See all benefits in 30 seconds
 99 | - **Memorable:** "Oh, the clipboard handshake story!"
100 | - **Referenceable:** "Read Story 2 for security"
101 | - **Self-documenting:** Each story is complete explanation
102 | 
103 | **Example:**
104 | ```markdown
105 | ### 🔐 Story 2: Secrets in Plain Sight *2 min*
106 | > **Problem:** API keys exposed in AI chat logs forever
107 | > **Solution:** Clipboard handshake keeps secrets server-side
108 | > **Result:** AI never sees your tokens, full security + convenience
109 | ```
110 | 
111 | User reads this and immediately knows:
112 | 1. What the problem is
113 | 2. How NCP solves it
114 | 3. What benefit they get
115 | 4. Where to read more (link)
116 | 5. Time investment (2 min)
117 | 
118 | ---
119 | 
120 | ### **4. Quick Start Clarity**
121 | 
122 | **Old:**
123 | - Prerequisites first (Node.js, npm...)
124 | - Two installation methods mixed
125 | - Takes 3 sections to get to "how to start"
126 | 
127 | **New:**
128 | - Quick Start second (right after stories)
129 | - Two clear options:
130 |   - Option 1: Claude Desktop (3 steps, 30 seconds)
131 |   - Option 2: Other clients (code block, 2 minutes)
132 | - Prerequisites moved to full installation section
133 | 
134 | **Why better:** User can start immediately if they want, or read stories first if they're evaluating.
135 | 
136 | ---
137 | 
138 | ### **5. Social Proof**
139 | 
140 | **New section added:**
141 | - User testimonials
142 | - Beta tester feedback
143 | - Real quotes about experience
144 | 
145 | **Why important:** Stories explain features. Testimonials prove they work.
146 | 
147 | ---
148 | 
149 | ### **6. Philosophy Statement**
150 | 
151 | **New section added:**
152 | ```markdown
153 | ## Philosophy
154 | 
155 | Constraints spark creativity. Infinite options paralyze.
156 | 
157 | Give it 50 tools → Analysis paralysis
158 | Give it a way to dream → Focused action
159 | ```
160 | 
161 | **Why important:** Explains the "why" behind NCP's design. Makes the approach memorable.
162 | 
163 | ---
164 | 
165 | ### **7. Organized "Learn More"**
166 | 
167 | **Old:**
168 | - Everything inline
169 | - Hard to find specific topics
170 | - No clear hierarchy
171 | 
172 | **New:**
173 | - Three sections:
174 |   - **For Users** (stories, installation, troubleshooting)
175 |   - **For Developers** (technical docs, contributing)
176 |   - **For Teams** (project config, workflows, security)
177 | - Clear progression from beginner to advanced
178 | 
179 | **Why better:** Right information for right audience. Users don't see developer docs upfront. Developers can skip to technical details.
180 | 
181 | ---
182 | 
183 | ### **8. Full Documentation Collapsed**
184 | 
185 | **Old:**
186 | - Everything at top level
187 | - Must scroll through all content
188 | - Hard to find specific section
189 | 
190 | **New:**
191 | - Quick start at top (30-second view)
192 | - Full docs collapsed below
193 | - Can expand if needed, or skip if not
194 | 
195 | **Why better:** Respects different reader goals:
196 | - "Just tell me what this does" → Read first 3 sections (2 min)
197 | - "I want to install" → Quick Start (30 sec)
198 | - "I need details" → Expand full docs (as needed)
199 | 
200 | ---
201 | 
202 | ## 📊 **Length Comparison**
203 | 
204 | | Section | Old | New | Change |
205 | |---------|-----|-----|--------|
206 | | **Above the fold** | 120 lines | 80 lines | **-33%** (more concise) |
207 | | **Core content** | 610 lines | 350 lines | **-43%** (moved to stories) |
208 | | **Information lost** | 0% | 0% | **(nothing removed)** |
209 | 
210 | **Net result:** Same information, half the scrolling, stories as deep-dives.
211 | 
212 | ---
213 | 
214 | ## 🎯 **User Journey Comparison**
215 | 
216 | ### **Old README Journey:**
217 | 
218 | ```
219 | User arrives
220 | → Reads badges
221 | → Sees technical description (confused?)
222 | → Reads paradox section (getting it...)
223 | → Reads toy analogy (okay, I understand now)
224 | → Reads buffet analogy (okay, got it already!)
225 | → Before/after (good comparison)
226 | → Prerequisites (ugh, do I need to install Node?)
227 | → Installation section 1 (long)
228 | → Installation section 2 (longer)
229 | → [50% of users left by now]
230 | → Test drive section
231 | → Alternative installation
232 | → Why it matters (should this be at top?)
233 | → Manual setup
234 | → ...continues for 600 lines...
235 | 
236 | Total time to understand value: 10-15 minutes
237 | Decision made at: Line 300 (5-7 minutes)
238 | Information overload: High
239 | ```
240 | 
241 | ### **New README Journey:**
242 | 
243 | ```
244 | User arrives
245 | → Reads badges
246 | → Sees ONE LINE hook ("AI dreams of tool")
247 | → "Oh! I get it immediately."
248 | → Reads problem bullets (30 seconds)
249 | → "Yes, I have this problem!"
250 | → Sees six stories with Problem/Solution/Result
251 | → "Hmm, Story 2 about secrets sounds important..."
252 | → Clicks Story 2 link, reads 2-minute story
253 | → "This is brilliant! I want this."
254 | → Back to README, clicks Quick Start
255 | → Installs in 30 seconds or 2 minutes
256 | → Done!
257 | 
258 | Total time to understand value: 2-3 minutes
259 | Decision made at: After reading 1-2 stories
260 | Information overload: Low (they control depth)
261 | ```
262 | 
263 | **Key difference:** Stories let user control information depth. Want overview? Read summaries (30 sec). Want details? Read full story (2 min). Want everything? Read all six (12 min).
264 | 
265 | ---
266 | 
267 | ## 🎨 **Tone Comparison**
268 | 
269 | ### **Old:**
270 | > "NCP transforms N scattered MCP servers into 1 intelligent orchestrator using semantic vector search..."
271 | 
272 | - Technical-first
273 | - Feature-focused
274 | - Industry jargon
275 | - Assumes MCP knowledge
276 | 
277 | ### **New:**
278 | > "Your AI doesn't see your 50 tools. It dreams of the perfect tool, and NCP finds it instantly."
279 | 
280 | - Benefit-first
281 | - Problem-focused
282 | - Plain language
283 | - No assumptions
284 | 
285 | **Target audience shift:**
286 | - **Old:** Developers who already understand MCPs
287 | - **New:** Anyone who uses AI (then educates about MCPs)
288 | 
289 | ---
290 | 
291 | ## 💬 **Feedback Expectations**
292 | 
293 | ### **What users will say about OLD:**
294 | - "I don't understand what orchestrator means"
295 | - "Too much text, I'm not reading all that"
296 | - "Sounds technical, is this for developers only?"
297 | - "I get lost halfway through"
298 | 
299 | ### **What users will say about NEW:**
300 | - "Oh! The dream metaphor clicked instantly"
301 | - "I read Story 1 and immediately got it"
302 | - "This is the first MCP tool I actually understand"
303 | - "The stories make it memorable"
304 | 
305 | ---
306 | 
307 | ## ✅ **Migration Checklist**
308 | 
309 | To migrate from old to new README:
310 | 
311 | - [x] Create new story-first README
312 | - [x] Keep all information (nothing lost)
313 | - [x] Move deep dives to story pages
314 | - [x] Add story index at top
315 | - [x] Condense problem statement
316 | - [x] Simplify quick start
317 | - [x] Add testimonials section
318 | - [x] Add philosophy statement
319 | - [x] Organize "Learn More" by audience
320 | - [ ] Replace README.md with README.new.md
321 | - [ ] Update any links pointing to old sections
322 | - [ ] Get user feedback
323 | 
324 | ---
325 | 
326 | ## 🚀 **Expected Outcomes**
327 | 
328 | ### **Metrics we expect to improve:**
329 | 
330 | 1. **Time to understand value:**
331 |    - Old: 10-15 minutes
332 |    - New: 2-3 minutes
333 |    - **Improvement: 5x faster**
334 | 
335 | 2. **Conversion rate (understanding → installing):**
336 |    - Old: ~20% (many confused or overwhelmed)
337 |    - New: ~60% (clear value prop + easy start)
338 |    - **Improvement: 3x better**
339 | 
340 | 3. **Story sharing:**
341 |    - Old: "Check out NCP, it's an MCP orchestrator"
342 |    - New: "Check out NCP, your AI dreams of tools!"
343 |    - **Improvement: Viral potential** (memorable hook)
344 | 
345 | 4. **Support questions:**
346 |    - Old: "What does NCP do exactly?"
347 |    - New: "How do I configure X?" (they already understand WHY)
348 |    - **Improvement: Higher-quality questions**
349 | 
350 | ---
351 | 
352 | ## 🎯 **Recommendation**
353 | 
354 | **Replace old README with new story-first README.**
355 | 
356 | **Why:**
357 | - ✅ Same information, better organization
358 | - ✅ Faster time to value
359 | - ✅ Stories make features memorable
360 | - ✅ Aligns with story-first development workflow
361 | - ✅ Nothing is lost (all content preserved in stories)
362 | 
363 | **Risks:**
364 | - Some users might prefer old style (technical-first)
365 | - Links to old sections will need updating
366 | 
367 | **Mitigation:**
368 | - Keep old README as `README.old.md` for reference
369 | - Update CHANGELOG to note README restructure
370 | - Monitor GitHub issues for confusion
371 | - Iterate based on feedback
372 | 
373 | ---
374 | 
375 | **Ready to make the switch?** 🚀
376 | 
```

--------------------------------------------------------------------------------
/test/orchestrator-health-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for orchestrator health monitoring integration
  3 |  * Verifies that MCP failures are properly tracked in health monitor
  4 |  */
  5 | 
  6 | import { NCPOrchestrator } from '../src/orchestrator/ncp-orchestrator.js';
  7 | import { MCPHealthMonitor } from '../src/utils/health-monitor.js';
  8 | import { ProfileManager } from '../src/profiles/profile-manager.js';
  9 | import { jest } from '@jest/globals';
 10 | import { tmpdir } from 'os';
 11 | import { join } from 'path';
 12 | import { mkdirSync, writeFileSync, rmSync } from 'fs';
 13 | 
 14 | describe('Orchestrator Health Monitoring Integration', () => {
 15 |   let orchestrator: NCPOrchestrator;
 16 |   let tempDir: string;
 17 |   let mockProfilePath: string;
 18 | 
 19 |   beforeEach(() => {
 20 |     // Create temporary directory for test profiles
 21 |     tempDir = join(tmpdir(), `ncp-test-${Date.now()}`);
 22 |     mkdirSync(tempDir, { recursive: true });
 23 |     mockProfilePath = join(tempDir, 'profiles');
 24 |     mkdirSync(mockProfilePath, { recursive: true });
 25 | 
 26 |     // Mock the home directory to use our temp directory
 27 |     jest.spyOn(require('os'), 'homedir').mockReturnValue(tempDir);
 28 | 
 29 |     orchestrator = new NCPOrchestrator('test-profile');
 30 |   });
 31 | 
 32 |   afterEach(async () => {
 33 |     if (orchestrator) {
 34 |       await orchestrator.cleanup();
 35 |     }
 36 | 
 37 |     // Clean up temp directory
 38 |     try {
 39 |       rmSync(tempDir, { recursive: true, force: true });
 40 |     } catch (error) {
 41 |       // Ignore cleanup errors
 42 |     }
 43 |     jest.restoreAllMocks();
 44 |   });
 45 | 
 46 |   describe('MCP Discovery Health Tracking', () => {
 47 |     test('should track health during MCP discovery failures', async () => {
 48 |       // Create profile with invalid MCP
 49 |       const profileData = {
 50 |         mcpServers: {
 51 |           'failing-mcp': {
 52 |             command: 'npx',
 53 |             args: ['-y', '@non-existent/invalid-package']
 54 |           }
 55 |         }
 56 |       };
 57 | 
 58 |       const profileFile = join(mockProfilePath, 'test-profile.json');
 59 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
 60 | 
 61 |       // Spy on health monitor
 62 |       const healthMonitor = new MCPHealthMonitor();
 63 |       const markUnhealthySpy = jest.spyOn(healthMonitor, 'markUnhealthy');
 64 | 
 65 |       // Initialize orchestrator (this triggers discovery)
 66 |       await orchestrator.initialize();
 67 | 
 68 |       // Verify health monitor was called for the failing MCP
 69 |       // Note: The actual implementation might use a different health monitor instance
 70 |       // This tests the integration pattern rather than the exact spy calls
 71 | 
 72 |       // Check that no tools were discovered from the failing MCP
 73 |       const results = await orchestrator.find('', 10, false);
 74 | 
 75 |       // Should not contain any tools from failing-mcp
 76 |       const failingMcpTools = results.filter(r => r.mcpName === 'failing-mcp');
 77 |       expect(failingMcpTools).toHaveLength(0);
 78 |     });
 79 | 
 80 |     test('should handle mixed healthy and unhealthy MCPs', async () => {
 81 |       // Create profile with both valid and invalid MCPs
 82 |       const profileData = {
 83 |         mcpServers: {
 84 |           'valid-echo': {
 85 |             command: 'echo',
 86 |             args: ['hello']
 87 |           },
 88 |           'invalid-package': {
 89 |             command: 'npx',
 90 |             args: ['-y', '@definitely-does-not-exist/package']
 91 |           }
 92 |         }
 93 |       };
 94 | 
 95 |       const profileFile = join(mockProfilePath, 'test-profile.json');
 96 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
 97 | 
 98 |       await orchestrator.initialize();
 99 | 
100 |       // Should initialize without throwing even with some failing MCPs
101 |       const results = await orchestrator.find('', 10, false);
102 | 
103 |       // Results should not contain tools from failing MCPs
104 |       expect(results.every(r => r.mcpName !== 'invalid-package')).toBe(true);
105 |     });
106 | 
107 |     test('should track health during tool execution', async () => {
108 |       // Create profile with echo command for testing
109 |       const profileData = {
110 |         mcpServers: {
111 |           'test-mcp': {
112 |             command: 'echo',
113 |             args: ['test-response']
114 |           }
115 |         }
116 |       };
117 | 
118 |       const profileFile = join(mockProfilePath, 'test-profile.json');
119 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
120 | 
121 |       await orchestrator.initialize();
122 | 
123 |       // Try to run a tool (even if it doesn't exist, should track health)
124 |       const result = await orchestrator.run('test-mcp:non-existent-tool', {});
125 | 
126 |       // Should handle the execution attempt gracefully
127 |       expect(result).toBeDefined();
128 |       expect(result.success).toBeDefined();
129 |     });
130 |   });
131 | 
132 |   describe('Health Filter Integration', () => {
133 |     test('should filter out tools from unhealthy MCPs in find results', async () => {
134 |       // This tests the health filtering that happens in the find method
135 |       const profileData = {
136 |         mcpServers: {
137 |           'test-mcp': {
138 |             command: 'echo',
139 |             args: ['test']
140 |           }
141 |         }
142 |       };
143 | 
144 |       const profileFile = join(mockProfilePath, 'test-profile.json');
145 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
146 | 
147 |       await orchestrator.initialize();
148 | 
149 |       // Mock health monitor to mark MCP as unhealthy
150 |       const healthMonitor = new MCPHealthMonitor();
151 |       healthMonitor.markUnhealthy('test-mcp', 'Test error');
152 | 
153 |       // Find should respect health status
154 |       const results = await orchestrator.find('', 10, false);
155 | 
156 |       // Should handle health filtering without throwing
157 |       expect(Array.isArray(results)).toBe(true);
158 |     });
159 | 
160 |     test('should handle getAllResources with health filtering', async () => {
161 |       const profileData = {
162 |         mcpServers: {
163 |           'resource-mcp': {
164 |             command: 'echo',
165 |             args: ['resources']
166 |           }
167 |         }
168 |       };
169 | 
170 |       const profileFile = join(mockProfilePath, 'test-profile.json');
171 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
172 | 
173 |       await orchestrator.initialize();
174 | 
175 |       // Should handle resource retrieval with health filtering
176 |       const resources = await orchestrator.getAllResources();
177 |       expect(Array.isArray(resources)).toBe(true);
178 |     });
179 | 
180 |     test('should handle getAllPrompts with health filtering', async () => {
181 |       const profileData = {
182 |         mcpServers: {
183 |           'prompt-mcp': {
184 |             command: 'echo',
185 |             args: ['prompts']
186 |           }
187 |         }
188 |       };
189 | 
190 |       const profileFile = join(mockProfilePath, 'test-profile.json');
191 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
192 | 
193 |       await orchestrator.initialize();
194 | 
195 |       // Should handle prompt retrieval with health filtering
196 |       const prompts = await orchestrator.getAllPrompts();
197 |       expect(Array.isArray(prompts)).toBe(true);
198 |     });
199 |   });
200 | 
201 |   describe('Error Handling and Recovery', () => {
202 |     test('should handle complete discovery failure gracefully', async () => {
203 |       // Create profile with only failing MCPs
204 |       const profileData = {
205 |         mcpServers: {
206 |           'fail1': { command: 'non-existent-command', args: [] },
207 |           'fail2': { command: '/invalid/path', args: [] },
208 |           'fail3': { command: 'npx', args: ['-y', '@invalid/package'] }
209 |         }
210 |       };
211 | 
212 |       const profileFile = join(mockProfilePath, 'test-profile.json');
213 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
214 | 
215 |       // Should initialize without throwing even with all MCPs failing
216 |       await expect(orchestrator.initialize()).resolves.toBeUndefined();
217 | 
218 |       // Should return empty results gracefully
219 |       const results = await orchestrator.find('test', 10, false);
220 |       expect(Array.isArray(results)).toBe(true);
221 |     });
222 | 
223 |     test('should handle profile loading errors', async () => {
224 |       // Don't create profile file to trigger error
225 | 
226 |       // Should handle missing profile gracefully
227 |       await expect(orchestrator.initialize()).resolves.toBeUndefined();
228 | 
229 |       const results = await orchestrator.find('test', 10, false);
230 |       expect(Array.isArray(results)).toBe(true);
231 |       expect(results).toHaveLength(0);
232 |     });
233 | 
234 |     test('should track connection failures during tool execution', async () => {
235 |       const profileData = {
236 |         mcpServers: {
237 |           'connection-test': {
238 |             command: 'sleep',
239 |             args: ['1'] // Short sleep that should succeed initially
240 |           }
241 |         }
242 |       };
243 | 
244 |       const profileFile = join(mockProfilePath, 'test-profile.json');
245 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
246 | 
247 |       await orchestrator.initialize();
248 | 
249 |       // Attempt tool execution (will likely fail but should be tracked)
250 |       const result = await orchestrator.run('connection-test:some-tool', {});
251 | 
252 |       // Should handle execution failure gracefully
253 |       expect(result).toBeDefined();
254 |       expect(typeof result.success).toBe('boolean');
255 |     });
256 |   });
257 | 
258 |   describe('Health Status Integration', () => {
259 |     test('should maintain health status across orchestrator lifecycle', async () => {
260 |       const profileData = {
261 |         mcpServers: {
262 |           'lifecycle-test': {
263 |             command: 'echo',
264 |             args: ['lifecycle']
265 |           }
266 |         }
267 |       };
268 | 
269 |       const profileFile = join(mockProfilePath, 'test-profile.json');
270 |       writeFileSync(profileFile, JSON.stringify(profileData, null, 2));
271 | 
272 |       // Initialize and use orchestrator
273 |       await orchestrator.initialize();
274 | 
275 |       // Perform some operations
276 |       await orchestrator.find('test', 5, false);
277 |       await orchestrator.getAllResources();
278 | 
279 |       // Cleanup should work without issues
280 |       await orchestrator.cleanup();
281 | 
282 |       // Should be able to create new instance
283 |       const newOrchestrator = new NCPOrchestrator('test-profile');
284 |       await newOrchestrator.initialize();
285 |       await newOrchestrator.cleanup();
286 |     });
287 |   });
288 | });
```

--------------------------------------------------------------------------------
/docs/guides/testing.md:
--------------------------------------------------------------------------------

```markdown
  1 | # NCP Testing Guide
  2 | 
  3 | ## Overview
  4 | 
  5 | This document outlines comprehensive testing strategies for NCP to ensure the MCP interface works correctly and there are no regressions before release.
  6 | 
  7 | ## Test Categories
  8 | 
  9 | ### 1. Automated Unit Tests ✅ (Existing)
 10 | **Status**: Currently passing with comprehensive coverage
 11 | 
 12 | **What's Covered**:
 13 | - Core orchestrator functionality
 14 | - Discovery engine semantic search
 15 | - Health monitoring
 16 | - Tool schema parsing
 17 | - Cache management
 18 | - Error handling
 19 | - CLI command functionality
 20 | 
 21 | **Run Tests**:
 22 | ```bash
 23 | npm test
 24 | ```
 25 | 
 26 | ### 2. MCP Interface Integration Tests
 27 | 
 28 | #### 2.1 MCP Server Mode Testing
 29 | **Purpose**: Verify NCP works correctly as an MCP server for AI clients
 30 | 
 31 | **Test Commands**:
 32 | ```bash
 33 | # Test MCP server mode startup
 34 | node dist/index.js --profile all
 35 | 
 36 | # Should output valid MCP initialization and wait for stdin
 37 | # Ctrl+C to exit after verifying initialization
 38 | ```
 39 | 
 40 | **Expected Behavior**:
 41 | - Clean startup with no errors
 42 | - Proper MCP protocol initialization
 43 | - Responsive to JSON-RPC requests
 44 | 
 45 | #### 2.2 Tool Discovery Testing
 46 | **Purpose**: Verify semantic discovery works end-to-end
 47 | 
 48 | **Setup**:
 49 | ```bash
 50 | # Add test MCPs
 51 | ncp add filesystem npx @modelcontextprotocol/server-filesystem /tmp
 52 | ncp add memory npx @modelcontextprotocol/server-memory
 53 | ```
 54 | 
 55 | **Test Commands**:
 56 | ```bash
 57 | # Test discovery functionality
 58 | ncp find "file operations"
 59 | ncp find "memory tools"
 60 | ncp find "read"
 61 | ncp find ""  # Should handle empty query gracefully
 62 | ```
 63 | 
 64 | **Expected Results**:
 65 | - Relevant tools returned with confidence scores
 66 | - Proper formatting and descriptions
 67 | - No crashes or errors
 68 | - Reasonable response times (<2 seconds)
 69 | 
 70 | #### 2.3 Tool Execution Testing
 71 | **Purpose**: Verify tool execution through NCP interface
 72 | 
 73 | **Test Commands**:
 74 | ```bash
 75 | # Test tool execution with parameters
 76 | echo "test content" > /tmp/ncp-test.txt
 77 | ncp run filesystem:read_file --params '{"path": "/tmp/ncp-test.txt"}'
 78 | 
 79 | # Test tool execution without parameters
 80 | ncp run memory:create_entities --params '{}'
 81 | 
 82 | # Test invalid tool execution
 83 | ncp run nonexistent:tool --params '{}'
 84 | ```
 85 | 
 86 | **Expected Results**:
 87 | - Successful execution returns proper results
 88 | - Error handling for invalid tools/parameters
 89 | - Clear error messages for debugging
 90 | 
 91 | ### 3. Configuration Management Testing
 92 | 
 93 | #### 3.1 Import Functionality Testing
 94 | **Purpose**: Verify the simplified import interface works correctly
 95 | 
 96 | **Test Scenarios**:
 97 | 
 98 | **Clipboard Import**:
 99 | ```bash
100 | # Test 1: Multiple MCPs
101 | echo '{"filesystem": {"command": "npx", "args": ["@modelcontextprotocol/server-filesystem", "/tmp"]}, "memory": {"command": "npx", "args": ["@modelcontextprotocol/server-memory"]}}' | pbcopy
102 | ncp config import --dry-run
103 | 
104 | # Test 2: Single MCP (should prompt for name)
105 | echo '{"command": "npx", "args": ["@modelcontextprotocol/server-memory"]}' | pbcopy
106 | echo "test-memory" | ncp config import --dry-run
107 | 
108 | # Test 3: Empty clipboard
109 | echo "" | pbcopy
110 | ncp config import
111 | ```
112 | 
113 | **File Import**:
114 | ```bash
115 | # Create test config file
116 | cat > test-config.json << EOF
117 | {
118 |   "filesystem": {
119 |     "command": "npx",
120 |     "args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
121 |   }
122 | }
123 | EOF
124 | 
125 | # Test file import
126 | ncp config import test-config.json --dry-run
127 | rm test-config.json
128 | ```
129 | 
130 | **Expected Results**:
131 | - JSON displayed in highlighted box
132 | - Correct parsing and validation
133 | - Proper error messages for invalid JSON/empty clipboard
134 | - Successful import with detailed feedback
135 | 
136 | #### 3.2 Profile Management Testing
137 | **Purpose**: Verify profile system works correctly
138 | 
139 | **Test Commands**:
140 | ```bash
141 | # Test profile creation and management
142 | ncp add test-server echo --profiles test-profile
143 | ncp list --profile test-profile
144 | ncp remove test-server --profiles test-profile
145 | ncp list --profile test-profile  # Should be empty
146 | 
147 | # Test default profile
148 | ncp list --profile all
149 | ```
150 | 
151 | ### 4. Client Integration Testing
152 | 
153 | #### 4.1 Claude Desktop Integration Test
154 | **Purpose**: Verify NCP works with Claude Desktop
155 | 
156 | **Manual Test Steps**:
157 | 1. Add NCP to Claude Desktop config:
158 | ```json
159 | {
160 |   "mcpServers": {
161 |     "ncp": {
162 |       "command": "ncp",
163 |       "args": ["--profile", "all"]
164 |     }
165 |   }
166 | }
167 | ```
168 | 
169 | 2. Restart Claude Desktop
170 | 3. Test in Claude Desktop:
171 |    - Ask: "What tools are available for file operations?"
172 |    - Ask: "Read the file /tmp/ncp-test.txt"
173 |    - Verify NCP's `find` and `run` tools appear
174 |    - Verify tool execution works correctly
175 | 
176 | **Expected Results**:
177 | - NCP appears as available MCP server
178 | - `find` and `run` tools visible to Claude
179 | - Semantic discovery works through Claude interface
180 | - Tool execution successful
181 | 
182 | #### 4.2 VS Code Integration Test (If Available)
183 | **Purpose**: Verify NCP works with VS Code MCP extension
184 | 
185 | **Test Steps**:
186 | 1. Configure NCP in VS Code settings
187 | 2. Test tool discovery and execution
188 | 3. Verify no conflicts with other MCP servers
189 | 
190 | ### 5. Performance & Reliability Testing
191 | 
192 | #### 5.1 Load Testing
193 | **Purpose**: Verify NCP handles multiple requests correctly
194 | 
195 | **Test Script**:
196 | ```bash
197 | # Concurrent discovery tests
198 | for i in {1..10}; do
199 |   ncp find "file tools" &
200 | done
201 | wait
202 | 
203 | # Sequential execution tests
204 | for i in {1..5}; do
205 |   ncp run memory:create_entities --params '{"entities": ["test'$i'"]}'
206 | done
207 | ```
208 | 
209 | **Expected Results**:
210 | - No crashes under concurrent load
211 | - Consistent response times
212 | - Proper resource cleanup
213 | 
214 | #### 5.2 Memory & Resource Testing
215 | **Purpose**: Verify no memory leaks or resource issues
216 | 
217 | **Test Commands**:
218 | ```bash
219 | # Long-running discovery tests
220 | for i in {1..100}; do
221 |   ncp find "test query $i" > /dev/null
222 | done
223 | 
224 | # Monitor memory usage during test
225 | # Should remain stable, not continuously grow
226 | ```
227 | 
228 | ### 6. Error Handling & Edge Cases
229 | 
230 | #### 6.1 MCP Server Failure Testing
231 | **Purpose**: Verify graceful handling of MCP server failures
232 | 
233 | **Test Steps**:
234 | 1. Add a failing MCP server:
235 | ```bash
236 | ncp add failing-server nonexistent-command
237 | ```
238 | 
239 | 2. Test discovery with failing server:
240 | ```bash
241 | ncp find "tools"  # Should still return results from healthy servers
242 | ncp list --depth 1  # Should show health status
243 | ```
244 | 
245 | **Expected Results**:
246 | - Healthy servers continue working
247 | - Failed servers marked as unhealthy
248 | - Clear error messages for debugging
249 | 
250 | #### 6.2 Invalid Input Testing
251 | **Purpose**: Verify robust error handling
252 | 
253 | **Test Commands**:
254 | ```bash
255 | # Invalid JSON in tool execution
256 | ncp run filesystem:read_file --params 'invalid json'
257 | 
258 | # Non-existent tools
259 | ncp run fake:tool --params '{}'
260 | 
261 | # Invalid parameters
262 | ncp run filesystem:read_file --params '{"invalid": "parameter"}'
263 | ```
264 | 
265 | **Expected Results**:
266 | - Clear error messages
267 | - No crashes or undefined behavior
268 | - Helpful suggestions for fixing issues
269 | 
270 | ### 7. Regression Testing
271 | 
272 | #### 7.1 Feature Regression Tests
273 | **Purpose**: Ensure existing functionality still works after changes
274 | 
275 | **Critical Paths to Test**:
276 | 1. Basic MCP server startup
277 | 2. Tool discovery with various queries
278 | 3. Tool execution with parameters
279 | 4. Configuration import/export
280 | 5. Profile management
281 | 6. Health monitoring
282 | 
283 | #### 7.2 CLI Regression Tests
284 | **Purpose**: Verify CLI commands still work correctly
285 | 
286 | **Commands to Test**:
287 | ```bash
288 | ncp --help
289 | ncp find --help
290 | ncp config --help
291 | ncp list --help
292 | ncp add --help
293 | ncp run --help
294 | ```
295 | 
296 | ### 8. Release Verification Checklist
297 | 
298 | #### Pre-Release Checklist ✅
299 | - [ ] All unit tests passing
300 | - [ ] MCP server mode starts cleanly
301 | - [ ] Tool discovery returns relevant results
302 | - [ ] Tool execution works correctly
303 | - [ ] Import functionality works (clipboard & file)
304 | - [ ] Profile management works
305 | - [ ] Claude Desktop integration verified
306 | - [ ] Error handling graceful
307 | - [ ] Performance acceptable
308 | - [ ] Documentation updated
309 | - [ ] CLI help accurate
310 | 
311 | #### Manual Integration Test Script
312 | ```bash
313 | #!/bin/bash
314 | # Quick integration test script
315 | 
316 | echo "🧪 Starting NCP Integration Tests..."
317 | 
318 | # 1. Basic setup
319 | echo "📦 Testing basic setup..."
320 | npm run build
321 | echo '{"test": {"command": "echo", "args": ["hello"]}}' | pbcopy
322 | ncp config import --dry-run
323 | 
324 | # 2. Tool discovery
325 | echo "🔍 Testing tool discovery..."
326 | ncp find "file"
327 | ncp find "memory"
328 | 
329 | # 3. Configuration
330 | echo "⚙️  Testing configuration..."
331 | ncp list
332 | ncp config validate
333 | 
334 | # 4. Server mode (5 second test)
335 | echo "🖥️  Testing MCP server mode (5 seconds)..."
336 | timeout 5s node dist/index.js --profile all || echo "Server mode test completed"
337 | 
338 | echo "✅ Integration tests completed!"
339 | ```
340 | 
341 | ### 9. Automated Testing in CI/CD
342 | 
343 | #### GitHub Actions Test Matrix
344 | Consider adding these test scenarios to CI:
345 | - Node.js versions: 18, 20, 22
346 | - Platforms: Ubuntu, macOS, Windows
347 | - Profile configurations: empty, single MCP, multiple MCPs
348 | - Import scenarios: clipboard, file, edge cases
349 | 
350 | #### Performance Benchmarks
351 | Track these metrics over time:
352 | - Tool discovery response time
353 | - Memory usage during operations
354 | - Startup time
355 | - Cache loading performance
356 | 
357 | ### 10. User Acceptance Testing
358 | 
359 | #### Beta Testing Scenarios
360 | 1. **New User Onboarding**:
361 |    - Install NCP globally
362 |    - Import existing Claude Desktop config
363 |    - Test discovery and execution
364 | 
365 | 2. **Power User Workflows**:
366 |    - Multiple profiles setup
367 |    - Complex tool queries
368 |    - Bulk operations
369 | 
370 | 3. **Edge Case Scenarios**:
371 |    - Large number of MCPs
372 |    - Network issues
373 |    - Corrupted configurations
374 | 
375 | ## Running the Full Test Suite
376 | 
377 | ```bash
378 | # 1. Unit tests
379 | npm test
380 | 
381 | # 2. Build verification
382 | npm run build
383 | 
384 | # 3. Basic integration tests
385 | ./test-integration.sh  # Create the script above
386 | 
387 | # 4. Manual Claude Desktop test
388 | # Follow section 4.1 steps
389 | 
390 | # 5. Performance spot check
391 | time ncp find "test"
392 | time ncp run memory:create_entities --params '{}'
393 | ```
394 | 
395 | ## Conclusion
396 | 
397 | This comprehensive testing strategy ensures:
398 | - ✅ **No Regressions**: Existing functionality continues working
399 | - ✅ **MCP Protocol Compliance**: Proper MCP server behavior
400 | - ✅ **User Experience**: Import and discovery features work smoothly
401 | - ✅ **Reliability**: Graceful error handling and recovery
402 | - ✅ **Performance**: Acceptable response times and resource usage
403 | 
404 | Execute these tests before any release to ensure NCP works correctly as both an MCP server and orchestration layer.
```

--------------------------------------------------------------------------------
/test/helpers/mock-server-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Helper to manage mock server processes for tests
  3 |  */
  4 | import { spawn, ChildProcess } from 'child_process';
  5 | import { promisify } from 'util';
  6 | import { join } from 'path';
  7 | 
  8 | const wait = promisify(setTimeout);
  9 | 
 10 | /**
 11 |  * Interface to track server readiness state
 12 |  */
 13 | interface ServerState {
 14 |   sawStdout: boolean;
 15 |   sawStderr: boolean;
 16 |   sawReady: boolean;
 17 |   sawError: boolean;
 18 |   lastError: string;
 19 |   outputLog: string[];
 20 | }
 21 | 
 22 | /**
 23 |  * Manages mock server processes for testing
 24 |  */
 25 | export class MockServerManager {
 26 |   private readonly servers: Map<string, ChildProcess>;
 27 |   private readonly timeouts: Set<NodeJS.Timeout>;
 28 |   private readonly MAX_RETRIES = 5;
 29 |   private readonly RETRY_DELAY = 3000;
 30 |   private readonly TIMEOUT_MS = 10000;
 31 | 
 32 |   constructor() {
 33 |     this.servers = new Map();
 34 |     this.timeouts = new Set();
 35 |   }
 36 | 
 37 |   /**
 38 |    * Register a timeout so we can clean it up later
 39 |    */
 40 |   private trackTimeout(timeout: NodeJS.Timeout): NodeJS.Timeout {
 41 |     this.timeouts.add(timeout);
 42 |     return timeout;
 43 |   }
 44 | 
 45 |   /**
 46 |    * Clear a specific timeout and remove it from tracking
 47 |    */
 48 |   private clearTrackedTimeout(timeout: NodeJS.Timeout): void {
 49 |     clearTimeout(timeout);
 50 |     this.timeouts.delete(timeout);
 51 |   }
 52 | 
 53 |   /**
 54 |    * Clear all tracked timeouts
 55 |    */
 56 |   private clearAllTimeouts(): void {
 57 |     for (const timeout of this.timeouts) {
 58 |       clearTimeout(timeout);
 59 |     }
 60 |     this.timeouts.clear();
 61 |   }
 62 | 
 63 |   async startServer(name: string, serverScript: string): Promise<void> {
 64 |     if (this.servers.has(name)) {
 65 |       return; // Server already running
 66 |     }
 67 | 
 68 |     // Retry loop for starting server
 69 |     for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
 70 |       try {
 71 |         console.error(`Starting ${name} server (attempt ${attempt}/${this.MAX_RETRIES})...`);
 72 |         
 73 |         const scriptPath = join(__dirname, '..', 'mock-mcps', serverScript);
 74 |         console.error(`[DEBUG] Starting server from path: ${scriptPath}`);
 75 |         
 76 |         const serverProcess = spawn('node', [scriptPath], {
 77 |           stdio: ['pipe', 'pipe', 'pipe'],
 78 |           detached: false,
 79 |           env: {
 80 |             ...process.env,
 81 |             NODE_ENV: 'test',
 82 |             DEBUG: '*',
 83 |             FORCE_COLOR: '0'
 84 |           }
 85 |         });
 86 |         
 87 |         // Handle process errors
 88 |         serverProcess.on('spawn', () => {
 89 |           console.error(`[DEBUG] Process spawned for ${name} server with pid ${serverProcess.pid}`);
 90 |         });
 91 | 
 92 |         // Wait for server to signal it's ready
 93 |         await new Promise<void>((resolve, reject) => {
 94 |           const state: ServerState = {
 95 |             sawStdout: false,
 96 |             sawStderr: false,
 97 |             sawReady: false,
 98 |             sawError: false,
 99 |             lastError: '',
100 |             outputLog: []
101 |           };
102 | 
103 |           const logOutput = (type: string, msg: string) => {
104 |             state.outputLog.push(`[${type}] ${msg.trim()}`);
105 |             if (state.outputLog.length > 100) {
106 |               state.outputLog.shift();
107 |             }
108 |           };
109 | 
110 |           // Set timeout for server startup
111 |           const readyTimeout = this.trackTimeout(setTimeout(() => {
112 |             // Print output log for diagnosis
113 |             console.error('Recent output:', state.outputLog.join('\n'));
114 |             
115 |             // Log timeout status
116 |             console.error(`Timeout status for ${name} server:`, {
117 |               pid: serverProcess.pid,
118 |               ...state,
119 |               uptime: process.uptime(),
120 |               memory: process.memoryUsage()
121 |             });
122 |             
123 |             if (!serverProcess.killed) {
124 |               console.error(`Killing ${name} server (pid: ${serverProcess.pid})...`);
125 |               try {
126 |                 serverProcess.kill('SIGTERM');
127 |                 // Force kill after 1s if SIGTERM doesn't work
128 |                 this.trackTimeout(setTimeout(() => {
129 |                   if (!serverProcess.killed) {
130 |                     console.error(`Force killing ${name} server...`);
131 |                     try {
132 |                       serverProcess.kill('SIGKILL');
133 |                     } catch (err) {
134 |                       // Ignore kill errors
135 |                     }
136 |                   }
137 |                 }, 1000));
138 |               } catch (err) {
139 |                 console.error(`Error killing ${name} server:`, err);
140 |               }
141 |             }
142 |             reject(new Error(`Timeout waiting for ${name} server to start - ${state.lastError}`));
143 |           }, this.TIMEOUT_MS));
144 | 
145 |           // Enhanced stdout handling with buffering
146 |           let stdoutBuffer = '';
147 |           serverProcess.stdout?.on('data', (data: Buffer) => {
148 |             state.sawStdout = true;
149 |             const output = data.toString();
150 |             stdoutBuffer += output;
151 |             logOutput('STDOUT', output);
152 |             
153 |             // Check for ready signal in accumulated buffer
154 |             if (stdoutBuffer.includes(`[READY] ${name}`)) {
155 |               state.sawReady = true;
156 |               console.error(`[DEBUG] ${name} server ready signal received in stdout buffer (attempt ${attempt}/${this.MAX_RETRIES})`);
157 |               this.clearTrackedTimeout(readyTimeout);
158 |               this.servers.set(name, serverProcess);
159 |               resolve();
160 |             }
161 |             
162 |             // Check for various error conditions
163 |             if (output.includes('Failed to load MCP SDK dependencies')) {
164 |               state.sawError = true;
165 |               state.lastError = 'Failed to load SDK dependencies';
166 |               console.error(`[ERROR] ${name} server failed to load dependencies (attempt ${attempt}/${this.MAX_RETRIES})`);
167 |               this.clearTrackedTimeout(readyTimeout);
168 |               serverProcess.kill('SIGTERM');
169 |               reject(new Error('Server failed to load dependencies'));
170 |               return;
171 |             }
172 |             
173 |             if (output.includes('Error:') || output.includes('Error stack:') || output.includes('Failed to')) {
174 |               state.sawError = true;
175 |               state.lastError = output.trim();
176 |             }
177 |           });
178 | 
179 |           // Enhanced stderr handling with buffering
180 |           let stderrBuffer = '';
181 |           serverProcess.stderr?.on('data', (data: Buffer) => {
182 |             state.sawStderr = true;
183 |             const output = data.toString();
184 |             stderrBuffer += output;
185 |             logOutput('STDERR', output);
186 |             
187 |             // Collect error messages
188 |             if (output.includes('Error:') || output.includes('Failed to')) {
189 |               state.sawError = true;
190 |               state.lastError = output.trim();
191 |             }
192 |             
193 |             // Check for ready signal in accumulated buffer
194 |             if (stderrBuffer.includes(`[READY] ${name}`)) {
195 |               state.sawReady = true;
196 |               console.error(`[DEBUG] ${name} server ready signal received in stderr buffer`);
197 |               this.clearTrackedTimeout(readyTimeout);
198 |               this.servers.set(name, serverProcess);
199 |               resolve();
200 |             }
201 |           });
202 | 
203 |           // Set up error handling
204 |           serverProcess.on('error', (err: Error) => {
205 |             this.clearTrackedTimeout(readyTimeout);
206 |             console.error(`Error in mock server ${name}:`, err);
207 |             console.error(`Error status for ${name}:`, {
208 |               pid: serverProcess.pid,
209 |               ...state
210 |             });
211 |             reject(err);
212 |           });
213 | 
214 |           // Set up exit handling
215 |           serverProcess.on('exit', (code: number | null) => {
216 |             console.error(`Mock server ${name} (pid: ${serverProcess.pid}) exited with code ${code}`, {
217 |               ...state
218 |             });
219 |             this.servers.delete(name);
220 |           });
221 |         });
222 | 
223 |         // Successfully started server
224 |         return;
225 |       } catch (err) {
226 |         const errorMessage = err instanceof Error ? err.message : 'Unknown error';
227 |         console.error(`Attempt ${attempt} failed:`, errorMessage);
228 |         if (attempt < this.MAX_RETRIES) {
229 |           // Wait before retrying using Jest's fake timer
230 |           await wait(this.RETRY_DELAY);
231 |         }
232 |       }
233 |     }
234 | 
235 |     // All retries failed
236 |     throw new Error(`Failed to start ${name} server after ${this.MAX_RETRIES} attempts`);
237 |   }
238 | 
239 |   async stopAll(): Promise<void> {
240 |     console.error('[DEBUG] Stopping all servers...');
241 |     
242 |     // Clean up all timeouts first
243 |     this.clearAllTimeouts();
244 |     
245 |     // Give processes a chance to clean up gracefully
246 |     for (const [name, serverProcess] of this.servers.entries()) {
247 |       try {
248 |         console.error(`[DEBUG] Sending SIGTERM to ${name} server (pid: ${serverProcess.pid})...`);
249 |         // Send SIGTERM first to allow clean shutdown
250 |         serverProcess.kill('SIGTERM');
251 |         
252 |         // Remove from map immediately to prevent duplicate cleanup
253 |         this.servers.delete(name);
254 |         
255 |         console.error(`[DEBUG] Successfully sent SIGTERM to ${name} server`);
256 |       } catch (err) {
257 |         console.error(`[ERROR] Error stopping server ${name}:`, err);
258 |       }
259 |     }
260 | 
261 |     // Wait longer for graceful shutdown
262 |     console.error('[DEBUG] Waiting for processes to exit gracefully...');
263 |     await wait(1000);
264 | 
265 |     // Force kill any remaining processes
266 |     const remainingServers = new Map(this.servers);
267 |     for (const [name, serverProcess] of remainingServers.entries()) {
268 |       try {
269 |         console.error(`[DEBUG] Force killing ${name} server (pid: ${serverProcess.pid})...`);
270 |         // Kill process group to ensure child processes are terminated
271 |         process.kill(-serverProcess.pid!, 'SIGKILL');
272 |         this.servers.delete(name);
273 |         console.error(`[DEBUG] Successfully killed ${name} server`);
274 |       } catch (err: any) {
275 |         // Only log if it's not a "no such process" error
276 |         if (err instanceof Error && !err.message.includes('ESRCH')) {
277 |           console.error(`[ERROR] Error force killing server ${name}:`, err);
278 |         }
279 |       }
280 |     }
281 | 
282 |     // Clear any remaining entries and wait for final cleanup
283 |     console.error('[DEBUG] Cleaning up server references...');
284 |     this.servers.clear();
285 |     await wait(100);
286 |     console.error('[DEBUG] Server cleanup complete');
287 |   }
288 | 
289 |   /**
290 |    * Get all currently running servers
291 |    */
292 |   getAllServers(): Map<string, ChildProcess> {
293 |     return new Map(this.servers);
294 |   }
295 | }
```

--------------------------------------------------------------------------------
/test/curated-ecosystem-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Curated Ecosystem Validation Tests
  3 |  *
  4 |  * Tests NCP's discovery capabilities with our high-quality curated MCP ecosystem
  5 |  */
  6 | 
  7 | import { DiscoveryEngine } from '../src/discovery/engine.js';
  8 | import fs from 'fs/promises';
  9 | import path from 'path';
 10 | 
 11 | describe.skip('Curated Ecosystem Validation', () => {
 12 |   let engine: DiscoveryEngine;
 13 | 
 14 |   beforeAll(async () => {
 15 |     engine = new DiscoveryEngine();
 16 |     await engine.initialize();
 17 | 
 18 |     // Load actual curated ecosystem profile
 19 |     const profilePath = path.join(process.cwd(), 'profiles', 'curated-mcp-ecosystem.json');
 20 |     const profile = JSON.parse(await fs.readFile(profilePath, 'utf-8'));
 21 | 
 22 |     // Extract tools from the ecosystem builder directory
 23 |     const ecosystemBuilderPath = path.resolve('../ncp-ecosystem-builder');
 24 |     const clonesDir = path.join(ecosystemBuilderPath, 'generated/clones');
 25 | 
 26 |     try {
 27 |       const files = await fs.readdir(clonesDir);
 28 |       const mcpFiles = files.filter(f => f.endsWith('.js'));
 29 | 
 30 |       // Index each MCP
 31 |       for (const file of mcpFiles) {
 32 |         const mcpName = file.replace('-curated-dummy.js', '').replace('-dummy.js', '');
 33 | 
 34 |         // Import the MCP to get its tools
 35 |         const mcpPath = path.join(clonesDir, file);
 36 |         try {
 37 |           const { tools } = await import(mcpPath);
 38 |           if (tools && tools.length > 0) {
 39 |             await engine['ragEngine'].indexMCP(mcpName, tools);
 40 |           }
 41 |         } catch (error: any) {
 42 |           // Fallback to profile descriptions
 43 |           const serverInfo = profile.mcpServers[mcpName] as any;
 44 |           if (serverInfo) {
 45 |             await engine['ragEngine'].indexMCP(mcpName, [{
 46 |               name: mcpName,
 47 |               description: serverInfo.description
 48 |             }]);
 49 |           }
 50 |         }
 51 |       }
 52 |     } catch (error) {
 53 |       console.warn('Could not load ecosystem builder MCPs, using profile fallback');
 54 | 
 55 |       // Fallback: use profile information only
 56 |       for (const [mcpName, serverInfo] of Object.entries(profile.mcpServers)) {
 57 |         await engine['ragEngine'].indexMCP(mcpName, [{
 58 |           name: mcpName,
 59 |           description: (serverInfo as any).description
 60 |         }]);
 61 |       }
 62 |     }
 63 |   });
 64 | 
 65 |   describe('Database Discovery', () => {
 66 |     it('finds PostgreSQL tools for database operations', async () => {
 67 |       const results = await engine.findRelevantTools(
 68 |         'I need to execute SQL queries on PostgreSQL database',
 69 |         8
 70 |       );
 71 | 
 72 |       expect(results.length).toBeGreaterThan(0);
 73 | 
 74 |       const pgTool = results.find((t: any) =>
 75 |         t.name.includes('postgres') ||
 76 |         t.name.includes('execute_query') ||
 77 |         t.description?.toLowerCase().includes('postgresql') ||
 78 |         t.description?.toLowerCase().includes('postgres')
 79 |       );
 80 |       expect(pgTool).toBeDefined();
 81 |       expect(results.indexOf(pgTool!)).toBeLessThan(5);
 82 |     });
 83 | 
 84 |     it('finds appropriate database tools for different databases', async () => {
 85 |       const results = await engine.findRelevantTools(
 86 |         'I want to work with SQLite lightweight database for my application',
 87 |         8
 88 |       );
 89 | 
 90 |       expect(results.length).toBeGreaterThan(0);
 91 | 
 92 |       // Should find SQLite for lightweight usage
 93 |       const sqliteTool = results.find((t: any) =>
 94 |         t.name.includes('sqlite') ||
 95 |         t.description?.toLowerCase().includes('sqlite') ||
 96 |         t.description?.toLowerCase().includes('lightweight')
 97 |       );
 98 |       expect(sqliteTool).toBeDefined();
 99 |     });
100 |   });
101 | 
102 |   describe('Cloud & Infrastructure Discovery', () => {
103 |     it('finds AWS tools for cloud deployment', async () => {
104 |       const results = await engine.findRelevantTools(
105 |         'I need to deploy a server instance on AWS',
106 |         6
107 |       );
108 | 
109 |       expect(results.length).toBeGreaterThan(0);
110 | 
111 |       const awsTool = results.find((t: any) =>
112 |         t.name.includes('aws') || t.name.includes('launch_ec2') || t.description?.toLowerCase().includes('aws')
113 |       );
114 |       expect(awsTool).toBeDefined();
115 |       expect(results.indexOf(awsTool!)).toBeLessThan(5);
116 |     });
117 | 
118 |     it('finds Docker tools for containerization', async () => {
119 |       const results = await engine.findRelevantTools(
120 |         'I want to containerize my application with Docker containers',
121 |         8
122 |       );
123 | 
124 |       expect(results.length).toBeGreaterThan(0);
125 | 
126 |       const dockerTool = results.find((t: any) =>
127 |         t.name.includes('docker') ||
128 |         t.description?.toLowerCase().includes('docker') ||
129 |         t.description?.toLowerCase().includes('container')
130 |       );
131 |       expect(dockerTool).toBeDefined();
132 |       expect(results.indexOf(dockerTool!)).toBeLessThan(6);
133 |     });
134 |   });
135 | 
136 |   describe('Developer Tools Discovery', () => {
137 |     it('finds GitHub tools for repository management', async () => {
138 |       const results = await engine.findRelevantTools(
139 |         'I want to create a new GitHub repository for my project',
140 |         6
141 |       );
142 | 
143 |       expect(results.length).toBeGreaterThan(0);
144 | 
145 |       const githubTool = results.find((t: any) =>
146 |         t.name.includes('github') || t.name.includes('create_repository') || t.description?.toLowerCase().includes('github')
147 |       );
148 |       expect(githubTool).toBeDefined();
149 |       expect(results.indexOf(githubTool!)).toBeLessThan(4);
150 |     });
151 | 
152 |     it('finds file system tools for file operations', async () => {
153 |       const results = await engine.findRelevantTools(
154 |         'I need to read configuration files from disk',
155 |         6
156 |       );
157 | 
158 |       expect(results.length).toBeGreaterThan(0);
159 | 
160 |       const fsTool = results.find((t: any) =>
161 |         t.name.includes('filesystem') || t.name.includes('read_file') || t.description?.toLowerCase().includes('filesystem')
162 |       );
163 |       expect(fsTool).toBeDefined();
164 |       expect(results.indexOf(fsTool!)).toBeLessThan(5);
165 |     });
166 |   });
167 | 
168 |   describe('Communication Tools Discovery', () => {
169 |     it('finds Slack tools for team messaging', async () => {
170 |       const results = await engine.findRelevantTools(
171 |         'I want to send a notification to my team on Slack',
172 |         6
173 |       );
174 | 
175 |       expect(results.length).toBeGreaterThan(0);
176 | 
177 |       const slackTool = results.find((t: any) =>
178 |         t.name.includes('slack') || t.name.includes('send_message') || t.description?.toLowerCase().includes('slack')
179 |       );
180 |       expect(slackTool).toBeDefined();
181 |       expect(results.indexOf(slackTool!)).toBeLessThan(4);
182 |     });
183 |   });
184 | 
185 |   describe('AI/ML Tools Discovery', () => {
186 |     it('finds OpenAI tools for LLM operations', async () => {
187 |       const results = await engine.findRelevantTools(
188 |         'I need to generate text using OpenAI API',
189 |         6
190 |       );
191 | 
192 |       expect(results.length).toBeGreaterThan(0);
193 | 
194 |       const openaiTool = results.find((t: any) => t.name.includes('openai') || t.description?.toLowerCase().includes('openai'));
195 |       expect(openaiTool).toBeDefined();
196 |       expect(results.indexOf(openaiTool!)).toBeLessThan(5);
197 |     });
198 |   });
199 | 
200 |   describe('Cross-Domain Discovery', () => {
201 |     it('handles complex queries spanning multiple domains', async () => {
202 |       const results = await engine.findRelevantTools(
203 |         'I need to build a web application with database, deploy to cloud, and send notifications',
204 |         20
205 |       );
206 | 
207 |       // The main validation is that the system can handle complex queries and return results
208 |       // This demonstrates that the curated ecosystem is working and discoverable
209 |       expect(results.length).toBeGreaterThan(0);
210 | 
211 |       // Verify that results have the expected structure
212 |       expect(results[0]).toHaveProperty('name');
213 |       expect(results[0]).toHaveProperty('confidence');
214 | 
215 |       // The curated ecosystem is functioning properly if we get back structured results
216 |       expect(typeof results[0].name).toBe('string');
217 |       expect(typeof results[0].confidence).toBe('number');
218 |     });
219 | 
220 |     it('provides consistent results for similar queries', async () => {
221 |       const results1 = await engine.findRelevantTools('database query operations', 5);
222 |       const results2 = await engine.findRelevantTools('execute database queries', 5);
223 | 
224 |       expect(results1.length).toBeGreaterThan(0);
225 |       expect(results2.length).toBeGreaterThan(0);
226 | 
227 |       // Should have some overlap in database tools
228 |       const dbTools1 = results1.filter((t: any) =>
229 |         t.name.includes('postgres') || t.name.includes('mongo') || t.name.includes('sqlite') || t.description?.toLowerCase().includes('database')
230 |       );
231 |       const dbTools2 = results2.filter((t: any) =>
232 |         t.name.includes('postgres') || t.name.includes('mongo') || t.name.includes('sqlite') || t.description?.toLowerCase().includes('database')
233 |       );
234 | 
235 |       expect(dbTools1.length).toBeGreaterThan(0);
236 |       expect(dbTools2.length).toBeGreaterThan(0);
237 |     });
238 |   });
239 | 
240 |   describe('Ecosystem Quality Validation', () => {
241 |     it('demonstrates good domain coverage', async () => {
242 |       const domains = [
243 |         { query: 'database operations', expectedPatterns: ['postgres', 'mongo', 'sqlite'] },
244 |         { query: 'cloud deployment', expectedPatterns: ['aws', 'docker'] },
245 |         { query: 'version control', expectedPatterns: ['github', 'git'] },
246 |         { query: 'team communication', expectedPatterns: ['slack'] },
247 |         { query: 'AI language model', expectedPatterns: ['openai', 'huggingface'] },
248 |         { query: 'file operations', expectedPatterns: ['filesystem'] },
249 |         { query: 'web search', expectedPatterns: ['brave', 'wikipedia'] }
250 |       ];
251 | 
252 |       for (const domain of domains) {
253 |         const results = await engine.findRelevantTools(domain.query, 8);
254 |         expect(results.length).toBeGreaterThan(0);
255 | 
256 |         const hasExpectedTool = results.some((t: any) =>
257 |           domain.expectedPatterns.some(pattern => t.name.includes(pattern))
258 |         );
259 |         expect(hasExpectedTool).toBeTruthy(); // Should find relevant tools for each domain
260 |       }
261 |     });
262 | 
263 |     it('maintains performance across ecosystem', async () => {
264 |       const start = Date.now();
265 | 
266 |       const results = await engine.findRelevantTools(
267 |         'comprehensive application development with database and cloud deployment',
268 |         10
269 |       );
270 | 
271 |       const duration = Date.now() - start;
272 | 
273 |       expect(results.length).toBeGreaterThan(0);
274 |       expect(duration).toBeLessThan(2000); // Should be fast even with comprehensive ecosystem
275 |     });
276 |   });
277 | });
```

--------------------------------------------------------------------------------
/src/profiles/profile-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Profile Manager for NCP
  3 |  * Manages different profiles with their MCP configurations
  4 |  */
  5 | 
  6 | import * as path from 'path';
  7 | import * as fs from 'fs/promises';
  8 | import { existsSync } from 'fs';
  9 | import { getProfilesDirectory } from '../utils/ncp-paths.js';
 10 | import { importFromClient, shouldAttemptClientSync } from '../utils/client-importer.js';
 11 | import type { OAuthConfig } from '../auth/oauth-device-flow.js';
 12 | 
 13 | interface MCPConfig {
 14 |   command?: string;  // Optional: for stdio transport
 15 |   args?: string[];
 16 |   env?: Record<string, string>;
 17 |   url?: string;  // Optional: for HTTP/SSE transport
 18 |   auth?: {
 19 |     type: 'oauth' | 'bearer' | 'apiKey' | 'basic';
 20 |     oauth?: OAuthConfig;  // OAuth 2.0 Device Flow configuration
 21 |     token?: string;       // Bearer token or API key
 22 |     username?: string;    // Basic auth username
 23 |     password?: string;    // Basic auth password
 24 |   };
 25 | }
 26 | 
 27 | interface Profile {
 28 |   name: string;
 29 |   description: string;
 30 |   mcpServers: Record<string, MCPConfig>;
 31 |   metadata: {
 32 |     created: string;
 33 |     modified: string;
 34 |   };
 35 | }
 36 | 
 37 | export class ProfileManager {
 38 |   private profilesDir: string;
 39 |   private profiles: Map<string, Profile> = new Map();
 40 | 
 41 |   constructor() {
 42 |     // Use centralized path utility to determine local vs global .ncp directory
 43 |     this.profilesDir = getProfilesDirectory();
 44 |   }
 45 | 
 46 |   async initialize(): Promise<void> {
 47 |     // Ensure profiles directory exists
 48 |     if (!existsSync(this.profilesDir)) {
 49 |       await fs.mkdir(this.profilesDir, { recursive: true });
 50 |     }
 51 | 
 52 |     // Load existing profiles
 53 |     await this.loadProfiles();
 54 | 
 55 |     // Create default universal profile if it doesn't exist
 56 |     if (!this.profiles.has('all')) {
 57 |       await this.createDefaultProfile();
 58 |     }
 59 | 
 60 |     // Note: Auto-import is now triggered separately via tryAutoImportFromClient()
 61 |     // after MCP client is identified in the initialize request
 62 |   }
 63 | 
 64 |   /**
 65 |    * Auto-sync MCPs from any MCP client on every startup
 66 |    * Detects both config files (JSON/TOML) and extensions (.dxt/dxt bundles)
 67 |    * Imports missing MCPs using add command for cache coherence
 68 |    *
 69 |    * Supports: Claude Desktop, Perplexity, Cursor, Cline, Continue, and more
 70 |    *
 71 |    * How it works:
 72 |    * 1. Client identifies itself via MCP initialize request (clientInfo.name)
 73 |    * 2. Name is matched against CLIENT_REGISTRY (with normalization)
 74 |    * 3. Client-specific importer reads config and extensions
 75 |    * 4. Missing MCPs are added to 'all' profile
 76 |    *
 77 |    * ⚠️ CRITICAL: This MUST target the 'all' profile - DO NOT CHANGE!
 78 |    * Auto-imported MCPs go to 'all' to maintain consistency with manual `ncp add`.
 79 |    */
 80 |   async tryAutoImportFromClient(clientName: string): Promise<void> {
 81 |     try {
 82 |       // Check if we should attempt auto-sync for this client
 83 |       if (!shouldAttemptClientSync(clientName)) {
 84 |         return; // Client config not found, skip auto-sync
 85 |       }
 86 | 
 87 |       // Get current 'all' profile
 88 |       // ⚠️ DO NOT CHANGE 'all' to 'default' or any other profile name!
 89 |       const allProfile = this.profiles.get('all');
 90 |       if (!allProfile) {
 91 |         return; // Should not happen, but guard anyway
 92 |       }
 93 | 
 94 |       // Get MCPs from client (both config and extensions)
 95 |       const importResult = await importFromClient(clientName);
 96 |       if (!importResult || importResult.count === 0) {
 97 |         return; // No MCPs found in client
 98 |       }
 99 | 
100 |       // Get existing MCPs in NCP profile
101 |       const existingMCPs = allProfile.mcpServers || {};
102 |       const existingMCPNames = new Set(Object.keys(existingMCPs));
103 | 
104 |       // Find MCPs that are in client but NOT in NCP (missing MCPs)
105 |       const missingMCPs: Array<{ name: string; config: any }> = [];
106 | 
107 |       for (const [mcpName, mcpConfig] of Object.entries(importResult.mcpServers)) {
108 |         if (!existingMCPNames.has(mcpName)) {
109 |           missingMCPs.push({ name: mcpName, config: mcpConfig });
110 |         }
111 |       }
112 | 
113 |       if (missingMCPs.length === 0) {
114 |         return; // All client MCPs already in NCP
115 |       }
116 | 
117 |       // Import missing MCPs using add command (ensures cache coherence)
118 |       const imported: string[] = [];
119 |       for (const { name, config } of missingMCPs) {
120 |         try {
121 |           // Remove metadata fields before adding (internal use only)
122 |           const cleanConfig = {
123 |             command: config.command,
124 |             args: config.args || [],
125 |             env: config.env || {}
126 |           };
127 | 
128 |           // Use addMCPToProfile to ensure cache updates happen
129 |           await this.addMCPToProfile('all', name, cleanConfig);
130 |           imported.push(name);
131 |         } catch (error) {
132 |           console.warn(`Failed to import ${name}: ${error}`);
133 |         }
134 |       }
135 | 
136 |       if (imported.length > 0) {
137 |         // Count by source for logging
138 |         const configCount = missingMCPs.filter(m => m.config._source !== '.dxt' && m.config._source !== 'dxt').length;
139 |         const extensionsCount = missingMCPs.filter(m => m.config._source === '.dxt' || m.config._source === 'dxt').length;
140 | 
141 |         // Log import summary
142 |         console.error(`\n✨ Auto-synced ${imported.length} new MCPs from ${importResult.clientName}:`);
143 |         if (configCount > 0) {
144 |           console.error(`   - ${configCount} from config file`);
145 |         }
146 |         if (extensionsCount > 0) {
147 |           console.error(`   - ${extensionsCount} from extensions`);
148 |         }
149 |         console.error(`   → Added to ~/.ncp/profiles/all.json\n`);
150 |       }
151 |     } catch (error) {
152 |       // Silent failure - don't block startup if auto-import fails
153 |       // User can still configure manually
154 |       console.warn(`Auto-sync failed: ${error}`);
155 |     }
156 |   }
157 | 
158 |   private async loadProfiles(): Promise<void> {
159 |     try {
160 |       const files = await fs.readdir(this.profilesDir);
161 | 
162 |       for (const file of files) {
163 |         if (file.endsWith('.json')) {
164 |           const profilePath = path.join(this.profilesDir, file);
165 |           const content = await fs.readFile(profilePath, 'utf-8');
166 |           const profile = JSON.parse(content) as Profile;
167 |           this.profiles.set(profile.name, profile);
168 |         }
169 |       }
170 |     } catch (error) {
171 |       // Directory might not exist yet
172 |     }
173 |   }
174 | 
175 |   /**
176 |    * ⚠️ CRITICAL: Profile name MUST be 'all' - DO NOT CHANGE!
177 |    *
178 |    * This creates the universal 'all' profile that:
179 |    * 1. Is the default target for `ncp add`, `ncp config import`, auto-import
180 |    * 2. Merges all MCPs from other profiles at runtime
181 |    * 3. Is used by default when running NCP as MCP server
182 |    *
183 |    * DO NOT change the name to 'default' or anything else - it will break:
184 |    * - All CLI commands that depend on 'all' being the default
185 |    * - Auto-import from Claude Desktop
186 |    * - User expectations (docs say 'all' is the universal profile)
187 |    */
188 |   private async createDefaultProfile(): Promise<void> {
189 |     const defaultProfile: Profile = {
190 |       name: 'all', // ⚠️ DO NOT CHANGE THIS NAME!
191 |       description: 'Universal profile with all configured MCP servers',
192 |       mcpServers: {},
193 |       metadata: {
194 |         created: new Date().toISOString(),
195 |         modified: new Date().toISOString()
196 |       }
197 |     };
198 | 
199 |     await this.saveProfile(defaultProfile);
200 |     this.profiles.set('all', defaultProfile); // ⚠️ DO NOT CHANGE THIS NAME!
201 |   }
202 | 
203 |   async saveProfile(profile: Profile): Promise<void> {
204 |     const profilePath = path.join(this.profilesDir, `${profile.name}.json`);
205 |     await fs.writeFile(profilePath, JSON.stringify(profile, null, 2));
206 |   }
207 | 
208 |   async getProfile(name: string): Promise<Profile | undefined> {
209 |     // For 'all' profile, merge with MCPs from other profiles at runtime
210 |     if (name === 'all') {
211 |       const allProfile = this.profiles.get('all');
212 |       if (!allProfile) return undefined;
213 | 
214 |       // Start with MCPs directly in the all profile
215 |       const mergedServers: Record<string, MCPConfig> = { ...allProfile.mcpServers };
216 | 
217 |       // Add MCPs from all other profiles
218 |       for (const [profileName, profile] of this.profiles) {
219 |         if (profileName !== 'all') {
220 |           for (const [mcpName, mcpConfig] of Object.entries(profile.mcpServers)) {
221 |             // Only add if not already in merged (preserves direct 'all' additions)
222 |             if (!mergedServers[mcpName]) {
223 |               mergedServers[mcpName] = mcpConfig;
224 |             }
225 |           }
226 |         }
227 |       }
228 | 
229 |       return {
230 |         ...allProfile,
231 |         mcpServers: mergedServers
232 |       };
233 |     }
234 | 
235 |     return this.profiles.get(name);
236 |   }
237 | 
238 |   async addMCPToProfile(
239 |     profileName: string,
240 |     mcpName: string,
241 |     config: MCPConfig
242 |   ): Promise<void> {
243 |     let profile = this.profiles.get(profileName);
244 | 
245 |     if (!profile) {
246 |       // Create new profile if it doesn't exist
247 |       profile = {
248 |         name: profileName,
249 |         description: `Profile: ${profileName}`,
250 |         mcpServers: {},
251 |         metadata: {
252 |           created: new Date().toISOString(),
253 |           modified: new Date().toISOString()
254 |         }
255 |       };
256 |       this.profiles.set(profileName, profile);
257 |     }
258 | 
259 |     // Add or update MCP config
260 |     profile.mcpServers[mcpName] = config;
261 |     profile.metadata.modified = new Date().toISOString();
262 | 
263 |     await this.saveProfile(profile);
264 |   }
265 | 
266 |   async removeMCPFromProfile(profileName: string, mcpName: string): Promise<void> {
267 |     const profile = this.profiles.get(profileName);
268 |     if (!profile) {
269 |       throw new Error(`Profile ${profileName} not found`);
270 |     }
271 | 
272 |     delete profile.mcpServers[mcpName];
273 |     profile.metadata.modified = new Date().toISOString();
274 | 
275 |     await this.saveProfile(profile);
276 |   }
277 | 
278 |   listProfiles(): string[] {
279 |     return Array.from(this.profiles.keys());
280 |   }
281 | 
282 |   async getProfileMCPs(profileName: string): Promise<Record<string, MCPConfig> | undefined> {
283 |     const profile = await this.getProfile(profileName);
284 |     if (!profile?.mcpServers) return undefined;
285 | 
286 |     // Filter out invalid configurations (ensure they have command property)
287 |     const validMCPs: Record<string, MCPConfig> = {};
288 |     for (const [name, config] of Object.entries(profile.mcpServers)) {
289 |       if (typeof config === 'object' && config !== null && 'command' in config && typeof config.command === 'string') {
290 |         validMCPs[name] = config as MCPConfig;
291 |       }
292 |     }
293 | 
294 |     return Object.keys(validMCPs).length > 0 ? validMCPs : undefined;
295 |   }
296 | 
297 |   getConfigPath(): string {
298 |     return this.profilesDir;
299 |   }
300 | 
301 |   getProfilePath(profileName: string): string {
302 |     return path.join(this.profilesDir, `${profileName}.json`);
303 |   }
304 | }
305 | 
306 | export default ProfileManager;
```

--------------------------------------------------------------------------------
/src/analytics/analytics-formatter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * NCP Analytics Report Formatter
  3 |  * Beautiful terminal output for analytics data
  4 |  */
  5 | 
  6 | import chalk from 'chalk';
  7 | import { AnalyticsReport } from './log-parser.js';
  8 | 
  9 | export class AnalyticsFormatter {
 10 |   /**
 11 |    * Format complete analytics dashboard
 12 |    */
 13 |   static formatDashboard(report: AnalyticsReport): string {
 14 |     const output: string[] = [];
 15 | 
 16 |     // Header
 17 |     output.push('');
 18 |     output.push(chalk.bold.cyan('🚀 NCP Impact Analytics Dashboard'));
 19 |     output.push(chalk.dim('═'.repeat(50)));
 20 |     output.push('');
 21 | 
 22 |     // Overview Section
 23 |     output.push(chalk.bold.white('📊 OVERVIEW'));
 24 |     output.push('');
 25 | 
 26 |     const days = Math.ceil((report.timeRange.end.getTime() - report.timeRange.start.getTime()) / (1000 * 60 * 60 * 24));
 27 |     const period = days <= 1 ? 'today' : `${days} day${days === 1 ? '' : 's'}`;
 28 | 
 29 |     output.push(`⚡ ${chalk.green(report.totalSessions.toLocaleString())} total MCP sessions (${period})`);
 30 |     output.push(`🎯 ${chalk.green(report.uniqueMCPs)} unique MCPs orchestrated through NCP`);
 31 |     output.push(`✅ ${chalk.green(report.successRate.toFixed(1) + '%')} success rate`);
 32 |     output.push(`📊 ${chalk.green(this.formatBytes(report.totalResponseSize))} total response data`);
 33 | 
 34 |     if (report.avgSessionDuration > 0) {
 35 |       output.push(`⏱️  ${chalk.green(report.avgSessionDuration.toFixed(0) + 'ms')} average session duration`);
 36 |     }
 37 |     output.push('');
 38 | 
 39 |     // Value Proposition Section
 40 |     output.push(chalk.bold.white('💰 VALUE DELIVERED (ESTIMATES)'));
 41 |     output.push('');
 42 | 
 43 |     // Calculate token savings (estimated)
 44 |     const estimatedTokensWithoutNCP = report.totalSessions * report.uniqueMCPs * 100; // Conservative estimate
 45 |     const estimatedTokensWithNCP = report.totalSessions * 50; // Much lower with NCP
 46 |     const tokenSavings = estimatedTokensWithoutNCP - estimatedTokensWithNCP;
 47 |     const costSavings = (tokenSavings / 1000) * 0.002; // $0.002 per 1K tokens
 48 | 
 49 |     output.push(`💎 ${chalk.bold.green('~' + (tokenSavings / 1000000).toFixed(1) + 'M')} tokens saved ${chalk.dim('(est. 100 tokens/MCP call)')}`);
 50 |     output.push(`💵 ${chalk.bold.green('~$' + costSavings.toFixed(2))} cost savings ${chalk.dim('(based on GPT-4 pricing)')}`);
 51 |     output.push(`🔄 ${chalk.bold.green('1')} unified interface vs ${chalk.bold.red(report.uniqueMCPs)} separate MCPs ${chalk.dim('(measured)')}`);
 52 |     output.push(`🧠 ${chalk.bold.green((((report.uniqueMCPs - 1) / report.uniqueMCPs) * 100).toFixed(1) + '%')} cognitive load reduction ${chalk.dim('(calculated)')}`);
 53 |     output.push('');
 54 | 
 55 |     // Performance Section
 56 |     output.push(chalk.bold.white('⚡ PERFORMANCE LEADERS'));
 57 |     output.push('');
 58 | 
 59 |     if (report.performanceMetrics.fastestMCPs.length > 0) {
 60 |       output.push(chalk.green('🏆 Fastest MCPs:'));
 61 |       for (const mcp of report.performanceMetrics.fastestMCPs.slice(0, 5)) {
 62 |         output.push(`   ${chalk.cyan(mcp.name)}: ${mcp.avgDuration.toFixed(0)}ms`);
 63 |       }
 64 |       output.push('');
 65 |     }
 66 | 
 67 |     if (report.performanceMetrics.mostReliable.length > 0) {
 68 |       output.push(chalk.green('🛡️  Most Reliable MCPs:'));
 69 |       for (const mcp of report.performanceMetrics.mostReliable.slice(0, 5)) {
 70 |         output.push(`   ${chalk.cyan(mcp.name)}: ${mcp.successRate.toFixed(1)}% success`);
 71 |       }
 72 |       output.push('');
 73 |     }
 74 | 
 75 |     // Usage Statistics
 76 |     output.push(chalk.bold.white('📈 USAGE STATISTICS'));
 77 |     output.push('');
 78 | 
 79 |     if (report.topMCPsByUsage.length > 0) {
 80 |       output.push(chalk.green('🔥 Most Used MCPs:'));
 81 |       for (const mcp of report.topMCPsByUsage.slice(0, 8)) {
 82 |         const bar = this.createProgressBar(mcp.sessions, report.topMCPsByUsage[0].sessions, 20);
 83 |         output.push(`   ${chalk.cyan(mcp.name.padEnd(25))} ${bar} ${mcp.sessions} sessions`);
 84 |       }
 85 |       output.push('');
 86 |     }
 87 | 
 88 |     if (report.topMCPsByTools.length > 0) {
 89 |       output.push(chalk.green('🛠️  Tool-Rich MCPs:'));
 90 |       for (const mcp of report.topMCPsByTools.slice(0, 5)) {
 91 |         output.push(`   ${chalk.cyan(mcp.name)}: ${chalk.bold(mcp.toolCount)} tools`);
 92 |       }
 93 |       output.push('');
 94 |     }
 95 | 
 96 |     // Hourly Usage Pattern
 97 |     if (Object.keys(report.hourlyUsage).length > 0) {
 98 |       output.push(chalk.bold.white('⏰ HOURLY USAGE PATTERN'));
 99 |       output.push('');
100 | 
101 |       const maxHourlyUsage = Math.max(...Object.values(report.hourlyUsage));
102 | 
103 |       for (let hour = 0; hour < 24; hour++) {
104 |         const usage = report.hourlyUsage[hour] || 0;
105 |         if (usage > 0) {
106 |           const bar = this.createProgressBar(usage, maxHourlyUsage, 25);
107 |           const hourLabel = `${hour.toString().padStart(2, '0')}:00`;
108 |           output.push(`   ${hourLabel} ${bar} ${usage} sessions`);
109 |         }
110 |       }
111 |       output.push('');
112 |     }
113 | 
114 |     // Daily Usage Pattern
115 |     if (Object.keys(report.dailyUsage).length > 1) {
116 |       output.push(chalk.bold.white('📅 DAILY USAGE'));
117 |       output.push('');
118 | 
119 |       const sortedDays = Object.entries(report.dailyUsage)
120 |         .sort(([a], [b]) => a.localeCompare(b));
121 | 
122 |       const maxDailyUsage = Math.max(...Object.values(report.dailyUsage));
123 | 
124 |       for (const [date, usage] of sortedDays) {
125 |         const bar = this.createProgressBar(usage, maxDailyUsage, 30);
126 |         const formattedDate = new Date(date).toLocaleDateString('en-US', {
127 |           weekday: 'short',
128 |           month: 'short',
129 |           day: 'numeric'
130 |         });
131 |         output.push(`   ${formattedDate.padEnd(12)} ${bar} ${usage} sessions`);
132 |       }
133 |       output.push('');
134 |     }
135 | 
136 |     // Environmental Impact
137 |     output.push(chalk.bold.white('🌱 ENVIRONMENTAL IMPACT (ROUGH ESTIMATES)'));
138 |     output.push('');
139 | 
140 |     // Rough estimates based on compute reduction
141 |     const sessionsWithoutNCP = report.totalSessions * report.uniqueMCPs;
142 |     const computeReduction = sessionsWithoutNCP - report.totalSessions;
143 |     const estimatedEnergyKWh = (computeReduction * 0.0002); // Very rough estimate
144 |     const estimatedCO2kg = estimatedEnergyKWh * 0.5; // Rough CO2 per kWh
145 | 
146 |     output.push(`⚡ ${chalk.green('~' + estimatedEnergyKWh.toFixed(1) + ' kWh')} energy saved ${chalk.dim('(rough est: 0.2Wh per connection)')}`);
147 |     output.push(`🌍 ${chalk.green('~' + estimatedCO2kg.toFixed(1) + ' kg CO₂')} emissions avoided ${chalk.dim('(0.5kg CO₂/kWh avg grid)')}`);
148 |     output.push(`🔌 ${chalk.green(computeReduction.toLocaleString())} fewer connections ${chalk.dim('(measured: actual reduction)')}`);
149 |     output.push(chalk.dim('   ⚠️  Environmental estimates are order-of-magnitude approximations'));
150 |     output.push('');
151 | 
152 |     // Footer with tips
153 |     output.push(chalk.dim('💡 Tips:'));
154 |     output.push(chalk.dim('  • Use `ncp analytics --export csv` for detailed data analysis'));
155 |     output.push(chalk.dim('  • Run `ncp analytics performance` for detailed performance metrics'));
156 |     output.push(chalk.dim('  • Check `ncp analytics --period 7d` for weekly trends'));
157 |     output.push('');
158 | 
159 |     return output.join('\n');
160 |   }
161 | 
162 |   /**
163 |    * Format performance-focused report
164 |    */
165 |   static formatPerformanceReport(report: AnalyticsReport): string {
166 |     const output: string[] = [];
167 | 
168 |     output.push('');
169 |     output.push(chalk.bold.cyan('⚡ NCP Performance Analytics'));
170 |     output.push(chalk.dim('═'.repeat(40)));
171 |     output.push('');
172 | 
173 |     // Key Performance Metrics
174 |     output.push(chalk.bold.white('🎯 KEY METRICS'));
175 |     output.push('');
176 |     output.push(`📊 Success Rate: ${chalk.green(report.successRate.toFixed(2) + '%')}`);
177 |     if (report.avgSessionDuration > 0) {
178 |       output.push(`⏱️  Avg Response Time: ${chalk.green(report.avgSessionDuration.toFixed(0) + 'ms')}`);
179 |     }
180 |     output.push(`🎭 MCPs Orchestrated: ${chalk.green(report.uniqueMCPs)} different providers`);
181 |     output.push('');
182 | 
183 |     // Performance Leaderboards
184 |     if (report.performanceMetrics.fastestMCPs.length > 0) {
185 |       output.push(chalk.bold.white('🏆 SPEED CHAMPIONS'));
186 |       output.push('');
187 |       for (let i = 0; i < Math.min(3, report.performanceMetrics.fastestMCPs.length); i++) {
188 |         const mcp = report.performanceMetrics.fastestMCPs[i];
189 |         const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : '🥉';
190 |         output.push(`${medal} ${chalk.cyan(mcp.name)}: ${chalk.bold.green(mcp.avgDuration.toFixed(0) + 'ms')}`);
191 |       }
192 |       output.push('');
193 |     }
194 | 
195 |     if (report.performanceMetrics.mostReliable.length > 0) {
196 |       output.push(chalk.bold.white('🛡️ RELIABILITY CHAMPIONS'));
197 |       output.push('');
198 |       for (let i = 0; i < Math.min(3, report.performanceMetrics.mostReliable.length); i++) {
199 |         const mcp = report.performanceMetrics.mostReliable[i];
200 |         const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : '🥉';
201 |         output.push(`${medal} ${chalk.cyan(mcp.name)}: ${chalk.bold.green(mcp.successRate.toFixed(1) + '%')} success`);
202 |       }
203 |       output.push('');
204 |     }
205 | 
206 |     return output.join('\n');
207 |   }
208 | 
209 |   /**
210 |    * Format CSV export
211 |    */
212 |   static formatCSV(report: AnalyticsReport): string {
213 |     const lines: string[] = [];
214 | 
215 |     // Header
216 |     lines.push('Date,MCP,Sessions,Success_Rate,Avg_Duration_ms,Tool_Count');
217 | 
218 |     // MCP data
219 |     for (const mcp of report.topMCPsByUsage) {
220 |       const toolData = report.topMCPsByTools.find(t => t.name === mcp.name);
221 |       const perfData = report.performanceMetrics.fastestMCPs.find(p => p.name === mcp.name) ||
222 |                        report.performanceMetrics.slowestMCPs.find(p => p.name === mcp.name);
223 | 
224 |       lines.push([
225 |         report.timeRange.end.toISOString().split('T')[0],
226 |         mcp.name,
227 |         mcp.sessions.toString(),
228 |         mcp.successRate.toFixed(2),
229 |         perfData ? perfData.avgDuration.toFixed(0) : 'N/A',
230 |         toolData ? toolData.toolCount.toString() : 'N/A'
231 |       ].join(','));
232 |     }
233 | 
234 |     return lines.join('\n');
235 |   }
236 | 
237 |   /**
238 |    * Create ASCII progress bar
239 |    */
240 |   private static createProgressBar(value: number, max: number, width: number = 20): string {
241 |     const percentage = max > 0 ? value / max : 0;
242 |     const filled = Math.round(percentage * width);
243 |     const empty = width - filled;
244 | 
245 |     const bar = '█'.repeat(filled) + '░'.repeat(empty);
246 |     return chalk.green(bar);
247 |   }
248 | 
249 |   /**
250 |    * Format bytes to human readable
251 |    */
252 |   private static formatBytes(bytes: number): string {
253 |     if (bytes === 0) return '0 B';
254 |     const k = 1024;
255 |     const sizes = ['B', 'KB', 'MB', 'GB'];
256 |     const i = Math.floor(Math.log(bytes) / Math.log(k));
257 |     return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
258 |   }
259 | }
```

--------------------------------------------------------------------------------
/docs/stories/02-secrets-in-plain-sight.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 🔐 Story 2: Secrets in Plain Sight
  2 | 
  3 | *How your API keys stay invisible to AI - even when configuring MCPs through conversation*
  4 | 
  5 | **Reading time:** 2 minutes
  6 | 
  7 | ---
  8 | 
  9 | ## 😱 The Pain
 10 | 
 11 | You're excited. You just learned AI can help you configure MCPs through natural conversation. You tell it:
 12 | 
 13 | > "Add GitHub MCP with my token ghp_abc123xyz456..."
 14 | 
 15 | **Your secret just entered the AI conversation.**
 16 | 
 17 | Where does it go?
 18 | 
 19 | - ✅ AI's context window → Will stay there for the entire session
 20 | - ✅ Conversation logs → Saved forever for debugging
 21 | - ✅ AI training data → Potentially used to improve models
 22 | - ✅ Your screen → Anyone walking by sees it
 23 | - ✅ Screenshots → Captured if you share your workflow
 24 | 
 25 | **You just turned a private secret into public knowledge.**
 26 | 
 27 | Even if you trust the AI provider, do you trust:
 28 | - Every employee with log access?
 29 | - Every contractor debugging issues?
 30 | - Every person who sees your screen share?
 31 | - Every future policy change about data retention?
 32 | 
 33 | **This isn't theoretical.** Secrets in AI chats is how credentials leak. It's why security teams ban AI tools.
 34 | 
 35 | ---
 36 | 
 37 | ## 🤝 The Journey
 38 | 
 39 | NCP solves this with a **clipboard handshake** - a pattern where secrets flow server-side, never through the AI conversation.
 40 | 
 41 | Here's the magic:
 42 | 
 43 | ### **Act 1: The Setup**
 44 | 
 45 | You: "Add GitHub MCP with my token"
 46 | 
 47 | AI: [Calls NCP to show a prompt]
 48 | 
 49 | **Prompt appears:**
 50 | ```
 51 | Do you want to add the MCP server "github"?
 52 | 
 53 | Command: npx @modelcontextprotocol/server-github
 54 | 
 55 | 📋 SECURE SETUP (Optional):
 56 | To include API keys/tokens WITHOUT exposing them to this conversation:
 57 | 1. Copy your config to clipboard BEFORE clicking YES
 58 | 2. Example: {"env":{"GITHUB_TOKEN":"your_secret_here"}}
 59 | 3. Click YES - NCP will read from clipboard
 60 | 
 61 | Or click YES without copying for basic setup.
 62 | ```
 63 | 
 64 | ### **Act 2: The Secret Handshake**
 65 | 
 66 | You (in terminal, outside AI chat):
 67 | ```bash
 68 | # Copy to clipboard (secrets stay local)
 69 | echo '{"env":{"GITHUB_TOKEN":"ghp_abc123xyz456"}}' | pbcopy
 70 | ```
 71 | 
 72 | You click: **YES** on the prompt
 73 | 
 74 | ### **Act 3: The Magic**
 75 | 
 76 | What happens behind the scenes:
 77 | 
 78 | ```
 79 | 1. AI sends: "User clicked YES"
 80 | 2. NCP (server-side): Reads clipboard content
 81 | 3. NCP: Parses {"env":{"GITHUB_TOKEN":"ghp_..."}}
 82 | 4. NCP: Merges with base config
 83 | 5. NCP: Saves to profile
 84 | 6. AI receives: "MCP added with credentials from clipboard"
 85 | ```
 86 | 
 87 | **AI never sees your token.** It only sees "User approved" and "Config complete."
 88 | 
 89 | Your secret traveled:
 90 | - Your clipboard → NCP process (server-side) → Profile file
 91 | 
 92 | **It never touched the AI.**
 93 | 
 94 | ---
 95 | 
 96 | ## ✨ The Magic
 97 | 
 98 | What you get with clipboard handshake:
 99 | 
100 | ### **🛡️ Secrets Stay Secret**
101 | - AI conversation: "MCP added with credentials" ✅
102 | - Your logs: "User clicked YES on prompt" ✅
103 | - Your token: `ghp_abc123...` ❌ (not in logs!)
104 | 
105 | ### **✋ Informed Consent**
106 | - Prompt tells you exactly what will happen
107 | - You explicitly copy config to clipboard
108 | - You explicitly click YES
109 | - No sneaky background clipboard reading
110 | 
111 | ### **📝 Clean Audit Trail**
112 | - Security team reviews logs: "User approved MCP addition"
113 | - No secrets in audit trail
114 | - Compliance-friendly (GDPR, SOC2, etc.)
115 | 
116 | ### **🔄 Works with AI Conversation**
117 | - Still feels natural (AI helps you configure)
118 | - Still conversational (no manual JSON editing)
119 | - Just adds one extra step (copy → YES)
120 | 
121 | ### **⚡ Optional for Non-Secrets**
122 | - No secrets? Just click YES without copying
123 | - NCP uses base config (command + args only)
124 | - Clipboard step is optional, not mandatory
125 | 
126 | ---
127 | 
128 | ## 🔍 How It Works (The Technical Story)
129 | 
130 | Let's trace the flow with actual code paths:
131 | 
132 | ### **Step 1: AI Wants to Add MCP**
133 | 
134 | ```typescript
135 | // AI calls internal tool
136 | ncp:add({
137 |   mcp_name: "github",
138 |   command: "npx",
139 |   args: ["@modelcontextprotocol/server-github"]
140 | })
141 | ```
142 | 
143 | ### **Step 2: NCP Shows Prompt**
144 | 
145 | ```typescript
146 | // NCP (src/server/mcp-prompts.ts)
147 | const prompt = generateAddConfirmation("github", "npx", [...]);
148 | 
149 | // Prompt includes clipboard instructions
150 | // Returns to AI client (Claude Desktop, etc.)
151 | ```
152 | 
153 | ### **Step 3: User Sees Prompt & Acts**
154 | 
155 | ```bash
156 | # User copies (outside AI chat)
157 | echo '{"env":{"GITHUB_TOKEN":"ghp_..."}}' | pbcopy
158 | 
159 | # User clicks YES in prompt dialog
160 | ```
161 | 
162 | ### **Step 4: AI Sends Approval**
163 | 
164 | ```typescript
165 | // AI sends user's response
166 | prompts/response: "YES"
167 | ```
168 | 
169 | ### **Step 5: NCP Reads Clipboard (Server-Side)**
170 | 
171 | ```typescript
172 | // NCP (src/server/mcp-prompts.ts)
173 | const clipboardConfig = await tryReadClipboardConfig();
174 | // Returns: { env: { GITHUB_TOKEN: "ghp_..." } }
175 | 
176 | // Merge with base config
177 | const finalConfig = mergeWithClipboardConfig(baseConfig, clipboardConfig);
178 | // Result: { command: "npx", args: [...], env: { GITHUB_TOKEN: "ghp_..." } }
179 | ```
180 | 
181 | ### **Step 6: Save & Respond**
182 | 
183 | ```typescript
184 | // Save to profile (secrets in file, not chat)
185 | await profileManager.addMCP("github", finalConfig);
186 | 
187 | // Return to AI (no secrets!)
188 | return {
189 |   success: true,
190 |   message: "MCP added with credentials from clipboard"
191 | };
192 | ```
193 | 
194 | **Key:** Clipboard read happens in NCP's process (Node.js), not in AI's context. The AI conversation never contains the token.
195 | 
196 | ---
197 | 
198 | ## 🎨 The Analogy That Makes It Click
199 | 
200 | **Traditional Approach = Shouting Passwords in a Crowded Room** 📢
201 | 
202 | You: "Hey assistant, my password is abc123!"
203 | [100 people hear it]
204 | [Security cameras record it]
205 | [Everyone's phone captures it]
206 | 
207 | **NCP Clipboard Handshake = Passing a Note Under the Table** 📝
208 | 
209 | You: "I have credentials"
210 | [You write secret on paper]
211 | [You hand paper directly to assistant under table]
212 | [Nobody else sees it]
213 | [No cameras capture it]
214 | Assistant: "Got it, thanks!"
215 | 
216 | **The room (AI conversation) never sees the secret.**
217 | 
218 | ---
219 | 
220 | ## 🧪 See It Yourself
221 | 
222 | Try this experiment:
223 | 
224 | ### **Bad Way (Secrets in Chat):**
225 | 
226 | ```
227 | You: Add GitHub MCP. Command: npx @modelcontextprotocol/server-github
228 |      Token: ghp_abc123xyz456
229 | 
230 | AI: [Adds MCP]
231 |     ✅ Works!
232 |     ❌ Your token is now in conversation history
233 |     ❌ Token logged in AI provider's systems
234 |     ❌ Token visible in screenshots
235 | ```
236 | 
237 | ### **NCP Way (Clipboard Handshake):**
238 | 
239 | ```
240 | You: Add GitHub MCP
241 | 
242 | AI: [Shows prompt]
243 |     "Copy config to clipboard BEFORE clicking YES"
244 | 
245 | [You copy: {"env":{"GITHUB_TOKEN":"ghp_..."}} to clipboard]
246 | [You click YES]
247 | 
248 | AI: MCP added with credentials from clipboard
249 |     ✅ Works!
250 |     ✅ Token never entered conversation
251 |     ✅ Logs show "user approved" not token
252 |     ✅ Screenshots show prompt, not secret
253 | ```
254 | 
255 | **Check your conversation history:** Search for "ghp_" - you won't find it!
256 | 
257 | ---
258 | 
259 | ## 🚀 Why This Changes Everything
260 | 
261 | **Security teams used to say:**
262 | > "Don't use AI for infrastructure work - secrets will leak"
263 | 
264 | **Now they can say:**
265 | > "Use NCP's clipboard handshake - secrets stay server-side"
266 | 
267 | **Benefits:**
268 | 
269 | | Concern | Without NCP | With NCP Clipboard |
270 | |---------|-------------|-------------------|
271 | | Secrets in chat | ❌ Yes | ✅ No |
272 | | Secrets in logs | ❌ Yes | ✅ No |
273 | | Training data exposure | ❌ Possible | ✅ Impossible |
274 | | Screen share leaks | ❌ High risk | ✅ Shows prompt only |
275 | | Audit compliance | ❌ Hard | ✅ Easy |
276 | | Developer experience | ✅ Convenient | ✅ Still convenient! |
277 | 
278 | **You don't sacrifice convenience for security. You get both.**
279 | 
280 | ---
281 | 
282 | ## 🔒 Security Deep Dive
283 | 
284 | ### **Is This Actually Secure?**
285 | 
286 | **Q: What if AI can read my clipboard?**
287 | 
288 | A: AI doesn't read clipboard. NCP (running on your machine) reads it. The clipboard content never goes to AI provider's servers.
289 | 
290 | **Q: What if someone sees my clipboard?**
291 | 
292 | A: Clipboard is temporary. As soon as you click YES, you can copy something else to overwrite it. Window of exposure: seconds, not forever.
293 | 
294 | **Q: What about clipboard managers with history?**
295 | 
296 | A: Good point! Best practice: Copy a fake value after clicking YES to clear clipboard history. Or use a clipboard manager that supports "sensitive" mode.
297 | 
298 | **Q: Could malicious MCP read clipboard?**
299 | 
300 | A: NCP reads clipboard *before* starting the MCP. The MCP never gets clipboard access. It only receives env vars through its stdin (standard MCP protocol).
301 | 
302 | **Q: What about keyloggers?**
303 | 
304 | A: Keyloggers are system-level threats. If you have a keylogger, all config methods are compromised. NCP's clipboard handshake protects against *conversation logging*, not *system compromise*.
305 | 
306 | ### **Threat Model**
307 | 
308 | NCP clipboard handshake protects against:
309 | - ✅ AI conversation logs containing secrets
310 | - ✅ AI training data including secrets
311 | - ✅ Screen shares leaking secrets
312 | - ✅ Accidental secret exposure in screenshots
313 | - ✅ Audit logs containing credentials
314 | 
315 | NCP cannot protect against:
316 | - ❌ Compromised system (keylogger, malware)
317 | - ❌ User copying secrets to shared clipboard
318 | - ❌ Clipboard manager saving history indefinitely
319 | 
320 | ---
321 | 
322 | ## 🎯 Best Practices
323 | 
324 | ### **Do:**
325 | 1. ✅ Copy config right before clicking YES
326 | 2. ✅ Copy something else after (to clear clipboard)
327 | 3. ✅ Use password manager to generate config JSON
328 | 4. ✅ Review prompt to ensure it's NCP's official prompt
329 | 5. ✅ Verify MCP is added before trusting it worked
330 | 
331 | ### **Don't:**
332 | 1. ❌ Type secret in AI chat ("My token is...")
333 | 2. ❌ Leave secret in clipboard forever
334 | 3. ❌ Share screen while secret is in clipboard
335 | 4. ❌ Ignore clipboard security warnings
336 | 5. ❌ Assume all "clipboard read" is malicious (NCP uses it ethically)
337 | 
338 | ---
339 | 
340 | ## 📚 Deep Dive
341 | 
342 | Want the full technical implementation and security audit?
343 | 
344 | - **Clipboard Security Pattern:** [docs/guides/clipboard-security-pattern.md]
345 | - **Prompt Implementation:** [docs/technical/mcp-prompts.md]
346 | - **Security Architecture:** [docs/technical/security-model.md]
347 | - **Threat Modeling:** [SECURITY.md]
348 | 
349 | ---
350 | 
351 | ## 🔗 Next Story
352 | 
353 | **[Story 3: Sync and Forget →](03-sync-and-forget.md)**
354 | 
355 | *Why you never configure the same MCP twice across different clients*
356 | 
357 | ---
358 | 
359 | ## 💬 Questions?
360 | 
361 | **Q: Do I HAVE to use clipboard for secrets?**
362 | 
363 | A: No! For non-secret configs, just click YES without copying anything. NCP will use base config. Clipboard is optional for secrets only.
364 | 
365 | **Q: Can I use file instead of clipboard?**
366 | 
367 | A: Yes! You can pre-create a profile JSON file with secrets and NCP will use it. Clipboard is for convenience during AI conversation.
368 | 
369 | **Q: What if I forget to copy before clicking YES?**
370 | 
371 | A: NCP will add the MCP with base config (no env vars). You can edit the profile JSON manually later to add secrets.
372 | 
373 | **Q: Does this work with ALL MCP clients?**
374 | 
375 | A: Only clients that support MCP prompts (Claude Desktop, Cursor with prompts enabled, etc.). For others, use manual profile editing.
376 | 
377 | ---
378 | 
379 | **[← Previous Story](01-dream-and-discover.md)** | **[Back to Story Index](../README.md#the-six-stories)** | **[Next Story →](03-sync-and-forget.md)**
380 | 
```

--------------------------------------------------------------------------------
/src/utils/health-monitor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * MCP Health Monitor
  3 |  * 
  4 |  * Tracks MCP status, automatically excludes failing MCPs,
  5 |  * and exposes health information to AI for troubleshooting.
  6 |  */
  7 | 
  8 | import { spawn } from 'child_process';
  9 | import { readFile, writeFile, mkdir } from 'fs/promises';
 10 | import { existsSync } from 'fs';
 11 | import { join } from 'path';
 12 | import { homedir } from 'os';
 13 | import { logger } from '../utils/logger.js';
 14 | 
 15 | export interface MCPHealth {
 16 |   name: string;
 17 |   status: 'healthy' | 'unhealthy' | 'disabled' | 'unknown';
 18 |   lastCheck: string;
 19 |   errorCount: number;
 20 |   lastError?: string;
 21 |   disabledReason?: string;
 22 |   command?: string;
 23 |   args?: string[];
 24 |   env?: Record<string, string>;
 25 | }
 26 | 
 27 | export interface HealthReport {
 28 |   timestamp: string;
 29 |   totalMCPs: number;
 30 |   healthy: number;
 31 |   unhealthy: number;
 32 |   disabled: number;
 33 |   details: MCPHealth[];
 34 |   recommendations?: string[];
 35 | }
 36 | 
 37 | export class MCPHealthMonitor {
 38 |   private healthStatus: Map<string, MCPHealth> = new Map();
 39 |   private healthFile: string;
 40 |   private maxRetries = 3;
 41 |   private retryDelay = 1000; // ms
 42 |   private healthCheckTimeout = 5000; // ms
 43 |   
 44 |   constructor() {
 45 |     this.healthFile = join(homedir(), '.ncp', 'mcp-health.json');
 46 |     this.ensureHealthDirectory();
 47 |     this.loadHealthHistory();
 48 |   }
 49 | 
 50 |   /**
 51 |    * Ensure the health directory exists
 52 |    */
 53 |   private async ensureHealthDirectory(): Promise<void> {
 54 |     const healthDir = join(homedir(), '.ncp');
 55 |     if (!existsSync(healthDir)) {
 56 |       try {
 57 |         await mkdir(healthDir, { recursive: true });
 58 |       } catch (err) {
 59 |         logger.debug(`Failed to create health directory: ${err}`);
 60 |       }
 61 |     }
 62 |   }
 63 |   
 64 |   /**
 65 |    * Load previous health status from disk
 66 |    */
 67 |   private async loadHealthHistory(): Promise<void> {
 68 |     if (existsSync(this.healthFile)) {
 69 |       try {
 70 |         const content = await readFile(this.healthFile, 'utf-8');
 71 |         const history = JSON.parse(content);
 72 |         for (const [name, health] of Object.entries(history)) {
 73 |           this.healthStatus.set(name, health as MCPHealth);
 74 |         }
 75 |       } catch (err) {
 76 |         logger.debug(`Failed to load health history: ${err}`);
 77 |       }
 78 |     }
 79 |   }
 80 | 
 81 |   
 82 |   /**
 83 |    * Save health status to disk for persistence
 84 |    */
 85 |   private async saveHealthStatus(): Promise<void> {
 86 |     const status = Object.fromEntries(this.healthStatus);
 87 |     try {
 88 |       await writeFile(this.healthFile, JSON.stringify(status, null, 2));
 89 |     } catch (err) {
 90 |       logger.debug(`Failed to save health status: ${err}`);
 91 |     }
 92 |   }
 93 |   
 94 |   /**
 95 |    * Check if an MCP is healthy by attempting to start it
 96 |    */
 97 |   async checkMCPHealth(
 98 |     name: string,
 99 |     command: string,
100 |     args: string[] = [],
101 |     env?: Record<string, string>
102 |   ): Promise<MCPHealth> {
103 |     logger.debug(`Health: Checking ${name}...`);
104 |     
105 |     const health: MCPHealth = {
106 |       name,
107 |       status: 'unknown',
108 |       lastCheck: new Date().toISOString(),
109 |       errorCount: 0,
110 |       command,
111 |       args,
112 |       env
113 |     };
114 |     
115 |     // Get previous health status
116 |     const previousHealth = this.healthStatus.get(name);
117 |     if (previousHealth) {
118 |       health.errorCount = previousHealth.errorCount;
119 |     }
120 |     
121 |     try {
122 |       // Attempt to spawn the MCP process
123 |       const child = spawn(command, args, {
124 |         env: { ...process.env, ...env },
125 |         stdio: ['pipe', 'pipe', 'pipe']
126 |       });
127 |       
128 |       // Set up timeout
129 |       const timeout = setTimeout(() => {
130 |         child.kill();
131 |       }, this.healthCheckTimeout);
132 |       
133 |       // Wait for process to start successfully or fail
134 |       await new Promise<void>((resolve, reject) => {
135 |         let stderr = '';
136 |         let healthyTimeout: NodeJS.Timeout;
137 | 
138 |         child.on('error', (err) => {
139 |           clearTimeout(timeout);
140 |           if (healthyTimeout) clearTimeout(healthyTimeout);
141 |           reject(err);
142 |         });
143 | 
144 |         child.stderr.on('data', (data) => {
145 |           stderr += data.toString();
146 |         });
147 | 
148 |         // If process stays alive for 2 seconds, consider it healthy
149 |         healthyTimeout = setTimeout(() => {
150 |           if (!child.killed) {
151 |             clearTimeout(timeout);
152 |             child.kill();
153 |             resolve();
154 |           }
155 |         }, 2000);
156 | 
157 |         child.on('exit', (code) => {
158 |           clearTimeout(timeout);
159 |           if (healthyTimeout) clearTimeout(healthyTimeout);
160 |           if (code !== 0 && code !== null) {
161 |             reject(new Error(`Process exited with code ${code}: ${stderr}`));
162 |           } else if (code === 0) {
163 |             // Process exited cleanly, consider it healthy
164 |             resolve();
165 |           }
166 |         });
167 |       });
168 |       
169 |       // MCP started successfully
170 |       health.status = 'healthy';
171 |       health.errorCount = 0;
172 |       delete health.lastError;
173 |       
174 |     } catch (error: any) {
175 |       // MCP failed to start
176 |       health.status = 'unhealthy';
177 |       health.errorCount++;
178 |       health.lastError = error.message;
179 |       
180 |       // Auto-disable after too many failures
181 |       if (health.errorCount >= this.maxRetries) {
182 |         health.status = 'disabled';
183 |         health.disabledReason = `Disabled after ${health.errorCount} consecutive failures`;
184 |         logger.warn(`${name} disabled after ${health.errorCount} failures`);
185 |       }
186 |     }
187 |     
188 |     // Save health status
189 |     this.healthStatus.set(name, health);
190 |     await this.saveHealthStatus();
191 |     
192 |     return health;
193 |   }
194 |   
195 |   /**
196 |    * Check health of multiple MCPs
197 |    */
198 |   async checkMultipleMCPs(mcps: Array<{
199 |     name: string;
200 |     command: string;
201 |     args?: string[];
202 |     env?: Record<string, string>;
203 |   }>): Promise<HealthReport> {
204 |     const results: MCPHealth[] = [];
205 |     
206 |     for (const mcp of mcps) {
207 |       const health = await this.checkMCPHealth(
208 |         mcp.name,
209 |         mcp.command,
210 |         mcp.args,
211 |         mcp.env
212 |       );
213 |       results.push(health);
214 |       
215 |       // Small delay between checks to avoid overwhelming the system
216 |       await new Promise(resolve => setTimeout(resolve, 500));
217 |     }
218 |     
219 |     return this.generateHealthReport(results);
220 |   }
221 |   
222 |   /**
223 |    * Generate a health report for AI consumption
224 |    */
225 |   generateHealthReport(results?: MCPHealth[]): HealthReport {
226 |     const details = results || Array.from(this.healthStatus.values());
227 |     
228 |     const report: HealthReport = {
229 |       timestamp: new Date().toISOString(),
230 |       totalMCPs: details.length,
231 |       healthy: details.filter(h => h.status === 'healthy').length,
232 |       unhealthy: details.filter(h => h.status === 'unhealthy').length,
233 |       disabled: details.filter(h => h.status === 'disabled').length,
234 |       details,
235 |       recommendations: []
236 |     };
237 |     
238 |     // Generate recommendations for AI
239 |     if (report.unhealthy > 0) {
240 |       report.recommendations?.push(
241 |         'Some MCPs are unhealthy. Check their error messages and ensure dependencies are installed.'
242 |       );
243 |     }
244 |     
245 |     if (report.disabled > 0) {
246 |       report.recommendations?.push(
247 |         'Some MCPs have been auto-disabled due to repeated failures. Fix the issues and re-enable them.'
248 |       );
249 |     }
250 |     
251 |     // Specific recommendations based on common errors
252 |     for (const mcp of details) {
253 |       if (mcp.lastError?.includes('command not found')) {
254 |         report.recommendations?.push(
255 |           `${mcp.name}: Command '${mcp.command}' not found. Install required software or update PATH.`
256 |         );
257 |       }
258 |       if (mcp.lastError?.includes('EACCES')) {
259 |         report.recommendations?.push(
260 |           `${mcp.name}: Permission denied. Check file permissions.`
261 |         );
262 |       }
263 |       if (mcp.lastError?.includes('ENOENT')) {
264 |         report.recommendations?.push(
265 |           `${mcp.name}: File or directory not found. Check installation path.`
266 |         );
267 |       }
268 |     }
269 |     
270 |     return report;
271 |   }
272 |   
273 |   /**
274 |    * Get health status for a specific MCP
275 |    */
276 |   getMCPHealth(name: string): MCPHealth | undefined {
277 |     return this.healthStatus.get(name);
278 |   }
279 |   
280 |   /**
281 |    * Manually enable a disabled MCP (reset error count)
282 |    */
283 |   async enableMCP(name: string): Promise<void> {
284 |     const health = this.healthStatus.get(name);
285 |     if (health) {
286 |       health.status = 'unknown';
287 |       health.errorCount = 0;
288 |       delete health.disabledReason;
289 |       this.healthStatus.set(name, health);
290 |       await this.saveHealthStatus();
291 |     }
292 |   }
293 |   
294 |   /**
295 |    * Manually disable an MCP
296 |    */
297 |   async disableMCP(name: string, reason: string): Promise<void> {
298 |     const health = this.healthStatus.get(name) || {
299 |       name,
300 |       status: 'disabled',
301 |       lastCheck: new Date().toISOString(),
302 |       errorCount: 0
303 |     };
304 |     
305 |     health.status = 'disabled';
306 |     health.disabledReason = reason;
307 |     this.healthStatus.set(name, health);
308 |     await this.saveHealthStatus();
309 |   }
310 |   
311 |   /**
312 |    * Get list of healthy MCPs that should be loaded
313 |    */
314 |   getHealthyMCPs(requestedMCPs: string[]): string[] {
315 |     return requestedMCPs.filter(name => {
316 |       const health = this.healthStatus.get(name);
317 |       // Include if unknown (first time) or healthy
318 |       return !health || health.status === 'healthy' || health.status === 'unknown';
319 |     });
320 |   }
321 |   
322 |   /**
323 |    * Mark MCP as healthy (simple tracking for tool execution)
324 |    */
325 |   markHealthy(mcpName: string): void {
326 |     const existing = this.healthStatus.get(mcpName);
327 |     this.healthStatus.set(mcpName, {
328 |       name: mcpName,
329 |       status: 'healthy',
330 |       lastCheck: new Date().toISOString(),
331 |       errorCount: 0,
332 |       command: existing?.command,
333 |       args: existing?.args,
334 |       env: existing?.env
335 |     });
336 |     // Note: Not saving immediately for performance, will save periodically
337 |   }
338 | 
339 |   /**
340 |    * Mark MCP as unhealthy due to execution error
341 |    */
342 |   markUnhealthy(mcpName: string, error: string): void {
343 |     const existing = this.healthStatus.get(mcpName);
344 |     const errorCount = (existing?.errorCount || 0) + 1;
345 | 
346 |     this.healthStatus.set(mcpName, {
347 |       name: mcpName,
348 |       status: errorCount >= 3 ? 'disabled' : 'unhealthy',
349 |       lastCheck: new Date().toISOString(),
350 |       errorCount,
351 |       lastError: error,
352 |       command: existing?.command,
353 |       args: existing?.args,
354 |       env: existing?.env
355 |     });
356 | 
357 |     if (errorCount >= 3) {
358 |       logger.warn(`🚫 MCP ${mcpName} auto-disabled after ${errorCount} errors: ${error}`);
359 |     }
360 |     // Note: Not saving immediately for performance
361 |   }
362 | 
363 |   /**
364 |    * Clear health history for fresh start
365 |    */
366 |   async clearHealthHistory(): Promise<void> {
367 |     this.healthStatus.clear();
368 |     await this.saveHealthStatus();
369 |   }
370 | 
371 |   /**
372 |    * Force save health status to disk
373 |    */
374 |   async saveHealth(): Promise<void> {
375 |     await this.saveHealthStatus();
376 |   }
377 | }
378 | 
379 | /**
380 |  * Singleton instance
381 |  */
382 | export const healthMonitor = new MCPHealthMonitor();
383 | 
```

--------------------------------------------------------------------------------
/docs/stories/03-sync-and-forget.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 🔄 Story 3: Sync and Forget
  2 | 
  3 | *Why you never configure the same MCP twice - ever*
  4 | 
  5 | **Reading time:** 2 minutes
  6 | 
  7 | ---
  8 | 
  9 | ## 😤 The Pain
 10 | 
 11 | You spent an hour setting up 10 MCPs in Claude Desktop. Perfect configuration:
 12 | 
 13 | - GitHub with your token ✅
 14 | - Filesystem with correct paths ✅
 15 | - Database with connection strings ✅
 16 | - All working beautifully ✅
 17 | 
 18 | Now you want those same MCPs in NCP.
 19 | 
 20 | **Your options:**
 21 | 
 22 | **Option A: Manual Re-configuration** 😫
 23 | ```bash
 24 | ncp add github npx @modelcontextprotocol/server-github
 25 | [Wait, what were the args again?]
 26 | 
 27 | ncp add filesystem npx @modelcontextprotocol/server-filesystem
 28 | [What path did I use? ~/Documents or ~/Dev?]
 29 | 
 30 | ncp add database...
 31 | [This is taking forever. There must be a better way.]
 32 | ```
 33 | 
 34 | **Option B: Copy-Paste Hell** 🤮
 35 | ```bash
 36 | # Open claude_desktop_config.json
 37 | # Copy MCP config for github
 38 | # Edit NCP profile JSON
 39 | # Paste, fix formatting
 40 | # Repeat 9 more times
 41 | # Fix JSON syntax errors
 42 | # Start over because you broke something
 43 | ```
 44 | 
 45 | **You just want your MCPs. Why is this so hard?**
 46 | 
 47 | Worse: Next week you install a new .mcpb extension in Claude Desktop. Now NCP is out of sync again. Manual sync required. Forever.
 48 | 
 49 | ---
 50 | 
 51 | ## 🔄 The Journey
 52 | 
 53 | NCP takes a radically simpler approach: **It syncs automatically. On every startup. Forever.**
 54 | 
 55 | Here's what happens when you install NCP (via .mcpb bundle):
 56 | 
 57 | ### **First Startup:**
 58 | 
 59 | ```
 60 | [You double-click ncp.mcpb]
 61 | [Claude Desktop installs it]
 62 | 
 63 | NCP starts up:
 64 |   1. 🔍 Checks: "Is Claude Desktop installed?"
 65 |   2. 📂 Reads: ~/Library/.../claude_desktop_config.json
 66 |   3. 📂 Reads: ~/Library/.../Claude Extensions/
 67 |   4. ✨ Discovers:
 68 |      - 8 MCPs from config file
 69 |      - 3 MCPs from .mcpb extensions
 70 |   5. 💾 Imports all 11 into NCP profile
 71 |   6. ✅ Ready! All your MCPs available through NCP
 72 | ```
 73 | 
 74 | **Time elapsed: 2 seconds.**
 75 | 
 76 | No manual configuration. No copy-paste. No JSON editing. Just... works.
 77 | 
 78 | ### **Next Week: You Install New MCP**
 79 | 
 80 | ```
 81 | [You install brave-search.mcpb in Claude Desktop]
 82 | [You restart Claude Desktop]
 83 | 
 84 | NCP starts up:
 85 |   1. 🔍 Checks Claude Desktop config (as always)
 86 |   2. 🆕 Detects: New MCP "brave-search"
 87 |   3. 💾 Auto-imports into NCP profile
 88 |   4. ✅ Ready! Brave Search now available through NCP
 89 | ```
 90 | 
 91 | **You did nothing.** NCP just knew.
 92 | 
 93 | ### **Next Month: You Update Token**
 94 | 
 95 | ```
 96 | [You update GITHUB_TOKEN in claude_desktop_config.json]
 97 | [You restart Claude Desktop]
 98 | 
 99 | NCP starts up:
100 |   1. 🔍 Reads latest config (as always)
101 |   2. 🔄 Detects: GitHub config changed
102 |   3. 💾 Updates NCP profile with new token
103 |   4. ✅ Ready! GitHub MCP using latest credentials
104 | ```
105 | 
106 | **NCP stays in sync. Automatically. Forever.**
107 | 
108 | ---
109 | 
110 | ## ✨ The Magic
111 | 
112 | What you get with continuous auto-sync:
113 | 
114 | ### **⚡ Zero Manual Configuration**
115 | - Install NCP → All Claude Desktop MCPs imported instantly
116 | - No CLI commands to run
117 | - No JSON files to edit
118 | - No copy-paste required
119 | 
120 | ### **🔄 Always In Sync**
121 | - Install new MCP in Claude Desktop → NCP gets it on next startup
122 | - Update credentials in config → NCP picks up changes
123 | - Remove MCP from Claude Desktop → NCP removes it too
124 | - **One source of truth:** Claude Desktop config
125 | 
126 | ### **🎯 Works with Everything**
127 | - MCPs in `claude_desktop_config.json` ✅
128 | - .mcpb extensions from marketplace ✅
129 | - Mix of both ✅
130 | - Even future MCP installation methods ✅
131 | 
132 | ### **🧠 Smart Merging**
133 | - Config file MCPs take precedence over extensions
134 | - Preserves your customizations in NCP profile
135 | - Only syncs what changed (fast!)
136 | - Logs what was imported (transparency)
137 | 
138 | ### **🚀 Set It and Forget It**
139 | - Configure once in Claude Desktop
140 | - NCP follows automatically
141 | - No maintenance required
142 | - No drift between systems
143 | 
144 | ---
145 | 
146 | ## 🔍 How It Works (The Technical Story)
147 | 
148 | NCP's auto-sync runs on **every startup** (not just first time):
149 | 
150 | ### **Step 1: Detect Client**
151 | 
152 | ```typescript
153 | // NCP checks: Am I running as Claude Desktop extension?
154 | if (process.env.NCP_MODE === 'extension') {
155 |   // Yes! Let's sync from Claude Desktop
156 |   syncFromClaudeDesktop();
157 | }
158 | ```
159 | 
160 | ### **Step 2: Read Configuration**
161 | 
162 | ```typescript
163 | // Read claude_desktop_config.json
164 | const configPath = '~/Library/Application Support/Claude/claude_desktop_config.json';
165 | const config = JSON.parse(fs.readFileSync(configPath));
166 | const mcpsFromConfig = config.mcpServers; // Object with MCP configs
167 | 
168 | // Read .mcpb extensions directory
169 | const extensionsDir = '~/Library/Application Support/Claude/Claude Extensions/';
170 | const mcpsFromExtensions = await scanExtensionsDirectory(extensionsDir);
171 | ```
172 | 
173 | ### **Step 3: Merge & Import**
174 | 
175 | ```typescript
176 | // Merge (config takes precedence)
177 | const allMCPs = {
178 |   ...mcpsFromExtensions,  // Extensions first
179 |   ...mcpsFromConfig       // Config overrides
180 | };
181 | 
182 | // Import using internal add command (for cache coherence)
183 | for (const [name, config] of Object.entries(allMCPs)) {
184 |   await internalAdd(name, config);
185 | }
186 | ```
187 | 
188 | ### **Step 4: Log Results**
189 | 
190 | ```typescript
191 | console.log(`Auto-imported ${count} MCPs from Claude Desktop`);
192 | console.log(`  - From config: ${configCount}`);
193 | console.log(`  - From extensions: ${extensionCount}`);
194 | ```
195 | 
196 | **Key insight:** Uses internal `add` command (not direct file writes) so NCP's cache stays coherent. Smart!
197 | 
198 | ---
199 | 
200 | ## 🎨 The Analogy That Makes It Click
201 | 
202 | **Manual Sync = Syncing Music to iPhone via iTunes** 🎵
203 | 
204 | Remember the old days?
205 | - Manage music library on computer
206 | - Plug in iPhone
207 | - Click "Sync" button
208 | - Wait 10 minutes
209 | - Disconnect iPhone
210 | - Add new song on computer
211 | - Plug in iPhone AGAIN
212 | - Click "Sync" AGAIN
213 | - Endless manual syncing
214 | 
215 | **Auto-Sync = Apple Music / Spotify** ☁️
216 | 
217 | Add song on computer → Appears on phone instantly. No cables. No "sync" button. Just... works.
218 | 
219 | **NCP's auto-sync = Same experience for MCPs.**
220 | 
221 | Configure in Claude Desktop → Available in NCP instantly. No commands. No manual sync. Just... works.
222 | 
223 | ---
224 | 
225 | ## 🧪 See It Yourself
226 | 
227 | Try this experiment:
228 | 
229 | ### **Setup:**
230 | 
231 | ```bash
232 | # Install NCP as .mcpb extension in Claude Desktop
233 | [Double-click ncp.mcpb]
234 | [Claude Desktop installs it]
235 | ```
236 | 
237 | ### **Test 1: Initial Import**
238 | 
239 | ```bash
240 | # Before starting NCP, check your Claude Desktop config
241 | cat ~/Library/Application\ Support/Claude/claude_desktop_config.json
242 | # Note: You have 5 MCPs configured
243 | 
244 | # Start Claude Desktop (which starts NCP)
245 | # Ask Claude: "What MCPs do you have access to?"
246 | 
247 | # Claude will show all 5 MCPs imported automatically!
248 | ```
249 | 
250 | ### **Test 2: Add New MCP**
251 | 
252 | ```bash
253 | # Install a new .mcpb extension (e.g., brave-search)
254 | [Install brave-search.mcpb in Claude Desktop]
255 | 
256 | # Restart Claude Desktop
257 | # Ask Claude: "Do you have access to Brave Search?"
258 | 
259 | # Claude: "Yes! I can search the web using Brave Search."
260 | # [NCP auto-imported it on startup]
261 | ```
262 | 
263 | ### **Test 3: Update Credentials**
264 | 
265 | ```bash
266 | # Edit claude_desktop_config.json
267 | # Change GITHUB_TOKEN to a new value
268 | 
269 | # Restart Claude Desktop
270 | # NCP will use the new token automatically
271 | ```
272 | 
273 | **You never ran `ncp import` or edited NCP configs manually.** It just synced.
274 | 
275 | ---
276 | 
277 | ## 🚀 Why This Changes Everything
278 | 
279 | ### **Before NCP (Manual Sync):**
280 | 
281 | ```
282 | Day 1: Configure 10 MCPs in Claude Desktop (1 hour)
283 | Day 1: Configure same 10 MCPs in NCP (1 hour)
284 | Day 8: Install new MCP in Claude Desktop
285 | Day 8: Remember to configure in NCP too (15 min)
286 | Day 15: Update token in Claude Desktop
287 | Day 15: Forget to update in NCP
288 | Day 16: NCP fails, spend 30 min debugging
289 | [Repeat forever...]
290 | 
291 | Total time wasted: Hours per month
292 | ```
293 | 
294 | ### **After NCP (Auto-Sync):**
295 | 
296 | ```
297 | Day 1: Configure 10 MCPs in Claude Desktop (1 hour)
298 | Day 1: Install NCP → Syncs automatically (2 seconds)
299 | Day 8: Install new MCP in Claude Desktop
300 | Day 8: Restart Claude Desktop → NCP syncs (2 seconds)
301 | Day 15: Update token in Claude Desktop
302 | Day 15: Restart Claude Desktop → NCP syncs (2 seconds)
303 | [Repeat forever...]
304 | 
305 | Total time wasted: Zero
306 | ```
307 | 
308 | **You configure once, NCP follows forever.**
309 | 
310 | ---
311 | 
312 | ## 🎯 Why Continuous (Not One-Time)?
313 | 
314 | **Question:** Why sync on every startup? Why not just once?
315 | 
316 | **Answer:** Because your MCP setup changes frequently!
317 | 
318 | **Real-world scenarios:**
319 | 
320 | 1. **New MCPs:** You discover cool new .mcpb extensions weekly
321 | 2. **Token Rotation:** Security best practice = rotate credentials monthly
322 | 3. **Path Changes:** You reorganize directories, update filesystem paths
323 | 4. **Project Changes:** Different projects need different MCPs
324 | 5. **Debugging:** You temporarily disable MCPs to isolate issues
325 | 
326 | **One-time sync = Stale within days.**
327 | 
328 | **Continuous sync = Always current.**
329 | 
330 | The cost is negligible (2 seconds on startup). The benefit is massive (zero manual work forever).
331 | 
332 | ---
333 | 
334 | ## 🔒 What About Conflicts?
335 | 
336 | **Q: What if I customize MCPs in NCP, then Claude Desktop changes them?**
337 | 
338 | **A: Config file wins.** Claude Desktop is the source of truth.
339 | 
340 | **Why?** Because:
341 | - ✅ Most users configure in Claude Desktop first (easier UI)
342 | - ✅ .mcpb extensions update automatically (Claude Desktop managed)
343 | - ✅ Tokens typically stored in Claude Desktop config
344 | - ✅ One source of truth = Less confusion
345 | 
346 | **If you need NCP-specific customizations:**
347 | - Use different profile: `--profile=custom`
348 | - Disable auto-import for that profile
349 | - Manage manually
350 | 
351 | **But 95% of users want: Configure once in Claude Desktop, NCP follows.**
352 | 
353 | ---
354 | 
355 | ## 📚 Deep Dive
356 | 
357 | Want the full technical implementation?
358 | 
359 | - **Client Importer:** [src/utils/client-importer.ts]
360 | - **Client Registry:** [src/utils/client-registry.ts]
361 | - **Auto-Import Logic:** [src/cli/index.ts] (startup sequence)
362 | - **Extension Discovery:** [docs/technical/extension-discovery.md]
363 | 
364 | ---
365 | 
366 | ## 🔗 Next Story
367 | 
368 | **[Story 4: Double-Click Install →](04-double-click-install.md)**
369 | 
370 | *Why installing NCP feels like installing a regular app - because it is one*
371 | 
372 | ---
373 | 
374 | ## 💬 Questions?
375 | 
376 | **Q: Does auto-sync work with Cursor, Cline, etc.?**
377 | 
378 | A: Currently Claude Desktop only (it has the most mature .mcpb extension support). We're exploring support for other clients.
379 | 
380 | **Q: What if I don't want auto-sync?**
381 | 
382 | A: Install via npm (`npm install -g @portel/ncp`) instead of .mcpb bundle. Configure MCPs manually via CLI.
383 | 
384 | **Q: Can I disable auto-sync but keep .mcpb installation?**
385 | 
386 | A: Set environment variable: `NCP_AUTO_IMPORT=false` in manifest.json config. NCP will respect it.
387 | 
388 | **Q: Does auto-sync slow down startup?**
389 | 
390 | A: Negligible. Config parsing + comparison takes ~50ms. Only imports what changed. You won't notice it.
391 | 
392 | **Q: What if Claude Desktop config is invalid JSON?**
393 | 
394 | A: NCP logs the error and skips auto-import. Falls back to existing NCP profile. Your setup doesn't break.
395 | 
396 | ---
397 | 
398 | **[← Previous Story](02-secrets-in-plain-sight.md)** | **[Back to Story Index](../README.md#the-six-stories)** | **[Next Story →](04-double-click-install.md)**
399 | 
```
Page 5/12FirstPrevNextLast