UNPKG

mcp-orchestrator

Version:

MCP Orchestrator - Discover and install MCPs with automatic OAuth support. Uses Claude CLI for OAuth MCPs (Canva, Asana, etc). 34 trusted MCPs from Claude Partners.

450 lines (449 loc) • 19.3 kB
#!/usr/bin/env node /** * MCP Orchestrator Server * Main entry point - creates the orchestrator MCP server */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { ConfigManager } from './config-manager.js'; import { MCP_REGISTRY, findMCPById, searchMCPs } from './mcp-registry.js'; import { discoverMCPs } from './discovery.js'; async function main() { console.error('šŸš€ Starting MCP Orchestrator Server...\n'); // Create config manager const configManager = new ConfigManager(); // Create orchestrator server const server = new McpServer({ name: 'mcp-orchestrator', version: '0.5.0' }, { capabilities: {} }); // ============================================ // META-TOOL: list_available_mcps // ============================================ server.registerTool('list_available_mcps', { title: 'List Available MCP Servers', description: 'Browse all MCP servers available in the registry', inputSchema: { search: z.string().optional().describe('Optional search query to filter MCPs') }, outputSchema: { mcps: z.array(z.object({ id: z.string(), name: z.string(), description: z.string() })), count: z.number() } }, async ({ search }) => { console.error(`šŸ“‹ Listing available MCPs (search: ${search || 'none'})...`); const mcps = search ? searchMCPs(search) : MCP_REGISTRY; const output = { mcps: mcps.map(mcp => ({ id: mcp.id, name: mcp.name, description: mcp.description })), count: mcps.length }; return { content: [ { type: 'text', text: `Found ${output.count} MCP servers:\n\n${mcps.map(m => `• ${m.name} (${m.id}): ${m.description}`).join('\n')}` } ], structuredContent: output }; }); // ============================================ // META-TOOL: discover_mcps (INTELLIGENT DISCOVERY) // ============================================ server.registerTool('discover_mcps', { title: 'Discover Relevant MCP Servers', description: 'Intelligently discover which MCP servers would be most helpful for your specific task', inputSchema: { taskDescription: z.string().describe('Describe what you want to accomplish (e.g., "I want to read JSON files from my project")') }, outputSchema: { recommendations: z.array(z.object({ id: z.string(), name: z.string(), description: z.string(), score: z.number(), reasons: z.array(z.string()), matchedKeywords: z.array(z.string()), installTime: z.string().optional(), pros: z.array(z.string()).optional(), cons: z.array(z.string()).optional(), useCases: z.array(z.string()).optional() })), analyzedContext: z.object({ keywords: z.array(z.string()), categories: z.array(z.string()), taskType: z.string() }) } }, async ({ taskDescription }) => { console.error(`šŸ” Discovering MCPs for: "${taskDescription}"...`); const results = discoverMCPs(taskDescription, 5); // Get the context for transparency const { analyzeTaskContext } = await import('./discovery.js'); const context = analyzeTaskContext(taskDescription); const output = { recommendations: results.map(r => ({ id: r.mcp.id, name: r.mcp.name, description: r.mcp.description, score: r.score, reasons: r.reasons, matchedKeywords: r.matchedKeywords, installTime: r.mcp.installTime, pros: r.mcp.pros, cons: r.mcp.cons, useCases: r.mcp.useCases })), analyzedContext: { keywords: context.keywords, categories: context.categories, taskType: context.taskType } }; const responseText = results.length > 0 ? `šŸŽÆ Found ${results.length} relevant MCP servers:\n\n${results.map((r, i) => { let text = `${i + 1}. **${r.mcp.name}** (score: ${r.score})\n ${r.mcp.description}`; if (r.mcp.installTime) { text += `\n ā±ļø Install time: ${r.mcp.installTime}`; } text += `\n āœ“ ${r.reasons.join('\n āœ“ ')}`; if (r.mcp.pros && r.mcp.pros.length > 0) { text += `\n šŸ‘ Pros: ${r.mcp.pros.slice(0, 2).join(', ')}`; } if (r.mcp.cons && r.mcp.cons.length > 0) { text += `\n āš ļø Cons: ${r.mcp.cons.slice(0, 2).join(', ')}`; } return text; }).join('\n\n')}\n\nšŸ“Š Analysis:\n• Keywords: ${context.keywords.slice(0, 5).join(', ')}\n• Categories: ${context.categories.join(', ') || 'none'}\n• Task type: ${context.taskType}` : `No relevant MCPs found for: "${taskDescription}"`; return { content: [ { type: 'text', text: responseText } ], structuredContent: output }; }); // ============================================ // META-TOOL: connect_mcp (adds to config) // ============================================ server.registerTool('connect_mcp', { title: 'Connect to MCP Server', description: 'Connect to a specific MCP server and load its tools', inputSchema: { mcpId: z.string().describe('ID of the MCP server to connect (e.g., "filesystem", "github")'), autoInstall: z.boolean().optional().describe('Automatically install the MCP if not present (default: true)') }, outputSchema: { success: z.boolean(), serverId: z.string(), configPath: z.string().optional(), product: z.string().optional(), message: z.string(), requiresRestart: z.boolean(), error: z.string().optional() } }, async ({ mcpId, autoInstall = true }) => { try { console.error(`šŸ”Œ Adding MCP to config: ${mcpId}...`); // Find MCP config const config = findMCPById(mcpId); if (!config) { const output = { success: false, serverId: mcpId, message: `MCP server "${mcpId}" not found in registry`, requiresRestart: false, error: `MCP server "${mcpId}" not found in registry` }; return { content: [{ type: 'text', text: `āŒ ${output.error}` }], structuredContent: output, isError: true }; } // Check if this MCP requires OAuth const needsOAuth = configManager.requiresOAuth(config); if (needsOAuth) { // Use Claude CLI for OAuth MCPs (handles OAuth flow automatically) console.error(`šŸ” ${config.name} requires OAuth - using Claude CLI...`); // Auto-install package first if needed let wasInstalled = false; if (autoInstall && config.packageName) { const { autoInstall: runAutoInstall } = await import('./auto-installer.js'); const installResult = await runAutoInstall(config); if (!installResult.success && !installResult.alreadyInstalled) { const output = { success: false, serverId: mcpId, message: `Failed to install ${config.name}: ${installResult.error}`, requiresRestart: false, error: `Failed to install ${config.name}: ${installResult.error}` }; return { content: [{ type: 'text', text: `āŒ ${output.error}` }], structuredContent: output, isError: true }; } wasInstalled = !installResult.alreadyInstalled; } // Run claude mcp add (will open browser for OAuth) const cliResult = configManager.addMCPViaCLI(config); if (!cliResult.success) { return { content: [{ type: 'text', text: `āŒ ${cliResult.message}` }], structuredContent: { success: false, serverId: mcpId, message: cliResult.message, requiresRestart: false, error: cliResult.message }, isError: true }; } const installMsg = wasInstalled ? 'āœ… Installed and added' : 'āœ… Added'; return { content: [ { type: 'text', text: `${installMsg} ${config.name} via Claude CLI!\n\nšŸ” OAuth authorization completed.\n\nāš ļø You must RESTART Claude Code for changes to take effect.` } ], structuredContent: { success: true, serverId: mcpId, message: cliResult.message, requiresRestart: true, usedCLI: true } }; } // Non-OAuth MCP - use config file approach let wasInstalled = false; if (autoInstall && config.packageName) { const { autoInstall: runAutoInstall } = await import('./auto-installer.js'); const installResult = await runAutoInstall(config); if (!installResult.success && !installResult.alreadyInstalled) { const output = { success: false, serverId: mcpId, message: `Failed to install ${config.name}: ${installResult.error}`, requiresRestart: false, error: `Failed to install ${config.name}: ${installResult.error}` }; return { content: [{ type: 'text', text: `āŒ ${output.error}` }], structuredContent: output, isError: true }; } wasInstalled = !installResult.alreadyInstalled; } // Add to config const result = configManager.addMCPToConfig(config); if (!result.success) { return { content: [{ type: 'text', text: `āŒ ${result.message}` }], structuredContent: { success: false, serverId: mcpId, configPath: result.configPath, product: result.product, message: result.message || 'Failed to add MCP to config', requiresRestart: false, error: result.message }, isError: true }; } const output = { success: true, serverId: mcpId, configPath: result.configPath, product: result.product, message: result.message, requiresRestart: true }; const installMessage = wasInstalled ? `āœ… Installed and ` : 'āœ… '; const restartMessage = result.product === 'code' ? 'āš ļø You must RESTART Claude Code for changes to take effect.' : 'āš ļø You must RESTART Claude Desktop for changes to take effect.'; return { content: [ { type: 'text', text: `${installMessage}${result.message}\n\n${restartMessage}\n\nConfig file: ${result.configPath}` } ], structuredContent: output }; } catch (error) { console.error(`āŒ Unexpected error in connect_mcp:`, error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: `āŒ Unexpected error: ${errorMessage}` }], structuredContent: { success: false, serverId: mcpId, message: errorMessage, requiresRestart: false, error: errorMessage }, isError: true }; } }); // ============================================ // META-TOOL: list_active_mcps (from config) // ============================================ server.registerTool('list_active_mcps', { title: 'List MCP Servers in Config', description: 'Show all currently connected MCP servers', inputSchema: {}, outputSchema: { success: z.boolean(), mcps: z.array(z.string()), count: z.number(), configPath: z.string(), product: z.string() } }, async () => { console.error('šŸ“‹ Listing MCPs in config...'); const result = configManager.listMCPsInConfig(); const output = { success: result.success, mcps: result.mcps, count: result.mcps.length, configPath: result.configPath, product: result.product }; return { content: [ { type: 'text', text: result.mcps.length > 0 ? `MCPs in ${result.product === 'code' ? 'Claude Code' : 'Claude Desktop'} config (${result.mcps.length}):\n\n${result.mcps.map(id => `• ${id}`).join('\n')}\n\nConfig file: ${result.configPath}` : `No MCPs in ${result.product === 'code' ? 'Claude Code' : 'Claude Desktop'} config\n\nConfig file: ${result.configPath}` } ], structuredContent: output }; }); // ============================================ // META-TOOL: disconnect_mcp (remove from config) // ============================================ server.registerTool('disconnect_mcp', { title: 'Remove MCP from Config', description: 'Disconnect from an active MCP server', inputSchema: { connectionId: z.string().describe('ID of the connection to close') }, outputSchema: { success: z.boolean(), message: z.string(), requiresRestart: z.boolean(), error: z.string().optional() } }, async ({ connectionId }) => { console.error(`šŸ”Œ Removing MCP from config: ${connectionId}...`); const result = configManager.removeMCPFromConfig(connectionId); const output = { success: result.success, message: result.message, requiresRestart: result.success, error: result.success ? undefined : result.message }; const restartMessage = result.success ? `\n\nāš ļø You must RESTART ${result.product === 'code' ? 'Claude Code' : 'Claude Desktop'} for changes to take effect.` : ''; return { content: [ { type: 'text', text: result.success ? `āœ… ${result.message}${restartMessage}` : `āŒ ${result.message}` } ], structuredContent: output, isError: !result.success }; }); // ============================================ // META-TOOL: generate_add_command // ============================================ server.registerTool('generate_add_command', { title: 'Generate Claude MCP Add Command', description: 'Generate a `claude mcp add` command for manual installation', inputSchema: { mcpId: z.string().describe('ID of the MCP server') }, outputSchema: { success: z.boolean(), command: z.string().optional(), error: z.string().optional() } }, async ({ mcpId }) => { console.error(`šŸ“ Generating add command for: ${mcpId}...`); const config = findMCPById(mcpId); if (!config) { return { content: [{ type: 'text', text: `āŒ MCP server "${mcpId}" not found in registry` }], structuredContent: { success: false, error: `MCP server "${mcpId}" not found in registry` }, isError: true }; } const command = configManager.generateAddCommand(config); return { content: [ { type: 'text', text: `To add ${config.name}, run this command:\n\n\`\`\`\n${command}\n\`\`\`` } ], structuredContent: { success: true, command } }; }); // Connect to stdio transport const transport = new StdioServerTransport(); await server.connect(transport); console.error('āœ… MCP Orchestrator is ready!\n'); console.error('Available meta-tools:'); console.error(' • discover_mcps - šŸŽÆ INTELLIGENT: Discover relevant MCPs for your task'); console.error(' • list_available_mcps - Browse available MCP servers'); console.error(' • connect_mcp - Add MCP to Claude config (requires restart)'); console.error(' • list_active_mcps - Show MCPs in config'); console.error(' • disconnect_mcp - Remove MCP from config (requires restart)'); console.error(' • generate_add_command - Generate `claude mcp add` command\n'); } // Handle CLI commands if (process.argv.includes('setup-claude')) { // Run setup instead of starting server import('./setup-claude.js').then(module => { // Setup will run automatically }).catch(console.error); } else { // Normal server mode main().catch(console.error); }