UNPKG

mcp-jarvis-config

Version:

MCP JARVIS configuration tool for Claude Desktop

451 lines (388 loc) 16.1 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 crypto = require('crypto'); // 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 { // Check if we're on Windows if (process.platform !== 'win32') { console.error('This script is designed for Windows only. For macOS, use the standard mcp-jarvis-config command.'); process.exit(1); } 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 Windows system.'); // Get temp directory for script files const tempDir = os.tmpdir(); const scriptId = crypto.randomBytes(8).toString('hex'); // Use PowerShell to show a folder selection dialog const folderSelectionScript = ` Add-Type -AssemblyName System.Windows.Forms; $folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog; $folderBrowser.Description = "Select your Downloads folder"; $folderBrowser.RootFolder = "MyComputer"; if ($folderBrowser.ShowDialog() -eq "OK") { Write-Output $folderBrowser.SelectedPath; } else { Write-Output "CANCELLED"; } `; const folderScriptPath = path.join(tempDir, `folder-selection-${scriptId}.ps1`); fs.writeFileSync(folderScriptPath, folderSelectionScript); try { // Execute the PowerShell script file const downloadPath = execSync(`powershell -ExecutionPolicy Bypass -File "${folderScriptPath}"`, { encoding: 'utf8' }).trim(); // Clean up the temp script file try { fs.unlinkSync(folderScriptPath); } catch (e) { /* ignore error */ } if (downloadPath === "CANCELLED") { console.log('Folder selection was cancelled. Configuration aborted.'); process.exit(0); } console.log(`Selected downloads folder: ${downloadPath}`); // Define Claude Desktop config file path for Windows const appDataDir = process.env.APPDATA || path.join(os.homedir(), 'AppData/Roaming'); const configDir = path.join(appDataDir, '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 = ` Add-Type -AssemblyName System.Windows.Forms; $result = [System.Windows.Forms.MessageBox]::Show( "Claude Desktop doesn't appear to be installed. You need to install it before continuing.", "Claude Desktop Required", [System.Windows.Forms.MessageBoxButtons]::OKCancel, [System.Windows.Forms.MessageBoxIcon]::Information ); Write-Output $result; `; const installScriptPath = path.join(tempDir, `claude-check-${scriptId}.ps1`); fs.writeFileSync(installScriptPath, installPromptScript); try { const installResponse = execSync(`powershell -ExecutionPolicy Bypass -File "${installScriptPath}"`, { encoding: 'utf8' }).trim(); // Clean up the temp script file try { fs.unlinkSync(installScriptPath); } catch (e) { /* ignore error */ } if (installResponse === "OK") { // Open Claude Desktop download page execSync('start 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); } } catch (err) { console.error('Error showing dialog:', err); process.exit(1); } } // Create config directory if it doesn't exist 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 promptForPlaceholdersWin(Array.from(placeholders), tempDir, scriptId); // Apply replacements applyReplacements(updatedConfig, replacements); // Save config await saveConfig(updatedConfig, claudeConfigPath); // Show success const successScript = ` Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show( "MCP JARVIS has been successfully configured!" + [char]10 + [char]10 + "You can now start using Claude Desktop with MCP JARVIS capabilities.", "Installation Complete", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information ); `; const successScriptPath = path.join(tempDir, `success-message-${scriptId}.ps1`); fs.writeFileSync(successScriptPath, successScript); try { execSync(`powershell -ExecutionPolicy Bypass -File "${successScriptPath}"`); try { fs.unlinkSync(successScriptPath); } catch (e) { /* ignore error */ } } catch (err) { console.error('Error displaying success dialog:', err); } console.log('\nMCP JARVIS is now configured and ready to use!'); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } } // Windows version of backing up 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.replace(/\\/g, '/'); } 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.replace(/\\/g, '/'); } 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(' '); } // Windows version of prompting for placeholders async function promptForPlaceholdersWin(placeholders, tempDir, scriptId) { const replacements = {}; for (const placeholder of placeholders) { const displayName = getDisplayName(placeholder); try { // PowerShell script to show a dialog with options const promptScript = ` Add-Type -AssemblyName System.Windows.Forms; $result = [System.Windows.Forms.MessageBox]::Show( "Would you like to set up ${displayName}?", "${displayName} Setup", [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question ); Write-Output $result; `; const promptScriptPath = path.join(tempDir, `prompt-${scriptId}-${crypto.randomBytes(4).toString('hex')}.ps1`); fs.writeFileSync(promptScriptPath, promptScript); try { const response = execSync(`powershell -ExecutionPolicy Bypass -File "${promptScriptPath}"`, { encoding: 'utf8' }).trim(); try { fs.unlinkSync(promptScriptPath); } catch (e) { /* ignore error */ } if (response === "Yes") { // User wants to enter a value const getValueScript = ` Add-Type -AssemblyName Microsoft.VisualBasic; $value = [Microsoft.VisualBasic.Interaction]::InputBox("Enter your ${displayName}:", "${displayName}", ""); Write-Output $value; `; const getValueScriptPath = path.join(tempDir, `value-${scriptId}-${crypto.randomBytes(4).toString('hex')}.ps1`); fs.writeFileSync(getValueScriptPath, getValueScript); try { const value = execSync(`powershell -ExecutionPolicy Bypass -File "${getValueScriptPath}"`, { encoding: 'utf8' }).trim(); try { fs.unlinkSync(getValueScriptPath); } catch (e) { /* ignore error */ } if (value && value.length > 0) { replacements[placeholder] = value; console.log(`Set ${displayName} value`); } } catch (valueError) { console.log(`Error getting value for ${displayName}: ${valueError.message}`); } } else { console.log(`Skipping ${displayName}`); } } catch (promptError) { console.log(`Error showing prompt for ${displayName}: ${promptError.message}`); } } catch (error) { console.log(`Skipping ${displayName} due to error: ${error.message}`); } } 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); } } } } } // Windows version of saving config 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(`MCP configuration updated successfully at: ${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); });