Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
extension-init.ts8.67 kB
/** * Extension Initialization * * Handles NCP initialization when running as a Claude Desktop extension (.dxt). * Processes user configuration and sets up the environment accordingly. */ import { homedir } from 'os'; import { join } from 'path'; import { existsSync, mkdirSync, symlinkSync, unlinkSync, chmodSync, readlinkSync } from 'fs'; import { logger } from '../utils/logger.js'; import { importFromClient } from '../utils/client-importer.js'; import ProfileManager from '../profiles/profile-manager.js'; export interface ExtensionConfig { profile: string; configPath: string; enableGlobalCLI: boolean; autoImport: boolean; debug: boolean; } /** * Parse extension configuration from environment variables */ export function parseExtensionConfig(): ExtensionConfig { return { profile: process.env.NCP_PROFILE || 'all', configPath: expandPath(process.env.NCP_CONFIG_PATH || '~/.ncp'), enableGlobalCLI: process.env.NCP_ENABLE_GLOBAL_CLI === 'true', autoImport: process.env.NCP_AUTO_IMPORT !== 'false', // Default true debug: process.env.NCP_DEBUG === 'true' }; } /** * Expand ~ to home directory */ function expandPath(path: string): string { if (path.startsWith('~/')) { return join(homedir(), path.slice(2)); } return path; } /** * Initialize NCP as an extension */ export async function initializeExtension(): Promise<void> { const config = parseExtensionConfig(); if (config.debug) { process.env.NCP_DEBUG = 'true'; // Note: Debug logs are now written to file instead of console // to avoid Claude Desktop notification spam } // 1. Ensure config directory exists ensureConfigDirectory(config.configPath); // Log configuration when debug is enabled if (config.debug) { logger.debug(`Extension Configuration:`); logger.debug(` Profile: ${config.profile}`); logger.debug(` Config Path: ${config.configPath}`); logger.debug(` Global CLI: ${config.enableGlobalCLI}`); logger.debug(` Auto-import: ${config.autoImport}`); const logFile = logger.getLogFilePath(); if (logFile) { logger.debug(` Log file: ${logFile}`); } } // 2. Set up global CLI if enabled if (config.enableGlobalCLI) { await setupGlobalCLI(config.debug); } // Note: Auto-import is handled by ProfileManager.initialize() // No need to call it here - avoids duplicate imports logger.info(`✅ NCP extension initialized (profile: ${config.profile})`); } /** * Ensure configuration directory exists */ function ensureConfigDirectory(configPath: string): void { const profilesDir = join(configPath, 'profiles'); if (!existsSync(profilesDir)) { mkdirSync(profilesDir, { recursive: true }); logger.info(`Created NCP config directory: ${profilesDir}`); } } /** * Set up global CLI access via symlink */ async function setupGlobalCLI(debug: boolean): Promise<void> { try { // Find NCP executable (within extension bundle) const extensionDir = join(__dirname, '..'); const ncpExecutable = join(extensionDir, 'dist', 'index.js'); if (!existsSync(ncpExecutable)) { logger.warn('NCP executable not found, skipping global CLI setup'); return; } // Create symlink in /usr/local/bin (requires sudo, may fail) const globalLink = '/usr/local/bin/ncp'; // Check if existing symlink points to npm package if (existsSync(globalLink)) { try { const target = readlinkSync(globalLink); // If it points to npm package, don't overwrite if (target.includes('node_modules/@portel/ncp') || target.includes('node_modules\\@portel\\ncp')) { logger.info('NPM-installed ncp detected, skipping DXT global CLI setup'); logger.info('Using npm version. To use DXT version, run: npm uninstall -g @portel/ncp'); if (debug) { logger.debug(`Existing symlink: ${globalLink} -> ${target}`); } return; } // Otherwise, remove the existing symlink (might be old DXT or something else) unlinkSync(globalLink); if (debug) { logger.debug(`Removed existing symlink: ${globalLink} -> ${target}`); } } catch (err) { // Ignore errors - might be a regular file or permission issue // Will try to create symlink anyway } } // Try to create symlink try { symlinkSync(ncpExecutable, globalLink); chmodSync(globalLink, 0o755); logger.info('✅ Global CLI access enabled: ncp command available'); logger.debug(`Created symlink: ${globalLink} -> ${ncpExecutable}`); } catch (err: any) { // Likely permission error logger.warn(`Could not create global CLI link (requires sudo): ${err.message}`); logger.info(`Run manually: sudo ln -sf ${ncpExecutable} /usr/local/bin/ncp`); } } catch (error: any) { logger.error(`Failed to set up global CLI: ${error.message}`); } } /** * Auto-import MCPs from Claude Desktop */ async function autoImportClaudeMCPs(profileName: string, debug: boolean): Promise<void> { try { logger.debug('Auto-importing Claude Desktop MCPs...'); // Import from Claude Desktop const result = await importFromClient('claude-desktop'); if (!result || result.count === 0) { logger.debug('No MCPs found in Claude Desktop config'); return; } // Initialize profile manager const profileManager = new ProfileManager(); await profileManager.initialize(); // Get or create profile let profile = await profileManager.getProfile(profileName); if (!profile) { profile = { name: profileName, description: `Auto-imported from Claude Desktop`, mcpServers: {}, metadata: { created: new Date().toISOString(), modified: new Date().toISOString() } }; } // Import each MCP let importedCount = 0; let skippedNCP = 0; for (const [name, config] of Object.entries(result.mcpServers)) { // Skip NCP instances (avoid importing ourselves!) if (isNCPInstance(name, config)) { skippedNCP++; logger.debug(`Skipping ${name} (NCP instance - avoiding recursion)`); continue; } // Skip if already exists (don't overwrite user configs) if (profile!.mcpServers[name]) { logger.debug(`Skipping ${name} (already configured)`); continue; } // Detect transport type for logging const transport = detectTransportType(config); // Add to profile profile!.mcpServers[name] = config; importedCount++; const source = config._source || 'config'; logger.debug(`Imported ${name} from ${source} (transport: ${transport})`); } // Update metadata profile!.metadata.modified = new Date().toISOString(); // Save profile await profileManager.saveProfile(profile!); logger.info(`✅ Auto-imported ${importedCount} MCPs from Claude Desktop into '${profileName}' profile`); if (skippedNCP > 0) { logger.info(` (Skipped ${skippedNCP} NCP instance${skippedNCP > 1 ? 's' : ''} to avoid recursion)`); } logger.debug(`Total MCPs in profile: ${Object.keys(profile!.mcpServers).length}`); } catch (error: any) { logger.error(`Failed to auto-import Claude Desktop MCPs: ${error.message}`); } } /** * Check if running as extension */ export function isRunningAsExtension(): boolean { return process.env.NCP_MODE === 'extension'; } /** * Detect transport type from MCP config */ function detectTransportType(config: any): string { // HTTP/SSE transport uses 'url' field (Claude Desktop native support) if (config.url) { return 'HTTP/SSE'; } // stdio transport uses 'command' and 'args' fields if (config.command) { return 'stdio'; } return 'unknown'; } /** * Detect if an MCP config is an NCP instance * Prevents importing ourselves and causing recursion */ function isNCPInstance(name: string, config: any): boolean { // Check 1: Name contains "ncp" (case-insensitive) if (name.toLowerCase().includes('ncp')) { return true; } // Check 2: Command points to NCP executable const command = config.command?.toLowerCase() || ''; if (command.includes('ncp')) { return true; } // Check 3: Args contain NCP-specific flags const args = config.args || []; const argsStr = args.join(' ').toLowerCase(); if (argsStr.includes('--profile') || argsStr.includes('ncp')) { return true; } // Check 4: Display name in env vars const env = config.env || {}; if (env.NCP_PROFILE || env.NCP_DISPLAY_NAME) { return true; } return false; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server