This is page 1 of 9. Use http://codebase.md/portel-dev/ncp?page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .dxtignore ├── .github │ ├── FEATURE_STORY_TEMPLATE.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── mcp_server_request.yml │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── publish-mcp-registry.yml │ └── release.yml ├── .gitignore ├── .mcpbignore ├── .npmignore ├── .release-it.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── COMPLETE-IMPLEMENTATION-SUMMARY.md ├── CONTRIBUTING.md ├── CRITICAL-ISSUES-FOUND.md ├── docs │ ├── clients │ │ ├── claude-desktop.md │ │ ├── cline.md │ │ ├── continue.md │ │ ├── cursor.md │ │ ├── perplexity.md │ │ └── README.md │ ├── download-stats.md │ ├── guides │ │ ├── clipboard-security-pattern.md │ │ ├── how-it-works.md │ │ ├── mcp-prompts-for-user-interaction.md │ │ ├── mcpb-installation.md │ │ ├── ncp-registry-command.md │ │ ├── pre-release-checklist.md │ │ ├── telemetry-design.md │ │ └── testing.md │ ├── images │ │ ├── ncp-add.png │ │ ├── ncp-find.png │ │ ├── ncp-help.png │ │ ├── ncp-import.png │ │ ├── ncp-list.png │ │ └── ncp-transformation-flow.png │ ├── mcp-registry-setup.md │ ├── pr-schema-additions.ts │ └── stories │ ├── 01-dream-and-discover.md │ ├── 02-secrets-in-plain-sight.md │ ├── 03-sync-and-forget.md │ ├── 04-double-click-install.md │ ├── 05-runtime-detective.md │ └── 06-official-registry.md ├── DYNAMIC-RUNTIME-SUMMARY.md ├── EXTENSION-CONFIG-DISCOVERY.md ├── INSTALL-EXTENSION.md ├── INTERNAL-MCP-ARCHITECTURE.md ├── jest.config.js ├── LICENSE ├── MANAGEMENT-TOOLS-COMPLETE.md ├── manifest.json ├── manifest.json.backup ├── MCP-CONFIG-SCHEMA-IMPLEMENTATION-EXAMPLE.ts ├── MCP-CONFIG-SCHEMA-SIMPLE-EXAMPLE.json ├── MCP-CONFIGURATION-SCHEMA-FORMAT.json ├── MCPB-ARCHITECTURE-DECISION.md ├── NCP-EXTENSION-COMPLETE.md ├── package-lock.json ├── package.json ├── parity-between-cli-and-mcp.txt ├── PROMPTS-IMPLEMENTATION.md ├── README-COMPARISON.md ├── README.md ├── README.new.md ├── REGISTRY-INTEGRATION-COMPLETE.md ├── RELEASE-PROCESS-IMPROVEMENTS.md ├── RELEASE-SUMMARY.md ├── RELEASE.md ├── RUNTIME-DETECTION-COMPLETE.md ├── scripts │ ├── cleanup │ │ └── scan-repository.js │ └── sync-server-version.cjs ├── SECURITY.md ├── server.json ├── src │ ├── analytics │ │ ├── analytics-formatter.ts │ │ ├── log-parser.ts │ │ └── visual-formatter.ts │ ├── auth │ │ ├── oauth-device-flow.ts │ │ └── token-store.ts │ ├── cache │ │ ├── cache-patcher.ts │ │ ├── csv-cache.ts │ │ └── schema-cache.ts │ ├── cli │ │ └── index.ts │ ├── discovery │ │ ├── engine.ts │ │ ├── mcp-domain-analyzer.ts │ │ ├── rag-engine.ts │ │ ├── search-enhancer.ts │ │ └── semantic-enhancement-engine.ts │ ├── extension │ │ └── extension-init.ts │ ├── index-mcp.ts │ ├── index.ts │ ├── internal-mcps │ │ ├── internal-mcp-manager.ts │ │ ├── ncp-management.ts │ │ └── types.ts │ ├── orchestrator │ │ └── ncp-orchestrator.ts │ ├── profiles │ │ └── profile-manager.ts │ ├── server │ │ ├── mcp-prompts.ts │ │ └── mcp-server.ts │ ├── services │ │ ├── config-prompter.ts │ │ ├── config-schema-reader.ts │ │ ├── error-handler.ts │ │ ├── output-formatter.ts │ │ ├── registry-client.ts │ │ ├── tool-context-resolver.ts │ │ ├── tool-finder.ts │ │ ├── tool-schema-parser.ts │ │ └── usage-tips-generator.ts │ ├── testing │ │ ├── create-real-mcp-definitions.ts │ │ ├── dummy-mcp-server.ts │ │ ├── mcp-definitions.json │ │ ├── real-mcp-analyzer.ts │ │ ├── real-mcp-definitions.json │ │ ├── real-mcps.csv │ │ ├── setup-dummy-mcps.ts │ │ ├── setup-tiered-profiles.ts │ │ ├── test-profile.json │ │ ├── test-semantic-enhancement.ts │ │ └── verify-profile-scaling.ts │ ├── transports │ │ └── filtered-stdio-transport.ts │ └── utils │ ├── claude-desktop-importer.ts │ ├── client-importer.ts │ ├── client-registry.ts │ ├── config-manager.ts │ ├── health-monitor.ts │ ├── highlighting.ts │ ├── logger.ts │ ├── markdown-renderer.ts │ ├── mcp-error-parser.ts │ ├── mcp-wrapper.ts │ ├── ncp-paths.ts │ ├── parameter-prompter.ts │ ├── paths.ts │ ├── progress-spinner.ts │ ├── response-formatter.ts │ ├── runtime-detector.ts │ ├── schema-examples.ts │ ├── security.ts │ ├── text-utils.ts │ ├── update-checker.ts │ ├── updater.ts │ └── version.ts ├── STORY-DRIVEN-DOCUMENTATION.md ├── STORY-FIRST-WORKFLOW.md ├── test │ ├── __mocks__ │ │ ├── chalk.js │ │ ├── transformers.js │ │ ├── updater.js │ │ └── version.ts │ ├── cache-loading-focused.test.ts │ ├── cache-optimization.test.ts │ ├── cli-help-validation.sh │ ├── coverage-boost.test.ts │ ├── curated-ecosystem-validation.test.ts │ ├── discovery-engine.test.ts │ ├── discovery-fallback-focused.test.ts │ ├── ecosystem-discovery-focused.test.ts │ ├── ecosystem-discovery-validation-simple.test.ts │ ├── final-80-percent-push.test.ts │ ├── final-coverage-push.test.ts │ ├── health-integration.test.ts │ ├── health-monitor.test.ts │ ├── helpers │ │ └── mock-server-manager.ts │ ├── integration │ │ └── mcp-client-simulation.test.cjs │ ├── logger.test.ts │ ├── mcp-ecosystem-discovery.test.ts │ ├── mcp-error-parser.test.ts │ ├── mcp-immediate-response-check.js │ ├── mcp-server-protocol.test.ts │ ├── mcp-timeout-scenarios.test.ts │ ├── mcp-wrapper.test.ts │ ├── mock-mcps │ │ ├── aws-server.js │ │ ├── base-mock-server.mjs │ │ ├── brave-search-server.js │ │ ├── docker-server.js │ │ ├── filesystem-server.js │ │ ├── git-server.mjs │ │ ├── github-server.js │ │ ├── neo4j-server.js │ │ ├── notion-server.js │ │ ├── playwright-server.js │ │ ├── postgres-server.js │ │ ├── shell-server.js │ │ ├── slack-server.js │ │ └── stripe-server.js │ ├── mock-smithery-mcp │ │ ├── index.js │ │ ├── package.json │ │ └── smithery.yaml │ ├── ncp-orchestrator.test.ts │ ├── orchestrator-health-integration.test.ts │ ├── orchestrator-simple-branches.test.ts │ ├── performance-benchmark.test.ts │ ├── quick-coverage.test.ts │ ├── rag-engine.test.ts │ ├── regression-snapshot.test.ts │ ├── search-enhancer.test.ts │ ├── session-id-passthrough.test.ts │ ├── setup.ts │ ├── tool-context-resolver.test.ts │ ├── tool-schema-parser.test.ts │ ├── user-story-discovery.test.ts │ └── version-util.test.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` # Development files node_modules .git .env *.log .DS_Store # Build output (will be generated during build) dist # NCP runtime files .ncp/cache .ncp/embeddings.json .ncp/embeddings-metadata.json # Testing and development test/ tests/ *.test.js *.test.ts coverage/ ``` -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- ```json { "git": { "commitMessage": "release: ${version}", "tagName": "${version}" }, "github": { "release": true, "releaseName": "Release ${version}" }, "npm": { "publish": true }, "plugins": { "@release-it/conventional-changelog": { "preset": "conventionalcommits", "infile": "CHANGELOG.md" } } } ``` -------------------------------------------------------------------------------- /.dxtignore: -------------------------------------------------------------------------------- ``` # Development files .git/ .github/ coverage/ test/ docs/ scripts/ # Cache and user data .ncp/ *.cache *.tmp # Source files (only need compiled dist/) src/ *.ts tsconfig.json jest.config.js # CLI-specific code (MCP-only bundle uses index-mcp.js) dist/cli/ dist/index.js dist/index.js.map # Documentation *.md !manifest.json # Build artifacts *.tgz *.dxt *.backup # IDE .vscode/ .idea/ *.swp # Development configs .eslintrc* .prettierrc* .travis.yml .dockerignore # Other unnecessary files CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md LICENSE .release-it.json .npmignore ``` -------------------------------------------------------------------------------- /.mcpbignore: -------------------------------------------------------------------------------- ``` # Development files .git/ .github/ coverage/ test/ docs/ scripts/ # Cache and user data .ncp/ *.cache *.tmp # Source files (only need compiled dist/) src/ *.ts tsconfig.json jest.config.js # CLI-specific code (MCP-only bundle uses index-mcp.js) dist/cli/ dist/index.js dist/index.js.map # Documentation *.md !manifest.json # Build artifacts *.tgz *.dxt *.backup # IDE .vscode/ .idea/ *.swp # Development configs .eslintrc* .prettierrc* .travis.yml .dockerignore # Other unnecessary files CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md LICENSE .release-it.json .npmignore ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` # Source code and development files src/ test/ scripts/ coverage/ docs/ .github/ *.ts !dist/**/*.d.ts # Build artifacts that shouldn't be published *.map *.tsbuildinfo # Configuration files .gitignore .npmignore tsconfig.json jest.config.js .release-it.json .editorconfig .prettierrc* .eslintrc* # Documentation (keep only README.md and LICENSE) CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md SECURITY.md RELEASE.md *.prd.md # Development and testing artifacts *.test.js *.spec.js *.test.ts *.spec.ts **/testing/ **/__tests__/ **/__mocks__/ # Local and temporary files .env* *.local.* *.tmp *.temp *.backup.* .DS_Store *.log # Sensitive files and credentials .mcpregistry_* *_token *_secret *.key *.pem # Local runtime data .ncp/ *.cache # Uncomment if you don't need TypeScript support # dist/**/*.d.ts # Example and reference files (not needed for runtime) MCP-CONFIG-*.ts MCP-CONFIG-*.json MCP-CONFIGURATION-*.json *-EXAMPLE.* # IDE files .vscode/ .idea/ *.swp *.swo # Images and media (not needed for runtime) *.png *.jpg *.jpeg *.gif *.svg # Specific files not needed in package .dockerignore ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ dist/ coverage/ # Build artifacts *.mcpb *.dxt *.tgz # Local configuration .env .env.local *.local.* .claude.local.md # Sensitive tokens and credentials .mcpregistry_* *_token *_secret *.key *.pem # Cache and temp files .ncp/ *.cache *.tmp *.temp *.backup.* # AI and draft content *.prd.md *.draft.md *.ai.md *.notes.md *.temp.md # Session documentation (move to commercial repo) SESSION-*.md IMPLEMENTATION-*.md CLEANUP-*.md TEST-PLAN.md TESTING-*.md # Test and script files *.test.js *.script.js !jest.config.js !test/**/*.test.ts # IDE .idea/ .vscode/ *.swp *.swo *~ .DS_Store # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Scripts directory (development files not for release) scripts/ !scripts/*.cjs !scripts/cleanup/ # Ignore image files in project root (temporary screenshots) /*.png /*.jpg /*.jpeg /*.gif # Keep documentation images !docs/images/** # User runtime data (created when users run NCP) .ncp/ ~/.ncp/ # Development artifacts (use ncp-dev-workspace instead) /profiles/ /ecosystem-repo-setup/ test-*.js experiment-*.js *-ecosystem.json # Date-prefixed AI exports 2025-*.txt ``` -------------------------------------------------------------------------------- /docs/clients/README.md: -------------------------------------------------------------------------------- ```markdown # NCP Installation Guides by Client Choose your MCP client below for detailed installation instructions. --- ## 🎯 Supported Clients ### 🖥️ [Claude Desktop](./claude-desktop.md) **Installation Methods:** Extension (.dxt) + JSON Config **Features:** - ✅ One-click extension installation - ✅ Auto-import of existing MCPs - ✅ Auto-sync on every startup - ✅ Both CLI and extension modes supported **Best for:** Most users, production use [→ Read Claude Desktop Guide](./claude-desktop.md) --- ### 🔍 [Perplexity](./perplexity.md) **Installation Methods:** JSON Config only **Features:** - ✅ Manual JSON configuration - ⚠️ No auto-import yet (coming soon) - ⚠️ .dxt extension support coming soon **Best for:** Perplexity Mac app users [→ Read Perplexity Guide](./perplexity.md) --- ### 💻 [Cursor IDE](./cursor.md) **Installation Methods:** JSON Config only **Features:** - ✅ JSON configuration via Cline settings - ✅ Works with Cursor's AI features - ✅ Standard MCP integration **Best for:** Cursor IDE users [→ Read Cursor Guide](./cursor.md) --- ### 🔧 [Cline (VS Code)](./cline.md) **Installation Methods:** JSON Config only **Features:** - ✅ VS Code extension integration - ✅ JSON configuration - ✅ Works with Claude API **Best for:** VS Code + Cline users [→ Read Cline Guide](./cline.md) --- ### ⚡ [Continue (VS Code)](./continue.md) **Installation Methods:** JSON Config only **Features:** - ✅ VS Code extension integration - ✅ Nested experimental config format - ✅ Works with multiple AI models **Best for:** VS Code + Continue users [→ Read Continue Guide](./continue.md) --- ## 🆚 Installation Method Comparison | Client | Extension (.dxt) | JSON Config | Auto-Import | |--------|-----------------|-------------|-------------| | **Claude Desktop** | ✅ Recommended | ✅ Available | ✅ Yes | | **Perplexity** | ⏳ Coming Soon | ✅ Available | ⏳ Coming Soon | | **Cursor** | ❌ Not Supported | ✅ Available | ❌ No | | **Cline** | ❌ Not Supported | ✅ Available | ❌ No | | **Continue** | ❌ Not Supported | ✅ Available | ❌ No | --- ## 📋 Quick Start by Client ### Claude Desktop (Recommended) ```bash # Download and drag-drop ncp.dxt # OR use JSON config: npm install -g @portel/ncp ncp config import # Edit claude_desktop_config.json to use NCP ``` ### All Other Clients ```bash # 1. Install NCP npm install -g @portel/ncp # 2. Add your MCPs ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ncp add github npx @modelcontextprotocol/server-github # 3. Configure your client's JSON config # (See client-specific guide for config file location) # 4. Restart your client ``` --- ## 🎯 Which Installation Method Should I Use? ### Use Extension (.dxt) Installation If: - ✅ You're using Claude Desktop - ✅ You want one-click installation - ✅ You want automatic MCP detection - ✅ You want auto-sync on startup ### Use JSON Configuration If: - ✅ Your client doesn't support .dxt yet - ✅ You prefer manual control - ✅ You need custom profile setups - ✅ You're testing or developing --- ## 🔮 Future Support Clients we're tracking for future .dxt support: - 🔜 **Perplexity** - Testing .dxt drag-and-drop - 🔜 **Cursor** - Investigating extension support - 🔜 **Windsurf** - Monitoring for MCP support - 🔜 **Zed** - Awaiting official MCP integration Want to see NCP support for another client? [Open a feature request](https://github.com/portel-dev/ncp/issues/new?template=feature_request.yml) --- ## 🤝 Contributing Client Guides Found an issue or want to improve a guide? 1. **Report issues:** [GitHub Issues](https://github.com/portel-dev/ncp/issues) 2. **Suggest improvements:** [GitHub Discussions](https://github.com/portel-dev/ncp/discussions) 3. **Submit PR:** [Contributing Guide](../../CONTRIBUTING.md) --- ## 📚 Additional Resources - **[Main README](../../README.md)** - Overview and features - **[How It Works](../guides/how-it-works.md)** - Technical architecture - **[Testing Guide](../guides/testing.md)** - Verification steps - **[Troubleshooting](../../README.md#-troubleshooting)** - Common issues --- ## 📍 Quick Links ### Configuration File Locations **Claude Desktop:** - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` - Linux: `~/.config/Claude/claude_desktop_config.json` **Cursor/Cline:** - macOS: `~/Library/Application Support/[Client]/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - Windows: `%APPDATA%/[Client]/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - Linux: `~/.config/[Client]/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` **Continue:** - All platforms: `~/.continue/config.json` **Perplexity:** - macOS: `~/Library/Containers/ai.perplexity.mac/Data/Documents/mcp_servers` **NCP Profiles:** - All platforms: `~/.ncp/profiles/` --- **Questions?** Ask in [GitHub Discussions](https://github.com/portel-dev/ncp/discussions) ``` -------------------------------------------------------------------------------- /README.new.md: -------------------------------------------------------------------------------- ```markdown # NCP - Your AI's Personal Assistant [](https://www.npmjs.com/package/@portel/ncp) [](https://www.npmjs.com/package/@portel/ncp) [](https://github.com/portel-dev/ncp/releases) [](https://www.elastic.co/licensing/elastic-license) [](https://modelcontextprotocol.io/) <!-- mcp-name: io.github.portel-dev/ncp --> --- ## 🎯 **One Line That Changes Everything** **Your AI doesn't see your 50 tools. It dreams of the perfect tool, and NCP finds it instantly.** That's it. That's NCP. --- ## 😫 **The Problem** You installed 10 MCPs to supercharge your AI. Instead: - **AI becomes indecisive** ("Should I use `read_file` or `get_file_content`?") - **Conversations end early** (50 tool schemas = 100k+ tokens before work starts) - **Wrong tools picked** (AI confused by similar-sounding options) - **Computer works harder** (all MCPs running constantly, most idle) **The paradox:** More tools = Less productivity. > **What's MCP?** The [Model Context Protocol](https://modelcontextprotocol.io) by Anthropic lets AI assistants connect to external tools. Think of MCPs as "plugins" that give your AI superpowers. --- ## ✨ **The Solution: Six Stories** Every NCP feature solves a real problem. Here's how: ### **[🌟 Story 1: Dream and Discover](docs/stories/01-dream-and-discover.md)** *2 min* > **Problem:** AI overwhelmed by 50+ tool schemas > **Solution:** AI writes what it needs, NCP finds the perfect tool > **Result:** 97% fewer tokens, 5x faster, AI becomes decisive ### **[🔐 Story 2: Secrets in Plain Sight](docs/stories/02-secrets-in-plain-sight.md)** *2 min* > **Problem:** API keys exposed in AI chat logs forever > **Solution:** Clipboard handshake keeps secrets server-side > **Result:** AI never sees your tokens, full security + convenience ### **[🔄 Story 3: Sync and Forget](docs/stories/03-sync-and-forget.md)** *2 min* > **Problem:** Configure same MCPs twice (Claude Desktop + NCP) > **Solution:** NCP auto-syncs from Claude Desktop on every startup > **Result:** Zero manual configuration, always in sync ### **[📦 Story 4: Double-Click Install](docs/stories/04-double-click-install.md)** *2 min* > **Problem:** Installing MCPs requires terminal, npm, JSON editing > **Solution:** Download .mcpb → Double-click → Done > **Result:** 30-second install, feels like native app ### **[🕵️ Story 5: Runtime Detective](docs/stories/05-runtime-detective.md)** *2 min* > **Problem:** MCPs break when Claude Desktop runtime changes > **Solution:** NCP detects runtime dynamically on every boot > **Result:** Adapts automatically, no version mismatches ### **[🌐 Story 6: Official Registry](docs/stories/06-official-registry.md)** *2 min* > **Problem:** Finding right MCP takes hours of Googling > **Solution:** AI searches 2,200+ MCPs from official registry > **Result:** Discovery through conversation, install in seconds **Read all six stories: 12 minutes total.** You'll understand exactly why NCP transforms how you work with MCPs. --- ## 🚀 **Quick Start** ### **Option 1: Claude Desktop Users** (Recommended) 1. Download [ncp.mcpb](https://github.com/portel-dev/ncp/releases/latest/download/ncp.mcpb) 2. Double-click the file 3. Click "Install" when Claude Desktop prompts 4. **Done!** NCP auto-syncs all your Claude Desktop MCPs **Time:** 30 seconds | **Difficulty:** Zero | **Story:** [Double-Click Install →](docs/stories/04-double-click-install.md) --- ### **Option 2: All Other Clients** (Cursor, Cline, Continue, etc.) ```bash # 1. Install NCP npm install -g @portel/ncp # 2. Import existing MCPs (copy your config to clipboard first) ncp config import # 3. Configure your AI client { "mcpServers": { "ncp": { "command": "ncp" } } } ``` **Time:** 2 minutes | **Difficulty:** Copy-paste | **Full Guide:** [Installation →](#installation) --- ## 📊 **The Difference (Numbers)** | Your MCP Setup | Without NCP | With NCP | Improvement | |----------------|-------------|----------|-------------| | **Tokens used** | 100,000+ (tool schemas) | 2,500 (2 tools) | **97% saved** | | **AI response time** | 8 seconds (analyzing) | <1 second (instant) | **8x faster** | | **Wrong tool selection** | 30% of attempts | <3% of attempts | **10x accuracy** | | **Conversation length** | 50 messages (context limit) | 600+ messages | **12x longer** | | **Computer CPU usage** | High (all MCPs running) | Low (on-demand loading) | **~70% saved** | **Real measurements from production usage.** Your mileage may vary, but the pattern holds: NCP makes AI faster, smarter, cheaper. --- ## 📚 **Learn More** ### **For Users:** - 🎯 **[The Six Stories](docs/stories/)** - Understand NCP through narratives (12 min) - 🔧 **[Installation Guide](#installation-full-guide)** - Detailed setup for all platforms - 🧪 **[Test Drive](#test-drive)** - Try NCP CLI to see what AI experiences - 🛟 **[Troubleshooting](#troubleshooting)** - Fix common issues ### **For Developers:** - 📖 **[How It Works](HOW-IT-WORKS.md)** - Technical deep dive - 🏗️ **[Architecture](STORY-DRIVEN-DOCUMENTATION.md)** - System design and stories - 🤝 **[Contributing](CONTRIBUTING.md)** - Help make NCP better - 📝 **[Feature Stories](.github/FEATURE_STORY_TEMPLATE.md)** - Propose new features ### **For Teams:** - 🚀 **[Project-Level Config](#project-level-configuration)** - Per-project MCPs - 👥 **[Team Workflows](docs/stories/03-sync-and-forget.md)** - Consistent setup - 🔐 **[Security Pattern](docs/stories/02-secrets-in-plain-sight.md)** - Safe credential handling --- ## 🎓 **What People Say** > "NCP does not expose any tools to AI. Instead, it lets the AI dream of a tool and come up with a user story for that tool. With that story, it is able to discover the tool and use it right away." > > *— The story that started it all* > "Installing MCPs used to take 45 minutes and require terminal knowledge. Now it's 30 seconds and a double-click." > > *— Beta tester feedback on .mcpb installation* > "My AI went from 'let me think about which tool to use...' to just doing the task immediately. The difference is night and day." > > *— User report on token reduction* --- ## 💡 **Philosophy** NCP is built on one core insight: **Constraints spark creativity. Infinite options paralyze.** - A poet given "write about anything" → Writer's block - A poet given "write a haiku about rain" → Instant inspiration **Your AI is no different.** Give it 50 tools → Analysis paralysis, wrong choices, exhaustion Give it a way to dream → Focused thinking, fast decisions, confident action **NCP provides the constraint (semantic search) that unlocks the superpower (any tool, on demand).** --- # 📖 **Full Documentation** ## Installation (Full Guide) ### **Prerequisites** - **Node.js 18+** ([Download](https://nodejs.org/)) - **npm** (included with Node.js) or **npx** - **Terminal access** (Mac/Linux: Terminal, Windows: PowerShell) ### **Method 1: .mcpb Bundle** (Claude Desktop Only) **Best for:** Claude Desktop users who want zero configuration **Steps:** 1. **Download:** [ncp.mcpb](https://github.com/portel-dev/ncp/releases/latest/download/ncp.mcpb) from latest release 2. **Install:** Double-click the downloaded file 3. **Confirm:** Click "Install" in Claude Desktop prompt 4. **Done:** NCP auto-syncs all your existing MCPs on startup **Features:** - ✅ Continuous auto-sync from Claude Desktop ([Story 3](docs/stories/03-sync-and-forget.md)) - ✅ Dynamic runtime detection ([Story 5](docs/stories/05-runtime-detective.md)) - ✅ Optional global CLI (toggle in settings) - ✅ Tiny bundle size (126KB, MCP-only) **Manual configuration** (optional): ```bash # Edit profile to add more MCPs nano ~/.ncp/profiles/all.json ``` **Read more:** [Story 4: Double-Click Install →](docs/stories/04-double-click-install.md) --- ### **Method 2: npm Package** (All Clients) **Best for:** Cursor, Cline, Continue, VS Code, CLI-heavy workflows **Steps:** ```bash # 1. Install NCP globally npm install -g @portel/ncp # 2. Import existing MCPs from clipboard # (Copy your claude_desktop_config.json content first) ncp config import # 3. Or add MCPs manually ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ncp add github npx @modelcontextprotocol/server-github # 4. Configure your AI client # Add to config file (location varies by client): { "mcpServers": { "ncp": { "command": "ncp" } } } # 5. Restart your AI client ``` **Client-specific config locations:** - **Claude Desktop:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Cursor:** (See [Cursor docs](https://cursor.sh/docs)) - **Cline/Continue:** (See respective docs) - **VS Code:** `~/Library/Application Support/Code/User/settings.json` **Alternative: npx** (no global install) ```bash # Replace 'ncp' with 'npx @portel/ncp' in all commands npx @portel/ncp config import npx @portel/ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents # Client config: { "mcpServers": { "ncp": { "command": "npx", "args": ["@portel/ncp"] } } } ``` --- ## Test Drive Experience what your AI experiences with NCP's CLI: ### **Smart Discovery** ```bash # Ask like a human, not a programmer ncp find "I need to read a file" ncp find "help me send an email" ncp find "search for something online" ``` **Notice:** NCP understands intent, not just keywords. ### **Ecosystem Overview** ```bash # See all MCPs and their tools ncp list --depth 2 # Check MCP health ncp list --depth 1 # Get help ncp --help ``` ### **Direct Testing** ```bash # Test tool execution safely ncp run filesystem:read_file --params '{"path": "/tmp/test.txt"}' --dry-run # Run for real ncp run filesystem:read_file --params '{"path": "/tmp/test.txt"}' ``` ### **Verify Installation** ```bash # 1. Check version ncp --version # 2. List imported MCPs ncp list # 3. Test discovery ncp find "file" # 4. Validate config ncp config validate ``` **Success indicators:** - ✅ `ncp --version` shows version number - ✅ `ncp list` shows your imported MCPs - ✅ `ncp find` returns relevant tools - ✅ Your AI client shows only NCP in tool list (2 tools) --- ## Project-Level Configuration **New:** Configure MCPs per project for team consistency and Cloud IDE compatibility. ```bash # In any project directory mkdir .ncp # Add project-specific MCPs ncp add filesystem npx @modelcontextprotocol/server-filesystem ./ ncp add github npx @modelcontextprotocol/server-github # NCP automatically uses .ncp/ if it exists, otherwise falls back to ~/.ncp/ ``` **Perfect for:** - 🤖 Claude Code projects (project-specific tooling) - 👥 Team consistency (ship `.ncp/` folder with repo) - 🔧 Project-specific needs (frontend vs backend MCPs) - 📦 Environment isolation (no global conflicts) **Example:** ``` frontend-app/ .ncp/profiles/all.json # → playwright, lighthouse, browser-context src/ api-backend/ .ncp/profiles/all.json # → postgres, redis, docker, kubernetes server/ ``` --- ## Advanced Features ### **Multi-Profile Organization** Organize MCPs by environment or project: ```bash # Development profile ncp add --profile dev filesystem npx @modelcontextprotocol/server-filesystem ~/dev # Production profile ncp add --profile prod database npx production-db-server # Use specific profile ncp --profile dev find "file tools" # Configure AI client with profile { "mcpServers": { "ncp": { "command": "ncp", "args": ["--profile", "dev"] } } } ``` ### **Registry Discovery** Search and install MCPs from official registry through AI: ``` You: "Find database MCPs" AI: [Shows numbered list from registry] 1. ⭐ PostgreSQL (Official, 1,240 downloads) 2. ⭐ SQLite (Official, 890 downloads) ... You: "Install 1 and 2" AI: [Installs PostgreSQL and SQLite] ✅ Done! ``` **Read more:** [Story 6: Official Registry →](docs/stories/06-official-registry.md) ### **Secure Credential Configuration** Configure API keys without exposing them to AI chat: ``` You: "Add GitHub MCP" AI: [Shows prompt] "Copy your config to clipboard BEFORE clicking YES: {"env":{"GITHUB_TOKEN":"your_token"}}" [You copy config to clipboard] [You click YES] AI: "MCP added with credentials from clipboard" [Your token never entered the conversation!] ``` **Read more:** [Story 2: Secrets in Plain Sight →](docs/stories/02-secrets-in-plain-sight.md) --- ## Troubleshooting ### **Import Issues** ```bash # Check what was imported ncp list # Validate config health ncp config validate # See detailed logs DEBUG=ncp:* ncp config import ``` ### **AI Not Using Tools** 1. **Verify NCP is running:** `ncp list` (should show your MCPs) 2. **Test discovery:** `ncp find "file"` (should return results) 3. **Check AI config:** Ensure config points to `ncp` command 4. **Restart AI client** after config changes ### **Performance Issues** ```bash # Check MCP health (unhealthy MCPs slow everything) ncp list --depth 1 # Clear cache if needed rm -rf ~/.ncp/cache # Monitor with debug logs DEBUG=ncp:* ncp find "test" ``` ### **Version Detection Issues** ```bash # If ncp -v shows wrong version: which ncp # Check which ncp is being used npm list -g @portel/ncp # Verify installed version # Reinstall if needed npm uninstall -g @portel/ncp npm install -g @portel/ncp ``` --- ## Popular MCPs ### **AI Reasoning & Memory** ```bash ncp add sequential-thinking npx @modelcontextprotocol/server-sequential-thinking ncp add memory npx @modelcontextprotocol/server-memory ``` ### **Development Tools** ```bash ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/code ncp add github npx @modelcontextprotocol/server-github ncp add shell npx @modelcontextprotocol/server-shell ``` ### **Productivity & Integrations** ```bash ncp add brave-search npx @modelcontextprotocol/server-brave-search ncp add gmail npx @mcptools/gmail-mcp ncp add slack npx @modelcontextprotocol/server-slack ncp add postgres npx @modelcontextprotocol/server-postgres ``` **Discover 2,200+ more:** [Smithery.ai](https://smithery.ai) | [mcp.so](https://mcp.so) | [Official Registry](https://registry.modelcontextprotocol.io/) --- ## Contributing Help make NCP even better: - 🐛 **Bug reports:** [GitHub Issues](https://github.com/portel-dev/ncp/issues) - 💡 **Feature ideas:** [GitHub Discussions](https://github.com/portel-dev/ncp/discussions) - 📖 **Documentation:** Improve stories or technical docs - 🔄 **Pull requests:** [Contributing Guide](CONTRIBUTING.md) - 🎯 **Feature stories:** Use [our template](.github/FEATURE_STORY_TEMPLATE.md) **We use story-first development:** Every feature starts as a story (pain → journey → magic) before we write code. [Learn more →](STORY-FIRST-WORKFLOW.md) --- ## License **Elastic License 2.0** - [Full License](LICENSE) **TL;DR:** Free for all use including commercial. Cannot be offered as a hosted service to third parties. --- ## Learn More - 🌟 **[The Six Stories](docs/stories/)** - Why NCP is different (12 min) - 📖 **[How It Works](HOW-IT-WORKS.md)** - Technical deep dive - 🏗️ **[Story-Driven Docs](STORY-DRIVEN-DOCUMENTATION.md)** - Our approach - 📝 **[Story-First Workflow](STORY-FIRST-WORKFLOW.md)** - How we build features - 🔐 **[Security](SECURITY.md)** - Security policy and reporting - 📜 **[Changelog](CHANGELOG.md)** - Version history --- **Built with ❤️ by the NCP Team | Star us on [GitHub](https://github.com/portel-dev/ncp)** ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown [](https://www.npmjs.com/package/@portel/ncp) [](https://www.npmjs.com/package/@portel/ncp) [](https://github.com/portel-dev/ncp/releases) [](https://github.com/portel-dev/ncp/releases/latest) [](https://www.elastic.co/licensing/elastic-license) [](https://modelcontextprotocol.io/) <!-- mcp-name: io.github.portel-dev/ncp --> # NCP - Natural Context Provider ## 🎯 **One MCP to Rule Them All**  **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. 🚀 **NEW:** Project-level configuration - each project can define its own MCPs automatically **Result:** Same tools, same capabilities, but your AI becomes **focused**, **efficient**, and **cost-effective** again. > **What's MCP?** The [Model Context Protocol](https://modelcontextprotocol.io) by Anthropic lets AI assistants connect to external tools and data sources. Think of MCPs as "plugins" that give your AI superpowers like file access, web search, databases, and more. --- ## 😤 **The MCP Paradox: More Tools = Less Productivity** You added MCPs to make your AI more powerful. Instead: - **AI picks wrong tools** ("Should I use `read_file` or `get_file_content`?") - **Sessions end early** ("I've reached my context limit analyzing tools") - **Costs explode** (50+ schemas burn tokens before work even starts) - **AI becomes indecisive** (used to act fast, now asks clarifying questions) --- ## 🧸 **Why Too Many Toys Break the Fun** Think about it: **A child with one toy** → Treasures it, masters it, creates endless games with it **A child with 50 toys** → Can't hold them all, loses pieces, gets overwhelmed, stops playing entirely **Your AI is that child.** MCPs are the toys. More isn't always better. Or picture this: You're **craving pizza**. Someone hands you a pizza → Pure joy! 🍕 But take you to a **buffet with 200 dishes** → Analysis paralysis. You spend 20 minutes deciding, lose your appetite, leave unsatisfied. **Same with your AI:** Give it one perfect tool → Instant action. Give it 50 tools → Cognitive overload. The most creative people thrive with **constraints**, not infinite options. Your AI is no different. **Think about it:** - A poet with "write about anything" → Writer's block - A poet with "write a haiku about rain" → Instant inspiration - A developer with access to "all programming languages" → Analysis paralysis - A developer with "Python for this task" → Focused solution **Your AI needs the same focus.** NCP gives it constraints that spark creativity, not chaos that kills it. --- ## 📊 **The Before & After Reality** ### **Before NCP: Tool Schema Explosion** 😵💫 When your AI connects to multiple MCPs directly: ``` 🤖 AI Assistant Context: ├── Filesystem MCP (12 tools) ─ 15,000 tokens ├── Database MCP (8 tools) ─── 12,000 tokens ├── Web Search MCP (6 tools) ── 8,000 tokens ├── Email MCP (15 tools) ───── 18,000 tokens ├── Shell MCP (10 tools) ───── 14,000 tokens ├── GitHub MCP (20 tools) ──── 25,000 tokens └── Slack MCP (9 tools) ────── 11,000 tokens 💀 Total: 80 tools = 103,000 tokens of schemas ``` **What happens:** - AI burns 50%+ of context just understanding what tools exist - Spends 5-8 seconds analyzing which tool to use - Often picks wrong tool due to schema confusion - Hits context limits mid-conversation ### **After NCP: Unified Intelligence** ✨ With NCP's orchestration: ``` 🤖 AI Assistant Context: └── NCP (2 unified tools) ──── 2,500 tokens 🎯 Behind the scenes: NCP manages all 80 tools 📈 Context saved: 100,500 tokens (97% reduction!) ⚡ Decision time: Sub-second tool selection 🎪 AI behavior: Confident, focused, decisive ``` **Real results from our testing:** | Your MCP Setup | Without NCP | With NCP | Token Savings | |----------------|-------------|----------|---------------| | **Small** (5 MCPs, 25 tools) | 15,000 tokens | 8,000 tokens | **47% saved** | | **Medium** (15 MCPs, 75 tools) | 45,000 tokens | 12,000 tokens | **73% saved** | | **Large** (30 MCPs, 150 tools) | 90,000 tokens | 15,000 tokens | **83% saved** | | **Enterprise** (50+ MCPs, 250+ tools) | 150,000 tokens | 20,000 tokens | **87% saved** | **Translation:** - **5x faster responses** (8 seconds → 1.5 seconds) - **12x longer conversations** before hitting limits - **90% reduction** in wrong tool selection - **Zero context exhaustion** in typical sessions --- ## 📋 **Prerequisites** - **Node.js 18+** ([Download here](https://nodejs.org/)) - **npm** (included with Node.js) or **npx** for running packages - **Command line access** (Terminal on Mac/Linux, Command Prompt/PowerShell on Windows) ## 🚀 **Installation** Choose your preferred installation method: | Method | Best For | Downloads | |--------|----------|-----------| | **📦 .mcpb Bundle** | Claude Desktop users |  | | **📥 npm Package** | All MCP clients, CLI users |  | ### **⚡ Option 1: One-Click Installation (.mcpb)** - Claude Desktop Only **For Claude Desktop users** - Download and double-click to install: 1. **Download NCP Desktop Extension:** [ncp.dxt](https://github.com/portel-dev/ncp/releases/latest/download/ncp.dxt) 2. **Double-click** the downloaded `ncp.dxt` file 3. **Claude Desktop** will prompt you to install - click "Install" 4. **Auto-sync with Claude Desktop** - NCP continuously syncs MCPs: - Detects MCPs from `claude_desktop_config.json` - Detects .mcpb-installed extensions - **Runs on every startup** to find new MCPs - Uses internal `add` command for cache coherence > 🔄 **Continuous sync:** NCP automatically detects and imports new MCPs every time you start it! Add an MCP to Claude Desktop → NCP auto-syncs it on next startup. Zero manual configuration needed. If you want to add more MCPs later, **configure manually** by editing `~/.ncp/profiles/all.json`: ```bash # Edit the profile configuration nano ~/.ncp/profiles/all.json ``` ```json { "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/yourname"] }, "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx" } } } } ``` 5. **Restart Claude Desktop** and NCP will load your configured MCPs > ℹ️ **About .dxt (Desktop Extension) installation:** > - **Slim & Fast:** Desktop extension is MCP-only (126KB, no CLI code) > - **Manual config:** Edit JSON files directly (no `ncp add` command) > - **Power users:** Fastest startup, direct control over configuration > - **Optional CLI:** Install `npm install -g @portel/ncp` separately if you want CLI tools > > **Why .dxt is slim:** > The desktop extension excludes all CLI code, making it 13% smaller and faster to load than the full npm package. Perfect for production use where you manage configs manually or via automation. --- ### **🔧 Option 2: npm Installation** - All MCP Clients (Cursor, Cline, Continue, etc.) ### **Step 1: Import Your Existing MCPs** ⚡ Already have MCPs? Don't start over - import everything instantly: ```bash # Install NCP globally (recommended) npm install -g @portel/ncp # Copy your claude_desktop_config.json content to clipboard: # 1. Open your claude_desktop_config.json file (see locations above) # 2. Select all content (Ctrl+A / Cmd+A) and copy (Ctrl+C / Cmd+C) # 3. Then run: ncp config import # ✨ Magic! NCP auto-detects and imports ALL your MCPs from clipboard ``` > **Note:** All commands below assume global installation (`npm install -g`). For npx usage, see the [Alternative Installation](#alternative-installation-with-npx) section.  ### **Step 2: Connect NCP to Your AI** 🔗 Replace your entire MCP configuration with this **single entry**: ```json { "mcpServers": { "ncp": { "command": "ncp" } } } ``` ### **Step 3: Watch the Magic** ✨ Your AI now sees just 2 simple tools instead of 50+ complex ones:  **🎉 Done!** Same tools, same capabilities, but your AI is now **focused** and **efficient**. --- ## 🧪 **Test Drive: See the Difference Yourself** Want to experience what your AI experiences? NCP has a human-friendly CLI: ### **🔍 Smart Discovery** ```bash # Ask like your AI would ask: ncp find "I need to read a file" ncp find "help me send an email" ncp find "search for something online" ```  **Notice:** NCP understands intent, not just keywords. Just like your AI needs. ### **📋 Ecosystem Overview** ```bash # See your complete MCP ecosystem: ncp list --depth 2 # Get help anytime: ncp --help ```  ### **⚡ Direct Testing** ```bash # Test any tool safely: ncp run filesystem:read_file --params '{"path": "/tmp/test.txt"}' ``` **Why this matters:** You can debug and test tools directly, just like your AI would use them. ### **✅ Verify Everything Works** ```bash # 1. Check NCP is installed correctly ncp --version # 2. Confirm your MCPs are imported ncp list # 3. Test tool discovery ncp find "file" # 4. Test a simple tool (if you have filesystem MCP) ncp run filesystem:read_file --params '{"path": "/tmp/test.txt"}' --dry-run ``` **✅ Success indicators:** - NCP shows version number - `ncp list` shows your imported MCPs - `ncp find` returns relevant tools - Your AI client shows only NCP in its tool list --- ## 🔄 **Alternative Installation with npx** Prefer not to install globally? Use `npx` for any client configuration: ```bash # All the above commands work with npx - just replace 'ncp' with 'npx @portel/ncp': # Import MCPs npx @portel/ncp config import # Add MCPs npx @portel/ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents # Find tools npx @portel/ncp find "file operations" # Configure client (example: Claude Desktop) { "mcpServers": { "ncp": { "command": "npx", "args": ["@portel/ncp"] } } } ``` > **When to use npx:** Perfect for trying NCP, CI/CD environments, or when you can't install packages globally. --- ## 💡 **Why NCP Transforms Your AI Experience** ### **🧠 Restores AI Focus** - **Before:** "I see 50 tools... which should I use... let me think..." - **After:** "I need file access. Done." *(sub-second decision)* ### **💰 Massive Token Savings** - **Before:** 100k+ tokens just for tool schemas - **After:** 2.5k tokens for unified interface - **Result:** 40x token efficiency = 40x longer conversations ### **🎯 Eliminates Tool Confusion** - **Before:** AI picks `read_file` when you meant `search_files` - **After:** NCP's semantic engine finds the RIGHT tool for the task ### **🚀 Faster, Smarter Responses** - **Before:** 8-second delay analyzing tool options - **After:** Instant tool selection, immediate action **Bottom line:** Your AI goes from overwhelmed to **laser-focused**. --- ## 🛠️ **For Power Users: Manual Setup** Prefer to build from scratch? Add MCPs manually: ```bash # Add the most popular MCPs: # AI reasoning and memory ncp add sequential-thinking npx @modelcontextprotocol/server-sequential-thinking ncp add memory npx @modelcontextprotocol/server-memory # File and development tools ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents # Path: directory to access ncp add github npx @modelcontextprotocol/server-github # No path needed # Search and productivity ncp add brave-search npx @modelcontextprotocol/server-brave-search # No path needed ```  **💡 Pro tip:** Browse [Smithery.ai](https://smithery.ai) (2,200+ MCPs) or [mcp.so](https://mcp.so) to discover tools for your specific needs. --- ## 🎯 **Popular MCPs That Work Great with NCP** ### **🔥 Most Downloaded** ```bash # Community favorites (download counts from Smithery.ai): ncp add sequential-thinking npx @modelcontextprotocol/server-sequential-thinking # 5,550+ downloads ncp add memory npx @modelcontextprotocol/server-memory # 4,200+ downloads ncp add brave-search npx @modelcontextprotocol/server-brave-search # 680+ downloads ``` ### **🛠️ Development Essentials** ```bash # Popular dev tools: ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/code ncp add github npx @modelcontextprotocol/server-github ncp add shell npx @modelcontextprotocol/server-shell ``` ### **🌐 Productivity & Integrations** ```bash # Enterprise favorites: ncp add gmail npx @mcptools/gmail-mcp ncp add slack npx @modelcontextprotocol/server-slack ncp add google-drive npx @modelcontextprotocol/server-gdrive ncp add postgres npx @modelcontextprotocol/server-postgres ncp add puppeteer npx @hisma/server-puppeteer ``` --- ## ⚙️ **Configuration for Different AI Clients** ### **Claude Desktop** (Most Popular) **Configuration File Location:** - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` - **Linux:** `~/.config/Claude/claude_desktop_config.json` Replace your entire `claude_desktop_config.json` with: ```json { "mcpServers": { "ncp": { "command": "ncp" } } } ``` **📌 Important:** Restart Claude Desktop after saving the config file. > **Note:** Configuration file locations are current as of this writing. For the most up-to-date setup instructions, please refer to the [official Claude Desktop documentation](https://claude.ai/docs). ### **Claude Code** NCP works automatically! Just run: ```bash ncp add <your-mcps> ``` ### **VS Code with GitHub Copilot** **Settings File Location:** - **macOS:** `~/Library/Application Support/Code/User/settings.json` - **Windows:** `%APPDATA%\Code\User\settings.json` - **Linux:** `~/.config/Code/User/settings.json` Add to your VS Code `settings.json`: ```json { "mcp.servers": { "ncp": { "command": "ncp" } } } ``` **📌 Important:** Restart VS Code after saving the settings file. > **Disclaimer:** Configuration paths and methods are accurate as of this writing. VS Code and its extensions may change these locations or integration methods. Please consult the [official VS Code documentation](https://code.visualstudio.com/docs) for the most current information. ### **Cursor IDE** ```json { "mcp": { "servers": { "ncp": { "command": "ncp" } } } } ``` > **Disclaimer:** Configuration format and location may vary by Cursor IDE version. Please refer to [Cursor's official documentation](https://cursor.sh/docs) for the most up-to-date setup instructions. --- ## 🔧 **Advanced Features** ### **Smart Health Monitoring** NCP automatically detects broken MCPs and routes around them: ```bash ncp list --depth 1 # See health status ncp config validate # Check configuration health ``` **🎯 Result:** Your AI never gets stuck on broken tools. ### **Multi-Profile Organization** Organize MCPs by project or environment: ```bash # Development setup ncp add --profile dev filesystem npx @modelcontextprotocol/server-filesystem ~/dev # Production setup ncp add --profile prod database npx production-db-server # Use specific profile ncp --profile dev find "file tools" ``` ### **🚀 Project-Level Configuration** **New:** Configure MCPs per project with automatic detection - perfect for teams and Cloud IDEs: ```bash # In any project directory, create local MCP configuration: mkdir .ncp ncp add filesystem npx @modelcontextprotocol/server-filesystem ./ ncp add github npx @modelcontextprotocol/server-github # NCP automatically detects and uses project-local configuration ncp find "save file" # Uses only project MCPs ``` **How it works:** - 📁 **Local `.ncp` directory exists** → Uses project configuration - 🏠 **No local `.ncp` directory** → Falls back to global `~/.ncp` - 🎯 **Zero profile management needed** → Everything goes to default `all.json` **Perfect for:** - 🤖 **Claude Code projects** (project-specific MCP tooling) - 👥 **Team consistency** (ship `.ncp` folder with your repo) - 🔧 **Project-specific tooling** (each project defines its own MCPs) - 📦 **Environment isolation** (no global MCP conflicts) ```bash # Example project structures: frontend-app/ .ncp/profiles/all.json # → playwright, lighthouse, browser-context src/ api-backend/ .ncp/profiles/all.json # → postgres, redis, docker, kubernetes server/ ``` ### **Import from Anywhere** ```bash # From clipboard (any JSON config) ncp config import # From specific file ncp config import "~/my-mcp-config.json" # From Claude Desktop (auto-detected paths) ncp config import ``` --- ## 🛟 **Troubleshooting** ### **Import Issues** ```bash # Check what was imported ncp list # Validate health of imported MCPs ncp config validate # See detailed import logs DEBUG=ncp:* ncp config import ``` ### **AI Not Using Tools** - **Check connection:** `ncp list` (should show your MCPs) - **Test discovery:** `ncp find "your query"` - **Validate config:** Ensure your AI client points to `ncp` command ### **Performance Issues** ```bash # Check MCP health (unhealthy MCPs slow everything down) ncp list --depth 1 # Clear cache if needed rm -rf ~/.ncp/cache # Monitor with debug logs DEBUG=ncp:* ncp find "test" ``` --- ## 📚 **Deep Dive: How It Works** Want the technical details? Token analysis, architecture diagrams, and performance benchmarks: 📖 **[Read the Technical Guide →](HOW-IT-WORKS.md)** Learn about: - Vector similarity search algorithms - N-to-1 orchestration architecture - Real-world token usage comparisons - Health monitoring and failover systems --- ## 🤝 **Contributing** Help make NCP even better: - 🐛 **Bug reports:** [GitHub Issues](https://github.com/portel-dev/ncp/issues) - 💡 **Feature requests:** [GitHub Discussions](https://github.com/portel-dev/ncp/discussions) - 🔄 **Pull requests:** [Contributing Guide](CONTRIBUTING.md) --- ## 📄 **License** Elastic License 2.0 - [Full License](LICENSE) **TLDR:** Free for all use including commercial. Cannot be offered as a hosted service to third parties. ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown # Security Policy ## Supported Versions We actively support the following versions of NCP with security updates: | Version | Supported | | ------- | ------------------ | | 1.2.x | :white_check_mark: | | 1.1.x | :white_check_mark: | | 1.0.x | :x: | | < 1.0 | :x: | ## Reporting a Vulnerability We take security vulnerabilities seriously. If you discover a security vulnerability in NCP, please report it responsibly. ### How to Report **Please do NOT report security vulnerabilities through public GitHub issues.** Instead, please report security vulnerabilities by email to: **[email protected]** Include the following information in your report: - Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.) - Full paths of source file(s) related to the manifestation of the issue - The location of the affected source code (tag/branch/commit or direct URL) - Any special configuration required to reproduce the issue - Step-by-step instructions to reproduce the issue - Proof-of-concept or exploit code (if possible) - Impact of the issue, including how an attacker might exploit the issue ### What to Expect - **Acknowledgment**: We will acknowledge receipt of your report within 48 hours - **Initial Response**: We will provide an initial response within 7 days with next steps - **Updates**: We will keep you informed of our progress throughout the process - **Resolution**: We aim to resolve critical vulnerabilities within 30 days - **Credit**: We will credit you in our security advisory (unless you prefer to remain anonymous) ### Security Update Process 1. **Vulnerability Assessment**: Our team will verify and assess the impact 2. **Fix Development**: We will develop and test a fix 3. **Security Advisory**: We will publish a security advisory (if applicable) 4. **Patch Release**: We will release a patched version 5. **Disclosure**: We will coordinate disclosure timing with the reporter ### Scope This security policy applies to: - The main NCP application - All supported versions - Official Docker containers - Dependencies we directly maintain ### Out of Scope The following are generally considered out of scope: - Issues in third-party MCP servers (report to their maintainers) - Vulnerabilities requiring physical access to the system - Issues affecting only unsupported versions - Social engineering attacks ### Bug Bounty Currently, we do not offer a paid bug bounty program. However, we deeply appreciate security researchers who help improve NCP's security and will publicly acknowledge their contributions. ### Questions If you have questions about this security policy, please contact us at [email protected]. --- **Thank you for helping keep NCP secure!** ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [email protected]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown # Contributing to NCP Thank you for your interest in contributing to the Natural Context Protocol (NCP) project! This document provides guidelines and information for contributors. ## Code of Conduct This project follows a standard code of conduct. Be respectful, inclusive, and professional in all interactions. ## Development Workflow ### Prerequisites - Node.js 18+ and npm - TypeScript knowledge - Familiarity with Jest testing framework - Understanding of the Model Context Protocol (MCP) ### Setup ```bash # Fork and clone the repository git clone https://github.com/YOUR_USERNAME/ncp.git cd ncp # Install dependencies npm install # Run tests to ensure everything works npm test ``` ### Test-Driven Development NCP follows strict TDD principles: 1. **Write tests first** - All new features must have tests written before implementation 2. **Red-Green-Refactor** - Follow the TDD cycle strictly 3. **High coverage** - Maintain 95%+ test coverage 4. **Integration tests** - Test component interactions, not just units ### Running Tests ```bash # Run all tests npm test # Run tests in watch mode during development npm test -- --watch # Run specific test suites npm test -- --testNamePattern="ProfileManager" npm test -- --testNamePattern="CLI" # Run with coverage report npm run test:coverage ``` ### Code Quality Standards - **TypeScript Strict Mode**: All code must pass strict TypeScript checks - **ESLint**: Follow the project's ESLint configuration - **No Console Logs**: Use proper logging mechanisms in production code - **Error Handling**: All async operations must have proper error handling - **Resource Cleanup**: Always clean up resources (timeouts, connections, etc.) ### Commit Guidelines Follow conventional commit format: ``` type(scope): description Examples: feat(cli): add new profile export command fix(transport): resolve timeout cleanup issue docs(readme): update installation instructions test(orchestrator): add edge case coverage ``` **Commit Types:** - `feat`: New features - `fix`: Bug fixes - `docs`: Documentation changes - `test`: Test additions or modifications - `refactor`: Code refactoring - `perf`: Performance improvements - `chore`: Maintenance tasks ### Pull Request Process 1. **Create a feature branch**: ```bash git checkout -b feature/your-feature-name ``` 2. **Write tests first**: - Create comprehensive test cases - Ensure tests fail initially (red phase) 3. **Implement your feature**: - Write minimal code to pass tests (green phase) - Refactor for quality and maintainability 4. **Ensure quality**: ```bash # All tests must pass npm test # Code must compile without errors npm run build # Follow TypeScript strict mode npm run typecheck ``` 5. **Document your changes**: - Update README if needed - Add/update JSDoc comments - Update CHANGELOG.md 6. **Submit pull request**: - Clear title and description - Reference any related issues - Include test results - Request review from maintainers ### Project Structure ``` src/ ├── core/ # Core orchestration and discovery ├── transport/ # MCP communication layers ├── profiles/ # Profile management system ├── discovery/ # Semantic matching algorithms ├── cli/ # Command-line interface └── types/ # TypeScript type definitions tests/ ├── setup.ts # Jest configuration └── jest-setup.ts # Environment setup ``` ### Feature Development Guidelines #### Adding New Transport Types 1. Create interface in `src/types/index.ts` 2. Implement transport in `src/transport/` 3. Add comprehensive tests 4. Update orchestrator to support new transport 5. Add CLI commands if needed #### Extending Semantic Discovery 1. Add test cases in `src/discovery/semantic-matcher.test.ts` 2. Implement matching algorithms 3. Update confidence scoring mechanisms 4. Ensure backward compatibility #### CLI Command Addition 1. Write CLI tests first in `src/cli/index.test.ts` 2. Implement command handlers 3. Add help documentation 4. Test error handling thoroughly ### Testing Guidelines #### Unit Tests - Test individual functions and methods - Mock external dependencies - Cover edge cases and error conditions - Use descriptive test names #### Integration Tests - Test component interactions - Verify end-to-end workflows - Test real MCP server connections (when possible) - Validate error propagation #### Test Structure ```typescript describe('ComponentName', () => { beforeEach(() => { // Setup for each test }); afterEach(() => { // Cleanup after each test }); describe('feature description', () => { it('should behave correctly in normal case', async () => { // Test implementation }); it('should handle error case gracefully', async () => { // Error case testing }); }); }); ``` ### Performance Considerations - **Token Efficiency**: All features should maintain or improve token reduction - **Memory Management**: Proper cleanup of resources and event listeners - **Async Operations**: Use proper async/await patterns - **Caching**: Consider caching strategies for expensive operations ### Security Guidelines - **Input Validation**: Validate all external inputs - **Path Security**: Prevent path traversal attacks - **Process Security**: Secure child process spawning - **Error Information**: Don't leak sensitive information in errors ### Documentation Standards - **JSDoc Comments**: All public APIs must have JSDoc - **README Updates**: Keep README synchronized with features - **Example Code**: Provide working examples for new features - **Architecture Docs**: Update architecture documentation for significant changes ### Getting Help - **GitHub Issues**: For bugs and feature requests - **GitHub Discussions**: For questions and general discussion - **Code Review**: Maintainers will provide feedback on pull requests ### License By contributing to NCP, you agree that your contributions will be licensed under the Elastic License v2. --- Thank you for contributing to NCP! Your efforts help make AI tool integration more efficient for everyone. ``` -------------------------------------------------------------------------------- /test/__mocks__/version.ts: -------------------------------------------------------------------------------- ```typescript /** * Mock version for tests */ export const version = '1.3.2'; export const packageName = '@portel/ncp'; ``` -------------------------------------------------------------------------------- /test/mock-smithery-mcp/package.json: -------------------------------------------------------------------------------- ```json { "name": "test-smithery-mcp", "version": "1.0.0", "description": "Mock MCP for testing Smithery config detection", "main": "index.js" } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node /** * NCP Entry Point * Routes to CLI interface or MCP server mode */ // Import the CLI application import './cli/index.js'; ``` -------------------------------------------------------------------------------- /test/__mocks__/updater.js: -------------------------------------------------------------------------------- ```javascript // Mock updater to prevent import.meta issues in tests export const updater = { checkForUpdates: jest.fn(), getUpdateMessage: jest.fn(() => ''), shouldCheckForUpdates: jest.fn(() => false) }; export default updater; ``` -------------------------------------------------------------------------------- /test/__mocks__/transformers.js: -------------------------------------------------------------------------------- ```javascript /** * Mock for @xenova/transformers to avoid downloading models in tests */ export const pipeline = jest.fn().mockResolvedValue({ similarity: jest.fn().mockResolvedValue(0.8) }); export const AutoTokenizer = { from_pretrained: jest.fn().mockResolvedValue({ encode: jest.fn().mockReturnValue([1, 2, 3]) }) }; ``` -------------------------------------------------------------------------------- /test/mock-smithery-mcp/smithery.yaml: -------------------------------------------------------------------------------- ```yaml startCommand: type: stdio configSchema: type: object required: ["testApiKey", "testEndpoint"] properties: testApiKey: type: string description: "API key for test service authentication" testEndpoint: type: string description: "API endpoint URL for the test service" ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- ```yaml blank_issues_enabled: false contact_links: - name: 📚 Documentation & Guides url: https://github.com/portel-dev/ncp/blob/main/README.md about: Check our comprehensive documentation and guides first - name: 💬 Discussions url: https://github.com/portel-dev/ncp/discussions about: Ask questions, share ideas, and discuss NCP with the community - name: 🔒 Security Issues url: https://github.com/portel-dev/ncp/security/policy about: Please report security vulnerabilities responsibly via our security policy ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "ES2020", "moduleResolution": "node", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "dist", "test", "src/testing/**/*" ] } ``` -------------------------------------------------------------------------------- /test/quick-coverage.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect } from "@jest/globals"; import { MCPHealthMonitor } from "../src/utils/health-monitor.js"; describe("Quick Coverage Boost", () => { it("should trigger auto-disable warning", () => { const monitor = new MCPHealthMonitor(); // Trigger 3+ errors to hit line 352 monitor.markUnhealthy("autodisable", "Error 1"); monitor.markUnhealthy("autodisable", "Error 2"); monitor.markUnhealthy("autodisable", "Error 3"); const health = monitor.getMCPHealth("autodisable"); expect(health?.status).toBe("disabled"); }); }); ``` -------------------------------------------------------------------------------- /src/testing/test-profile.json: -------------------------------------------------------------------------------- ```json { "name": "test", "description": "Minimal test profile for fast testing", "mcpServers": { "test-filesystem": { "command": "node", "args": ["test/mock-mcps/filesystem-server.js"], "env": {} }, "test-github": { "command": "node", "args": ["test/mock-mcps/github-server.js"], "env": {} }, "test-git": { "command": "node", "args": ["test/mock-mcps/git-server.js"], "env": {} } }, "metadata": { "createdAt": "2024-09-27T00:00:00.000Z", "updatedAt": "2024-09-27T00:00:00.000Z" } } ``` -------------------------------------------------------------------------------- /src/internal-mcps/types.ts: -------------------------------------------------------------------------------- ```typescript /** * Internal MCP Types * * Defines interfaces for MCPs that are implemented internally by NCP itself * These MCPs appear in tool discovery like external MCPs but are handled internally */ export interface InternalTool { name: string; description: string; inputSchema: { type: string; properties: Record<string, any>; required?: string[]; }; } export interface InternalToolResult { success: boolean; content?: string; error?: string; } export interface InternalMCP { name: string; description: string; tools: InternalTool[]; /** * Execute a tool from this internal MCP */ executeTool(toolName: string, parameters: any): Promise<InternalToolResult>; } ``` -------------------------------------------------------------------------------- /scripts/sync-server-version.cjs: -------------------------------------------------------------------------------- ``` // Syncs server.json version with package.json const fs = require('fs'); const path = require('path'); const pkgPath = path.resolve(__dirname, '../package.json'); const serverPath = path.resolve(__dirname, '../server.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); const server = JSON.parse(fs.readFileSync(serverPath, 'utf8')); if (server.version !== pkg.version) { server.version = pkg.version; if (server.packages && server.packages[0]) { server.packages[0].version = pkg.version; } fs.writeFileSync(serverPath, JSON.stringify(server, null, 2)); console.log(`server.json version updated to ${pkg.version}`); } else { console.log('server.json version already matches package.json'); } ``` -------------------------------------------------------------------------------- /MCP-CONFIG-SCHEMA-SIMPLE-EXAMPLE.json: -------------------------------------------------------------------------------- ```json { "comment": "Simple, common example - just environment variables", "description": "This is what most MCP servers will return", "protocolVersion": "0.1.0", "capabilities": { "tools": {} }, "serverInfo": { "name": "gmail-mcp", "version": "1.0.0" }, "configurationSchema": { "environmentVariables": [ { "name": "GCP_OAUTH_KEYS_PATH", "description": "Path to the GCP OAuth keys JSON file", "type": "path", "required": true, "sensitive": true }, { "name": "CREDENTIALS_PATH", "description": "Path to the stored credentials JSON file", "type": "path", "required": true, "sensitive": true } ] } } ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run tests run: npm test - name: Test package functionality (skip in CI - too resource intensive with 1070 MCPs) run: echo "Package stress test skipped in CI - requires 1070 MCP initialization which exceeds CI time limits" ``` -------------------------------------------------------------------------------- /test/version-util.test.ts: -------------------------------------------------------------------------------- ```typescript import { jest } from '@jest/globals'; describe('version', () => { beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); }); it('returns test version when running in Jest', () => { jest.isolateModules(() => { const { getPackageInfo } = require('../src/utils/version'); const result = getPackageInfo(); // In Jest, should return test version expect(result.packageName).toBe('@portel/ncp'); expect(result.version).toBe('0.0.0-test'); }); }); it('exports version and packageName constants', () => { jest.isolateModules(() => { const { version, packageName } = require('../src/utils/version'); // Should export constants expect(packageName).toBe('@portel/ncp'); expect(version).toBe('0.0.0-test'); }); }); }); ``` -------------------------------------------------------------------------------- /test/coverage-boost.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect } from "@jest/globals"; import { DiscoveryEngine } from "../src/discovery/engine.js"; describe("Coverage Boost Tests", () => { let engine: DiscoveryEngine; beforeEach(async () => { engine = new DiscoveryEngine(); await engine.initialize(); }); it("should exercise pattern extraction", async () => { await engine.indexTool({ name: "test:tool", description: "create files and edit multiple directories with various operations", mcpName: "test" }); const stats = engine.getStats(); expect(stats.totalTools).toBeGreaterThan(0); }); it("should exercise similarity matching", async () => { await engine.indexTool({ name: "similar:one", description: "database operations and queries", mcpName: "db" }); await engine.indexTool({ name: "similar:two", description: "file system operations", mcpName: "fs" }); const related = await engine.findRelatedTools("similar:one"); expect(Array.isArray(related)).toBe(true); }); }); ``` -------------------------------------------------------------------------------- /test/cli-help-validation.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # CLI Help Validation - Minimal version cd /Users/arul/Projects/ncp-production-clean echo "Testing CLI Help Output..." echo "" node dist/cli/index.js --help 2>&1 | grep -q "add" && echo "✓ add command" || echo "✗ add command" node dist/cli/index.js --help 2>&1 | grep -q "find" && echo "✓ find command" || echo "✗ find command" node dist/cli/index.js --help 2>&1 | grep -q "run" && echo "✓ run command" || echo "✗ run command" node dist/cli/index.js --help 2>&1 | grep -q "list" && echo "✓ list command" || echo "✗ list command" node dist/cli/index.js --help 2>&1 | grep -q "analytics" && echo "✓ analytics command" || echo "✗ analytics command" node dist/cli/index.js --help 2>&1 | grep -q "config" && echo "✓ config command" || echo "✗ config command" node dist/cli/index.js --help 2>&1 | grep -q "remove" && echo "✓ remove command" || echo "✗ remove command" node dist/cli/index.js --help 2>&1 | grep -q "repair" && echo "✓ repair command" || echo "✗ repair command" node dist/cli/index.js --help 2>&1 | grep -q "update" && echo "✓ update command" || echo "✗ update command" echo "" echo "✅ All commands present in help" ``` -------------------------------------------------------------------------------- /server.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", "name": "io.github.portel-dev/ncp", "description": "N-to-1 MCP Orchestration. Unified gateway for multiple MCP servers with intelligent tool discovery.", "repository": { "url": "https://github.com/portel-dev/ncp", "source": "github" }, "version": "1.5.3", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@portel/ncp", "version": "1.5.3", "runtimeHint": "npx", "transport": { "type": "stdio" }, "environmentVariables": [ { "name": "NCP_DEBUG", "description": "Enable debug logging for troubleshooting", "default": "false" }, { "name": "NCP_MODE", "description": "Operating mode: 'mcp' for AI assistant integration or 'cli' for command-line", "default": "mcp" }, { "name": "NO_COLOR", "description": "Disable colored output in logs and CLI", "default": "false" } ] } ] } ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript export default { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', roots: ['<rootDir>/test'], testMatch: [ '**/?(*.)+(spec|test).[tj]s' ], transform: { '^.+\\.ts$': ['ts-jest', { useESM: true, tsconfig: { module: 'ES2020', target: 'ES2020' } }] }, coverageDirectory: 'coverage', collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.test.ts', '!src/**/*.spec.ts' ], coverageReporters: ['text', 'lcov', 'html'], coverageThreshold: { global: { branches: 60, functions: 80, lines: 75, statements: 75 } }, moduleFileExtensions: ['ts', 'js', 'json'], extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', '@xenova/transformers': '<rootDir>/test/__mocks__/transformers.js', '^chalk$': '<rootDir>/test/__mocks__/chalk.js', '../utils/updater.js': '<rootDir>/test/__mocks__/updater.js', // '^.*/utils/version\\.js$': '<rootDir>/test/__mocks__/version.ts' }, setupFilesAfterEnv: ['<rootDir>/test/setup.ts'], testTimeout: 15000, verbose: true, forceExit: true, detectOpenHandles: false, workerIdleMemoryLimit: '512MB', maxWorkers: 1 }; ``` -------------------------------------------------------------------------------- /test/__mocks__/chalk.js: -------------------------------------------------------------------------------- ```javascript // Mock chalk for Jest testing // Provides basic color functions that return the input string const mockChalk = { green: (str) => str, red: (str) => str, yellow: (str) => str, blue: (str) => str, cyan: (str) => str, magenta: (str) => str, white: (str) => str, gray: (str) => str, grey: (str) => str, black: (str) => str, bold: (str) => str, italic: (str) => str, underline: (str) => str, dim: (str) => str, inverse: (str) => str, strikethrough: (str) => str, // Support for chaining green: { bold: (str) => str, dim: (str) => str, italic: (str) => str, underline: (str) => str, }, red: { bold: (str) => str, dim: (str) => str, italic: (str) => str, underline: (str) => str, }, yellow: { bold: (str) => str, dim: (str) => str, italic: (str) => str, underline: (str) => str, }, blue: { bold: (str) => str, dim: (str) => str, italic: (str) => str, underline: (str) => str, }, // Default export default: function(str) { return str; } }; // Add all color functions to the default function Object.keys(mockChalk).forEach(key => { if (key !== 'default') { mockChalk.default[key] = mockChalk[key]; } }); module.exports = mockChalk; module.exports.default = mockChalk; ``` -------------------------------------------------------------------------------- /src/utils/version.ts: -------------------------------------------------------------------------------- ```typescript /** * Version utility for reading package version * Uses Node.js createRequire pattern with fixed relative path to package.json */ import { createRequire } from 'node:module'; import { realpathSync } from 'node:fs'; // Read package.json using the standard approach for ES modules // This file is at: dist/utils/version.js // Package.json is at: ../../package.json (relative to compiled file) export function getPackageInfo() { try { // In test environments, return test version if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID) { return { version: '0.0.0-test', packageName: '@portel/ncp' }; } // Get the entry script location (handles symlinks for global npm installs) // e.g., /usr/local/bin/ncp (symlink) -> /usr/.../ncp/dist/index.js (real) const entryScript = realpathSync(process.argv[1]); const entryScriptUrl = `file://${entryScript}`; // Use createRequire to load package.json with fixed relative path const require = createRequire(entryScriptUrl); const pkg = require('../package.json'); // From dist/index.js to package.json return { version: pkg.version, packageName: pkg.name }; } catch (e) { throw new Error(`Failed to load package.json: ${e}`); } } export const version = getPackageInfo().version; export const packageName = getPackageInfo().packageName; ``` -------------------------------------------------------------------------------- /src/index-mcp.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node /** * NCP MCP-Only Entry Point * * This is a slim entry point for .mcpb bundles that runs ONLY as an MCP server. * It does NOT include CLI functionality to minimize bundle size and improve performance. * * For CLI tools (ncp add, ncp find, etc.), install via npm: * npm install -g @portel/ncp * * Configuration: * - Reads from ~/.ncp/profiles/all.json (or specified profile) * - Manually edit the JSON file to add/remove MCPs * - No CLI commands needed */ import { MCPServer } from './server/mcp-server.js'; import { setOverrideWorkingDirectory } from './utils/ncp-paths.js'; // Handle --working-dir parameter for MCP server mode const workingDirIndex = process.argv.indexOf('--working-dir'); if (workingDirIndex !== -1 && workingDirIndex + 1 < process.argv.length) { const workingDirValue = process.argv[workingDirIndex + 1]; setOverrideWorkingDirectory(workingDirValue); } // Handle --profile parameter const profileIndex = process.argv.indexOf('--profile'); const profileName = profileIndex !== -1 ? (process.argv[profileIndex + 1] || 'all') : 'all'; // Debug logging for integration tests if (process.env.NCP_DEBUG === 'true') { console.error(`[DEBUG] MCP-only mode`); console.error(`[DEBUG] profileIndex: ${profileIndex}`); console.error(`[DEBUG] process.argv: ${process.argv.join(' ')}`); console.error(`[DEBUG] Selected profile: ${profileName}`); } // Start MCP server const server = new MCPServer(profileName); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- ```typescript /** * Test setup for NCP-OSS * Configures global test environment */ // Extend Jest timeout for integration tests jest.setTimeout(30000); // Mock console methods for cleaner test output const originalConsole = { ...console }; beforeEach(() => { // Suppress console.log in tests unless NODE_ENV=test-verbose if (process.env.NODE_ENV !== 'test-verbose') { jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'info').mockImplementation(() => {}); } }); afterEach(() => { // Restore console methods if (process.env.NODE_ENV !== 'test-verbose') { const logMock = console.log as jest.Mock; const infoMock = console.info as jest.Mock; if (logMock && typeof logMock.mockRestore === 'function') { logMock.mockRestore(); } if (infoMock && typeof infoMock.mockRestore === 'function') { infoMock.mockRestore(); } } }); // Clean up after each test afterEach(async () => { // Clear all timers jest.clearAllTimers(); jest.clearAllMocks(); // Force garbage collection if available if (global.gc) { global.gc(); } }); // Global cleanup after all tests afterAll(async () => { // Clear all timers jest.clearAllTimers(); jest.clearAllMocks(); // Force close any open handles if (process.stdout && typeof process.stdout.destroy === 'function') { // Don't actually destroy stdout, just ensure it's flushed } // Force garbage collection if available if (global.gc) { global.gc(); } // Give a small delay for cleanup await new Promise(resolve => setTimeout(resolve, 50)); }); ``` -------------------------------------------------------------------------------- /test/mock-smithery-mcp/index.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock MCP Server for Testing Smithery Config Detection * This server intentionally DOES NOT include configurationSchema in capabilities * to test fallback to smithery.yaml detection */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // Create server WITHOUT configurationSchema in capabilities const server = new Server( { name: 'test-smithery-mcp', version: '1.0.0', }, { capabilities: { tools: {}, // Intentionally NO configurationSchema here // to test smithery.yaml fallback }, } ); // Register tool list handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'test_tool', description: 'A test tool for Smithery config detection', inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'Test message' } } } } ] }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'test_tool') { return { content: [ { type: 'text', text: `Test tool executed: ${args.message || 'no message'}` } ] }; } throw new Error(`Unknown tool: ${name}`); }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('[INFO] Mock Smithery MCP server running'); } main().catch((error) => { console.error('[ERROR] Server failed:', error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /MCP-CONFIG-SCHEMA-IMPLEMENTATION-EXAMPLE.ts: -------------------------------------------------------------------------------- ```typescript /** * Example: How MCP Servers Implement configurationSchema * * This shows the actual code that MCP servers add to return * configuration schema in their initialize() response. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; // Create MCP server with configurationSchema in capabilities const server = new Server( { name: 'gmail-mcp', version: '1.0.0', }, { capabilities: { tools: {}, // ADD THIS: configurationSchema in capabilities configurationSchema: { environmentVariables: [ { name: 'GCP_OAUTH_KEYS_PATH', description: 'Path to the GCP OAuth keys JSON file', type: 'path', required: true, sensitive: true, }, { name: 'CREDENTIALS_PATH', description: 'Path to the stored credentials JSON file', type: 'path', required: true, sensitive: true, }, ], }, }, } ); // The rest of your MCP server code... server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Your tools... ], }; }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main(); /** * What this does: * * 1. MCP client (like NCP) calls initialize() * 2. Server returns InitializeResult with configurationSchema * 3. Client sees schema and prompts user for required config * 4. User provides GCP_OAUTH_KEYS_PATH and CREDENTIALS_PATH * 5. Client sets environment variables and starts server * * Before (without schema): * Error: GCP_OAUTH_KEYS_PATH is required * (User has to figure out what's needed) * * After (with schema): * 📋 Configuration needed: * GCP_OAUTH_KEYS_PATH: [required, path] * Path to the GCP OAuth keys JSON file * Enter GCP_OAUTH_KEYS_PATH: _ * (User is guided through setup) */ ``` -------------------------------------------------------------------------------- /src/services/tool-schema-parser.ts: -------------------------------------------------------------------------------- ```typescript /** * Shared service for parsing tool schemas * Single source of truth for parameter extraction */ export interface ParameterInfo { name: string; type: string; required: boolean; description?: string; } export class ToolSchemaParser { /** * Parse parameters from a tool schema */ static parseParameters(schema: any): ParameterInfo[] { const params: ParameterInfo[] = []; if (!schema || typeof schema !== 'object') { return params; } const properties = schema.properties || {}; const required = schema.required || []; for (const [name, prop] of Object.entries(properties)) { const propDef = prop as any; params.push({ name, type: propDef.type || 'unknown', required: required.includes(name), description: propDef.description }); } return params; } /** * Get only required parameters from a schema */ static getRequiredParameters(schema: any): ParameterInfo[] { return this.parseParameters(schema).filter(p => p.required); } /** * Get only optional parameters from a schema */ static getOptionalParameters(schema: any): ParameterInfo[] { return this.parseParameters(schema).filter(p => !p.required); } /** * Check if a schema has any required parameters */ static hasRequiredParameters(schema: any): boolean { if (!schema || typeof schema !== 'object') { return false; } const required = schema.required || []; return Array.isArray(required) && required.length > 0; } /** * Count total parameters in a schema */ static countParameters(schema: any): { total: number; required: number; optional: number } { const params = this.parseParameters(schema); const required = params.filter(p => p.required).length; return { total: params.length, required, optional: params.length - required }; } /** * Get parameter by name from schema */ static getParameter(schema: any, paramName: string): ParameterInfo | undefined { return this.parseParameters(schema).find(p => p.name === paramName); } } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml name: Release on: workflow_dispatch: inputs: release_type: description: 'Release type' required: true default: 'minor' type: choice options: - patch - minor - major dry_run: description: 'Dry run (test without publishing)' required: false default: false type: boolean permissions: contents: write issues: write pull-requests: write id-token: write # Required for OIDC npm publishing jobs: release: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run tests run: npm test - name: Test package functionality run: npm run test:package - name: Build Desktop Extension (DXT) run: npm run build:dxt - name: Get package version id: get_version run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - name: Configure Git run: | git config --global user.name "GitHub Actions" git config --global user.email "[email protected]" - name: Release (Dry Run) if: ${{ inputs.dry_run == true }} run: npm run release -- --increment=${{ inputs.release_type }} --dry-run --ci env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Release if: ${{ inputs.dry_run == false }} run: npm run release -- --increment=${{ inputs.release_type }} --ci env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Desktop Extension to release if: ${{ inputs.dry_run == false }} uses: softprops/action-gh-release@v1 with: files: ncp.dxt tag_name: ${{ steps.get_version.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -------------------------------------------------------------------------------- /src/utils/ncp-paths.ts: -------------------------------------------------------------------------------- ```typescript /** * NCP Paths Utility * Determines whether to use local or global .ncp directory */ import * as path from 'path'; import { readFileSync, existsSync } from 'fs'; import * as os from 'os'; let _ncpBaseDir: string | null = null; let _overrideWorkingDirectory: string | null = null; /** * Set override working directory for profile resolution * This allows the --working-dir parameter to override process.cwd() */ export function setOverrideWorkingDirectory(dir: string | null): void { _overrideWorkingDirectory = dir; // Clear cached base directory so it gets recalculated with new working directory _ncpBaseDir = null; } /** * Get the effective working directory (override or process.cwd()) */ export function getEffectiveWorkingDirectory(): string { return _overrideWorkingDirectory || process.cwd(); } /** * Determines the base .ncp directory to use * Only uses local .ncp if directory already exists, otherwise falls back to global ~/.ncp */ export function getNcpBaseDirectory(): string { if (_ncpBaseDir) return _ncpBaseDir; // Start from effective working directory and traverse up let currentDir = getEffectiveWorkingDirectory(); const root = path.parse(currentDir).root; while (currentDir !== root) { const localNcpDir = path.join(currentDir, '.ncp'); // Only use local .ncp if the directory already exists if (existsSync(localNcpDir)) { _ncpBaseDir = localNcpDir; return _ncpBaseDir; } currentDir = path.dirname(currentDir); } // Fallback to global ~/.ncp directory (will be created if needed) _ncpBaseDir = path.join(os.homedir(), '.ncp'); return _ncpBaseDir; } /** * Get the profiles directory (local or global) */ export function getProfilesDirectory(): string { return path.join(getNcpBaseDirectory(), 'profiles'); } /** * Get the cache directory (local or global) */ export function getCacheDirectory(): string { return path.join(getNcpBaseDirectory(), 'cache'); } /** * Get the logs directory (local or global) */ export function getLogsDirectory(): string { return path.join(getNcpBaseDirectory(), 'logs'); } /** * Check if we're using a local NCP installation */ export function isLocalNcpInstallation(): boolean { const baseDir = getNcpBaseDirectory(); return !baseDir.startsWith(os.homedir()); } ``` -------------------------------------------------------------------------------- /src/utils/markdown-renderer.ts: -------------------------------------------------------------------------------- ```typescript /** * Markdown-to-Terminal Renderer * Converts markdown content to beautiful colored terminal output */ // ANSI Color codes const colors = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m', underline: '\x1b[4m', // Colors black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m', // Bright colors brightRed: '\x1b[91m', brightGreen: '\x1b[92m', brightYellow: '\x1b[93m', brightBlue: '\x1b[94m', brightMagenta: '\x1b[95m', brightCyan: '\x1b[96m', brightWhite: '\x1b[97m' }; /** * Simple markdown-to-terminal renderer using ANSI codes */ export function renderMarkdown(content: string): string { let rendered = content; // Bold text: **text** or __text__ rendered = rendered.replace(/\*\*(.*?)\*\*/g, `${colors.bold}$1${colors.reset}`); rendered = rendered.replace(/__(.*?)__/g, `${colors.bold}$1${colors.reset}`); // Italic text: *text* or _text_ rendered = rendered.replace(/\*(.*?)\*/g, `${colors.italic}$1${colors.reset}`); rendered = rendered.replace(/_(.*?)_/g, `${colors.italic}$1${colors.reset}`); // Inline code: `code` rendered = rendered.replace(/`(.*?)`/g, `${colors.yellow}$1${colors.reset}`); // Headers: # ## ### rendered = rendered.replace(/^(#{1,6})\s+(.*)$/gm, (match, hashes, text) => { const level = hashes.length; const color = level <= 2 ? colors.brightCyan : colors.cyan; return `${color}${colors.bold}${text}${colors.reset}`; }); return rendered; } /** * Enhanced output with emoji and color support */ export function enhancedOutput(content: string): string { // First apply markdown rendering let rendered = renderMarkdown(content); // Additional terminal enhancements rendered = rendered // Enhance search indicators .replace(/🔍/g, '\x1b[36m🔍\x1b[0m') // Cyan search icon .replace(/📁/g, '\x1b[33m📁\x1b[0m') // Yellow folder icon .replace(/📋/g, '\x1b[32m📋\x1b[0m') // Green clipboard icon .replace(/💡/g, '\x1b[93m💡\x1b[0m') // Bright yellow tip icon .replace(/📄/g, '\x1b[34m📄\x1b[0m') // Blue navigation icon .replace(/✅/g, '\x1b[32m✅\x1b[0m') // Green success .replace(/❌/g, '\x1b[31m❌\x1b[0m') // Red error .replace(/🚀/g, '\x1b[35m🚀\x1b[0m'); // Magenta rocket return rendered; } ``` -------------------------------------------------------------------------------- /src/internal-mcps/internal-mcp-manager.ts: -------------------------------------------------------------------------------- ```typescript /** * Internal MCP Manager * * Manages MCPs that are implemented internally by NCP * These appear in tool discovery like external MCPs but are handled internally */ import { InternalMCP, InternalToolResult } from './types.js'; import { NCPManagementMCP } from './ncp-management.js'; import ProfileManager from '../profiles/profile-manager.js'; import { logger } from '../utils/logger.js'; export class InternalMCPManager { private internalMCPs: Map<string, InternalMCP> = new Map(); constructor() { // Register internal MCPs this.registerInternalMCP(new NCPManagementMCP()); } /** * Register an internal MCP */ private registerInternalMCP(mcp: InternalMCP): void { this.internalMCPs.set(mcp.name, mcp); logger.debug(`Registered internal MCP: ${mcp.name}`); } /** * Initialize internal MCPs with ProfileManager */ initialize(profileManager: ProfileManager): void { for (const mcp of this.internalMCPs.values()) { if ('setProfileManager' in mcp && typeof mcp.setProfileManager === 'function') { mcp.setProfileManager(profileManager); } } } /** * Get all internal MCPs for tool discovery */ getAllInternalMCPs(): InternalMCP[] { return Array.from(this.internalMCPs.values()); } /** * Execute a tool from an internal MCP * @param mcpName The internal MCP name (e.g., "ncp") * @param toolName The tool name (e.g., "add") * @param parameters Tool parameters */ async executeInternalTool( mcpName: string, toolName: string, parameters: any ): Promise<InternalToolResult> { const mcp = this.internalMCPs.get(mcpName); if (!mcp) { return { success: false, error: `Internal MCP not found: ${mcpName}` }; } try { return await mcp.executeTool(toolName, parameters); } catch (error: any) { logger.error(`Internal tool execution failed: ${mcpName}:${toolName} - ${error.message}`); return { success: false, error: error.message || 'Internal tool execution failed' }; } } /** * Check if an MCP is internal */ isInternalMCP(mcpName: string): boolean { return this.internalMCPs.has(mcpName); } /** * Get tool names for a specific internal MCP */ getInternalMCPTools(mcpName: string): string[] { const mcp = this.internalMCPs.get(mcpName); return mcp ? mcp.tools.map(t => t.name) : []; } } ``` -------------------------------------------------------------------------------- /.github/workflows/publish-mcp-registry.yml: -------------------------------------------------------------------------------- ```yaml name: Publish to MCP Registry on: release: types: [published] permissions: contents: read id-token: write # Required for GitHub OIDC authentication with MCP registry jobs: publish-to-mcp-registry: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Get version from tag id: get_version run: | VERSION=${GITHUB_REF#refs/tags/} echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Publishing version: $VERSION" - name: Update server.json with release version run: | VERSION=${{ steps.get_version.outputs.version }} jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp mv server.json.tmp server.json cat server.json - name: Validate server.json run: | curl -sS https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json -o /tmp/server.schema.json # Basic JSON validation jq empty server.json echo "✓ server.json is valid JSON" - name: Download MCP Publisher run: | VERSION="v1.1.0" OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') curl -L "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${VERSION#v}_${OS}_${ARCH}.tar.gz" | tar xz chmod +x mcp-publisher ./mcp-publisher --version - name: Login to MCP Registry run: | # Try GitHub OIDC first (preferred, no secrets needed) if ./mcp-publisher login github-oidc; then echo "✓ Authenticated via GitHub OIDC" elif [ -n "${{ secrets.MCP_GITHUB_TOKEN }}" ]; then # Fallback to GitHub PAT if OIDC fails or org not detected echo "⚠ OIDC failed, falling back to GitHub PAT" echo "${{ secrets.MCP_GITHUB_TOKEN }}" | ./mcp-publisher login github --token-stdin else echo "❌ Authentication failed. See RELEASE.md for setup instructions." exit 1 fi - name: Publish to MCP Registry run: ./mcp-publisher publish - name: Summary run: | echo "✅ Successfully published NCP v${{ steps.get_version.outputs.version }} to MCP Registry" echo "📦 Package: io.github.portel-dev/ncp" echo "🔗 NPM: https://www.npmjs.com/package/@portel/ncp" ``` -------------------------------------------------------------------------------- /test/session-id-passthrough.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Test for Session ID Transparency * Verifies that _meta (including session_id) is forwarded transparently to MCP servers */ import { NCPOrchestrator } from '../src/orchestrator/ncp-orchestrator.js'; import { MCPServer } from '../src/server/mcp-server.js'; describe('Session ID Passthrough', () => { it('should forward _meta field from client to orchestrator', async () => { const server = new MCPServer('default', false, false); await server.initialize(); // Simulate client request with session_id in _meta const request = { jsonrpc: '2.0' as const, id: 1, method: 'tools/call', params: { name: 'run', arguments: { tool: 'chrome-devtools:list_pages', parameters: {} }, _meta: { session_id: 'test_session_123', custom_field: 'custom_value' } } }; const response = await server.handleRequest(request); // Should not error - _meta should be forwarded transparently expect(response).toBeDefined(); // May error if chrome-devtools not available, but shouldn't crash on _meta if (response?.error) { // Error should be about MCP availability, not _meta handling expect(response.error.message).not.toContain('_meta'); } }); it('should work without _meta (backwards compatibility)', async () => { const server = new MCPServer('default', false, false); await server.initialize(); // Simulate client request WITHOUT _meta const request = { jsonrpc: '2.0' as const, id: 1, method: 'tools/call', params: { name: 'run', arguments: { tool: 'chrome-devtools:list_pages', parameters: {} } // No _meta field } }; const response = await server.handleRequest(request); // Should still work - _meta is optional expect(response).toBeDefined(); // Should not crash when _meta is absent }); it('should forward empty _meta object', async () => { const server = new MCPServer('default', false, false); await server.initialize(); const request = { jsonrpc: '2.0' as const, id: 1, method: 'tools/call', params: { name: 'run', arguments: { tool: 'chrome-devtools:list_pages', parameters: {} }, _meta: {} // Empty _meta } }; const response = await server.handleRequest(request); expect(response).toBeDefined(); // Should handle empty _meta gracefully }); }); ``` -------------------------------------------------------------------------------- /docs/clients/cursor.md: -------------------------------------------------------------------------------- ```markdown # Installing NCP on Cursor **Method:** JSON configuration only --- ## 📋 Overview Cursor IDE supports MCP servers via JSON configuration. Configure NCP once and access all your MCP tools through a unified interface. --- ## 🔧 Installation Steps ### 1. Install NCP ```bash npm install -g @portel/ncp ``` ### 2. Import Your Existing MCPs (Optional) ```bash # If you have MCPs configured elsewhere, copy config to clipboard # Then run: ncp config import ``` ### 3. Add MCPs to NCP ```bash # Add your MCPs ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ncp add github npx @modelcontextprotocol/server-github ncp add brave-search npx @modelcontextprotocol/server-brave-search # Verify ncp list ``` ### 4. Configure Cursor **Config file location:** - **macOS:** `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - **Windows:** `%APPDATA%/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - **Linux:** `~/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` **Edit the file:** ```bash # macOS nano ~/Library/Application\ Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json ``` **Replace contents with:** ```json { "mcpServers": { "ncp": { "command": "ncp" } } } ``` ### 5. Restart Cursor 1. Quit Cursor completely (⌘Q on Mac, Alt+F4 on Windows) 2. Reopen Cursor 3. Start using NCP in your AI chat --- ## 🎯 Using NCP in Cursor Ask your AI assistant: - "List all available MCP tools" - "Find tools to read files" - "Execute filesystem:read_file on /tmp/test.txt" --- ## 🐛 Troubleshooting **NCP command not found:** ```bash npm install -g @portel/ncp ncp --version ``` **Cursor doesn't see NCP:** 1. Verify config file path is correct for your OS 2. Check JSON syntax is valid 3. Restart Cursor completely 4. Check Cursor's developer console for errors **NCP shows no MCPs:** ```bash ncp list # If empty, add MCPs: ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ``` --- ## 📚 More Resources - **[Claude Desktop Guide](./claude-desktop.md)** - For Claude Desktop users - **[Main README](../../README.md)** - Full documentation - **[Cursor Documentation](https://cursor.sh/docs)** - Official Cursor docs --- ## 🤝 Need Help? - **GitHub Issues:** [Report bugs](https://github.com/portel-dev/ncp/issues) - **GitHub Discussions:** [Ask questions](https://github.com/portel-dev/ncp/discussions) ``` -------------------------------------------------------------------------------- /src/utils/text-utils.ts: -------------------------------------------------------------------------------- ```typescript /** * Shared utilities for text processing and formatting * Consolidates text wrapping and formatting logic */ export interface TextWrapOptions { maxWidth: number; indent?: string; cleanupPrefixes?: boolean; preserveWhitespace?: boolean; } export class TextUtils { /** * Wrap text to fit within specified width with optional indentation */ static wrapText(text: string, options: TextWrapOptions): string { const { maxWidth, indent = '', cleanupPrefixes = false, preserveWhitespace = false } = options; if (!text) { return text; } // Clean up text if requested (used by MCP server) let processedText = text; if (cleanupPrefixes) { processedText = text .replace(/^[^:]+:\s*/, '') // Remove "desktop-commander: " prefix .replace(/\s+/g, ' ') // Replace multiple whitespace with single space .trim(); } else if (!preserveWhitespace) { // Basic cleanup for CLI usage processedText = text.trim(); } // If text fits within width, return as-is if (processedText.length <= maxWidth) { return processedText; } // Split into words and wrap const words = processedText.split(' '); const lines: string[] = []; let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; if (testLine.length <= maxWidth) { currentLine = testLine; } else { if (currentLine) { lines.push(currentLine); currentLine = word; } else { // Single word longer than maxWidth lines.push(word); } } } if (currentLine) { lines.push(currentLine); } // Join lines with proper indentation for continuation return lines.map((line, index) => index === 0 ? line : `\n${indent}${line}` ).join(''); } /** * Wrap text with background color applied to each line (CLI-specific) */ static wrapTextWithBackground( text: string, maxWidth: number, indent: string, backgroundFormatter: (text: string) => string ): string { if (text.length <= maxWidth) { return `${indent}${backgroundFormatter(text)}`; } const wrappedText = this.wrapText(text, { maxWidth, indent: '', preserveWhitespace: true }); const lines = wrappedText.split('\n'); return lines.map((line, index) => { const formattedLine = backgroundFormatter(line); return index === 0 ? `${indent}${formattedLine}` : `${indent}${formattedLine}`; }).join('\n'); } } ``` -------------------------------------------------------------------------------- /docs/clients/cline.md: -------------------------------------------------------------------------------- ```markdown # Installing NCP on Cline (VS Code Extension) **Method:** JSON configuration only --- ## 📋 Overview Cline (formerly Claude Dev) is a VS Code extension that supports MCP servers. Configure NCP to unify all your MCP tools under one intelligent interface. --- ## 🔧 Installation Steps ### 1. Install NCP ```bash npm install -g @portel/ncp ``` ### 2. Import Your Existing MCPs (Optional) ```bash # If you have MCPs configured elsewhere, copy config to clipboard # Then run: ncp config import ``` ### 3. Add MCPs to NCP ```bash # Add your MCPs ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ncp add github npx @modelcontextprotocol/server-github ncp add sequential-thinking npx @modelcontextprotocol/server-sequential-thinking # Verify ncp list ``` ### 4. Configure Cline **Config file location:** - **macOS:** `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - **Windows:** `%APPDATA%/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - **Linux:** `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` **Edit the file:** ```bash # macOS nano ~/Library/Application\ Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json ``` **Replace contents with:** ```json { "mcpServers": { "ncp": { "command": "ncp" } } } ``` ### 5. Restart VS Code 1. Close all VS Code windows 2. Reopen VS Code 3. Open Cline extension 4. Start using NCP --- ## 🎯 Using NCP in Cline In Cline chat, ask: - "List all available MCP tools using NCP" - "Find tools for file operations" - "Use NCP to search for GitHub-related tools" --- ## 🐛 Troubleshooting **NCP command not found:** ```bash npm install -g @portel/ncp ncp --version ``` **Cline doesn't see NCP:** 1. Verify config file exists and path is correct 2. Check JSON syntax is valid 3. Restart VS Code completely (close all windows) 4. Check VS Code's Developer Tools console for errors **NCP shows no MCPs:** ```bash ncp list # If empty, add MCPs: ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ``` --- ## 📚 More Resources - **[Claude Desktop Guide](./claude-desktop.md)** - For Claude Desktop users - **[Main README](../../README.md)** - Full documentation - **[Cline Extension](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev)** - VS Code Marketplace --- ## 🤝 Need Help? - **GitHub Issues:** [Report bugs](https://github.com/portel-dev/ncp/issues) - **GitHub Discussions:** [Ask questions](https://github.com/portel-dev/ncp/discussions) ``` -------------------------------------------------------------------------------- /src/utils/progress-spinner.ts: -------------------------------------------------------------------------------- ```typescript /** * Simple CLI Progress Spinner * Shows animated progress during long-running operations */ import chalk from 'chalk'; export class ProgressSpinner { private interval: NodeJS.Timeout | null = null; private frame = 0; private message = ''; private subMessage = ''; private isSpinning = false; private isFirstRender = true; private readonly frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; start(message: string): void { if (this.isSpinning) { this.stop(); } this.message = message; this.subMessage = ''; this.frame = 0; this.isSpinning = true; this.isFirstRender = true; // Add blank line before spinner starts (don't overwrite command line) process.stdout.write('\n'); // Hide cursor process.stdout.write('\u001B[?25l'); this.interval = setInterval(() => { this.render(); this.frame = (this.frame + 1) % this.frames.length; }, 80); this.render(); } updateMessage(message: string, subMessage?: string): void { if (!this.isSpinning) return; this.message = message; if (subMessage !== undefined) { this.subMessage = subMessage; } } updateSubMessage(subMessage: string): void { if (!this.isSpinning) return; this.subMessage = subMessage; } stop(): void { if (!this.isSpinning) return; if (this.interval) { clearInterval(this.interval); this.interval = null; } this.isSpinning = false; // Clear the current line(s) this.clearLines(); // Show cursor process.stdout.write('\u001B[?25h'); } success(message: string): void { this.stop(); console.log(chalk.green(`✅ ${message}`)); } error(message: string): void { this.stop(); console.log(chalk.red(`❌ ${message}`)); } private render(): void { if (!this.isSpinning) return; // Clear previous output (skip on first render to preserve newline) if (!this.isFirstRender) { this.clearLines(); } else { this.isFirstRender = false; } // Main spinner line const spinnerChar = chalk.cyan(this.frames[this.frame]); const mainLine = `${spinnerChar} ${chalk.white(this.message)}`; process.stdout.write(mainLine); // Sub-message line (debug logs) if (this.subMessage) { const subLine = `\n ${chalk.dim(this.subMessage)}`; process.stdout.write(subLine); } } private clearLines(): void { // Move to beginning of line and clear process.stdout.write('\r\u001B[K'); // If there was a sub-message, clear the previous line too if (this.subMessage) { process.stdout.write('\u001B[A\r\u001B[K'); } } } // Export a singleton instance for convenience export const spinner = new ProgressSpinner(); ``` -------------------------------------------------------------------------------- /MCP-CONFIGURATION-SCHEMA-FORMAT.json: -------------------------------------------------------------------------------- ```json { "comment": "MCP InitializeResult with configurationSchema", "description": "This is the JSON format MCP servers return in their initialize() response", "protocolVersion": "0.1.0", "capabilities": { "tools": {}, "resources": {}, "prompts": {} }, "serverInfo": { "name": "example-mcp-server", "version": "1.0.0" }, "instructions": "Optional human-readable setup instructions", "configurationSchema": { "comment": "This is the configuration schema that clients like NCP will use", "environmentVariables": [ { "name": "API_KEY", "description": "API key for service authentication", "type": "string", "required": true, "sensitive": true, "pattern": "^[a-zA-Z0-9]{32}$", "examples": ["abcd1234efgh5678ijkl9012mnop3456"] }, { "name": "DATABASE_URL", "description": "PostgreSQL database connection URL", "type": "url", "required": true, "sensitive": true, "pattern": "^postgresql://.*", "examples": ["postgresql://user:pass@localhost:5432/dbname"] }, { "name": "CONFIG_PATH", "description": "Path to configuration file", "type": "path", "required": false, "default": "~/.config/app.json", "examples": ["/etc/app/config.json", "~/.config/app.json"] }, { "name": "LOG_LEVEL", "description": "Logging level (debug, info, warn, error)", "type": "string", "required": false, "default": "info", "examples": ["debug", "info", "warn", "error"] }, { "name": "MAX_RETRIES", "description": "Maximum number of retry attempts", "type": "number", "required": false, "default": 3, "examples": [3, 5, 10] }, { "name": "ENABLE_CACHE", "description": "Enable response caching", "type": "boolean", "required": false, "default": true, "examples": [true, false] } ], "arguments": [ { "name": "allowed-directory", "description": "Directories that the MCP is allowed to access", "type": "path", "required": true, "multiple": true, "examples": ["/home/user/documents", "/var/data"] }, { "name": "port", "description": "Port number to listen on", "type": "number", "required": false, "default": 8080, "examples": [8080, 3000, 9000] } ], "other": [ { "name": "custom-setting", "description": "Custom configuration setting", "type": "string", "required": false, "examples": ["value1", "value2"] } ] } } ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript /** * Logger utility for NCP * * Controls logging based on context: * - When running as MCP server: minimal/no logging to stderr * - When running as CLI or debugging: full logging */ export class Logger { private static instance: Logger; private isMCPMode: boolean = false; private isCLIMode: boolean = false; private debugMode: boolean = false; private constructor() { // Check if running CLI commands (list, find, run, add, remove, config, etc.) this.isCLIMode = process.argv.some(arg => ['list', 'find', 'run', 'add', 'remove', 'config', '--help', 'help', '--version', '-v', '-h', 'import'].includes(arg) ); // Detect if running as MCP server - more reliable detection // MCP server mode: default when no CLI commands are provided this.isMCPMode = !this.isCLIMode || process.env.NCP_MODE === 'mcp'; // Enable debug mode ONLY if explicitly requested this.debugMode = process.env.NCP_DEBUG === 'true' || process.argv.includes('--debug'); } static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } /** * Log informational messages * Completely suppressed in MCP mode and CLI mode unless debugging */ info(message: string): void { if (this.debugMode) { console.error(`[NCP] ${message}`); } } /** * Log only essential startup messages in MCP mode */ mcpInfo(message: string): void { if (this.debugMode) { console.error(`[NCP] ${message}`); } // In MCP mode and CLI mode, stay completely silent unless debugging } /** * Log debug messages * Only shown in debug mode */ debug(message: string): void { if (this.debugMode) { console.error(`[NCP DEBUG] ${message}`); } } /** * Log error messages * Always shown (but minimal in MCP mode) */ error(message: string, error?: any): void { if (this.isMCPMode && !this.debugMode) { // In MCP mode, only log critical errors if (error?.critical) { console.error(`[NCP ERROR] ${message}`); } } else { console.error(`[NCP ERROR] ${message}`); if (error) { console.error(error); } } } /** * Log warnings * Completely suppressed in MCP mode and CLI mode unless debugging */ warn(message: string): void { if (this.debugMode) { console.error(`[NCP WARN] ${message}`); } } /** * Log progress updates * Completely suppressed in MCP mode and CLI mode unless debugging */ progress(message: string): void { if (this.debugMode) { console.error(`[NCP] ${message}`); } } /** * Check if in MCP mode */ isInMCPMode(): boolean { return this.isMCPMode; } /** * Force enable/disable MCP mode */ setMCPMode(enabled: boolean): void { this.isMCPMode = enabled; } } // Singleton export export const logger = Logger.getInstance(); ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- ```yaml name: Bug Report description: File a bug report to help us improve NCP title: "[Bug]: " labels: ["bug", "triage"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Please provide as much detail as possible to help us reproduce and fix the issue. - type: checkboxes id: terms attributes: label: Prerequisites description: Please confirm the following before submitting options: - label: I have searched existing issues to ensure this is not a duplicate required: true - label: I am using a supported version of NCP (1.1.x or 1.2.x) required: true - label: I have read the documentation and troubleshooting guide required: true - type: input id: version attributes: label: NCP Version description: What version of NCP are you running? placeholder: "e.g., 1.2.1" validations: required: true - type: dropdown id: environment attributes: label: Environment description: What environment are you running NCP in? options: - macOS - Windows - Linux (Ubuntu) - Linux (other) - Docker - Other (specify in description) validations: required: true - type: input id: node-version attributes: label: Node.js Version description: What version of Node.js are you using? placeholder: "e.g., 20.11.0" validations: required: true - type: textarea id: description attributes: label: Bug Description description: A clear and concise description of what the bug is placeholder: Describe what happened and what you expected to happen validations: required: true - type: textarea id: reproduction attributes: label: Steps to Reproduce description: How can we reproduce this issue? placeholder: | 1. Run command '...' 2. Configure MCP server '...' 3. Execute action '...' 4. See error validations: required: true - type: textarea id: expected attributes: label: Expected Behavior description: What did you expect to happen? validations: required: true - type: textarea id: actual attributes: label: Actual Behavior description: What actually happened? validations: required: true - type: textarea id: logs attributes: label: Error Messages/Logs description: Please paste any relevant error messages or logs render: shell - type: textarea id: config attributes: label: NCP Configuration description: | Please share relevant parts of your NCP configuration (remove any sensitive information) render: json - type: textarea id: additional attributes: label: Additional Context description: Add any other context about the problem here placeholder: Screenshots, related issues, potential solutions, etc. ``` -------------------------------------------------------------------------------- /docs/clients/continue.md: -------------------------------------------------------------------------------- ```markdown # Installing NCP on Continue (VS Code Extension) **Method:** JSON configuration only --- ## 📋 Overview Continue is a VS Code extension that supports MCP servers via JSON configuration. Use NCP to consolidate all your MCP tools into a unified, intelligent interface. --- ## 🔧 Installation Steps ### 1. Install NCP ```bash npm install -g @portel/ncp ``` ### 2. Import Your Existing MCPs (Optional) ```bash # If you have MCPs configured elsewhere, copy config to clipboard # Then run: ncp config import ``` ### 3. Add MCPs to NCP ```bash # Add your MCPs ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ncp add github npx @modelcontextprotocol/server-github ncp add memory npx @modelcontextprotocol/server-memory # Verify ncp list ``` ### 4. Configure Continue **Config file location:** - **All platforms:** `~/.continue/config.json` **Edit the file:** ```bash nano ~/.continue/config.json ``` **Add NCP to the `experimental.modelContextProtocolServers` section:** ```json { "models": [...], "experimental": { "modelContextProtocolServers": { "ncp": { "command": "ncp" } } } } ``` > **Note:** Continue uses nested configuration under `experimental.modelContextProtocolServers`, not top-level `mcpServers`. ### 5. Restart VS Code 1. Close all VS Code windows 2. Reopen VS Code 3. Open Continue extension 4. Start using NCP --- ## 🎯 Using NCP in Continue In Continue chat, ask: - "List all available MCP tools" - "Find tools for reading files" - "Use NCP to discover GitHub tools" --- ## 🐛 Troubleshooting **NCP command not found:** ```bash npm install -g @portel/ncp ncp --version ``` **Continue doesn't see NCP:** 1. Verify `~/.continue/config.json` has correct structure 2. Ensure NCP is under `experimental.modelContextProtocolServers` 3. Check JSON syntax is valid 4. Restart VS Code completely 5. Check Continue extension logs **NCP shows no MCPs:** ```bash ncp list # If empty, add MCPs: ncp add filesystem npx @modelcontextprotocol/server-filesystem ~/Documents ``` --- ## 📝 Continue Config Format Reference **Correct format:** ```json { "models": [ { "title": "Claude 3.5 Sonnet", "provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "apiKey": "..." } ], "experimental": { "modelContextProtocolServers": { "ncp": { "command": "ncp" } } } } ``` **⚠️ Note the nested structure:** - `experimental` → `modelContextProtocolServers` → `ncp` --- ## 📚 More Resources - **[Claude Desktop Guide](./claude-desktop.md)** - For Claude Desktop users - **[Main README](../../README.md)** - Full documentation - **[Continue Extension](https://marketplace.visualstudio.com/items?itemName=Continue.continue)** - VS Code Marketplace - **[Continue Docs](https://docs.continue.dev/)** - Official Continue documentation --- ## 🤝 Need Help? - **GitHub Issues:** [Report bugs](https://github.com/portel-dev/ncp/issues) - **GitHub Discussions:** [Ask questions](https://github.com/portel-dev/ncp/discussions) ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown # Pull Request ## Description <!-- Provide a brief description of the changes in this PR --> ## Type of Change <!-- Mark the relevant option with [x] --> - [ ] 🐛 Bug fix (non-breaking change that fixes an issue) - [ ] ✨ New feature (non-breaking change that adds functionality) - [ ] 💥 Breaking change (fix or feature that causes existing functionality to not work as expected) - [ ] 📚 Documentation update - [ ] 🔧 Configuration change - [ ] ⚡ Performance improvement - [ ] 🧪 Test additions or improvements - [ ] 🎨 Code style/formatting changes - [ ] 📦 Dependencies update ## Related Issues <!-- Link to related issues. Use "Closes #123" to auto-close issues when PR is merged --> - Closes # - Related to # ## Changes Made <!-- Provide a detailed list of changes --> ### Added - ### Changed - ### Removed - ### Fixed - ## Testing <!-- Describe the testing you've performed --> ### Test Environment - [ ] Local development - [ ] Docker container - [ ] Multiple Node.js versions - [ ] Multiple operating systems ### Test Cases - [ ] Unit tests pass (`npm test`) - [ ] Integration tests pass - [ ] Manual testing completed - [ ] Edge cases considered ### Test Commands Used ```bash # Add commands used for testing npm test npm run build ``` ## MCP Server Compatibility <!-- If this affects MCP server integration --> - [ ] Tested with multiple MCP servers - [ ] No breaking changes to MCP protocol usage - [ ] Discovery/search functionality unaffected - [ ] Error handling improved/maintained ## Documentation <!-- Check if documentation needs updates --> - [ ] README.md updated (if needed) - [ ] API documentation updated - [ ] Configuration examples updated - [ ] Migration guide provided (for breaking changes) ## Performance Impact <!-- Describe any performance implications --> - [ ] No performance impact expected - [ ] Performance improvement included - [ ] Performance regression possible (explain below) <!-- If performance impact, provide details --> ## Screenshots/Examples <!-- Add screenshots or examples if UI/CLI changes are involved --> ## Checklist <!-- Review checklist before submitting --> ### Code Quality - [ ] Code follows project style guidelines - [ ] Self-review completed - [ ] Code is properly commented - [ ] No console.log or debug statements left - [ ] Error handling is appropriate ### Security - [ ] No sensitive information exposed - [ ] Input validation added where needed - [ ] Security implications considered ### Compatibility - [ ] Changes are backward compatible (or breaking changes documented) - [ ] Works with supported Node.js versions - [ ] Cross-platform compatibility maintained ## Deployment Notes <!-- Any special deployment considerations --> - [ ] No special deployment steps required - [ ] Configuration changes needed (document below) - [ ] Database migrations required - [ ] Environment variables added/changed <!-- Add deployment notes if needed --> ## Reviewer Notes <!-- Anything specific for reviewers to focus on --> ## Additional Context <!-- Add any other context about the pull request here --> ``` -------------------------------------------------------------------------------- /src/services/tool-context-resolver.ts: -------------------------------------------------------------------------------- ```typescript /** * Shared service for resolving tool contexts * Maps MCP names to their context types for parameter prediction */ export class ToolContextResolver { private static readonly contextMap: Record<string, string> = { 'filesystem': 'filesystem', 'memory': 'database', 'shell': 'system', 'sequential-thinking': 'ai', 'portel': 'development', 'tavily': 'web', 'desktop-commander': 'system', 'stripe': 'payment', 'context7-mcp': 'documentation', 'search': 'search', 'weather': 'weather', 'http': 'web', 'github': 'development', 'gitlab': 'development', 'slack': 'communication', 'discord': 'communication', 'email': 'communication', 'database': 'database', 'redis': 'database', 'mongodb': 'database', 'postgresql': 'database', 'mysql': 'database', 'elasticsearch': 'search', 'docker': 'system', 'kubernetes': 'system', 'aws': 'cloud', 'azure': 'cloud', 'gcp': 'cloud' }; /** * Get context for a tool based on its full name (mcp:tool format) */ static getContext(toolIdentifier: string): string { const [mcpName] = toolIdentifier.split(':'); return this.getContextByMCP(mcpName); } /** * Get context for a specific MCP */ static getContextByMCP(mcpName: string): string { if (!mcpName) return 'general'; const normalizedName = mcpName.toLowerCase(); // Direct match if (this.contextMap[normalizedName]) { return this.contextMap[normalizedName]; } // Partial match for common patterns if (normalizedName.includes('file') || normalizedName.includes('fs')) { return 'filesystem'; } if (normalizedName.includes('db') || normalizedName.includes('data')) { return 'database'; } if (normalizedName.includes('web') || normalizedName.includes('http')) { return 'web'; } if (normalizedName.includes('api')) { return 'web'; } if (normalizedName.includes('cloud') || normalizedName.includes('aws') || normalizedName.includes('azure') || normalizedName.includes('gcp')) { return 'cloud'; } if (normalizedName.includes('docker') || normalizedName.includes('container')) { return 'system'; } if (normalizedName.includes('git')) { return 'development'; } return 'general'; } /** * Get all known contexts */ static getAllContexts(): string[] { const contexts = new Set(Object.values(this.contextMap)); contexts.add('general'); return Array.from(contexts).sort(); } /** * Check if a context is known */ static isKnownContext(context: string): boolean { return this.getAllContexts().includes(context); } /** * Add or update a context mapping (for runtime configuration) */ static addMapping(mcpName: string, context: string): void { this.contextMap[mcpName.toLowerCase()] = context; } /** * Get all MCP names for a specific context */ static getMCPsForContext(context: string): string[] { return Object.entries(this.contextMap) .filter(([_, ctx]) => ctx === context) .map(([mcp, _]) => mcp) .sort(); } } ``` -------------------------------------------------------------------------------- /src/utils/updater.ts: -------------------------------------------------------------------------------- ```typescript /** * Auto-Update System for @portel/ncp * * Checks NPM registry for newer versions and notifies users. * Follows npm best practices - users control when to update. */ import { logger } from './logger.js'; import { version as packageVersion, packageName } from './version.js'; interface VersionInfo { current: string; latest: string; isOutdated: boolean; updateCommand: string; } export class NPCUpdater { private readonly packageName = packageName; private readonly checkInterval = 24 * 60 * 60 * 1000; // 24 hours private readonly timeout = 5000; // 5 seconds /** * Get current package version from package.json */ private async getCurrentVersion(): Promise<string> { return packageVersion; } /** * Fetch latest version from NPM registry */ private async getLatestVersion(): Promise<string | null> { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await fetch(`https://registry.npmjs.org/${this.packageName}/latest`, { signal: controller.signal, headers: { 'Accept': 'application/json', 'User-Agent': 'ncp-updater/1.0.0' } }); clearTimeout(timeoutId); if (!response.ok) { return null; } const data = await response.json(); return data.version; } catch (error) { // Fail silently - don't disrupt normal operation logger.debug(`Update check failed: ${error}`); return null; } } /** * Compare version strings (semver-like) */ private isNewerVersion(current: string, latest: string): boolean { const currentParts = current.split('.').map(Number); const latestParts = latest.split('.').map(Number); for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { const currentPart = currentParts[i] || 0; const latestPart = latestParts[i] || 0; if (latestPart > currentPart) return true; if (latestPart < currentPart) return false; } return false; } /** * Check if update is available */ async checkForUpdates(): Promise<VersionInfo | null> { const current = await this.getCurrentVersion(); const latest = await this.getLatestVersion(); if (!latest) { return null; // Network/registry error } const isOutdated = this.isNewerVersion(current, latest); return { current, latest, isOutdated, updateCommand: `npm update -g ${this.packageName}` }; } /** * Get update tip for --find results (if update available) */ async getUpdateTip(): Promise<string | null> { try { const versionInfo = await this.checkForUpdates(); if (versionInfo?.isOutdated) { return `🚀 Update available: v${versionInfo.current} → v${versionInfo.latest} (run: ${versionInfo.updateCommand})`; } return null; } catch (error) { // Fail silently - updates shouldn't break normal operation logger.debug(`Update check error: ${error}`); return null; } } } // Singleton instance export const updater = new NPCUpdater(); ``` -------------------------------------------------------------------------------- /test/mock-mcps/postgres-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock PostgreSQL MCP Server * Real MCP server structure with actual tool definitions but mock implementations * This tests discovery without needing actual PostgreSQL connection */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'postgres-test', version: '1.0.0', description: 'PostgreSQL database operations including queries, schema management, and data manipulation' }; const tools = [ { name: 'query', description: 'Execute SQL queries to retrieve data from PostgreSQL database tables. Find records, search data, analyze information.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'SQL query string to execute' }, params: { type: 'array', description: 'Optional parameters for parameterized queries', items: { type: 'string' } } }, required: ['query'] } }, { name: 'insert', description: 'Insert new records into PostgreSQL database tables. Store customer data, add new information, create records.', inputSchema: { type: 'object', properties: { table: { type: 'string', description: 'Target table name' }, data: { type: 'object', description: 'Record data to insert as key-value pairs' } }, required: ['table', 'data'] } }, { name: 'update', description: 'Update existing records in PostgreSQL database tables. Modify customer information, change email addresses, edit data.', inputSchema: { type: 'object', properties: { table: { type: 'string', description: 'Target table name' }, data: { type: 'object', description: 'Updated record data as key-value pairs' }, where: { type: 'string', description: 'WHERE clause conditions for targeting specific records' } }, required: ['table', 'data', 'where'] } }, { name: 'delete', description: 'Delete records from PostgreSQL database tables. Remove old data, clean expired records, purge information.', inputSchema: { type: 'object', properties: { table: { type: 'string', description: 'Target table name' }, where: { type: 'string', description: 'WHERE clause conditions for targeting records to delete' } }, required: ['table', 'where'] } }, { name: 'create_table', description: 'Create new tables in PostgreSQL database with schema definition. Set up user session storage, design tables for customer data.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Table name' }, schema: { type: 'object', description: 'Table schema definition with columns and types' } }, required: ['name', 'schema'] } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/transports/filtered-stdio-transport.ts: -------------------------------------------------------------------------------- ```typescript /** * Filtered Output for MCP Clients * * Since we're running as a client (not intercepting server output), * we need to filter the console output that leaks through when * executing tools via subprocess MCPs. */ import { logger } from '../utils/logger.js'; // Store original console methods const originalConsoleLog = console.log; const originalConsoleError = console.error; const originalConsoleWarn = console.warn; const originalConsoleInfo = console.info; // Track if filtering is active let filteringActive = false; /** * List of patterns to filter from console output */ const FILTER_PATTERNS = [ // MCP server startup messages 'running on stdio', 'MCP Server running', 'MCP server running', 'Server running on stdio', 'Client does not support', // Specific MCP messages 'Secure MCP Filesystem Server', 'Knowledge Graph MCP Server', 'Sequential Thinking MCP Server', 'Stripe MCP Server', // Connection messages 'Connecting to server:', 'Streamable HTTP connection', 'Received exit signal', 'Starting cleanup process', 'Final cleanup on exit', '[Runner]', // Tool execution artifacts 'Shell cwd was reset' ]; /** * Check if a message should be filtered */ function shouldFilter(args: any[]): boolean { if (!filteringActive) return false; const message = args.map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg) ).join(' '); return FILTER_PATTERNS.some(pattern => message.includes(pattern)); } /** * Create a filtered console method */ function createFilteredMethod(originalMethod: typeof console.log): typeof console.log { return function(...args: any[]) { if (!shouldFilter(args)) { originalMethod.apply(console, args); } else if (process.env.NCP_DEBUG_FILTER === 'true') { // In debug mode, show what we're filtering originalConsoleError.call(console, '[Filtered]:', ...args); } } as typeof console.log; } /** * Enable console output filtering */ export function enableOutputFilter(): void { if (filteringActive) return; filteringActive = true; console.log = createFilteredMethod(originalConsoleLog) as typeof console.log; console.error = createFilteredMethod(originalConsoleError) as typeof console.error; console.warn = createFilteredMethod(originalConsoleWarn) as typeof console.warn; console.info = createFilteredMethod(originalConsoleInfo) as typeof console.info; logger.debug('Console output filtering enabled'); } /** * Disable console output filtering */ export function disableOutputFilter(): void { if (!filteringActive) return; filteringActive = false; console.log = originalConsoleLog; console.error = originalConsoleError; console.warn = originalConsoleWarn; console.info = originalConsoleInfo; logger.debug('Console output filtering disabled'); } /** * Execute a function with filtered output */ export async function withFilteredOutput<T>(fn: () => Promise<T>): Promise<T> { enableOutputFilter(); try { return await fn(); } finally { disableOutputFilter(); } } /** * Check if we're in CLI mode (where filtering should be applied) */ export function shouldApplyFilter(): boolean { // Apply filter in CLI mode but not in server mode return !process.argv.includes('--server') && !process.env.NCP_MODE?.includes('mcp'); } ``` -------------------------------------------------------------------------------- /test/mock-mcps/stripe-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock Stripe MCP Server * Real MCP server structure for payment processing testing */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'stripe-test', version: '1.0.0', description: 'Complete payment processing for online businesses including charges, subscriptions, and refunds' }; const tools = [ { name: 'create_payment', description: 'Process credit card payments and charges from customers. Charge customer for order, process payment from customer.', inputSchema: { type: 'object', properties: { amount: { type: 'number', description: 'Payment amount in cents' }, currency: { type: 'string', description: 'Three-letter currency code (USD, EUR, etc.)' }, customer: { type: 'string', description: 'Customer identifier or email' }, description: { type: 'string', description: 'Payment description for records' } }, required: ['amount', 'currency'] } }, { name: 'refund_payment', description: 'Process refunds for previously charged payments. Refund cancelled subscription, return customer money.', inputSchema: { type: 'object', properties: { payment_id: { type: 'string', description: 'Original payment identifier to refund' }, amount: { type: 'number', description: 'Refund amount in cents (optional, defaults to full amount)' }, reason: { type: 'string', description: 'Reason for refund' } }, required: ['payment_id'] } }, { name: 'create_subscription', description: 'Create recurring subscription billing for customers. Set up monthly billing, create subscription plans.', inputSchema: { type: 'object', properties: { customer: { type: 'string', description: 'Customer identifier' }, price: { type: 'string', description: 'Subscription price identifier or amount' }, trial_days: { type: 'number', description: 'Optional trial period in days' }, interval: { type: 'string', description: 'Billing interval (monthly, yearly, etc.)' } }, required: ['customer', 'price'] } }, { name: 'list_payments', description: 'List payment transactions with filtering and pagination. See all payment transactions from today, view payment history.', inputSchema: { type: 'object', properties: { customer: { type: 'string', description: 'Optional customer filter' }, date_range: { type: 'object', description: 'Optional date range filter with start and end dates' }, status: { type: 'string', description: 'Optional payment status filter (succeeded, failed, pending)' }, limit: { type: 'number', description: 'Maximum number of results to return' } } } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- ```yaml name: Feature Request description: Suggest a new feature or enhancement for NCP title: "[Feature]: " labels: ["enhancement", "triage"] body: - type: markdown attributes: value: | Thanks for suggesting a new feature! Please provide detailed information to help us understand your request. - type: checkboxes id: terms attributes: label: Prerequisites description: Please confirm the following before submitting options: - label: I have searched existing issues to ensure this feature hasn't been requested required: true - label: I have checked the roadmap to see if this feature is already planned required: true - type: dropdown id: type attributes: label: Feature Type description: What type of feature is this? options: - New MCP Server Support - CLI Enhancement - Discovery/Search Improvement - Performance Optimization - Developer Experience - Documentation - Other validations: required: true - type: textarea id: problem attributes: label: Problem Statement description: What problem does this feature solve? placeholder: "As a [user type], I want [functionality] so that [benefit]" validations: required: true - type: textarea id: solution attributes: label: Proposed Solution description: Describe your ideal solution for this problem placeholder: What would you like to see implemented? validations: required: true - type: textarea id: alternatives attributes: label: Alternatives Considered description: What other approaches have you considered? placeholder: Are there workarounds or alternative solutions you've tried? - type: dropdown id: priority attributes: label: Priority Level description: How important is this feature to you? options: - Critical - Blocking my workflow - High - Would significantly improve my workflow - Medium - Nice to have improvement - Low - Minor convenience validations: required: true - type: textarea id: use-cases attributes: label: Use Cases description: Describe specific scenarios where this feature would be useful placeholder: | 1. When working with [specific MCP/workflow]... 2. During [specific development phase]... 3. For users who need to [specific task]... - type: textarea id: examples attributes: label: Examples/Mockups description: | Provide examples, mockups, or references to similar implementations You can attach images or link to examples from other tools - type: checkboxes id: contribution attributes: label: Contribution description: Would you be interested in contributing to this feature? options: - label: I would be willing to help implement this feature - label: I can provide testing/feedback during development - label: I can help with documentation - type: textarea id: additional attributes: label: Additional Context description: Any other information that would help us understand this request placeholder: Links to relevant documentation, similar features in other tools, etc. ``` -------------------------------------------------------------------------------- /test/mock-mcps/slack-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock Slack MCP Server * Real MCP server structure for Slack integration testing */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'slack-test', version: '1.0.0', description: 'Slack integration for messaging, channel management, file sharing, and team communication' }; const tools = [ { name: 'send_message', description: 'Send messages to Slack channels or direct messages. Share updates, notify teams, communicate with colleagues.', inputSchema: { type: 'object', properties: { channel: { type: 'string', description: 'Channel name or user ID to send message to' }, text: { type: 'string', description: 'Message content to send' }, thread_ts: { type: 'string', description: 'Optional thread timestamp for replies' } }, required: ['channel', 'text'] } }, { name: 'create_channel', description: 'Create new Slack channels for team collaboration. Set up project channels, organize team discussions.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Channel name' }, purpose: { type: 'string', description: 'Channel purpose description' }, private: { type: 'boolean', description: 'Whether channel should be private' } }, required: ['name'] } }, { name: 'upload_file', description: 'Upload files to Slack channels for sharing and collaboration. Share documents, images, code files.', inputSchema: { type: 'object', properties: { file: { type: 'string', description: 'File path or content to upload' }, channels: { type: 'string', description: 'Comma-separated list of channel names' }, title: { type: 'string', description: 'File title' }, initial_comment: { type: 'string', description: 'Initial comment when sharing file' } }, required: ['file', 'channels'] } }, { name: 'get_channel_history', description: 'Retrieve message history from Slack channels. Read past conversations, search team discussions.', inputSchema: { type: 'object', properties: { channel: { type: 'string', description: 'Channel ID to get history from' }, count: { type: 'number', description: 'Number of messages to retrieve' }, oldest: { type: 'string', description: 'Oldest timestamp for message range' }, latest: { type: 'string', description: 'Latest timestamp for message range' } }, required: ['channel'] } }, { name: 'set_channel_topic', description: 'Set or update channel topic and purpose. Update channel information, set discussion guidelines.', inputSchema: { type: 'object', properties: { channel: { type: 'string', description: 'Channel ID' }, topic: { type: 'string', description: 'New channel topic' } }, required: ['channel', 'topic'] } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json { "manifest_version": "0.2", "name": "ncp", "display_name": "NCP - Natural Context Provider", "version": "1.5.0", "description": "N-to-1 MCP Orchestration. Unified gateway for multiple MCP servers with intelligent tool discovery and auto-import.", "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.", "author": { "name": "Portel", "url": "https://github.com/portel-dev/ncp" }, "user_config": { "profile": { "type": "string", "title": "Profile Name", "description": "Which NCP profile to use (e.g., 'all', 'development', 'minimal'). Auto-imported MCPs from your MCP client will be added to this profile.", "default": "all" }, "config_path": { "type": "string", "title": "Configuration Path", "description": "Where to store NCP configurations. Use '~/.ncp' for global (shared across projects), '.ncp' for local (per-project), or specify custom path.", "default": "~/.ncp" }, "enable_global_cli": { "type": "boolean", "title": "Enable Global CLI Access", "description": "Create a global 'ncp' command for terminal usage. Allows running NCP from command line anywhere on your system.", "default": false }, "auto_import_client_mcps": { "type": "boolean", "title": "Auto-import Client MCPs", "description": "Automatically import all MCPs from your MCP client (Claude Desktop, Perplexity, Cursor, etc.) on startup. Syncs both config files and extensions.", "default": true }, "enable_debug_logging": { "type": "boolean", "title": "Enable Debug Logging", "description": "Show detailed logs for troubleshooting (runtime detection, MCP loading, etc.)", "default": false } }, "server": { "type": "node", "entry_point": "dist/index-mcp.js", "mcp_config": { "command": "node", "args": [ "${__dirname}/dist/index-mcp.js", "--profile=${user_config.profile}", "--config-path=${user_config.config_path}" ], "env": { "NCP_PROFILE": "${user_config.profile}", "NCP_CONFIG_PATH": "${user_config.config_path}", "NCP_ENABLE_GLOBAL_CLI": "${user_config.enable_global_cli}", "NCP_AUTO_IMPORT": "${user_config.auto_import_client_mcps}", "NCP_DEBUG": "${user_config.enable_debug_logging}", "NCP_MODE": "extension" } } }, "tools": [ { "name": "find", "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." }, { "name": "run", "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." } ], "keywords": ["mcp", "model-context-protocol", "ai-orchestration", "tool-discovery", "multi-mcp", "claude", "ai-gateway", "auto-import"], "license": "Elastic-2.0" } ``` -------------------------------------------------------------------------------- /test/mock-mcps/github-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock GitHub MCP Server * Real MCP server structure for GitHub API integration testing */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'github-test', version: '1.0.0', description: 'GitHub API integration for repository management, file operations, issues, and pull requests' }; const tools = [ { name: 'create_repository', description: 'Create a new GitHub repository with configuration options. Set up new project, initialize repository.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Repository name' }, description: { type: 'string', description: 'Repository description' }, private: { type: 'boolean', description: 'Whether repository should be private' }, auto_init: { type: 'boolean', description: 'Initialize with README' } }, required: ['name'] } }, { name: 'create_issue', description: 'Create GitHub issues for bug reports and feature requests. Report bugs, request features, track tasks.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Issue title' }, body: { type: 'string', description: 'Issue description' }, labels: { type: 'array', description: 'Issue labels', items: { type: 'string' } }, assignees: { type: 'array', description: 'User assignments', items: { type: 'string' } } }, required: ['title'] } }, { name: 'create_pull_request', description: 'Create pull requests for code review and merging changes. Submit code changes, request reviews.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Pull request title' }, body: { type: 'string', description: 'Pull request description' }, head: { type: 'string', description: 'Source branch' }, base: { type: 'string', description: 'Target branch' } }, required: ['title', 'head', 'base'] } }, { name: 'get_file_contents', description: 'Read file contents from GitHub repositories. Access source code, read configuration files.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'File path in repository' }, ref: { type: 'string', description: 'Branch or commit reference' } }, required: ['path'] } }, { name: 'search_repositories', description: 'Search GitHub repositories by keywords, topics, and filters. Find open source projects, discover libraries.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query with keywords' }, sort: { type: 'string', description: 'Sort criteria (stars, forks, updated)' }, order: { type: 'string', description: 'Sort order (asc, desc)' } }, required: ['query'] } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /test/mock-mcps/notion-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock Notion MCP Server * Real MCP server structure for Notion workspace integration testing */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'notion-test', version: '1.0.0', description: 'Notion workspace management for documents, databases, and collaborative content creation' }; const tools = [ { name: 'create_page', description: 'Create new Notion pages and documents with content. Write notes, create documentation, start new projects.', inputSchema: { type: 'object', properties: { parent: { type: 'string', description: 'Parent page or database ID' }, title: { type: 'string', description: 'Page title' }, content: { type: 'array', description: 'Page content blocks' }, properties: { type: 'object', description: 'Page properties if parent is database' } }, required: ['parent', 'title'] } }, { name: 'create_database', description: 'Create structured Notion databases with properties and schema. Set up project tracking, create data tables.', inputSchema: { type: 'object', properties: { parent: { type: 'string', description: 'Parent page ID' }, title: { type: 'string', description: 'Database title' }, properties: { type: 'object', description: 'Database schema properties' }, description: { type: 'string', description: 'Database description' } }, required: ['parent', 'title', 'properties'] } }, { name: 'query_database', description: 'Query Notion databases with filtering and sorting. Search data, find records, analyze information.', inputSchema: { type: 'object', properties: { database_id: { type: 'string', description: 'Database ID to query' }, filter: { type: 'object', description: 'Query filter conditions' }, sorts: { type: 'array', description: 'Sort criteria' }, start_cursor: { type: 'string', description: 'Pagination cursor' } }, required: ['database_id'] } }, { name: 'update_page', description: 'Update existing Notion pages with new content and properties. Edit documents, modify data, update information.', inputSchema: { type: 'object', properties: { page_id: { type: 'string', description: 'Page ID to update' }, properties: { type: 'object', description: 'Properties to update' }, content: { type: 'array', description: 'New content blocks to append' } }, required: ['page_id'] } }, { name: 'search_pages', description: 'Search across Notion workspace for pages and content. Find documents, locate information, discover content.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query text' }, filter: { type: 'object', description: 'Search filter criteria' }, sort: { type: 'object', description: 'Sort results by criteria' } } } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/pr-schema-additions.ts: -------------------------------------------------------------------------------- ```typescript /** * Configuration Schema Types * * These types should be added to schema/draft/schema.ts */ /** * Describes a configuration parameter needed by the server. */ export interface ConfigurationParameter { /** * Unique identifier for this parameter (e.g., "GITHUB_TOKEN", "allowed-directory") */ name: string; /** * Human-readable description of what this parameter is for */ description: string; /** * Type of the parameter value */ type: "string" | "number" | "boolean" | "path" | "url"; /** * Whether this parameter is required for the server to function */ required: boolean; /** * Whether this contains sensitive data (passwords, API keys) * If true, clients should mask input when prompting users */ sensitive?: boolean; /** * Default value if not provided by the user */ default?: string | number | boolean; /** * Whether multiple values are allowed (for array parameters) */ multiple?: boolean; /** * Validation pattern (regex) for string parameters */ pattern?: string; /** * Example values to help users understand expected format */ examples?: string[]; } /** * Declares configuration requirements for the server. * * Servers can use this to communicate what environment variables, * command-line arguments, or other configuration they need to function properly. * * This enables clients to: * - Detect missing configuration before attempting connection * - Prompt users interactively for required values * - Validate configuration before startup * - Provide helpful error messages */ export interface ConfigurationSchema { /** * Environment variables required by the server */ environmentVariables?: ConfigurationParameter[]; /** * Command-line arguments required by the server */ arguments?: ConfigurationParameter[]; /** * Other configuration requirements (files, URLs, etc.) */ other?: ConfigurationParameter[]; } /** * MODIFICATION TO EXISTING InitializeResult INTERFACE * * Add this field to the existing InitializeResult interface: */ export interface InitializeResult extends Result { protocolVersion: string; capabilities: ServerCapabilities; serverInfo: Implementation; instructions?: string; /** * Optional schema declaring the server's configuration requirements. * * Servers can use this to communicate what environment variables, * command-line arguments, or other configuration they need. * * Clients can use this information to: * - Validate configuration before attempting connection * - Prompt users for missing required configuration * - Provide better error messages and setup guidance * * This field is optional and backward compatible - servers that don't * provide it continue to work as before. * * @example * ```typescript * // Filesystem server declaring path requirement * { * "configurationSchema": { * "arguments": [{ * "name": "allowed-directory", * "description": "Directory path that the server is allowed to access", * "type": "path", * "required": true, * "multiple": true * }] * } * } * * // API server declaring token requirement * { * "configurationSchema": { * "environmentVariables": [{ * "name": "GITHUB_TOKEN", * "description": "GitHub personal access token with repo permissions", * "type": "string", * "required": true, * "sensitive": true, * "pattern": "^ghp_[a-zA-Z0-9]{36}$" * }] * } * } * ``` */ configurationSchema?: ConfigurationSchema; } ``` -------------------------------------------------------------------------------- /test/mock-mcps/neo4j-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock Neo4j MCP Server * Real MCP server structure for Neo4j graph database testing */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'neo4j-test', version: '1.0.0', description: 'Neo4j graph database server with schema management and read/write cypher operations' }; const tools = [ { name: 'execute_cypher', description: 'Execute Cypher queries on Neo4j graph database. Query relationships, find patterns, analyze connections.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Cypher query string' }, parameters: { type: 'object', description: 'Query parameters as key-value pairs' }, database: { type: 'string', description: 'Target database name' } }, required: ['query'] } }, { name: 'create_node', description: 'Create nodes in Neo4j graph with labels and properties. Add entities, create graph elements.', inputSchema: { type: 'object', properties: { labels: { type: 'array', description: 'Node labels', items: { type: 'string' } }, properties: { type: 'object', description: 'Node properties as key-value pairs' } }, required: ['labels'] } }, { name: 'create_relationship', description: 'Create relationships between nodes in Neo4j graph. Connect entities, define associations, build graph structure.', inputSchema: { type: 'object', properties: { from_node_id: { type: 'string', description: 'Source node ID' }, to_node_id: { type: 'string', description: 'Target node ID' }, relationship_type: { type: 'string', description: 'Relationship type/label' }, properties: { type: 'object', description: 'Relationship properties' } }, required: ['from_node_id', 'to_node_id', 'relationship_type'] } }, { name: 'find_path', description: 'Find paths between nodes in Neo4j graph database. Discover connections, analyze relationships, trace routes.', inputSchema: { type: 'object', properties: { start_node: { type: 'object', description: 'Starting node criteria' }, end_node: { type: 'object', description: 'Ending node criteria' }, relationship_types: { type: 'array', description: 'Allowed relationship types', items: { type: 'string' } }, max_depth: { type: 'number', description: 'Maximum path depth' } }, required: ['start_node', 'end_node'] } }, { name: 'manage_schema', description: 'Manage Neo4j database schema including indexes and constraints. Optimize queries, ensure data integrity.', inputSchema: { type: 'object', properties: { action: { type: 'string', description: 'Schema action (create_index, drop_index, create_constraint, drop_constraint)' }, label: { type: 'string', description: 'Node label or relationship type' }, properties: { type: 'array', description: 'Properties for index/constraint', items: { type: 'string' } }, constraint_type: { type: 'string', description: 'Constraint type (unique, exists, key)' } }, required: ['action', 'label', 'properties'] } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/testing/setup-dummy-mcps.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node /** * Setup Dummy MCPs for Testing * * Creates NCP profile configurations that use dummy MCP servers for testing * the semantic enhancement system without requiring real MCP connections. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { getNcpBaseDirectory } from '../utils/ncp-paths.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); interface McpDefinitionsFile { mcps: Record<string, any>; } async function setupDummyMcps(): Promise<void> { try { // Load MCP definitions const definitionsPath = path.join(__dirname, 'mcp-definitions.json'); const definitionsContent = await fs.readFile(definitionsPath, 'utf-8'); const definitions: McpDefinitionsFile = JSON.parse(definitionsContent); // Get NCP base directory and ensure profiles directory exists const ncpBaseDir = await getNcpBaseDirectory(); const profilesDir = path.join(ncpBaseDir, 'profiles'); await fs.mkdir(profilesDir, { recursive: true }); // Create test profile configuration const profileConfig = { name: "semantic-test", description: "Testing profile with dummy MCPs for semantic enhancement validation", mcpServers: {} as Record<string, any>, metadata: { created: new Date().toISOString(), modified: new Date().toISOString() } }; // Build dummy MCP server path const dummyServerPath = path.join(__dirname, 'dummy-mcp-server.ts'); const nodeExecutable = process.execPath; const tsNodePath = path.join(path.dirname(nodeExecutable), 'npx'); // Add each MCP from definitions as a dummy MCP for (const [mcpName, mcpDef] of Object.entries(definitions.mcps)) { profileConfig.mcpServers[mcpName] = { command: 'npx', args: [ 'tsx', // Use tsx to run TypeScript directly dummyServerPath, '--mcp-name', mcpName, '--definitions-file', definitionsPath ] }; } // Write profile configuration const profilePath = path.join(profilesDir, 'semantic-test.json'); await fs.writeFile(profilePath, JSON.stringify(profileConfig, null, 2)); console.log(`✅ Created semantic-test profile with ${Object.keys(definitions.mcps).length} dummy MCPs:`); Object.keys(definitions.mcps).forEach(name => { console.log(` - ${name}: ${definitions.mcps[name].description}`); }); console.log(`\nProfile saved to: ${profilePath}`); console.log(`\nTo use this profile:`); console.log(` npx ncp --profile semantic-test list`); console.log(` npx ncp --profile semantic-test find "commit my code to git"`); console.log(` npx ncp --profile semantic-test run git:commit --params '{"message":"test commit"}'`); // Create a simplified test profile with just key MCPs for faster testing const quickTestConfig = { name: "semantic-quick", description: "Quick test profile with essential MCPs for semantic enhancement", mcpServers: { shell: profileConfig.mcpServers.shell, git: profileConfig.mcpServers.git, postgres: profileConfig.mcpServers.postgres, openai: profileConfig.mcpServers.openai }, metadata: { created: new Date().toISOString(), modified: new Date().toISOString() } }; const quickProfilePath = path.join(profilesDir, 'semantic-quick.json'); await fs.writeFile(quickProfilePath, JSON.stringify(quickTestConfig, null, 2)); console.log(`\n✅ Created semantic-quick profile with 4 essential MCPs`); console.log(`Profile saved to: ${quickProfilePath}`); } catch (error) { console.error('Failed to setup dummy MCPs:', error); process.exit(1); } } // Main execution if (import.meta.url === `file://${process.argv[1]}`) { setupDummyMcps(); } ``` -------------------------------------------------------------------------------- /src/services/config-schema-reader.ts: -------------------------------------------------------------------------------- ```typescript /** * Configuration Schema Reader * * Reads configurationSchema from MCP InitializeResult * Caches schemas for future use during `ncp add` and `ncp repair` */ export interface ConfigurationParameter { name: string; description: string; type: 'string' | 'number' | 'boolean' | 'path' | 'url'; required: boolean; sensitive?: boolean; default?: string | number | boolean; multiple?: boolean; pattern?: string; examples?: string[]; } export interface ConfigurationSchema { environmentVariables?: ConfigurationParameter[]; arguments?: ConfigurationParameter[]; other?: ConfigurationParameter[]; } export interface InitializeResult { protocolVersion: string; capabilities: any; serverInfo: { name: string; version: string; }; instructions?: string; configurationSchema?: ConfigurationSchema; } export class ConfigSchemaReader { /** * Extract configuration schema from InitializeResult */ readSchema(initResult: InitializeResult): ConfigurationSchema | null { if (!initResult || !initResult.configurationSchema) { return null; } return initResult.configurationSchema; } /** * Get all required parameters from schema */ getRequiredParameters(schema: ConfigurationSchema): ConfigurationParameter[] { const required: ConfigurationParameter[] = []; if (schema.environmentVariables) { required.push(...schema.environmentVariables.filter(p => p.required)); } if (schema.arguments) { required.push(...schema.arguments.filter(p => p.required)); } if (schema.other) { required.push(...schema.other.filter(p => p.required)); } return required; } /** * Check if schema has any required parameters */ hasRequiredConfig(schema: ConfigurationSchema | null): boolean { if (!schema) return false; return this.getRequiredParameters(schema).length > 0; } /** * Get parameter by name from schema */ getParameter(schema: ConfigurationSchema, name: string): ConfigurationParameter | null { const allParams = [ ...(schema.environmentVariables || []), ...(schema.arguments || []), ...(schema.other || []) ]; return allParams.find(p => p.name === name) || null; } /** * Format schema for display */ formatSchema(schema: ConfigurationSchema): string { const lines: string[] = []; if (schema.environmentVariables && schema.environmentVariables.length > 0) { lines.push('Environment Variables:'); schema.environmentVariables.forEach(param => { const required = param.required ? '(required)' : '(optional)'; const sensitive = param.sensitive ? ' [sensitive]' : ''; lines.push(` - ${param.name} ${required}${sensitive}`); lines.push(` ${param.description}`); if (param.examples && param.examples.length > 0 && !param.sensitive) { lines.push(` Examples: ${param.examples.join(', ')}`); } }); lines.push(''); } if (schema.arguments && schema.arguments.length > 0) { lines.push('Command Arguments:'); schema.arguments.forEach(param => { const required = param.required ? '(required)' : '(optional)'; const multiple = param.multiple ? ' [multiple]' : ''; lines.push(` - ${param.name} ${required}${multiple}`); lines.push(` ${param.description}`); if (param.examples && param.examples.length > 0) { lines.push(` Examples: ${param.examples.join(', ')}`); } }); lines.push(''); } if (schema.other && schema.other.length > 0) { lines.push('Other Configuration:'); schema.other.forEach(param => { const required = param.required ? '(required)' : '(optional)'; lines.push(` - ${param.name} ${required}`); lines.push(` ${param.description}`); }); } return lines.join('\n'); } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mcp_server_request.yml: -------------------------------------------------------------------------------- ```yaml name: MCP Server Support Request description: Request support for a new MCP server in NCP title: "[MCP]: Add support for " labels: ["mcp-server", "enhancement", "triage"] body: - type: markdown attributes: value: | Request support for a new MCP server in NCP's discovery and management system. - type: input id: server-name attributes: label: MCP Server Name description: What is the name of the MCP server? placeholder: "e.g., @modelcontextprotocol/server-slack" validations: required: true - type: input id: repository attributes: label: Repository URL description: Link to the MCP server's repository placeholder: "https://github.com/..." validations: required: true - type: input id: npm-package attributes: label: NPM Package (if available) description: NPM package name if the server is published placeholder: "e.g., @modelcontextprotocol/server-slack" - type: textarea id: description attributes: label: Server Description description: What does this MCP server do? placeholder: Brief description of the server's functionality validations: required: true - type: dropdown id: category attributes: label: Server Category description: What category best describes this server? options: - Database (SQL, NoSQL, etc.) - Cloud Services (AWS, Azure, GCP) - Development Tools (Git, Docker, etc.) - Communication (Slack, Discord, etc.) - File System - Web/API Services - Productivity Tools - System Administration - Other validations: required: true - type: checkboxes id: server-status attributes: label: Server Status description: Please verify the server status options: - label: The server is actively maintained required: true - label: The server has clear documentation required: true - label: The server follows MCP protocol specifications required: true - type: textarea id: tools attributes: label: Available Tools description: List the main tools/functions this server provides placeholder: | - send_message: Send messages to channels - create_channel: Create new channels - list_channels: Get available channels - type: textarea id: use-cases attributes: label: Use Cases description: When would users want to use this MCP server? placeholder: | - Automating Slack notifications - Managing team communication - Integrating with CI/CD pipelines - type: input id: priority-score attributes: label: Priority Justification description: Why should this server be prioritized? placeholder: "Popular tool with X GitHub stars, requested by Y users, fills gap in Z domain" - type: textarea id: configuration attributes: label: Configuration Requirements description: What configuration is needed to use this server? placeholder: | Required: - API_TOKEN: Slack bot token - WORKSPACE_ID: Slack workspace identifier Optional: - DEFAULT_CHANNEL: Default channel for messages - type: checkboxes id: contribution attributes: label: Contribution description: Can you help with implementation? options: - label: I can help test the integration - label: I can provide configuration examples - label: I can help with documentation - label: I maintain or contribute to this MCP server - type: textarea id: additional attributes: label: Additional Information description: Any other relevant information placeholder: Links to documentation, similar integrations, special considerations ``` -------------------------------------------------------------------------------- /test/mock-mcps/playwright-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Mock Playwright MCP Server * Real MCP server structure for browser automation testing */ import { MockMCPServer } from './base-mock-server.js'; const serverInfo = { name: 'playwright-test', version: '1.0.0', description: 'Browser automation and web scraping with cross-browser support' }; const tools = [ { name: 'navigate_to_page', description: 'Navigate to web pages and URLs for automation tasks. Open websites, load web applications.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to' }, wait_until: { type: 'string', description: 'Wait condition (load, domcontentloaded, networkidle)' }, timeout: { type: 'number', description: 'Navigation timeout in milliseconds' } }, required: ['url'] } }, { name: 'click_element', description: 'Click on web page elements using selectors. Click buttons, links, form elements.', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector or XPath for element' }, button: { type: 'string', description: 'Mouse button to click (left, right, middle)' }, click_count: { type: 'number', description: 'Number of clicks' } }, required: ['selector'] } }, { name: 'fill_form_field', description: 'Fill form inputs and text fields on web pages. Enter text, complete forms, input data.', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for input field' }, value: { type: 'string', description: 'Text value to fill' }, clear: { type: 'boolean', description: 'Clear field before filling' } }, required: ['selector', 'value'] } }, { name: 'take_screenshot', description: 'Capture screenshots of web pages for testing and documentation. Take page screenshots, save visual evidence.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'File path to save screenshot' }, full_page: { type: 'boolean', description: 'Capture full page or just viewport' }, quality: { type: 'number', description: 'JPEG quality (0-100)' } }, required: ['path'] } }, { name: 'extract_text', description: 'Extract text content from web page elements. Scrape data, read page content, get element text.', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for element' }, attribute: { type: 'string', description: 'Optional attribute to extract instead of text' } }, required: ['selector'] } }, { name: 'wait_for_element', description: 'Wait for elements to appear or become available on web pages. Wait for dynamic content, ensure element visibility.', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for element to wait for' }, state: { type: 'string', description: 'Element state to wait for (visible, hidden, attached)' }, timeout: { type: 'number', description: 'Wait timeout in milliseconds' } }, required: ['selector'] } } ]; // Create and run the server const server = new MockMCPServer(serverInfo, tools); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/utils/security.ts: -------------------------------------------------------------------------------- ```typescript /** * Security utilities for NCP * Handles sensitive data masking and sanitization */ /** * Masks sensitive information in command strings * Detects and masks API keys, tokens, passwords, etc. */ export function maskSensitiveData(text: string): string { if (!text) return text; let masked = text; // Mask API keys (various patterns) masked = masked.replace( /sk_test_[a-zA-Z0-9]{50,}/g, (match) => `sk_test_*****${match.slice(-4)}` ); masked = masked.replace( /sk_live_[a-zA-Z0-9]{50,}/g, (match) => `sk_live_*****${match.slice(-4)}` ); // Mask other common API key patterns masked = masked.replace( /--api-key[=\s]+([a-zA-Z0-9_-]{16,})/gi, (match, key) => match.replace(key, `*****${key.slice(-4)}`) ); // Mask --key parameters masked = masked.replace( /--key[=\s]+([a-zA-Z0-9_-]{16,})/gi, (match, key) => match.replace(key, `*****${key.slice(-4)}`) ); // Mask tokens masked = masked.replace( /--token[=\s]+([a-zA-Z0-9_-]{16,})/gi, (match, token) => match.replace(token, `*****${token.slice(-4)}`) ); // Mask passwords masked = masked.replace( /--password[=\s]+([^\s]+)/gi, (match, password) => match.replace(password, '*****') ); // Mask JWT tokens masked = masked.replace( /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, (match) => `eyJ*****${match.slice(-4)}` ); // Mask UUID-like keys masked = masked.replace( /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, (match) => `*****${match.slice(-4)}` ); return masked; } /** * Formats command display with proper masking * @param showAsTemplates - If true, shows template variables like {{API_KEY}} instead of masked values */ export function formatCommandDisplay(command: string, args: string[] = [], showAsTemplates: boolean = true): string { const fullCommand = `${command} ${args.join(' ')}`.trim(); if (showAsTemplates) { return maskSensitiveDataAsTemplates(fullCommand); } return maskSensitiveData(fullCommand); } /** * Masks sensitive data by replacing with template variable names * This provides cleaner display without exposing any part of secrets */ export function maskSensitiveDataAsTemplates(text: string): string { if (!text) return text; let masked = text; // Replace API key parameters masked = masked.replace( /--api-key[=\s]+([^\s]+)/gi, '--api-key={{API_KEY}}' ); // Replace key parameters (like Upstash keys) masked = masked.replace( /--key[=\s]+([^\s]+)/gi, '--key={{API_KEY}}' ); // Replace token parameters masked = masked.replace( /--token[=\s]+([^\s]+)/gi, '--token={{TOKEN}}' ); // Replace OAuth tokens masked = masked.replace( /--oauth-token[=\s]+([^\s]+)/gi, '--oauth-token={{OAUTH_TOKEN}}' ); // Replace password parameters masked = masked.replace( /--password[=\s]+([^\s]+)/gi, '--password={{PASSWORD}}' ); // Replace secret parameters masked = masked.replace( /--secret[=\s]+([^\s]+)/gi, '--secret={{SECRET}}' ); // Replace auth parameters masked = masked.replace( /--auth[=\s]+([^\s]+)/gi, '--auth={{AUTH}}' ); // Replace environment variable references that look like they contain secrets masked = masked.replace( /\$\{?([A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|API|AUTH)[A-Z_]*)\}?/g, '{{$1}}' ); return masked; } /** * Checks if a string contains sensitive data patterns */ export function containsSensitiveData(text: string): boolean { if (!text) return false; const sensitivePatterns = [ /sk_test_[a-zA-Z0-9]{99}/, /sk_live_[a-zA-Z0-9]{99}/, /--api-key[=\s]+[a-zA-Z0-9_-]{16,}/i, /--token[=\s]+[a-zA-Z0-9_-]{16,}/i, /--password[=\s]+[^\s]+/i, /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/, /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i ]; return sensitivePatterns.some(pattern => pattern.test(text)); } ``` -------------------------------------------------------------------------------- /src/utils/runtime-detector.ts: -------------------------------------------------------------------------------- ```typescript /** * Runtime Detector * * Detects which runtime (bundled vs system) NCP is currently running with. * This is detected fresh on every boot to respect Claude Desktop's dynamic settings. */ import { existsSync } from 'fs'; import { getBundledRuntimePath } from './client-registry.js'; export interface RuntimeInfo { /** The runtime being used ('bundled' or 'system') */ type: 'bundled' | 'system'; /** Path to Node.js runtime to use */ nodePath: string; /** Path to Python runtime to use (if available) */ pythonPath?: string; } /** * Detect which runtime NCP is currently running with. * * Strategy: * 1. Check process.execPath (how NCP was launched) * 2. Compare with known bundled runtime paths * 3. If match → we're running via bundled runtime * 4. If no match → we're running via system runtime */ export function detectRuntime(): RuntimeInfo { const currentNodePath = process.execPath; // Check if we're running via Claude Desktop's bundled Node const claudeBundledNode = getBundledRuntimePath('claude-desktop', 'node'); const claudeBundledPython = getBundledRuntimePath('claude-desktop', 'python'); // If our execPath matches the bundled Node path, we're running via bundled runtime if (claudeBundledNode && currentNodePath === claudeBundledNode) { return { type: 'bundled', nodePath: claudeBundledNode, pythonPath: claudeBundledPython || undefined }; } // Check if execPath is inside Claude.app (might be different bundled path) const isInsideClaudeApp = currentNodePath.includes('/Claude.app/') || currentNodePath.includes('\\Claude\\') || currentNodePath.includes('/Claude/'); if (isInsideClaudeApp && claudeBundledNode && existsSync(claudeBundledNode)) { // We're running from Claude Desktop, use its bundled runtimes return { type: 'bundled', nodePath: claudeBundledNode, pythonPath: claudeBundledPython || undefined }; } // Otherwise, we're running via system runtime return { type: 'system', nodePath: 'node', // Use system node pythonPath: 'python3' // Use system python }; } /** * Get runtime to use for spawning .dxt extension processes. * Uses the same runtime that NCP itself is running with. */ export function getRuntimeForExtension(command: string): string { const runtime = detectRuntime(); // If command is 'node' or ends with '/node', use detected Node runtime if (command === 'node' || command.endsWith('/node') || command.endsWith('\\node.exe')) { return runtime.nodePath; } // If command is 'npx', use npx from detected Node runtime if (command === 'npx' || command.endsWith('/npx') || command.endsWith('\\npx.cmd')) { // If using bundled runtime, construct npx path from node path if (runtime.type === 'bundled') { // Bundled node path: /Applications/Claude.app/.../node // Bundled npx path: /Applications/Claude.app/.../npx const npxPath = runtime.nodePath.replace(/\/node$/, '/npx').replace(/\\node\.exe$/, '\\npx.cmd'); return npxPath; } // For system runtime, use system npx return 'npx'; } // If command is 'python3'/'python', use detected Python runtime if (command === 'python3' || command === 'python' || command.endsWith('/python3') || command.endsWith('/python') || command.endsWith('\\python.exe') || command.endsWith('\\python3.exe')) { return runtime.pythonPath || command; // Fallback to original if no Python detected } // For other commands, return as-is return command; } /** * Log runtime detection info for debugging */ export function logRuntimeInfo(): void { const runtime = detectRuntime(); console.log(`[Runtime Detection]`); console.log(` Type: ${runtime.type}`); console.log(` Node: ${runtime.nodePath}`); if (runtime.pythonPath) { console.log(` Python: ${runtime.pythonPath}`); } console.log(` Process execPath: ${process.execPath}`); } ```