UNPKG

mcp-jarvis-config

Version:

MCP JARVIS configuration tool for Claude Desktop

401 lines (343 loc) 14.3 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { exec, execSync } = require('child_process'); const os = require('os'); const http = require('http'); const https = require('https'); const { findPlaceholders, replacePlaceholders, getDisplayName } = require('./process-placeholders'); // Define configuration template based on config-light.json const DEFAULT_CONFIG = { "mcpServers": { "brave-search": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-brave-search" ], "env": { "BRAVE_API_KEY": "YOUR_API_KEY_HERE" }, "disabled": false }, "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/downloads/folder" ] }, "youtube-transcript": { "command": "npx", "args": ["-y", "@80ai20u/mcp-youtube-transcript"] }, "markdown-downloader": { "command": "npx", "args": [ "-y", "mcp-markdown-downloader", "/downloads/folder" ] } } }; // Parse command line arguments const args = process.argv.slice(2); // Check Node.js version const requiredNodeVersion = '20.18.2'; const currentVersion = process.version.slice(1); // Remove the 'v' prefix function compareVersions(v1, v2) { const v1parts = v1.split('.').map(Number); const v2parts = v2.split('.').map(Number); for (let i = 0; i < v1parts.length; ++i) { if (v2parts.length === i) { return 1; } if (v1parts[i] === v2parts[i]) { continue; } return v1parts[i] > v2parts[i] ? 1 : -1; } return v1parts.length === v2parts.length ? 0 : -1; } if (compareVersions(requiredNodeVersion, currentVersion) > 0) { console.error(`Error: MCP JARVIS requires Node.js version ${requiredNodeVersion} or later.`); console.error(`Current version: ${currentVersion}`); console.error(`Please update Node.js from https://nodejs.org/ and try again.`); process.exit(1); } // Function to fetch config from URL function fetchConfig(url) { return new Promise((resolve, reject) => { const client = url.startsWith('https') ? https : http; client.get(url, (res) => { if (res.statusCode < 200 || res.statusCode >= 300) { return reject(new Error(`Failed to fetch config: ${res.statusCode}`)); } const data = []; res.on('data', chunk => data.push(chunk)); res.on('end', () => { try { const rawData = Buffer.concat(data).toString(); console.log(`Raw config data received: ${rawData.substring(0, 100)}...`); const config = JSON.parse(rawData); // Validate the config structure if (!config || !config.mcpServers || typeof config.mcpServers !== 'object') { return reject(new Error('Invalid config structure: mcpServers object missing or invalid')); } console.log(`Config loaded successfully with ${Object.keys(config.mcpServers).length} servers`); resolve(config); } catch (e) { reject(new Error(`Invalid JSON in config: ${e.message}`)); } }); }).on('error', reject); }); } // Main installation function function startInstallation(configToUse) { // Welcome message console.log('╔════════════════════════════════════════╗'); console.log('║ MCP JARVIS Configuration Tool ║'); console.log('╚════════════════════════════════════════╝'); console.log(''); console.log('This tool will configure MCP JARVIS on your Mac.'); console.log(''); // Check if we're on macOS if (process.platform !== 'darwin') { console.error('This tool is currently designed for macOS only.'); process.exit(1); } // Use AppleScript to show a native folder selection dialog console.log('Please select your Downloads folder in the dialog that appears...'); const applescriptDialog = `tell application "System Events" activate set folderPath to POSIX path of (choose folder with prompt "Select your Downloads folder:") end tell`; exec(`osascript -e '${applescriptDialog}'`, (error, stdout, stderr) => { if (error) { console.error(`Error: ${error.message}`); return; } const downloadPath = stdout.trim(); console.log(`Selected downloads folder: ${downloadPath}`); // Get username from the OS directly const username = os.userInfo().username; // Define Claude Desktop config file path const configDir = path.join('/Users', username, 'Library/Application Support/Claude'); const claudeConfigPath = path.join(configDir, 'claude_desktop_config.json'); // Check if Claude Desktop is installed if (!fs.existsSync(configDir)) { // Prompt the user to install Claude Desktop const installPromptScript = `tell application "System Events" activate set dialogResult to display dialog "Claude Desktop doesn't appear to be installed. You need to install it before continuing." buttons {"Cancel", "Download Claude"} default button 2 set buttonPressed to button returned of dialogResult return buttonPressed end tell`; const installResponse = execSync(`osascript -e '${installPromptScript}'`).toString().trim(); if (installResponse === "Download Claude") { // Open Claude Desktop download page execSync('open https://claude.ai/download'); console.log('Please install Claude Desktop and run this tool again.'); process.exit(0); } else { console.log('Installation canceled. Please install Claude Desktop before configuring MCP JARVIS.'); process.exit(0); } } // Create config directory if it doesn't exist (should not happen if Claude is installed) if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); console.log(`Created directory: ${configDir}`); } // Create backup of existing configuration if it exists and is not empty if (fs.existsSync(claudeConfigPath)) { try { const existingConfig = fs.readFileSync(claudeConfigPath, 'utf8'); // Check if the config is not empty (more than just {}) if (existingConfig.trim().length > 2) { // Create a dated backup const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const backupPath = path.join(configDir, `claude_desktop_config-${year}-${month}-${day}.json`); // Don't overwrite an existing backup from the same day let finalBackupPath = backupPath; let counter = 1; while (fs.existsSync(finalBackupPath)) { finalBackupPath = path.join(configDir, `claude_desktop_config-${year}-${month}-${day}-${counter}.json`); counter++; } fs.copyFileSync(claudeConfigPath, finalBackupPath); console.log(`Backed up existing configuration to: ${finalBackupPath}`); } } catch (err) { console.log(`Warning: Could not back up existing configuration: ${err.message}`); } } // Deep clone the configuration to avoid modifying the original const config = JSON.parse(JSON.stringify(configToUse)); // Update all instances of /downloads/folder with the selected path function updatePaths(obj, downloadPath) { if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { if (typeof obj[i] === 'string' && obj[i] === '/downloads/folder') { obj[i] = downloadPath; } else if (typeof obj[i] === 'object' && obj[i] !== null) { updatePaths(obj[i], downloadPath); } } } else if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (typeof obj[key] === 'string' && obj[key] === '/downloads/folder') { obj[key] = downloadPath; } else if (typeof obj[key] === 'object' && obj[key] !== null) { updatePaths(obj[key], downloadPath); } } } } updatePaths(config, downloadPath); // Find all placeholders in the config const placeholders = findPlaceholders(config); console.log(`Found ${placeholders.size} placeholders in configuration`); // Ask for placeholders to be filled askForPlaceholders(config, placeholders, 0, {}, (updatedConfig) => { // Read existing Claude Desktop config if it exists let claudeConfig = {}; if (fs.existsSync(claudeConfigPath)) { try { const existingConfig = fs.readFileSync(claudeConfigPath, 'utf8'); if (isValidJson(existingConfig)) { claudeConfig = JSON.parse(existingConfig); console.log('Loaded existing Claude Desktop configuration.'); } } catch (err) { console.log('Could not parse existing Claude Desktop config file. Creating a new one.'); } } // Log the configuration we're about to apply console.log(`Applying configuration with ${Object.keys(updatedConfig.mcpServers).length} MCP servers`); // Update the config with MCP servers if (updatedConfig && updatedConfig.mcpServers) { claudeConfig.mcpServers = updatedConfig.mcpServers; } else { console.error('Error: Invalid configuration structure - mcpServers not found'); process.exit(1); } // Write the updated config try { const configJson = JSON.stringify(claudeConfig, null, 2); fs.writeFileSync(claudeConfigPath, configJson); console.log(`MCP configuration updated successfully at: ${claudeConfigPath}`); console.log(`Configuration size: ${configJson.length} bytes`); } catch (err) { console.error(`Error writing configuration: ${err.message}`); process.exit(1); } // Display success dialog const successScript = `tell application "System Events" activate display dialog "MCP JARVIS has been successfully configured!\n\nYou can now start using Claude Desktop with MCP JARVIS capabilities." buttons {"Finish"} default button 1 with title "Installation Complete" with icon 1 end tell`; exec(`osascript -e '${successScript}'`, (err) => { if (err) { console.error('Error displaying success dialog:', err); } console.log('\nMCP JARVIS is now configured and ready to use!'); console.log('You can now start using Claude Desktop with MCP JARVIS capabilities.'); }); }); }); } function askForPlaceholders(config, placeholders, index, replacements, callback) { const placeholderArray = Array.from(placeholders); // Base case: no more placeholders to process if (index >= placeholderArray.length) { // Apply all replacements const updatedConfig = replacePlaceholders(config, replacements); callback(updatedConfig); return; } const placeholder = placeholderArray[index]; const displayName = getDisplayName(placeholder); // AppleScript to show a dialog with options const promptScript = `tell application "System Events" activate set dialogResult to display dialog "Would you like to set up ${displayName}?" buttons {"Skip", "Enter Value"} default button 2 set buttonPressed to button returned of dialogResult return buttonPressed end tell`; exec(`osascript -e '${promptScript}'`, (error, stdout, stderr) => { if (error || stdout.trim() === 'Skip') { // User skipped - move to next placeholder askForPlaceholders(config, placeholders, index + 1, replacements, callback); return; } // User wants to enter a value const getValueScript = `tell application "System Events" activate set dialogResult to display dialog "Enter your ${displayName}:" default answer "" with title "${displayName}" set inputValue to text returned of dialogResult return inputValue end tell`; exec(`osascript -e '${getValueScript}'`, (valueError, valueOutput, valueStderr) => { const value = valueOutput.trim(); if (value && value.length > 0) { // Store this replacement replacements[placeholder] = value; // Show success message exec(`osascript -e 'display notification "${displayName} has been set." with title "MCP JARVIS Configuration"'`); } // Process the next placeholder askForPlaceholders(config, placeholders, index + 1, replacements, callback); }); }); } // Check if the given string is a valid JSON function isValidJson(str) { try { const json = JSON.parse(str); return typeof json === 'object'; } catch (e) { return false; } } // Add error handling process.on('uncaughtException', (err) => { console.error('An unexpected error occurred:'); console.error(err); // Show native dialog const errorScript = `tell application "System Events" activate display dialog "An error occurred: ${err.message.replace(/"/g, '\\"')}" buttons {"OK"} with icon stop end tell`; exec(`osascript -e '${errorScript}'`); process.exit(1); }); // Main execution logic if (args.includes('--config-url')) { const urlIndex = args.indexOf('--config-url'); if (args.length > urlIndex + 1) { const configUrl = args[urlIndex + 1]; console.log(`Fetching configuration from: ${configUrl}`); fetchConfig(configUrl) .then(config => { console.log('Successfully loaded custom configuration'); startInstallation(config); }) .catch(error => { console.error(`Error loading configuration: ${error.message}`); process.exit(1); }); } else { console.error('Error: No URL provided after --config-url'); process.exit(1); } } else { // Use default configuration startInstallation(DEFAULT_CONFIG); }