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
JavaScript
/**
* 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;
}
}