mcp-jarvis-config
Version:
MCP JARVIS configuration tool for Claude Desktop
395 lines (341 loc) • 12.9 kB
JavaScript
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);
});