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.
305 lines โข 11.8 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
import { existsSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import crypto from 'crypto';
import { isIP } from 'node:net';
import { spawn } from 'child_process';
export async function setupCommand(options) {
console.log(chalk.bold.green('๐ ๏ธ Twenty MCP Server Setup Wizard'));
console.log(chalk.gray('This wizard will help you configure your Twenty MCP Server\n'));
try {
const config = {
authEnabled: false,
requireAuth: false,
ipProtectionEnabled: false,
ipAllowlist: [],
trustedProxies: [],
blockUnknownIPs: true,
};
// Step 1: Twenty CRM Configuration
await setupTwentyCRM(config);
// Step 2: Authentication (optional or if --oauth flag)
if (options.oauth || await shouldSetupAuth()) {
await setupAuthentication(config);
}
// Step 3: IP Protection (optional or if --ip-protection flag)
if (options.ipProtection || await shouldSetupIPProtection()) {
await setupIPProtection(config);
}
// Step 4: Write configuration
await writeConfiguration(config);
// Step 5: Test configuration (unless --skip-tests)
if (!options.skipTests) {
await testConfiguration();
}
// Step 6: Show next steps
showNextSteps();
}
catch (error) {
console.error(chalk.red('\nโ Setup failed:'), error instanceof Error ? error.message : error);
process.exit(1);
}
}
async function setupTwentyCRM(config) {
console.log(chalk.bold.blue('\n๐ Step 1: Configure Twenty CRM Connection'));
console.log(chalk.gray('Connect to your Twenty CRM instance\n'));
const questions = [
{
type: 'input',
name: 'apiKey',
message: 'Twenty API Key:',
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: 'input',
name: 'baseUrl',
message: 'Twenty Base URL:',
default: 'https://api.twenty.com',
validate: (input) => {
try {
new URL(input);
return true;
}
catch {
return 'Please enter a valid URL';
}
},
},
];
const answers = await inquirer.prompt(questions);
config.twentyApiKey = answers.apiKey;
config.twentyBaseUrl = answers.baseUrl;
console.log(chalk.green('โ
Twenty CRM configured'));
}
async function shouldSetupAuth() {
const { enableAuth } = await inquirer.prompt([
{
type: 'confirm',
name: 'enableAuth',
message: 'Enable OAuth 2.1 authentication? (Recommended for multi-user setups)',
default: false,
},
]);
return enableAuth;
}
async function setupAuthentication(config) {
console.log(chalk.bold.blue('\n๐ Step 2: Configure OAuth Authentication'));
console.log(chalk.gray('Set up secure user authentication with Clerk\n'));
const questions = [
{
type: 'input',
name: 'publishableKey',
message: 'Clerk Publishable Key (pk_test_... or pk_live_...):',
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_...):',
validate: (input) => {
if (!input.startsWith('sk_'))
return 'Secret key should start with "sk_"';
return true;
},
},
{
type: 'confirm',
name: 'requireAuth',
message: 'Require OAuth login for all connections? (If false, allows direct API key access)',
default: false,
},
];
const answers = await inquirer.prompt(questions);
config.authEnabled = true;
config.clerkPublishableKey = answers.publishableKey;
config.clerkSecretKey = answers.secretKey;
config.requireAuth = answers.requireAuth;
config.encryptionSecret = crypto.randomBytes(32).toString('hex');
// Extract domain from publishable key
const domain = answers.publishableKey.split('pk_test_')[1] || answers.publishableKey.split('pk_live_')[1];
if (domain) {
config.clerkDomain = domain.replace(/\$$/, '') + '.clerk.accounts.dev';
}
console.log(chalk.green('โ
OAuth authentication configured'));
}
async function shouldSetupIPProtection() {
const { enableIP } = await inquirer.prompt([
{
type: 'confirm',
name: 'enableIP',
message: 'Enable IP address protection? (Restrict access to specific IPs/networks)',
default: false,
},
]);
return enableIP;
}
async function setupIPProtection(config) {
console.log(chalk.bold.blue('\n๐ก๏ธ Step 3: Configure IP Address Protection'));
console.log(chalk.gray('Control which IP addresses can access your server\n'));
const questions = [
{
type: 'input',
name: 'allowedIPs',
message: 'Allowed IP addresses/CIDR blocks (comma-separated):',
default: '127.0.0.1,192.168.1.0/24',
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: 'input',
name: 'trustedProxies',
message: 'Trusted proxy IPs (comma-separated, optional):',
default: '',
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;
},
},
{
type: 'confirm',
name: 'blockUnknown',
message: 'Block connections when client IP cannot be determined?',
default: true,
},
];
const answers = await inquirer.prompt(questions);
config.ipProtectionEnabled = true;
config.ipAllowlist = answers.allowedIPs;
config.trustedProxies = answers.trustedProxies;
config.blockUnknownIPs = answers.blockUnknown;
console.log(chalk.green('โ
IP protection configured'));
}
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 writeConfiguration(config) {
console.log(chalk.bold.blue('\n๐พ Writing Configuration'));
const envPath = join(process.cwd(), '.env');
let envContent = '';
// Read existing .env if it exists
if (existsSync(envPath)) {
envContent = readFileSync(envPath, 'utf8');
}
// Parse existing env vars to avoid duplicates
const existingVars = new Set();
envContent.split('\n').forEach(line => {
const match = line.match(/^([A-Z_]+)=/);
if (match) {
existingVars.add(match[1]);
}
});
const newVars = [
['TWENTY_API_KEY', config.twentyApiKey],
['TWENTY_BASE_URL', config.twentyBaseUrl],
['AUTH_ENABLED', config.authEnabled.toString()],
['REQUIRE_AUTH', config.requireAuth.toString()],
['AUTH_PROVIDER', config.authEnabled ? 'clerk' : undefined],
['CLERK_PUBLISHABLE_KEY', config.clerkPublishableKey],
['CLERK_SECRET_KEY', config.clerkSecretKey],
['CLERK_DOMAIN', config.clerkDomain],
['API_KEY_ENCRYPTION_SECRET', config.encryptionSecret],
['MCP_SERVER_URL', 'http://localhost:3000'],
['IP_PROTECTION_ENABLED', config.ipProtectionEnabled?.toString()],
['IP_ALLOWLIST', config.ipAllowlist?.join(',')],
['TRUSTED_PROXIES', config.trustedProxies?.join(',')],
['IP_BLOCK_UNKNOWN', config.blockUnknownIPs?.toString()],
];
// Add new vars that don't exist
newVars.forEach(([key, value]) => {
if (value && !existingVars.has(key)) {
envContent += `${key}=${value}\n`;
}
});
writeFileSync(envPath, envContent);
console.log(chalk.green('โ
Configuration saved to .env'));
}
async function testConfiguration() {
console.log(chalk.bold.blue('\n๐งช Testing Configuration'));
return new Promise((resolve, reject) => {
const testProcess = spawn('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'));
resolve();
}
else {
console.log(chalk.yellow('โ ๏ธ Configuration test had issues, but setup completed'));
console.log(chalk.gray('Run "twenty-mcp test" for detailed diagnostics'));
resolve(); // Don't fail setup for test issues
}
});
testProcess.on('error', (error) => {
console.log(chalk.yellow('โ ๏ธ Could not run configuration test'));
resolve(); // Don't fail setup for test issues
});
});
}
function showNextSteps() {
console.log(chalk.bold.green('\n๐ Setup Complete!'));
console.log(chalk.gray('Your Twenty MCP Server is ready to use\n'));
console.log(chalk.bold.yellow('Next Steps:'));
console.log(chalk.cyan(' 1. twenty-mcp test') + chalk.gray(' - Test your configuration'));
console.log(chalk.cyan(' 2. twenty-mcp start') + chalk.gray(' - Start the server'));
console.log(chalk.cyan(' 3. twenty-mcp status') + chalk.gray(' - Check server status'));
console.log('');
console.log(chalk.bold.yellow('IDE Integration:'));
console.log(chalk.gray(' Add this server to your IDE with the path:'));
const currentPath = process.cwd();
console.log(chalk.cyan(` ${currentPath}/dist/index.js`));
console.log('');
console.log(chalk.bold.yellow('Documentation:'));
console.log(chalk.gray(' README.md - Complete setup guide'));
console.log(chalk.gray(' OAUTH.md - OAuth authentication details'));
console.log(chalk.gray(' TOOLS.md - Available MCP tools'));
console.log('');
}
//# sourceMappingURL=setup.js.map