mcp-jarvis-config
Version:
MCP JARVIS configuration tool for Claude Desktop
451 lines (388 loc) • 16.1 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');
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);
});