UNPKG

mcp-jarvis-config

Version:

MCP JARVIS configuration tool for Claude Desktop

395 lines (341 loc) 12.9 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'); // Default Config const DEFAULT_CONFIG = { "mcpServers": { "brave-search": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "{{BRAVE_API_KEY}}" } }, "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"] } } }; // Main function async function main() { try { const args = process.argv.slice(2); let config = DEFAULT_CONFIG; // Check for config URL 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}`); config = await fetchConfig(configUrl); console.log('Successfully loaded custom configuration'); } } // Start installation with the config await installConfig(config); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } } // Fetch config from URL async 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); if (!config || !config.mcpServers || typeof config.mcpServers !== 'object') { return reject(new Error('Invalid config: mcpServers object missing')); } console.log(`Config loaded with ${Object.keys(config.mcpServers).length} servers`); resolve(config); } catch (e) { reject(new Error(`Invalid JSON: ${e.message}`)); } }); }).on('error', reject); }); } // Install the configuration async function installConfig(config) { // 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.'); // Check if we're on macOS if (process.platform !== 'darwin') { throw new Error('This tool is currently designed for macOS only.'); } // Select downloads folder console.log('Please select your Downloads folder...'); const downloadPath = await selectFolder(); console.log(`Selected downloads folder: ${downloadPath}`); // Get username and config paths const username = os.userInfo().username; const configDir = path.join('/Users', username, 'Library/Application Support/Claude'); const claudeConfigPath = path.join(configDir, 'claude_desktop_config.json'); // Check if Claude is installed if (!fs.existsSync(configDir)) { const response = await showDialog( "Claude Desktop doesn't appear to be installed. You need to install it before continuing.", ["Cancel", "Download Claude"] ); if (response === "Download Claude") { 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.'); process.exit(0); } } // Create config dir if needed if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Backup existing config await backupConfig(claudeConfigPath); // Update paths in config const updatedConfig = JSON.parse(JSON.stringify(config)); // Deep clone updateDownloadsPaths(updatedConfig, downloadPath); // Find and process placeholders const placeholders = findPlaceholders(updatedConfig); console.log(`Found ${placeholders.size} placeholders`); // Get values for placeholders const replacements = await promptForPlaceholders(Array.from(placeholders)); // Apply replacements applyReplacements(updatedConfig, replacements); // Save config await saveConfig(updatedConfig, claudeConfigPath); // Show success await showDialog( "MCP JARVIS has been successfully configured!\n\nYou can now start using Claude Desktop with MCP JARVIS capabilities.", ["Finish"], "Installation Complete" ); console.log('\nMCP JARVIS is now configured and ready to use!'); } // Select folder using AppleScript function selectFolder() { return new Promise((resolve, reject) => { const script = `tell application "System Events" activate set folderPath to POSIX path of (choose folder with prompt "Select your Downloads folder:") end tell`; exec(`osascript -e '${script}'`, (error, stdout, stderr) => { if (error) reject(error); else resolve(stdout.trim()); }); }); } // Show dialog using AppleScript function showDialog(message, buttons, title = "") { return new Promise((resolve, reject) => { const titleOption = title ? `with title "${title}"` : ''; const buttonStr = buttons.map(b => `"${b}"`).join(', '); const defaultButton = buttons.length > 0 ? `default button ${buttons.length}` : ''; const script = `tell application "System Events" activate set dialogResult to display dialog "${message}" buttons {${buttonStr}} ${defaultButton} ${titleOption} set buttonPressed to button returned of dialogResult return buttonPressed end tell`; exec(`osascript -e '${script}'`, (error, stdout, stderr) => { if (error) reject(error); else resolve(stdout.trim()); }); }); } // Backup existing config async function backupConfig(configPath) { if (!fs.existsSync(configPath)) return; try { const existingConfig = fs.readFileSync(configPath, 'utf8'); // If config is empty, no need to backup if (existingConfig.trim().length <= 2) return; // Create backup filename with date const date = new Date(); const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const backupDir = path.dirname(configPath); let backupPath = path.join(backupDir, `claude_desktop_config-${dateStr}.json`); // Don't overwrite existing backups let counter = 1; while (fs.existsSync(backupPath)) { backupPath = path.join(backupDir, `claude_desktop_config-${dateStr}-${counter}.json`); counter++; } fs.copyFileSync(configPath, backupPath); console.log(`Backed up existing configuration to: ${backupPath}`); } catch (err) { console.log(`Warning: Could not back up configuration: ${err.message}`); } } // Update download folder paths function updateDownloadsPaths(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) { updateDownloadsPaths(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) { updateDownloadsPaths(obj[key], downloadPath); } } } } // Find placeholders like {{PLACEHOLDER}} in config function findPlaceholders(obj, placeholders = new Set()) { if (Array.isArray(obj)) { for (const item of obj) { if (typeof item === 'object' && item !== null) { findPlaceholders(item, placeholders); } else if (typeof item === 'string') { const matches = item.match(/\{\{([A-Z0-9_]+)\}\}/g); if (matches) { matches.forEach(match => placeholders.add(match)); } } } } else if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (typeof obj[key] === 'object' && obj[key] !== null) { findPlaceholders(obj[key], placeholders); } else if (typeof obj[key] === 'string') { const matches = obj[key].match(/\{\{([A-Z0-9_]+)\}\}/g); if (matches) { matches.forEach(match => placeholders.add(match)); } } } } return placeholders; } // Get a friendly display name from placeholder function getDisplayName(placeholder) { return placeholder .replace(/^\{\{|\}\}$/g, '') // Remove {{ and }} .split('_') .map(word => word.charAt(0) + word.slice(1).toLowerCase()) .join(' '); } // Prompt user for placeholder values async function promptForPlaceholders(placeholders) { const replacements = {}; for (const placeholder of placeholders) { const displayName = getDisplayName(placeholder); try { const response = await showDialog( `Would you like to set up ${displayName}?`, ["Skip", "Enter Value"] ); if (response === "Enter Value") { const valueScript = `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`; const value = await new Promise((resolve, reject) => { exec(`osascript -e '${valueScript}'`, (error, stdout, stderr) => { if (error) reject(error); else resolve(stdout.trim()); }); }); if (value && value.length > 0) { replacements[placeholder] = value; console.log(`Set ${displayName} value`); } } } catch (error) { console.log(`Skipping ${displayName}`); } } return replacements; } // Apply replacements to config function applyReplacements(obj, replacements) { if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { if (typeof obj[i] === 'object' && obj[i] !== null) { applyReplacements(obj[i], replacements); } else if (typeof obj[i] === 'string') { for (const [placeholder, value] of Object.entries(replacements)) { obj[i] = obj[i].replace(placeholder, value); } } } } else if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (typeof obj[key] === 'object' && obj[key] !== null) { applyReplacements(obj[key], replacements); } else if (typeof obj[key] === 'string') { for (const [placeholder, value] of Object.entries(replacements)) { obj[key] = obj[key].replace(placeholder, value); } } } } } // Save config to file async function saveConfig(config, configPath) { // Read existing config if available let claudeConfig = {}; if (fs.existsSync(configPath)) { try { const existingConfig = fs.readFileSync(configPath, 'utf8'); try { claudeConfig = JSON.parse(existingConfig); console.log('Loaded existing Claude Desktop configuration.'); } catch { console.log('Could not parse existing config. Creating new one.'); } } catch (err) { console.log(`Could not read config file: ${err.message}`); } } // Update config with MCP servers claudeConfig.mcpServers = config.mcpServers; // Write config try { const configJson = JSON.stringify(claudeConfig, null, 2); fs.writeFileSync(configPath, configJson); console.log(`Configuration saved to: ${configPath}`); console.log(`Configuration size: ${configJson.length} bytes`); } catch (err) { throw new Error(`Failed to write configuration: ${err.message}`); } } // Run the main function main().catch(err => { console.error(`Fatal error: ${err.message}`); process.exit(1); });