This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Build the project
npm run build
# Run tests
npm run test # Unit tests
npm run test:integration # CLI entry point integration tests
npm run test:integration:dxt # DXT entry point integration tests
npm run test:critical # Critical protocol and timeout tests
npm run test:e2e # End-to-end tests
# Development mode
npm run dev # Build and start CLI
# DXT build
npm run build:dxt # Build DXT package (uses build-dxt-clean.sh)
npm run build:dxt:patched # Build with build directory patches
npm run test:dxt # Test DXT package# Jest single test file
npx jest tests/path/to/test.test.ts
# Debug specific test
NODE_OPTIONS=--experimental-vm-modules npx jest tests/your-test.test.ts --verbose --detectOpenHandles# Tool discovery
node dist/index.js find "search query"
node dist/index.js find # List all tools
# Tool execution
node dist/index.js run mcp:tool --params '{"key": "value"}'
node dist/index.js run mcp:tool --dry-run # Preview without executing
# Configuration
node dist/index.js list # List all MCPs
node dist/index.js add mcp-name npx @package/name
node dist/index.js remove mcp-nameGitHub (Public): Only essentials
- Package files (package.json, package-lock.json)
- TypeScript config (tsconfig.json, jest.config.js)
- Public documentation (README.md, LICENSE, CLAUDE.md, CHANGELOG.md, etc.)
- Source & tests (src/, tests/, docs/)
Local Only (_internal/ - gitignored): Development/experimental files
_internal/scripts/- One-time testing utilities (profile builders, ecosystem tests)_internal/planning/- Research docs, feature plans, analysis_internal/schemas/- MCP configuration examples, test data_internal/archives/- Backups, old metadata_internal/artifacts/- Build outputs_internal/test-configs/- Test configuration files
These are never committed to GitHub—keep development clutter local.
Before committing or releasing:
npm run check:root # Validates repository cleanlinessThe check runs automatically before npm run release and npm run prepublishOnly.
NCP has two entry points serving different use cases:
-
CLI Entry (
src/index.ts→src/cli/index.ts)- Purpose: Command-line tools (
ncp find,ncp add,ncp list, etc.) - Installation:
npm install -g @portel/ncp - Build output:
dist/index.js - Includes: Full CLI functionality + MCP server
- Purpose: Command-line tools (
-
MCP Server Entry (
src/index-mcp.ts)- Purpose: DXT package (.mcpb), runs as MCP server only
- Installation: Claude Desktop and other MCP clients
- Build output:
dist/index-mcp.js - Includes: MCP server only (no CLI to minimize bundle size)
Important: Both entry points use the SAME MCPServer class from src/server/mcp-server.ts. This ensures consistent behavior across CLI and DXT environments.
┌─────────────────────────────────────────────────────────────┐
│ Client │
│ (Claude Desktop, Cursor, VS Code, or CLI commands) │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MCPServer │
│ (src/server/mcp-server.ts) │
│ - MCP protocol handling (uses official @modelcontextprotocol/sdk) │
│ - Request routing (find/run/code) │
│ - Tool definition exposure │
│ - Prompts/Resources handling │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ NCPOrchestrator │
│ (src/orchestrator/ncp-orchestrator.ts) │
│ - Core orchestration logic │
│ - MCP connection management │
│ - Tool discovery and execution │
│ - Health monitoring │
└───┬───────────────┬───────────────┬───────────────┬─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│Discovery│ │ Internal│ │ Code │ │ Photon │
│ Engine │ │ MCP │ │ Executor │ │ Runtime │
└─────────┘ └──────────┘ └──────────┘ └──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
Vector search Internal MCP TypeScript .photon.ts files
Tool discovery (schedule, Sandbox Dynamic loading
skills, etc.) Worker Threads and execution
-
ToolDiscoveryService (
src/orchestrator/services/tool-discovery.ts)- Tool search and discovery logic
- Vector similarity matching
- Multi-query support
-
CacheService (
src/orchestrator/services/cache-service.ts)- CSV cache management for fast loading
- Schema cache for tool metadata
- Incremental cache updates
-
ConnectionPoolManager (
src/orchestrator/services/connection-pool.ts)- MCP connection pool management
- Health checks and reconnection
- Transport factory (stdio, SSE, HTTP)
-
SkillsService (
src/orchestrator/services/skills-service.ts)- Skills discovery and management
- Marketplace integration
- Resource loading
-
PhotonService (
src/orchestrator/services/photon-service.ts)- Photon loading and execution
- Custom MCP support via .photon.ts files
- Dynamic adapter creation
src/
├── cli/ # CLI command-line interface
│ ├── index.ts # CLI entry point router
│ └── commands/ # CLI command implementations (add, find, list, etc.)
├── server/ # MCP server
│ ├── mcp-server.ts # Main server implementation (SDK-based)
│ └── mcp-prompts.ts # Prompt definitions for user interactions
├── orchestrator/ # Core orchestration logic
│ ├── ncp-orchestrator.ts # Main orchestrator class
│ └── services/ # Orchestrator services
│ ├── tool-discovery.ts
│ ├── cache-service.ts
│ ├── connection-pool.ts
│ ├── skills-service.ts
│ └── photon-service.ts
├── internal-mcps/ # Internal MCP implementations
│ ├── scheduler.ts # Scheduling MCP (cron jobs)
│ ├── skills.ts # Skills management MCP
│ ├── analytics.ts # Usage analytics MCP
│ ├── ncp-management.ts # NCP management (add/remove/list)
│ ├── code.ts # Code execution MCP
│ ├── marketplace.ts # MCP registry client
│ ├── *.photon.ts # Photon runtime files (intelligence, shell)
│ ├── internal-mcp-manager.ts # Internal MCP coordinator
│ └── photon-loader.ts # Photon file loader
├── code-mode/ # Code execution mode
│ ├── code-executor.ts # Main code execution engine
│ ├── code-worker.ts # Worker thread implementation
│ ├── sandbox/ # Sandbox implementations
│ │ ├── index.ts # Sandbox selection (4-tier)
│ │ └── subprocess-sandbox.ts
│ ├── validation/ # Code security validation
│ │ ├── code-analyzer.ts # AST-based static analysis
│ │ └── semantic-validator.ts # Pattern-based validation
│ └── network-policy.ts # Network permission manager
├── discovery/ # Tool discovery engine
│ ├── engine.ts # Main discovery engine
│ ├── rag-engine.ts # Vector search engine
│ └── semantic-enhancement-engine.ts
├── cache/ # Caching system
│ ├── csv-cache.ts # CSV cache for tool metadata
│ ├── schema-cache.ts # Schema cache for tool definitions
│ ├── cache-patcher.ts # Incremental cache updates
│ └── version-aware-validator.ts
├── auth/ # Authentication handling
│ ├── oauth-device-flow.ts # OAuth 2.0 Device Flow
│ ├── oauth-auth-code-flow.ts # OAuth 2.1 Auth Code + PKCE
│ ├── mcp-oauth-provider.ts # OAuth provider integration
│ ├── token-store.ts # Secure token storage
│ └── secure-credential-store.ts
├── services/ # Various services
│ ├── tool-finder.ts # Tool finding and search
│ ├── cli-*.ts # CLI-related services
│ └── ...
├── profiles/ # Profile management
│ └── profile-manager.ts
├── utils/ # Utility functions
│ ├── logger.ts
│ ├── ncp-paths.ts
│ └── ...
├── index.ts # CLI entry point
└── index-mcp.ts # MCP server entry point
Photon is NCP's custom TypeScript MCP system that allows users to write .photon.ts files to extend NCP functionality without publishing npm packages.
- Discovery: Scan
~/.ncp/photons/and project-local.ncp/photons/directories - Load: Dynamic import of
.photon.tsfiles using ES modules - Adapt: Convert Photon to internal MCP using
PhotonAdapter - Expose: Photon tools exposed via
find/runto AI clients
// ~/.ncp/photons/my-tool.photon.ts
// Manifest (required)
export const manifest = {
name: 'my-tool',
version: '1.0.0',
description: 'My custom tool',
author: 'Your Name'
};
// Tool functions (exported async functions become MCP tools)
export async function myFunction(params: { input: string }) {
return { result: `Processed: ${params.input}` };
}
export async function anotherTool(params: { x: number; y: number }) {
return { sum: params.x + params.y };
}Photon function parameters are automatically converted to JSON schemas using TypeScript type inference:
- Parameter names → Schema property names
- TypeScript types → JSON Schema types
- JSDoc comments → Schema descriptions
- CLI/DXT global: Set
enablePhotonRuntime: truein~/.ncp/settings.json - Environment variable:
NCP_ENABLE_PHOTON_RUNTIME=true - DXT bundle: Set env var in client config (DXT ignores settings.json):
{
"mcpServers": {
"ncp": {
"command": "ncp-mcp",
"env": {
"NCP_ENABLE_PHOTON_RUNTIME": "true"
}
}
}
}NCP includes built-in Photons in src/internal-mcps/:
- intelligence.photon.ts - AI-powered tool discovery
- shell.photon.ts - Shell command execution
These are loaded automatically when Photon runtime is enabled.
| Aspect | Photon | Internal MCP |
|---|---|---|
| Location | ~/.ncp/photons/ or .ncp/photons/ |
src/internal-mcps/*.ts |
| Purpose | User extensions | Core NCP functionality |
| Loading | Dynamic import | Built-in compilation |
| Distribution | User-created | Shipped with NCP |
| Examples | Custom tools, integrations | scheduler, skills, analytics |
Before committing ANY changes to server code, verify ALL of these:
- Did I run the test suite? (
npm run test) - Did I test both CLI and DXT entry points? (
npm run test:integrationANDnpm run test:integration:dxt) - Did all integration tests pass?
- Did I run the comprehensive DXT test? (
npm run build && node tests/integration/comprehensive-dxt-test.cjs) - Did all 5 comprehensive tests pass?
- If I added a feature, did I add tests for it?
- If I'm touching
src/index-mcp.ts, did I verifyawait server.run()is present? - If I'm adding MCP protocol handlers, did I test with
notifications/initialized?
If ANY checkbox is unchecked, DO NOT COMMIT. Fix the issue first.
We use ONE official SDK-based server implementation:
src/server/mcp-server.ts(MCPServer) - Uses official @modelcontextprotocol/sdk- Used by CLI entry point (dist/index.js)
- Used by DXT entry point (dist/index-mcp.js)
- Fully async initialization with background indexing
- Immediate protocol responses (no blocking)
Historical Context: We previously had TWO implementations (custom MCPServer and SDK-based MCPServerSDK) which caused recurring bugs:
- Features added to one but forgotten in the other
- Tests only covered CLI entry point, missing DXT bugs
- Every release had bugs discovered by users in production
Solution: Eliminated duplication by using ONLY the official @modelcontextprotocol/sdk:
- Single source of truth for all MCP protocol handling
- Official SDK ensures protocol compliance
- No feature divergence possible
- All tests cover the same implementation
- NEVER block the event loop: MCP clients (Claude Desktop, Cursor) will timeout and reject connections if protocol responses are delayed
- Background initialization is mandatory: Server must respond to protocol handshakes immediately, run heavy initialization in background promises
- NO synchronous file operations: Always use async file APIs (
readFile,writeFile,access) instead of sync versions (readFileSync,writeFileSync,existsSync) - Cache operations must be async: Any disk I/O during initialization must not block - use
awaitconsistently - Test with production scenarios: If building for MCP server mode, test as MCP server, not just CLI
- ALWAYS await async initialization: src/index-mcp.ts MUST await server.run() - wrap in async IIFE if needed:
(async () => { const server = new MCPServer(profileName); await server.run(); // REQUIRED - process exits without await })();
- Send notifications/initialized: After initialize response, send notification to trigger oninitialized callback:
// After receiving initialize response test.sendNotification('notifications/initialized', {});
- Incremental caching is critical: When one MCP changes, only re-index that MCP, not all 37
- Track what actually changed: Use sets/maps to track newly indexed items, only save those to cache
- Avoid redundant writes: Before writing to cache, check if data actually changed
- Profile hash comparison: Use config hashes to detect changes, not timestamps
- Dual cache system: CSV cache for fast loading, metadata cache for schemas
- Per-MCP patching: Use
patchAddMCP()for individual MCP updates, not full cache rewrites - Schema preservation: Tool schemas MUST be saved to metadata cache during indexing
- Cache migration: Detect missing metadata and auto-trigger re-indexing for backward compatibility
- Metadata validation: Always check if schemas exist before loading from cache
CRITICAL: Tests MUST match production environment
-
Test BOTH entry points:
npm run test:integration # CLI entry (dist/index.js) npm run test:integration:dxt # DXT entry (dist/index-mcp.js)
-
Comprehensive DXT test (tests/integration/comprehensive-dxt-test.cjs):
- ✅ Server initialization with clientInfo
- ✅ Auto-import detects and imports extensions
- ✅ Tool discovery (find) works
- ✅ Tool execution (run) works
- ✅ Multiple sequential requests without crashes
-
Use correct profile paths in tests:
- DXT uses local
.ncp/directory (process.cwd()) - CLI uses global
~/.ncp/directory - Tests must match the environment they're testing
- DXT uses local
-
Follow MCP protocol completely:
// 1. Send initialize request const id = test.sendRequest('initialize', { protocolVersion: '2024-11-05', clientInfo: { name: 'claude-desktop', version: '0.14.0' } }); // 2. Wait for response await test.waitForResponse(id); // 3. Send initialized notification (REQUIRED to trigger oninitialized) test.sendNotification('notifications/initialized', {});
- Test all CLI commands: When asked to test, actually run every command listed (find, run, list, etc.)
- Test with actual installations: Use
npm linkand test from different directories - No hardcoded fallbacks: Fallback values hide real errors - fail fast to surface issues
- Verify schema display: Tool discovery showing
[no parameters]means schemas weren't saved - Check logs: When debugging, grep for specific operations like "Patching tool metadata" or "Saving.*MCP"
- Test with JSON-RPC directly: Don't involve AI in tests - use direct JSON-RPC communication
- Verify auto-import: Check that profile file is updated with new MCPs after initialization
IMPORTANT: Code execution is NOT just "display code results". It actually EXECUTES TypeScript with full MCP access via Worker Threads.
Both the code tool (direct code-mode) and code:run tool (scheduled jobs) use the same mechanism:
User executes code
↓
orchestrator.executeCode(code, timeout)
↓
CodeExecutor.executeCode()
↓
4-Tier Sandbox Selection (most secure available):
1. IsolatedVMSandbox (V8 Isolate via isolated-vm) - Cloudflare Workers tech
2. SubprocessSandbox (separate Node.js process)
3. Worker Threads (vm module isolation)
4. Direct VM (fallback)
↓
Sandbox creates namespaces:
- schedule.* (for scheduling)
- ncp.* (for discovery)
- analytics.* (for analytics)
- mail.* (for email)
- github.* (for GitHub)
- ... all other MCPs ...
↓
Code executes with full MCP access
↓
Results returned
When code executes, all MCPs are available as callable namespaces:
// All of these work in code execution:
const jobs = await schedule.list({ limit: 10 });
const tools = await ncp.find({ description: "email" });
const overview = await analytics.overview({});
const mails = await mail({ operation: "unread" });
// Can orchestrate multiple MCPs in one execution:
const emails = await mail({ operation: "list", limit: 5 });
const notes = await notes({ operation: "create", text: "..." });
const msg = await messages({ operation: "send", number: "..." });-
Worker Thread Isolation: Code runs in isolated Worker thread with resource limits
- Memory: 128MB max
- Execution time: 30s default, 5min max
- Network: Controlled via NetworkPolicyManager
-
MCP Namespace Access: All enabled MCPs available as namespaces
- Tools passed from
toolsProvider()which includes:- External MCPs (from ~/.ncp/config.json)
- Internal MCPs (schedule, analytics, skills, code, ncp)
- Skill tools (from ~/.ncp/skills/)
- Tools passed from
-
Tool Executor Callback: When code calls an MCP method, messages are sent to main thread
- Tool call: Worker → Main thread
- Tool result: Main thread → Worker
- Synchronous from code perspective (via Promise handling)
-
Security Hardening (Defense in Depth):
Static Analysis (AST-based):
- TypeScript compiler API parses code before execution
- Blocks dangerous globals: eval, Function, process, require, __dirname, __filename
- Blocks metaprogramming: Reflect, Proxy, Symbol, WeakRef, FinalizationRegistry
- Blocks descriptor manipulation: defineProperty, setPrototypeOf, getOwnPropertyDescriptor
- Detects prototype pollution: proto, constructor, prototype access
Semantic Validation (Pattern-based):
- Detects malicious intent patterns:
- Data exfiltration (read credentials + send externally)
- Credential harvesting (multiple secret/password accesses)
- Reconnaissance (system enumeration)
- Persistence mechanisms (cron, startup scripts)
- Backdoor patterns (reverse shells, remote access)
- Privilege escalation (sudo, admin, root)
- Data destruction (delete_all, rm -rf, truncate)
- Suspicious namespace+method combinations (shell., ssh., exec.*)
Runtime Protection:
- Built-in prototypes frozen (Object, Array, Function, RegExp, Error, Promise)
- Dangerous globals deleted from context
- Network requests routed through NetworkPolicyManager
- Memory limits enforced (128MB default)
- Execution timeout (30s default, 5min max)
V8 Isolate Separation (when isolated-vm available):
- Completely separate V8 isolate (no shared memory)
- Memory limits enforced at V8 level
- Same technology as Cloudflare Workers
- No access to Node.js APIs by design
| Aspect | code tool (direct) |
code:run (scheduled) |
|---|---|---|
| Entry point | MCP tools/call handler |
Internal MCP tool |
| Execution | orchestrator.executeCode() |
orchestrator.executeCode() |
| MCP Access | ✅ Full namespace injection | ✅ Full namespace injection |
| Return Format | Formatted text for Claude | JSON result |
| Use Case | Direct in Claude chat | Scheduled job automation |
| Difference | NONE - Same mechanism |
This is critical: When designing scheduled jobs with code:run, assume MCPs are available as namespaces. They will be.
When testing code execution:
- Don't test for "no namespace injection" - it WILL be injected
- Test that MCPs are callable - verify methods execute
- Test error handling - what happens when a method fails
- Test multi-MCP orchestration - do multiple calls work together
Example test:
const code = `
const schedule_result = await schedule.list({ limit: 1 });
const ncp_result = await ncp.find({ description: "test" });
const analytics_result = await analytics.overview({});
return {
schedule_works: !!schedule_result,
ncp_works: !!ncp_result,
analytics_works: !!analytics_result
};
`;
// This will return { schedule_works: true, ncp_works: true, analytics_works: true }Best Practice: Use camelCase consistently throughout the entire codebase (code AND configuration files)
- Variables, functions, methods:
camelCase(e.g.,enableCodeMode,autoImport,maxDebugFiles) - Classes, interfaces, types:
PascalCase(e.g.,GlobalSettings,MCPServer,InternalMCP) - Constants:
UPPER_CASE(e.g.,DEFAULT_SETTINGS,MAX_RETRIES) - JSON configuration:
camelCasefor consistency with TypeScript (e.g., manifest.json, settings.json)
Key Principle: NEVER mix camelCase and snake_case in the same codebase. Consistency prevents confusion and makes code maintainable. JavaScript/TypeScript systems standardize on camelCase (even for JSON configuration files).
Application to NCP:
- manifest.json: Use camelCase for all config keys
- ✅
enableCodeMode(notenable_code_mode) - ✅
enableSkills(notenable_skills) - ✅
enableScheduleMcp(notenable_schedule_mcp) - ✅
enablePhotonRuntime(notenable_photon_runtime)
- ✅
- Environment variables: Use UPPER_SNAKE_CASE (follows convention for env vars)
NCP_ENABLE_CODE_MODE,NCP_ENABLE_SKILLS, etc.
- TypeScript interfaces/code: Use camelCase
- GlobalSettings.enableCodeMode
- GlobalSettings.enableSkills
References:
- TypeScript ESLint Naming Convention Rule
- TypeScript Deep Dive Style Guide
- JSON Naming Conventions Stack Overflow
- JSON Best Practices Blog
- Complete the root cause fix: Don't just fix symptoms, understand and fix the underlying issue
- Verify incremental changes work: When optimizing caching, test that partial updates work correctly
- Old cache migration: New features must handle users with old cache format gracefully
- Document assumptions: If code assumes something (like all MCPs have schemas), validate it
CRITICAL: Always use npm run build:dxt which runs scripts/build-dxt-clean.sh. Never use manual build steps.
- Problem:
@anthropic-ai/mcpbbundler excludesbuild/directories from node_modules by default - Impact: Dependencies like
human-signalsrequirebuild/src/main.jsto work - missing causes immediate crash - Symptoms: "ERR_MODULE_NOT_FOUND: Cannot find module '.../build/src/main.js'" in logs
- Root Cause: mcpb treats build/ as build artifacts to exclude, even when they're published package code
- Solution:
build-dxt-clean.shautomatically patches missing build directories after mcpb packing
- Build TypeScript (
npm run build) - Create clean temp directory with production files only
- Fresh
npm install --omit=devin clean directory (no mixed dev/prod dependencies) - Pack with mcpb (creates initial zip)
- Workaround: Extract DXT (unzip), manually add missing build directories, re-pack (zip)
- Critical: Use
unzip/zipcommands - DXT is ZIP format, NOT tar.gz! - Auto-test: Verify zip format, dependencies, and test server startup
- Output size and SHA256 hash for verification
- Dependency verification: Check critical build directories exist (human-signals/build)
- MCP spec timing: Initialize < 5000ms, tools/list < 2000ms (tested automatically)
- No crashes: Server must stay up during initialize → tools/list sequence
- Test unpacked DXT: Always unpack and test before distributing to catch packaging issues early
- Global error handlers:
index-mcp.tshas uncaughtException and unhandledRejection handlers - stderr logging: All errors log to stderr with
[NCP FATAL]or[NCP ERROR]prefix - Stack traces: Full stack traces included for debugging in Claude Desktop logs
- Explicit exit: Process exits with code 1 on fatal errors (no silent failures)
Before making ANY release, verify ALL of these:
# 1. Check all GitHub issues are resolved
gh issue list --state open
# Must return empty - no open issues allowed before release
# 2. Verify CI is passing on all platforms
gh run list --limit 1
# Must show "success" for the latest run
# 3. Run full test suite locally
npm run test:critical
npm run test:e2e
npm run test:integration
npm run test:integration:dxt
# 4. Build and test DXT package
npm run build:dxt:patched
npm run test:dxtIf ANY check fails, DO NOT RELEASE. Fix the issue first.
- No release until user confirms: Build and test, but wait for explicit confirmation before releasing
- Test incrementally: After each fix, rebuild and verify it works before moving to next issue
- Always use build:dxt: Never manually run mcpb - use the tested build script
- Verify Windows CI: Windows tests must pass - check
Run Tests (windows-latest)job specifically