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.

242 lines (241 loc) 9.99 kB
/** * Connection Manager * Handles spawning, connecting, and managing MCP server connections */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'; import { randomUUID } from 'crypto'; import { MCPOAuthClientProvider } from './oauth/client-provider.js'; import { OAuthCallbackServer } from './oauth/callback-server.js'; export class ConnectionManager { connections = new Map(); /** * Detect if MCP is remote (HTTP/SSE) based on command */ isRemoteMCP(config) { if (config.command?.startsWith('remote:')) { // Format: "remote:sse:https://example.com" or "remote:streamable-http:https://example.com" const parts = config.command.split(':'); if (parts.length >= 3) { const type = parts[1]; const url = parts.slice(2).join(':'); // Rejoin in case URL has colons if (type === 'sse') { return { isRemote: true, type: 'sse', url }; } else if (type === 'streamable-http') { return { isRemote: true, type: 'http', url }; } } } // Check if packageName is a URL if (config.packageName?.startsWith('http://') || config.packageName?.startsWith('https://')) { return { isRemote: true, type: 'http', url: config.packageName }; } return { isRemote: false }; } /** * Create appropriate transport based on MCP type */ createTransport(config, oauthProvider) { const remoteInfo = this.isRemoteMCP(config); if (remoteInfo.isRemote && remoteInfo.url) { console.error(`🌐 Creating remote transport (${remoteInfo.type}) for ${config.name}...`); if (remoteInfo.type === 'sse') { // SSE also supports OAuth return new SSEClientTransport(new URL(remoteInfo.url), { authProvider: oauthProvider }); } else { // StreamableHTTP supports OAuth return new StreamableHTTPClientTransport(new URL(remoteInfo.url), { authProvider: oauthProvider }); } } // Default: local stdio transport console.error(`💻 Creating local transport for ${config.name}...`); return new StdioClientTransport({ command: config.command, args: config.args || [], env: config.env ? { ...process.env, ...config.env } : undefined }); } /** * Connect to an MCP server with OAuth support */ async connect(config) { try { // Check if already connected const existing = this.findByServerId(config.id); if (existing) { return { success: true, connection: existing }; } // Check if this is a remote OAuth MCP const remoteInfo = this.isRemoteMCP(config); const needsOAuth = remoteInfo.isRemote && config.env?.authType === 'oauth'; let oauthProvider; let callbackServer; let transport; if (needsOAuth) { console.error(`🔐 ${config.name} requires OAuth authentication`); // Create OAuth provider oauthProvider = new MCPOAuthClientProvider(config.name); // Create transport with OAuth transport = this.createTransport(config, oauthProvider); // Start callback server callbackServer = new OAuthCallbackServer(8095); } else { // No OAuth needed transport = this.createTransport(config); } // Create client (using let so we can replace it after OAuth) let client = new Client({ name: 'mcp-orchestrator', version: '0.4.1' }, { capabilities: {} }); try { // Attempt connection await client.connect(transport); } catch (error) { if (error instanceof UnauthorizedError && callbackServer && oauthProvider) { // OAuth flow required console.error(`🌐 Starting OAuth authorization flow...`); // Wait for user to authorize (browser opens automatically via oauthProvider) const callbackPromise = callbackServer.waitForCallback(); const authCode = await callbackPromise; console.error(`✅ Authorization code received`); // Finish OAuth flow (both SSE and StreamableHTTP support finishAuth) if (transport instanceof StreamableHTTPClientTransport || transport instanceof SSEClientTransport) { await transport.finishAuth(authCode); console.error(`✅ OAuth flow completed, tokens saved`); } // Close the old transport before creating a new one try { await client.close(); console.error(`✅ Closed old transport`); } catch (e) { console.error(`⚠️ Error closing old transport (might already be closed):`, e); } // Create a NEW transport with the now-authenticated OAuth provider console.error(`🔌 Creating new authenticated transport...`); const newTransport = this.createTransport(config, oauthProvider); console.error(`✅ New transport created with authenticated OAuth provider`); // Create a NEW client const newClient = new Client({ name: 'mcp-orchestrator', version: '0.4.1' }, { capabilities: {} }); // Connect with authenticated transport console.error(`🔌 Connecting with OAuth token...`); await newClient.connect(newTransport); console.error(`✅ Successfully reconnected with OAuth token`); // Update references to use new client/transport client = newClient; transport = newTransport; console.error(`✅ Updated client and transport references`); } else { // Not an OAuth error, rethrow throw error; } } finally { // Clean up callback server if (callbackServer) { callbackServer.stop(); } } // List available tools console.error(`📋 Requesting tools list from ${config.name}...`); const toolsResult = await client.listTools(); console.error(`✅ Received ${toolsResult.tools.length} tools from ${config.name}`); // Create connection object const connection = { id: randomUUID(), serverId: config.id, client, transport, status: 'connected', connectedAt: new Date(), tools: toolsResult.tools }; // Store in pool this.connections.set(connection.id, connection); console.error(`✅ Connected to ${config.name} (${toolsResult.tools.length} tools)`); return { success: true, connection }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`❌ Failed to connect to ${config.name}:`, errorMessage); return { success: false, error: errorMessage }; } } /** * Disconnect from an MCP server */ async disconnect(connectionId) { const connection = this.connections.get(connectionId); if (!connection) { console.error(`❌ Connection ${connectionId} not found`); return false; } try { // console.error(`🔌 Disconnecting from ${connection.serverId}...`); await connection.client.close(); connection.status = 'disconnected'; this.connections.delete(connectionId); // console.error(`✅ Disconnected from ${connection.serverId}`); return true; } catch (error) { // console.error(`❌ Error disconnecting:`, error); return false; } } /** * Get a connection by ID */ get(connectionId) { return this.connections.get(connectionId); } /** * Find connection by server ID */ findByServerId(serverId) { return Array.from(this.connections.values()) .find(conn => conn.serverId === serverId); } /** * Find connection that has a specific tool */ findByTool(toolName) { return Array.from(this.connections.values()) .find(conn => conn.tools.some(t => t.name === toolName)); } /** * Get all active connections */ getAll() { return Array.from(this.connections.values()); } /** * Get connection count */ count() { return this.connections.size; } }