mcp-jarvis-config
Version:
MCP JARVIS configuration tool for Claude Desktop
401 lines (343 loc) • 14.3 kB
JavaScript
#!/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);
}