#
tokens: 49594/50000 11/189 files (page 7/12)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 7 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

--------------------------------------------------------------------------------
/INTERNAL-MCP-ARCHITECTURE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Internal MCP Architecture - Complete! 🎉
  2 | 
  3 | ## ✅ **What Was Implemented**
  4 | 
  5 | We've successfully implemented an **internal MCP architecture** where NCP exposes management tools as if they were regular MCPs, but they're handled internally without external processes.
  6 | 
  7 | ---
  8 | 
  9 | ## 🏗️ **Architecture Overview**
 10 | 
 11 | ### **Before: Direct Exposure** ❌
 12 | ```
 13 | NCP MCP Server
 14 | ├── find (top-level)
 15 | ├── run (top-level)
 16 | ├── add_mcp (top-level)     ← Exposed directly!
 17 | └── remove_mcp (top-level)  ← Exposed directly!
 18 | ```
 19 | 
 20 | ### **After: Internal MCP Pattern** ✅
 21 | ```
 22 | NCP MCP Server
 23 | ├── find (top-level)  ← Search tools in configured MCPs
 24 | └── run (top-level)   ← Execute ANY tool (external or internal)
 25 | 
 26 | Internal MCPs (discovered via find, executed via run):
 27 | └── ncp (internal MCP)
 28 |     ├── add       ← ncp:add
 29 |     ├── remove    ← ncp:remove
 30 |     ├── list      ← ncp:list
 31 |     ├── import    ← ncp:import (clipboard/file/discovery)
 32 |     └── export    ← ncp:export (clipboard/file)
 33 | ```
 34 | 
 35 | ---
 36 | 
 37 | ## 🔑 **Key Concepts**
 38 | 
 39 | ### **1. The "Inception" Pattern**
 40 | 
 41 | | Tool | Purpose | Analogy |
 42 | |------|---------|---------|
 43 | | **`find`** (top-level) | Find tools in **configured** MCPs | "What can I do with what I have?" |
 44 | | **`ncp:import`** (internal) | Find **new MCPs** from registry | "What else can I add?" (inception!) |
 45 | 
 46 | ### **2. Internal vs External MCPs**
 47 | 
 48 | | Aspect | External MCPs | Internal MCPs |
 49 | |--------|---------------|---------------|
 50 | | **Process** | Separate process (node, python, etc.) | No process (handled internally) |
 51 | | **Discovery** | Same (appears in `find` results) | Same (appears in `find` results) |
 52 | | **Execution** | Via MCP protocol (stdio transport) | Direct method call |
 53 | | **Configuration** | Needs command, args, env | Hardcoded in NCP |
 54 | | **Examples** | github, filesystem, brave-search | ncp (management tools) |
 55 | 
 56 | ---
 57 | 
 58 | ## 📁 **Files Created**
 59 | 
 60 | ### **1. Internal MCP Types** (`src/internal-mcps/types.ts`)
 61 | ```typescript
 62 | export interface InternalTool {
 63 |   name: string;
 64 |   description: string;
 65 |   inputSchema: { /* JSON Schema */ };
 66 | }
 67 | 
 68 | export interface InternalMCP {
 69 |   name: string;
 70 |   description: string;
 71 |   tools: InternalTool[];
 72 |   executeTool(toolName: string, parameters: any): Promise<InternalToolResult>;
 73 | }
 74 | ```
 75 | 
 76 | ### **2. NCP Management MCP** (`src/internal-mcps/ncp-management.ts`)
 77 | 
 78 | Implements all management tools:
 79 | 
 80 | ```typescript
 81 | export class NCPManagementMCP implements InternalMCP {
 82 |   name = 'ncp';
 83 |   description = 'NCP configuration management tools';
 84 | 
 85 |   tools = [
 86 |     {
 87 |       name: 'add',
 88 |       description: 'Add single MCP (with clipboard security)',
 89 |       inputSchema: { mcp_name, command, args?, profile? }
 90 |     },
 91 |     {
 92 |       name: 'remove',
 93 |       description: 'Remove MCP',
 94 |       inputSchema: { mcp_name, profile? }
 95 |     },
 96 |     {
 97 |       name: 'list',
 98 |       description: 'List configured MCPs',
 99 |       inputSchema: { profile? }
100 |     },
101 |     {
102 |       name: 'import',
103 |       description: 'Bulk import MCPs',
104 |       inputSchema: {
105 |         from: 'clipboard' | 'file' | 'discovery',
106 |         source?: string  // file path or search query
107 |       }
108 |     },
109 |     {
110 |       name: 'export',
111 |       description: 'Export configuration',
112 |       inputSchema: {
113 |         to: 'clipboard' | 'file',
114 |         destination?: string,  // file path
115 |         profile?: string
116 |       }
117 |     }
118 |   ];
119 | }
120 | ```
121 | 
122 | ### **3. Internal MCP Manager** (`src/internal-mcps/internal-mcp-manager.ts`)
123 | 
124 | Manages all internal MCPs:
125 | 
126 | ```typescript
127 | export class InternalMCPManager {
128 |   private internalMCPs: Map<string, InternalMCP> = new Map();
129 | 
130 |   constructor() {
131 |     // Register internal MCPs
132 |     this.registerInternalMCP(new NCPManagementMCP());
133 |   }
134 | 
135 |   initialize(profileManager: ProfileManager): void {
136 |     // Initialize each internal MCP with ProfileManager
137 |   }
138 | 
139 |   async executeInternalTool(mcpName: string, toolName: string, params: any) {
140 |     // Route to appropriate internal MCP
141 |   }
142 | 
143 |   isInternalMCP(mcpName: string): boolean {
144 |     // Check if MCP is internal
145 |   }
146 | }
147 | ```
148 | 
149 | ---
150 | 
151 | ## 🔄 **Integration with Orchestrator**
152 | 
153 | ### **Changes to `NCPOrchestrator`**
154 | 
155 | **1. Added InternalMCPManager:**
156 | ```typescript
157 | private internalMCPManager: InternalMCPManager;
158 | 
159 | constructor() {
160 |   // ...
161 |   this.internalMCPManager = new InternalMCPManager();
162 | }
163 | ```
164 | 
165 | **2. Initialize internal MCPs after ProfileManager:**
166 | ```typescript
167 | private async loadProfile() {
168 |   if (!this.profileManager) {
169 |     this.profileManager = new ProfileManager();
170 |     await this.profileManager.initialize();
171 | 
172 |     // Initialize internal MCPs with ProfileManager
173 |     this.internalMCPManager.initialize(this.profileManager);
174 |   }
175 | }
176 | ```
177 | 
178 | **3. Add internal MCPs to tool discovery:**
179 | ```typescript
180 | async initialize() {
181 |   // ... index external MCPs ...
182 | 
183 |   // Add internal MCPs to discovery
184 |   this.addInternalMCPsToDiscovery();
185 | }
186 | 
187 | private addInternalMCPsToDiscovery() {
188 |   const internalMCPs = this.internalMCPManager.getAllInternalMCPs();
189 | 
190 |   for (const mcp of internalMCPs) {
191 |     // Add to definitions
192 |     this.definitions.set(mcp.name, { /* ... */ });
193 | 
194 |     // Add tools to allTools
195 |     for (const tool of mcp.tools) {
196 |       this.allTools.push({ name: tool.name, description: tool.description, mcpName: mcp.name });
197 |       this.toolToMCP.set(`${mcp.name}:${tool.name}`, mcp.name);
198 |     }
199 | 
200 |     // Index in discovery engine
201 |     this.discovery.indexMCPTools(mcp.name, discoveryTools);
202 |   }
203 | }
204 | ```
205 | 
206 | **4. Route internal tool execution:**
207 | ```typescript
208 | async run(toolName: string, parameters: any) {
209 |   // Parse tool name
210 |   const [mcpName, actualToolName] = toolName.split(':');
211 | 
212 |   // Check if internal MCP
213 |   if (this.internalMCPManager.isInternalMCP(mcpName)) {
214 |     return await this.internalMCPManager.executeInternalTool(
215 |       mcpName,
216 |       actualToolName,
217 |       parameters
218 |     );
219 |   }
220 | 
221 |   // Otherwise, execute as external MCP
222 |   // ...
223 | }
224 | ```
225 | 
226 | ---
227 | 
228 | ## 🎯 **Tool Definitions**
229 | 
230 | ### **`ncp:add`**
231 | ```typescript
232 | {
233 |   from: 'clipboard' | 'file' | 'discovery',
234 |   source?: string
235 | }
236 | 
237 | // Examples:
238 | ncp:add { mcp_name: "github", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] }
239 | // User can copy {"env":{"GITHUB_TOKEN":"secret"}} to clipboard before approving
240 | ```
241 | 
242 | ### **`ncp:remove`**
243 | ```typescript
244 | {
245 |   mcp_name: string,
246 |   profile?: string
247 | }
248 | 
249 | // Example:
250 | ncp:remove { mcp_name: "github" }
251 | ```
252 | 
253 | ### **`ncp:list`**
254 | ```typescript
255 | {
256 |   profile?: string
257 | }
258 | 
259 | // Example:
260 | ncp:list { }  // Lists all MCPs in 'all' profile
261 | ```
262 | 
263 | ### **`ncp:import`** (Unified bulk import)
264 | ```typescript
265 | {
266 |   from: 'clipboard' | 'file' | 'discovery',
267 |   source?: string
268 | }
269 | 
270 | // Mode 1: From clipboard
271 | ncp:import { }  // Reads JSON from clipboard
272 | 
273 | // Mode 2: From file
274 | ncp:import { from: "file", source: "~/configs/my-mcps.json" }
275 | 
276 | // Mode 3: From discovery (registry)
277 | ncp:import { from: "discovery", source: "github automation" }
278 | // Shows numbered list → User selects → Prompts for each → Imports all
279 | ```
280 | 
281 | ### **`ncp:export`**
282 | ```typescript
283 | {
284 |   to: 'clipboard' | 'file',
285 |   destination?: string,
286 |   profile?: string
287 | }
288 | 
289 | // Example 1: To clipboard
290 | ncp:export { }  // Exports to clipboard
291 | 
292 | // Example 2: To file
293 | ncp:export { to: "file", destination: "~/backups/ncp-config.json" }
294 | ```
295 | 
296 | ---
297 | 
298 | ## 🚀 **User Experience**
299 | 
300 | ### **Scenario: Add GitHub MCP**
301 | 
302 | **User:** "Add GitHub MCP"
303 | 
304 | **AI workflow:**
305 | 1. Calls `prompts/get confirm_add_mcp` → Shows dialog
306 | 2. User copies `{"env":{"GITHUB_TOKEN":"ghp_..."}}` → Clicks YES
307 | 3. AI calls `run` with `ncp:add` → Tool executes internally
308 | 4. Returns success (secrets never seen by AI!)
309 | 
310 | ### **Scenario: Bulk Import from Clipboard**
311 | 
312 | **User:** "Import MCPs from my clipboard"
313 | 
314 | **AI workflow:**
315 | 1. User copies JSON config to clipboard:
316 |    ```json
317 |    {
318 |      "mcpServers": {
319 |        "github": { "command": "npx", "args": [...] },
320 |        "filesystem": { "command": "npx", "args": [...] }
321 |      }
322 |    }
323 |    ```
324 | 2. AI calls `run` with `ncp:import { }`
325 | 3. NCP reads clipboard → Imports all MCPs
326 | 4. Returns: "✅ Imported 2 MCPs from clipboard"
327 | 
328 | ### **Scenario: Discovery Mode** (Future)
329 | 
330 | **User:** "Find MCPs for GitHub automation"
331 | 
332 | **AI workflow:**
333 | 1. Calls `run` with `ncp:import { from: "discovery", source: "github automation" }`
334 | 2. NCP queries registry → Returns numbered list:
335 |    ```
336 |    1. github - Official GitHub MCP
337 |    2. github-actions - Trigger workflows
338 |    3. octokit - Full GitHub API
339 |    ```
340 | 3. AI shows list to user → User responds "1,3"
341 | 4. For each selected:
342 |    - Show `confirm_add_mcp` prompt
343 |    - User copies secrets if needed → Clicks YES
344 |    - Add MCP with clipboard config
345 | 5. Returns: "✅ Imported 2 MCPs"
346 | 
347 | ---
348 | 
349 | ## 🔒 **Security Benefits**
350 | 
351 | ### **Clipboard Security Pattern** (From Phase 1)
352 | - ✅ User explicitly instructed to copy before clicking YES
353 | - ✅ Secrets read server-side (never exposed to AI)
354 | - ✅ Audit trail shows approval, not secrets
355 | - ✅ Informed consent (not sneaky background reading)
356 | 
357 | ### **Internal MCP Architecture** (Phase 2)
358 | - ✅ Management tools discoverable like any MCP
359 | - ✅ No direct exposure in top-level tools
360 | - ✅ Consistent interface (find → run)
361 | - ✅ Can be extended with more internal MCPs
362 | 
363 | ---
364 | 
365 | ## 📊 **Before vs After**
366 | 
367 | ### **Before: Direct Exposure**
368 | ```
369 | tools/list → 4 tools
370 |   - find
371 |   - run
372 |   - add_mcp      ← Direct exposure!
373 |   - remove_mcp   ← Direct exposure!
374 | ```
375 | 
376 | ### **After: Internal MCP Pattern**
377 | ```
378 | tools/list → 2 tools
379 |   - find
380 |   - run
381 | 
382 | find results → Includes internal MCPs
383 |   - ncp:add
384 |   - ncp:remove
385 |   - ncp:list
386 |   - ncp:import
387 |   - ncp:export
388 | 
389 | run → Routes internal MCPs to InternalMCPManager
390 | ```
391 | 
392 | ---
393 | 
394 | ## 🎯 **Benefits**
395 | 
396 | 1. **Clean Separation** - Top-level tools remain minimal (find, run)
397 | 2. **Consistency** - Internal MCPs work exactly like external MCPs
398 | 3. **Discoverability** - Users find management tools via `find`
399 | 4. **Extensibility** - Easy to add more internal MCPs
400 | 5. **Security** - Clipboard pattern integrated into management tools
401 | 6. **No Process Overhead** - Internal MCPs execute instantly (no stdio transport)
402 | 
403 | ---
404 | 
405 | ## 🧪 **Testing**
406 | 
407 | ### **Test 1: Discover Internal MCPs**
408 | ```bash
409 | echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"find","arguments":{"description":"ncp"}}}' | npx ncp
410 | ```
411 | 
412 | **Expected:** Returns `ncp:add`, `ncp:remove`, `ncp:list`, `ncp:import`, `ncp:export`
413 | 
414 | ### **Test 2: List Configured MCPs**
415 | ```bash
416 | echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run","arguments":{"tool":"ncp:list"}}}' | npx ncp
417 | ```
418 | 
419 | **Expected:** Returns list of configured MCPs
420 | 
421 | ### **Test 3: Add MCP**
422 | ```bash
423 | # First show prompt
424 | echo '{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"confirm_add_mcp","arguments":{"mcp_name":"test","command":"echo","args":["hello"]}}}' | npx ncp
425 | 
426 | # Then call add tool
427 | echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"run","arguments":{"tool":"ncp:add","parameters":{"mcp_name":"test","command":"echo","args":["hello"]}}}}' | npx ncp
428 | ```
429 | 
430 | **Expected:** MCP added to profile
431 | 
432 | ---
433 | 
434 | ## 🚀 **Next Steps** (Future Phases)
435 | 
436 | ### **Phase 3: Registry Integration** (Pending)
437 | - Implement `ncp:import` discovery mode
438 | - Query MCP registry API
439 | - Show numbered/checkbox list
440 | - Batch prompt + import workflow
441 | 
442 | ### **Phase 4: Advanced Features**
443 | - `ncp:update` - Update MCP configuration
444 | - `ncp:enable` / `ncp:disable` - Toggle MCPs without removing
445 | - `ncp:validate` - Test MCP before adding
446 | - `ncp:clone` - Duplicate MCP with different config
447 | 
448 | ---
449 | 
450 | ## 📝 **Key Implementation Details**
451 | 
452 | ### **Tool ID Format**
453 | ```typescript
454 | // External MCPs: "mcpName:toolName"
455 | "github:create_issue"
456 | "filesystem:read_file"
457 | 
458 | // Internal MCPs: "mcpName:toolName"
459 | "ncp:add"
460 | "ncp:import"
461 | ```
462 | 
463 | ### **Tool Routing Logic**
464 | ```typescript
465 | if (toolIdentifier.includes(':')) {
466 |   const [mcpName, toolName] = toolIdentifier.split(':');
467 | 
468 |   if (internalMCPManager.isInternalMCP(mcpName)) {
469 |     // Route to internal MCP
470 |     return internalMCPManager.executeInternalTool(mcpName, toolName, params);
471 |   } else {
472 |     // Route to external MCP via MCP protocol
473 |     return await connection.client.callTool({ name: toolName, arguments: params });
474 |   }
475 | }
476 | ```
477 | 
478 | ---
479 | 
480 | ## ✅ **Implementation Complete!**
481 | 
482 | We've successfully created an elegant internal MCP architecture that:
483 | - ✅ Keeps top-level tools minimal (find, run only)
484 | - ✅ Exposes management tools as an internal MCP (`ncp`)
485 | - ✅ Maintains clipboard security pattern
486 | - ✅ Provides clean parameter design (`from/to` + `source/destination`)
487 | - ✅ Integrates seamlessly with tool discovery
488 | - ✅ Routes execution correctly (internal vs external)
489 | 
490 | **The foundation is solid. Ready for registry integration (Phase 3)!** 🎉
491 | 
```

--------------------------------------------------------------------------------
/docs/stories/05-runtime-detective.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 🕵️ Story 5: Runtime Detective
  2 | 
  3 | *How NCP automatically uses the right Node.js - even when you toggle Claude Desktop settings*
  4 | 
  5 | **Reading time:** 2 minutes
  6 | 
  7 | ---
  8 | 
  9 | ## 😵 The Pain
 10 | 
 11 | You installed NCP as a .mcpb extension in Claude Desktop. It works perfectly! Then...
 12 | 
 13 | **Scenario 1: The Mystery Crash**
 14 | 
 15 | ```
 16 | [You toggle "Use Built-in Node.js for MCP" setting in Claude Desktop]
 17 | [Restart Claude Desktop]
 18 | [NCP starts loading your MCPs...]
 19 | [Filesystem MCP: ❌ FAILED]
 20 | [GitHub MCP: ❌ FAILED]
 21 | [Database MCP: ❌ FAILED]
 22 | 
 23 | You: "What broke?! It was working 5 minutes ago!"
 24 | ```
 25 | 
 26 | **The Problem:** Your .mcpb extensions were using Claude Desktop's bundled Node.js (v20). You toggled the setting. Now they're trying to use your system Node.js (v18). Some Node.js 20 features don't exist in v18. Everything breaks.
 27 | 
 28 | **Scenario 2: The Path Confusion**
 29 | 
 30 | ```
 31 | [NCP installed globally via npm]
 32 | [Uses system Node.js /usr/local/bin/node]
 33 | [.mcpb extensions installed in Claude Desktop]
 34 | [Expect Claude's bundled Node.js]
 35 | 
 36 | NCP spawns extension:
 37 |   command: "node /path/to/extension/index.js"
 38 | 
 39 | Which node???
 40 |   - System node (/usr/local/bin/node)? Wrong version!
 41 |   - Claude's bundled node? Don't know the path!
 42 |   - Extension breaks silently
 43 | ```
 44 | 
 45 | **The Root Problem:** Node.js runtime is a **moving target**:
 46 | 
 47 | - Claude Desktop ships its own Node.js (predictable version)
 48 | - Your system has different Node.js (unpredictable version)
 49 | - Users toggle settings (changes which runtime to use)
 50 | - Extensions need to match the runtime NCP is using
 51 | - **Getting it wrong = everything breaks**
 52 | 
 53 | ---
 54 | 
 55 | ## 🕵️ The Journey
 56 | 
 57 | NCP acts as a **runtime detective** - it figures out which runtime it's using, then ensures all MCPs use the same one.
 58 | 
 59 | ### **How Detection Works:**
 60 | 
 61 | **On Every Startup** (not just once):
 62 | 
 63 | ```typescript
 64 | // Step 1: Check how NCP itself was launched
 65 | const myPath = process.execPath;
 66 | // Example: /Applications/Claude.app/.../node
 67 | 
 68 | // Step 2: Is this Claude Desktop's bundled runtime?
 69 | if (myPath.includes('/Claude.app/') ||
 70 |     myPath.includes('/Claude/resources/')) {
 71 |   // Yes! I'm running via Claude's bundled Node.js
 72 |   runtime = 'bundled';
 73 |   nodePath = '/Applications/Claude.app/.../node';
 74 |   pythonPath = '/Applications/Claude.app/.../python3';
 75 | } else {
 76 |   // No! I'm running via system runtime
 77 |   runtime = 'system';
 78 |   nodePath = 'node';    // Use system node
 79 |   pythonPath = 'python3'; // Use system python
 80 | }
 81 | 
 82 | // Step 3: Log what we detected (for debugging)
 83 | console.log(`Runtime detected: ${runtime}`);
 84 | console.log(`Node path: ${nodePath}`);
 85 | ```
 86 | 
 87 | **Why Every Startup?** Because the runtime can change!
 88 | 
 89 | - User toggles "Use Built-in Node.js" → Runtime changes
 90 | - User switches between .mcpb and npm install → Runtime changes
 91 | - User updates Claude Desktop → Bundled runtime path changes
 92 | 
 93 | **Static detection (at install time) would break. Dynamic detection (at runtime) adapts.**
 94 | 
 95 | ### **How MCP Spawning Works:**
 96 | 
 97 | When NCP needs to start an MCP:
 98 | 
 99 | ```typescript
100 | // MCP config from manifest.json
101 | const mcpConfig = {
102 |   command: "node",  // Generic command
103 |   args: ["${__dirname}/dist/index.js"]
104 | };
105 | 
106 | // Runtime detector translates to actual runtime
107 | const actualCommand = getRuntimeForCommand(mcpConfig.command);
108 | // If detected bundled: "/Applications/Claude.app/.../node"
109 | // If detected system: "node"
110 | 
111 | // Spawn MCP with correct runtime
112 | spawn(actualCommand, mcpConfig.args);
113 | ```
114 | 
115 | **Result:** MCPs always use the same runtime NCP is using. No mismatches. No breaks.
116 | 
117 | ---
118 | 
119 | ## ✨ The Magic
120 | 
121 | What you get with dynamic runtime detection:
122 | 
123 | ### **🎯 Just Works**
124 | - Install NCP any way (npm, .mcpb, manual)
125 | - NCP detects runtime automatically
126 | - MCPs use correct runtime automatically
127 | - Zero configuration required
128 | 
129 | ### **🔄 Adapts to Settings**
130 | - Toggle "Use Built-in Node.js" → NCP adapts on next startup
131 | - Switch between Claude Desktop and system → NCP adapts
132 | - Update Claude Desktop → NCP finds new runtime path
133 | 
134 | ### **🐛 No Version Mismatches**
135 | - NCP running via Node 20 → MCPs use Node 20
136 | - NCP running via Node 18 → MCPs use Node 18
137 | - **Always matched.** No subtle version bugs.
138 | 
139 | ### **🔍 Debuggable**
140 | - NCP logs detected runtime on startup
141 | - Shows Node path, Python path
142 | - Easy to verify correct runtime selected
143 | 
144 | ### **⚡ Works Across Platforms**
145 | - macOS: Detects `/Applications/Claude.app/...`
146 | - Windows: Detects `C:\...\Claude\resources\...`
147 | - Linux: Detects `/opt/Claude/resources/...`
148 | 
149 | ---
150 | 
151 | ## 🔍 How It Works (The Technical Story)
152 | 
153 | ### **Runtime Detection Algorithm:**
154 | 
155 | ```typescript
156 | // src/utils/runtime-detector.ts
157 | 
158 | export function detectRuntime(): RuntimeInfo {
159 |   const currentNodePath = process.execPath;
160 | 
161 |   // Check if we're running via Claude Desktop's bundled Node
162 |   const claudeBundledNode = getBundledRuntimePath('claude-desktop', 'node');
163 |   // Returns: "/Applications/Claude.app/.../node" (platform-specific)
164 | 
165 |   // If our execPath matches the bundled Node path → bundled runtime
166 |   if (currentNodePath === claudeBundledNode) {
167 |     return {
168 |       type: 'bundled',
169 |       nodePath: claudeBundledNode,
170 |       pythonPath: getBundledRuntimePath('claude-desktop', 'python')
171 |     };
172 |   }
173 | 
174 |   // Check if execPath is inside Claude.app → probably bundled
175 |   const isInsideClaudeApp = currentNodePath.includes('/Claude.app/') ||
176 |                             currentNodePath.includes('\\Claude\\');
177 | 
178 |   if (isInsideClaudeApp && existsSync(claudeBundledNode)) {
179 |     return {
180 |       type: 'bundled',
181 |       nodePath: claudeBundledNode,
182 |       pythonPath: getBundledRuntimePath('claude-desktop', 'python')
183 |     };
184 |   }
185 | 
186 |   // Otherwise → system runtime
187 |   return {
188 |     type: 'system',
189 |     nodePath: 'node',      // Use system node
190 |     pythonPath: 'python3'  // Use system python
191 |   };
192 | }
193 | ```
194 | 
195 | ### **Command Translation:**
196 | 
197 | ```typescript
198 | // src/utils/runtime-detector.ts
199 | 
200 | export function getRuntimeForExtension(command: string): string {
201 |   const runtime = detectRuntime();
202 | 
203 |   // If command is 'node' → translate to actual runtime
204 |   if (command === 'node' || command.endsWith('/node')) {
205 |     return runtime.nodePath;
206 |   }
207 | 
208 |   // If command is 'python3' → translate to actual runtime
209 |   if (command === 'python3' || command === 'python') {
210 |     return runtime.pythonPath || command;
211 |   }
212 | 
213 |   // Other commands → return as-is
214 |   return command;
215 | }
216 | ```
217 | 
218 | ### **Client Registry (Platform-Specific Paths):**
219 | 
220 | ```typescript
221 | // src/utils/client-registry.ts
222 | 
223 | export const CLIENT_REGISTRY = {
224 |   'claude-desktop': {
225 |     bundledRuntimes: {
226 |       node: {
227 |         darwin: '/Applications/Claude.app/.../node',
228 |         win32: '%LOCALAPPDATA%/Programs/Claude/.../node.exe',
229 |         linux: '/opt/Claude/resources/.../node'
230 |       },
231 |       python: {
232 |         darwin: '/Applications/Claude.app/.../python3',
233 |         win32: '%LOCALAPPDATA%/Programs/Claude/.../python.exe',
234 |         linux: '/opt/Claude/resources/.../python3'
235 |       }
236 |     }
237 |   }
238 | };
239 | ```
240 | 
241 | **NCP knows where Claude Desktop hides its runtimes on every platform!**
242 | 
243 | ---
244 | 
245 | ## 🎨 The Analogy That Makes It Click
246 | 
247 | **Static Runtime (Wrong Approach) = Directions Written on Paper** 🗺️
248 | 
249 | ```
250 | "Go to 123 Main Street"
251 | [Next week: Store moves to 456 Oak Avenue]
252 | [Your paper still says 123 Main Street]
253 | [You arrive at wrong location]
254 | [Confused why nothing works]
255 | ```
256 | 
257 | **Dynamic Runtime (NCP Approach) = GPS Navigation** 📍
258 | 
259 | ```
260 | "Navigate to Store"
261 | [GPS finds current location of store]
262 | [Store moves? GPS updates automatically]
263 | [You always arrive at correct location]
264 | [Never confused, always works]
265 | ```
266 | 
267 | **NCP doesn't remember where runtime was. It detects where runtime IS.**
268 | 
269 | ---
270 | 
271 | ## 🧪 See It Yourself
272 | 
273 | Try this experiment:
274 | 
275 | ### **Test 1: Detect Current Runtime**
276 | 
277 | ```bash
278 | # Install NCP and check logs
279 | ncp list
280 | 
281 | # Look for startup logs:
282 | [Runtime Detection]
283 |   Type: bundled
284 |   Node: /Applications/Claude.app/.../node
285 |   Python: /Applications/Claude.app/.../python3
286 |   Process execPath: /Applications/Claude.app/.../node
287 | ```
288 | 
289 | ### **Test 2: Toggle Setting and See Adaptation**
290 | 
291 | ```bash
292 | # Before toggle
293 | [Claude Desktop: "Use Built-in Node.js for MCP" = ON]
294 | [Restart Claude Desktop]
295 | [Check logs: Type: bundled]
296 | 
297 | # Toggle setting
298 | [Claude Desktop: "Use Built-in Node.js for MCP" = OFF]
299 | [Restart Claude Desktop]
300 | [Check logs: Type: system]
301 | 
302 | # NCP adapted automatically!
303 | ```
304 | 
305 | ### **Test 3: Install via npm and Compare**
306 | 
307 | ```bash
308 | # Install NCP globally
309 | npm install -g @portel/ncp
310 | 
311 | # Run and check detection
312 | ncp list
313 | 
314 | # Look for startup logs:
315 | [Runtime Detection]
316 |   Type: system
317 |   Node: node
318 |   Python: python3
319 |   Process execPath: /usr/local/bin/node
320 | 
321 | # Different runtime detected! But MCPs will still use system runtime consistently.
322 | ```
323 | 
324 | ---
325 | 
326 | ## 🚀 Why This Changes Everything
327 | 
328 | ### **Before Runtime Detection (Chaos):**
329 | 
330 | ```
331 | User installs .mcpb extension
332 | → Works with bundled Node.js
333 | 
334 | User toggles "Use Built-in Node.js" setting
335 | → MCPs try to use system Node.js
336 | → Version mismatch
337 | → Cryptic errors
338 | → User spends 2 hours debugging
339 | 
340 | User gives up, uninstalls
341 | ```
342 | 
343 | ### **After Runtime Detection (Harmony):**
344 | 
345 | ```
346 | User installs .mcpb extension
347 | → Works with bundled Node.js
348 | 
349 | User toggles "Use Built-in Node.js" setting
350 | → NCP detects change on next startup
351 | → MCPs automatically switch to system Node.js
352 | → Everything still works
353 | 
354 | User: "That was easy! It just works."
355 | ```
356 | 
357 | **The difference:** **Adaptability.**
358 | 
359 | ---
360 | 
361 | ## 🎯 Why Dynamic (Not Static)?
362 | 
363 | **Question:** Why detect runtime on every startup? Why not cache the result?
364 | 
365 | **Answer:** Because the runtime isn't stable!
366 | 
367 | **Things that change runtime:**
368 | 
369 | 1. **User toggles settings** (most common)
370 | 2. **User updates Claude Desktop** (bundled runtime path changes)
371 | 3. **User updates system Node.js** (system runtime version changes)
372 | 4. **User switches installation method** (.mcpb → npm or vice versa)
373 | 5. **CI/CD environment** (different runtime per environment)
374 | 
375 | **Static detection** = Breaks when any of these change (frequent!)
376 | 
377 | **Dynamic detection** = Adapts automatically (resilient!)
378 | 
379 | **Cost:** ~5ms on startup to detect runtime.
380 | 
381 | **Benefit:** Never breaks due to runtime changes.
382 | 
383 | **Obvious trade-off.**
384 | 
385 | ---
386 | 
387 | ## 🔒 Edge Cases Handled
388 | 
389 | ### **Edge Case 1: Claude Desktop Not Installed**
390 | 
391 | ```typescript
392 | // getBundledRuntimePath returns null if Claude Desktop not found
393 | if (!claudeBundledNode) {
394 |   // Fall back to system runtime
395 |   return { type: 'system', nodePath: 'node', pythonPath: 'python3' };
396 | }
397 | ```
398 | 
399 | ### **Edge Case 2: Bundled Runtime Missing**
400 | 
401 | ```typescript
402 | // Check if bundled runtime actually exists
403 | if (claudeBundledNode && existsSync(claudeBundledNode)) {
404 |   // Use it
405 | } else {
406 |   // Fall back to system
407 | }
408 | ```
409 | 
410 | ### **Edge Case 3: Running in Test Environment**
411 | 
412 | ```typescript
413 | // In tests, use system runtime (for predictability)
414 | if (process.env.NODE_ENV === 'test') {
415 |   return { type: 'system', nodePath: 'node', pythonPath: 'python3' };
416 | }
417 | ```
418 | 
419 | ### **Edge Case 4: Symlinked Global Install**
420 | 
421 | ```typescript
422 | // process.execPath follows symlinks
423 | // /usr/local/bin/ncp (symlink) → /usr/lib/node_modules/ncp/... (real)
424 | const realPath = realpathSync(process.execPath);
425 | // Use real path for detection
426 | ```
427 | 
428 | **NCP handles all the weird scenarios. You don't have to think about it.**
429 | 
430 | ---
431 | 
432 | ## 📚 Deep Dive
433 | 
434 | Want the full technical implementation?
435 | 
436 | - **Runtime Detector:** [src/utils/runtime-detector.ts]
437 | - **Client Registry:** [src/utils/client-registry.ts]
438 | - **Command Translation:** [Runtime detection summary]
439 | - **Platform Support:** [docs/technical/platform-detection.md]
440 | 
441 | ---
442 | 
443 | ## 🔗 Next Story
444 | 
445 | **[Story 6: Official Registry →](06-official-registry.md)**
446 | 
447 | *How AI discovers 2,200+ MCPs without you lifting a finger*
448 | 
449 | ---
450 | 
451 | ## 💬 Questions?
452 | 
453 | **Q: What if I want to force a specific runtime?**
454 | 
455 | A: Set environment variable: `NCP_FORCE_RUNTIME=/path/to/node`. NCP will respect it. (Advanced users only!)
456 | 
457 | **Q: Can I see which runtime was detected?**
458 | 
459 | A: Yes! Check NCP startup logs or run `ncp --debug`. Shows detected runtime type and paths.
460 | 
461 | **Q: What if Claude Desktop's bundled runtime is broken?**
462 | 
463 | A: NCP will detect it's not working (spawn fails) and log error. You can manually configure system runtime as fallback.
464 | 
465 | **Q: Does runtime detection work for Python MCPs?**
466 | 
467 | A: Yes! NCP detects both Node.js and Python bundled runtimes. Same logic applies.
468 | 
469 | **Q: What about other runtimes (Go, Rust, etc.)?**
470 | 
471 | A: MCPs in compiled languages (Go, Rust) don't need runtime detection. They're self-contained binaries. NCP just runs them as-is.
472 | 
473 | ---
474 | 
475 | **[← Previous Story](04-double-click-install.md)** | **[Back to Story Index](../README.md#the-six-stories)** | **[Next Story →](06-official-registry.md)**
476 | 
```

--------------------------------------------------------------------------------
/test/integration/mcp-client-simulation.test.cjs:
--------------------------------------------------------------------------------

```
  1 | #!/usr/bin/env node
  2 | /**
  3 |  * Integration Test: MCP Client Simulation
  4 |  *
  5 |  * Simulates real AI client behavior (Claude Desktop, Perplexity) to catch bugs
  6 |  * that unit tests miss. This should be run before EVERY release.
  7 |  *
  8 |  * Tests:
  9 |  * 1. Server responds to initialize immediately
 10 |  * 2. tools/list returns tools < 100ms even during indexing
 11 |  * 3. find returns partial results during indexing (not empty)
 12 |  * 4. Cache profileHash persists across restarts
 13 |  * 5. Second startup uses cache (no re-indexing)
 14 |  */
 15 | 
 16 | const { spawn } = require('child_process');
 17 | const fs = require('fs');
 18 | const path = require('path');
 19 | const os = require('os');
 20 | 
 21 | // Test configuration
 22 | const NCP_DIR = path.join(os.homedir(), '.ncp');
 23 | const PROFILES_DIR = path.join(NCP_DIR, 'profiles');
 24 | const CACHE_DIR = path.join(NCP_DIR, 'cache');
 25 | const TEST_PROFILE = 'integration-test';
 26 | const TIMEOUT_MS = 10000;
 27 | 
 28 | // Ensure test profile exists
 29 | function setupTestProfile() {
 30 |   // Create .ncp directory structure
 31 |   if (!fs.existsSync(PROFILES_DIR)) {
 32 |     fs.mkdirSync(PROFILES_DIR, { recursive: true });
 33 |   }
 34 |   if (!fs.existsSync(CACHE_DIR)) {
 35 |     fs.mkdirSync(CACHE_DIR, { recursive: true });
 36 |   }
 37 | 
 38 |   // Create minimal test profile with filesystem MCP
 39 |   const profilePath = path.join(PROFILES_DIR, `${TEST_PROFILE}.json`);
 40 |   const testProfile = {
 41 |     mcpServers: {
 42 |       filesystem: {
 43 |         command: 'npx',
 44 |         args: ['@modelcontextprotocol/server-filesystem']
 45 |       }
 46 |     }
 47 |   };
 48 | 
 49 |   fs.writeFileSync(profilePath, JSON.stringify(testProfile, null, 2));
 50 |   logInfo(`Created test profile at ${profilePath}`);
 51 | }
 52 | 
 53 | // ANSI colors for output
 54 | const colors = {
 55 |   green: '\x1b[32m',
 56 |   red: '\x1b[31m',
 57 |   yellow: '\x1b[33m',
 58 |   blue: '\x1b[34m',
 59 |   reset: '\x1b[0m'
 60 | };
 61 | 
 62 | function log(emoji, message, color = 'reset') {
 63 |   console.log(`${emoji} ${colors[color]}${message}${colors.reset}`);
 64 | }
 65 | 
 66 | function logError(message) {
 67 |   log('❌', `FAIL: ${message}`, 'red');
 68 | }
 69 | 
 70 | function logSuccess(message) {
 71 |   log('✓', message, 'green');
 72 | }
 73 | 
 74 | function logInfo(message) {
 75 |   log('ℹ️', message, 'blue');
 76 | }
 77 | 
 78 | class MCPClientSimulator {
 79 |   constructor() {
 80 |     this.ncp = null;
 81 |     this.responses = [];
 82 |     this.responseBuffer = '';
 83 |     this.requestId = 0;
 84 |   }
 85 | 
 86 |   start() {
 87 |     return new Promise((resolve, reject) => {
 88 |       logInfo('Starting NCP MCP server...');
 89 | 
 90 |       this.ncp = spawn('node', ['dist/index.js', '--profile', TEST_PROFILE], {
 91 |         stdio: ['pipe', 'pipe', 'pipe'],
 92 |         env: {
 93 |           ...process.env,
 94 |           NCP_MODE: 'mcp',
 95 |           NO_COLOR: 'true',  // Disable colors in output
 96 |           NCP_DEBUG: 'true'  // Enable debug logging
 97 |         }
 98 |       });
 99 | 
100 |       this.ncp.stdout.on('data', (data) => {
101 |         this.responseBuffer += data.toString();
102 |         const lines = this.responseBuffer.split('\n');
103 | 
104 |         lines.slice(0, -1).forEach(line => {
105 |           if (line.trim()) {
106 |             try {
107 |               const response = JSON.parse(line);
108 |               this.responses.push(response);
109 |             } catch (e) {
110 |               // Ignore non-JSON lines (logs, etc.)
111 |             }
112 |           }
113 |         });
114 | 
115 |         this.responseBuffer = lines[lines.length - 1];
116 |       });
117 | 
118 |       this.ncp.stderr.on('data', (data) => {
119 |         // Collect stderr for debugging
120 |         const msg = data.toString();
121 |         if (msg.includes('[DEBUG]')) {
122 |           console.log(msg.trim());
123 |         }
124 |       });
125 | 
126 |       this.ncp.on('error', reject);
127 | 
128 |       // Give it a moment to start
129 |       setTimeout(resolve, 100);
130 |     });
131 |   }
132 | 
133 |   sendRequest(method, params = {}) {
134 |     this.requestId++;
135 |     const request = {
136 |       jsonrpc: '2.0',
137 |       id: this.requestId,
138 |       method,
139 |       params
140 |     };
141 | 
142 |     this.ncp.stdin.write(JSON.stringify(request) + '\n');
143 |     return this.requestId;
144 |   }
145 | 
146 |   waitForResponse(id, timeoutMs = 5000) {
147 |     return new Promise((resolve, reject) => {
148 |       const startTime = Date.now();
149 | 
150 |       const checkResponse = () => {
151 |         const response = this.responses.find(r => r.id === id);
152 |         if (response) {
153 |           resolve(response);
154 |           return;
155 |         }
156 | 
157 |         if (Date.now() - startTime > timeoutMs) {
158 |           reject(new Error(`Timeout waiting for response to request ${id}`));
159 |           return;
160 |         }
161 | 
162 |         setTimeout(checkResponse, 10);
163 |       };
164 | 
165 |       checkResponse();
166 |     });
167 |   }
168 | 
169 |   async stop() {
170 |     if (this.ncp) {
171 |       this.ncp.kill();
172 |       await new Promise(resolve => setTimeout(resolve, 100));
173 |     }
174 |   }
175 | }
176 | 
177 | async function test1_Initialize() {
178 |   logInfo('Test 1: Initialize request responds immediately');
179 | 
180 |   const client = new MCPClientSimulator();
181 |   await client.start();
182 | 
183 |   const startTime = Date.now();
184 |   const id = client.sendRequest('initialize', {
185 |     protocolVersion: '2024-11-05',
186 |     capabilities: {},
187 |     clientInfo: { name: 'test-client', version: '1.0.0' }
188 |   });
189 | 
190 |   const response = await client.waitForResponse(id);
191 |   const duration = Date.now() - startTime;
192 | 
193 |   await client.stop();
194 | 
195 |   if (response.error) {
196 |     logError(`Initialize failed: ${response.error.message}`);
197 |     return false;
198 |   }
199 | 
200 |   if (duration > 1000) {
201 |     logError(`Initialize took ${duration}ms (should be < 1000ms)`);
202 |     return false;
203 |   }
204 | 
205 |   if (!response.result?.protocolVersion) {
206 |     logError('Initialize response missing protocolVersion');
207 |     return false;
208 |   }
209 | 
210 |   logSuccess(`Initialize responded in ${duration}ms`);
211 |   return true;
212 | }
213 | 
214 | async function test2_ToolsListDuringIndexing() {
215 |   logInfo('Test 2: tools/list responds < 100ms even during indexing');
216 | 
217 |   const client = new MCPClientSimulator();
218 |   await client.start();
219 | 
220 |   // Call tools/list immediately (during indexing)
221 |   const startTime = Date.now();
222 |   const id = client.sendRequest('tools/list');
223 | 
224 |   const response = await client.waitForResponse(id);
225 |   const duration = Date.now() - startTime;
226 | 
227 |   await client.stop();
228 | 
229 |   if (response.error) {
230 |     logError(`tools/list failed: ${response.error.message}`);
231 |     return false;
232 |   }
233 | 
234 |   if (duration > 100) {
235 |     logError(`tools/list took ${duration}ms (should be < 100ms)`);
236 |     return false;
237 |   }
238 | 
239 |   if (!response.result?.tools || response.result.tools.length === 0) {
240 |     logError('tools/list returned no tools');
241 |     return false;
242 |   }
243 | 
244 |   const toolNames = response.result.tools.map(t => t.name);
245 |   if (!toolNames.includes('find') || !toolNames.includes('run')) {
246 |     logError(`tools/list missing required tools. Got: ${toolNames.join(', ')}`);
247 |     return false;
248 |   }
249 | 
250 |   logSuccess(`tools/list responded in ${duration}ms with ${response.result.tools.length} tools`);
251 |   return true;
252 | }
253 | 
254 | async function test3_FindDuringIndexing() {
255 |   logInfo('Test 3: find returns partial results during indexing (not empty)');
256 | 
257 |   const client = new MCPClientSimulator();
258 |   await client.start();
259 | 
260 |   // Call find immediately (during indexing) - like Perplexity does
261 |   const id = client.sendRequest('tools/call', {
262 |     name: 'find',
263 |     arguments: { description: 'list files' }
264 |   });
265 | 
266 |   const response = await client.waitForResponse(id, 10000);
267 | 
268 |   await client.stop();
269 | 
270 |   if (response.error) {
271 |     logError(`find failed: ${response.error.message}`);
272 |     return false;
273 |   }
274 | 
275 |   const text = response.result?.content?.[0]?.text || '';
276 | 
277 |   // Should either:
278 |   // 1. Return partial results with indexing message
279 |   // 2. Return "indexing in progress" message
280 |   // Should NOT return blank or "No tools found" without context
281 | 
282 |   if (text.includes('No tools found') && !text.includes('Indexing')) {
283 |     logError('find returned empty without indexing context');
284 |     return false;
285 |   }
286 | 
287 |   if (text.length === 0) {
288 |     logError('find returned empty response');
289 |     return false;
290 |   }
291 | 
292 |   const hasIndexingMessage = text.includes('Indexing in progress') || text.includes('indexing');
293 |   const hasResults = text.includes('**') || text.includes('tools') || text.includes('MCP');
294 | 
295 |   if (!hasIndexingMessage && !hasResults) {
296 |     logError('find response has neither indexing message nor results');
297 |     return false;
298 |   }
299 | 
300 |   logSuccess(`find returned ${hasResults ? 'partial results' : 'indexing message'}`);
301 |   return true;
302 | }
303 | 
304 | async function test4_CacheProfileHashPersists() {
305 |   logInfo('Test 4: Cache profileHash persists correctly');
306 | 
307 |   // Clear cache first
308 |   const metaPath = path.join(CACHE_DIR, `${TEST_PROFILE}-cache-meta.json`);
309 |   const csvPath = path.join(CACHE_DIR, `${TEST_PROFILE}-tools.csv`);
310 | 
311 |   if (fs.existsSync(metaPath)) {
312 |     fs.unlinkSync(metaPath);
313 |   }
314 |   if (fs.existsSync(csvPath)) {
315 |     fs.unlinkSync(csvPath);
316 |   }
317 | 
318 |   // Start server and let it create cache
319 |   const client1 = new MCPClientSimulator();
320 |   await client1.start();
321 | 
322 |   const id1 = client1.sendRequest('tools/call', {
323 |     name: 'find',
324 |     arguments: {}
325 |   });
326 | 
327 |   await client1.waitForResponse(id1, 10000);
328 | 
329 |   // Wait a bit for indexing to potentially complete
330 |   await new Promise(resolve => setTimeout(resolve, 2000));
331 | 
332 |   await client1.stop();
333 | 
334 |   // Wait for cache to be finalized and written
335 |   await new Promise(resolve => setTimeout(resolve, 1000));
336 | 
337 |   // Check cache metadata
338 |   if (!fs.existsSync(metaPath)) {
339 |     logError('Cache metadata file not created');
340 |     logInfo(`Expected at: ${metaPath}`);
341 | 
342 |     // List what's in cache dir for debugging
343 |     if (fs.existsSync(CACHE_DIR)) {
344 |       const files = fs.readdirSync(CACHE_DIR);
345 |       logInfo(`Files in cache dir: ${files.join(', ')}`);
346 |     }
347 | 
348 |     return false;
349 |   }
350 | 
351 |   const metadata = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
352 | 
353 |   if (!metadata.profileHash || metadata.profileHash === '') {
354 |     logError(`profileHash is empty: "${metadata.profileHash}"`);
355 |     return false;
356 |   }
357 | 
358 |   logSuccess(`Cache profileHash saved: ${metadata.profileHash.substring(0, 16)}...`);
359 |   return true;
360 | }
361 | 
362 | async function test5_NoReindexingOnRestart() {
363 |   logInfo('Test 5: Second startup uses cache (no re-indexing)');
364 | 
365 |   const metaPath = path.join(CACHE_DIR, `${TEST_PROFILE}-cache-meta.json`);
366 | 
367 |   // Get initial cache state
368 |   const metaBefore = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
369 |   const hashBefore = metaBefore.profileHash;
370 |   const lastUpdatedBefore = metaBefore.lastUpdated;
371 | 
372 |   // Wait a moment to ensure timestamp would change if re-indexed
373 |   await new Promise(resolve => setTimeout(resolve, 1000));
374 | 
375 |   // Start server again
376 |   const client = new MCPClientSimulator();
377 |   await client.start();
378 | 
379 |   const id = client.sendRequest('tools/call', {
380 |     name: 'find',
381 |     arguments: {}
382 |   });
383 | 
384 |   await client.waitForResponse(id, 10000);
385 |   await client.stop();
386 | 
387 |   // Wait for any potential cache updates
388 |   await new Promise(resolve => setTimeout(resolve, 500));
389 | 
390 |   // Check cache wasn't regenerated
391 |   const metaAfter = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
392 |   const hashAfter = metaAfter.profileHash;
393 | 
394 |   if (hashBefore !== hashAfter) {
395 |     logError(`profileHash changed on restart (cache invalidated):\n  Before: ${hashBefore}\n  After: ${hashAfter}`);
396 |     return false;
397 |   }
398 | 
399 |   // Note: lastUpdated might change slightly due to timestamp updates, that's OK
400 |   // The key is profileHash stays the same
401 | 
402 |   logSuccess('Cache persisted correctly (profileHash unchanged on restart)');
403 |   return true;
404 | }
405 | 
406 | async function runAllTests() {
407 |   console.log('\n' + '='.repeat(60));
408 |   console.log('🧪 NCP Integration Test Suite');
409 |   console.log('   Simulating Real AI Client Behavior');
410 |   console.log('='.repeat(60) + '\n');
411 | 
412 |   // Setup test environment
413 |   setupTestProfile();
414 | 
415 |   const tests = [
416 |     test1_Initialize,
417 |     test2_ToolsListDuringIndexing,
418 |     test3_FindDuringIndexing,
419 |     test4_CacheProfileHashPersists,
420 |     test5_NoReindexingOnRestart
421 |   ];
422 | 
423 |   let passed = 0;
424 |   let failed = 0;
425 | 
426 |   for (const test of tests) {
427 |     try {
428 |       const result = await test();
429 |       if (result) {
430 |         passed++;
431 |       } else {
432 |         failed++;
433 |       }
434 |     } catch (error) {
435 |       logError(`${test.name} threw error: ${error.message}`);
436 |       failed++;
437 |     }
438 |     console.log(''); // Blank line between tests
439 |   }
440 | 
441 |   console.log('='.repeat(60));
442 |   console.log(`📊 Results: ${passed} passed, ${failed} failed`);
443 |   console.log('='.repeat(60) + '\n');
444 | 
445 |   if (failed > 0) {
446 |     console.log('❌ INTEGRATION TESTS FAILED - DO NOT RELEASE\n');
447 |     process.exit(1);
448 |   } else {
449 |     console.log('✅ ALL INTEGRATION TESTS PASSED - Safe to release\n');
450 |     process.exit(0);
451 |   }
452 | }
453 | 
454 | // Cleanup on exit
455 | process.on('exit', () => {
456 |   // Clean up test profile cache if needed
457 |   const metaPath = path.join(CACHE_DIR, `${TEST_PROFILE}-cache-meta.json`);
458 |   if (fs.existsSync(metaPath)) {
459 |     // Optionally clean up: fs.unlinkSync(metaPath);
460 |   }
461 | });
462 | 
463 | // Run tests
464 | runAllTests().catch(error => {
465 |   logError(`Test suite crashed: ${error.message}`);
466 |   console.error(error);
467 |   process.exit(1);
468 | });
469 | 
```

--------------------------------------------------------------------------------
/src/analytics/log-parser.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * NCP Analytics Log Parser
  3 |  * Parses real MCP session logs to extract performance and usage insights
  4 |  */
  5 | 
  6 | import { readFileSync, readdirSync, statSync } from 'fs';
  7 | import { join } from 'path';
  8 | import * as os from 'os';
  9 | 
 10 | export interface MCPSession {
 11 |   mcpName: string;
 12 |   startTime: Date;
 13 |   endTime?: Date;
 14 |   duration?: number;
 15 |   toolCount?: number;
 16 |   tools?: string[];
 17 |   exitCode?: number;
 18 |   success: boolean;
 19 |   responseSize: number;
 20 |   errorMessages: string[];
 21 | }
 22 | 
 23 | export interface AnalyticsReport {
 24 |   totalSessions: number;
 25 |   uniqueMCPs: number;
 26 |   timeRange: { start: Date; end: Date };
 27 |   successRate: number;
 28 |   avgSessionDuration: number;
 29 |   totalResponseSize: number;
 30 |   topMCPsByUsage: Array<{ name: string; sessions: number; successRate: number }>;
 31 |   topMCPsByTools: Array<{ name: string; toolCount: number }>;
 32 |   performanceMetrics: {
 33 |     fastestMCPs: Array<{ name: string; avgDuration: number }>;
 34 |     slowestMCPs: Array<{ name: string; avgDuration: number }>;
 35 |     mostReliable: Array<{ name: string; successRate: number }>;
 36 |     leastReliable: Array<{ name: string; successRate: number }>;
 37 |   };
 38 |   dailyUsage: Record<string, number>;
 39 |   hourlyUsage: Record<number, number>;
 40 | }
 41 | 
 42 | export class NCPLogParser {
 43 |   private logsDir: string;
 44 | 
 45 |   constructor() {
 46 |     // Always use global ~/.ncp/logs for analytics data
 47 |     // This ensures we analyze the real usage data, not local development data
 48 |     this.logsDir = join(os.homedir(), '.ncp', 'logs');
 49 |   }
 50 | 
 51 |   /**
 52 |    * Parse a single log file to extract session data
 53 |    */
 54 |   private parseLogFile(filePath: string): MCPSession[] {
 55 |     try {
 56 |       const content = readFileSync(filePath, 'utf-8');
 57 |       const sessions: MCPSession[] = [];
 58 | 
 59 |       // Extract MCP name from filename: mcp-{name}-2025w39.log
 60 |       const fileName = filePath.split('/').pop() || '';
 61 |       const mcpMatch = fileName.match(/mcp-(.+)-\d{4}w\d{2}\.log/);
 62 |       const mcpName = mcpMatch ? mcpMatch[1] : 'unknown';
 63 | 
 64 |       // Split content into individual sessions
 65 |       const sessionBlocks = content.split(/--- MCP .+ Session Started: .+ ---/);
 66 | 
 67 |       for (let i = 1; i < sessionBlocks.length; i++) {
 68 |         const block = sessionBlocks[i];
 69 |         const session = this.parseSessionBlock(mcpName, block);
 70 |         if (session) {
 71 |           sessions.push(session);
 72 |         }
 73 |       }
 74 | 
 75 |       return sessions;
 76 |     } catch (error) {
 77 |       console.error(`Error parsing log file ${filePath}:`, error);
 78 |       return [];
 79 |     }
 80 |   }
 81 | 
 82 |   /**
 83 |    * Parse individual session block
 84 |    */
 85 |   private parseSessionBlock(mcpName: string, block: string): MCPSession | null {
 86 |     try {
 87 |       const lines = block.split('\n').filter(line => line.trim());
 88 | 
 89 |       // Find session start time from the previous separator
 90 |       const sessionStartRegex = /--- MCP .+ Session Started: (.+) ---/;
 91 |       let startTime: Date | undefined;
 92 | 
 93 |       // Look for start time in the content before this block
 94 |       const startMatch = block.match(sessionStartRegex);
 95 |       if (startMatch) {
 96 |         startTime = new Date(startMatch[1]);
 97 |       } else {
 98 |         // Fallback: use first timestamp we can find
 99 |         const firstLine = lines[0];
100 |         if (firstLine) {
101 |           startTime = new Date(); // Use current time as fallback
102 |         }
103 |       }
104 | 
105 |       if (!startTime) return null;
106 | 
107 |       let toolCount = 0;
108 |       let tools: string[] = [];
109 |       let exitCode: number | undefined;
110 |       let responseSize = 0;
111 |       let errorMessages: string[] = [];
112 |       let endTime: Date | undefined;
113 | 
114 |       for (const line of lines) {
115 |         // Extract tool information
116 |         if (line.includes('Loaded MCP with') && line.includes('tools:')) {
117 |           const toolMatch = line.match(/Loaded MCP with (\d+) tools: (.+)/);
118 |           if (toolMatch) {
119 |             toolCount = parseInt(toolMatch[1]);
120 |             tools = toolMatch[2].split(', ').map(t => t.trim());
121 |           }
122 |         }
123 | 
124 |         // Extract JSON responses and their size
125 |         if (line.startsWith('[STDOUT]') && line.includes('{"result"')) {
126 |           const jsonPart = line.substring('[STDOUT] '.length);
127 |           responseSize += jsonPart.length;
128 |         }
129 | 
130 |         // Extract errors
131 |         if (line.includes('[STDERR]') && (line.includes('Error') || line.includes('Failed'))) {
132 |           errorMessages.push(line);
133 |         }
134 | 
135 |         // Extract exit code
136 |         if (line.includes('[EXIT] Process exited with code')) {
137 |           const exitMatch = line.match(/code (\d+)/);
138 |           if (exitMatch) {
139 |             exitCode = parseInt(exitMatch[1]);
140 |             endTime = new Date(startTime.getTime() + 5000); // Estimate end time
141 |           }
142 |         }
143 |       }
144 | 
145 |       // Calculate duration (estimated)
146 |       const duration = endTime ? endTime.getTime() - startTime.getTime() : undefined;
147 |       const success = exitCode === 0 || exitCode === undefined || (toolCount > 0 && responseSize > 0);
148 | 
149 |       return {
150 |         mcpName,
151 |         startTime,
152 |         endTime,
153 |         duration,
154 |         toolCount: toolCount || undefined,
155 |         tools: tools.length > 0 ? tools : undefined,
156 |         exitCode,
157 |         success,
158 |         responseSize,
159 |         errorMessages
160 |       };
161 |     } catch (error) {
162 |       return null;
163 |     }
164 |   }
165 | 
166 |   /**
167 |    * Parse all log files and generate analytics report
168 |    * @param options - Filter options for time range
169 |    */
170 |   async parseAllLogs(options?: {
171 |     from?: Date;
172 |     to?: Date;
173 |     period?: number; // days
174 |     today?: boolean;
175 |   }): Promise<AnalyticsReport> {
176 |     const sessions: MCPSession[] = [];
177 | 
178 |     try {
179 |       const logFiles = readdirSync(this.logsDir)
180 |         .filter(file => file.endsWith('.log'))
181 |         .map(file => join(this.logsDir, file));
182 | 
183 |       console.log(`📊 Parsing ${logFiles.length} log files...`);
184 | 
185 |       // Calculate date range
186 |       let fromDate: Date | undefined;
187 |       let toDate: Date | undefined;
188 | 
189 |       if (options?.today) {
190 |         // Today only
191 |         fromDate = new Date();
192 |         fromDate.setHours(0, 0, 0, 0);
193 |         toDate = new Date();
194 |         toDate.setHours(23, 59, 59, 999);
195 |       } else if (options?.period) {
196 |         // Last N days
197 |         toDate = new Date();
198 |         fromDate = new Date();
199 |         fromDate.setDate(fromDate.getDate() - options.period);
200 |         fromDate.setHours(0, 0, 0, 0);
201 |       } else if (options?.from || options?.to) {
202 |         // Custom range
203 |         fromDate = options.from;
204 |         toDate = options.to || new Date();
205 | 
206 |         // If toDate is provided, set to end of that day
207 |         if (options?.to) {
208 |           toDate = new Date(options.to);
209 |           toDate.setHours(23, 59, 59, 999);
210 |         }
211 | 
212 |         // If fromDate is provided, set to start of that day
213 |         if (options?.from) {
214 |           fromDate = new Date(options.from);
215 |           fromDate.setHours(0, 0, 0, 0);
216 |         }
217 |       }
218 | 
219 |       for (const logFile of logFiles) {
220 |         const fileSessions = this.parseLogFile(logFile);
221 | 
222 |         // Filter sessions by date range if specified
223 |         const filteredSessions = fromDate || toDate
224 |           ? fileSessions.filter(session => {
225 |               if (fromDate && session.startTime < fromDate) return false;
226 |               if (toDate && session.startTime > toDate) return false;
227 |               return true;
228 |             })
229 |           : fileSessions;
230 | 
231 |         sessions.push(...filteredSessions);
232 |       }
233 | 
234 |       if (fromDate || toDate) {
235 |         const rangeDesc = options?.today
236 |           ? 'today'
237 |           : options?.period
238 |           ? `last ${options.period} days`
239 |           : `${fromDate?.toLocaleDateString() || 'start'} to ${toDate?.toLocaleDateString() || 'now'}`;
240 |         console.log(`📅 Filtering for ${rangeDesc}: ${sessions.length} sessions`);
241 |       }
242 | 
243 |       return this.generateReport(sessions);
244 |     } catch (error) {
245 |       console.error('Error reading logs directory:', error);
246 |       return this.generateReport(sessions);
247 |     }
248 |   }
249 | 
250 |   /**
251 |    * Generate comprehensive analytics report
252 |    */
253 |   private generateReport(sessions: MCPSession[]): AnalyticsReport {
254 |     if (sessions.length === 0) {
255 |       return {
256 |         totalSessions: 0,
257 |         uniqueMCPs: 0,
258 |         timeRange: { start: new Date(), end: new Date() },
259 |         successRate: 0,
260 |         avgSessionDuration: 0,
261 |         totalResponseSize: 0,
262 |         topMCPsByUsage: [],
263 |         topMCPsByTools: [],
264 |         performanceMetrics: {
265 |           fastestMCPs: [],
266 |           slowestMCPs: [],
267 |           mostReliable: [],
268 |           leastReliable: []
269 |         },
270 |         dailyUsage: {},
271 |         hourlyUsage: {}
272 |       };
273 |     }
274 | 
275 |     // Basic metrics
276 |     const totalSessions = sessions.length;
277 |     const uniqueMCPs = new Set(sessions.map(s => s.mcpName)).size;
278 |     const successfulSessions = sessions.filter(s => s.success).length;
279 |     const successRate = (successfulSessions / totalSessions) * 100;
280 | 
281 |     // Time range
282 |     const sortedByTime = sessions.filter(s => s.startTime).sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
283 |     const timeRange = {
284 |       start: sortedByTime[0]?.startTime || new Date(),
285 |       end: sortedByTime[sortedByTime.length - 1]?.startTime || new Date()
286 |     };
287 | 
288 |     // Duration metrics
289 |     const sessionsWithDuration = sessions.filter(s => s.duration && s.duration > 0);
290 |     const avgSessionDuration = sessionsWithDuration.length > 0
291 |       ? sessionsWithDuration.reduce((sum, s) => sum + (s.duration || 0), 0) / sessionsWithDuration.length
292 |       : 0;
293 | 
294 |     // Response size
295 |     const totalResponseSize = sessions.reduce((sum, s) => sum + s.responseSize, 0);
296 | 
297 |     // MCP usage statistics
298 |     const mcpStats = new Map<string, { sessions: number; successes: number; totalTools: number; durations: number[] }>();
299 | 
300 |     for (const session of sessions) {
301 |       const stats = mcpStats.get(session.mcpName) || { sessions: 0, successes: 0, totalTools: 0, durations: [] };
302 |       stats.sessions++;
303 |       if (session.success) stats.successes++;
304 |       if (session.toolCount) stats.totalTools = Math.max(stats.totalTools, session.toolCount);
305 |       if (session.duration && session.duration > 0) stats.durations.push(session.duration);
306 |       mcpStats.set(session.mcpName, stats);
307 |     }
308 | 
309 |     // Top MCPs by usage
310 |     const topMCPsByUsage = Array.from(mcpStats.entries())
311 |       .map(([name, stats]) => ({
312 |         name,
313 |         sessions: stats.sessions,
314 |         successRate: (stats.successes / stats.sessions) * 100
315 |       }))
316 |       .sort((a, b) => b.sessions - a.sessions)
317 |       .slice(0, 10);
318 | 
319 |     // Top MCPs by tool count
320 |     const topMCPsByTools = Array.from(mcpStats.entries())
321 |       .filter(([_, stats]) => stats.totalTools > 0)
322 |       .map(([name, stats]) => ({
323 |         name,
324 |         toolCount: stats.totalTools
325 |       }))
326 |       .sort((a, b) => b.toolCount - a.toolCount)
327 |       .slice(0, 10);
328 | 
329 |     // Performance metrics
330 |     const mcpPerformance = Array.from(mcpStats.entries())
331 |       .map(([name, stats]) => ({
332 |         name,
333 |         avgDuration: stats.durations.length > 0 ? stats.durations.reduce((a, b) => a + b, 0) / stats.durations.length : 0,
334 |         successRate: (stats.successes / stats.sessions) * 100
335 |       }))
336 |       .filter(m => m.avgDuration > 0);
337 | 
338 |     const fastestMCPs = mcpPerformance
339 |       .sort((a, b) => a.avgDuration - b.avgDuration)
340 |       .slice(0, 5);
341 | 
342 |     const slowestMCPs = mcpPerformance
343 |       .sort((a, b) => b.avgDuration - a.avgDuration)
344 |       .slice(0, 5);
345 | 
346 |     const mostReliable = Array.from(mcpStats.entries())
347 |       .map(([name, stats]) => ({
348 |         name,
349 |         successRate: (stats.successes / stats.sessions) * 100
350 |       }))
351 |       .filter(m => mcpStats.get(m.name)!.sessions >= 3) // At least 3 sessions for reliability
352 |       .sort((a, b) => b.successRate - a.successRate)
353 |       .slice(0, 5);
354 | 
355 |     const leastReliable = Array.from(mcpStats.entries())
356 |       .map(([name, stats]) => ({
357 |         name,
358 |         successRate: (stats.successes / stats.sessions) * 100
359 |       }))
360 |       .filter(m => mcpStats.get(m.name)!.sessions >= 3)
361 |       .sort((a, b) => a.successRate - b.successRate)
362 |       .slice(0, 5);
363 | 
364 |     // Daily usage
365 |     const dailyUsage: Record<string, number> = {};
366 |     for (const session of sessions) {
367 |       const day = session.startTime.toISOString().split('T')[0];
368 |       dailyUsage[day] = (dailyUsage[day] || 0) + 1;
369 |     }
370 | 
371 |     // Hourly usage
372 |     const hourlyUsage: Record<number, number> = {};
373 |     for (const session of sessions) {
374 |       const hour = session.startTime.getHours();
375 |       hourlyUsage[hour] = (hourlyUsage[hour] || 0) + 1;
376 |     }
377 | 
378 |     return {
379 |       totalSessions,
380 |       uniqueMCPs,
381 |       timeRange,
382 |       successRate,
383 |       avgSessionDuration,
384 |       totalResponseSize,
385 |       topMCPsByUsage,
386 |       topMCPsByTools,
387 |       performanceMetrics: {
388 |         fastestMCPs,
389 |         slowestMCPs,
390 |         mostReliable,
391 |         leastReliable
392 |       },
393 |       dailyUsage,
394 |       hourlyUsage
395 |     };
396 |   }
397 | }
```

--------------------------------------------------------------------------------
/test/ecosystem-discovery-validation-simple.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Simple Ecosystem Discovery Validation
  3 |  * Tests that NCP can find relevant tools from our realistic MCP ecosystem
  4 |  */
  5 | 
  6 | import { DiscoveryEngine } from '../src/discovery/engine.js';
  7 | 
  8 | describe('Simple Ecosystem Discovery Validation', () => {
  9 |   let engine: DiscoveryEngine;
 10 | 
 11 |   beforeAll(async () => {
 12 |     engine = new DiscoveryEngine();
 13 |     await engine.initialize();
 14 | 
 15 |     // Clear any existing cached tools to ensure clean test environment
 16 |     await engine['ragEngine'].clearCache();
 17 | 
 18 |     // Create comprehensive ecosystem with 20 realistic tools
 19 |     const ecosystemTools = [
 20 |       // Database Operations
 21 |       { name: 'query', description: 'Execute SQL queries to retrieve data from PostgreSQL database tables. Find records, search data, analyze information.', mcpName: 'postgres-test' },
 22 |       { name: 'insert', description: 'Insert new records into PostgreSQL database tables. Store customer data, add new information, create records.', mcpName: 'postgres-test' },
 23 |       { name: 'execute_cypher', description: 'Execute Cypher queries on Neo4j graph database. Query relationships, find patterns, analyze connections.', mcpName: 'neo4j-test' },
 24 | 
 25 |       // Payment Processing
 26 |       { name: 'create_payment', description: 'Process credit card payments and charges from customers. Charge customer for order, process payment from customer.', mcpName: 'stripe-test' },
 27 |       { name: 'refund_payment', description: 'Process refunds for previously charged payments. Refund cancelled subscription, return customer money.', mcpName: 'stripe-test' },
 28 | 
 29 |       // Developer Tools
 30 |       { name: 'create_repository', description: 'Create a new GitHub repository with configuration options. Set up new project, initialize repository.', mcpName: 'github-test' },
 31 |       { name: 'create_issue', description: 'Create GitHub issues for bug reports and feature requests. Report bugs, request features, track tasks.', mcpName: 'github-test' },
 32 |       { name: 'commit_changes', description: 'Create Git commits to save changes to version history. Save progress, commit code changes, record modifications.', mcpName: 'git-test' },
 33 |       { name: 'create_branch', description: 'Create new Git branches for feature development and parallel work. Start new features, create development branches.', mcpName: 'git-test' },
 34 | 
 35 |       // File Operations
 36 |       { name: 'read_file', description: 'Read contents of files from local filesystem. Load configuration files, read text documents, access data files.', mcpName: 'filesystem-test' },
 37 |       { name: 'write_file', description: 'Write content to files on local filesystem. Create configuration files, save data, generate reports.', mcpName: 'filesystem-test' },
 38 |       { name: 'create_directory', description: 'Create new directories and folder structures. Organize files, set up project structure, create folder hierarchies.', mcpName: 'filesystem-test' },
 39 | 
 40 |       // Web Automation
 41 |       { name: 'click_element', description: 'Click on web page elements using selectors. Click buttons, links, form elements.', mcpName: 'playwright-test' },
 42 |       { name: 'take_screenshot', description: 'Capture screenshots of web pages for testing and documentation. Take page screenshots, save visual evidence.', mcpName: 'playwright-test' },
 43 |       { name: 'fill_form_field', description: 'Fill form inputs and text fields on web pages. Enter text, complete forms, input data.', mcpName: 'playwright-test' },
 44 | 
 45 |       // Cloud & Infrastructure
 46 |       { name: 'create_ec2_instance', description: 'Launch new EC2 virtual machine instances with configuration. Create servers, deploy applications to cloud.', mcpName: 'aws-test' },
 47 |       { name: 'upload_to_s3', description: 'Upload files and objects to S3 storage buckets. Store files in cloud, backup data, host static content.', mcpName: 'aws-test' },
 48 |       { name: 'run_container', description: 'Run Docker containers from images with configuration options. Deploy applications, start services.', mcpName: 'docker-test' },
 49 |       { name: 'send_message', description: 'Send messages to Slack channels or direct messages. Share updates, notify teams, communicate with colleagues.', mcpName: 'slack-test' },
 50 |       { name: 'web_search', description: 'Search the web using Brave Search API with privacy protection. Find information, research topics, get current data.', mcpName: 'brave-search-test' },
 51 |     ];
 52 | 
 53 |     // Group by MCP and index
 54 |     const toolsByMCP = new Map();
 55 |     for (const tool of ecosystemTools) {
 56 |       if (!toolsByMCP.has(tool.mcpName)) {
 57 |         toolsByMCP.set(tool.mcpName, []);
 58 |       }
 59 |       toolsByMCP.get(tool.mcpName).push({
 60 |         name: tool.name,
 61 |         description: tool.description
 62 |       });
 63 |     }
 64 | 
 65 |     // Index each MCP
 66 |     for (const [mcpName, tools] of toolsByMCP) {
 67 |       await engine['ragEngine'].indexMCP(mcpName, tools);
 68 |     }
 69 |   });
 70 | 
 71 |   describe('Domain-Specific Discovery', () => {
 72 |     it('finds database tools for data queries', async () => {
 73 |       const results = await engine.findRelevantTools('query customer data from database', 8);
 74 |       expect(results.length).toBeGreaterThan(0);
 75 | 
 76 |       const hasDbTool = results.some(t =>
 77 |         (t.name.includes('postgres') && t.name.includes('query')) ||
 78 |         (t.name.includes('neo4j') && t.name.includes('cypher'))
 79 |       );
 80 |       expect(hasDbTool).toBeTruthy();
 81 |     });
 82 | 
 83 |     it('finds payment tools for financial operations', async () => {
 84 |       const results = await engine.findRelevantTools('process credit card payment', 8);
 85 |       expect(results.length).toBeGreaterThan(0);
 86 | 
 87 |       const hasPaymentTool = results.some(t =>
 88 |         t.name.includes('stripe') && (t.name.includes('payment') || t.name.includes('create'))
 89 |       );
 90 |       expect(hasPaymentTool).toBeTruthy();
 91 |     });
 92 | 
 93 |     it('finds version control tools for code management', async () => {
 94 |       const results = await engine.findRelevantTools('commit code changes', 8);
 95 |       expect(results.length).toBeGreaterThan(0);
 96 | 
 97 |       const hasGitTool = results.some(t =>
 98 |         t.name.includes('git') && t.name.includes('commit')
 99 |       );
100 |       expect(hasGitTool).toBeTruthy();
101 |     });
102 | 
103 |     it('finds file system tools for file operations', async () => {
104 |       const results = await engine.findRelevantTools('save configuration to file', 8);
105 |       expect(results.length).toBeGreaterThan(0);
106 | 
107 |       const hasFileTool = results.some(t =>
108 |         t.name.includes('filesystem') && t.name.includes('write')
109 |       );
110 |       expect(hasFileTool).toBeTruthy();
111 |     });
112 | 
113 |     it('finds web automation tools for browser tasks', async () => {
114 |       const results = await engine.findRelevantTools('take screenshot of webpage', 8);
115 |       expect(results.length).toBeGreaterThan(0);
116 | 
117 |       const hasWebTool = results.some(t =>
118 |         t.name.includes('playwright') && t.name.includes('screenshot')
119 |       );
120 |       expect(hasWebTool).toBeTruthy();
121 |     });
122 | 
123 |     it('finds cloud tools for infrastructure deployment', async () => {
124 |       const results = await engine.findRelevantTools('deploy server to AWS cloud', 8);
125 |       expect(results.length).toBeGreaterThan(0);
126 | 
127 |       // Debug: Log what tools are actually returned
128 |       console.log('Cloud deployment query returned:', results.map(t => ({ name: t.name, confidence: t.confidence || 'N/A' })));
129 | 
130 |       const hasCloudTool = results.some(t =>
131 |         t.name.includes('ec2') || t.name.includes('instance') || t.name.includes('container')
132 |       );
133 |       if (!hasCloudTool) {
134 |         console.log('Expected to find tools with ec2/instance/container but got:', results.map(t => t.name));
135 |       }
136 |       expect(hasCloudTool).toBeTruthy();
137 |     });
138 |   });
139 | 
140 |   describe('Cross-Domain Scenarios', () => {
141 |     it('handles complex multi-domain queries', async () => {
142 |       const results = await engine.findRelevantTools('build and deploy web application with database', 12);
143 |       expect(results.length).toBeGreaterThan(3);
144 | 
145 |       // Should find tools from multiple domains - check for any relevant tools
146 |       const hasDeploymentTools = results.some(r =>
147 |         r.name.includes('docker') || r.name.includes('aws') ||
148 |         r.name.includes('git') || r.name.includes('github')
149 |       );
150 |       const hasDatabaseTools = results.some(r =>
151 |         r.name.includes('postgres') || r.name.includes('neo4j')
152 |       );
153 |       const hasFileTools = results.some(r =>
154 |         r.name.includes('filesystem') || r.name.includes('file')
155 |       );
156 | 
157 |       // Should find tools from at least one relevant domain
158 |       const foundRelevantTools = hasDeploymentTools || hasDatabaseTools || hasFileTools;
159 |       expect(foundRelevantTools).toBeTruthy();
160 |     });
161 | 
162 |     it('prioritizes relevant tools for specific contexts', async () => {
163 |       const results = await engine.findRelevantTools('refund customer payment for cancelled order', 6);
164 |       expect(results.length).toBeGreaterThan(0);
165 | 
166 |       // Refund should be prioritized over create payment
167 |       const refundTool = results.find(t => t.name.includes('refund'));
168 |       const createTool = results.find(t => t.name.includes('create_payment'));
169 | 
170 |       if (refundTool && createTool) {
171 |         expect(results.indexOf(refundTool)).toBeLessThan(results.indexOf(createTool));
172 |       } else {
173 |         expect(refundTool).toBeDefined(); // At minimum, refund tool should be found
174 |       }
175 |     });
176 |   });
177 | 
178 |   describe('Ecosystem Scale Validation', () => {
179 |     it('demonstrates improved specificity with diverse tool set', async () => {
180 |       // Test that having diverse tools improves matching specificity
181 |       const specificQuery = 'create GitHub issue for bug report';
182 |       const results = await engine.findRelevantTools(specificQuery, 6);
183 | 
184 |       expect(results.length).toBeGreaterThan(0);
185 | 
186 |       // Should find the specific GitHub issue tool
187 |       const issueTool = results.find(t =>
188 |         t.name.includes('github') && t.name.includes('issue')
189 |       );
190 |       expect(issueTool).toBeDefined();
191 |     });
192 | 
193 |     it('maintains performance with ecosystem scale', async () => {
194 |       const start = Date.now();
195 | 
196 |       const results = await engine.findRelevantTools('analyze user data and generate report', 8);
197 | 
198 |       const duration = Date.now() - start;
199 | 
200 |       expect(results.length).toBeGreaterThan(0);
201 |       expect(duration).toBeLessThan(1000); // Should complete under 1 second
202 |     });
203 | 
204 |     it('provides consistent results across similar queries', async () => {
205 |       const query1 = 'store files in cloud storage';
206 |       const query2 = 'upload files to cloud bucket';
207 | 
208 |       const results1 = await engine.findRelevantTools(query1, 5);
209 |       const results2 = await engine.findRelevantTools(query2, 5);
210 | 
211 |       expect(results1.length).toBeGreaterThan(0);
212 |       expect(results2.length).toBeGreaterThan(0);
213 | 
214 |       // Should both find S3 upload tool
215 |       const hasS3_1 = results1.some(t => t.name.includes('s3') || t.name.includes('upload'));
216 |       const hasS3_2 = results2.some(t => t.name.includes('s3') || t.name.includes('upload'));
217 | 
218 |       expect(hasS3_1).toBeTruthy();
219 |       expect(hasS3_2).toBeTruthy();
220 |     });
221 |   });
222 | 
223 |   describe('Coverage Validation', () => {
224 |     it('can discover tools from all major ecosystem domains', async () => {
225 |       const domains = [
226 |         { name: 'Database', query: 'database query', expectPattern: ['query', 'cypher'] },
227 |         { name: 'Payment', query: 'payment processing', expectPattern: ['payment', 'create_payment'] },
228 |         { name: 'Version Control', query: 'git repository', expectPattern: ['repository', 'branch', 'commit'] },
229 |         { name: 'File System', query: 'file operations', expectPattern: ['file', 'read_file', 'write_file'] },
230 |         { name: 'Web Automation', query: 'browser automation', expectPattern: ['click', 'screenshot', 'fill'] },
231 |         { name: 'Cloud', query: 'cloud deployment', expectPattern: ['ec2', 'container', 's3'] },
232 |         { name: 'Communication', query: 'team messaging', expectPattern: ['message', 'send_message'] },
233 |         { name: 'Search', query: 'web search', expectPattern: ['search', 'web_search'] }
234 |       ];
235 | 
236 |       let successCount = 0;
237 |       for (const domain of domains) {
238 |         const results = await engine.findRelevantTools(domain.query, 8);
239 | 
240 |         if (results.length === 0) {
241 |           console.log(`⚠️  ${domain.name} query "${domain.query}" returned no results`);
242 |           continue;
243 |         }
244 | 
245 |         const found = results.some(t =>
246 |           domain.expectPattern.some(pattern => t.name.includes(pattern))
247 |         );
248 | 
249 |         if (found) {
250 |           successCount++;
251 |         } else {
252 |           console.log(`❌ ${domain.name} query "${domain.query}" failed pattern matching:`);
253 |           console.log('  Expected patterns:', domain.expectPattern);
254 |           console.log('  Got tools:', results.map(t => t.name));
255 |         }
256 |       }
257 | 
258 |       // Expect at least 80% of domains to work (4 out of 5)
259 |       expect(successCount).toBeGreaterThanOrEqual(4);
260 |     });
261 |   });
262 | });
```

--------------------------------------------------------------------------------
/src/cache/cache-patcher.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Cache Patcher for NCP
  3 |  * Provides incremental, MCP-by-MCP cache patching operations
  4 |  * Enables fast startup by avoiding full re-indexing
  5 |  */
  6 | 
  7 | import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
  8 | import { join } from 'path';
  9 | import { createHash } from 'crypto';
 10 | import { getCacheDirectory } from '../utils/ncp-paths.js';
 11 | import { logger } from '../utils/logger.js';
 12 | 
 13 | export interface Tool {
 14 |   name: string;
 15 |   description: string;
 16 |   inputSchema?: any;
 17 | }
 18 | 
 19 | export interface ToolMetadataCache {
 20 |   version: string;
 21 |   profileHash: string;        // SHA256 of entire profile
 22 |   lastModified: number;
 23 |   mcps: {
 24 |     [mcpName: string]: {
 25 |       configHash: string;      // SHA256 of command+args+env
 26 |       discoveredAt: number;
 27 |       tools: Array<{
 28 |         name: string;
 29 |         description: string;
 30 |         inputSchema: any;
 31 |       }>;
 32 |       serverInfo: {
 33 |         name: string;
 34 |         version: string;
 35 |         description?: string;
 36 |       };
 37 |     }
 38 |   }
 39 | }
 40 | 
 41 | export interface EmbeddingsCache {
 42 |   version: string;
 43 |   modelVersion: string;        // all-MiniLM-L6-v2
 44 |   lastModified: number;
 45 |   vectors: {
 46 |     [toolId: string]: number[];  // toolId = "mcpName:toolName"
 47 |   };
 48 |   metadata: {
 49 |     [toolId: string]: {
 50 |       mcpName: string;
 51 |       generatedAt: number;
 52 |       enhancedDescription: string;  // Used for generation
 53 |     }
 54 |   }
 55 | }
 56 | 
 57 | export interface MCPConfig {
 58 |   command?: string;  // Optional: for stdio transport
 59 |   args?: string[];
 60 |   env?: Record<string, string>;
 61 |   url?: string;  // Optional: for HTTP/SSE transport
 62 | }
 63 | 
 64 | export class CachePatcher {
 65 |   private cacheDir: string;
 66 |   private toolMetadataCachePath: string;
 67 |   private embeddingsCachePath: string;
 68 |   private embeddingsMetadataCachePath: string;
 69 | 
 70 |   constructor() {
 71 |     this.cacheDir = getCacheDirectory();
 72 |     this.toolMetadataCachePath = join(this.cacheDir, 'all-tools.json');
 73 |     this.embeddingsCachePath = join(this.cacheDir, 'embeddings.json');
 74 |     this.embeddingsMetadataCachePath = join(this.cacheDir, 'embeddings-metadata.json');
 75 | 
 76 |     // Ensure cache directory exists
 77 |     if (!existsSync(this.cacheDir)) {
 78 |       mkdirSync(this.cacheDir, { recursive: true });
 79 |     }
 80 |   }
 81 | 
 82 |   /**
 83 |    * Generate SHA256 hash for MCP configuration
 84 |    */
 85 |   generateConfigHash(config: MCPConfig): string {
 86 |     const hashInput = JSON.stringify({
 87 |       command: config.command,
 88 |       args: config.args || [],
 89 |       env: config.env || {}
 90 |     });
 91 |     return createHash('sha256').update(hashInput).digest('hex');
 92 |   }
 93 | 
 94 |   /**
 95 |    * Generate SHA256 hash for entire profile
 96 |    */
 97 |   generateProfileHash(profile: any): string {
 98 |     const hashInput = JSON.stringify(profile.mcpServers || {});
 99 |     return createHash('sha256').update(hashInput).digest('hex');
100 |   }
101 | 
102 |   /**
103 |    * Load cache with atomic file operations and error handling
104 |    */
105 |   private async loadCache<T>(path: string, defaultValue: T): Promise<T> {
106 |     try {
107 |       if (!existsSync(path)) {
108 |         logger.debug(`Cache file not found: ${path}, using default`);
109 |         return defaultValue;
110 |       }
111 | 
112 |       const content = readFileSync(path, 'utf-8');
113 |       const parsed = JSON.parse(content);
114 |       logger.debug(`Loaded cache from ${path}`);
115 |       return parsed as T;
116 |     } catch (error: any) {
117 |       logger.warn(`Failed to load cache from ${path}: ${error.message}, using default`);
118 |       return defaultValue;
119 |     }
120 |   }
121 | 
122 |   /**
123 |    * Save cache with atomic file operations to prevent corruption
124 |    */
125 |   private async saveCache<T>(path: string, data: T): Promise<void> {
126 |     try {
127 |       const tmpPath = `${path}.tmp`;
128 |       const content = JSON.stringify(data, null, 2);
129 | 
130 |       // Write to temporary file first
131 |       writeFileSync(tmpPath, content, 'utf-8');
132 | 
133 |       // Atomic replacement
134 |       await this.atomicReplace(tmpPath, path);
135 | 
136 |       logger.debug(`Saved cache to ${path}`);
137 |     } catch (error: any) {
138 |       logger.error(`Failed to save cache to ${path}: ${error.message}`);
139 |       throw error;
140 |     }
141 |   }
142 | 
143 |   /**
144 |    * Atomic file replacement to prevent corruption
145 |    */
146 |   private async atomicReplace(tmpPath: string, finalPath: string): Promise<void> {
147 |     const fs = await import('fs/promises');
148 |     await fs.rename(tmpPath, finalPath);
149 |   }
150 | 
151 |   /**
152 |    * Load tool metadata cache
153 |    */
154 |   async loadToolMetadataCache(): Promise<ToolMetadataCache> {
155 |     const defaultCache: ToolMetadataCache = {
156 |       version: '1.0.0',
157 |       profileHash: '',
158 |       lastModified: Date.now(),
159 |       mcps: {}
160 |     };
161 | 
162 |     return await this.loadCache(this.toolMetadataCachePath, defaultCache);
163 |   }
164 | 
165 |   /**
166 |    * Save tool metadata cache
167 |    */
168 |   async saveToolMetadataCache(cache: ToolMetadataCache): Promise<void> {
169 |     cache.lastModified = Date.now();
170 |     await this.saveCache(this.toolMetadataCachePath, cache);
171 |   }
172 | 
173 |   /**
174 |    * Load embeddings cache
175 |    */
176 |   async loadEmbeddingsCache(): Promise<EmbeddingsCache> {
177 |     const defaultCache: EmbeddingsCache = {
178 |       version: '1.0.0',
179 |       modelVersion: 'all-MiniLM-L6-v2',
180 |       lastModified: Date.now(),
181 |       vectors: {},
182 |       metadata: {}
183 |     };
184 | 
185 |     return await this.loadCache(this.embeddingsCachePath, defaultCache);
186 |   }
187 | 
188 |   /**
189 |    * Save embeddings cache
190 |    */
191 |   async saveEmbeddingsCache(cache: EmbeddingsCache): Promise<void> {
192 |     cache.lastModified = Date.now();
193 |     await this.saveCache(this.embeddingsCachePath, cache);
194 |   }
195 | 
196 |   /**
197 |    * Patch tool metadata cache - Add MCP
198 |    */
199 |   async patchAddMCP(mcpName: string, config: MCPConfig, tools: Tool[], serverInfo: any): Promise<void> {
200 |     logger.info(`🔧 Patching tool metadata cache: adding ${mcpName}`);
201 | 
202 |     const cache = await this.loadToolMetadataCache();
203 |     const configHash = this.generateConfigHash(config);
204 | 
205 |     cache.mcps[mcpName] = {
206 |       configHash,
207 |       discoveredAt: Date.now(),
208 |       tools: tools.map(tool => ({
209 |         name: tool.name,
210 |         description: tool.description || 'No description available',
211 |         inputSchema: tool.inputSchema || {}
212 |       })),
213 |       serverInfo: {
214 |         name: serverInfo?.name || mcpName,
215 |         version: serverInfo?.version || '1.0.0',
216 |         description: serverInfo?.description
217 |       }
218 |     };
219 | 
220 |     await this.saveToolMetadataCache(cache);
221 |     logger.info(`✅ Added ${tools.length} tools from ${mcpName} to metadata cache`);
222 |   }
223 | 
224 |   /**
225 |    * Patch tool metadata cache - Remove MCP
226 |    */
227 |   async patchRemoveMCP(mcpName: string): Promise<void> {
228 |     logger.info(`🔧 Patching tool metadata cache: removing ${mcpName}`);
229 | 
230 |     const cache = await this.loadToolMetadataCache();
231 | 
232 |     if (cache.mcps[mcpName]) {
233 |       const toolCount = cache.mcps[mcpName].tools.length;
234 |       delete cache.mcps[mcpName];
235 |       await this.saveToolMetadataCache(cache);
236 |       logger.info(`✅ Removed ${toolCount} tools from ${mcpName} from metadata cache`);
237 |     } else {
238 |       logger.warn(`MCP ${mcpName} not found in metadata cache`);
239 |     }
240 |   }
241 | 
242 |   /**
243 |    * Patch tool metadata cache - Update MCP
244 |    */
245 |   async patchUpdateMCP(mcpName: string, config: MCPConfig, tools: Tool[], serverInfo: any): Promise<void> {
246 |     logger.info(`🔧 Patching tool metadata cache: updating ${mcpName}`);
247 | 
248 |     // Remove then add for clean update
249 |     await this.patchRemoveMCP(mcpName);
250 |     await this.patchAddMCP(mcpName, config, tools, serverInfo);
251 |   }
252 | 
253 |   /**
254 |    * Patch embeddings cache - Add MCP tools
255 |    */
256 |   async patchAddEmbeddings(mcpName: string, toolEmbeddings: Map<string, any>): Promise<void> {
257 |     logger.info(`🔧 Patching embeddings cache: adding ${mcpName} vectors`);
258 | 
259 |     const cache = await this.loadEmbeddingsCache();
260 |     let addedCount = 0;
261 | 
262 |     for (const [toolId, embeddingData] of toolEmbeddings) {
263 |       if (embeddingData && embeddingData.embedding) {
264 |         // Convert Float32Array to regular array for JSON serialization
265 |         cache.vectors[toolId] = Array.from(embeddingData.embedding);
266 |         cache.metadata[toolId] = {
267 |           mcpName,
268 |           generatedAt: Date.now(),
269 |           enhancedDescription: embeddingData.enhancedDescription || ''
270 |         };
271 |         addedCount++;
272 |       }
273 |     }
274 | 
275 |     await this.saveEmbeddingsCache(cache);
276 |     logger.info(`✅ Added ${addedCount} embeddings for ${mcpName}`);
277 |   }
278 | 
279 |   /**
280 |    * Patch embeddings cache - Remove MCP tools
281 |    */
282 |   async patchRemoveEmbeddings(mcpName: string): Promise<void> {
283 |     logger.info(`🔧 Patching embeddings cache: removing ${mcpName} vectors`);
284 | 
285 |     const cache = await this.loadEmbeddingsCache();
286 |     let removedCount = 0;
287 | 
288 |     // Remove all tool embeddings for this MCP
289 |     const toolIdsToRemove = Object.keys(cache.metadata).filter(
290 |       toolId => cache.metadata[toolId].mcpName === mcpName
291 |     );
292 | 
293 |     for (const toolId of toolIdsToRemove) {
294 |       delete cache.vectors[toolId];
295 |       delete cache.metadata[toolId];
296 |       removedCount++;
297 |     }
298 | 
299 |     await this.saveEmbeddingsCache(cache);
300 |     logger.info(`✅ Removed ${removedCount} embeddings for ${mcpName}`);
301 |   }
302 | 
303 |   /**
304 |    * Update profile hash in tool metadata cache
305 |    */
306 |   async updateProfileHash(profileHash: string): Promise<void> {
307 |     const cache = await this.loadToolMetadataCache();
308 |     cache.profileHash = profileHash;
309 |     await this.saveToolMetadataCache(cache);
310 |     logger.debug(`Updated profile hash: ${profileHash.substring(0, 8)}...`);
311 |   }
312 | 
313 |   /**
314 |    * Validate if cache is current with profile
315 |    */
316 |   async validateCacheWithProfile(currentProfileHash: string): Promise<boolean> {
317 |     try {
318 |       const cache = await this.loadToolMetadataCache();
319 | 
320 |       // Handle empty or corrupt cache
321 |       if (!cache || !cache.profileHash) {
322 |         logger.info('Cache validation failed: no profile hash found');
323 |         return false;
324 |       }
325 | 
326 |       // Handle version mismatches
327 |       if (cache.version !== '1.0.0') {
328 |         logger.info(`Cache validation failed: version mismatch (${cache.version} → 1.0.0)`);
329 |         return false;
330 |       }
331 | 
332 |       const isValid = cache.profileHash === currentProfileHash;
333 | 
334 |       if (!isValid) {
335 |         logger.info(`Cache validation failed: profile changed (${cache.profileHash?.substring(0, 8)}... → ${currentProfileHash.substring(0, 8)}...)`);
336 |       } else {
337 |         logger.debug(`Cache validation passed: ${currentProfileHash.substring(0, 8)}...`);
338 |       }
339 | 
340 |       return isValid;
341 |     } catch (error: any) {
342 |       logger.warn(`Cache validation error: ${error.message}`);
343 |       return false;
344 |     }
345 |   }
346 | 
347 |   /**
348 |    * Validate cache integrity and repair if needed
349 |    */
350 |   async validateAndRepairCache(): Promise<{ valid: boolean; repaired: boolean }> {
351 |     try {
352 |       const stats = await this.getCacheStats();
353 | 
354 |       if (!stats.toolMetadataExists) {
355 |         logger.warn('Tool metadata cache missing');
356 |         return { valid: false, repaired: false };
357 |       }
358 | 
359 |       const cache = await this.loadToolMetadataCache();
360 | 
361 |       // Check for corruption
362 |       if (!cache.mcps || typeof cache.mcps !== 'object') {
363 |         logger.warn('Cache corruption detected: invalid mcps structure');
364 |         return { valid: false, repaired: false };
365 |       }
366 | 
367 |       // Check for missing tools
368 |       let hasMissingTools = false;
369 |       for (const [mcpName, mcpData] of Object.entries(cache.mcps)) {
370 |         if (!Array.isArray(mcpData.tools)) {
371 |           logger.warn(`Cache corruption detected: invalid tools array for ${mcpName}`);
372 |           hasMissingTools = true;
373 |         }
374 |       }
375 | 
376 |       if (hasMissingTools) {
377 |         logger.warn('Cache has missing or invalid tool data');
378 |         return { valid: false, repaired: false };
379 |       }
380 | 
381 |       logger.debug('Cache integrity validation passed');
382 |       return { valid: true, repaired: false };
383 | 
384 |     } catch (error: any) {
385 |       logger.error(`Cache validation failed: ${error.message}`);
386 |       return { valid: false, repaired: false };
387 |     }
388 |   }
389 | 
390 |   /**
391 |    * Get cache statistics
392 |    */
393 |   async getCacheStats(): Promise<{
394 |     toolMetadataExists: boolean;
395 |     embeddingsExists: boolean;
396 |     mcpCount: number;
397 |     toolCount: number;
398 |     embeddingCount: number;
399 |     lastModified: Date | null;
400 |   }> {
401 |     const toolMetadataExists = existsSync(this.toolMetadataCachePath);
402 |     const embeddingsExists = existsSync(this.embeddingsCachePath);
403 | 
404 |     let mcpCount = 0;
405 |     let toolCount = 0;
406 |     let embeddingCount = 0;
407 |     let lastModified: Date | null = null;
408 | 
409 |     if (toolMetadataExists) {
410 |       try {
411 |         const cache = await this.loadToolMetadataCache();
412 |         mcpCount = Object.keys(cache.mcps).length;
413 |         toolCount = Object.values(cache.mcps).reduce((sum, mcp) => sum + mcp.tools.length, 0);
414 |         lastModified = new Date(cache.lastModified);
415 |       } catch (error) {
416 |         // Ignore errors for stats
417 |       }
418 |     }
419 | 
420 |     if (embeddingsExists) {
421 |       try {
422 |         const cache = await this.loadEmbeddingsCache();
423 |         embeddingCount = Object.keys(cache.vectors).length;
424 |       } catch (error) {
425 |         // Ignore errors for stats
426 |       }
427 |     }
428 | 
429 |     return {
430 |       toolMetadataExists,
431 |       embeddingsExists,
432 |       mcpCount,
433 |       toolCount,
434 |       embeddingCount,
435 |       lastModified
436 |     };
437 |   }
438 | }
```

--------------------------------------------------------------------------------
/test/tool-schema-parser.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Comprehensive Tests for ToolSchemaParser
  3 |  * Following ncp-oss3 patterns for 95%+ coverage
  4 |  */
  5 | 
  6 | import { describe, it, expect } from '@jest/globals';
  7 | import { ToolSchemaParser, ParameterInfo } from '../src/services/tool-schema-parser';
  8 | 
  9 | describe('ToolSchemaParser - Comprehensive Coverage', () => {
 10 | 
 11 |   const sampleSchema = {
 12 |     properties: {
 13 |       path: {
 14 |         type: 'string',
 15 |         description: 'File path to read'
 16 |       },
 17 |       encoding: {
 18 |         type: 'string',
 19 |         description: 'File encoding (optional)'
 20 |       },
 21 |       maxSize: {
 22 |         type: 'number',
 23 |         description: 'Maximum file size in bytes'
 24 |       },
 25 |       recursive: {
 26 |         type: 'boolean',
 27 |         description: 'Whether to read recursively'
 28 |       }
 29 |     },
 30 |     required: ['path', 'maxSize']
 31 |   };
 32 | 
 33 |   const emptySchema = {};
 34 |   const noPropertiesSchema = { required: ['something'] };
 35 |   const noRequiredSchema = {
 36 |     properties: {
 37 |       optional1: { type: 'string' },
 38 |       optional2: { type: 'number' }
 39 |     }
 40 |   };
 41 | 
 42 |   describe('🎯 Parameter Parsing - Core Functionality', () => {
 43 |     it('should parse complete schema with all parameter types', () => {
 44 |       const params = ToolSchemaParser.parseParameters(sampleSchema);
 45 | 
 46 |       expect(params).toHaveLength(4);
 47 | 
 48 |       // Check path parameter (required string)
 49 |       const pathParam = params.find(p => p.name === 'path');
 50 |       expect(pathParam).toEqual({
 51 |         name: 'path',
 52 |         type: 'string',
 53 |         required: true,
 54 |         description: 'File path to read'
 55 |       });
 56 | 
 57 |       // Check encoding parameter (optional string)
 58 |       const encodingParam = params.find(p => p.name === 'encoding');
 59 |       expect(encodingParam).toEqual({
 60 |         name: 'encoding',
 61 |         type: 'string',
 62 |         required: false,
 63 |         description: 'File encoding (optional)'
 64 |       });
 65 | 
 66 |       // Check maxSize parameter (required number)
 67 |       const maxSizeParam = params.find(p => p.name === 'maxSize');
 68 |       expect(maxSizeParam).toEqual({
 69 |         name: 'maxSize',
 70 |         type: 'number',
 71 |         required: true,
 72 |         description: 'Maximum file size in bytes'
 73 |       });
 74 | 
 75 |       // Check recursive parameter (optional boolean)
 76 |       const recursiveParam = params.find(p => p.name === 'recursive');
 77 |       expect(recursiveParam).toEqual({
 78 |         name: 'recursive',
 79 |         type: 'boolean',
 80 |         required: false,
 81 |         description: 'Whether to read recursively'
 82 |       });
 83 |     });
 84 | 
 85 |     it('should handle schema with missing properties', () => {
 86 |       const params = ToolSchemaParser.parseParameters(noPropertiesSchema);
 87 |       expect(params).toEqual([]);
 88 |     });
 89 | 
 90 |     it('should handle schema with no required array', () => {
 91 |       const params = ToolSchemaParser.parseParameters(noRequiredSchema);
 92 |       expect(params).toHaveLength(2);
 93 | 
 94 |       params.forEach(param => {
 95 |         expect(param.required).toBe(false);
 96 |       });
 97 |     });
 98 | 
 99 |     it('should handle properties without type information', () => {
100 |       const schemaWithoutTypes = {
101 |         properties: {
102 |           mystery1: { description: 'Unknown type parameter' },
103 |           mystery2: { /* no type or description */ }
104 |         },
105 |         required: ['mystery1']
106 |       };
107 | 
108 |       const params = ToolSchemaParser.parseParameters(schemaWithoutTypes);
109 |       expect(params).toHaveLength(2);
110 | 
111 |       const mystery1 = params.find(p => p.name === 'mystery1');
112 |       expect(mystery1).toEqual({
113 |         name: 'mystery1',
114 |         type: 'unknown',
115 |         required: true,
116 |         description: 'Unknown type parameter'
117 |       });
118 | 
119 |       const mystery2 = params.find(p => p.name === 'mystery2');
120 |       expect(mystery2).toEqual({
121 |         name: 'mystery2',
122 |         type: 'unknown',
123 |         required: false,
124 |         description: undefined
125 |       });
126 |     });
127 |   });
128 | 
129 |   describe('🎯 Edge Cases and Error Handling', () => {
130 |     it('should handle null and undefined schemas', () => {
131 |       expect(ToolSchemaParser.parseParameters(null)).toEqual([]);
132 |       expect(ToolSchemaParser.parseParameters(undefined)).toEqual([]);
133 |     });
134 | 
135 |     it('should handle non-object schemas', () => {
136 |       expect(ToolSchemaParser.parseParameters('string')).toEqual([]);
137 |       expect(ToolSchemaParser.parseParameters(123)).toEqual([]);
138 |       expect(ToolSchemaParser.parseParameters([])).toEqual([]);
139 |       expect(ToolSchemaParser.parseParameters(true)).toEqual([]);
140 |     });
141 | 
142 |     it('should handle empty schema object', () => {
143 |       expect(ToolSchemaParser.parseParameters(emptySchema)).toEqual([]);
144 |     });
145 | 
146 |     it('should handle schema with null/undefined properties', () => {
147 |       const badSchema = {
148 |         properties: null,
149 |         required: undefined
150 |       };
151 |       expect(ToolSchemaParser.parseParameters(badSchema)).toEqual([]);
152 |     });
153 | 
154 |     it('should handle schema with non-array required field', () => {
155 |       const invalidRequiredSchema = {
156 |         properties: {
157 |           param1: { type: 'string' }
158 |         },
159 |         required: 'not-an-array'
160 |       };
161 |       const params = ToolSchemaParser.parseParameters(invalidRequiredSchema);
162 |       expect(params).toHaveLength(1);
163 |       expect(params[0].required).toBe(false);
164 |     });
165 |   });
166 | 
167 |   describe('🎯 Required Parameters Filtering', () => {
168 |     it('should extract only required parameters', () => {
169 |       const requiredParams = ToolSchemaParser.getRequiredParameters(sampleSchema);
170 | 
171 |       expect(requiredParams).toHaveLength(2);
172 |       expect(requiredParams.map(p => p.name)).toEqual(['path', 'maxSize']);
173 | 
174 |       requiredParams.forEach(param => {
175 |         expect(param.required).toBe(true);
176 |       });
177 |     });
178 | 
179 |     it('should return empty array for schema with no required parameters', () => {
180 |       const requiredParams = ToolSchemaParser.getRequiredParameters(noRequiredSchema);
181 |       expect(requiredParams).toEqual([]);
182 |     });
183 | 
184 |     it('should handle invalid schemas in getRequiredParameters', () => {
185 |       expect(ToolSchemaParser.getRequiredParameters(null)).toEqual([]);
186 |       expect(ToolSchemaParser.getRequiredParameters({})).toEqual([]);
187 |     });
188 |   });
189 | 
190 |   describe('🎯 Optional Parameters Filtering', () => {
191 |     it('should extract only optional parameters', () => {
192 |       const optionalParams = ToolSchemaParser.getOptionalParameters(sampleSchema);
193 | 
194 |       expect(optionalParams).toHaveLength(2);
195 |       expect(optionalParams.map(p => p.name)).toEqual(['encoding', 'recursive']);
196 | 
197 |       optionalParams.forEach(param => {
198 |         expect(param.required).toBe(false);
199 |       });
200 |     });
201 | 
202 |     it('should return all parameters when none are required', () => {
203 |       const optionalParams = ToolSchemaParser.getOptionalParameters(noRequiredSchema);
204 |       expect(optionalParams).toHaveLength(2);
205 | 
206 |       optionalParams.forEach(param => {
207 |         expect(param.required).toBe(false);
208 |       });
209 |     });
210 | 
211 |     it('should handle invalid schemas in getOptionalParameters', () => {
212 |       expect(ToolSchemaParser.getOptionalParameters(null)).toEqual([]);
213 |       expect(ToolSchemaParser.getOptionalParameters({})).toEqual([]);
214 |     });
215 |   });
216 | 
217 |   describe('🎯 Required Parameters Detection', () => {
218 |     it('should detect schemas with required parameters', () => {
219 |       expect(ToolSchemaParser.hasRequiredParameters(sampleSchema)).toBe(true);
220 |     });
221 | 
222 |     it('should detect schemas without required parameters', () => {
223 |       expect(ToolSchemaParser.hasRequiredParameters(noRequiredSchema)).toBe(false);
224 |       expect(ToolSchemaParser.hasRequiredParameters(emptySchema)).toBe(false);
225 |     });
226 | 
227 |     it('should handle edge cases in hasRequiredParameters', () => {
228 |       expect(ToolSchemaParser.hasRequiredParameters(null)).toBe(false);
229 |       expect(ToolSchemaParser.hasRequiredParameters(undefined)).toBe(false);
230 |       expect(ToolSchemaParser.hasRequiredParameters('string')).toBe(false);
231 |       expect(ToolSchemaParser.hasRequiredParameters(123)).toBe(false);
232 |     });
233 | 
234 |     it('should handle schema with empty required array', () => {
235 |       const emptyRequiredSchema = {
236 |         properties: { param1: { type: 'string' } },
237 |         required: []
238 |       };
239 |       expect(ToolSchemaParser.hasRequiredParameters(emptyRequiredSchema)).toBe(false);
240 |     });
241 | 
242 |     it('should handle schema with non-array required field', () => {
243 |       const invalidRequiredSchema = {
244 |         properties: { param1: { type: 'string' } },
245 |         required: 'not-an-array'
246 |       };
247 |       expect(ToolSchemaParser.hasRequiredParameters(invalidRequiredSchema)).toBe(false);
248 |     });
249 |   });
250 | 
251 |   describe('🎯 Parameter Counting', () => {
252 |     it('should count all parameter types correctly', () => {
253 |       const counts = ToolSchemaParser.countParameters(sampleSchema);
254 | 
255 |       expect(counts).toEqual({
256 |         total: 4,
257 |         required: 2,
258 |         optional: 2
259 |       });
260 |     });
261 | 
262 |     it('should count parameters in schema with no required fields', () => {
263 |       const counts = ToolSchemaParser.countParameters(noRequiredSchema);
264 | 
265 |       expect(counts).toEqual({
266 |         total: 2,
267 |         required: 0,
268 |         optional: 2
269 |       });
270 |     });
271 | 
272 |     it('should count parameters in schema with all required fields', () => {
273 |       const allRequiredSchema = {
274 |         properties: {
275 |           param1: { type: 'string' },
276 |           param2: { type: 'number' }
277 |         },
278 |         required: ['param1', 'param2']
279 |       };
280 | 
281 |       const counts = ToolSchemaParser.countParameters(allRequiredSchema);
282 | 
283 |       expect(counts).toEqual({
284 |         total: 2,
285 |         required: 2,
286 |         optional: 0
287 |       });
288 |     });
289 | 
290 |     it('should handle empty schemas in countParameters', () => {
291 |       expect(ToolSchemaParser.countParameters(emptySchema)).toEqual({
292 |         total: 0,
293 |         required: 0,
294 |         optional: 0
295 |       });
296 | 
297 |       expect(ToolSchemaParser.countParameters(null)).toEqual({
298 |         total: 0,
299 |         required: 0,
300 |         optional: 0
301 |       });
302 |     });
303 |   });
304 | 
305 |   describe('🎯 Individual Parameter Lookup', () => {
306 |     it('should find existing parameters by name', () => {
307 |       const pathParam = ToolSchemaParser.getParameter(sampleSchema, 'path');
308 |       expect(pathParam).toEqual({
309 |         name: 'path',
310 |         type: 'string',
311 |         required: true,
312 |         description: 'File path to read'
313 |       });
314 | 
315 |       const encodingParam = ToolSchemaParser.getParameter(sampleSchema, 'encoding');
316 |       expect(encodingParam).toEqual({
317 |         name: 'encoding',
318 |         type: 'string',
319 |         required: false,
320 |         description: 'File encoding (optional)'
321 |       });
322 |     });
323 | 
324 |     it('should return undefined for non-existent parameters', () => {
325 |       expect(ToolSchemaParser.getParameter(sampleSchema, 'nonexistent')).toBeUndefined();
326 |       expect(ToolSchemaParser.getParameter(sampleSchema, '')).toBeUndefined();
327 |     });
328 | 
329 |     it('should handle invalid schemas in getParameter', () => {
330 |       expect(ToolSchemaParser.getParameter(null, 'any')).toBeUndefined();
331 |       expect(ToolSchemaParser.getParameter({}, 'any')).toBeUndefined();
332 |       expect(ToolSchemaParser.getParameter('invalid', 'any')).toBeUndefined();
333 |     });
334 | 
335 |     it('should handle case-sensitive parameter names', () => {
336 |       expect(ToolSchemaParser.getParameter(sampleSchema, 'Path')).toBeUndefined();
337 |       expect(ToolSchemaParser.getParameter(sampleSchema, 'PATH')).toBeUndefined();
338 |       expect(ToolSchemaParser.getParameter(sampleSchema, 'path')).toBeDefined();
339 |     });
340 |   });
341 | 
342 |   describe('🎯 Complex Schema Scenarios', () => {
343 |     it('should handle nested object schemas', () => {
344 |       const nestedSchema = {
345 |         properties: {
346 |           config: {
347 |             type: 'object',
348 |             description: 'Configuration object',
349 |             properties: {
350 |               nested: { type: 'string' }
351 |             }
352 |           }
353 |         },
354 |         required: ['config']
355 |       };
356 | 
357 |       const params = ToolSchemaParser.parseParameters(nestedSchema);
358 |       expect(params).toHaveLength(1);
359 |       expect(params[0]).toEqual({
360 |         name: 'config',
361 |         type: 'object',
362 |         required: true,
363 |         description: 'Configuration object'
364 |       });
365 |     });
366 | 
367 |     it('should handle array type schemas', () => {
368 |       const arraySchema = {
369 |         properties: {
370 |           items: {
371 |             type: 'array',
372 |             description: 'List of items',
373 |             items: { type: 'string' }
374 |           }
375 |         },
376 |         required: ['items']
377 |       };
378 | 
379 |       const params = ToolSchemaParser.parseParameters(arraySchema);
380 |       expect(params[0]).toEqual({
381 |         name: 'items',
382 |         type: 'array',
383 |         required: true,
384 |         description: 'List of items'
385 |       });
386 |     });
387 | 
388 |     it('should handle schemas with special characters in property names', () => {
389 |       const specialSchema = {
390 |         properties: {
391 |           'kebab-case': { type: 'string' },
392 |           'snake_case': { type: 'number' },
393 |           'dot.notation': { type: 'boolean' },
394 |           'space name': { type: 'string' }
395 |         },
396 |         required: ['kebab-case', 'space name']
397 |       };
398 | 
399 |       const params = ToolSchemaParser.parseParameters(specialSchema);
400 |       expect(params).toHaveLength(4);
401 | 
402 |       const kebabParam = params.find(p => p.name === 'kebab-case');
403 |       expect(kebabParam?.required).toBe(true);
404 | 
405 |       const spaceParam = params.find(p => p.name === 'space name');
406 |       expect(spaceParam?.required).toBe(true);
407 | 
408 |       const snakeParam = params.find(p => p.name === 'snake_case');
409 |       expect(snakeParam?.required).toBe(false);
410 |     });
411 |   });
412 | });
```

--------------------------------------------------------------------------------
/docs/stories/06-official-registry.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 🌐 Story 6: Official Registry
  2 | 
  3 | *How AI discovers 2,200+ MCPs without you lifting a finger*
  4 | 
  5 | **Reading time:** 2 minutes
  6 | 
  7 | ---
  8 | 
  9 | ## 😤 The Pain
 10 | 
 11 | You need a database MCP. Here's what you have to do today:
 12 | 
 13 | **The Manual Discovery Process:**
 14 | 
 15 | ```
 16 | Step 1: Google "MCP database"
 17 | → Find blog post from 3 months ago
 18 | → List is outdated
 19 | 
 20 | Step 2: Visit Smithery.ai
 21 | → Browse through categories
 22 | → 2,200+ MCPs to wade through
 23 | → No way to preview without installing
 24 | 
 25 | Step 3: Find promising MCP
 26 | → Click to GitHub repo
 27 | → Read README (hopefully it's good)
 28 | → Find npm package name
 29 | → Hope it's maintained
 30 | 
 31 | Step 4: Copy installation command
 32 | → npm install -g @someone/mcp-postgres
 33 | → Still not sure if it's the right one
 34 | 
 35 | Step 5: Configure it
 36 | → Add to config file
 37 | → Restart Claude Desktop
 38 | → Test it
 39 | → Realize it's not what you needed
 40 | 
 41 | Step 6: Remove and try another
 42 | → Repeat steps 3-5 multiple times
 43 | → 2 hours wasted
 44 | ```
 45 | 
 46 | **Questions you can't easily answer:**
 47 | - Which database MCPs are **official** vs community?
 48 | - Which are **actively maintained**?
 49 | - Which are **most popular**?
 50 | - What **env vars** do they need?
 51 | - How do I install them **correctly**?
 52 | 
 53 | **You're on your own. Good luck.**
 54 | 
 55 | ---
 56 | 
 57 | ## 🌐 The Journey
 58 | 
 59 | NCP connects to the **Official MCP Registry** - Anthropic's central directory of vetted MCPs. Your AI can search it directly.
 60 | 
 61 | ### **Discovery Through Conversation:**
 62 | 
 63 | **You:** "Find database MCPs"
 64 | 
 65 | **AI:** [Calls `ncp:import` with discovery mode]
 66 | 
 67 | ```typescript
 68 | ncp:import({
 69 |   from: "discovery",
 70 |   source: "database"
 71 | })
 72 | ```
 73 | 
 74 | **NCP:** [Searches registry.modelcontextprotocol.io]
 75 | 
 76 | **AI shows you:**
 77 | 
 78 | ```
 79 | Found 8 database MCPs from official registry:
 80 | 
 81 | 1. ⭐ @modelcontextprotocol/server-postgres
 82 |    PostgreSQL database integration
 83 |    Status: Official | Downloads: 1,240
 84 | 
 85 | 2. 📦 @modelcontextprotocol/server-sqlite
 86 |    SQLite database integration
 87 |    Status: Official | Downloads: 890
 88 | 
 89 | 3. 📦 @modelcontextprotocol/server-mongodb
 90 |    MongoDB database integration
 91 |    Status: Community | Downloads: 456
 92 | 
 93 | 4. 📦 mcp-mysql-server
 94 |    MySQL database integration
 95 |    Status: Community | Downloads: 234
 96 | 
 97 | [... 4 more ...]
 98 | 
 99 | Which would you like to install?
100 | ```
101 | 
102 | **You:** "Install 1 and 2"
103 | 
104 | **AI:** [Calls import with selection]
105 | 
106 | ```typescript
107 | ncp:import({
108 |   from: "discovery",
109 |   source: "database",
110 |   selection: "1,2"
111 | })
112 | ```
113 | 
114 | **NCP:** [Imports PostgreSQL and SQLite MCPs with correct configs]
115 | 
116 | **Result:**
117 | ```
118 | ✅ Installed @modelcontextprotocol/server-postgres
119 | ✅ Installed @modelcontextprotocol/server-sqlite
120 | 
121 | Both MCPs ready to use! If they require credentials, use clipboard
122 | security pattern (Story 2) to configure API keys safely.
123 | ```
124 | 
125 | **Total time: 30 seconds.** (vs 2 hours manually)
126 | 
127 | ---
128 | 
129 | ## ✨ The Magic
130 | 
131 | What you get with registry integration:
132 | 
133 | ### **🔍 AI-Powered Discovery**
134 | - **Search by intent:** "Find file tools" not "grep filesystem npm"
135 | - **Semantic matching:** Registry understands what you need
136 | - **Natural language:** No technical keywords required
137 | - **Conversational:** Back-and-forth with AI to refine results
138 | 
139 | ### **⭐ Curated Results**
140 | - **Official badge:** Shows Anthropic-maintained MCPs
141 | - **Download counts:** See what's popular and trusted
142 | - **Status indicators:** Official vs Community vs Experimental
143 | - **Version info:** Always get latest stable version
144 | 
145 | ### **📦 One-Click Install**
146 | - **Select by number:** "Install 1, 3, and 5"
147 | - **Range selection:** "Install 1-5"
148 | - **Install all:** "Install *"
149 | - **Batch import:** Multiple MCPs installed in parallel
150 | 
151 | ### **✅ Correct Configuration**
152 | - **Registry knows the command:** `npx` or `node` or custom
153 | - **Registry knows the args:** Package identifier, required flags
154 | - **Registry knows env vars:** Shows what credentials you need
155 | - **No guessing:** NCP gets it right the first time
156 | 
157 | ### **🔒 Safe Credentials**
158 | - **Registry shows:** "This MCP needs GITHUB_TOKEN"
159 | - **You provide:** Via clipboard security pattern (Story 2)
160 | - **AI never sees:** Your actual token
161 | - **Works seamlessly:** Discovery + secure config in one flow
162 | 
163 | ---
164 | 
165 | ## 🔍 How It Works (The Technical Story)
166 | 
167 | ### **Registry API:**
168 | 
169 | ```typescript
170 | // NCP talks to official MCP Registry
171 | const REGISTRY_BASE = 'https://registry.modelcontextprotocol.io/v0';
172 | 
173 | // Search endpoint
174 | GET /v0/servers?limit=50
175 | → Returns: List of all MCPs with metadata
176 | 
177 | // Details endpoint
178 | GET /v0/servers/{encoded_name}
179 | → Returns: Full details including env vars, packages, etc.
180 | ```
181 | 
182 | ### **Search Flow:**
183 | 
184 | ```typescript
185 | // User: "Find database MCPs"
186 | // AI calls: ncp:import({ from: "discovery", source: "database" })
187 | 
188 | // Step 1: Search registry
189 | const results = await fetch(`${REGISTRY_BASE}/servers?limit=50`);
190 | const allServers = await results.json();
191 | 
192 | // Step 2: Filter by query
193 | const filtered = allServers.servers.filter(s =>
194 |   s.server.name.toLowerCase().includes('database') ||
195 |   s.server.description?.toLowerCase().includes('database')
196 | );
197 | 
198 | // Step 3: Format as numbered list
199 | const candidates = filtered.map((server, index) => ({
200 |   number: index + 1,
201 |   name: server.server.name,
202 |   displayName: extractShortName(server.server.name),
203 |   description: server.server.description,
204 |   status: server._meta?.['io.modelcontextprotocol.registry/official']?.status,
205 |   downloads: getDownloadCount(server), // From registry metadata
206 |   version: server.server.version
207 | }));
208 | 
209 | // Return to AI for display
210 | ```
211 | 
212 | ### **Import Flow:**
213 | 
214 | ```typescript
215 | // User: "Install 1 and 3"
216 | // AI calls: ncp:import({ from: "discovery", source: "database", selection: "1,3" })
217 | 
218 | // Step 1: Parse selection
219 | const selected = parseSelection("1,3", candidates);
220 | // Returns: [candidates[0], candidates[2]]
221 | 
222 | // Step 2: Get detailed info for each
223 | for (const candidate of selected) {
224 |   const details = await fetch(`${REGISTRY_BASE}/servers/${encodeURIComponent(candidate.name)}`);
225 |   const server = await details.json();
226 | 
227 |   // Extract install config
228 |   const pkg = server.server.packages[0];
229 |   const config = {
230 |     command: pkg.runtimeHint || 'npx',
231 |     args: [pkg.identifier],
232 |     env: {} // User provides via clipboard if needed
233 |   };
234 | 
235 |   // Import using internal add command
236 |   await internalAdd(candidate.displayName, config);
237 | }
238 | ```
239 | 
240 | ### **Caching:**
241 | 
242 | ```typescript
243 | // Registry responses cached for 5 minutes
244 | const CACHE_TTL = 5 * 60 * 1000;
245 | 
246 | // First search: Hits network (~200ms)
247 | ncp:import({ from: "discovery", source: "database" })
248 | 
249 | // Repeat search within 5 min: Hits cache (0ms)
250 | ncp:import({ from: "discovery", source: "database" })
251 | 
252 | // After 5 min: Cache expires, fetches fresh data
253 | ```
254 | 
255 | ---
256 | 
257 | ## 🎨 The Analogy That Makes It Click
258 | 
259 | **Manual Discovery = Library Without Card Catalog** 📚
260 | 
261 | ```
262 | You walk into library with 2,200 books.
263 | No organization. No search system. No librarian.
264 | You wander the aisles hoping to find what you need.
265 | Read book spines one by one.
266 | Pull out books to check if they're relevant.
267 | 3 hours later: Found 2 books, not sure if they're the best.
268 | ```
269 | 
270 | **Registry Discovery = Amazon Search** 🔍
271 | 
272 | ```
273 | You open Amazon.
274 | Type: "database book"
275 | See: Reviews, ratings, bestsellers, "customers also bought"
276 | Filter: By rating, by relevance, by date
277 | Click: Buy recommended book
278 | 5 minutes later: Book on the way, confident it's what you need.
279 | ```
280 | 
281 | **Registry gives MCPs the search/discovery experience of modern marketplaces.**
282 | 
283 | ---
284 | 
285 | ## 🧪 See It Yourself
286 | 
287 | Try this experiment:
288 | 
289 | ### **Test 1: Search Registry**
290 | 
291 | ```bash
292 | # Manual way (old)
293 | [Open browser]
294 | [Go to smithery.ai]
295 | [Search "filesystem"]
296 | [Read through results]
297 | [Copy npm command]
298 | [Run in terminal]
299 | [Total time: 5 minutes]
300 | 
301 | # Registry way (new)
302 | You: "Find filesystem MCPs"
303 | AI: [Shows numbered list from registry]
304 | You: "Install 1"
305 | AI: [Installs in seconds]
306 | [Total time: 30 seconds]
307 | ```
308 | 
309 | ### **Test 2: Compare Official vs Community**
310 | 
311 | ```
312 | You: "Find GitHub MCPs"
313 | 
314 | AI shows:
315 | 1. ⭐ @modelcontextprotocol/server-github [Official]
316 | 2. 📦 github-mcp-enhanced [Community]
317 | 3. 📦 mcp-github-toolkit [Community]
318 | 
319 | You can see at a glance which is official/supported!
320 | ```
321 | 
322 | ### **Test 3: Batch Install**
323 | 
324 | ```
325 | You: "Find AI reasoning MCPs"
326 | 
327 | AI shows:
328 | 1. sequential-thinking
329 | 2. memory
330 | 3. thinking-protocol
331 | 4. context-manager
332 | 
333 | You: "Install all"
334 | AI: [Installs 1-4 in parallel]
335 | 
336 | Done in seconds!
337 | ```
338 | 
339 | ---
340 | 
341 | ## 🚀 Why This Changes Everything
342 | 
343 | ### **Before Registry (Fragmented Discovery):**
344 | 
345 | **The ecosystem was scattered:**
346 | - Some MCPs on Smithery.ai
347 | - Some on GitHub awesome lists
348 | - Some only documented in blog posts
349 | - No central source of truth
350 | - No quality indicators
351 | - No official vs community distinction
352 | 
353 | **Finding MCPs was hard. Choosing the right one was harder.**
354 | 
355 | ### **After Registry (Unified Discovery):**
356 | 
357 | **The ecosystem is organized:**
358 | - ✅ All MCPs in central registry (registry.modelcontextprotocol.io)
359 | - ✅ Clear official vs community badges
360 | - ✅ Download counts show popularity
361 | - ✅ Correct install commands included
362 | - ✅ AI can search and install directly
363 | - ✅ One source of truth for all MCPs
364 | 
365 | **Finding MCPs is easy. Choosing the right one is obvious.**
366 | 
367 | ---
368 | 
369 | ## 🎯 Selection Syntax
370 | 
371 | NCP supports flexible selection formats:
372 | 
373 | ```typescript
374 | // Individual numbers
375 | selection: "1,3,5"
376 | → Installs: #1, #3, #5
377 | 
378 | // Ranges
379 | selection: "1-5"
380 | → Installs: #1, #2, #3, #4, #5
381 | 
382 | // Mixed
383 | selection: "1,3,7-10"
384 | → Installs: #1, #3, #7, #8, #9, #10
385 | 
386 | // All results
387 | selection: "*"
388 | → Installs: Everything shown
389 | 
390 | // Just one
391 | selection: "1"
392 | → Installs: #1 only
393 | ```
394 | 
395 | **Natural syntax. No programming knowledge required.**
396 | 
397 | ---
398 | 
399 | ## 📊 Registry Metadata
400 | 
401 | What registry provides per MCP:
402 | 
403 | ```typescript
404 | {
405 |   server: {
406 |     name: "io.github.modelcontextprotocol/server-filesystem",
407 |     description: "File system operations",
408 |     version: "0.2.0",
409 |     repository: {
410 |       url: "https://github.com/modelcontextprotocol/servers",
411 |       type: "git"
412 |     },
413 |     packages: [{
414 |       identifier: "@modelcontextprotocol/server-filesystem",
415 |       version: "0.2.0",
416 |       runtimeHint: "npx",
417 |       environmentVariables: [
418 |         {
419 |           name: "ROOT_PATH",
420 |           description: "Root directory for file operations",
421 |           isRequired: true
422 |         }
423 |       ]
424 |     }]
425 |   },
426 |   _meta: {
427 |     'io.modelcontextprotocol.registry/official': {
428 |       status: "official"  // or "community"
429 |     }
430 |   }
431 | }
432 | ```
433 | 
434 | **Registry tells NCP exactly how to install and configure each MCP.**
435 | 
436 | ---
437 | 
438 | ## 🔒 Security Considerations
439 | 
440 | **Q: Can malicious MCPs enter the registry?**
441 | 
442 | **A: Registry has curation process:**
443 | 
444 | 1. **Official MCPs:** Maintained by Anthropic, fully vetted
445 | 2. **Community MCPs:** User-submitted, reviewed before listing
446 | 3. **Each MCP shows status:** Official vs Community badge visible
447 | 4. **Source code linked:** GitHub repo always shown
448 | 5. **Download counts:** Popular = more eyes = more security
449 | 
450 | **Best practices:**
451 | 
452 | - ✅ Prefer official MCPs when available
453 | - ✅ Check GitHub repo before installing community MCPs
454 | - ✅ Review source code if handling sensitive data
455 | - ✅ Start with high-download-count MCPs (battle-tested)
456 | 
457 | **Registry doesn't execute code. It's a directory. You're still in control of what runs.**
458 | 
459 | ---
460 | 
461 | ## 📚 Deep Dive
462 | 
463 | Want the full technical implementation?
464 | 
465 | - **Registry Client:** [src/services/registry-client.ts]
466 | - **Discovery Mode:** [src/internal-mcps/ncp-management.ts] (import tool)
467 | - **Selection Parser:** [Parse selection format]
468 | - **API Docs:** [https://registry.modelcontextprotocol.io/](https://registry.modelcontextprotocol.io/)
469 | 
470 | ---
471 | 
472 | ## 🔗 Complete the Journey
473 | 
474 | **[← Back to Story 1: Dream and Discover](01-dream-and-discover.md)**
475 | 
476 | You've now read all 6 core stories that make NCP special:
477 | 
478 | 1. ✅ **Dream and Discover** - AI searches by intent, not by browsing tools
479 | 2. ✅ **Secrets in Plain Sight** - Clipboard handshake keeps credentials safe
480 | 3. ✅ **Sync and Forget** - Auto-imports Claude Desktop MCPs forever
481 | 4. ✅ **Double-Click Install** - .mcpb makes installation feel native
482 | 5. ✅ **Runtime Detective** - Adapts to your Node.js runtime automatically
483 | 6. ✅ **Official Registry** - Discovers 2,200+ MCPs through conversation
484 | 
485 | **Together, these stories explain why NCP transforms how you work with MCPs.**
486 | 
487 | ---
488 | 
489 | ## 💬 Questions?
490 | 
491 | **Q: How often is registry updated?**
492 | 
493 | A: Registry is live. New MCPs appear as soon as they're approved. NCP caches results for 5 minutes, then fetches fresh data.
494 | 
495 | **Q: Can I search for specific features?**
496 | 
497 | A: Yes! Try: "Find MCPs with email capabilities" or "Find MCPs for web scraping". Semantic search works across name + description.
498 | 
499 | **Q: What if registry is down?**
500 | 
501 | A: NCP falls back gracefully. You can still use existing MCPs and install new ones manually via `ncp add`.
502 | 
503 | **Q: Can I submit my MCP to registry?**
504 | 
505 | A: Yes! Visit [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/) for submission guidelines. (Process managed by Anthropic)
506 | 
507 | **Q: What about MCPs not in registry?**
508 | 
509 | A: You can still install them manually: `ncp add myserver npx my-custom-mcp`. Registry is for discovery convenience, not a requirement.
510 | 
511 | ---
512 | 
513 | **[← Previous Story](05-runtime-detective.md)** | **[Back to Story Index](../README.md#the-six-stories)**
514 | 
515 | ---
516 | 
517 | ## 🎉 What's Next?
518 | 
519 | Now that you understand how NCP works through these six stories, you're ready to:
520 | 
521 | 1. **[Install NCP →](../README.md#installation)** - Get started in 30 seconds
522 | 2. **[Try the examples →](../README.md#test-drive)** - See it in action
523 | 3. **[Read technical docs →](../technical/)** - Deep dive into implementation
524 | 4. **[Contribute →](../../CONTRIBUTING.md)** - Help make NCP even better
525 | 
526 | **Welcome to the NCP community!** 🚀
527 | 
```

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

```typescript
  1 | /**
  2 |  * NCP Visual Analytics Formatter
  3 |  * Enhanced terminal output with CLI charts and graphs
  4 |  */
  5 | 
  6 | import chalk from 'chalk';
  7 | import { AnalyticsReport } from './log-parser.js';
  8 | 
  9 | export class VisualAnalyticsFormatter {
 10 |   /**
 11 |    * Format analytics dashboard with visual charts
 12 |    */
 13 |   static async formatVisualDashboard(report: AnalyticsReport): Promise<string> {
 14 |     const output: string[] = [];
 15 | 
 16 |     // Header with enhanced styling
 17 |     output.push('');
 18 |     output.push(chalk.bold.cyan('🚀 NCP Impact Analytics Dashboard (Visual)'));
 19 |     output.push(chalk.dim('═'.repeat(60)));
 20 |     output.push('');
 21 | 
 22 |     // Overview Section with Key Metrics
 23 |     output.push(chalk.bold.white('📊 KEY METRICS 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' : `last ${days} days`;
 28 | 
 29 |     // Create metrics display with visual bars
 30 |     const metrics = [
 31 |       { label: 'Total Sessions', value: report.totalSessions, unit: 'sessions', color: chalk.green },
 32 |       { label: 'Unique MCPs', value: report.uniqueMCPs, unit: 'servers', color: chalk.cyan },
 33 |       { label: 'Success Rate', value: Math.round(report.successRate), unit: '%', color: chalk.yellow },
 34 |       { label: 'Response Data', value: Math.round(report.totalResponseSize / 1024 / 1024), unit: 'MB', color: chalk.blue }
 35 |     ];
 36 | 
 37 |     for (const metric of metrics) {
 38 |       const bar = this.createHorizontalBar(metric.value, Math.max(...metrics.map(m => m.value)), 25);
 39 |       output.push(`${metric.color(metric.label.padEnd(15))}: ${bar} ${metric.color(metric.value.toLocaleString())} ${chalk.dim(metric.unit)}`);
 40 |     }
 41 |     output.push('');
 42 | 
 43 |     // Usage Trends Chart
 44 |     if (Object.keys(report.dailyUsage).length > 3) {
 45 |       output.push(chalk.bold.white('📈 DAILY USAGE TRENDS'));
 46 |       output.push('');
 47 | 
 48 |       const dailyData = Object.entries(report.dailyUsage)
 49 |         .sort(([a], [b]) => a.localeCompare(b))
 50 |         .map(([_, usage]) => usage);
 51 | 
 52 |       if (dailyData.length > 1) {
 53 |         // Create simple ASCII line chart
 54 |         const chart = this.createLineChart(dailyData, 8, 40);
 55 |         output.push(chalk.green(chart));
 56 |         output.push(chalk.dim('   └─ Sessions per day over time'));
 57 |       }
 58 |       output.push('');
 59 |     }
 60 | 
 61 |     // Top MCPs Usage Chart
 62 |     if (report.topMCPsByUsage.length > 0) {
 63 |       output.push(chalk.bold.white('🔥 TOP MCP USAGE DISTRIBUTION'));
 64 |       output.push('');
 65 | 
 66 |       const topMCPs = report.topMCPsByUsage.slice(0, 8);
 67 |       const maxSessions = Math.max(...topMCPs.map(mcp => mcp.sessions));
 68 | 
 69 |       for (const mcp of topMCPs) {
 70 |         const percentage = ((mcp.sessions / report.totalSessions) * 100).toFixed(1);
 71 |         const bar = this.createColorfulBar(mcp.sessions, maxSessions, 30);
 72 |         const successIcon = mcp.successRate >= 95 ? '✅' : mcp.successRate >= 80 ? '⚠️' : '❌';
 73 | 
 74 |         output.push(`${chalk.cyan(mcp.name.padEnd(20))} ${bar} ${chalk.white(mcp.sessions.toString().padStart(3))} ${chalk.dim(`(${percentage}%)`)} ${successIcon}`);
 75 |       }
 76 |       output.push('');
 77 |     }
 78 | 
 79 |     // Performance Distribution
 80 |     if (report.performanceMetrics.fastestMCPs.length > 0) {
 81 |       output.push(chalk.bold.white('⚡ PERFORMANCE DISTRIBUTION'));
 82 |       output.push('');
 83 | 
 84 |       // Create performance buckets
 85 |       const performanceData = report.performanceMetrics.fastestMCPs.concat(report.performanceMetrics.slowestMCPs);
 86 |       const durations = performanceData.map(mcp => mcp.avgDuration).filter(d => d > 0);
 87 | 
 88 |       if (durations.length > 3) {
 89 |         // Create performance distribution chart
 90 |         const chart = this.createLineChart(durations.slice(0, 20), 6, 35);
 91 |         output.push(chalk.yellow(chart));
 92 |         output.push(chalk.dim('   └─ Response times across MCPs (ms)'));
 93 |       }
 94 |       output.push('');
 95 |     }
 96 | 
 97 |     // Value Delivered Section with Visual Impact
 98 |     output.push(chalk.bold.white('💰 VALUE IMPACT VISUALIZATION (ESTIMATES)'));
 99 |     output.push('');
100 | 
101 |     // Calculate savings
102 |     const estimatedTokensWithoutNCP = report.totalSessions * report.uniqueMCPs * 100;
103 |     const estimatedTokensWithNCP = report.totalSessions * 50;
104 |     const tokenSavings = estimatedTokensWithoutNCP - estimatedTokensWithNCP;
105 |     const costSavings = (tokenSavings / 1000) * 0.002;
106 | 
107 |     // Visual representation of savings
108 |     const savingsData = [
109 |       { label: 'Without NCP', value: estimatedTokensWithoutNCP, color: chalk.red },
110 |       { label: 'With NCP', value: estimatedTokensWithNCP, color: chalk.green }
111 |     ];
112 | 
113 |     const maxTokens = Math.max(...savingsData.map(s => s.value));
114 |     for (const saving of savingsData) {
115 |       const bar = this.createHorizontalBar(saving.value, maxTokens, 40);
116 |       output.push(`${saving.label.padEnd(12)}: ${bar} ${saving.color((saving.value / 1000000).toFixed(1))}M tokens`);
117 |     }
118 | 
119 |     output.push('');
120 |     output.push(`💎 ${chalk.bold.green((tokenSavings / 1000000).toFixed(1))}M tokens saved = ${chalk.bold.green('$' + costSavings.toFixed(2))} cost reduction`);
121 |     output.push(`🧠 ${chalk.bold.green((((report.uniqueMCPs - 1) / report.uniqueMCPs) * 100).toFixed(1) + '%')} cognitive load reduction`);
122 |     output.push('');
123 | 
124 |     // Environmental Impact with Visual Scale
125 |     output.push(chalk.bold.white('🌱 ENVIRONMENTAL IMPACT SCALE (ROUGH ESTIMATES)'));
126 |     output.push('');
127 | 
128 |     const sessionsWithoutNCP = report.totalSessions * report.uniqueMCPs;
129 |     const computeReduction = sessionsWithoutNCP - report.totalSessions;
130 |     const estimatedEnergyKWh = computeReduction * 0.0002;
131 |     const estimatedCO2kg = estimatedEnergyKWh * 0.5;
132 | 
133 |     // Visual representation of environmental savings
134 |     const envData = [
135 |       { label: 'Energy Saved', value: estimatedEnergyKWh, unit: 'kWh', icon: '⚡' },
136 |       { label: 'CO₂ Avoided', value: estimatedCO2kg, unit: 'kg', icon: '🌍' },
137 |       { label: 'Connections Saved', value: computeReduction / 1000, unit: 'k', icon: '🔌' }
138 |     ];
139 | 
140 |     const maxEnvValue = Math.max(...envData.map(e => e.value));
141 |     for (const env of envData) {
142 |       const bar = this.createGreenBar(env.value, maxEnvValue, 25);
143 |       output.push(`${env.icon} ${env.label.padEnd(18)}: ${bar} ${chalk.green(env.value.toFixed(1))} ${chalk.dim(env.unit)}`);
144 |     }
145 |     output.push('');
146 | 
147 |     // Footer with enhanced tips
148 |     output.push(chalk.bold.white('💡 INTERACTIVE COMMANDS'));
149 |     output.push('');
150 |     output.push(chalk.dim('  📊 ') + chalk.cyan('ncp analytics performance') + chalk.dim(' - Detailed performance metrics'));
151 |     output.push(chalk.dim('  📁 ') + chalk.cyan('ncp analytics export') + chalk.dim(' - Export data to CSV'));
152 |     output.push(chalk.dim('  🔄 ') + chalk.cyan('ncp analytics dashboard') + chalk.dim(' - Refresh this dashboard'));
153 |     output.push('');
154 | 
155 |     return output.join('\\n');
156 |   }
157 | 
158 |   /**
159 |    * Create horizontal progress bar with custom styling
160 |    */
161 |   private static createHorizontalBar(value: number, max: number, width: number): string {
162 |     const percentage = max > 0 ? value / max : 0;
163 |     const filled = Math.round(percentage * width);
164 |     const empty = width - filled;
165 | 
166 |     const filledChar = '█';
167 |     const emptyChar = '░';
168 | 
169 |     return chalk.green(filledChar.repeat(filled)) + chalk.dim(emptyChar.repeat(empty));
170 |   }
171 | 
172 |   /**
173 |    * Create colorful bar with gradient effect
174 |    */
175 |   private static createColorfulBar(value: number, max: number, width: number): string {
176 |     const percentage = max > 0 ? value / max : 0;
177 |     const filled = Math.round(percentage * width);
178 |     const empty = width - filled;
179 | 
180 |     // Create gradient effect based on value
181 |     let coloredBar = '';
182 |     for (let i = 0; i < filled; i++) {
183 |       const progress = i / width;
184 |       if (progress < 0.3) {
185 |         coloredBar += chalk.red('█');
186 |       } else if (progress < 0.6) {
187 |         coloredBar += chalk.yellow('█');
188 |       } else {
189 |         coloredBar += chalk.green('█');
190 |       }
191 |     }
192 | 
193 |     return coloredBar + chalk.dim('░'.repeat(empty));
194 |   }
195 | 
196 |   /**
197 |    * Create green-themed bar for environmental metrics
198 |    */
199 |   private static createGreenBar(value: number, max: number, width: number): string {
200 |     const percentage = max > 0 ? value / max : 0;
201 |     const filled = Math.round(percentage * width);
202 |     const empty = width - filled;
203 | 
204 |     const filledBar = chalk.bgGreen.black('█'.repeat(filled));
205 |     const emptyBar = chalk.dim('░'.repeat(empty));
206 | 
207 |     return filledBar + emptyBar;
208 |   }
209 | 
210 |   /**
211 |    * Format performance report with enhanced visuals
212 |    */
213 |   static async formatVisualPerformance(report: AnalyticsReport): Promise<string> {
214 |     const output: string[] = [];
215 | 
216 |     output.push('');
217 |     output.push(chalk.bold.cyan('⚡ NCP Performance Analytics (Visual)'));
218 |     output.push(chalk.dim('═'.repeat(50)));
219 |     output.push('');
220 | 
221 |     // Performance Overview with Gauges
222 |     output.push(chalk.bold.white('🎯 PERFORMANCE GAUGES'));
223 |     output.push('');
224 | 
225 |     const performanceMetrics = [
226 |       { label: 'Success Rate', value: report.successRate, max: 100, unit: '%', color: chalk.green },
227 |       { label: 'Avg Response', value: report.avgSessionDuration || 5000, max: 10000, unit: 'ms', color: chalk.yellow },
228 |       { label: 'MCPs Active', value: report.uniqueMCPs, max: 2000, unit: 'servers', color: chalk.cyan }
229 |     ];
230 | 
231 |     for (const metric of performanceMetrics) {
232 |       const gauge = this.createGauge(metric.value, metric.max);
233 |       output.push(`${metric.label.padEnd(15)}: ${gauge} ${metric.color(metric.value.toFixed(1))}${metric.unit}`);
234 |     }
235 |     output.push('');
236 | 
237 |     // Performance Leaderboard with Visual Ranking
238 |     if (report.performanceMetrics.fastestMCPs.length > 0) {
239 |       output.push(chalk.bold.white('🏆 SPEED CHAMPIONS PODIUM'));
240 |       output.push('');
241 | 
242 |       const topPerformers = report.performanceMetrics.fastestMCPs.slice(0, 5);
243 |       const medals = ['🥇', '🥈', '🥉', '🏅', '🎖️'];
244 | 
245 |       for (let i = 0; i < topPerformers.length; i++) {
246 |         const mcp = topPerformers[i];
247 |         const medal = medals[i] || '⭐';
248 |         const speedBar = this.createSpeedBar(mcp.avgDuration, 10000);
249 | 
250 |         output.push(`${medal} ${chalk.cyan(mcp.name.padEnd(20))} ${speedBar} ${chalk.bold.green(mcp.avgDuration.toFixed(0))}ms`);
251 |       }
252 |       output.push('');
253 |     }
254 | 
255 |     // Reliability Champions
256 |     if (report.performanceMetrics.mostReliable.length > 0) {
257 |       output.push(chalk.bold.white('🛡️ RELIABILITY CHAMPIONS'));
258 |       output.push('');
259 | 
260 |       const reliablePerformers = report.performanceMetrics.mostReliable.slice(0, 5);
261 | 
262 |       for (let i = 0; i < reliablePerformers.length; i++) {
263 |         const mcp = reliablePerformers[i];
264 |         const reliabilityBar = this.createReliabilityBar(mcp.successRate);
265 |         const shield = mcp.successRate >= 99 ? '🛡️' : mcp.successRate >= 95 ? '🔰' : '⚡';
266 | 
267 |         output.push(`${shield} ${chalk.cyan(mcp.name.padEnd(20))} ${reliabilityBar} ${chalk.bold.green(mcp.successRate.toFixed(1))}%`);
268 |       }
269 |       output.push('');
270 |     }
271 | 
272 |     return output.join('\\n');
273 |   }
274 | 
275 |   /**
276 |    * Create gauge visualization
277 |    */
278 |   private static createGauge(value: number, max: number): string {
279 |     const percentage = Math.min(value / max, 1);
280 |     const gaugeWidth = 20;
281 |     const filled = Math.round(percentage * gaugeWidth);
282 | 
283 |     // Create gauge with different colors based on performance
284 |     let gauge = '[';
285 |     for (let i = 0; i < gaugeWidth; i++) {
286 |       if (i < filled) {
287 |         if (percentage > 0.8) gauge += chalk.green('█');
288 |         else if (percentage > 0.5) gauge += chalk.yellow('█');
289 |         else gauge += chalk.red('█');
290 |       } else {
291 |         gauge += chalk.dim('░');
292 |       }
293 |     }
294 |     gauge += ']';
295 | 
296 |     return gauge;
297 |   }
298 | 
299 |   /**
300 |    * Create speed bar (faster = more green)
301 |    */
302 |   private static createSpeedBar(duration: number, maxDuration: number): string {
303 |     const speed = Math.max(0, 1 - (duration / maxDuration)); // Invert: faster = higher score
304 |     const barWidth = 15;
305 |     const filled = Math.round(speed * barWidth);
306 | 
307 |     return chalk.green('█'.repeat(filled)) + chalk.dim('░'.repeat(barWidth - filled));
308 |   }
309 | 
310 |   /**
311 |    * Create reliability bar
312 |    */
313 |   private static createReliabilityBar(successRate: number): string {
314 |     const barWidth = 15;
315 |     const filled = Math.round((successRate / 100) * barWidth);
316 | 
317 |     return chalk.blue('█'.repeat(filled)) + chalk.dim('░'.repeat(barWidth - filled));
318 |   }
319 | 
320 |   /**
321 |    * Create simple ASCII line chart
322 |    */
323 |   private static createLineChart(data: number[], height: number, width: number): string {
324 |     if (data.length === 0) return '';
325 | 
326 |     const min = Math.min(...data);
327 |     const max = Math.max(...data);
328 |     const range = max - min || 1;
329 | 
330 |     const lines: string[] = [];
331 | 
332 |     // Create chart grid
333 |     for (let row = 0; row < height; row++) {
334 |       const threshold = max - (row / (height - 1)) * range;
335 |       let line = '   ';
336 | 
337 |       for (let col = 0; col < Math.min(data.length, width); col++) {
338 |         const value = data[col];
339 |         const prevValue = col > 0 ? data[col - 1] : value;
340 | 
341 |         // Determine character based on value relative to threshold
342 |         if (value >= threshold) {
343 |           // Different characters for trends
344 |           if (col > 0) {
345 |             if (value > prevValue) line += '╱'; // Rising
346 |             else if (value < prevValue) line += '╲'; // Falling
347 |             else line += '─'; // Flat
348 |           } else {
349 |             line += '●'; // Start point
350 |           }
351 |         } else {
352 |           line += ' '; // Empty space
353 |         }
354 |       }
355 |       lines.push(line);
356 |     }
357 | 
358 |     // Add axis
359 |     const axis = '   ' + '─'.repeat(Math.min(data.length, width));
360 |     lines.push(axis);
361 | 
362 |     return lines.join('\n');
363 |   }
364 | }
```

--------------------------------------------------------------------------------
/docs/guides/mcpb-installation.md:
--------------------------------------------------------------------------------

```markdown
  1 | # One-Click Installation with .mcpb Files
  2 | 
  3 | ## 🚀 Slim & Fast MCP-Only Bundle
  4 | 
  5 | **The .mcpb installation is now optimized as a slim, MCP-only runtime:**
  6 | 
  7 | ✅ **What it includes:**
  8 | - NCP MCP server (126KB compressed, 462KB unpacked)
  9 | - All orchestration, discovery, and RAG capabilities
 10 | - Optimized for fast startup and low memory usage
 11 | 
 12 | ❌ **What it excludes:**
 13 | - CLI tools (`ncp add`, `ncp find`, `ncp list`, etc.)
 14 | - CLI dependencies (Commander.js, Inquirer.js, etc.)
 15 | - 13% smaller than full package, 16% less unpacked size
 16 | 
 17 | **Configuration methods:**
 18 | 1. **Manual JSON editing** (recommended for power users)
 19 | 2. **Optional:** Install npm package separately for CLI tools
 20 | 
 21 | **Why this design?**
 22 | - .mcpb bundles use Claude Desktop's sandboxed Node.js runtime
 23 | - This runtime is only available when Claude Desktop runs MCP servers
 24 | - It's NOT in your system PATH, so CLI commands can't work
 25 | - By excluding CLI code, we get faster startup and smaller bundle
 26 | 
 27 | **Choose your workflow:**
 28 | 
 29 | ### Option A: Manual Configuration (Slim bundle only)
 30 | 1. Install .mcpb (fast, lightweight)
 31 | 2. Edit `~/.ncp/profiles/all.json` manually
 32 | 3. Perfect for automation, power users, production deployments
 33 | 
 34 | ### Option B: CLI + .mcpb (Both installed)
 35 | 1. Install .mcpb (Claude Desktop integration)
 36 | 2. Install npm: `npm install -g @portel/ncp` (CLI tools)
 37 | 3. Use CLI to configure, benefit from slim .mcpb runtime
 38 | 
 39 | ## What is a .mcpb file?
 40 | 
 41 | .mcpb (MCP Bundle) files are zip-based packages that bundle an entire MCP server with all its dependencies into a single installable file. Think of them like:
 42 | - Chrome extensions (.crx)
 43 | - VS Code extensions (.vsix)
 44 | - But for MCP servers!
 45 | 
 46 | ## Why .mcpb for NCP?
 47 | 
 48 | Installing NCP traditionally requires:
 49 | 1. Node.js installation
 50 | 2. npm commands
 51 | 3. Manual configuration editing
 52 | 4. Understanding of file paths and environment variables
 53 | 
 54 | **With .mcpb:** Download → Double-click → Done! ✨
 55 | 
 56 | **But remember:** You still need npm for CLI tools (see limitation above).
 57 | 
 58 | ## Installation Steps
 59 | 
 60 | ### For Claude Desktop Users (Auto-Import + Manual Configuration)
 61 | 
 62 | 1. **Download the bundle:**
 63 |    - Go to [NCP Releases](https://github.com/portel-dev/ncp/releases/latest)
 64 |    - Download `ncp.mcpb` from the latest release
 65 | 
 66 | 2. **Install:**
 67 |    - **macOS/Windows:** Double-click the downloaded `ncp.mcpb` file
 68 |    - Claude Desktop will show an installation dialog
 69 |    - Click "Install"
 70 | 
 71 | 3. **Continuous auto-sync:**
 72 |    - **On every startup**, NCP automatically detects and imports NEW MCPs:
 73 |      - ✅ Scans `claude_desktop_config.json` for traditional MCPs
 74 |      - ✅ Scans Claude Extensions directory for .mcpb extensions
 75 |      - ✅ Compares with NCP profile to find missing MCPs
 76 |      - ✅ Auto-imports only the new ones using internal `add` command
 77 |    - You'll see: `✨ Auto-synced X new MCPs from Claude Desktop`
 78 |    - **Cache coherence maintained**: Using internal `add` ensures vector cache, discovery index, and all other caches stay in sync
 79 |    - No manual configuration needed!
 80 | 
 81 | 4. **Add more MCPs later (manual configuration):**
 82 | 
 83 | If you want to add additional MCPs after the initial import:
 84 | 
 85 | ```bash
 86 | # Create/edit the profile configuration
 87 | mkdir -p ~/.ncp/profiles
 88 | nano ~/.ncp/profiles/all.json
 89 | ```
 90 | 
 91 | Add your MCP servers (example configuration):
 92 | 
 93 | ```json
 94 | {
 95 |   "mcpServers": {
 96 |     "filesystem": {
 97 |       "command": "npx",
 98 |       "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/yourname"]
 99 |     },
100 |     "github": {
101 |       "command": "npx",
102 |       "args": ["-y", "@modelcontextprotocol/server-github"],
103 |       "env": {
104 |         "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
105 |       }
106 |     },
107 |     "postgres": {
108 |       "command": "npx",
109 |       "args": ["-y", "@modelcontextprotocol/server-postgres"],
110 |       "env": {
111 |         "DATABASE_URL": "postgresql://user:pass@localhost:5432/dbname"
112 |       }
113 |     },
114 |     "brave-search": {
115 |       "command": "npx",
116 |       "args": ["-y", "@modelcontextprotocol/server-brave-search"],
117 |       "env": {
118 |         "BRAVE_API_KEY": "your_brave_api_key"
119 |       }
120 |     }
121 |   }
122 | }
123 | ```
124 | 
125 | **Tips for manual configuration:**
126 | - Use the same format as Claude Desktop's `claude_desktop_config.json`
127 | - Environment variables go in `env` object
128 | - Paths should be absolute, not relative
129 | - Use `npx -y` to auto-install MCP packages on first use
130 | 
131 | 4. **Restart Claude Desktop:**
132 |    - Quit Claude Desktop completely
133 |    - Reopen it
134 |    - NCP will load and index your configured MCPs
135 | 
136 | 5. **Verify:**
137 |    - Ask Claude: "What MCP tools do you have?"
138 |    - You should see NCP's `find` and `run` tools
139 |    - Ask: "Find tools for searching files"
140 |    - NCP will show tools from your configured MCPs
141 | 
142 | ### For Other MCP Clients (Cursor, Cline, Continue)
143 | 
144 | The .mcpb format is currently supported only by Claude Desktop. For other clients, use the manual installation method:
145 | 
146 | ```bash
147 | # Install NCP via npm
148 | npm install -g @portel/ncp
149 | 
150 | # Configure your client's config file manually
151 | # See README.md for client-specific configuration
152 | ```
153 | 
154 | ## What Gets Installed?
155 | 
156 | The .mcpb bundle includes:
157 | - ✅ NCP compiled code (dist/)
158 | - ✅ All Node.js dependencies
159 | - ✅ Configuration manifest
160 | - ✅ Runtime environment setup
161 | 
162 | **You don't need:**
163 | - ❌ Node.js pre-installed (Claude Desktop includes it)
164 | - ❌ Manual npm commands
165 | - ❌ Manual configuration file editing
166 | 
167 | ## Troubleshooting
168 | 
169 | ### "Cannot open file" error (macOS)
170 | 
171 | macOS may block .mcpb files from unknown developers:
172 | 
173 | **Solution:**
174 | 1. Right-click the `ncp.mcpb` file
175 | 2. Select "Open With" → "Claude Desktop"
176 | 3. If prompted, click "Open" to allow
177 | 
178 | ### "Installation failed" error
179 | 
180 | **Possible causes:**
181 | 1. Claude Desktop not updated to latest version
182 |    - **Solution:** Update Claude Desktop to support .mcpb format
183 | 
184 | 2. Corrupted download
185 |    - **Solution:** Re-download the .mcpb file
186 | 
187 | 3. Conflicting existing NCP installation
188 |    - **Solution:** Remove existing NCP from Claude config first
189 | 
190 | ### NCP not showing in tool list
191 | 
192 | **Check:**
193 | 1. Restart Claude Desktop completely (Quit → Reopen)
194 | 2. Check Claude Desktop settings → MCPs → Verify NCP is listed
195 | 3. Ask Claude: "List your available tools"
196 | 
197 | ## How We Build the .mcpb File
198 | 
199 | For developers interested in how NCP creates the .mcpb bundle:
200 | 
201 | ```bash
202 | # Build the bundle locally
203 | npm run build:mcpb
204 | 
205 | # This runs:
206 | # 1. npm run build (compiles TypeScript)
207 | # 2. npx @anthropic-ai/mcpb pack (creates .mcpb from manifest.json)
208 | ```
209 | 
210 | The `manifest.json` describes:
211 | - NCP's capabilities
212 | - Entry point (dist/index.js)
213 | - Required tools
214 | - Environment variables
215 | - Node.js version requirements
216 | 
217 | ## Updating NCP
218 | 
219 | When a new version is released:
220 | 
221 | 1. **Download new .mcpb** from latest release
222 | 2. **Double-click to install** - it will replace the old version
223 | 3. **Restart Claude Desktop**
224 | 
225 | ## Comparison: .mcpb vs npm Installation
226 | 
227 | | Aspect | .mcpb Installation | npm Installation |
228 | |--------|-------------------|------------------|
229 | | **Ease** | Double-click | Multiple commands |
230 | | **Prerequisites** | None (Claude Desktop has runtime) | Node.js 18+ |
231 | | **Time** | 10 seconds + manual config | 2-3 minutes with CLI |
232 | | **Bundle Size** | **126KB** (slim, MCP-only) | ~2.5MB (full package with CLI) |
233 | | **Startup Time** | ⚡ Faster (no CLI code loading) | Standard (includes CLI) |
234 | | **Memory Usage** | 💚 Lower (minimal footprint) | Standard (full features) |
235 | | **CLI Tools** | ❌ NO - Manual JSON editing only | ✅ YES - `ncp add`, `ncp find`, etc. |
236 | | **MCP Server** | ✅ YES - Works in Claude Desktop | ✅ YES - Works in all MCP clients |
237 | | **Configuration** | 📝 Manual JSON editing | 🔧 CLI commands or JSON |
238 | | **Updates** | Download new .mcpb | `npm update -g @portel/ncp` |
239 | | **Client Support** | Claude Desktop only | All MCP clients |
240 | | **Best for** | ✅ Power users, automation, production | ✅ General users, development |
241 | 
242 | ## How Continuous Auto-Sync Works
243 | 
244 | **On every startup**, NCP automatically syncs with Claude Desktop to detect new MCPs:
245 | 
246 | ### Sync Process
247 | 
248 | 1. **Scans Claude Desktop configuration:**
249 |    - **JSON config:** Reads `~/Library/Application Support/Claude/claude_desktop_config.json`
250 |    - **.mcpb extensions:** Scans `~/Library/Application Support/Claude/Claude Extensions/`
251 | 
252 | 2. **Extracts MCP configurations:**
253 |    - For JSON MCPs: Extracts command, args, env
254 |    - For .mcpb extensions: Reads `manifest.json`, resolves `${__dirname}` paths
255 | 
256 | 3. **Detects missing MCPs:**
257 |    - Compares Claude Desktop MCPs vs NCP profile
258 |    - Identifies MCPs that exist in Claude Desktop but NOT in NCP
259 | 
260 | 4. **Imports missing MCPs using internal `add` command:**
261 |    - For each missing MCP: `await this.addMCPToProfile('all', name, config)`
262 |    - This ensures **cache coherence**:
263 |      - ✅ Profile JSON gets updated
264 |      - ✅ Cache invalidation triggers on next orchestrator init
265 |      - ✅ Vector embeddings regenerate for new tools
266 |      - ✅ Discovery index includes new MCPs
267 |      - ✅ All caches stay in sync
268 | 
269 | 5. **Skips existing MCPs:**
270 |    - If MCP already exists in NCP → No action
271 |    - Prevents duplicate imports and cache thrashing
272 | 
273 | ### Example Auto-Sync Output
274 | 
275 | **First startup (multiple MCPs found):**
276 | ```
277 | ✨ Auto-synced 6 new MCPs from Claude Desktop:
278 |    - 4 from claude_desktop_config.json
279 |    - 2 from .mcpb extensions
280 |    → Added to ~/.ncp/profiles/all.json
281 | ```
282 | 
283 | **Subsequent startup (1 new MCP detected):**
284 | ```
285 | ✨ Auto-synced 1 new MCPs from Claude Desktop:
286 |    - 1 from .mcpb extensions
287 |    → Added to ~/.ncp/profiles/all.json
288 | ```
289 | 
290 | **Subsequent startup (no new MCPs):**
291 | ```
292 | (No output - all Claude Desktop MCPs already in sync)
293 | ```
294 | 
295 | ### What Gets Imported
296 | 
297 | **From `claude_desktop_config.json`:**
298 | ```json
299 | {
300 |   "mcpServers": {
301 |     "filesystem": {
302 |       "command": "npx",
303 |       "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/name"]
304 |     }
305 |   }
306 | }
307 | ```
308 | 
309 | **From `.mcpb` extensions:**
310 | - Installed via double-click in Claude Desktop
311 | - Stored in `Claude Extensions/` directory
312 | - Automatically detected and imported with correct paths
313 | 
314 | **Result in NCP:**
315 | ```json
316 | {
317 |   "filesystem": {
318 |     "command": "npx",
319 |     "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/name"],
320 |     "_source": "json"
321 |   },
322 |   "apple-mcp": {
323 |     "command": "node",
324 |     "args": ["/Users/.../Claude Extensions/local.dxt.../dist/index.js"],
325 |     "_source": ".mcpb",
326 |     "_extensionId": "local.dxt.dhravya-shah.apple-mcp",
327 |     "_version": "1.0.0"
328 |   }
329 | }
330 | ```
331 | 
332 | ## FAQ
333 | 
334 | ### Q: Does NCP automatically sync with Claude Desktop?
335 | **A:** ✅ **YES!** On **every startup**, NCP automatically detects and imports NEW MCPs:
336 | - Scans `claude_desktop_config.json` for traditional MCPs
337 | - Scans Claude Extensions for .mcpb-installed extensions
338 | - Compares with NCP profile to find missing MCPs
339 | - Auto-imports only the new ones
340 | 
341 | **Workflow example:**
342 | 1. Day 1: Install NCP → Auto-syncs 5 existing MCPs
343 | 2. Day 2: Install new .mcpb extension in Claude Desktop
344 | 3. Day 3: Restart Claude Desktop → NCP auto-syncs the new MCP
345 | 4. **Zero manual configuration** - NCP stays in sync automatically!
346 | 
347 | ### Q: Can I use `ncp add` after .mcpb installation?
348 | **A:** ❌ **NO.** The .mcpb is a slim MCP-only bundle that excludes CLI code. You configure MCPs by editing `~/.ncp/profiles/all.json` manually.
349 | 
350 | **If you want CLI tools:** Run `npm install -g @portel/ncp` separately.
351 | 
352 | ### Q: How do I add MCPs without the CLI?
353 | **A:** Edit `~/.ncp/profiles/all.json` directly:
354 | 
355 | ```bash
356 | nano ~/.ncp/profiles/all.json
357 | ```
358 | 
359 | Add your MCPs using the same format as Claude Desktop's config:
360 | 
361 | ```json
362 | {
363 |   "mcpServers": {
364 |     "your-mcp-name": {
365 |       "command": "npx",
366 |       "args": ["-y", "@modelcontextprotocol/server-name"],
367 |       "env": {}
368 |     }
369 |   }
370 | }
371 | ```
372 | 
373 | Restart Claude Desktop for changes to take effect.
374 | 
375 | ### Q: Why is .mcpb smaller than npm?
376 | **A:** The .mcpb bundle (126KB) excludes all CLI code and dependencies:
377 | - ❌ No Commander.js, Inquirer.js, or other CLI libraries
378 | - ❌ No `dist/cli/` directory
379 | - ✅ Only MCP server, orchestrator, and discovery code
380 | 
381 | This makes it 13% smaller and faster to load.
382 | 
383 | ### Q: When should I use .mcpb vs npm?
384 | **A:**
385 | 
386 | **Use .mcpb if:**
387 | - You're comfortable editing JSON configs manually
388 | - You want the smallest, fastest MCP runtime
389 | - You're deploying in production/automation
390 | - You only use Claude Desktop
391 | 
392 | **Use npm if:**
393 | - You want CLI tools (`ncp add`, `ncp find`, etc.)
394 | - You use multiple MCP clients (Cursor, Cline, Continue)
395 | - You prefer commands over manual JSON editing
396 | - You want a complete solution
397 | 
398 | **Both:** Install .mcpb for slim runtime + npm for CLI tools
399 | 
400 | ### Q: Do I need Node.js installed for .mcpb?
401 | **A:** No, Claude Desktop includes Node.js runtime for .mcpb bundles. However, if you need the CLI tools (which you do for NCP), you'll need Node.js + npm anyway.
402 | 
403 | ### Q: Can I use .mcpb with Cursor/Cline/Continue?
404 | **A:** Not yet. The .mcpb format is currently Claude Desktop-only. Use npm installation for other clients.
405 | 
406 | ### Q: How do I uninstall?
407 | **A:** In Claude Desktop settings → MCPs → Find NCP → Click "Remove"
408 | 
409 | ### Q: Can I customize NCP settings via .mcpb?
410 | **A:** Basic environment variables can be configured through Claude Desktop settings after installation. For advanced configuration, use npm installation instead.
411 | 
412 | ### Q: Is .mcpb secure?
413 | **A:** .mcpb files are reviewed by Claude Desktop before installation. Always download from official NCP releases on GitHub.
414 | 
415 | ## Future Plans
416 | 
417 | - Support for other MCP clients (Cursor, Cline, Continue)
418 | - Auto-update mechanism
419 | - Configuration wizard within .mcpb
420 | - Multiple profile support
421 | 
422 | ## More Information
423 | 
424 | - [MCP Bundle Specification](https://github.com/anthropics/mcpb)
425 | - [Claude Desktop Extensions Documentation](https://www.anthropic.com/engineering/desktop-extensions)
426 | - [NCP GitHub Repository](https://github.com/portel-dev/ncp)
427 | 
```

--------------------------------------------------------------------------------
/EXTENSION-CONFIG-DISCOVERY.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Extension User Configuration - Major Discovery! 🎉
  2 | 
  3 | ## What We Found
  4 | 
  5 | Claude Desktop extensions support a **`user_config`** field in `manifest.json` that enables:
  6 | 
  7 | 1. ✅ **Declarative configuration** - Extensions declare what config they need
  8 | 2. ✅ **Type-safe inputs** - String, number, boolean, directory, file
  9 | 3. ✅ **Secure storage** - Sensitive values stored in OS keychain
 10 | 4. ✅ **Runtime injection** - Values injected via `${user_config.KEY}` template literals
 11 | 5. ✅ **Validation** - Required fields, min/max constraints, default values
 12 | 
 13 | **This opens MASSIVE possibilities for NCP!**
 14 | 
 15 | ---
 16 | 
 17 | ## Complete Specification
 18 | 
 19 | ### **Supported Configuration Types**
 20 | 
 21 | | Type | Description | Example Use Case |
 22 | |------|-------------|------------------|
 23 | | `string` | Text input | API keys, URLs, usernames |
 24 | | `number` | Numeric input | Port numbers, timeouts, limits |
 25 | | `boolean` | Checkbox/toggle | Enable/disable features |
 26 | | `directory` | Directory picker | Allowed paths, workspace folders |
 27 | | `file` | File picker | Config files, credentials |
 28 | 
 29 | ### **Configuration Properties**
 30 | 
 31 | ```typescript
 32 | interface UserConfigOption {
 33 |   type: 'string' | 'number' | 'boolean' | 'directory' | 'file';
 34 |   title: string;           // Display name in UI
 35 |   description?: string;    // Help text
 36 |   required?: boolean;      // Must be provided (default: false)
 37 |   default?: any;          // Default value (supports variables)
 38 |   sensitive?: boolean;    // Mask input + store in keychain
 39 |   multiple?: boolean;     // Allow multiple selections (directory/file)
 40 |   min?: number;          // Minimum value (number type)
 41 |   max?: number;          // Maximum value (number type)
 42 | }
 43 | ```
 44 | 
 45 | ### **Variable Substitution**
 46 | 
 47 | Supports these built-in variables:
 48 | - `${HOME}` - User home directory
 49 | - `${DESKTOP}` - Desktop folder
 50 | - `${__dirname}` - Extension directory
 51 | 
 52 | ### **Template Injection**
 53 | 
 54 | Reference user config in `mcp_config`:
 55 | ```json
 56 | {
 57 |   "user_config": {
 58 |     "api_key": { "type": "string", "sensitive": true }
 59 |   },
 60 |   "server": {
 61 |     "mcp_config": {
 62 |       "env": {
 63 |         "API_KEY": "${user_config.api_key}"  // ← Injected at runtime
 64 |       }
 65 |     }
 66 |   }
 67 | }
 68 | ```
 69 | 
 70 | ---
 71 | 
 72 | ## Real-World Examples
 73 | 
 74 | ### **Example 1: GitHub Extension**
 75 | 
 76 | ```json
 77 | {
 78 |   "name": "github",
 79 |   "user_config": {
 80 |     "github_token": {
 81 |       "type": "string",
 82 |       "title": "GitHub Personal Access Token",
 83 |       "description": "Token with repo and workflow permissions",
 84 |       "sensitive": true,
 85 |       "required": true
 86 |     },
 87 |     "default_owner": {
 88 |       "type": "string",
 89 |       "title": "Default Repository Owner",
 90 |       "description": "Your GitHub username or organization",
 91 |       "default": ""
 92 |     },
 93 |     "max_search_results": {
 94 |       "type": "number",
 95 |       "title": "Maximum Search Results",
 96 |       "description": "Limit number of results returned",
 97 |       "default": 10,
 98 |       "min": 1,
 99 |       "max": 100
100 |     }
101 |   },
102 |   "server": {
103 |     "mcp_config": {
104 |       "command": "node",
105 |       "args": ["${__dirname}/server/index.js"],
106 |       "env": {
107 |         "GITHUB_TOKEN": "${user_config.github_token}",
108 |         "DEFAULT_OWNER": "${user_config.default_owner}",
109 |         "MAX_RESULTS": "${user_config.max_search_results}"
110 |       }
111 |     }
112 |   }
113 | }
114 | ```
115 | 
116 | ### **Example 2: Filesystem Extension**
117 | 
118 | ```json
119 | {
120 |   "name": "filesystem",
121 |   "user_config": {
122 |     "allowed_directories": {
123 |       "type": "directory",
124 |       "title": "Allowed Directories",
125 |       "description": "Directories the server can access",
126 |       "multiple": true,
127 |       "required": true,
128 |       "default": ["${HOME}/Documents", "${HOME}/Desktop"]
129 |     },
130 |     "read_only": {
131 |       "type": "boolean",
132 |       "title": "Read-only Mode",
133 |       "description": "Prevent write operations",
134 |       "default": false
135 |     }
136 |   },
137 |   "server": {
138 |     "mcp_config": {
139 |       "command": "node",
140 |       "args": ["${__dirname}/server/index.js"],
141 |       "env": {
142 |         "ALLOWED_DIRECTORIES": "${user_config.allowed_directories}",
143 |         "READ_ONLY": "${user_config.read_only}"
144 |       }
145 |     }
146 |   }
147 | }
148 | ```
149 | 
150 | ### **Example 3: Database Extension**
151 | 
152 | ```json
153 | {
154 |   "name": "postgresql",
155 |   "user_config": {
156 |     "connection_string": {
157 |       "type": "string",
158 |       "title": "PostgreSQL Connection String",
159 |       "description": "Database connection URL",
160 |       "sensitive": true,
161 |       "required": true
162 |     },
163 |     "max_connections": {
164 |       "type": "number",
165 |       "title": "Maximum Connections",
166 |       "default": 10,
167 |       "min": 1,
168 |       "max": 50
169 |     },
170 |     "ssl_enabled": {
171 |       "type": "boolean",
172 |       "title": "Use SSL",
173 |       "default": true
174 |     },
175 |     "ssl_cert_path": {
176 |       "type": "file",
177 |       "title": "SSL Certificate",
178 |       "description": "Path to SSL certificate file"
179 |     }
180 |   },
181 |   "server": {
182 |     "mcp_config": {
183 |       "command": "node",
184 |       "args": ["${__dirname}/server/index.js"],
185 |       "env": {
186 |         "DATABASE_URL": "${user_config.connection_string}",
187 |         "MAX_CONNECTIONS": "${user_config.max_connections}",
188 |         "SSL_ENABLED": "${user_config.ssl_enabled}",
189 |         "SSL_CERT": "${user_config.ssl_cert_path}"
190 |       }
191 |     }
192 |   }
193 | }
194 | ```
195 | 
196 | ---
197 | 
198 | ## How Claude Desktop Handles This
199 | 
200 | ### **1. Configuration UI**
201 | When user enables extension:
202 | - Claude Desktop reads `user_config` from manifest
203 | - Renders configuration dialog with appropriate inputs
204 | - Shows titles, descriptions, validation rules
205 | - Validates input before allowing extension to activate
206 | 
207 | ### **2. Secure Storage**
208 | For sensitive fields:
209 | - Values stored in **OS keychain** (not in JSON config)
210 | - macOS: Keychain Access
211 | - Windows: Credential Manager
212 | - Linux: Secret Service API
213 | 
214 | ### **3. Runtime Injection**
215 | When spawning MCP server:
216 | - Reads user config from secure storage
217 | - Replaces `${user_config.KEY}` with actual values
218 | - Injects into environment variables or args
219 | - MCP server receives final config
220 | 
221 | ---
222 | 
223 | ## HUGE Possibilities for NCP!
224 | 
225 | ### **Current Problem**
226 | 
227 | When NCP auto-imports extensions:
228 | ```json
229 | {
230 |   "github": {
231 |     "command": "node",
232 |     "args": ["/path/to/extension/index.js"],
233 |     "env": {}  // ← EMPTY! No API key configured
234 |   }
235 | }
236 | ```
237 | 
238 | Extension won't work without configuration!
239 | 
240 | ### **Solution 1: Configuration Detection + Prompts**
241 | 
242 | **Flow:**
243 | ```
244 | 1. NCP auto-imports extension
245 |    ↓
246 | 2. Reads manifest.json → Detects user_config requirements
247 |    ↓
248 | 3. AI calls prompt: "configure_extension"
249 |    Shows: "GitHub extension needs: GitHub Token (required)"
250 |    ↓
251 | 4. User copies config to clipboard:
252 |    {"github_token": "ghp_..."}
253 |    ↓
254 | 5. User clicks YES
255 |    ↓
256 | 6. NCP reads clipboard (server-side)
257 |    ↓
258 | 7. NCP stores config securely
259 |    ↓
260 | 8. When spawning: Injects ${user_config.github_token} → env.GITHUB_TOKEN
261 |    ↓
262 | 9. Extension works perfectly!
263 | ```
264 | 
265 | ### **Solution 2: Batch Configuration via ncp:import**
266 | 
267 | **Discovery mode with configuration:**
268 | ```
269 | User: "Find GitHub MCPs"
270 | AI: Shows numbered list with config requirements
271 | 
272 | 1. ⭐ server-github (requires: API Token)
273 | 2. ⭐ github-actions (requires: Token, Repo)
274 | 
275 | User: "Import 1"
276 | AI: Shows prompt with clipboard instructions
277 | User: Copies {"github_token": "ghp_..."}
278 | User: Clicks YES
279 | AI: Imports with configuration
280 | ```
281 | 
282 | ### **Solution 3: Interactive Configuration via ncp:configure**
283 | 
284 | New internal tool:
285 | ```typescript
286 | ncp:configure {
287 |   mcp_name: "github",
288 |   // User copies full config to clipboard before calling
289 | }
290 | ```
291 | 
292 | Shows what's needed, collects via clipboard, stores securely.
293 | 
294 | ---
295 | 
296 | ## Implementation Plan
297 | 
298 | ### **Phase 1: Detection**
299 | 
300 | **Add to client-importer:**
301 | ```typescript
302 | // When importing .mcpb extensions
303 | const manifest = JSON.parse(manifestContent);
304 | 
305 | // Extract user_config requirements
306 | const userConfigSchema = manifest.user_config || {};
307 | const userConfigRequired = Object.entries(userConfigSchema)
308 |   .filter(([key, config]) => config.required)
309 |   .map(([key, config]) => ({
310 |     key,
311 |     title: config.title,
312 |     type: config.type,
313 |     sensitive: config.sensitive
314 |   }));
315 | 
316 | // Store in imported config
317 | mcpServers[mcpName] = {
318 |   command,
319 |   args,
320 |   env: mcpConfig.env || {},
321 |   _source: '.mcpb',
322 |   _userConfigSchema: userConfigSchema,      // ← NEW
323 |   _userConfigRequired: userConfigRequired,  // ← NEW
324 |   _userConfig: {}  // Will be populated later
325 | };
326 | ```
327 | 
328 | ### **Phase 2: Prompt Definition**
329 | 
330 | **Add new prompt:** `configure_extension`
331 | 
332 | ```typescript
333 | {
334 |   name: 'configure_extension',
335 |   description: 'Collect configuration for MCP extension',
336 |   arguments: [
337 |     { name: 'mcp_name', description: 'Extension name', required: true },
338 |     { name: 'config_schema', description: 'Configuration requirements', required: true }
339 |   ]
340 | }
341 | ```
342 | 
343 | **Prompt message:**
344 | ```
345 | Extension "${mcp_name}" requires configuration:
346 | 
347 | ${config_schema.map(field => `
348 | • ${field.title} (${field.type})
349 |   ${field.description}
350 |   ${field.required ? 'REQUIRED' : 'Optional'}
351 |   ${field.sensitive ? '⚠️ Sensitive - will be stored securely' : ''}
352 | `).join('\n')}
353 | 
354 | 📋 Copy configuration to clipboard in JSON format:
355 | {
356 |   "${field.key}": "your_value"
357 | }
358 | 
359 | Then click YES to save configuration.
360 | ```
361 | 
362 | ### **Phase 3: Storage**
363 | 
364 | **Secure user config storage:**
365 | ```typescript
366 | // Separate file for user configs
367 | ~/.ncp/user-configs/{profile-name}.json
368 | 
369 | {
370 |   "github": {
371 |     "github_token": "ghp_...",  // Will move to OS keychain later
372 |     "default_owner": "myorg"
373 |   },
374 |   "filesystem": {
375 |     "allowed_directories": ["/Users/me/Projects"]
376 |   }
377 | }
378 | ```
379 | 
380 | **Later:** Integrate with OS keychain for sensitive values.
381 | 
382 | ### **Phase 4: Runtime Injection**
383 | 
384 | **Update orchestrator spawn logic:**
385 | ```typescript
386 | // Before spawning
387 | const userConfig = await getUserConfig(mcpName);
388 | const resolvedEnv = resolveTemplates(definition.config.env, {
389 |   user_config: userConfig
390 | });
391 | 
392 | // Replace ${user_config.KEY} with actual values
393 | const transport = new StdioClientTransport({
394 |   command: resolvedCommand,
395 |   args: resolvedArgs,
396 |   env: resolvedEnv  // ← Injected values
397 | });
398 | ```
399 | 
400 | ### **Phase 5: New Internal Tools**
401 | 
402 | **`ncp:configure`** - Configure extension
403 | ```typescript
404 | {
405 |   mcp_name: string,
406 |   // User copies config to clipboard before calling
407 | }
408 | ```
409 | 
410 | **`ncp:list` enhancement** - Show config status
411 | ```
412 | ✓ github (configured)
413 |   • github_token: ******* (from clipboard)
414 |   • default_owner: myorg
415 | 
416 | ⚠ filesystem (needs configuration)
417 |   Required: allowed_directories
418 | ```
419 | 
420 | ---
421 | 
422 | ## Benefits
423 | 
424 | ### **For Users**
425 | 
426 | ✅ **No manual config editing** - AI handles everything via prompts
427 | ✅ **Clipboard security** - Secrets never exposed to AI
428 | ✅ **Guided configuration** - Shows exactly what's needed
429 | ✅ **Validation** - Type checking, required fields, constraints
430 | ✅ **Works with disabled extensions** - NCP manages config independently
431 | 
432 | ### **For Extensions**
433 | 
434 | ✅ **Standard configuration** - Same schema as Claude Desktop
435 | ✅ **Compatibility** - Works when enabled OR disabled
436 | ✅ **Secure storage** - OS keychain integration (future)
437 | ✅ **Type safety** - Number/boolean/string/directory/file types
438 | 
439 | ### **For NCP**
440 | 
441 | ✅ **Complete workflow** - Discovery → Import → Configure → Run
442 | ✅ **Differentiation** - Only MCP manager with smart config handling
443 | ✅ **User experience** - Seamless AI-driven configuration
444 | ✅ **Clipboard pattern** - Extends to configuration (not just secrets)
445 | 
446 | ---
447 | 
448 | ## Example End-to-End Workflow
449 | 
450 | ### **User Story: Install GitHub Extension**
451 | 
452 | ```
453 | User: "Find and install GitHub MCP"
454 | 
455 | AI: [Calls ncp:import discovery mode]
456 |     I found server-github in the registry.
457 | 
458 |     [Calls ncp:import with selection]
459 |     Imported server-github.
460 | 
461 |     ⚠️ This extension requires configuration:
462 |     • GitHub Personal Access Token (required, sensitive)
463 |     • Default Repository Owner (optional)
464 | 
465 |     [Shows configure_extension prompt]
466 | 
467 | Prompt: "Copy configuration to clipboard in this format:
468 | {
469 |   "github_token": "ghp_your_token_here",
470 |   "default_owner": "your_username"
471 | }
472 | 
473 | Then click YES to save configuration."
474 | 
475 | User: [Copies config to clipboard]
476 | User: [Clicks YES]
477 | 
478 | AI: [NCP reads clipboard, stores config]
479 |     ✅ GitHub extension configured and ready to use!
480 | 
481 | User: "Create an issue in my repo"
482 | 
483 | AI: [Calls ncp:run github:create_issue]
484 |     [NCP injects github_token into env]
485 |     [Extension works perfectly!]
486 | ```
487 | 
488 | ---
489 | 
490 | ## Next Steps
491 | 
492 | ### **Immediate (Can Do Now)**
493 | 
494 | 1. ✅ Extract `user_config` from manifest.json during import
495 | 2. ✅ Store schema with imported MCP config
496 | 3. ✅ Show warnings when extensions need configuration
497 | 
498 | ### **Short-term (Phase 1)**
499 | 
500 | 1. Add `configure_extension` prompt
501 | 2. Implement clipboard-based config collection
502 | 3. Store user configs in separate file
503 | 4. Implement template replacement (`${user_config.KEY}`)
504 | 
505 | ### **Medium-term (Phase 2)**
506 | 
507 | 1. Add `ncp:configure` internal tool
508 | 2. Enhance `ncp:list` to show config status
509 | 3. Add validation (type checking, required fields)
510 | 4. Support default values and variable substitution
511 | 
512 | ### **Long-term (Phase 3)**
513 | 
514 | 1. OS keychain integration for sensitive values
515 | 2. Config migration between profiles
516 | 3. Config export/import
517 | 4. Config versioning and updates
518 | 
519 | ---
520 | 
521 | ## Summary
522 | 
523 | **What we discovered:**
524 | - Extensions declare configuration needs via `user_config` in manifest.json
525 | - Claude Desktop handles UI, validation, secure storage, and injection
526 | - Template literals (`${user_config.KEY}`) replaced at runtime
527 | 
528 | **What this enables for NCP:**
529 | - AI-driven configuration via prompts + clipboard security
530 | - Auto-detect configuration requirements
531 | - Secure storage separate from MCP config
532 | - Runtime injection when spawning extensions
533 | - Complete discovery → import → configure → run workflow
534 | 
535 | **This is MASSIVE for the NCP user experience!** 🚀
536 | 
537 | Users can now:
538 | 1. Discover MCPs via AI
539 | 2. Import with one command
540 | 3. Configure via clipboard (secure!)
541 | 4. Run immediately with full functionality
542 | 
543 | No CLI, no manual JSON editing, no copy-paste of configs - everything through natural conversation!
544 | 
```
Page 7/12FirstPrevNextLast