This is page 2 of 12. Use http://codebase.md/portel-dev/ncp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .dxtignore ├── .github │ ├── FEATURE_STORY_TEMPLATE.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── mcp_server_request.yml │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── publish-mcp-registry.yml │ └── release.yml ├── .gitignore ├── .mcpbignore ├── .npmignore ├── .release-it.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── COMPLETE-IMPLEMENTATION-SUMMARY.md ├── CONTRIBUTING.md ├── CRITICAL-ISSUES-FOUND.md ├── docs │ ├── clients │ │ ├── claude-desktop.md │ │ ├── cline.md │ │ ├── continue.md │ │ ├── cursor.md │ │ ├── perplexity.md │ │ └── README.md │ ├── download-stats.md │ ├── guides │ │ ├── clipboard-security-pattern.md │ │ ├── how-it-works.md │ │ ├── mcp-prompts-for-user-interaction.md │ │ ├── mcpb-installation.md │ │ ├── ncp-registry-command.md │ │ ├── pre-release-checklist.md │ │ ├── telemetry-design.md │ │ └── testing.md │ ├── images │ │ ├── ncp-add.png │ │ ├── ncp-find.png │ │ ├── ncp-help.png │ │ ├── ncp-import.png │ │ ├── ncp-list.png │ │ └── ncp-transformation-flow.png │ ├── mcp-registry-setup.md │ ├── pr-schema-additions.ts │ └── stories │ ├── 01-dream-and-discover.md │ ├── 02-secrets-in-plain-sight.md │ ├── 03-sync-and-forget.md │ ├── 04-double-click-install.md │ ├── 05-runtime-detective.md │ └── 06-official-registry.md ├── DYNAMIC-RUNTIME-SUMMARY.md ├── EXTENSION-CONFIG-DISCOVERY.md ├── INSTALL-EXTENSION.md ├── INTERNAL-MCP-ARCHITECTURE.md ├── jest.config.js ├── LICENSE ├── MANAGEMENT-TOOLS-COMPLETE.md ├── manifest.json ├── manifest.json.backup ├── MCP-CONFIG-SCHEMA-IMPLEMENTATION-EXAMPLE.ts ├── MCP-CONFIG-SCHEMA-SIMPLE-EXAMPLE.json ├── MCP-CONFIGURATION-SCHEMA-FORMAT.json ├── MCPB-ARCHITECTURE-DECISION.md ├── NCP-EXTENSION-COMPLETE.md ├── package-lock.json ├── package.json ├── parity-between-cli-and-mcp.txt ├── PROMPTS-IMPLEMENTATION.md ├── README-COMPARISON.md ├── README.md ├── README.new.md ├── REGISTRY-INTEGRATION-COMPLETE.md ├── RELEASE-PROCESS-IMPROVEMENTS.md ├── RELEASE-SUMMARY.md ├── RELEASE.md ├── RUNTIME-DETECTION-COMPLETE.md ├── scripts │ ├── cleanup │ │ └── scan-repository.js │ └── sync-server-version.cjs ├── SECURITY.md ├── server.json ├── src │ ├── analytics │ │ ├── analytics-formatter.ts │ │ ├── log-parser.ts │ │ └── visual-formatter.ts │ ├── auth │ │ ├── oauth-device-flow.ts │ │ └── token-store.ts │ ├── cache │ │ ├── cache-patcher.ts │ │ ├── csv-cache.ts │ │ └── schema-cache.ts │ ├── cli │ │ └── index.ts │ ├── discovery │ │ ├── engine.ts │ │ ├── mcp-domain-analyzer.ts │ │ ├── rag-engine.ts │ │ ├── search-enhancer.ts │ │ └── semantic-enhancement-engine.ts │ ├── extension │ │ └── extension-init.ts │ ├── index-mcp.ts │ ├── index.ts │ ├── internal-mcps │ │ ├── internal-mcp-manager.ts │ │ ├── ncp-management.ts │ │ └── types.ts │ ├── orchestrator │ │ └── ncp-orchestrator.ts │ ├── profiles │ │ └── profile-manager.ts │ ├── server │ │ ├── mcp-prompts.ts │ │ └── mcp-server.ts │ ├── services │ │ ├── config-prompter.ts │ │ ├── config-schema-reader.ts │ │ ├── error-handler.ts │ │ ├── output-formatter.ts │ │ ├── registry-client.ts │ │ ├── tool-context-resolver.ts │ │ ├── tool-finder.ts │ │ ├── tool-schema-parser.ts │ │ └── usage-tips-generator.ts │ ├── testing │ │ ├── create-real-mcp-definitions.ts │ │ ├── dummy-mcp-server.ts │ │ ├── mcp-definitions.json │ │ ├── real-mcp-analyzer.ts │ │ ├── real-mcp-definitions.json │ │ ├── real-mcps.csv │ │ ├── setup-dummy-mcps.ts │ │ ├── setup-tiered-profiles.ts │ │ ├── test-profile.json │ │ ├── test-semantic-enhancement.ts │ │ └── verify-profile-scaling.ts │ ├── transports │ │ └── filtered-stdio-transport.ts │ └── utils │ ├── claude-desktop-importer.ts │ ├── client-importer.ts │ ├── client-registry.ts │ ├── config-manager.ts │ ├── health-monitor.ts │ ├── highlighting.ts │ ├── logger.ts │ ├── markdown-renderer.ts │ ├── mcp-error-parser.ts │ ├── mcp-wrapper.ts │ ├── ncp-paths.ts │ ├── parameter-prompter.ts │ ├── paths.ts │ ├── progress-spinner.ts │ ├── response-formatter.ts │ ├── runtime-detector.ts │ ├── schema-examples.ts │ ├── security.ts │ ├── text-utils.ts │ ├── update-checker.ts │ ├── updater.ts │ └── version.ts ├── STORY-DRIVEN-DOCUMENTATION.md ├── STORY-FIRST-WORKFLOW.md ├── test │ ├── __mocks__ │ │ ├── chalk.js │ │ ├── transformers.js │ │ ├── updater.js │ │ └── version.ts │ ├── cache-loading-focused.test.ts │ ├── cache-optimization.test.ts │ ├── cli-help-validation.sh │ ├── coverage-boost.test.ts │ ├── curated-ecosystem-validation.test.ts │ ├── discovery-engine.test.ts │ ├── discovery-fallback-focused.test.ts │ ├── ecosystem-discovery-focused.test.ts │ ├── ecosystem-discovery-validation-simple.test.ts │ ├── final-80-percent-push.test.ts │ ├── final-coverage-push.test.ts │ ├── health-integration.test.ts │ ├── health-monitor.test.ts │ ├── helpers │ │ └── mock-server-manager.ts │ ├── integration │ │ └── mcp-client-simulation.test.cjs │ ├── logger.test.ts │ ├── mcp-ecosystem-discovery.test.ts │ ├── mcp-error-parser.test.ts │ ├── mcp-immediate-response-check.js │ ├── mcp-server-protocol.test.ts │ ├── mcp-timeout-scenarios.test.ts │ ├── mcp-wrapper.test.ts │ ├── mock-mcps │ │ ├── aws-server.js │ │ ├── base-mock-server.mjs │ │ ├── brave-search-server.js │ │ ├── docker-server.js │ │ ├── filesystem-server.js │ │ ├── git-server.mjs │ │ ├── github-server.js │ │ ├── neo4j-server.js │ │ ├── notion-server.js │ │ ├── playwright-server.js │ │ ├── postgres-server.js │ │ ├── shell-server.js │ │ ├── slack-server.js │ │ └── stripe-server.js │ ├── mock-smithery-mcp │ │ ├── index.js │ │ ├── package.json │ │ └── smithery.yaml │ ├── ncp-orchestrator.test.ts │ ├── orchestrator-health-integration.test.ts │ ├── orchestrator-simple-branches.test.ts │ ├── performance-benchmark.test.ts │ ├── quick-coverage.test.ts │ ├── rag-engine.test.ts │ ├── regression-snapshot.test.ts │ ├── search-enhancer.test.ts │ ├── session-id-passthrough.test.ts │ ├── setup.ts │ ├── tool-context-resolver.test.ts │ ├── tool-schema-parser.test.ts │ ├── user-story-discovery.test.ts │ └── version-util.test.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /src/transports/filtered-stdio-transport.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Filtered Output for MCP Clients 3 | * 4 | * Since we're running as a client (not intercepting server output), 5 | * we need to filter the console output that leaks through when 6 | * executing tools via subprocess MCPs. 7 | */ 8 | 9 | import { logger } from '../utils/logger.js'; 10 | 11 | // Store original console methods 12 | const originalConsoleLog = console.log; 13 | const originalConsoleError = console.error; 14 | const originalConsoleWarn = console.warn; 15 | const originalConsoleInfo = console.info; 16 | 17 | // Track if filtering is active 18 | let filteringActive = false; 19 | 20 | /** 21 | * List of patterns to filter from console output 22 | */ 23 | const FILTER_PATTERNS = [ 24 | // MCP server startup messages 25 | 'running on stdio', 26 | 'MCP Server running', 27 | 'MCP server running', 28 | 'Server running on stdio', 29 | 'Client does not support', 30 | 31 | // Specific MCP messages 32 | 'Secure MCP Filesystem Server', 33 | 'Knowledge Graph MCP Server', 34 | 'Sequential Thinking MCP Server', 35 | 'Stripe MCP Server', 36 | 37 | // Connection messages 38 | 'Connecting to server:', 39 | 'Streamable HTTP connection', 40 | 'Received exit signal', 41 | 'Starting cleanup process', 42 | 'Final cleanup on exit', 43 | '[Runner]', 44 | 45 | // Tool execution artifacts 46 | 'Shell cwd was reset' 47 | ]; 48 | 49 | /** 50 | * Check if a message should be filtered 51 | */ 52 | function shouldFilter(args: any[]): boolean { 53 | if (!filteringActive) return false; 54 | 55 | const message = args.map(arg => 56 | typeof arg === 'string' ? arg : JSON.stringify(arg) 57 | ).join(' '); 58 | 59 | return FILTER_PATTERNS.some(pattern => message.includes(pattern)); 60 | } 61 | 62 | /** 63 | * Create a filtered console method 64 | */ 65 | function createFilteredMethod(originalMethod: typeof console.log): typeof console.log { 66 | return function(...args: any[]) { 67 | if (!shouldFilter(args)) { 68 | originalMethod.apply(console, args); 69 | } else if (process.env.NCP_DEBUG_FILTER === 'true') { 70 | // In debug mode, show what we're filtering 71 | originalConsoleError.call(console, '[Filtered]:', ...args); 72 | } 73 | } as typeof console.log; 74 | } 75 | 76 | /** 77 | * Enable console output filtering 78 | */ 79 | export function enableOutputFilter(): void { 80 | if (filteringActive) return; 81 | 82 | filteringActive = true; 83 | console.log = createFilteredMethod(originalConsoleLog) as typeof console.log; 84 | console.error = createFilteredMethod(originalConsoleError) as typeof console.error; 85 | console.warn = createFilteredMethod(originalConsoleWarn) as typeof console.warn; 86 | console.info = createFilteredMethod(originalConsoleInfo) as typeof console.info; 87 | 88 | logger.debug('Console output filtering enabled'); 89 | } 90 | 91 | /** 92 | * Disable console output filtering 93 | */ 94 | export function disableOutputFilter(): void { 95 | if (!filteringActive) return; 96 | 97 | filteringActive = false; 98 | console.log = originalConsoleLog; 99 | console.error = originalConsoleError; 100 | console.warn = originalConsoleWarn; 101 | console.info = originalConsoleInfo; 102 | 103 | logger.debug('Console output filtering disabled'); 104 | } 105 | 106 | /** 107 | * Execute a function with filtered output 108 | */ 109 | export async function withFilteredOutput<T>(fn: () => Promise<T>): Promise<T> { 110 | enableOutputFilter(); 111 | try { 112 | return await fn(); 113 | } finally { 114 | disableOutputFilter(); 115 | } 116 | } 117 | 118 | /** 119 | * Check if we're in CLI mode (where filtering should be applied) 120 | */ 121 | export function shouldApplyFilter(): boolean { 122 | // Apply filter in CLI mode but not in server mode 123 | return !process.argv.includes('--server') && 124 | !process.env.NCP_MODE?.includes('mcp'); 125 | } ``` -------------------------------------------------------------------------------- /test/mock-mcps/stripe-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Stripe MCP Server 5 | * Real MCP server structure for payment processing testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'stripe-test', 12 | version: '1.0.0', 13 | description: 'Complete payment processing for online businesses including charges, subscriptions, and refunds' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'create_payment', 19 | description: 'Process credit card payments and charges from customers. Charge customer for order, process payment from customer.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | amount: { 24 | type: 'number', 25 | description: 'Payment amount in cents' 26 | }, 27 | currency: { 28 | type: 'string', 29 | description: 'Three-letter currency code (USD, EUR, etc.)' 30 | }, 31 | customer: { 32 | type: 'string', 33 | description: 'Customer identifier or email' 34 | }, 35 | description: { 36 | type: 'string', 37 | description: 'Payment description for records' 38 | } 39 | }, 40 | required: ['amount', 'currency'] 41 | } 42 | }, 43 | { 44 | name: 'refund_payment', 45 | description: 'Process refunds for previously charged payments. Refund cancelled subscription, return customer money.', 46 | inputSchema: { 47 | type: 'object', 48 | properties: { 49 | payment_id: { 50 | type: 'string', 51 | description: 'Original payment identifier to refund' 52 | }, 53 | amount: { 54 | type: 'number', 55 | description: 'Refund amount in cents (optional, defaults to full amount)' 56 | }, 57 | reason: { 58 | type: 'string', 59 | description: 'Reason for refund' 60 | } 61 | }, 62 | required: ['payment_id'] 63 | } 64 | }, 65 | { 66 | name: 'create_subscription', 67 | description: 'Create recurring subscription billing for customers. Set up monthly billing, create subscription plans.', 68 | inputSchema: { 69 | type: 'object', 70 | properties: { 71 | customer: { 72 | type: 'string', 73 | description: 'Customer identifier' 74 | }, 75 | price: { 76 | type: 'string', 77 | description: 'Subscription price identifier or amount' 78 | }, 79 | trial_days: { 80 | type: 'number', 81 | description: 'Optional trial period in days' 82 | }, 83 | interval: { 84 | type: 'string', 85 | description: 'Billing interval (monthly, yearly, etc.)' 86 | } 87 | }, 88 | required: ['customer', 'price'] 89 | } 90 | }, 91 | { 92 | name: 'list_payments', 93 | description: 'List payment transactions with filtering and pagination. See all payment transactions from today, view payment history.', 94 | inputSchema: { 95 | type: 'object', 96 | properties: { 97 | customer: { 98 | type: 'string', 99 | description: 'Optional customer filter' 100 | }, 101 | date_range: { 102 | type: 'object', 103 | description: 'Optional date range filter with start and end dates' 104 | }, 105 | status: { 106 | type: 'string', 107 | description: 'Optional payment status filter (succeeded, failed, pending)' 108 | }, 109 | limit: { 110 | type: 'number', 111 | description: 'Maximum number of results to return' 112 | } 113 | } 114 | } 115 | } 116 | ]; 117 | 118 | // Create and run the server 119 | const server = new MockMCPServer(serverInfo, tools); 120 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Feature Request 2 | description: Suggest a new feature or enhancement for NCP 3 | title: "[Feature]: " 4 | labels: ["enhancement", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting a new feature! Please provide detailed information to help us understand your request. 10 | 11 | - type: checkboxes 12 | id: terms 13 | attributes: 14 | label: Prerequisites 15 | description: Please confirm the following before submitting 16 | options: 17 | - label: I have searched existing issues to ensure this feature hasn't been requested 18 | required: true 19 | - label: I have checked the roadmap to see if this feature is already planned 20 | required: true 21 | 22 | - type: dropdown 23 | id: type 24 | attributes: 25 | label: Feature Type 26 | description: What type of feature is this? 27 | options: 28 | - New MCP Server Support 29 | - CLI Enhancement 30 | - Discovery/Search Improvement 31 | - Performance Optimization 32 | - Developer Experience 33 | - Documentation 34 | - Other 35 | validations: 36 | required: true 37 | 38 | - type: textarea 39 | id: problem 40 | attributes: 41 | label: Problem Statement 42 | description: What problem does this feature solve? 43 | placeholder: "As a [user type], I want [functionality] so that [benefit]" 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | id: solution 49 | attributes: 50 | label: Proposed Solution 51 | description: Describe your ideal solution for this problem 52 | placeholder: What would you like to see implemented? 53 | validations: 54 | required: true 55 | 56 | - type: textarea 57 | id: alternatives 58 | attributes: 59 | label: Alternatives Considered 60 | description: What other approaches have you considered? 61 | placeholder: Are there workarounds or alternative solutions you've tried? 62 | 63 | - type: dropdown 64 | id: priority 65 | attributes: 66 | label: Priority Level 67 | description: How important is this feature to you? 68 | options: 69 | - Critical - Blocking my workflow 70 | - High - Would significantly improve my workflow 71 | - Medium - Nice to have improvement 72 | - Low - Minor convenience 73 | validations: 74 | required: true 75 | 76 | - type: textarea 77 | id: use-cases 78 | attributes: 79 | label: Use Cases 80 | description: Describe specific scenarios where this feature would be useful 81 | placeholder: | 82 | 1. When working with [specific MCP/workflow]... 83 | 2. During [specific development phase]... 84 | 3. For users who need to [specific task]... 85 | 86 | - type: textarea 87 | id: examples 88 | attributes: 89 | label: Examples/Mockups 90 | description: | 91 | Provide examples, mockups, or references to similar implementations 92 | You can attach images or link to examples from other tools 93 | 94 | - type: checkboxes 95 | id: contribution 96 | attributes: 97 | label: Contribution 98 | description: Would you be interested in contributing to this feature? 99 | options: 100 | - label: I would be willing to help implement this feature 101 | - label: I can provide testing/feedback during development 102 | - label: I can help with documentation 103 | 104 | - type: textarea 105 | id: additional 106 | attributes: 107 | label: Additional Context 108 | description: Any other information that would help us understand this request 109 | placeholder: Links to relevant documentation, similar features in other tools, etc. ``` -------------------------------------------------------------------------------- /test/mock-mcps/slack-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Slack MCP Server 5 | * Real MCP server structure for Slack integration testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'slack-test', 12 | version: '1.0.0', 13 | description: 'Slack integration for messaging, channel management, file sharing, and team communication' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'send_message', 19 | description: 'Send messages to Slack channels or direct messages. Share updates, notify teams, communicate with colleagues.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | channel: { 24 | type: 'string', 25 | description: 'Channel name or user ID to send message to' 26 | }, 27 | text: { 28 | type: 'string', 29 | description: 'Message content to send' 30 | }, 31 | thread_ts: { 32 | type: 'string', 33 | description: 'Optional thread timestamp for replies' 34 | } 35 | }, 36 | required: ['channel', 'text'] 37 | } 38 | }, 39 | { 40 | name: 'create_channel', 41 | description: 'Create new Slack channels for team collaboration. Set up project channels, organize team discussions.', 42 | inputSchema: { 43 | type: 'object', 44 | properties: { 45 | name: { 46 | type: 'string', 47 | description: 'Channel name' 48 | }, 49 | purpose: { 50 | type: 'string', 51 | description: 'Channel purpose description' 52 | }, 53 | private: { 54 | type: 'boolean', 55 | description: 'Whether channel should be private' 56 | } 57 | }, 58 | required: ['name'] 59 | } 60 | }, 61 | { 62 | name: 'upload_file', 63 | description: 'Upload files to Slack channels for sharing and collaboration. Share documents, images, code files.', 64 | inputSchema: { 65 | type: 'object', 66 | properties: { 67 | file: { 68 | type: 'string', 69 | description: 'File path or content to upload' 70 | }, 71 | channels: { 72 | type: 'string', 73 | description: 'Comma-separated list of channel names' 74 | }, 75 | title: { 76 | type: 'string', 77 | description: 'File title' 78 | }, 79 | initial_comment: { 80 | type: 'string', 81 | description: 'Initial comment when sharing file' 82 | } 83 | }, 84 | required: ['file', 'channels'] 85 | } 86 | }, 87 | { 88 | name: 'get_channel_history', 89 | description: 'Retrieve message history from Slack channels. Read past conversations, search team discussions.', 90 | inputSchema: { 91 | type: 'object', 92 | properties: { 93 | channel: { 94 | type: 'string', 95 | description: 'Channel ID to get history from' 96 | }, 97 | count: { 98 | type: 'number', 99 | description: 'Number of messages to retrieve' 100 | }, 101 | oldest: { 102 | type: 'string', 103 | description: 'Oldest timestamp for message range' 104 | }, 105 | latest: { 106 | type: 'string', 107 | description: 'Latest timestamp for message range' 108 | } 109 | }, 110 | required: ['channel'] 111 | } 112 | }, 113 | { 114 | name: 'set_channel_topic', 115 | description: 'Set or update channel topic and purpose. Update channel information, set discussion guidelines.', 116 | inputSchema: { 117 | type: 'object', 118 | properties: { 119 | channel: { 120 | type: 'string', 121 | description: 'Channel ID' 122 | }, 123 | topic: { 124 | type: 'string', 125 | description: 'New channel topic' 126 | } 127 | }, 128 | required: ['channel', 'topic'] 129 | } 130 | } 131 | ]; 132 | 133 | // Create and run the server 134 | const server = new MockMCPServer(serverInfo, tools); 135 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "manifest_version": "0.2", 3 | "name": "ncp", 4 | "display_name": "NCP - Natural Context Provider", 5 | "version": "1.5.0", 6 | "description": "N-to-1 MCP Orchestration. Unified gateway for multiple MCP servers with intelligent tool discovery and auto-import.", 7 | "long_description": "NCP transforms N scattered MCP servers into 1 intelligent orchestrator. Your AI sees just 2 simple tools instead of 50+ complex ones, while NCP handles all the routing, discovery, and execution behind the scenes. Features: semantic search, token optimization (97% reduction), automatic tool discovery, multi-client auto-import (Claude Desktop, Perplexity, Cursor, Cline, Continue), dynamic runtime detection, and optional global CLI access.", 8 | "author": { 9 | "name": "Portel", 10 | "url": "https://github.com/portel-dev/ncp" 11 | }, 12 | "user_config": { 13 | "profile": { 14 | "type": "string", 15 | "title": "Profile Name", 16 | "description": "Which NCP profile to use (e.g., 'all', 'development', 'minimal'). Auto-imported MCPs from your MCP client will be added to this profile.", 17 | "default": "all" 18 | }, 19 | "config_path": { 20 | "type": "string", 21 | "title": "Configuration Path", 22 | "description": "Where to store NCP configurations. Use '~/.ncp' for global (shared across projects), '.ncp' for local (per-project), or specify custom path.", 23 | "default": "~/.ncp" 24 | }, 25 | "enable_global_cli": { 26 | "type": "boolean", 27 | "title": "Enable Global CLI Access", 28 | "description": "Create a global 'ncp' command for terminal usage. Allows running NCP from command line anywhere on your system.", 29 | "default": false 30 | }, 31 | "auto_import_client_mcps": { 32 | "type": "boolean", 33 | "title": "Auto-import Client MCPs", 34 | "description": "Automatically import all MCPs from your MCP client (Claude Desktop, Perplexity, Cursor, etc.) on startup. Syncs both config files and extensions.", 35 | "default": true 36 | }, 37 | "enable_debug_logging": { 38 | "type": "boolean", 39 | "title": "Enable Debug Logging", 40 | "description": "Show detailed logs for troubleshooting (runtime detection, MCP loading, etc.)", 41 | "default": false 42 | } 43 | }, 44 | "server": { 45 | "type": "node", 46 | "entry_point": "dist/index-mcp.js", 47 | "mcp_config": { 48 | "command": "node", 49 | "args": [ 50 | "${__dirname}/dist/index-mcp.js", 51 | "--profile=${user_config.profile}", 52 | "--config-path=${user_config.config_path}" 53 | ], 54 | "env": { 55 | "NCP_PROFILE": "${user_config.profile}", 56 | "NCP_CONFIG_PATH": "${user_config.config_path}", 57 | "NCP_ENABLE_GLOBAL_CLI": "${user_config.enable_global_cli}", 58 | "NCP_AUTO_IMPORT": "${user_config.auto_import_client_mcps}", 59 | "NCP_DEBUG": "${user_config.enable_debug_logging}", 60 | "NCP_MODE": "extension" 61 | } 62 | } 63 | }, 64 | "tools": [ 65 | { 66 | "name": "find", 67 | "description": "Dual-mode tool discovery: (1) SEARCH MODE: Use with description parameter for intelligent vector search - describe your task as user story for best results. (2) LISTING MODE: Call without description parameter for paginated browsing of all available MCPs and tools." 68 | }, 69 | { 70 | "name": "run", 71 | "description": "Execute tools from managed MCP servers. Requires exact format 'mcp_name:tool_name' with required parameters. System provides suggestions if tool not found and automatic fallbacks when tools fail." 72 | } 73 | ], 74 | "keywords": ["mcp", "model-context-protocol", "ai-orchestration", "tool-discovery", "multi-mcp", "claude", "ai-gateway", "auto-import"], 75 | "license": "Elastic-2.0" 76 | } 77 | ``` -------------------------------------------------------------------------------- /test/mock-mcps/github-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock GitHub MCP Server 5 | * Real MCP server structure for GitHub API integration testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'github-test', 12 | version: '1.0.0', 13 | description: 'GitHub API integration for repository management, file operations, issues, and pull requests' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'create_repository', 19 | description: 'Create a new GitHub repository with configuration options. Set up new project, initialize repository.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | name: { 24 | type: 'string', 25 | description: 'Repository name' 26 | }, 27 | description: { 28 | type: 'string', 29 | description: 'Repository description' 30 | }, 31 | private: { 32 | type: 'boolean', 33 | description: 'Whether repository should be private' 34 | }, 35 | auto_init: { 36 | type: 'boolean', 37 | description: 'Initialize with README' 38 | } 39 | }, 40 | required: ['name'] 41 | } 42 | }, 43 | { 44 | name: 'create_issue', 45 | description: 'Create GitHub issues for bug reports and feature requests. Report bugs, request features, track tasks.', 46 | inputSchema: { 47 | type: 'object', 48 | properties: { 49 | title: { 50 | type: 'string', 51 | description: 'Issue title' 52 | }, 53 | body: { 54 | type: 'string', 55 | description: 'Issue description' 56 | }, 57 | labels: { 58 | type: 'array', 59 | description: 'Issue labels', 60 | items: { type: 'string' } 61 | }, 62 | assignees: { 63 | type: 'array', 64 | description: 'User assignments', 65 | items: { type: 'string' } 66 | } 67 | }, 68 | required: ['title'] 69 | } 70 | }, 71 | { 72 | name: 'create_pull_request', 73 | description: 'Create pull requests for code review and merging changes. Submit code changes, request reviews.', 74 | inputSchema: { 75 | type: 'object', 76 | properties: { 77 | title: { 78 | type: 'string', 79 | description: 'Pull request title' 80 | }, 81 | body: { 82 | type: 'string', 83 | description: 'Pull request description' 84 | }, 85 | head: { 86 | type: 'string', 87 | description: 'Source branch' 88 | }, 89 | base: { 90 | type: 'string', 91 | description: 'Target branch' 92 | } 93 | }, 94 | required: ['title', 'head', 'base'] 95 | } 96 | }, 97 | { 98 | name: 'get_file_contents', 99 | description: 'Read file contents from GitHub repositories. Access source code, read configuration files.', 100 | inputSchema: { 101 | type: 'object', 102 | properties: { 103 | path: { 104 | type: 'string', 105 | description: 'File path in repository' 106 | }, 107 | ref: { 108 | type: 'string', 109 | description: 'Branch or commit reference' 110 | } 111 | }, 112 | required: ['path'] 113 | } 114 | }, 115 | { 116 | name: 'search_repositories', 117 | description: 'Search GitHub repositories by keywords, topics, and filters. Find open source projects, discover libraries.', 118 | inputSchema: { 119 | type: 'object', 120 | properties: { 121 | query: { 122 | type: 'string', 123 | description: 'Search query with keywords' 124 | }, 125 | sort: { 126 | type: 'string', 127 | description: 'Sort criteria (stars, forks, updated)' 128 | }, 129 | order: { 130 | type: 'string', 131 | description: 'Sort order (asc, desc)' 132 | } 133 | }, 134 | required: ['query'] 135 | } 136 | } 137 | ]; 138 | 139 | // Create and run the server 140 | const server = new MockMCPServer(serverInfo, tools); 141 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /test/mock-mcps/notion-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Notion MCP Server 5 | * Real MCP server structure for Notion workspace integration testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'notion-test', 12 | version: '1.0.0', 13 | description: 'Notion workspace management for documents, databases, and collaborative content creation' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'create_page', 19 | description: 'Create new Notion pages and documents with content. Write notes, create documentation, start new projects.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | parent: { 24 | type: 'string', 25 | description: 'Parent page or database ID' 26 | }, 27 | title: { 28 | type: 'string', 29 | description: 'Page title' 30 | }, 31 | content: { 32 | type: 'array', 33 | description: 'Page content blocks' 34 | }, 35 | properties: { 36 | type: 'object', 37 | description: 'Page properties if parent is database' 38 | } 39 | }, 40 | required: ['parent', 'title'] 41 | } 42 | }, 43 | { 44 | name: 'create_database', 45 | description: 'Create structured Notion databases with properties and schema. Set up project tracking, create data tables.', 46 | inputSchema: { 47 | type: 'object', 48 | properties: { 49 | parent: { 50 | type: 'string', 51 | description: 'Parent page ID' 52 | }, 53 | title: { 54 | type: 'string', 55 | description: 'Database title' 56 | }, 57 | properties: { 58 | type: 'object', 59 | description: 'Database schema properties' 60 | }, 61 | description: { 62 | type: 'string', 63 | description: 'Database description' 64 | } 65 | }, 66 | required: ['parent', 'title', 'properties'] 67 | } 68 | }, 69 | { 70 | name: 'query_database', 71 | description: 'Query Notion databases with filtering and sorting. Search data, find records, analyze information.', 72 | inputSchema: { 73 | type: 'object', 74 | properties: { 75 | database_id: { 76 | type: 'string', 77 | description: 'Database ID to query' 78 | }, 79 | filter: { 80 | type: 'object', 81 | description: 'Query filter conditions' 82 | }, 83 | sorts: { 84 | type: 'array', 85 | description: 'Sort criteria' 86 | }, 87 | start_cursor: { 88 | type: 'string', 89 | description: 'Pagination cursor' 90 | } 91 | }, 92 | required: ['database_id'] 93 | } 94 | }, 95 | { 96 | name: 'update_page', 97 | description: 'Update existing Notion pages with new content and properties. Edit documents, modify data, update information.', 98 | inputSchema: { 99 | type: 'object', 100 | properties: { 101 | page_id: { 102 | type: 'string', 103 | description: 'Page ID to update' 104 | }, 105 | properties: { 106 | type: 'object', 107 | description: 'Properties to update' 108 | }, 109 | content: { 110 | type: 'array', 111 | description: 'New content blocks to append' 112 | } 113 | }, 114 | required: ['page_id'] 115 | } 116 | }, 117 | { 118 | name: 'search_pages', 119 | description: 'Search across Notion workspace for pages and content. Find documents, locate information, discover content.', 120 | inputSchema: { 121 | type: 'object', 122 | properties: { 123 | query: { 124 | type: 'string', 125 | description: 'Search query text' 126 | }, 127 | filter: { 128 | type: 'object', 129 | description: 'Search filter criteria' 130 | }, 131 | sort: { 132 | type: 'object', 133 | description: 'Sort results by criteria' 134 | } 135 | } 136 | } 137 | } 138 | ]; 139 | 140 | // Create and run the server 141 | const server = new MockMCPServer(serverInfo, tools); 142 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/pr-schema-additions.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Configuration Schema Types 3 | * 4 | * These types should be added to schema/draft/schema.ts 5 | */ 6 | 7 | /** 8 | * Describes a configuration parameter needed by the server. 9 | */ 10 | export interface ConfigurationParameter { 11 | /** 12 | * Unique identifier for this parameter (e.g., "GITHUB_TOKEN", "allowed-directory") 13 | */ 14 | name: string; 15 | 16 | /** 17 | * Human-readable description of what this parameter is for 18 | */ 19 | description: string; 20 | 21 | /** 22 | * Type of the parameter value 23 | */ 24 | type: "string" | "number" | "boolean" | "path" | "url"; 25 | 26 | /** 27 | * Whether this parameter is required for the server to function 28 | */ 29 | required: boolean; 30 | 31 | /** 32 | * Whether this contains sensitive data (passwords, API keys) 33 | * If true, clients should mask input when prompting users 34 | */ 35 | sensitive?: boolean; 36 | 37 | /** 38 | * Default value if not provided by the user 39 | */ 40 | default?: string | number | boolean; 41 | 42 | /** 43 | * Whether multiple values are allowed (for array parameters) 44 | */ 45 | multiple?: boolean; 46 | 47 | /** 48 | * Validation pattern (regex) for string parameters 49 | */ 50 | pattern?: string; 51 | 52 | /** 53 | * Example values to help users understand expected format 54 | */ 55 | examples?: string[]; 56 | } 57 | 58 | /** 59 | * Declares configuration requirements for the server. 60 | * 61 | * Servers can use this to communicate what environment variables, 62 | * command-line arguments, or other configuration they need to function properly. 63 | * 64 | * This enables clients to: 65 | * - Detect missing configuration before attempting connection 66 | * - Prompt users interactively for required values 67 | * - Validate configuration before startup 68 | * - Provide helpful error messages 69 | */ 70 | export interface ConfigurationSchema { 71 | /** 72 | * Environment variables required by the server 73 | */ 74 | environmentVariables?: ConfigurationParameter[]; 75 | 76 | /** 77 | * Command-line arguments required by the server 78 | */ 79 | arguments?: ConfigurationParameter[]; 80 | 81 | /** 82 | * Other configuration requirements (files, URLs, etc.) 83 | */ 84 | other?: ConfigurationParameter[]; 85 | } 86 | 87 | /** 88 | * MODIFICATION TO EXISTING InitializeResult INTERFACE 89 | * 90 | * Add this field to the existing InitializeResult interface: 91 | */ 92 | export interface InitializeResult extends Result { 93 | protocolVersion: string; 94 | capabilities: ServerCapabilities; 95 | serverInfo: Implementation; 96 | instructions?: string; 97 | 98 | /** 99 | * Optional schema declaring the server's configuration requirements. 100 | * 101 | * Servers can use this to communicate what environment variables, 102 | * command-line arguments, or other configuration they need. 103 | * 104 | * Clients can use this information to: 105 | * - Validate configuration before attempting connection 106 | * - Prompt users for missing required configuration 107 | * - Provide better error messages and setup guidance 108 | * 109 | * This field is optional and backward compatible - servers that don't 110 | * provide it continue to work as before. 111 | * 112 | * @example 113 | * ```typescript 114 | * // Filesystem server declaring path requirement 115 | * { 116 | * "configurationSchema": { 117 | * "arguments": [{ 118 | * "name": "allowed-directory", 119 | * "description": "Directory path that the server is allowed to access", 120 | * "type": "path", 121 | * "required": true, 122 | * "multiple": true 123 | * }] 124 | * } 125 | * } 126 | * 127 | * // API server declaring token requirement 128 | * { 129 | * "configurationSchema": { 130 | * "environmentVariables": [{ 131 | * "name": "GITHUB_TOKEN", 132 | * "description": "GitHub personal access token with repo permissions", 133 | * "type": "string", 134 | * "required": true, 135 | * "sensitive": true, 136 | * "pattern": "^ghp_[a-zA-Z0-9]{36}$" 137 | * }] 138 | * } 139 | * } 140 | * ``` 141 | */ 142 | configurationSchema?: ConfigurationSchema; 143 | } 144 | ``` -------------------------------------------------------------------------------- /test/mock-mcps/neo4j-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Neo4j MCP Server 5 | * Real MCP server structure for Neo4j graph database testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'neo4j-test', 12 | version: '1.0.0', 13 | description: 'Neo4j graph database server with schema management and read/write cypher operations' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'execute_cypher', 19 | description: 'Execute Cypher queries on Neo4j graph database. Query relationships, find patterns, analyze connections.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | query: { 24 | type: 'string', 25 | description: 'Cypher query string' 26 | }, 27 | parameters: { 28 | type: 'object', 29 | description: 'Query parameters as key-value pairs' 30 | }, 31 | database: { 32 | type: 'string', 33 | description: 'Target database name' 34 | } 35 | }, 36 | required: ['query'] 37 | } 38 | }, 39 | { 40 | name: 'create_node', 41 | description: 'Create nodes in Neo4j graph with labels and properties. Add entities, create graph elements.', 42 | inputSchema: { 43 | type: 'object', 44 | properties: { 45 | labels: { 46 | type: 'array', 47 | description: 'Node labels', 48 | items: { type: 'string' } 49 | }, 50 | properties: { 51 | type: 'object', 52 | description: 'Node properties as key-value pairs' 53 | } 54 | }, 55 | required: ['labels'] 56 | } 57 | }, 58 | { 59 | name: 'create_relationship', 60 | description: 'Create relationships between nodes in Neo4j graph. Connect entities, define associations, build graph structure.', 61 | inputSchema: { 62 | type: 'object', 63 | properties: { 64 | from_node_id: { 65 | type: 'string', 66 | description: 'Source node ID' 67 | }, 68 | to_node_id: { 69 | type: 'string', 70 | description: 'Target node ID' 71 | }, 72 | relationship_type: { 73 | type: 'string', 74 | description: 'Relationship type/label' 75 | }, 76 | properties: { 77 | type: 'object', 78 | description: 'Relationship properties' 79 | } 80 | }, 81 | required: ['from_node_id', 'to_node_id', 'relationship_type'] 82 | } 83 | }, 84 | { 85 | name: 'find_path', 86 | description: 'Find paths between nodes in Neo4j graph database. Discover connections, analyze relationships, trace routes.', 87 | inputSchema: { 88 | type: 'object', 89 | properties: { 90 | start_node: { 91 | type: 'object', 92 | description: 'Starting node criteria' 93 | }, 94 | end_node: { 95 | type: 'object', 96 | description: 'Ending node criteria' 97 | }, 98 | relationship_types: { 99 | type: 'array', 100 | description: 'Allowed relationship types', 101 | items: { type: 'string' } 102 | }, 103 | max_depth: { 104 | type: 'number', 105 | description: 'Maximum path depth' 106 | } 107 | }, 108 | required: ['start_node', 'end_node'] 109 | } 110 | }, 111 | { 112 | name: 'manage_schema', 113 | description: 'Manage Neo4j database schema including indexes and constraints. Optimize queries, ensure data integrity.', 114 | inputSchema: { 115 | type: 'object', 116 | properties: { 117 | action: { 118 | type: 'string', 119 | description: 'Schema action (create_index, drop_index, create_constraint, drop_constraint)' 120 | }, 121 | label: { 122 | type: 'string', 123 | description: 'Node label or relationship type' 124 | }, 125 | properties: { 126 | type: 'array', 127 | description: 'Properties for index/constraint', 128 | items: { type: 'string' } 129 | }, 130 | constraint_type: { 131 | type: 'string', 132 | description: 'Constraint type (unique, exists, key)' 133 | } 134 | }, 135 | required: ['action', 'label', 'properties'] 136 | } 137 | } 138 | ]; 139 | 140 | // Create and run the server 141 | const server = new MockMCPServer(serverInfo, tools); 142 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/testing/setup-dummy-mcps.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | /** 3 | * Setup Dummy MCPs for Testing 4 | * 5 | * Creates NCP profile configurations that use dummy MCP servers for testing 6 | * the semantic enhancement system without requiring real MCP connections. 7 | */ 8 | 9 | import * as fs from 'fs/promises'; 10 | import * as path from 'path'; 11 | import { fileURLToPath } from 'url'; 12 | import { getNcpBaseDirectory } from '../utils/ncp-paths.js'; 13 | 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | 17 | interface McpDefinitionsFile { 18 | mcps: Record<string, any>; 19 | } 20 | 21 | async function setupDummyMcps(): Promise<void> { 22 | try { 23 | // Load MCP definitions 24 | const definitionsPath = path.join(__dirname, 'mcp-definitions.json'); 25 | const definitionsContent = await fs.readFile(definitionsPath, 'utf-8'); 26 | const definitions: McpDefinitionsFile = JSON.parse(definitionsContent); 27 | 28 | // Get NCP base directory and ensure profiles directory exists 29 | const ncpBaseDir = await getNcpBaseDirectory(); 30 | const profilesDir = path.join(ncpBaseDir, 'profiles'); 31 | await fs.mkdir(profilesDir, { recursive: true }); 32 | 33 | // Create test profile configuration 34 | const profileConfig = { 35 | name: "semantic-test", 36 | description: "Testing profile with dummy MCPs for semantic enhancement validation", 37 | mcpServers: {} as Record<string, any>, 38 | metadata: { 39 | created: new Date().toISOString(), 40 | modified: new Date().toISOString() 41 | } 42 | }; 43 | 44 | // Build dummy MCP server path 45 | const dummyServerPath = path.join(__dirname, 'dummy-mcp-server.ts'); 46 | const nodeExecutable = process.execPath; 47 | const tsNodePath = path.join(path.dirname(nodeExecutable), 'npx'); 48 | 49 | // Add each MCP from definitions as a dummy MCP 50 | for (const [mcpName, mcpDef] of Object.entries(definitions.mcps)) { 51 | profileConfig.mcpServers[mcpName] = { 52 | command: 'npx', 53 | args: [ 54 | 'tsx', // Use tsx to run TypeScript directly 55 | dummyServerPath, 56 | '--mcp-name', 57 | mcpName, 58 | '--definitions-file', 59 | definitionsPath 60 | ] 61 | }; 62 | } 63 | 64 | // Write profile configuration 65 | const profilePath = path.join(profilesDir, 'semantic-test.json'); 66 | await fs.writeFile(profilePath, JSON.stringify(profileConfig, null, 2)); 67 | 68 | console.log(`✅ Created semantic-test profile with ${Object.keys(definitions.mcps).length} dummy MCPs:`); 69 | Object.keys(definitions.mcps).forEach(name => { 70 | console.log(` - ${name}: ${definitions.mcps[name].description}`); 71 | }); 72 | console.log(`\nProfile saved to: ${profilePath}`); 73 | console.log(`\nTo use this profile:`); 74 | console.log(` npx ncp --profile semantic-test list`); 75 | console.log(` npx ncp --profile semantic-test find "commit my code to git"`); 76 | console.log(` npx ncp --profile semantic-test run git:commit --params '{"message":"test commit"}'`); 77 | 78 | // Create a simplified test profile with just key MCPs for faster testing 79 | const quickTestConfig = { 80 | name: "semantic-quick", 81 | description: "Quick test profile with essential MCPs for semantic enhancement", 82 | mcpServers: { 83 | shell: profileConfig.mcpServers.shell, 84 | git: profileConfig.mcpServers.git, 85 | postgres: profileConfig.mcpServers.postgres, 86 | openai: profileConfig.mcpServers.openai 87 | }, 88 | metadata: { 89 | created: new Date().toISOString(), 90 | modified: new Date().toISOString() 91 | } 92 | }; 93 | 94 | const quickProfilePath = path.join(profilesDir, 'semantic-quick.json'); 95 | await fs.writeFile(quickProfilePath, JSON.stringify(quickTestConfig, null, 2)); 96 | 97 | console.log(`\n✅ Created semantic-quick profile with 4 essential MCPs`); 98 | console.log(`Profile saved to: ${quickProfilePath}`); 99 | 100 | } catch (error) { 101 | console.error('Failed to setup dummy MCPs:', error); 102 | process.exit(1); 103 | } 104 | } 105 | 106 | // Main execution 107 | if (import.meta.url === `file://${process.argv[1]}`) { 108 | setupDummyMcps(); 109 | } ``` -------------------------------------------------------------------------------- /src/services/config-schema-reader.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Configuration Schema Reader 3 | * 4 | * Reads configurationSchema from MCP InitializeResult 5 | * Caches schemas for future use during `ncp add` and `ncp repair` 6 | */ 7 | 8 | export interface ConfigurationParameter { 9 | name: string; 10 | description: string; 11 | type: 'string' | 'number' | 'boolean' | 'path' | 'url'; 12 | required: boolean; 13 | sensitive?: boolean; 14 | default?: string | number | boolean; 15 | multiple?: boolean; 16 | pattern?: string; 17 | examples?: string[]; 18 | } 19 | 20 | export interface ConfigurationSchema { 21 | environmentVariables?: ConfigurationParameter[]; 22 | arguments?: ConfigurationParameter[]; 23 | other?: ConfigurationParameter[]; 24 | } 25 | 26 | export interface InitializeResult { 27 | protocolVersion: string; 28 | capabilities: any; 29 | serverInfo: { 30 | name: string; 31 | version: string; 32 | }; 33 | instructions?: string; 34 | configurationSchema?: ConfigurationSchema; 35 | } 36 | 37 | export class ConfigSchemaReader { 38 | /** 39 | * Extract configuration schema from InitializeResult 40 | */ 41 | readSchema(initResult: InitializeResult): ConfigurationSchema | null { 42 | if (!initResult || !initResult.configurationSchema) { 43 | return null; 44 | } 45 | 46 | return initResult.configurationSchema; 47 | } 48 | 49 | /** 50 | * Get all required parameters from schema 51 | */ 52 | getRequiredParameters(schema: ConfigurationSchema): ConfigurationParameter[] { 53 | const required: ConfigurationParameter[] = []; 54 | 55 | if (schema.environmentVariables) { 56 | required.push(...schema.environmentVariables.filter(p => p.required)); 57 | } 58 | 59 | if (schema.arguments) { 60 | required.push(...schema.arguments.filter(p => p.required)); 61 | } 62 | 63 | if (schema.other) { 64 | required.push(...schema.other.filter(p => p.required)); 65 | } 66 | 67 | return required; 68 | } 69 | 70 | /** 71 | * Check if schema has any required parameters 72 | */ 73 | hasRequiredConfig(schema: ConfigurationSchema | null): boolean { 74 | if (!schema) return false; 75 | 76 | return this.getRequiredParameters(schema).length > 0; 77 | } 78 | 79 | /** 80 | * Get parameter by name from schema 81 | */ 82 | getParameter(schema: ConfigurationSchema, name: string): ConfigurationParameter | null { 83 | const allParams = [ 84 | ...(schema.environmentVariables || []), 85 | ...(schema.arguments || []), 86 | ...(schema.other || []) 87 | ]; 88 | 89 | return allParams.find(p => p.name === name) || null; 90 | } 91 | 92 | /** 93 | * Format schema for display 94 | */ 95 | formatSchema(schema: ConfigurationSchema): string { 96 | const lines: string[] = []; 97 | 98 | if (schema.environmentVariables && schema.environmentVariables.length > 0) { 99 | lines.push('Environment Variables:'); 100 | schema.environmentVariables.forEach(param => { 101 | const required = param.required ? '(required)' : '(optional)'; 102 | const sensitive = param.sensitive ? ' [sensitive]' : ''; 103 | lines.push(` - ${param.name} ${required}${sensitive}`); 104 | lines.push(` ${param.description}`); 105 | if (param.examples && param.examples.length > 0 && !param.sensitive) { 106 | lines.push(` Examples: ${param.examples.join(', ')}`); 107 | } 108 | }); 109 | lines.push(''); 110 | } 111 | 112 | if (schema.arguments && schema.arguments.length > 0) { 113 | lines.push('Command Arguments:'); 114 | schema.arguments.forEach(param => { 115 | const required = param.required ? '(required)' : '(optional)'; 116 | const multiple = param.multiple ? ' [multiple]' : ''; 117 | lines.push(` - ${param.name} ${required}${multiple}`); 118 | lines.push(` ${param.description}`); 119 | if (param.examples && param.examples.length > 0) { 120 | lines.push(` Examples: ${param.examples.join(', ')}`); 121 | } 122 | }); 123 | lines.push(''); 124 | } 125 | 126 | if (schema.other && schema.other.length > 0) { 127 | lines.push('Other Configuration:'); 128 | schema.other.forEach(param => { 129 | const required = param.required ? '(required)' : '(optional)'; 130 | lines.push(` - ${param.name} ${required}`); 131 | lines.push(` ${param.description}`); 132 | }); 133 | } 134 | 135 | return lines.join('\n'); 136 | } 137 | } 138 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mcp_server_request.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: MCP Server Support Request 2 | description: Request support for a new MCP server in NCP 3 | title: "[MCP]: Add support for " 4 | labels: ["mcp-server", "enhancement", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Request support for a new MCP server in NCP's discovery and management system. 10 | 11 | - type: input 12 | id: server-name 13 | attributes: 14 | label: MCP Server Name 15 | description: What is the name of the MCP server? 16 | placeholder: "e.g., @modelcontextprotocol/server-slack" 17 | validations: 18 | required: true 19 | 20 | - type: input 21 | id: repository 22 | attributes: 23 | label: Repository URL 24 | description: Link to the MCP server's repository 25 | placeholder: "https://github.com/..." 26 | validations: 27 | required: true 28 | 29 | - type: input 30 | id: npm-package 31 | attributes: 32 | label: NPM Package (if available) 33 | description: NPM package name if the server is published 34 | placeholder: "e.g., @modelcontextprotocol/server-slack" 35 | 36 | - type: textarea 37 | id: description 38 | attributes: 39 | label: Server Description 40 | description: What does this MCP server do? 41 | placeholder: Brief description of the server's functionality 42 | validations: 43 | required: true 44 | 45 | - type: dropdown 46 | id: category 47 | attributes: 48 | label: Server Category 49 | description: What category best describes this server? 50 | options: 51 | - Database (SQL, NoSQL, etc.) 52 | - Cloud Services (AWS, Azure, GCP) 53 | - Development Tools (Git, Docker, etc.) 54 | - Communication (Slack, Discord, etc.) 55 | - File System 56 | - Web/API Services 57 | - Productivity Tools 58 | - System Administration 59 | - Other 60 | validations: 61 | required: true 62 | 63 | - type: checkboxes 64 | id: server-status 65 | attributes: 66 | label: Server Status 67 | description: Please verify the server status 68 | options: 69 | - label: The server is actively maintained 70 | required: true 71 | - label: The server has clear documentation 72 | required: true 73 | - label: The server follows MCP protocol specifications 74 | required: true 75 | 76 | - type: textarea 77 | id: tools 78 | attributes: 79 | label: Available Tools 80 | description: List the main tools/functions this server provides 81 | placeholder: | 82 | - send_message: Send messages to channels 83 | - create_channel: Create new channels 84 | - list_channels: Get available channels 85 | 86 | - type: textarea 87 | id: use-cases 88 | attributes: 89 | label: Use Cases 90 | description: When would users want to use this MCP server? 91 | placeholder: | 92 | - Automating Slack notifications 93 | - Managing team communication 94 | - Integrating with CI/CD pipelines 95 | 96 | - type: input 97 | id: priority-score 98 | attributes: 99 | label: Priority Justification 100 | description: Why should this server be prioritized? 101 | placeholder: "Popular tool with X GitHub stars, requested by Y users, fills gap in Z domain" 102 | 103 | - type: textarea 104 | id: configuration 105 | attributes: 106 | label: Configuration Requirements 107 | description: What configuration is needed to use this server? 108 | placeholder: | 109 | Required: 110 | - API_TOKEN: Slack bot token 111 | - WORKSPACE_ID: Slack workspace identifier 112 | 113 | Optional: 114 | - DEFAULT_CHANNEL: Default channel for messages 115 | 116 | - type: checkboxes 117 | id: contribution 118 | attributes: 119 | label: Contribution 120 | description: Can you help with implementation? 121 | options: 122 | - label: I can help test the integration 123 | - label: I can provide configuration examples 124 | - label: I can help with documentation 125 | - label: I maintain or contribute to this MCP server 126 | 127 | - type: textarea 128 | id: additional 129 | attributes: 130 | label: Additional Information 131 | description: Any other relevant information 132 | placeholder: Links to documentation, similar integrations, special considerations ``` -------------------------------------------------------------------------------- /test/mock-mcps/playwright-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Playwright MCP Server 5 | * Real MCP server structure for browser automation testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'playwright-test', 12 | version: '1.0.0', 13 | description: 'Browser automation and web scraping with cross-browser support' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'navigate_to_page', 19 | description: 'Navigate to web pages and URLs for automation tasks. Open websites, load web applications.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | url: { 24 | type: 'string', 25 | description: 'URL to navigate to' 26 | }, 27 | wait_until: { 28 | type: 'string', 29 | description: 'Wait condition (load, domcontentloaded, networkidle)' 30 | }, 31 | timeout: { 32 | type: 'number', 33 | description: 'Navigation timeout in milliseconds' 34 | } 35 | }, 36 | required: ['url'] 37 | } 38 | }, 39 | { 40 | name: 'click_element', 41 | description: 'Click on web page elements using selectors. Click buttons, links, form elements.', 42 | inputSchema: { 43 | type: 'object', 44 | properties: { 45 | selector: { 46 | type: 'string', 47 | description: 'CSS selector or XPath for element' 48 | }, 49 | button: { 50 | type: 'string', 51 | description: 'Mouse button to click (left, right, middle)' 52 | }, 53 | click_count: { 54 | type: 'number', 55 | description: 'Number of clicks' 56 | } 57 | }, 58 | required: ['selector'] 59 | } 60 | }, 61 | { 62 | name: 'fill_form_field', 63 | description: 'Fill form inputs and text fields on web pages. Enter text, complete forms, input data.', 64 | inputSchema: { 65 | type: 'object', 66 | properties: { 67 | selector: { 68 | type: 'string', 69 | description: 'CSS selector for input field' 70 | }, 71 | value: { 72 | type: 'string', 73 | description: 'Text value to fill' 74 | }, 75 | clear: { 76 | type: 'boolean', 77 | description: 'Clear field before filling' 78 | } 79 | }, 80 | required: ['selector', 'value'] 81 | } 82 | }, 83 | { 84 | name: 'take_screenshot', 85 | description: 'Capture screenshots of web pages for testing and documentation. Take page screenshots, save visual evidence.', 86 | inputSchema: { 87 | type: 'object', 88 | properties: { 89 | path: { 90 | type: 'string', 91 | description: 'File path to save screenshot' 92 | }, 93 | full_page: { 94 | type: 'boolean', 95 | description: 'Capture full page or just viewport' 96 | }, 97 | quality: { 98 | type: 'number', 99 | description: 'JPEG quality (0-100)' 100 | } 101 | }, 102 | required: ['path'] 103 | } 104 | }, 105 | { 106 | name: 'extract_text', 107 | description: 'Extract text content from web page elements. Scrape data, read page content, get element text.', 108 | inputSchema: { 109 | type: 'object', 110 | properties: { 111 | selector: { 112 | type: 'string', 113 | description: 'CSS selector for element' 114 | }, 115 | attribute: { 116 | type: 'string', 117 | description: 'Optional attribute to extract instead of text' 118 | } 119 | }, 120 | required: ['selector'] 121 | } 122 | }, 123 | { 124 | name: 'wait_for_element', 125 | description: 'Wait for elements to appear or become available on web pages. Wait for dynamic content, ensure element visibility.', 126 | inputSchema: { 127 | type: 'object', 128 | properties: { 129 | selector: { 130 | type: 'string', 131 | description: 'CSS selector for element to wait for' 132 | }, 133 | state: { 134 | type: 'string', 135 | description: 'Element state to wait for (visible, hidden, attached)' 136 | }, 137 | timeout: { 138 | type: 'number', 139 | description: 'Wait timeout in milliseconds' 140 | } 141 | }, 142 | required: ['selector'] 143 | } 144 | } 145 | ]; 146 | 147 | // Create and run the server 148 | const server = new MockMCPServer(serverInfo, tools); 149 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/utils/security.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Security utilities for NCP 3 | * Handles sensitive data masking and sanitization 4 | */ 5 | 6 | /** 7 | * Masks sensitive information in command strings 8 | * Detects and masks API keys, tokens, passwords, etc. 9 | */ 10 | export function maskSensitiveData(text: string): string { 11 | if (!text) return text; 12 | 13 | let masked = text; 14 | 15 | // Mask API keys (various patterns) 16 | masked = masked.replace( 17 | /sk_test_[a-zA-Z0-9]{50,}/g, 18 | (match) => `sk_test_*****${match.slice(-4)}` 19 | ); 20 | 21 | masked = masked.replace( 22 | /sk_live_[a-zA-Z0-9]{50,}/g, 23 | (match) => `sk_live_*****${match.slice(-4)}` 24 | ); 25 | 26 | // Mask other common API key patterns 27 | masked = masked.replace( 28 | /--api-key[=\s]+([a-zA-Z0-9_-]{16,})/gi, 29 | (match, key) => match.replace(key, `*****${key.slice(-4)}`) 30 | ); 31 | 32 | // Mask --key parameters 33 | masked = masked.replace( 34 | /--key[=\s]+([a-zA-Z0-9_-]{16,})/gi, 35 | (match, key) => match.replace(key, `*****${key.slice(-4)}`) 36 | ); 37 | 38 | // Mask tokens 39 | masked = masked.replace( 40 | /--token[=\s]+([a-zA-Z0-9_-]{16,})/gi, 41 | (match, token) => match.replace(token, `*****${token.slice(-4)}`) 42 | ); 43 | 44 | // Mask passwords 45 | masked = masked.replace( 46 | /--password[=\s]+([^\s]+)/gi, 47 | (match, password) => match.replace(password, '*****') 48 | ); 49 | 50 | // Mask JWT tokens 51 | masked = masked.replace( 52 | /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, 53 | (match) => `eyJ*****${match.slice(-4)}` 54 | ); 55 | 56 | // Mask UUID-like keys 57 | masked = masked.replace( 58 | /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, 59 | (match) => `*****${match.slice(-4)}` 60 | ); 61 | 62 | return masked; 63 | } 64 | 65 | /** 66 | * Formats command display with proper masking 67 | * @param showAsTemplates - If true, shows template variables like {{API_KEY}} instead of masked values 68 | */ 69 | export function formatCommandDisplay(command: string, args: string[] = [], showAsTemplates: boolean = true): string { 70 | const fullCommand = `${command} ${args.join(' ')}`.trim(); 71 | 72 | if (showAsTemplates) { 73 | return maskSensitiveDataAsTemplates(fullCommand); 74 | } 75 | return maskSensitiveData(fullCommand); 76 | } 77 | 78 | /** 79 | * Masks sensitive data by replacing with template variable names 80 | * This provides cleaner display without exposing any part of secrets 81 | */ 82 | export function maskSensitiveDataAsTemplates(text: string): string { 83 | if (!text) return text; 84 | 85 | let masked = text; 86 | 87 | // Replace API key parameters 88 | masked = masked.replace( 89 | /--api-key[=\s]+([^\s]+)/gi, 90 | '--api-key={{API_KEY}}' 91 | ); 92 | 93 | // Replace key parameters (like Upstash keys) 94 | masked = masked.replace( 95 | /--key[=\s]+([^\s]+)/gi, 96 | '--key={{API_KEY}}' 97 | ); 98 | 99 | // Replace token parameters 100 | masked = masked.replace( 101 | /--token[=\s]+([^\s]+)/gi, 102 | '--token={{TOKEN}}' 103 | ); 104 | 105 | // Replace OAuth tokens 106 | masked = masked.replace( 107 | /--oauth-token[=\s]+([^\s]+)/gi, 108 | '--oauth-token={{OAUTH_TOKEN}}' 109 | ); 110 | 111 | // Replace password parameters 112 | masked = masked.replace( 113 | /--password[=\s]+([^\s]+)/gi, 114 | '--password={{PASSWORD}}' 115 | ); 116 | 117 | // Replace secret parameters 118 | masked = masked.replace( 119 | /--secret[=\s]+([^\s]+)/gi, 120 | '--secret={{SECRET}}' 121 | ); 122 | 123 | // Replace auth parameters 124 | masked = masked.replace( 125 | /--auth[=\s]+([^\s]+)/gi, 126 | '--auth={{AUTH}}' 127 | ); 128 | 129 | // Replace environment variable references that look like they contain secrets 130 | masked = masked.replace( 131 | /\$\{?([A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|API|AUTH)[A-Z_]*)\}?/g, 132 | '{{$1}}' 133 | ); 134 | 135 | return masked; 136 | } 137 | 138 | /** 139 | * Checks if a string contains sensitive data patterns 140 | */ 141 | export function containsSensitiveData(text: string): boolean { 142 | if (!text) return false; 143 | 144 | const sensitivePatterns = [ 145 | /sk_test_[a-zA-Z0-9]{99}/, 146 | /sk_live_[a-zA-Z0-9]{99}/, 147 | /--api-key[=\s]+[a-zA-Z0-9_-]{16,}/i, 148 | /--token[=\s]+[a-zA-Z0-9_-]{16,}/i, 149 | /--password[=\s]+[^\s]+/i, 150 | /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/, 151 | /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i 152 | ]; 153 | 154 | return sensitivePatterns.some(pattern => pattern.test(text)); 155 | } ``` -------------------------------------------------------------------------------- /src/utils/runtime-detector.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Runtime Detector 3 | * 4 | * Detects which runtime (bundled vs system) NCP is currently running with. 5 | * This is detected fresh on every boot to respect Claude Desktop's dynamic settings. 6 | */ 7 | 8 | import { existsSync } from 'fs'; 9 | import { getBundledRuntimePath } from './client-registry.js'; 10 | 11 | export interface RuntimeInfo { 12 | /** The runtime being used ('bundled' or 'system') */ 13 | type: 'bundled' | 'system'; 14 | 15 | /** Path to Node.js runtime to use */ 16 | nodePath: string; 17 | 18 | /** Path to Python runtime to use (if available) */ 19 | pythonPath?: string; 20 | } 21 | 22 | /** 23 | * Detect which runtime NCP is currently running with. 24 | * 25 | * Strategy: 26 | * 1. Check process.execPath (how NCP was launched) 27 | * 2. Compare with known bundled runtime paths 28 | * 3. If match → we're running via bundled runtime 29 | * 4. If no match → we're running via system runtime 30 | */ 31 | export function detectRuntime(): RuntimeInfo { 32 | const currentNodePath = process.execPath; 33 | 34 | // Check if we're running via Claude Desktop's bundled Node 35 | const claudeBundledNode = getBundledRuntimePath('claude-desktop', 'node'); 36 | const claudeBundledPython = getBundledRuntimePath('claude-desktop', 'python'); 37 | 38 | // If our execPath matches the bundled Node path, we're running via bundled runtime 39 | if (claudeBundledNode && currentNodePath === claudeBundledNode) { 40 | return { 41 | type: 'bundled', 42 | nodePath: claudeBundledNode, 43 | pythonPath: claudeBundledPython || undefined 44 | }; 45 | } 46 | 47 | // Check if execPath is inside Claude.app (might be different bundled path) 48 | const isInsideClaudeApp = currentNodePath.includes('/Claude.app/') || 49 | currentNodePath.includes('\\Claude\\') || 50 | currentNodePath.includes('/Claude/'); 51 | 52 | if (isInsideClaudeApp && claudeBundledNode && existsSync(claudeBundledNode)) { 53 | // We're running from Claude Desktop, use its bundled runtimes 54 | return { 55 | type: 'bundled', 56 | nodePath: claudeBundledNode, 57 | pythonPath: claudeBundledPython || undefined 58 | }; 59 | } 60 | 61 | // Otherwise, we're running via system runtime 62 | return { 63 | type: 'system', 64 | nodePath: 'node', // Use system node 65 | pythonPath: 'python3' // Use system python 66 | }; 67 | } 68 | 69 | /** 70 | * Get runtime to use for spawning .dxt extension processes. 71 | * Uses the same runtime that NCP itself is running with. 72 | */ 73 | export function getRuntimeForExtension(command: string): string { 74 | const runtime = detectRuntime(); 75 | 76 | // If command is 'node' or ends with '/node', use detected Node runtime 77 | if (command === 'node' || command.endsWith('/node') || command.endsWith('\\node.exe')) { 78 | return runtime.nodePath; 79 | } 80 | 81 | // If command is 'npx', use npx from detected Node runtime 82 | if (command === 'npx' || command.endsWith('/npx') || command.endsWith('\\npx.cmd')) { 83 | // If using bundled runtime, construct npx path from node path 84 | if (runtime.type === 'bundled') { 85 | // Bundled node path: /Applications/Claude.app/.../node 86 | // Bundled npx path: /Applications/Claude.app/.../npx 87 | const npxPath = runtime.nodePath.replace(/\/node$/, '/npx').replace(/\\node\.exe$/, '\\npx.cmd'); 88 | return npxPath; 89 | } 90 | // For system runtime, use system npx 91 | return 'npx'; 92 | } 93 | 94 | // If command is 'python3'/'python', use detected Python runtime 95 | if (command === 'python3' || command === 'python' || 96 | command.endsWith('/python3') || command.endsWith('/python') || 97 | command.endsWith('\\python.exe') || command.endsWith('\\python3.exe')) { 98 | return runtime.pythonPath || command; // Fallback to original if no Python detected 99 | } 100 | 101 | // For other commands, return as-is 102 | return command; 103 | } 104 | 105 | /** 106 | * Log runtime detection info for debugging 107 | */ 108 | export function logRuntimeInfo(): void { 109 | const runtime = detectRuntime(); 110 | console.log(`[Runtime Detection]`); 111 | console.log(` Type: ${runtime.type}`); 112 | console.log(` Node: ${runtime.nodePath}`); 113 | if (runtime.pythonPath) { 114 | console.log(` Python: ${runtime.pythonPath}`); 115 | } 116 | console.log(` Process execPath: ${process.execPath}`); 117 | } 118 | ``` -------------------------------------------------------------------------------- /test/mcp-immediate-response-check.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | /** 3 | * Test MCP Server Immediate Response 4 | * 5 | * Verifies that NCP MCP server: 6 | * 1. Responds to initialize immediately 7 | * 2. Responds to tools/list immediately (without waiting for indexing) 8 | * 3. Advertises 'find' and 'run' tools 9 | * 4. Does not block on indexing 10 | */ 11 | 12 | import { MCPServer } from '../dist/server/mcp-server.js'; 13 | 14 | async function testMCPImmediateResponse() { 15 | console.log('========================================'); 16 | console.log('Testing MCP Server Immediate Response'); 17 | console.log('========================================\n'); 18 | 19 | const server = new MCPServer('default', false, false); // No progress output 20 | 21 | // Test 1: Initialize should return immediately 22 | console.log('Test 1: Initialize returns immediately'); 23 | console.log('----------------------------------------'); 24 | const initStartTime = Date.now(); 25 | await server.initialize(); 26 | const initDuration = Date.now() - initStartTime; 27 | 28 | if (initDuration < 100) { 29 | console.log(`✓ PASS: Initialize returned in ${initDuration}ms (non-blocking)`); 30 | } else { 31 | console.log(`✗ FAIL: Initialize took ${initDuration}ms (should be < 100ms)`); 32 | } 33 | console.log(''); 34 | 35 | // Test 2: tools/list should return immediately 36 | console.log('Test 2: tools/list returns immediately (even during indexing)'); 37 | console.log('----------------------------------------'); 38 | const listStartTime = Date.now(); 39 | const toolsResponse = await server.handleRequest({ 40 | jsonrpc: '2.0', 41 | id: 1, 42 | method: 'tools/list' 43 | }); 44 | const listDuration = Date.now() - listStartTime; 45 | 46 | if (listDuration < 100) { 47 | console.log(`✓ PASS: tools/list returned in ${listDuration}ms (non-blocking)`); 48 | } else { 49 | console.log(`✗ FAIL: tools/list took ${listDuration}ms (should be < 100ms)`); 50 | } 51 | console.log(''); 52 | 53 | // Test 3: Verify tools are advertised 54 | console.log('Test 3: Advertises find and run tools'); 55 | console.log('----------------------------------------'); 56 | const tools = toolsResponse.result?.tools || []; 57 | const toolNames = tools.map(t => t.name); 58 | 59 | console.log(`Found ${tools.length} tools: ${toolNames.join(', ')}`); 60 | 61 | if (toolNames.includes('find')) { 62 | console.log('✓ PASS: find tool advertised'); 63 | } else { 64 | console.log('✗ FAIL: find tool NOT advertised'); 65 | } 66 | 67 | if (toolNames.includes('run')) { 68 | console.log('✓ PASS: run tool advertised'); 69 | } else { 70 | console.log('✗ FAIL: run tool NOT advertised'); 71 | } 72 | console.log(''); 73 | 74 | // Test 4: Initialize request returns immediately 75 | console.log('Test 4: Initialize request returns immediately'); 76 | console.log('----------------------------------------'); 77 | const initReqStartTime = Date.now(); 78 | const initResponse = await server.handleRequest({ 79 | jsonrpc: '2.0', 80 | id: 2, 81 | method: 'initialize', 82 | params: { 83 | protocolVersion: '2024-11-05', 84 | capabilities: {}, 85 | clientInfo: { name: 'test-client', version: '1.0.0' } 86 | } 87 | }); 88 | const initReqDuration = Date.now() - initReqStartTime; 89 | 90 | if (initReqDuration < 50) { 91 | console.log(`✓ PASS: Initialize request returned in ${initReqDuration}ms`); 92 | } else { 93 | console.log(`✗ FAIL: Initialize request took ${initReqDuration}ms (should be < 50ms)`); 94 | } 95 | 96 | if (initResponse.result?.protocolVersion) { 97 | console.log(`✓ PASS: Initialize returned protocol version ${initResponse.result.protocolVersion}`); 98 | } else { 99 | console.log('✗ FAIL: Initialize did not return protocol version'); 100 | } 101 | console.log(''); 102 | 103 | await server.cleanup(); 104 | 105 | console.log('========================================'); 106 | console.log('Test Summary'); 107 | console.log('========================================'); 108 | console.log('Expected behavior:'); 109 | console.log(' - initialize() returns immediately (< 100ms)'); 110 | console.log(' - tools/list returns immediately (< 100ms)'); 111 | console.log(' - Advertises find and run tools'); 112 | console.log(' - Indexing happens in background'); 113 | } 114 | 115 | testMCPImmediateResponse().catch(error => { 116 | console.error('Test failed with error:', error); 117 | process.exit(1); 118 | }); 119 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@portel/ncp", 3 | "version": "1.5.2", 4 | "mcpName": "io.github.portel-dev/ncp", 5 | "description": "Natural Context Provider - N-to-1 MCP Orchestration for AI Assistants", 6 | "main": "dist/index.js", 7 | "module": "dist/index.js", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | } 13 | }, 14 | "bin": { 15 | "ncp": "dist/index.js" 16 | }, 17 | "type": "module", 18 | "scripts": { 19 | "build": "tsc && chmod +x dist/index.js", 20 | "postinstall": "npm run build:if-dev", 21 | "build:if-dev": "[ -d node_modules/typescript ] && npm run build || echo 'Build skipped (production install)'", 22 | "start": "node dist/index.js", 23 | "dev": "npm run build && npm run start", 24 | "test": "jest --detectOpenHandles --forceExit", 25 | "test:coverage": "jest --coverage --detectOpenHandles --forceExit", 26 | "test:watch": "jest --watch", 27 | "test:critical": "jest test/mcp-server-protocol.test.ts test/mcp-timeout-scenarios.test.ts --verbose --detectOpenHandles --forceExit", 28 | "test:integration": "npm run build && node test/integration/mcp-client-simulation.test.cjs", 29 | "test:cli": "bash test/cli-help-validation.sh", 30 | "test:pre-publish": "npm run test:critical && npm run test:integration", 31 | "test:package": "node scripts/test-package-locally.cjs", 32 | "build:dxt": "npm run build && npm prune --production && npx @anthropic-ai/mcpb pack && npm run rename:dxt && npm install", 33 | "rename:dxt": "for f in *.mcpb; do [ -f \"$f\" ] && mv \"$f\" \"${f%.mcpb}.dxt\" || true; done", 34 | "stats": "node scripts/check-dxt-downloads.js", 35 | "prepack": "npm run build && npm run test:pre-publish", 36 | "prepublishOnly": "npm run build && npm run test:pre-publish && node scripts/sync-server-version.cjs", 37 | "release": "release-it", 38 | "release:dry": "release-it --dry-run" 39 | }, 40 | "keywords": [ 41 | "mcp", 42 | "model-context-protocol", 43 | "ai-orchestration", 44 | "tool-discovery" 45 | ], 46 | "author": "Portel", 47 | "license": "Elastic-2.0", 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/portel-dev/ncp.git" 51 | }, 52 | "homepage": "https://github.com/portel-dev/ncp#readme", 53 | "bugs": { 54 | "url": "https://github.com/portel-dev/ncp/issues" 55 | }, 56 | "types": "dist/index.d.ts", 57 | "engines": { 58 | "node": ">=18.0.0" 59 | }, 60 | "files": [ 61 | "dist", 62 | "LICENSE", 63 | "README.md", 64 | "server.json", 65 | "package.json" 66 | ], 67 | "files:comments": "Explicit list of files to include in the package", 68 | "dependencies": { 69 | "@modelcontextprotocol/sdk": "^1.18.0", 70 | "@types/prompts": "^2.4.9", 71 | "@xenova/transformers": "^2.17.2", 72 | "asciichart": "^1.5.25", 73 | "chalk": "^5.3.0", 74 | "cli-graph": "^3.2.2", 75 | "cli-highlight": "^2.1.11", 76 | "clipboardy": "^4.0.0", 77 | "commander": "^14.0.1", 78 | "env-paths": "^3.0.0", 79 | "json-colorizer": "^3.0.1", 80 | "marked": "^15.0.12", 81 | "marked-terminal": "^7.3.0", 82 | "prettyjson": "^1.2.5", 83 | "prompts": "^2.4.2", 84 | "yaml": "^2.8.1" 85 | }, 86 | "devDependencies": { 87 | "@agent-infra/mcp-server-browser": "^1.2.23", 88 | "@amap/amap-maps-mcp-server": "^0.0.8", 89 | "@anthropic-ai/mcpb": "^1.1.1", 90 | "@apify/actors-mcp-server": "^0.4.15", 91 | "@benborla29/mcp-server-mysql": "^2.0.5", 92 | "@currents/mcp": "^2.0.0", 93 | "@heroku/mcp-server": "^1.0.7", 94 | "@hubspot/mcp-server": "^0.4.0", 95 | "@modelcontextprotocol/server-filesystem": "^2025.8.21", 96 | "@modelcontextprotocol/server-sequential-thinking": "^2025.7.1", 97 | "@notionhq/notion-mcp-server": "^1.9.0", 98 | "@release-it/conventional-changelog": "^10.0.1", 99 | "@supabase/mcp-server-supabase": "^0.5.5", 100 | "@types/express": "^5.0.3", 101 | "@types/jest": "^30.0.0", 102 | "@types/marked-terminal": "^6.1.1", 103 | "@types/node": "^20.0.0", 104 | "@types/prettyjson": "^0.0.33", 105 | "@upstash/context7-mcp": "^1.0.17", 106 | "@winor30/mcp-server-datadog": "^1.6.0", 107 | "jest": "^30.1.3", 108 | "mcp-hello-world": "^1.1.2", 109 | "mcp-server": "^0.0.9", 110 | "mcp-server-kubernetes": "^2.9.6", 111 | "release-it": "^19.0.4", 112 | "ts-jest": "^29.4.2", 113 | "tsx": "^4.20.5", 114 | "typescript": "^5.0.0" 115 | } 116 | } 117 | ``` -------------------------------------------------------------------------------- /test/mock-mcps/shell-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Shell MCP Server 5 | * Real MCP server structure for shell command execution testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'shell-test', 12 | version: '1.0.0', 13 | description: 'Execute shell commands and system operations including scripts, processes, and system management' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'execute_command', 19 | description: 'Execute shell commands and system operations. Run scripts, manage processes, perform system tasks.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | command: { 24 | type: 'string', 25 | description: 'Shell command to execute' 26 | }, 27 | working_directory: { 28 | type: 'string', 29 | description: 'Working directory for command execution' 30 | }, 31 | timeout: { 32 | type: 'number', 33 | description: 'Command timeout in seconds' 34 | }, 35 | environment: { 36 | type: 'object', 37 | description: 'Environment variables for command' 38 | }, 39 | capture_output: { 40 | type: 'boolean', 41 | description: 'Capture command output' 42 | } 43 | }, 44 | required: ['command'] 45 | } 46 | }, 47 | { 48 | name: 'run_script', 49 | description: 'Execute shell scripts and batch operations with parameters. Run automation scripts, execute batch jobs.', 50 | inputSchema: { 51 | type: 'object', 52 | properties: { 53 | script_path: { 54 | type: 'string', 55 | description: 'Path to script file' 56 | }, 57 | arguments: { 58 | type: 'array', 59 | description: 'Script arguments', 60 | items: { type: 'string' } 61 | }, 62 | interpreter: { 63 | type: 'string', 64 | description: 'Script interpreter (bash, python, node, etc.)' 65 | }, 66 | working_directory: { 67 | type: 'string', 68 | description: 'Working directory for script' 69 | } 70 | }, 71 | required: ['script_path'] 72 | } 73 | }, 74 | { 75 | name: 'manage_process', 76 | description: 'Manage system processes including start, stop, and monitoring. Control services, manage applications.', 77 | inputSchema: { 78 | type: 'object', 79 | properties: { 80 | action: { 81 | type: 'string', 82 | description: 'Process action (start, stop, restart, status, list)' 83 | }, 84 | process_name: { 85 | type: 'string', 86 | description: 'Process or service name' 87 | }, 88 | pid: { 89 | type: 'number', 90 | description: 'Process ID for specific process operations' 91 | }, 92 | signal: { 93 | type: 'string', 94 | description: 'Signal to send to process (TERM, KILL, etc.)' 95 | } 96 | }, 97 | required: ['action'] 98 | } 99 | }, 100 | { 101 | name: 'check_system_info', 102 | description: 'Get system information including resources, processes, and status. Monitor system health, check resources.', 103 | inputSchema: { 104 | type: 'object', 105 | properties: { 106 | info_type: { 107 | type: 'string', 108 | description: 'Type of system info (cpu, memory, disk, network, processes)' 109 | }, 110 | detailed: { 111 | type: 'boolean', 112 | description: 'Include detailed information' 113 | } 114 | }, 115 | required: ['info_type'] 116 | } 117 | }, 118 | { 119 | name: 'manage_environment', 120 | description: 'Manage environment variables and system configuration. Set variables, configure system settings.', 121 | inputSchema: { 122 | type: 'object', 123 | properties: { 124 | action: { 125 | type: 'string', 126 | description: 'Environment action (get, set, unset, list)' 127 | }, 128 | variable: { 129 | type: 'string', 130 | description: 'Environment variable name' 131 | }, 132 | value: { 133 | type: 'string', 134 | description: 'Variable value for set action' 135 | }, 136 | scope: { 137 | type: 'string', 138 | description: 'Variable scope (session, user, system)' 139 | } 140 | }, 141 | required: ['action'] 142 | } 143 | } 144 | ]; 145 | 146 | // Create and run the server 147 | const server = new MockMCPServer(serverInfo, tools); 148 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /test/mock-mcps/brave-search-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Brave Search MCP Server 5 | * Real MCP server structure for Brave Search API testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'brave-search-test', 12 | version: '1.0.0', 13 | description: 'Web search capabilities with privacy-focused results and real-time information' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'web_search', 19 | description: 'Search the web using Brave Search API with privacy protection. Find information, research topics, get current data.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | query: { 24 | type: 'string', 25 | description: 'Search query string' 26 | }, 27 | count: { 28 | type: 'number', 29 | description: 'Number of results to return' 30 | }, 31 | offset: { 32 | type: 'number', 33 | description: 'Result offset for pagination' 34 | }, 35 | country: { 36 | type: 'string', 37 | description: 'Country code for localized results' 38 | }, 39 | search_lang: { 40 | type: 'string', 41 | description: 'Search language code' 42 | }, 43 | ui_lang: { 44 | type: 'string', 45 | description: 'UI language code' 46 | }, 47 | freshness: { 48 | type: 'string', 49 | description: 'Result freshness (pd, pw, pm, py for past day/week/month/year)' 50 | } 51 | }, 52 | required: ['query'] 53 | } 54 | }, 55 | { 56 | name: 'news_search', 57 | description: 'Search for news articles with current events and breaking news. Get latest news, find articles, track stories.', 58 | inputSchema: { 59 | type: 'object', 60 | properties: { 61 | query: { 62 | type: 'string', 63 | description: 'News search query' 64 | }, 65 | count: { 66 | type: 'number', 67 | description: 'Number of news results' 68 | }, 69 | offset: { 70 | type: 'number', 71 | description: 'Result offset' 72 | }, 73 | freshness: { 74 | type: 'string', 75 | description: 'News freshness filter' 76 | }, 77 | text_decorations: { 78 | type: 'boolean', 79 | description: 'Include text decorations in results' 80 | } 81 | }, 82 | required: ['query'] 83 | } 84 | }, 85 | { 86 | name: 'image_search', 87 | description: 'Search for images with filtering options. Find pictures, locate visual content, discover graphics.', 88 | inputSchema: { 89 | type: 'object', 90 | properties: { 91 | query: { 92 | type: 'string', 93 | description: 'Image search query' 94 | }, 95 | count: { 96 | type: 'number', 97 | description: 'Number of image results' 98 | }, 99 | offset: { 100 | type: 'number', 101 | description: 'Result offset' 102 | }, 103 | size: { 104 | type: 'string', 105 | description: 'Image size filter (small, medium, large, wallpaper)' 106 | }, 107 | color: { 108 | type: 'string', 109 | description: 'Color filter' 110 | }, 111 | type: { 112 | type: 'string', 113 | description: 'Image type (photo, clipart, lineart, animated)' 114 | }, 115 | layout: { 116 | type: 'string', 117 | description: 'Image layout (square, wide, tall)' 118 | } 119 | }, 120 | required: ['query'] 121 | } 122 | }, 123 | { 124 | name: 'video_search', 125 | description: 'Search for videos across platforms with filtering capabilities. Find educational content, tutorials, entertainment.', 126 | inputSchema: { 127 | type: 'object', 128 | properties: { 129 | query: { 130 | type: 'string', 131 | description: 'Video search query' 132 | }, 133 | count: { 134 | type: 'number', 135 | description: 'Number of video results' 136 | }, 137 | offset: { 138 | type: 'number', 139 | description: 'Result offset' 140 | }, 141 | duration: { 142 | type: 'string', 143 | description: 'Video duration filter (short, medium, long)' 144 | }, 145 | resolution: { 146 | type: 'string', 147 | description: 'Video resolution filter' 148 | } 149 | }, 150 | required: ['query'] 151 | } 152 | } 153 | ]; 154 | 155 | // Create and run the server 156 | const server = new MockMCPServer(serverInfo, tools); 157 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /test/mock-mcps/docker-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Docker MCP Server 5 | * Real MCP server structure for Docker container management testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'docker-test', 12 | version: '1.0.0', 13 | description: 'Container management including Docker operations, image building, and deployment' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'run_container', 19 | description: 'Run Docker containers from images with configuration options. Deploy applications, start services.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | image: { 24 | type: 'string', 25 | description: 'Docker image name and tag' 26 | }, 27 | name: { 28 | type: 'string', 29 | description: 'Container name' 30 | }, 31 | ports: { 32 | type: 'array', 33 | description: 'Port mappings (host:container)', 34 | items: { type: 'string' } 35 | }, 36 | volumes: { 37 | type: 'array', 38 | description: 'Volume mappings', 39 | items: { type: 'string' } 40 | }, 41 | environment: { 42 | type: 'object', 43 | description: 'Environment variables' 44 | }, 45 | detached: { 46 | type: 'boolean', 47 | description: 'Run container in background' 48 | } 49 | }, 50 | required: ['image'] 51 | } 52 | }, 53 | { 54 | name: 'build_image', 55 | description: 'Build Docker images from Dockerfile with build context. Create custom images, package applications.', 56 | inputSchema: { 57 | type: 'object', 58 | properties: { 59 | dockerfile_path: { 60 | type: 'string', 61 | description: 'Path to Dockerfile' 62 | }, 63 | context_path: { 64 | type: 'string', 65 | description: 'Build context directory' 66 | }, 67 | tag: { 68 | type: 'string', 69 | description: 'Image tag name' 70 | }, 71 | build_args: { 72 | type: 'object', 73 | description: 'Build arguments' 74 | }, 75 | no_cache: { 76 | type: 'boolean', 77 | description: 'Build without cache' 78 | } 79 | }, 80 | required: ['dockerfile_path', 'tag'] 81 | } 82 | }, 83 | { 84 | name: 'manage_container', 85 | description: 'Manage Docker container lifecycle including start, stop, restart operations. Control running containers.', 86 | inputSchema: { 87 | type: 'object', 88 | properties: { 89 | action: { 90 | type: 'string', 91 | description: 'Container action (start, stop, restart, remove, pause, unpause)' 92 | }, 93 | container: { 94 | type: 'string', 95 | description: 'Container name or ID' 96 | }, 97 | force: { 98 | type: 'boolean', 99 | description: 'Force action if needed' 100 | } 101 | }, 102 | required: ['action', 'container'] 103 | } 104 | }, 105 | { 106 | name: 'list_containers', 107 | description: 'List Docker containers with filtering and status information. View running containers, check container status.', 108 | inputSchema: { 109 | type: 'object', 110 | properties: { 111 | all: { 112 | type: 'boolean', 113 | description: 'Include stopped containers' 114 | }, 115 | filter: { 116 | type: 'object', 117 | description: 'Filter criteria' 118 | }, 119 | format: { 120 | type: 'string', 121 | description: 'Output format' 122 | } 123 | } 124 | } 125 | }, 126 | { 127 | name: 'execute_in_container', 128 | description: 'Execute commands inside running Docker containers. Debug containers, run maintenance tasks.', 129 | inputSchema: { 130 | type: 'object', 131 | properties: { 132 | container: { 133 | type: 'string', 134 | description: 'Container name or ID' 135 | }, 136 | command: { 137 | type: 'string', 138 | description: 'Command to execute' 139 | }, 140 | interactive: { 141 | type: 'boolean', 142 | description: 'Interactive mode' 143 | }, 144 | tty: { 145 | type: 'boolean', 146 | description: 'Allocate pseudo-TTY' 147 | }, 148 | user: { 149 | type: 'string', 150 | description: 'User to run command as' 151 | } 152 | }, 153 | required: ['container', 'command'] 154 | } 155 | } 156 | ]; 157 | 158 | // Create and run the server 159 | const server = new MockMCPServer(serverInfo, tools); 160 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- ```markdown 1 | # Release Process 2 | 3 | This document describes how to release a new version of NCP. 4 | 5 | ## Automated Release via GitHub Actions 6 | 7 | ### Prerequisites 8 | 9 | 1. All changes committed and pushed to `main` 10 | 2. All tests passing 11 | 3. Clean working directory 12 | 13 | ### Release Steps 14 | 15 | 1. **Trigger Release Workflow** 16 | - Go to Actions → Release workflow 17 | - Click "Run workflow" 18 | - Select release type: `patch`, `minor`, or `major` 19 | - Optionally check "Dry run" to test without publishing 20 | 21 | 2. **What Happens Automatically** 22 | 23 | The release workflow (`release.yml`) will: 24 | - ✅ Run full test suite 25 | - ✅ Build the project 26 | - ✅ Bump version in `package.json` 27 | - ✅ Update `CHANGELOG.md` with conventional commits 28 | - ✅ Create git tag (e.g., `1.4.0`) 29 | - ✅ Push tag to GitHub 30 | - ✅ Create GitHub Release 31 | - ✅ Publish to NPM (`@portel/ncp`) 32 | 33 | 3. **MCP Registry Publication** (Automatic) 34 | 35 | After GitHub Release is published, the MCP registry workflow (`publish-mcp-registry.yml`) automatically: 36 | - ✅ Syncs version to `server.json` 37 | - ✅ Validates `server.json` against MCP schema 38 | - ✅ Downloads MCP Publisher CLI 39 | - ✅ Authenticates via GitHub OIDC (no secrets needed!) 40 | - ✅ Publishes to MCP Registry 41 | 42 | **Registry Details**: 43 | - Package: `io.github.portel-dev/ncp` 44 | - Authentication: GitHub OIDC (automatic via `id-token: write` permission) 45 | - No manual steps required! 46 | 47 | ## Manual Release (Not Recommended) 48 | 49 | If you need to release manually: 50 | 51 | ```bash 52 | # Ensure clean state 53 | git status 54 | 55 | # Run release-it 56 | npm run release 57 | 58 | # This will: 59 | # - Prompt for version bump type 60 | # - Run tests 61 | # - Update version and CHANGELOG 62 | # - Create git tag 63 | # - Push to GitHub 64 | # - Publish to NPM 65 | 66 | # MCP Registry will auto-publish when GitHub Release is created 67 | ``` 68 | 69 | ## Version Numbering 70 | 71 | We follow [Semantic Versioning](https://semver.org/): 72 | 73 | - **Major** (X.0.0): Breaking changes 74 | - **Minor** (x.X.0): New features (backward compatible) 75 | - **Patch** (x.x.X): Bug fixes (backward compatible) 76 | 77 | ## Post-Release Checklist 78 | 79 | After release completes: 80 | 81 | - [ ] Verify NPM package: https://www.npmjs.com/package/@portel/ncp 82 | - [ ] Check GitHub Release: https://github.com/portel-dev/ncp/releases 83 | - [ ] Verify MCP Registry listing (may take a few minutes) 84 | - [ ] Test installation: `npx @portel/ncp@latest --version` 85 | - [ ] Announce release (if significant) 86 | 87 | ## Troubleshooting 88 | 89 | ### NPM Publish Failed 90 | - Check NPM authentication in GitHub Secrets 91 | - Verify package name isn't taken 92 | - Check `.npmignore` for correct file exclusions 93 | 94 | ### MCP Registry Publish Failed 95 | 96 | **Issue: Organization not detected with OIDC** 97 | 98 | If you see "portel-dev organization not detected" with GitHub OIDC: 99 | 100 | 1. **Use GitHub Personal Access Token (Recommended)**: 101 | - Create a GitHub PAT with `repo` and `read:org` scopes 102 | - Go to: Settings → Developer settings → Personal access tokens → Tokens (classic) 103 | - Generate new token with scopes: `repo`, `read:org` 104 | - Add as GitHub Secret: `Settings → Secrets → Actions → New repository secret` 105 | - Name: `MCP_GITHUB_TOKEN` 106 | - Value: Your PAT 107 | - Workflow will automatically fallback to PAT if OIDC fails 108 | 109 | 2. **Other troubleshooting**: 110 | - Check GitHub Actions logs for `publish-mcp-registry` workflow 111 | - Verify `server.json` is valid (run `jsonschema -i server.json /tmp/mcp-server.schema.json`) 112 | - Ensure `id-token: write` permission is set in workflow 113 | - Confirm you're an admin of `portel-dev` organization: `gh api orgs/portel-dev/memberships/$(gh api user -q .login)` 114 | 115 | ### Release Workflow Failed 116 | - Check test failures in Actions logs 117 | - Ensure clean working directory 118 | - Verify all dependencies are installed 119 | 120 | ## Emergency Hotfix Process 121 | 122 | For critical bugs requiring immediate release: 123 | 124 | 1. Create hotfix branch from affected release tag 125 | 2. Fix the bug 126 | 3. Follow normal release process with `patch` bump 127 | 4. Both NPM and MCP Registry will auto-publish 128 | 129 | ## Release Artifacts 130 | 131 | Each release produces: 132 | 133 | - **NPM Package**: `@portel/[email protected]` on npmjs.com 134 | - **MCP Registry Entry**: `io.github.portel-dev/ncp` in MCP registry 135 | - **GitHub Release**: Tagged release with changelog 136 | - **Git Tag**: `X.Y.Z` (or `vX.Y.Z` format) 137 | 138 | ## Contact 139 | 140 | For release issues or questions: 141 | - GitHub Issues: https://github.com/portel-dev/ncp/issues 142 | - Repository: https://github.com/portel-dev/ncp 143 | ``` -------------------------------------------------------------------------------- /docs/download-stats.md: -------------------------------------------------------------------------------- ```markdown 1 | # NCP Download Statistics 2 | 3 | **Last Updated:** Auto-updated by GitHub badges 4 | 5 | ## Total Downloads Across All Channels 6 | 7 | | Distribution Method | Total Downloads | Latest Version | 8 | |---------------------|-----------------|----------------| 9 | | **npm Package** |  |  | 10 | | **.mcpb Bundle** |  |  | 11 | 12 | --- 13 | 14 | ## Distribution Breakdown 15 | 16 | ### npm Package 17 | - **Best for:** All MCP clients (Cursor, Cline, Continue, VS Code) 18 | - **Includes:** Full CLI tools + MCP server 19 | - **Auto-updates:** Via `npm update -g @portel/ncp` 20 | - **Package size:** ~2.5MB 21 | 22 | [](https://www.npmjs.com/package/@portel/ncp) 23 | [](https://www.npmjs.com/package/@portel/ncp) 24 | 25 | ### .mcpb Bundle 26 | - **Best for:** Claude Desktop users 27 | - **Includes:** Slim MCP-only server (no CLI) 28 | - **Auto-updates:** Through Claude Desktop 29 | - **Bundle size:** 126KB (13% smaller) 30 | 31 | [](https://github.com/portel-dev/ncp/releases/latest) 32 | [](https://github.com/portel-dev/ncp/releases) 33 | 34 | --- 35 | 36 | ## Growth Metrics 37 | 38 | ### Monthly npm Downloads 39 |  40 | 41 | ### GitHub Release Downloads 42 |  43 | 44 | --- 45 | 46 | ## Version Adoption 47 | 48 | | Version | Release Date | Downloads | Status | 49 | |---------|--------------|-----------|--------| 50 | | Latest |  |  | ✅ Active | 51 | 52 | --- 53 | 54 | ## How to Check Live Stats 55 | 56 | ### Via npm 57 | ```bash 58 | npm info @portel/ncp 59 | # Or use npm-stat for detailed analytics 60 | npx npm-stat @portel/ncp 61 | ``` 62 | 63 | ### Via GitHub API 64 | ```bash 65 | # Run our custom script 66 | node scripts/check-mcpb-downloads.js 67 | ``` 68 | 69 | ### Via Shields.io API 70 | ```bash 71 | # npm downloads (all time) 72 | curl https://img.shields.io/npm/dt/@portel/ncp.json 73 | 74 | # GitHub release downloads (all time) 75 | curl https://img.shields.io/github/downloads/portel-dev/ncp/total.json 76 | ``` 77 | 78 | --- 79 | 80 | ## Platform Breakdown 81 | 82 | *Note: Platform-specific download statistics require implementing telemetry (see `docs/guides/telemetry-design.md`)* 83 | 84 | Without telemetry, we can only track: 85 | - ✅ Total downloads (npm + GitHub) 86 | - ✅ Downloads per version 87 | - ❌ Platform distribution (macOS/Windows/Linux) 88 | - ❌ Active installations 89 | - ❌ MCP client distribution 90 | 91 | --- 92 | 93 | ## Credibility Indicators 94 | 95 | Use these badges in your documentation, website, or presentations: 96 | 97 | ### Compact Badges (for README) 98 | ```markdown 99 | [](https://npmjs.com/package/@portel/ncp) 100 | [](https://github.com/portel-dev/ncp/releases) 101 | ``` 102 | 103 | ### Large Badges (for landing pages) 104 | ```markdown 105 | [](https://npmjs.com/package/@portel/ncp) 106 | [](https://github.com/portel-dev/ncp/releases) 107 | ``` 108 | 109 | --- 110 | 111 | ## Share Your Success 112 | 113 | Once you hit download milestones, celebrate with the community: 114 | 115 | - 🎉 **1,000 downloads** - "NCP has reached 1K downloads! Thank you!" 116 | - 🚀 **10,000 downloads** - "10K downloads milestone! Here's what's next..." 117 | - 🌟 **100,000 downloads** - "100K downloads! Here's the impact..." 118 | 119 | Share on: 120 | - Twitter/X with `#ModelContextProtocol` `#MCP` 121 | - LinkedIn tech community 122 | - Hacker News (Show HN) 123 | - Reddit (r/ChatGPT, r/ClaudeAI, r/LocalLLaMA) 124 | ``` -------------------------------------------------------------------------------- /src/cache/schema-cache.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Schema Cache 3 | * 4 | * Caches MCP configuration schemas for reuse across add/repair/import commands 5 | * Stores in JSON files alongside CSV cache 6 | */ 7 | 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | import { ConfigurationSchema } from '../services/config-schema-reader.js'; 11 | import { logger } from '../utils/logger.js'; 12 | 13 | export class SchemaCache { 14 | private cacheDir: string; 15 | 16 | constructor(cacheDir: string) { 17 | this.cacheDir = path.join(cacheDir, 'schemas'); 18 | this.ensureCacheDir(); 19 | } 20 | 21 | /** 22 | * Ensure cache directory exists 23 | */ 24 | private ensureCacheDir(): void { 25 | if (!fs.existsSync(this.cacheDir)) { 26 | fs.mkdirSync(this.cacheDir, { recursive: true }); 27 | } 28 | } 29 | 30 | /** 31 | * Save configuration schema to cache 32 | */ 33 | save(mcpName: string, schema: ConfigurationSchema): void { 34 | try { 35 | const filepath = this.getSchemaPath(mcpName); 36 | const data = { 37 | mcpName, 38 | schema, 39 | cachedAt: new Date().toISOString(), 40 | version: '1.0' 41 | }; 42 | 43 | fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8'); 44 | logger.debug(`Cached configuration schema for ${mcpName}`); 45 | } catch (error: any) { 46 | logger.error(`Failed to cache schema for ${mcpName}:`, error.message); 47 | } 48 | } 49 | 50 | /** 51 | * Load configuration schema from cache 52 | */ 53 | get(mcpName: string): ConfigurationSchema | null { 54 | try { 55 | const filepath = this.getSchemaPath(mcpName); 56 | 57 | if (!fs.existsSync(filepath)) { 58 | return null; 59 | } 60 | 61 | const data = JSON.parse(fs.readFileSync(filepath, 'utf-8')); 62 | logger.debug(`Loaded cached schema for ${mcpName}`); 63 | return data.schema as ConfigurationSchema; 64 | } catch (error: any) { 65 | logger.error(`Failed to load cached schema for ${mcpName}:`, error.message); 66 | return null; 67 | } 68 | } 69 | 70 | /** 71 | * Check if schema is cached 72 | */ 73 | has(mcpName: string): boolean { 74 | const filepath = this.getSchemaPath(mcpName); 75 | return fs.existsSync(filepath); 76 | } 77 | 78 | /** 79 | * Delete cached schema 80 | */ 81 | delete(mcpName: string): void { 82 | try { 83 | const filepath = this.getSchemaPath(mcpName); 84 | 85 | if (fs.existsSync(filepath)) { 86 | fs.unlinkSync(filepath); 87 | logger.debug(`Deleted cached schema for ${mcpName}`); 88 | } 89 | } catch (error: any) { 90 | logger.error(`Failed to delete cached schema for ${mcpName}:`, error.message); 91 | } 92 | } 93 | 94 | /** 95 | * Clear all cached schemas 96 | */ 97 | clear(): void { 98 | try { 99 | if (fs.existsSync(this.cacheDir)) { 100 | const files = fs.readdirSync(this.cacheDir); 101 | for (const file of files) { 102 | if (file.endsWith('.schema.json')) { 103 | fs.unlinkSync(path.join(this.cacheDir, file)); 104 | } 105 | } 106 | logger.debug('Cleared all cached schemas'); 107 | } 108 | } catch (error: any) { 109 | logger.error('Failed to clear schema cache:', error.message); 110 | } 111 | } 112 | 113 | /** 114 | * Get all cached schemas 115 | */ 116 | listAll(): Array<{ mcpName: string; cachedAt: string }> { 117 | try { 118 | if (!fs.existsSync(this.cacheDir)) { 119 | return []; 120 | } 121 | 122 | const files = fs.readdirSync(this.cacheDir); 123 | const schemas: Array<{ mcpName: string; cachedAt: string }> = []; 124 | 125 | for (const file of files) { 126 | if (file.endsWith('.schema.json')) { 127 | const filepath = path.join(this.cacheDir, file); 128 | const data = JSON.parse(fs.readFileSync(filepath, 'utf-8')); 129 | schemas.push({ 130 | mcpName: data.mcpName, 131 | cachedAt: data.cachedAt 132 | }); 133 | } 134 | } 135 | 136 | return schemas; 137 | } catch (error: any) { 138 | logger.error('Failed to list cached schemas:', error.message); 139 | return []; 140 | } 141 | } 142 | 143 | /** 144 | * Get file path for schema 145 | */ 146 | private getSchemaPath(mcpName: string): string { 147 | // Sanitize MCP name for filename 148 | const safeName = mcpName.replace(/[^a-zA-Z0-9-_]/g, '_'); 149 | return path.join(this.cacheDir, `${safeName}.schema.json`); 150 | } 151 | 152 | /** 153 | * Get cache statistics 154 | */ 155 | getStats(): { total: number; oldestCache: string | null; newestCache: string | null } { 156 | const all = this.listAll(); 157 | 158 | if (all.length === 0) { 159 | return { total: 0, oldestCache: null, newestCache: null }; 160 | } 161 | 162 | const sorted = all.sort((a, b) => 163 | new Date(a.cachedAt).getTime() - new Date(b.cachedAt).getTime() 164 | ); 165 | 166 | return { 167 | total: all.length, 168 | oldestCache: sorted[0].cachedAt, 169 | newestCache: sorted[sorted.length - 1].cachedAt 170 | }; 171 | } 172 | } 173 | ``` -------------------------------------------------------------------------------- /src/utils/schema-examples.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Dynamic Schema Examples Generator 3 | * 4 | * Generates realistic examples from actual available tools instead of hardcoded dummy examples 5 | */ 6 | 7 | export class SchemaExamplesGenerator { 8 | private tools: Array<{name: string, description: string}> = []; 9 | 10 | constructor(availableTools: Array<{name: string, description: string}>) { 11 | this.tools = availableTools; 12 | } 13 | 14 | /** 15 | * Get realistic tool execution examples 16 | */ 17 | getToolExecutionExamples(): string[] { 18 | const examples: string[] = []; 19 | 20 | // Always include Shell if available (most common) 21 | const shellTool = this.tools.find(t => t.name.startsWith('Shell:')); 22 | if (shellTool) { 23 | examples.push(shellTool.name); 24 | } 25 | 26 | // Add file operation example 27 | const fileTools = this.tools.filter(t => 28 | t.description?.toLowerCase().includes('file') || 29 | t.name.includes('write') || 30 | t.name.includes('read') 31 | ); 32 | if (fileTools.length > 0) { 33 | examples.push(fileTools[0].name); 34 | } 35 | 36 | // Add one more diverse example 37 | const otherTool = this.tools.find(t => 38 | !t.name.startsWith('Shell:') && 39 | !examples.includes(t.name) 40 | ); 41 | if (otherTool) { 42 | examples.push(otherTool.name); 43 | } 44 | 45 | return examples.slice(0, 3); // Max 3 examples 46 | } 47 | 48 | /** 49 | * Get realistic discovery query examples 50 | */ 51 | getDiscoveryExamples(): string[] { 52 | const examples: string[] = []; 53 | 54 | // Analyze available tools to suggest realistic queries 55 | const hasFileOps = this.tools.some(t => t.description?.toLowerCase().includes('file')); 56 | const hasGit = this.tools.some(t => t.description?.toLowerCase().includes('git')); 57 | const hasWeb = this.tools.some(t => t.description?.toLowerCase().includes('web') || t.description?.toLowerCase().includes('search')); 58 | 59 | if (hasFileOps) examples.push('create a new file'); 60 | if (hasGit) examples.push('check git status'); 61 | if (hasWeb) examples.push('search the web'); 62 | 63 | // Generic fallbacks 64 | if (examples.length === 0) { 65 | examples.push('run a command', 'list files'); 66 | } 67 | 68 | return examples.slice(0, 3); 69 | } 70 | 71 | /** 72 | * Generate complete tool execution schema with dynamic examples 73 | */ 74 | getToolExecutionSchema() { 75 | const examples = this.getToolExecutionExamples(); 76 | const exampleText = examples.length > 0 77 | ? `(e.g., ${examples.map(e => `"${e}"`).join(', ')})` 78 | : '(use the discover_tools command to find available tools)'; 79 | 80 | return { 81 | type: 'string', 82 | description: `The specific tool name to execute ${exampleText}` 83 | }; 84 | } 85 | 86 | /** 87 | * Generate discovery schema with realistic examples 88 | */ 89 | getDiscoverySchema() { 90 | const examples = this.getDiscoveryExamples(); 91 | const exampleText = examples.length > 0 92 | ? `(e.g., ${examples.map(e => `"${e}"`).join(', ')})` 93 | : ''; 94 | 95 | return { 96 | type: 'string', 97 | description: `Natural language description of what you want to do ${exampleText}` 98 | }; 99 | } 100 | 101 | /** 102 | * Get tool categories for better organization 103 | */ 104 | getToolCategories(): {[category: string]: string[]} { 105 | const categories: {[category: string]: string[]} = {}; 106 | 107 | for (const tool of this.tools) { 108 | const desc = tool.description?.toLowerCase() || ''; 109 | const name = tool.name.toLowerCase(); 110 | 111 | if (name.includes('shell') || desc.includes('command')) { 112 | categories['System Commands'] = categories['System Commands'] || []; 113 | categories['System Commands'].push(tool.name); 114 | } else if (desc.includes('file') || name.includes('read') || name.includes('write')) { 115 | categories['File Operations'] = categories['File Operations'] || []; 116 | categories['File Operations'].push(tool.name); 117 | } else if (desc.includes('web') || desc.includes('search')) { 118 | categories['Web & Search'] = categories['Web & Search'] || []; 119 | categories['Web & Search'].push(tool.name); 120 | } else if (desc.includes('git')) { 121 | categories['Git Operations'] = categories['Git Operations'] || []; 122 | categories['Git Operations'].push(tool.name); 123 | } else { 124 | categories['Other Tools'] = categories['Other Tools'] || []; 125 | categories['Other Tools'].push(tool.name); 126 | } 127 | } 128 | 129 | return categories; 130 | } 131 | } 132 | 133 | /** 134 | * Fallback examples when no tools are available yet (startup scenario) 135 | */ 136 | export const FALLBACK_EXAMPLES = { 137 | toolExecution: [ 138 | '"Shell:run_command"', 139 | '"desktop-commander:write_file"' 140 | ], 141 | discovery: [ 142 | '"run a shell command"', 143 | '"create a new file"', 144 | '"search for something"' 145 | ] 146 | }; ``` -------------------------------------------------------------------------------- /test/mock-mcps/aws-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock AWS MCP Server 5 | * Real MCP server structure for AWS services testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'aws-test', 12 | version: '1.0.0', 13 | description: 'Amazon Web Services integration for EC2, S3, Lambda, and cloud resource management' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'create_ec2_instance', 19 | description: 'Launch new EC2 virtual machine instances with configuration. Create servers, deploy applications to cloud.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | image_id: { 24 | type: 'string', 25 | description: 'AMI ID for instance' 26 | }, 27 | instance_type: { 28 | type: 'string', 29 | description: 'Instance size (t2.micro, m5.large, etc.)' 30 | }, 31 | key_name: { 32 | type: 'string', 33 | description: 'Key pair name for SSH access' 34 | }, 35 | security_groups: { 36 | type: 'array', 37 | description: 'Security group names', 38 | items: { type: 'string' } 39 | }, 40 | tags: { 41 | type: 'object', 42 | description: 'Instance tags as key-value pairs' 43 | } 44 | }, 45 | required: ['image_id', 'instance_type'] 46 | } 47 | }, 48 | { 49 | name: 'upload_to_s3', 50 | description: 'Upload files and objects to S3 storage buckets. Store files in cloud, backup data, host static content.', 51 | inputSchema: { 52 | type: 'object', 53 | properties: { 54 | bucket: { 55 | type: 'string', 56 | description: 'S3 bucket name' 57 | }, 58 | key: { 59 | type: 'string', 60 | description: 'Object key/path in bucket' 61 | }, 62 | file_path: { 63 | type: 'string', 64 | description: 'Local file path to upload' 65 | }, 66 | content_type: { 67 | type: 'string', 68 | description: 'MIME type of file' 69 | }, 70 | public: { 71 | type: 'boolean', 72 | description: 'Make object publicly accessible' 73 | } 74 | }, 75 | required: ['bucket', 'key', 'file_path'] 76 | } 77 | }, 78 | { 79 | name: 'create_lambda_function', 80 | description: 'Deploy serverless Lambda functions for event-driven computing. Run code without servers, process events.', 81 | inputSchema: { 82 | type: 'object', 83 | properties: { 84 | function_name: { 85 | type: 'string', 86 | description: 'Lambda function name' 87 | }, 88 | runtime: { 89 | type: 'string', 90 | description: 'Runtime environment (nodejs18.x, python3.9, etc.)' 91 | }, 92 | handler: { 93 | type: 'string', 94 | description: 'Function handler entry point' 95 | }, 96 | code: { 97 | type: 'object', 98 | description: 'Function code (zip file or inline)' 99 | }, 100 | role: { 101 | type: 'string', 102 | description: 'IAM role ARN for execution' 103 | } 104 | }, 105 | required: ['function_name', 'runtime', 'handler', 'code', 'role'] 106 | } 107 | }, 108 | { 109 | name: 'list_resources', 110 | description: 'List AWS resources across services with filtering options. View EC2 instances, S3 buckets, Lambda functions.', 111 | inputSchema: { 112 | type: 'object', 113 | properties: { 114 | service: { 115 | type: 'string', 116 | description: 'AWS service name (ec2, s3, lambda, etc.)' 117 | }, 118 | region: { 119 | type: 'string', 120 | description: 'AWS region to query' 121 | }, 122 | filters: { 123 | type: 'object', 124 | description: 'Service-specific filters' 125 | } 126 | }, 127 | required: ['service'] 128 | } 129 | }, 130 | { 131 | name: 'create_rds_database', 132 | description: 'Create managed RDS database instances with configuration. Set up MySQL, PostgreSQL databases in cloud.', 133 | inputSchema: { 134 | type: 'object', 135 | properties: { 136 | db_name: { 137 | type: 'string', 138 | description: 'Database instance identifier' 139 | }, 140 | engine: { 141 | type: 'string', 142 | description: 'Database engine (mysql, postgres, etc.)' 143 | }, 144 | instance_class: { 145 | type: 'string', 146 | description: 'Database instance size' 147 | }, 148 | allocated_storage: { 149 | type: 'number', 150 | description: 'Storage size in GB' 151 | }, 152 | username: { 153 | type: 'string', 154 | description: 'Master username' 155 | }, 156 | password: { 157 | type: 'string', 158 | description: 'Master password' 159 | } 160 | }, 161 | required: ['db_name', 'engine', 'instance_class', 'allocated_storage', 'username', 'password'] 162 | } 163 | } 164 | ]; 165 | 166 | // Create and run the server 167 | const server = new MockMCPServer(serverInfo, tools); 168 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/mcp-registry-setup.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Registry Publishing Setup 2 | 3 | ## Overview 4 | 5 | NCP automatically publishes to the MCP Registry when a GitHub Release is created. This document explains the authentication setup. 6 | 7 | ## Authentication Methods 8 | 9 | ### Method 1: GitHub OIDC (Automatic) 10 | 11 | **Pros**: 12 | - ✅ No secrets to configure 13 | - ✅ Automatic via GitHub Actions 14 | - ✅ Most secure (short-lived tokens) 15 | 16 | **Cons**: 17 | - ⚠️ May not detect organization membership correctly 18 | - ⚠️ Known issue with `portel-dev` organization detection 19 | 20 | **How it works**: 21 | - Workflow has `id-token: write` permission 22 | - GitHub Actions generates OIDC token automatically 23 | - MCP Publisher uses token to authenticate 24 | 25 | **No setup required** - works out of the box (if organization detection works) 26 | 27 | --- 28 | 29 | ### Method 2: GitHub Personal Access Token (Fallback) 30 | 31 | **Use this if OIDC fails to detect `portel-dev` organization** 32 | 33 | #### Setup Steps 34 | 35 | 1. **Create GitHub PAT**: 36 | - Go to: https://github.com/settings/tokens 37 | - Click: "Tokens (classic)" → "Generate new token (classic)" 38 | - Name: `MCP Registry Publishing` 39 | - Scopes needed: 40 | - ✅ `repo` (Full control of private repositories) 41 | - ✅ `read:org` (Read org and team membership) 42 | - Click "Generate token" 43 | - **Copy the token** (you won't see it again!) 44 | 45 | 2. **Add as GitHub Secret**: 46 | - Go to: https://github.com/portel-dev/ncp/settings/secrets/actions 47 | - Click: "New repository secret" 48 | - Name: `MCP_GITHUB_TOKEN` 49 | - Value: Paste your PAT 50 | - Click: "Add secret" 51 | 52 | 3. **Workflow will automatically use it**: 53 | - Workflow tries OIDC first 54 | - If OIDC fails, falls back to `MCP_GITHUB_TOKEN` 55 | - No code changes needed! 56 | 57 | --- 58 | 59 | ## Verification 60 | 61 | ### Check Organization Membership 62 | 63 | Verify you're an admin of `portel-dev`: 64 | 65 | ```bash 66 | gh api orgs/portel-dev/memberships/$(gh api user -q .login) 67 | ``` 68 | 69 | Expected output: 70 | ```json 71 | { 72 | "role": "admin", 73 | "state": "active" 74 | } 75 | ``` 76 | 77 | ### Check Repository Permissions 78 | 79 | ```bash 80 | gh api repos/portel-dev/ncp/collaborators/$(gh api user -q .login)/permission 81 | ``` 82 | 83 | Expected output: 84 | ```json 85 | { 86 | "permission": "admin", 87 | "role_name": "admin" 88 | } 89 | ``` 90 | 91 | ### Validate server.json 92 | 93 | ```bash 94 | curl -sS https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json -o /tmp/server.schema.json 95 | jsonschema -i server.json /tmp/server.schema.json 96 | ``` 97 | 98 | Should output: (no errors = valid) 99 | 100 | --- 101 | 102 | ## Testing the Workflow 103 | 104 | ### Option 1: Wait for Real Release 105 | 106 | The workflow triggers automatically when you publish a GitHub Release via the Release workflow. 107 | 108 | ### Option 2: Manual Test (Local) 109 | 110 | You can test MCP Publisher authentication locally: 111 | 112 | ```bash 113 | # Download MCP Publisher 114 | VERSION="v1.1.0" 115 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 116 | ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') 117 | 118 | curl -L "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${VERSION#v}_${OS}_${ARCH}.tar.gz" | tar xz 119 | 120 | # Test authentication 121 | ./mcp-publisher login github-oidc # Try OIDC first 122 | 123 | # Or with PAT 124 | export GITHUB_TOKEN="your-pat-here" 125 | echo "$GITHUB_TOKEN" | ./mcp-publisher login github --token-stdin 126 | 127 | # Dry run publish (doesn't actually publish) 128 | ./mcp-publisher publish --dry-run 129 | ``` 130 | 131 | --- 132 | 133 | ## Troubleshooting 134 | 135 | ### "Organization portel-dev not detected" 136 | 137 | **Solution**: Use GitHub PAT (Method 2 above) 138 | 139 | This is a known limitation with GitHub OIDC tokens not always exposing organization membership. 140 | 141 | ### "Authentication failed" 142 | 143 | **Check**: 144 | 1. PAT is valid and not expired 145 | 2. PAT has `repo` and `read:org` scopes 146 | 3. Secret name is exactly `MCP_GITHUB_TOKEN` 147 | 4. You're an admin of `portel-dev` organization 148 | 149 | ### "Invalid server.json" 150 | 151 | **Check**: 152 | - Description is ≤100 characters 153 | - Version format is valid (e.g., `1.4.0`) 154 | - All required fields present 155 | - Run validation: `jsonschema -i server.json /tmp/server.schema.json` 156 | 157 | --- 158 | 159 | ## Security Notes 160 | 161 | ### GitHub PAT Best Practices 162 | 163 | - ✅ Use classic tokens (fine-grained tokens not yet supported by MCP Publisher) 164 | - ✅ Minimum scopes: `repo`, `read:org` 165 | - ✅ Store as GitHub Secret (never commit to code) 166 | - ✅ Rotate token periodically 167 | - ✅ Revoke immediately if compromised 168 | 169 | ### OIDC vs PAT 170 | 171 | | Feature | OIDC | PAT | 172 | |---------|------|-----| 173 | | Security | ⭐⭐⭐⭐⭐ Short-lived | ⭐⭐⭐ Long-lived | 174 | | Setup | Zero config | Requires secret | 175 | | Org Detection | ⚠️ May fail | ✅ Reliable | 176 | | Recommended | If it works | If OIDC fails | 177 | 178 | --- 179 | 180 | ## What Gets Published 181 | 182 | Each release publishes to: 183 | 184 | 1. **NPM**: `@portel/[email protected]` 185 | 2. **MCP Registry**: `io.github.portel-dev/ncp` 186 | 3. **GitHub Releases**: Tagged release 187 | 188 | All automatic via GitHub Actions! 189 | 190 | --- 191 | 192 | ## Support 193 | 194 | If you encounter issues: 195 | 196 | 1. Check GitHub Actions logs 197 | 2. Review this troubleshooting guide 198 | 3. Test authentication locally 199 | 4. Open an issue: https://github.com/portel-dev/ncp/issues 200 | ``` -------------------------------------------------------------------------------- /src/utils/parameter-prompter.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Interactive parameter prompting system 3 | * Guides users through tool parameters with intelligent prompts 4 | */ 5 | import * as readline from 'readline'; 6 | import chalk from 'chalk'; 7 | 8 | export interface ParameterInfo { 9 | name: string; 10 | type: string; 11 | required: boolean; 12 | description?: string; 13 | } 14 | 15 | export class ParameterPrompter { 16 | private rl: readline.Interface; 17 | 18 | constructor() { 19 | this.rl = readline.createInterface({ 20 | input: process.stdin, 21 | output: process.stdout 22 | }); 23 | } 24 | 25 | /** 26 | * Prompt user for all tool parameters interactively 27 | */ 28 | async promptForParameters( 29 | toolName: string, 30 | parameters: ParameterInfo[], 31 | predictor: any, 32 | toolContext: string 33 | ): Promise<any> { 34 | console.log(chalk.blue(`📝 Tool "${toolName}" requires parameters. Let me guide you through them:\n`)); 35 | 36 | const result: any = {}; 37 | 38 | // Sort parameters: required first, then optional 39 | const sortedParams = [...parameters].sort((a, b) => { 40 | if (a.required && !b.required) return -1; 41 | if (!a.required && b.required) return 1; 42 | return 0; 43 | }); 44 | 45 | for (const param of sortedParams) { 46 | const value = await this.promptForParameter(param, predictor, toolContext, toolName); 47 | if (value !== null && value !== undefined && value !== '') { 48 | result[param.name] = this.convertValue(value, param.type); 49 | } 50 | } 51 | 52 | return result; 53 | } 54 | 55 | /** 56 | * Prompt for a single parameter 57 | */ 58 | private async promptForParameter( 59 | param: ParameterInfo, 60 | predictor: any, 61 | toolContext: string, 62 | toolName: string 63 | ): Promise<string | null> { 64 | const icon = param.required ? '📄' : '📔'; 65 | const status = param.required ? 'Required' : 'Optional'; 66 | const typeInfo = chalk.cyan(`(${param.type})`); 67 | 68 | console.log(`${icon} ${chalk.bold(param.name)} ${typeInfo} - ${chalk.yellow(status)}`); 69 | 70 | if (param.description) { 71 | console.log(` ${chalk.gray(param.description)}`); 72 | } 73 | 74 | // Generate intelligent suggestion 75 | const suggestion = predictor.predictValue( 76 | param.name, 77 | param.type, 78 | toolContext, 79 | param.description, 80 | toolName 81 | ); 82 | 83 | let prompt = ' Enter value'; 84 | if (!param.required) { 85 | prompt += ' (press Enter to skip)'; 86 | } 87 | if (suggestion && typeof suggestion === 'string' && suggestion !== 'example') { 88 | prompt += ` [${chalk.green(suggestion)}]`; 89 | } 90 | prompt += ': '; 91 | 92 | const input = await this.question(prompt); 93 | 94 | // If user pressed Enter and we have a suggestion, use it 95 | if (input === '' && suggestion && param.required) { 96 | console.log(` ${chalk.gray(`Using suggested value: ${suggestion}`)}`); 97 | return String(suggestion); 98 | } 99 | 100 | // If optional and empty, skip 101 | if (input === '' && !param.required) { 102 | console.log(` ${chalk.gray('Skipped')}`); 103 | return null; 104 | } 105 | 106 | // If required but empty, use suggestion or ask again 107 | if (input === '' && param.required) { 108 | if (suggestion) { 109 | console.log(` ${chalk.gray(`Using suggested value: ${suggestion}`)}`); 110 | return String(suggestion); 111 | } else { 112 | console.log(chalk.red(' This parameter is required. Please provide a value.')); 113 | return await this.promptForParameter(param, predictor, toolContext, toolName); 114 | } 115 | } 116 | 117 | console.log(); // Add spacing 118 | return input; 119 | } 120 | 121 | /** 122 | * Convert string input to appropriate type 123 | */ 124 | private convertValue(value: string, type: string): any { 125 | if (value === '') return undefined; 126 | 127 | switch (type) { 128 | case 'number': 129 | case 'integer': 130 | const num = Number(value); 131 | return isNaN(num) ? value : num; 132 | 133 | case 'boolean': 134 | const lower = value.toLowerCase(); 135 | if (lower === 'true' || lower === 'yes' || lower === '1') return true; 136 | if (lower === 'false' || lower === 'no' || lower === '0') return false; 137 | return Boolean(value); 138 | 139 | case 'array': 140 | try { 141 | // Try to parse as JSON array first 142 | if (value.startsWith('[')) { 143 | return JSON.parse(value); 144 | } 145 | // Otherwise split by comma 146 | return value.split(',').map(s => s.trim()); 147 | } catch { 148 | return value.split(',').map(s => s.trim()); 149 | } 150 | 151 | case 'object': 152 | try { 153 | return JSON.parse(value); 154 | } catch { 155 | return { value }; 156 | } 157 | 158 | default: 159 | return value; 160 | } 161 | } 162 | 163 | /** 164 | * Prompt user with a question 165 | */ 166 | private question(prompt: string): Promise<string> { 167 | return new Promise((resolve) => { 168 | this.rl.question(prompt, (answer) => { 169 | resolve(answer.trim()); 170 | }); 171 | }); 172 | } 173 | 174 | /** 175 | * Close the readline interface 176 | */ 177 | close(): void { 178 | this.rl.close(); 179 | } 180 | } ``` -------------------------------------------------------------------------------- /src/utils/paths.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * NCP Standardized File System Paths 3 | * 4 | * This file defines all file paths used by NCP components. 5 | * DO NOT hardcode paths elsewhere - always import from here. 6 | * 7 | * Cross-platform directory locations: 8 | * - Windows: %APPDATA%\ncp\ (e.g., C:\Users\Username\AppData\Roaming\ncp\) 9 | * - macOS: ~/Library/Preferences/ncp/ 10 | * - Linux: ~/.config/ncp/ 11 | * 12 | * See NCP_FILE_SYSTEM_ARCHITECTURE.md for complete documentation. 13 | */ 14 | 15 | import * as path from 'path'; 16 | import * as os from 'os'; 17 | import * as fs from 'fs/promises'; 18 | import envPaths from 'env-paths'; 19 | 20 | // Cross-platform user directories using env-paths 21 | const paths = envPaths('ncp'); 22 | 23 | // Base Directories (cross-platform) 24 | export const NCP_BASE_DIR = paths.config; 25 | export const NCP_PROFILES_DIR = path.join(NCP_BASE_DIR, 'profiles'); 26 | export const NCP_CACHE_DIR = path.join(NCP_BASE_DIR, 'cache'); 27 | export const NCP_LOGS_DIR = path.join(NCP_BASE_DIR, 'logs'); 28 | export const NCP_CONFIG_DIR = path.join(NCP_BASE_DIR, 'config'); 29 | export const NCP_TEMP_DIR = path.join(NCP_BASE_DIR, 'temp'); 30 | 31 | // Profile Files 32 | export const PROFILE_ALL = path.join(NCP_PROFILES_DIR, 'all.json'); 33 | export const PROFILE_CLAUDE_DESKTOP = path.join(NCP_PROFILES_DIR, 'claude-desktop.json'); 34 | export const PROFILE_CLAUDE_CODE = path.join(NCP_PROFILES_DIR, 'claude-code.json'); 35 | export const PROFILE_DEV = path.join(NCP_PROFILES_DIR, 'dev.json'); 36 | export const PROFILE_MINIMAL = path.join(NCP_PROFILES_DIR, 'minimal.json'); 37 | 38 | // Cache Files (currently stored in base directory, not cache subdirectory) 39 | export const TOOL_CACHE_FILE = path.join(NCP_BASE_DIR, 'tool-cache.json'); 40 | export const MCP_HEALTH_CACHE = path.join(NCP_BASE_DIR, 'mcp-health.json'); 41 | export const DISCOVERY_INDEX_CACHE = path.join(NCP_CACHE_DIR, 'discovery-index.json'); 42 | 43 | // Profile-specific vector database files 44 | export const EMBEDDINGS_DIR = path.join(NCP_CACHE_DIR, 'embeddings'); 45 | export const EMBEDDINGS_METADATA_DIR = path.join(NCP_CACHE_DIR, 'metadata'); 46 | 47 | // Log Files 48 | export const MAIN_LOG_FILE = path.join(NCP_LOGS_DIR, 'ncp.log'); 49 | export const MCP_LOG_FILE = path.join(NCP_LOGS_DIR, 'mcp-connections.log'); 50 | export const DISCOVERY_LOG_FILE = path.join(NCP_LOGS_DIR, 'discovery.log'); 51 | 52 | // Config Files 53 | export const GLOBAL_SETTINGS = path.join(NCP_CONFIG_DIR, 'settings.json'); 54 | 55 | // Client-specific configs 56 | export const CLIENT_CONFIGS_DIR = path.join(NCP_CONFIG_DIR, 'client-configs'); 57 | export const CLAUDE_DESKTOP_CONFIG = path.join(CLIENT_CONFIGS_DIR, 'claude-desktop.json'); 58 | export const CLAUDE_CODE_CONFIG = path.join(CLIENT_CONFIGS_DIR, 'claude-code.json'); 59 | 60 | // Temporary directories 61 | export const MCP_PROBES_TEMP = path.join(NCP_TEMP_DIR, 'mcp-probes'); 62 | export const INSTALLATION_TEMP = path.join(NCP_TEMP_DIR, 'installation'); 63 | 64 | /** 65 | * Ensures all NCP directories exist 66 | * MUST be called on startup by all NCP components 67 | */ 68 | export async function ensureNCPDirectories(): Promise<void> { 69 | const directories = [ 70 | NCP_BASE_DIR, 71 | NCP_PROFILES_DIR, 72 | NCP_CACHE_DIR, 73 | NCP_LOGS_DIR, 74 | NCP_CONFIG_DIR, 75 | NCP_TEMP_DIR, 76 | CLIENT_CONFIGS_DIR, 77 | MCP_PROBES_TEMP, 78 | INSTALLATION_TEMP, 79 | EMBEDDINGS_DIR, 80 | EMBEDDINGS_METADATA_DIR 81 | ]; 82 | 83 | for (const dir of directories) { 84 | try { 85 | await fs.mkdir(dir, { recursive: true }); 86 | } catch (error) { 87 | console.error(`Failed to create directory ${dir}:`, error); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Gets profile path by name 94 | * @param profileName - Name of the profile (without .json extension) 95 | * @returns Full path to profile file 96 | */ 97 | export function getProfilePath(profileName: string): string { 98 | return path.join(NCP_PROFILES_DIR, `${profileName}.json`); 99 | } 100 | 101 | /** 102 | * Migration utility: Move file if it exists at old location 103 | * @param oldPath - Current file location 104 | * @param newPath - New standardized location 105 | */ 106 | export async function migrateFile(oldPath: string, newPath: string): Promise<boolean> { 107 | try { 108 | // Check if old file exists 109 | await fs.access(oldPath); 110 | 111 | // Ensure new directory exists 112 | await fs.mkdir(path.dirname(newPath), { recursive: true }); 113 | 114 | // Move file 115 | await fs.rename(oldPath, newPath); 116 | console.log(`Migrated: ${oldPath} → ${newPath}`); 117 | return true; 118 | } catch (error) { 119 | // File doesn't exist at old location, that's fine 120 | return false; 121 | } 122 | } 123 | 124 | /** 125 | * Migrate all files from old locations to standardized locations 126 | * Should be called once during upgrade 127 | */ 128 | export async function migrateAllFiles(): Promise<void> { 129 | console.log('Starting NCP file migration...'); 130 | 131 | // Migrate tool cache 132 | await migrateFile('.tool-cache.json', TOOL_CACHE_FILE); 133 | await migrateFile('tool-cache.json', TOOL_CACHE_FILE); 134 | 135 | // Migrate old profile files 136 | await migrateFile('.ncp/profiles/default.json', PROFILE_ALL); 137 | await migrateFile('.ncp/profiles/development.json', PROFILE_DEV); 138 | 139 | console.log('NCP file migration complete'); 140 | } ``` -------------------------------------------------------------------------------- /test/mock-mcps/filesystem-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Mock Filesystem MCP Server 5 | * Real MCP server structure for file system operations testing 6 | */ 7 | 8 | import { MockMCPServer } from './base-mock-server.js'; 9 | 10 | const serverInfo = { 11 | name: 'filesystem-test', 12 | version: '1.0.0', 13 | description: 'Local file system operations including reading, writing, directory management, and permissions' 14 | }; 15 | 16 | const tools = [ 17 | { 18 | name: 'read_file', 19 | description: 'Read contents of files from local filesystem. Load configuration files, read text documents, access data files.', 20 | inputSchema: { 21 | type: 'object', 22 | properties: { 23 | path: { 24 | type: 'string', 25 | description: 'File path to read' 26 | }, 27 | encoding: { 28 | type: 'string', 29 | description: 'Text encoding (utf8, ascii, etc.)' 30 | } 31 | }, 32 | required: ['path'] 33 | } 34 | }, 35 | { 36 | name: 'write_file', 37 | description: 'Write content to files on local filesystem. Create configuration files, save data, generate reports.', 38 | inputSchema: { 39 | type: 'object', 40 | properties: { 41 | path: { 42 | type: 'string', 43 | description: 'File path to write to' 44 | }, 45 | content: { 46 | type: 'string', 47 | description: 'Content to write to file' 48 | }, 49 | encoding: { 50 | type: 'string', 51 | description: 'Text encoding (utf8, ascii, etc.)' 52 | }, 53 | create_dirs: { 54 | type: 'boolean', 55 | description: 'Create parent directories if they do not exist' 56 | } 57 | }, 58 | required: ['path', 'content'] 59 | } 60 | }, 61 | { 62 | name: 'create_directory', 63 | description: 'Create new directories and folder structures. Organize files, set up project structure, create folder hierarchies.', 64 | inputSchema: { 65 | type: 'object', 66 | properties: { 67 | path: { 68 | type: 'string', 69 | description: 'Directory path to create' 70 | }, 71 | recursive: { 72 | type: 'boolean', 73 | description: 'Create parent directories if needed' 74 | }, 75 | mode: { 76 | type: 'string', 77 | description: 'Directory permissions (octal notation)' 78 | } 79 | }, 80 | required: ['path'] 81 | } 82 | }, 83 | { 84 | name: 'list_directory', 85 | description: 'List files and directories with filtering and sorting options. Browse folders, find files, explore directory structure.', 86 | inputSchema: { 87 | type: 'object', 88 | properties: { 89 | path: { 90 | type: 'string', 91 | description: 'Directory path to list' 92 | }, 93 | recursive: { 94 | type: 'boolean', 95 | description: 'Include subdirectories recursively' 96 | }, 97 | include_hidden: { 98 | type: 'boolean', 99 | description: 'Include hidden files and directories' 100 | }, 101 | pattern: { 102 | type: 'string', 103 | description: 'Glob pattern to filter files' 104 | } 105 | }, 106 | required: ['path'] 107 | } 108 | }, 109 | { 110 | name: 'delete_file', 111 | description: 'Delete files and directories from filesystem. Remove old files, clean up temporary data, delete folders.', 112 | inputSchema: { 113 | type: 'object', 114 | properties: { 115 | path: { 116 | type: 'string', 117 | description: 'File or directory path to delete' 118 | }, 119 | recursive: { 120 | type: 'boolean', 121 | description: 'Delete directories and contents recursively' 122 | }, 123 | force: { 124 | type: 'boolean', 125 | description: 'Force deletion without confirmation' 126 | } 127 | }, 128 | required: ['path'] 129 | } 130 | }, 131 | { 132 | name: 'copy_file', 133 | description: 'Copy files and directories to new locations. Backup files, duplicate data, organize content.', 134 | inputSchema: { 135 | type: 'object', 136 | properties: { 137 | source: { 138 | type: 'string', 139 | description: 'Source file or directory path' 140 | }, 141 | destination: { 142 | type: 'string', 143 | description: 'Destination path for copy' 144 | }, 145 | overwrite: { 146 | type: 'boolean', 147 | description: 'Overwrite destination if it exists' 148 | }, 149 | preserve_attributes: { 150 | type: 'boolean', 151 | description: 'Preserve file timestamps and permissions' 152 | } 153 | }, 154 | required: ['source', 'destination'] 155 | } 156 | }, 157 | { 158 | name: 'get_file_info', 159 | description: 'Get detailed information about files and directories. Check file size, modification time, permissions.', 160 | inputSchema: { 161 | type: 'object', 162 | properties: { 163 | path: { 164 | type: 'string', 165 | description: 'File or directory path' 166 | }, 167 | follow_symlinks: { 168 | type: 'boolean', 169 | description: 'Follow symbolic links' 170 | } 171 | }, 172 | required: ['path'] 173 | } 174 | } 175 | ]; 176 | 177 | // Create and run the server 178 | const server = new MockMCPServer(serverInfo, tools); 179 | server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /test/final-coverage-push.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Final Coverage Push - Simple targeted tests to reach 80% coverage 3 | * Focus on easy wins and edge cases 4 | */ 5 | 6 | import { describe, it, expect } from '@jest/globals'; 7 | import { DiscoveryEngine } from '../src/discovery/engine.js'; 8 | import { PersistentRAGEngine } from '../src/discovery/rag-engine.js'; 9 | 10 | describe('Final Coverage Push - Simple Tests', () => { 11 | describe('Discovery Engine Pattern Extraction', () => { 12 | it('should extract patterns from complex tool descriptions', async () => { 13 | const engine = new DiscoveryEngine(); 14 | 15 | // Test with tool that has complex description patterns 16 | await engine.indexTool({ 17 | id: 'complex:multi-operation-tool', 18 | name: 'multi-operation-tool', 19 | description: 'Create, read, update and delete multiple files in directory while executing commands and validating operations' 20 | }); 21 | 22 | // Should extract multiple verb-object patterns 23 | const stats = engine.getStats(); 24 | expect(stats.totalPatterns).toBeGreaterThan(5); 25 | }); 26 | 27 | it('should handle pattern extraction edge cases', async () => { 28 | const engine = new DiscoveryEngine(); 29 | 30 | // Test with empty and problematic descriptions 31 | const problematicTools = [ 32 | { 33 | id: 'empty:desc', 34 | name: 'empty-desc', 35 | description: '' 36 | }, 37 | { 38 | id: 'special:chars', 39 | name: 'special-chars', 40 | description: 'Tool with "quoted text" and (parentheses) and symbols @#$%' 41 | }, 42 | { 43 | id: 'long:name-with-many-parts', 44 | name: 'very-long-tool-name-with-many-hyphenated-parts', 45 | description: 'Normal description' 46 | } 47 | ]; 48 | 49 | for (const tool of problematicTools) { 50 | await engine.indexTool(tool); 51 | } 52 | 53 | // Should handle all cases without errors 54 | const stats = engine.getStats(); 55 | expect(stats.totalTools).toBe(3); 56 | }); 57 | 58 | it('should test findRelatedTools similarity calculation', async () => { 59 | const engine = new DiscoveryEngine(); 60 | 61 | // Index multiple related tools 62 | const tools = [ 63 | { 64 | id: 'fileops:read', 65 | name: 'fileops:read', 66 | description: 'Read file content from filesystem' 67 | }, 68 | { 69 | id: 'fileops:write', 70 | name: 'fileops:write', 71 | description: 'Write file content to filesystem' 72 | }, 73 | { 74 | id: 'mathops:calculate', 75 | name: 'mathops:calculate', 76 | description: 'Perform mathematical calculations' 77 | } 78 | ]; 79 | 80 | for (const tool of tools) { 81 | await engine.indexTool(tool); 82 | } 83 | 84 | // Find related tools for the read operation 85 | const related = await engine.findRelatedTools('fileops:read'); 86 | 87 | expect(related.length).toBeGreaterThan(0); 88 | // Should find write operation as related (similar description) 89 | const writeRelated = related.find(r => r.id === 'fileops:write'); 90 | expect(writeRelated).toBeTruthy(); 91 | expect(writeRelated?.similarity).toBeGreaterThan(0.3); 92 | }); 93 | }); 94 | 95 | describe('RAG Engine Basic Operations', () => { 96 | it('should handle minimal tool indexing', async () => { 97 | const ragEngine = new PersistentRAGEngine(); 98 | await ragEngine.initialize(); 99 | 100 | // Index tool with minimal description 101 | await ragEngine.indexMCP('minimal-server', [ 102 | { 103 | id: 'minimal:tool', 104 | name: 'tool', 105 | description: 'x', // Very short description 106 | inputSchema: {} 107 | } 108 | ]); 109 | 110 | // Test discovery with empty/minimal query 111 | const results = await ragEngine.discover('', 5); 112 | expect(Array.isArray(results)).toBe(true); 113 | }); 114 | 115 | it('should handle cache operations', async () => { 116 | const ragEngine = new PersistentRAGEngine(); 117 | await ragEngine.initialize(); 118 | 119 | // Index some tools 120 | await ragEngine.indexMCP('test-server', [ 121 | { 122 | id: 'test:refresh-tool', 123 | name: 'refresh-tool', 124 | description: 'Tool for testing cache refresh operations', 125 | inputSchema: {} 126 | } 127 | ]); 128 | 129 | // Test cache refresh 130 | await ragEngine.refreshCache(); 131 | 132 | // Test cache clear 133 | await ragEngine.clearCache(); 134 | 135 | // Should still work after clear 136 | const results = await ragEngine.discover('refresh', 1); 137 | expect(Array.isArray(results)).toBe(true); 138 | }); 139 | 140 | it('should handle domain classification', async () => { 141 | const ragEngine = new PersistentRAGEngine(); 142 | await ragEngine.initialize(); 143 | 144 | // Test with query that contains multiple domain indicators 145 | await ragEngine.indexMCP('multi-domain', [ 146 | { 147 | id: 'multi:payment-file-web', 148 | name: 'payment-file-web', 149 | description: 'Process payment files on web server database', 150 | inputSchema: {} 151 | } 152 | ]); 153 | 154 | const results = await ragEngine.discover('payment web database file', 3); 155 | expect(results.length).toBeGreaterThanOrEqual(0); 156 | }); 157 | }); 158 | }); ``` -------------------------------------------------------------------------------- /src/services/tool-finder.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Shared service for tool discovery and search 3 | * Handles pagination, filtering, and result organization 4 | */ 5 | 6 | import { NCPOrchestrator } from '../orchestrator/ncp-orchestrator.js'; 7 | 8 | export interface FindOptions { 9 | query?: string; 10 | page?: number; 11 | limit?: number; 12 | depth?: number; 13 | mcpFilter?: string | null; 14 | } 15 | 16 | export interface PaginationInfo { 17 | page: number; 18 | totalPages: number; 19 | totalResults: number; 20 | startIndex: number; 21 | endIndex: number; 22 | resultsInPage: number; 23 | } 24 | 25 | export interface GroupedTool { 26 | toolName: string; 27 | confidence: number; 28 | description?: string; 29 | schema?: any; 30 | } 31 | 32 | export interface FindResult { 33 | tools: any[]; 34 | groupedByMCP: Record<string, GroupedTool[]>; 35 | pagination: PaginationInfo; 36 | mcpFilter: string | null; 37 | isListing: boolean; 38 | query: string; 39 | } 40 | 41 | export class ToolFinder { 42 | constructor(private orchestrator: NCPOrchestrator) {} 43 | 44 | /** 45 | * Main search method with all features 46 | */ 47 | async find(options: FindOptions = {}): Promise<FindResult> { 48 | const { 49 | query = '', 50 | page = 1, 51 | limit = query ? 5 : 20, 52 | depth = 2, 53 | mcpFilter = null 54 | } = options; 55 | 56 | // Detect MCP-specific search if not explicitly provided 57 | const detectedMCPFilter = mcpFilter || this.detectMCPFilter(query); 58 | 59 | // Adjust search query based on MCP filter 60 | const searchQuery = detectedMCPFilter ? '' : query; 61 | 62 | // Get results with proper confidence-based ordering from orchestrator 63 | // Request enough for pagination but not excessive amounts 64 | const searchLimit = Math.min(1000, (page * limit) + 50); // Get enough for current page + buffer 65 | const allResults = await this.orchestrator.find(searchQuery, searchLimit, depth >= 1); 66 | 67 | // Apply MCP filtering if detected 68 | const filteredResults = detectedMCPFilter ? 69 | allResults.filter(r => r.mcpName.toLowerCase() === detectedMCPFilter.toLowerCase()) : 70 | allResults; 71 | 72 | // Results are already sorted by confidence from orchestrator - maintain that order 73 | // Calculate pagination 74 | const pagination = this.calculatePagination(filteredResults.length, page, limit); 75 | 76 | // Get page results while preserving confidence-based order 77 | const pageResults = filteredResults.slice(pagination.startIndex, pagination.endIndex); 78 | 79 | // Group by MCP 80 | const groupedByMCP = this.groupByMCP(pageResults); 81 | 82 | return { 83 | tools: pageResults, 84 | groupedByMCP, 85 | pagination, 86 | mcpFilter: detectedMCPFilter, 87 | isListing: !query || query.trim() === '', 88 | query 89 | }; 90 | } 91 | 92 | /** 93 | * Calculate pagination details 94 | */ 95 | private calculatePagination(totalResults: number, page: number, limit: number): PaginationInfo { 96 | const totalPages = Math.ceil(totalResults / limit); 97 | const safePage = Math.max(1, Math.min(page, totalPages || 1)); 98 | const startIndex = (safePage - 1) * limit; 99 | const endIndex = Math.min(startIndex + limit, totalResults); 100 | 101 | return { 102 | page: safePage, 103 | totalPages, 104 | totalResults, 105 | startIndex, 106 | endIndex, 107 | resultsInPage: endIndex - startIndex 108 | }; 109 | } 110 | 111 | /** 112 | * Group tools by their MCP 113 | */ 114 | private groupByMCP(results: any[]): Record<string, GroupedTool[]> { 115 | const groups: Record<string, GroupedTool[]> = {}; 116 | 117 | results.forEach(result => { 118 | if (!groups[result.mcpName]) { 119 | groups[result.mcpName] = []; 120 | } 121 | groups[result.mcpName].push({ 122 | toolName: result.toolName, 123 | confidence: result.confidence, 124 | description: result.description, 125 | schema: result.schema 126 | }); 127 | }); 128 | 129 | return groups; 130 | } 131 | 132 | /** 133 | * Detect if query is an MCP-specific search 134 | */ 135 | private detectMCPFilter(query: string): string | null { 136 | if (!query) return null; 137 | 138 | const lowerQuery = query.toLowerCase().trim(); 139 | 140 | // Common MCP names to check 141 | const knownMCPs = [ 142 | 'filesystem', 'memory', 'shell', 'portel', 'tavily', 143 | 'desktop-commander', 'stripe', 'sequential-thinking', 144 | 'context7-mcp', 'github', 'gitlab', 'slack' 145 | ]; 146 | 147 | // Check for exact MCP name match 148 | for (const mcp of knownMCPs) { 149 | if (lowerQuery === mcp || lowerQuery === `${mcp}:`) { 150 | return mcp; 151 | } 152 | } 153 | 154 | // Check if query starts with MCP:tool pattern 155 | if (lowerQuery.includes(':')) { 156 | const [potentialMCP] = lowerQuery.split(':'); 157 | if (knownMCPs.includes(potentialMCP)) { 158 | return potentialMCP; 159 | } 160 | } 161 | 162 | return null; 163 | } 164 | 165 | /** 166 | * Get sample tools when no results found 167 | */ 168 | async getSampleTools(count: number = 8): Promise<{ mcpName: string; description: string }[]> { 169 | const sampleTools = await this.orchestrator.find('', count); 170 | const mcpSet = new Set<string>(); 171 | const samples: { mcpName: string; description: string }[] = []; 172 | 173 | for (const tool of sampleTools) { 174 | if (!mcpSet.has(tool.mcpName)) { 175 | mcpSet.add(tool.mcpName); 176 | samples.push({ 177 | mcpName: tool.mcpName, 178 | description: tool.mcpName // TODO: Get from MCP server info 179 | }); 180 | } 181 | } 182 | 183 | return samples; 184 | } 185 | 186 | } ``` -------------------------------------------------------------------------------- /test/orchestrator-simple-branches.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Simple Orchestrator Branch Coverage Tests 3 | * Target key uncovered branches without complex mocking 4 | */ 5 | 6 | import { describe, it, expect, beforeEach, jest } from '@jest/globals'; 7 | import { NCPOrchestrator } from '../src/orchestrator/ncp-orchestrator'; 8 | import * as fs from 'fs'; 9 | 10 | jest.mock('fs'); 11 | 12 | describe('Orchestrator Simple Branch Tests', () => { 13 | let orchestrator: NCPOrchestrator; 14 | const mockFs = fs as jest.Mocked<typeof fs>; 15 | 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | orchestrator = new NCPOrchestrator('test'); 19 | mockFs.existsSync.mockReturnValue(false); 20 | }); 21 | 22 | describe('Error Path Coverage', () => { 23 | it('should handle MCP not configured error', async () => { 24 | // Set up minimal profile 25 | const emptyProfile = { name: 'test', mcpServers: {} }; 26 | mockFs.existsSync.mockReturnValue(true); 27 | mockFs.readFileSync.mockReturnValue(JSON.stringify(emptyProfile) as any); 28 | 29 | await orchestrator.initialize(); 30 | 31 | // Try to run tool from unconfigured MCP - should trigger line 419 32 | const result = await orchestrator.run('nonexistent:tool', {}); 33 | 34 | expect(result.success).toBe(false); 35 | expect(result.error).toContain('not found'); 36 | }); 37 | 38 | it('should handle initialization with no profile', async () => { 39 | // Profile file doesn't exist 40 | mockFs.existsSync.mockReturnValue(false); 41 | 42 | // Should not throw 43 | await expect(orchestrator.initialize()).resolves.not.toThrow(); 44 | }); 45 | 46 | it('should handle find with empty query', async () => { 47 | const profile = { name: 'test', mcpServers: {} }; 48 | mockFs.existsSync.mockReturnValue(true); 49 | mockFs.readFileSync.mockReturnValue(JSON.stringify(profile) as any); 50 | 51 | await orchestrator.initialize(); 52 | 53 | // Empty query should return empty results (line 276-277) 54 | const results = await orchestrator.find('', 5); 55 | expect(Array.isArray(results)).toBe(true); 56 | }); 57 | 58 | it('should handle getAllResources with no MCPs', async () => { 59 | const profile = { name: 'test', mcpServers: {} }; 60 | mockFs.existsSync.mockReturnValue(true); 61 | mockFs.readFileSync.mockReturnValue(JSON.stringify(profile) as any); 62 | 63 | await orchestrator.initialize(); 64 | 65 | // Should return empty array 66 | const resources = await orchestrator.getAllResources(); 67 | expect(Array.isArray(resources)).toBe(true); 68 | expect(resources).toEqual([]); 69 | }); 70 | 71 | it('should handle tool execution with invalid format', async () => { 72 | const profile = { name: 'test', mcpServers: {} }; 73 | mockFs.existsSync.mockReturnValue(true); 74 | mockFs.readFileSync.mockReturnValue(JSON.stringify(profile) as any); 75 | 76 | await orchestrator.initialize(); 77 | 78 | // Should handle invalid tool format 79 | const result = await orchestrator.run('invalid-format', {}); 80 | expect(result.success).toBe(false); 81 | }); 82 | 83 | it('should handle getAllPrompts with no MCPs', async () => { 84 | const profile = { name: 'test', mcpServers: {} }; 85 | mockFs.existsSync.mockReturnValue(true); 86 | mockFs.readFileSync.mockReturnValue(JSON.stringify(profile) as any); 87 | 88 | await orchestrator.initialize(); 89 | 90 | // Should return empty array 91 | const prompts = await orchestrator.getAllPrompts(); 92 | expect(Array.isArray(prompts)).toBe(true); 93 | expect(prompts).toEqual([]); 94 | }); 95 | 96 | it('should handle multiple initialization calls safely', async () => { 97 | const profile = { name: 'test', mcpServers: {} }; 98 | mockFs.existsSync.mockReturnValue(true); 99 | mockFs.readFileSync.mockReturnValue(JSON.stringify(profile) as any); 100 | 101 | // Multiple calls should be safe 102 | await orchestrator.initialize(); 103 | await orchestrator.initialize(); 104 | await orchestrator.initialize(); 105 | 106 | // Should not throw 107 | expect(true).toBe(true); 108 | }); 109 | 110 | it('should handle cleanup gracefully', async () => { 111 | // Should not throw even without initialization 112 | await expect(orchestrator.cleanup()).resolves.not.toThrow(); 113 | }); 114 | }); 115 | 116 | describe('Cache Edge Cases', () => { 117 | it('should handle corrupted cache file', async () => { 118 | const profile = { name: 'test', mcpServers: {} }; 119 | 120 | // Profile exists but cache is corrupted 121 | mockFs.existsSync.mockImplementation((path: any) => { 122 | return path.toString().includes('.json'); 123 | }); 124 | 125 | mockFs.readFileSync.mockImplementation((path: any) => { 126 | if (path.toString().includes('profiles')) { 127 | return JSON.stringify(profile) as any; 128 | } else { 129 | return 'corrupted cache data' as any; 130 | } 131 | }); 132 | 133 | // Should handle corrupted cache gracefully 134 | await expect(orchestrator.initialize()).resolves.not.toThrow(); 135 | }); 136 | 137 | it('should handle cache save failure', async () => { 138 | const profile = { name: 'test', mcpServers: {} }; 139 | mockFs.existsSync.mockReturnValue(true); 140 | mockFs.readFileSync.mockReturnValue(JSON.stringify(profile) as any); 141 | 142 | // Mock writeFileSync to throw 143 | mockFs.writeFileSync.mockImplementation(() => { 144 | throw new Error('Write failed'); 145 | }); 146 | 147 | // Should handle write failure gracefully 148 | await expect(orchestrator.initialize()).resolves.not.toThrow(); 149 | }); 150 | }); 151 | }); ``` -------------------------------------------------------------------------------- /src/utils/update-checker.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Update Checker for NCP 3 | * Checks for new versions and notifies users 4 | */ 5 | 6 | import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; 7 | import { join } from 'path'; 8 | import { homedir } from 'os'; 9 | import chalk from 'chalk'; 10 | import { version as packageVersion, packageName } from './version.js'; 11 | 12 | interface UpdateCheckResult { 13 | hasUpdate: boolean; 14 | currentVersion: string; 15 | latestVersion?: string; 16 | updateAvailable?: boolean; 17 | } 18 | 19 | interface UpdateCache { 20 | lastCheck: number; 21 | latestVersion: string; 22 | notificationShown: boolean; 23 | } 24 | 25 | export class UpdateChecker { 26 | private packageVersion: string; 27 | private packageName: string; 28 | private cacheFile: string; 29 | private readonly checkInterval = 24 * 60 * 60 * 1000; // 24 hours 30 | 31 | constructor() { 32 | // Use package info from module-level constants (read from package.json) 33 | this.packageName = packageName; 34 | this.packageVersion = packageVersion; 35 | 36 | // Cache file location 37 | const ncpDir = join(homedir(), '.ncp'); 38 | if (!existsSync(ncpDir)) { 39 | mkdirSync(ncpDir, { recursive: true }); 40 | } 41 | this.cacheFile = join(ncpDir, 'update-cache.json'); 42 | } 43 | 44 | private loadCache(): UpdateCache | null { 45 | try { 46 | if (!existsSync(this.cacheFile)) { 47 | return null; 48 | } 49 | return JSON.parse(readFileSync(this.cacheFile, 'utf8')); 50 | } catch { 51 | return null; 52 | } 53 | } 54 | 55 | private saveCache(cache: UpdateCache): void { 56 | try { 57 | writeFileSync(this.cacheFile, JSON.stringify(cache, null, 2)); 58 | } catch { 59 | // Ignore cache write errors 60 | } 61 | } 62 | 63 | private async fetchLatestVersion(): Promise<string | null> { 64 | try { 65 | // Add timeout to prevent hanging 66 | const controller = new AbortController(); 67 | const timeout = setTimeout(() => controller.abort(), 3000); // 3 second timeout 68 | 69 | const response = await fetch(`https://registry.npmjs.org/${this.packageName}/latest`, { 70 | signal: controller.signal 71 | }); 72 | 73 | clearTimeout(timeout); 74 | 75 | if (!response.ok) { 76 | return null; 77 | } 78 | const data = await response.json(); 79 | return data.version || null; 80 | } catch { 81 | return null; 82 | } 83 | } 84 | 85 | private compareVersions(current: string, latest: string): boolean { 86 | // Simple semantic version comparison 87 | const parseVersion = (v: string) => v.split('.').map(num => parseInt(num, 10)); 88 | const currentParts = parseVersion(current); 89 | const latestParts = parseVersion(latest); 90 | 91 | for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { 92 | const currentPart = currentParts[i] || 0; 93 | const latestPart = latestParts[i] || 0; 94 | 95 | if (latestPart > currentPart) return true; 96 | if (latestPart < currentPart) return false; 97 | } 98 | return false; 99 | } 100 | 101 | async checkForUpdates(forceCheck = false): Promise<UpdateCheckResult> { 102 | const cache = this.loadCache(); 103 | const now = Date.now(); 104 | 105 | // Check if we need to fetch (force check or cache expired) 106 | const shouldCheck = forceCheck || 107 | !cache || 108 | (now - cache.lastCheck) > this.checkInterval; 109 | 110 | let latestVersion = cache?.latestVersion; 111 | 112 | if (shouldCheck) { 113 | const fetchedVersion = await this.fetchLatestVersion(); 114 | if (fetchedVersion) { 115 | latestVersion = fetchedVersion; 116 | 117 | // Save to cache 118 | this.saveCache({ 119 | lastCheck: now, 120 | latestVersion: fetchedVersion, 121 | notificationShown: false 122 | }); 123 | } 124 | } 125 | 126 | const hasUpdate = latestVersion ? this.compareVersions(this.packageVersion, latestVersion) : false; 127 | 128 | return { 129 | hasUpdate, 130 | currentVersion: this.packageVersion, 131 | latestVersion, 132 | updateAvailable: hasUpdate 133 | }; 134 | } 135 | 136 | async showUpdateNotification(): Promise<void> { 137 | const cache = this.loadCache(); 138 | if (cache?.notificationShown) { 139 | return; // Already shown for this version 140 | } 141 | 142 | const result = await this.checkForUpdates(); 143 | if (result.hasUpdate && result.latestVersion) { 144 | console.log(); 145 | console.log(chalk.yellow('📦 Update Available!')); 146 | console.log(chalk.dim(` Current: ${result.currentVersion}`)); 147 | console.log(chalk.green(` Latest: ${result.latestVersion}`)); 148 | console.log(); 149 | console.log(chalk.cyan(' Run: npm install -g @portel/ncp@latest')); 150 | console.log(chalk.dim(' Or: ncp update')); 151 | console.log(); 152 | 153 | // Mark notification as shown 154 | if (cache) { 155 | cache.notificationShown = true; 156 | this.saveCache(cache); 157 | } 158 | } 159 | } 160 | 161 | async performUpdate(): Promise<boolean> { 162 | try { 163 | const { spawn } = await import('child_process'); 164 | 165 | console.log(chalk.blue('🔄 Updating NCP...')); 166 | 167 | return new Promise((resolve) => { 168 | const updateProcess = spawn('npm', ['install', '-g', '@portel/ncp@latest'], { 169 | stdio: 'inherit' 170 | }); 171 | 172 | updateProcess.on('close', (code) => { 173 | if (code === 0) { 174 | console.log(chalk.green('✅ NCP updated successfully!')); 175 | console.log(chalk.dim(' Restart your terminal or run: source ~/.bashrc')); 176 | resolve(true); 177 | } else { 178 | console.log(chalk.red('❌ Update failed. Please try manually:')); 179 | console.log(chalk.dim(' npm install -g @portel/ncp@latest')); 180 | resolve(false); 181 | } 182 | }); 183 | }); 184 | } catch (error) { 185 | console.log(chalk.red('❌ Update failed:'), error); 186 | return false; 187 | } 188 | } 189 | } ```