UNPKG

twenty-mcp-server

Version:

Easy-to-install Model Context Protocol server for Twenty CRM. Try instantly with 'npx twenty-mcp-server setup' or install globally for permanent use.

584 lines โ€ข 24.2 kB
import chalk from 'chalk'; import inquirer from 'inquirer'; import { ConfigManager } from '../config-manager.js'; import crypto from 'crypto'; import { isIP } from 'node:net'; import { crossPlatformSpawn } from '../platform-utils.js'; import { getExecutionContext } from '../utils/execution-context.js'; import { explainNPXConfiguration, showNPXCompletion } from '../utils/npx-helpers.js'; export async function enhancedSetupCommand(options) { const executionContext = getExecutionContext(); // Show context-appropriate setup header if (executionContext.type === 'npx') { console.log(chalk.bold.green('๐Ÿ› ๏ธ Twenty MCP Server Setup (via npx)')); console.log(chalk.gray('Configure your Twenty MCP Server - settings will be saved globally\n')); } else { console.log(chalk.bold.green('๐Ÿ› ๏ธ Twenty MCP Server Enhanced Setup Wizard')); console.log(chalk.gray('Professional configuration for your Twenty MCP Server\n')); } try { // For npx users, always use global config unless explicitly specified const useGlobal = executionContext.type === 'npx' ? true : (options.global || false); const configManager = new ConfigManager(useGlobal ? undefined : process.cwd()); let config; // Handle special modes if (options.reset) { return await resetConfiguration(configManager); } if (options.import) { return await importExistingConfig(configManager); } // Load or create configuration if (configManager.exists()) { console.log(chalk.blue('๐Ÿ“‹ Existing configuration found')); const shouldUpdate = await confirmUpdate(); if (!shouldUpdate) { console.log(chalk.gray('Setup cancelled')); return; } config = configManager.load(); } else { console.log(chalk.blue('๐Ÿ†• Creating new configuration')); config = configManager.load(); // Gets defaults } // Welcome and overview await showWelcome(executionContext); // Step 1: Twenty CRM Configuration config = await setupTwentyCRM(config); // Step 2: Authentication (optional or if --oauth flag) if (options.oauth || await shouldSetupFeature('OAuth 2.1 Authentication', 'Secure multi-user access')) { config = await setupAuthentication(config); } // Step 3: IP Protection (optional or if --ip-protection flag) if (options.ipProtection || await shouldSetupFeature('IP Address Protection', 'Network-level access control')) { config = await setupIPProtection(config); } // Step 4: Server Configuration config = await setupServerSettings(config); // Step 5: User Preferences config = await setupUserPreferences(config); // Step 6: Save configuration await saveConfiguration(configManager, config); // Step 7: Test configuration (unless --skip-tests) if (!options.skipTests) { await testConfiguration(configManager); } // Step 8: Show completion and next steps await showCompletion(config, configManager, executionContext); } catch (error) { if (error instanceof Error && error.message === 'SETUP_CANCELLED') { console.log(chalk.yellow('\nโš ๏ธ Setup cancelled by user')); return; } console.error(chalk.red('\nโŒ Setup failed:'), error instanceof Error ? error.message : error); process.exit(1); } } async function confirmUpdate() { const { update } = await inquirer.prompt([ { type: 'confirm', name: 'update', message: 'Update existing configuration?', default: true, }, ]); return update; } async function showWelcome(executionContext) { if (executionContext.type === 'npx') { console.log(chalk.bold.cyan('\n๐ŸŽ‰ Welcome to Twenty MCP Server Setup!')); console.log(chalk.gray('\nYou\'re configuring Twenty MCP Server via npx - perfect for trying it out!')); console.log(chalk.gray('Your configuration will be saved globally and persist between npx runs.\n')); explainNPXConfiguration(); } else { console.log(chalk.bold.cyan('\n๐ŸŽ‰ Welcome to Twenty MCP Server Setup!')); console.log(chalk.gray('\nThis wizard will help you configure your Twenty CRM integration for AI assistants.')); console.log(chalk.gray('We\'ll walk through each feature and explain the benefits.\n')); } const { ready } = await inquirer.prompt([ { type: 'confirm', name: 'ready', message: 'Ready to begin?', default: true, }, ]); if (!ready) { throw new Error('SETUP_CANCELLED'); } } async function shouldSetupFeature(featureName, description) { console.log(chalk.bold.blue(`\n๐Ÿค” ${featureName}`)); console.log(chalk.gray(description + '\n')); const { enable } = await inquirer.prompt([ { type: 'confirm', name: 'enable', message: `Configure ${featureName}?`, default: false, }, ]); return enable; } async function setupTwentyCRM(config) { console.log(chalk.bold.blue('\n๐Ÿ“‹ Step 1: Twenty CRM Connection')); console.log(chalk.gray('Connect your Twenty CRM instance for AI assistant access\n')); console.log(chalk.yellow('๐Ÿ“š How to get your Twenty API key:')); console.log(' 1. Open your Twenty CRM instance'); console.log(' 2. Go to Settings โ†’ Developers โ†’ API Keys'); console.log(' 3. Create a new API key'); console.log(' 4. Copy the key (it won\'t be shown again!)\n'); const questions = [ { type: 'input', name: 'apiKey', message: 'Twenty API Key:', default: config.twenty.apiKey, validate: (input) => { if (!input.trim()) return 'API key is required'; if (!input.startsWith('eyJ')) return 'API key should be a JWT token starting with "eyJ"'; return true; }, }, { type: 'list', name: 'baseUrlChoice', message: 'Twenty instance type:', choices: [ { name: 'Twenty Cloud (https://api.twenty.com)', value: 'cloud' }, { name: 'Self-hosted instance', value: 'self' }, { name: 'Local development (localhost)', value: 'local' }, ], default: config.twenty.baseUrl === 'https://api.twenty.com' ? 'cloud' : config.twenty.baseUrl?.includes('localhost') ? 'local' : 'self', }, ]; const answers = await inquirer.prompt(questions); let baseUrl = ''; if (answers.baseUrlChoice === 'cloud') { baseUrl = 'https://api.twenty.com'; } else if (answers.baseUrlChoice === 'local') { const { port } = await inquirer.prompt([ { type: 'input', name: 'port', message: 'Local port:', default: '3000', validate: (input) => { const port = parseInt(input, 10); return port > 0 && port < 65536 ? true : 'Please enter a valid port number'; }, }, ]); baseUrl = `http://localhost:${port}`; } else { const { customUrl } = await inquirer.prompt([ { type: 'input', name: 'customUrl', message: 'Your Twenty instance URL:', default: config.twenty.baseUrl, validate: (input) => { try { new URL(input); return true; } catch { return 'Please enter a valid URL'; } }, }, ]); baseUrl = customUrl; } config.twenty.apiKey = answers.apiKey; config.twenty.baseUrl = baseUrl; console.log(chalk.green('โœ… Twenty CRM configured')); return config; } async function setupAuthentication(config) { console.log(chalk.bold.blue('\n๐Ÿ” Step 2: OAuth 2.1 Authentication')); console.log(chalk.gray('Secure, user-specific access to your Twenty CRM\n')); console.log(chalk.yellow('๐ŸŽฏ Benefits of OAuth Authentication:')); console.log(' โœ“ Each user has their own secure access'); console.log(' โœ“ No shared API keys - better security'); console.log(' โœ“ Easy user management via Clerk dashboard'); console.log(' โœ“ Industry-standard OAuth 2.1 protocol'); console.log(' โœ“ API keys encrypted at rest\n'); const { setupAuth } = await inquirer.prompt([ { type: 'confirm', name: 'setupAuth', message: 'Enable OAuth authentication?', default: config.auth.enabled, }, ]); if (!setupAuth) { config.auth.enabled = false; console.log(chalk.gray('OAuth authentication disabled')); return config; } console.log(chalk.yellow('\n๐Ÿ“š Setting up Clerk (Free tier available):')); console.log(' 1. Visit: https://dashboard.clerk.com/apps'); console.log(' 2. Create an app or select existing'); console.log(' 3. Go to "API Keys" in sidebar'); console.log(' 4. Copy both keys below\n'); const authQuestions = [ { type: 'input', name: 'publishableKey', message: 'Clerk Publishable Key (pk_test_... or pk_live_...):', default: config.auth.clerkPublishableKey, validate: (input) => { if (!input.startsWith('pk_')) return 'Publishable key should start with "pk_"'; return true; }, }, { type: 'input', name: 'secretKey', message: 'Clerk Secret Key (sk_test_... or sk_live_...):', default: config.auth.clerkSecretKey, validate: (input) => { if (!input.startsWith('sk_')) return 'Secret key should start with "sk_"'; return true; }, }, { type: 'list', name: 'authMode', message: 'Authentication mode:', choices: [ { name: 'Flexible - Allow both OAuth and direct API key access', value: 'flexible', short: 'Flexible' }, { name: 'Strict - Require OAuth login before API key access', value: 'strict', short: 'Strict' }, ], default: config.auth.requireAuth ? 'strict' : 'flexible', }, ]; const authAnswers = await inquirer.prompt(authQuestions); config.auth.enabled = true; config.auth.clerkPublishableKey = authAnswers.publishableKey; config.auth.clerkSecretKey = authAnswers.secretKey; config.auth.requireAuth = authAnswers.authMode === 'strict'; config.auth.provider = 'clerk'; // Generate encryption secret if not exists if (!config.auth.encryptionSecret) { config.auth.encryptionSecret = crypto.randomBytes(32).toString('hex'); } // Extract domain from publishable key const domain = authAnswers.publishableKey.split('pk_test_')[1] || authAnswers.publishableKey.split('pk_live_')[1]; if (domain) { config.auth.clerkDomain = domain.replace(/\$$/, '') + '.clerk.accounts.dev'; } console.log(chalk.green('โœ… OAuth authentication configured')); return config; } async function setupIPProtection(config) { console.log(chalk.bold.blue('\n๐Ÿ›ก๏ธ Step 3: IP Address Protection')); console.log(chalk.gray('Network-level security for your MCP server\n')); console.log(chalk.yellow('๐ŸŽฏ Benefits of IP Protection:')); console.log(' โœ“ Restrict access to known networks'); console.log(' โœ“ Block unauthorized connections'); console.log(' โœ“ Corporate network security'); console.log(' โœ“ VPN-only access control\n'); const { enableIP } = await inquirer.prompt([ { type: 'confirm', name: 'enableIP', message: 'Enable IP address protection?', default: config.ipProtection.enabled, }, ]); if (!enableIP) { config.ipProtection.enabled = false; console.log(chalk.gray('IP protection disabled')); return config; } console.log(chalk.yellow('\n๐Ÿ“‹ IP Configuration Examples:')); console.log(' โ€ข Single IP: 192.168.1.100'); console.log(' โ€ข CIDR range: 192.168.1.0/24'); console.log(' โ€ข IPv6: 2001:db8::/32'); console.log(' โ€ข Note: 127.0.0.1 (localhost) is always allowed\n'); const ipQuestions = [ { type: 'input', name: 'allowedIPs', message: 'Allowed IP addresses/CIDR blocks (comma-separated):', default: config.ipProtection.allowlist.join(','), filter: (input) => input.split(',').map(ip => ip.trim()).filter(ip => ip), validate: (input) => { const invalid = input.filter(ip => !validateIPOrCIDR(ip)); if (invalid.length > 0) { return `Invalid IP/CIDR format: ${invalid.join(', ')}`; } return true; }, }, { type: 'confirm', name: 'setupProxies', message: 'Configure trusted reverse proxies?', default: config.ipProtection.trustedProxies.length > 0, }, ]; const ipAnswers = await inquirer.prompt(ipQuestions); config.ipProtection.enabled = true; config.ipProtection.allowlist = ipAnswers.allowedIPs; if (ipAnswers.setupProxies) { const { trustedProxies } = await inquirer.prompt([ { type: 'input', name: 'trustedProxies', message: 'Trusted proxy IPs (comma-separated, optional):', default: config.ipProtection.trustedProxies.join(','), filter: (input) => input ? input.split(',').map(ip => ip.trim()).filter(ip => ip) : [], validate: (input) => { if (input.length === 0) return true; const invalid = input.filter(ip => !validateIPOrCIDR(ip)); if (invalid.length > 0) { return `Invalid proxy IP format: ${invalid.join(', ')}`; } return true; }, }, ]); config.ipProtection.trustedProxies = trustedProxies; } const { blockUnknown } = await inquirer.prompt([ { type: 'confirm', name: 'blockUnknown', message: 'Block connections when client IP cannot be determined?', default: config.ipProtection.blockUnknownIPs, }, ]); config.ipProtection.blockUnknownIPs = blockUnknown; console.log(chalk.green('โœ… IP protection configured')); return config; } async function setupServerSettings(config) { console.log(chalk.bold.blue('\nโš™๏ธ Step 4: Server Configuration')); console.log(chalk.gray('Configure how your MCP server runs\n')); const serverQuestions = [ { type: 'list', name: 'mode', message: 'Default server mode:', choices: [ { name: 'HTTP Server - Web-based access, easier debugging', value: 'http' }, { name: 'Stdio - Direct protocol communication', value: 'stdio' }, ], default: config.server.mode, }, { type: 'input', name: 'port', message: 'HTTP server port:', default: config.server.port.toString(), when: (answers) => answers.mode === 'http', validate: (input) => { const port = parseInt(input, 10); return port > 0 && port < 65536 ? true : 'Please enter a valid port number'; }, }, { type: 'confirm', name: 'verbose', message: 'Enable verbose logging by default?', default: config.server.verbose, }, ]; const serverAnswers = await inquirer.prompt(serverQuestions); config.server.mode = serverAnswers.mode; config.server.port = serverAnswers.port ? parseInt(serverAnswers.port, 10) : config.server.port; config.server.verbose = serverAnswers.verbose; console.log(chalk.green('โœ… Server settings configured')); return config; } async function setupUserPreferences(config) { console.log(chalk.bold.blue('\n๐ŸŽ›๏ธ Step 5: User Preferences')); console.log(chalk.gray('Customize your Twenty MCP experience\n')); const prefQuestions = [ { type: 'confirm', name: 'autoStart', message: 'Auto-start server when running commands?', default: config.preferences.autoStart, }, { type: 'confirm', name: 'checkUpdates', message: 'Check for updates automatically?', default: config.preferences.checkUpdates, }, { type: 'confirm', name: 'telemetry', message: 'Send anonymous usage telemetry to help improve the project?', default: config.preferences.telemetry, }, ]; const prefAnswers = await inquirer.prompt(prefQuestions); config.preferences.autoStart = prefAnswers.autoStart; config.preferences.checkUpdates = prefAnswers.checkUpdates; config.preferences.telemetry = prefAnswers.telemetry; console.log(chalk.green('โœ… User preferences configured')); return config; } async function saveConfiguration(configManager, config) { console.log(chalk.bold.blue('\n๐Ÿ’พ Saving Configuration')); try { configManager.save(config); configManager.syncToEnv(config); console.log(chalk.green('โœ… Configuration saved successfully')); console.log(chalk.gray(` Config: ${configManager.getConfigPath()}`)); console.log(chalk.gray(` Environment: ${configManager.getEnvPath()}`)); } catch (error) { throw new Error(`Failed to save configuration: ${error instanceof Error ? error.message : error}`); } } async function testConfiguration(configManager) { console.log(chalk.bold.blue('\n๐Ÿงช Testing Configuration')); // Load environment variables configManager.loadEnv(); return new Promise((resolve) => { const testProcess = crossPlatformSpawn('npm', ['run', 'validate'], { stdio: 'pipe', cwd: process.cwd(), }); let output = ''; testProcess.stdout?.on('data', (data) => { output += data.toString(); }); testProcess.stderr?.on('data', (data) => { output += data.toString(); }); testProcess.on('close', (code) => { if (code === 0) { console.log(chalk.green('โœ… Configuration test passed')); } else { console.log(chalk.yellow('โš ๏ธ Configuration test had issues, but setup completed')); console.log(chalk.gray('Run "twenty-mcp test" for detailed diagnostics')); } resolve(); }); testProcess.on('error', (error) => { console.log(chalk.yellow('โš ๏ธ Could not run configuration test')); resolve(); }); }); } async function showCompletion(config, configManager, executionContext) { if (executionContext.type === 'npx') { showNPXCompletion(); } else { console.log(chalk.bold.green('\n๐ŸŽ‰ Setup Complete!')); console.log(chalk.gray('Your Twenty MCP Server is professionally configured\n')); } // Configuration summary console.log(chalk.bold.yellow('๐Ÿ“‹ Configuration Summary:')); console.log(` Twenty CRM: ${chalk.green('โœ“')} ${config.twenty.baseUrl}`); console.log(` OAuth Auth: ${config.auth.enabled ? chalk.green('โœ“ Enabled') : chalk.gray('โœ— Disabled')}`); console.log(` IP Protection: ${config.ipProtection.enabled ? chalk.green('โœ“ Enabled') : chalk.gray('โœ— Disabled')}`); console.log(` Server Mode: ${chalk.blue(config.server.mode.toUpperCase())}`); console.log(` Port: ${chalk.blue(config.server.port)}\n`); // Context-aware next steps const commandPrefix = executionContext.type === 'npx' ? 'npx twenty-mcp-server' : 'twenty-mcp'; if (executionContext.type !== 'npx') { console.log(chalk.bold.yellow('๐Ÿš€ Next Steps:')); console.log(chalk.cyan(` 1. ${commandPrefix} test`) + chalk.gray(' - Test your configuration')); console.log(chalk.cyan(` 2. ${commandPrefix} start`) + chalk.gray(' - Start the server')); console.log(chalk.cyan(` 3. ${commandPrefix} status`) + chalk.gray(' - Check server status\n')); // IDE integration console.log(chalk.bold.yellow('๐Ÿ’ป IDE Integration:')); console.log(chalk.gray(' Use this path in your IDE configuration:')); console.log(chalk.cyan(` ${process.cwd()}/dist/index.js\n`)); } // Additional resources console.log(chalk.bold.yellow('๐Ÿ“š Resources:')); console.log(chalk.gray(' โ€ข Configuration file: ') + chalk.cyan(configManager.getConfigPath())); console.log(chalk.gray(' โ€ข Environment file: ') + chalk.cyan(configManager.getEnvPath())); console.log(chalk.gray(' โ€ข Documentation: README.md')); console.log(chalk.gray(' โ€ข Tool reference: TOOLS.md\n')); if (config.auth.enabled) { console.log(chalk.bold.yellow('๐Ÿ” OAuth Setup:')); console.log(chalk.gray(` โ€ข Test OAuth: ${commandPrefix} test --oauth`)); console.log(chalk.gray(' โ€ข OAuth docs: OAUTH.md\n')); } } function validateIPOrCIDR(input) { if (input.includes('/')) { const [ip, prefix] = input.split('/'); const prefixNum = parseInt(prefix, 10); if (isNaN(prefixNum)) return false; const ipVersion = isIP(ip); if (ipVersion === 4) { return prefixNum >= 0 && prefixNum <= 32; } else if (ipVersion === 6) { return prefixNum >= 0 && prefixNum <= 128; } return false; } else { return isIP(input) !== 0; } } async function resetConfiguration(configManager) { console.log(chalk.bold.red('๐Ÿ”„ Reset Configuration')); console.log(chalk.gray('This will reset all settings to defaults\n')); const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Are you sure you want to reset all configuration?', default: false, }, ]); if (!confirm) { console.log(chalk.gray('Reset cancelled')); return; } configManager.reset(); console.log(chalk.green('โœ… Configuration reset to defaults')); } async function importExistingConfig(configManager) { console.log(chalk.bold.blue('๐Ÿ“ฅ Import Existing Configuration')); console.log(chalk.gray('Import settings from existing .env file\n')); const config = configManager.importFromEnv(); console.log(chalk.green('โœ… Configuration imported from .env file')); console.log(chalk.yellow('\nImported settings:')); if (config.twenty.apiKey) console.log(chalk.gray(' โ€ข Twenty API Key')); if (config.twenty.baseUrl) console.log(chalk.gray(` โ€ข Base URL: ${config.twenty.baseUrl}`)); if (config.auth.enabled) console.log(chalk.gray(' โ€ข OAuth authentication')); if (config.ipProtection.enabled) console.log(chalk.gray(' โ€ข IP protection')); } //# sourceMappingURL=enhanced-setup.js.map