@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
JavaScript
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;
}