UNPKG

@andrew_eragon/mcp-server-salesforce

Version:

A SaaS-ready Salesforce connector MCP Server with connection pooling and multi-user support.

201 lines (200 loc) โ€ข 8.84 kB
import { ConnectionType } from '../types/connection.js'; import { createSalesforceConnection } from './connection.js'; /** * Connection pool for managing multiple user connections */ class ConnectionPool { constructor() { this.connections = new Map(); this.maxIdleTime = 30 * 60 * 1000; // 30 minutes this.cleanupInterval = 5 * 60 * 1000; // 5 minutes // Start periodic cleanup this.startCleanup(); } /** * Generate a unique key for user credentials */ generateKey(credentials) { const { connectionType, username, accessToken, instanceUrl, clientId } = credentials; switch (connectionType) { case ConnectionType.Access_Token: // For access token, use token + instance URL return `access_token:${accessToken}:${instanceUrl}`; case ConnectionType.OAuth_2_0_Client_Credentials: // For OAuth, use client ID + instance URL return `oauth:${clientId}:${instanceUrl}`; case ConnectionType.User_Password: default: // For username/password, use username + instance URL return `user_pass:${username}:${instanceUrl}`; } } /** * Get or create a connection for the given credentials */ async getConnection(credentials) { const key = this.generateKey(credentials); const now = Date.now(); // Check if we have a valid cached connection const entry = this.connections.get(key); if (entry) { // Update last accessed time entry.lastAccessed = now; // Verify connection is still valid try { // Simple validation - check if we can get identity await entry.connection.identity(); console.error(`โ™ป๏ธ Reusing connection for key: ${key.substring(0, 50)}...`); return entry.connection; } catch (error) { console.error(`โŒ Cached connection invalid, creating new one: ${error instanceof Error ? error.message : String(error)}`); this.connections.delete(key); } } // Create new connection console.error(`๐Ÿ”Œ Creating new connection for key: ${key.substring(0, 50)}...`); const config = { type: credentials.connectionType, loginUrl: credentials.instanceUrl || 'https://login.salesforce.com' }; // Temporarily set environment variables for this connection const originalEnv = { ...process.env }; try { // Set credentials in environment for connection creation if (credentials.connectionType === ConnectionType.Access_Token) { process.env.SALESFORCE_ACCESS_TOKEN = credentials.accessToken; process.env.SALESFORCE_INSTANCE_URL = credentials.instanceUrl; } else if (credentials.connectionType === ConnectionType.OAuth_2_0_Client_Credentials) { process.env.SALESFORCE_CLIENT_ID = credentials.clientId; process.env.SALESFORCE_CLIENT_SECRET = credentials.clientSecret; process.env.SALESFORCE_INSTANCE_URL = credentials.instanceUrl; } else if (credentials.connectionType === ConnectionType.OAuth_Response) { process.env.SALESFORCE_ACCESS_TOKEN = credentials.accessToken; process.env.SALESFORCE_REFRESH_TOKEN = credentials.refreshToken; process.env.SALESFORCE_INSTANCE_URL = credentials.instanceUrl; process.env.SALESFORCE_ID = credentials.id; process.env.SALESFORCE_SCOPE = credentials.scope; process.env.SALESFORCE_ISSUED_AT = credentials.issuedAt; process.env.SALESFORCE_SIGNATURE = credentials.signature; process.env.SALESFORCE_TOKEN_TYPE = credentials.tokenType; } else { process.env.SALESFORCE_USERNAME = credentials.username; process.env.SALESFORCE_PASSWORD = credentials.password; process.env.SALESFORCE_TOKEN = credentials.token; process.env.SALESFORCE_INSTANCE_URL = credentials.instanceUrl; } process.env.SALESFORCE_CONNECTION_TYPE = credentials.connectionType; const connection = await createSalesforceConnection(config); // Cache the connection this.connections.set(key, { connection, lastAccessed: now, credentials }); return connection; } finally { // Restore original environment process.env = originalEnv; } } /** * Clean up idle connections */ cleanup() { const now = Date.now(); const keysToDelete = []; for (const [key, entry] of this.connections.entries()) { if (now - entry.lastAccessed > this.maxIdleTime) { keysToDelete.push(key); } } if (keysToDelete.length > 0) { console.error(`๐Ÿงน Cleaning up ${keysToDelete.length} idle connection(s)`); keysToDelete.forEach(key => this.connections.delete(key)); } } /** * Start periodic cleanup */ startCleanup() { this.cleanupTimer = setInterval(() => { this.cleanup(); }, this.cleanupInterval); } /** * Stop cleanup and clear all connections */ destroy() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.connections.clear(); } /** * Get pool statistics */ getStats() { return { activeConnections: this.connections.size, connectionKeys: Array.from(this.connections.keys()).map(k => k.substring(0, 50) + '...') }; } } // Singleton pool instance export const connectionPool = new ConnectionPool(); /** * Extract user credentials from environment variables * This supports per-request environment variables set by the MCP client */ export function extractCredentialsFromEnv() { const connectionType = process.env.SALESFORCE_CONNECTION_TYPE || ConnectionType.User_Password; const credentials = { connectionType, instanceUrl: process.env.SALESFORCE_INSTANCE_URL || 'https://login.salesforce.com' }; switch (connectionType) { case ConnectionType.Access_Token: credentials.accessToken = process.env.SALESFORCE_ACCESS_TOKEN; credentials.instanceUrl = process.env.SALESFORCE_INSTANCE_URL; if (!credentials.accessToken || !credentials.instanceUrl) { throw new Error('SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL are required for Access Token authentication'); } break; case ConnectionType.OAuth_2_0_Client_Credentials: credentials.clientId = process.env.SALESFORCE_CLIENT_ID; credentials.clientSecret = process.env.SALESFORCE_CLIENT_SECRET; credentials.instanceUrl = process.env.SALESFORCE_INSTANCE_URL; if (!credentials.clientId || !credentials.clientSecret) { throw new Error('SALESFORCE_CLIENT_ID and SALESFORCE_CLIENT_SECRET are required for OAuth 2.0 Client Credentials Flow'); } break; case ConnectionType.OAuth_Response: credentials.accessToken = process.env.SALESFORCE_ACCESS_TOKEN; credentials.refreshToken = process.env.SALESFORCE_REFRESH_TOKEN; credentials.instanceUrl = process.env.SALESFORCE_INSTANCE_URL; credentials.id = process.env.SALESFORCE_ID; credentials.scope = process.env.SALESFORCE_SCOPE; credentials.issuedAt = process.env.SALESFORCE_ISSUED_AT; credentials.signature = process.env.SALESFORCE_SIGNATURE; credentials.tokenType = process.env.SALESFORCE_TOKEN_TYPE; if (!credentials.accessToken || !credentials.instanceUrl) { throw new Error('SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL are required for OAuth Response authentication'); } break; case ConnectionType.User_Password: default: credentials.username = process.env.SALESFORCE_USERNAME; credentials.password = process.env.SALESFORCE_PASSWORD; credentials.token = process.env.SALESFORCE_TOKEN; if (!credentials.username || !credentials.password) { throw new Error('SALESFORCE_USERNAME and SALESFORCE_PASSWORD are required for Username/Password authentication'); } break; } return credentials; }