UNPKG

@aiondadotcom/mcp-salesforce

Version:

Model Context Protocol (MCP) server for Salesforce integration using OAuth authentication

389 lines (345 loc) 11.5 kB
import jsforce from 'jsforce'; import { TokenManager } from '../auth/token-manager.js'; import { FileStorageManager } from '../auth/file-storage.js'; export class SalesforceClient { constructor(clientId, clientSecret, instanceUrl) { this.clientId = clientId; this.clientSecret = clientSecret; this.instanceUrl = instanceUrl; this.tokenManager = new TokenManager(clientId, clientSecret, instanceUrl); this.connection = null; this.initialized = false; } /** * Initialize the Salesforce client */ async initialize() { if (this.initialized) { return true; } try { // Initialize token manager const hasTokens = await this.tokenManager.initialize(); if (!hasTokens) { throw new Error('Authentication required - no valid tokens found. Use the salesforce_auth tool to authenticate with Salesforce.'); } // Create jsforce connection await this.createConnection(); this.initialized = true; return true; } catch (error) { throw error; } } /** * Create and configure jsforce connection */ async createConnection() { try { const accessToken = await this.tokenManager.getValidAccessToken(); const tokenInfo = this.tokenManager.getTokenInfo(); // Get API version from config file const fileStorage = new FileStorageManager(); const apiConfig = await fileStorage.getApiConfig(); this.connection = new jsforce.Connection({ instanceUrl: tokenInfo.instance_url, accessToken: accessToken, version: apiConfig.apiVersion }); // Test connection await this.connection.identity(); } catch (error) { throw new Error(`Failed to create Salesforce connection: ${error.message}`); } } /** * Ensure connection is valid, refresh if needed */ async ensureValidConnection() { if (!this.initialized) { await this.initialize(); } try { // Check if token needs refresh if (await this.tokenManager.needsRefresh()) { await this.tokenManager.refreshTokens(); await this.createConnection(); } } catch (error) { throw error; } } /** * Execute SOQL query */ async query(soql, options = {}) { await this.ensureValidConnection(); try { const result = await this.connection.query(soql, options); return { totalSize: result.totalSize, done: result.done, records: result.records, nextRecordsUrl: result.nextRecordsUrl }; } catch (error) { // Handle authentication errors first if (this.isAuthenticationError(error)) { throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.'); } // Handle specific Salesforce errors if (error.name === 'INVALID_QUERY' || error.errorCode === 'INVALID_QUERY') { throw new Error(`Invalid SOQL query: ${error.message}`); } if (error.name === 'INVALID_FIELD' || error.errorCode === 'INVALID_FIELD') { throw new Error(`Invalid field in query: ${error.message}`); } if (error.name === 'INVALID_TYPE' || error.errorCode === 'INVALID_TYPE') { throw new Error(`Invalid object type: ${error.message}`); } // Handle authentication errors if (error.message.includes('Session expired') || error.message.includes('INVALID_SESSION_ID')) { await this.tokenManager.refreshTokens(); await this.createConnection(); // Retry once after token refresh try { const retryResult = await this.connection.query(soql, options); return { totalSize: retryResult.totalSize, done: retryResult.done, records: retryResult.records, nextRecordsUrl: retryResult.nextRecordsUrl }; } catch (retryError) { throw new Error(`Query failed after token refresh: ${retryError.message}`); } } // Generic error handling with better message const errorMessage = error.message || 'Unknown Salesforce error'; throw new Error(`Salesforce query error: ${this.formatSalesforceError(error)}`); } } /** * Create a new record */ async create(sobject, data) { await this.ensureValidConnection(); try { const result = await this.connection.sobject(sobject).create(data); if (result.success) { return { id: result.id, success: true, sobject: sobject, data: data }; } else { throw new Error(`Create failed: ${JSON.stringify(result.errors)}`); } } catch (error) { if (this.isAuthenticationError(error)) { throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.'); } throw new Error(`Create ${sobject} failed: ${this.formatSalesforceError(error)}`); } } /** * Update an existing record */ async update(sobject, id, data) { await this.ensureValidConnection(); try { const result = await this.connection.sobject(sobject).update({ Id: id, ...data }); if (result.success) { return { id: result.id, success: true, sobject: sobject, data: data }; } else { throw new Error(`Update failed: ${JSON.stringify(result.errors)}`); } } catch (error) { if (this.isAuthenticationError(error)) { throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.'); } throw new Error(`Update ${sobject} failed: ${this.formatSalesforceError(error)}`); } } /** * Delete a record */ async delete(sobject, id) { await this.ensureValidConnection(); try { const result = await this.connection.sobject(sobject).destroy(id); if (result.success) { return { id: result.id, success: true, sobject: sobject }; } else { throw new Error(`Delete failed: ${JSON.stringify(result.errors)}`); } } catch (error) { if (this.isAuthenticationError(error)) { throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.'); } throw new Error(`Delete ${sobject} failed: ${this.formatSalesforceError(error)}`); } } /** * Describe an SObject (get schema information) */ async describe(sobject) { await this.ensureValidConnection(); try { const result = await this.connection.sobject(sobject).describe(); return { name: result.name, label: result.label, labelPlural: result.labelPlural, keyPrefix: result.keyPrefix, createable: result.createable, updateable: result.updateable, deletable: result.deletable, queryable: result.queryable, fields: result.fields.map(field => ({ name: field.name, label: field.label, type: field.type, length: field.length, required: !field.nillable && !field.defaultedOnCreate, createable: field.createable, updateable: field.updateable, picklistValues: field.picklistValues || [], referenceTo: field.referenceTo || [], relationshipName: field.relationshipName })), recordTypeInfos: result.recordTypeInfos || [] }; } catch (error) { if (this.isAuthenticationError(error)) { throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.'); } throw new Error(`Describe ${sobject} failed: ${this.formatSalesforceError(error)}`); } } /** * Get all available SObjects */ async describeGlobal() { await this.ensureValidConnection(); try { const result = await this.connection.describeGlobal(); const sobjects = result.sobjects .filter(sobject => sobject.queryable) // Only queryable objects .map(sobject => ({ name: sobject.name, label: sobject.label, labelPlural: sobject.labelPlural, keyPrefix: sobject.keyPrefix, custom: sobject.custom, createable: sobject.createable, updateable: sobject.updateable, deletable: sobject.deletable, queryable: sobject.queryable })) .sort((a, b) => a.label.localeCompare(b.label)); return sobjects; } catch (error) { if (this.isAuthenticationError(error)) { throw new Error('Authentication required - your Salesforce session has expired. Use the salesforce_auth tool to re-authenticate.'); } throw new Error(`Global describe failed: ${this.formatSalesforceError(error)}`); } } /** * Get user information */ async getUserInfo() { await this.ensureValidConnection(); try { const identity = await this.connection.identity(); return { id: identity.user_id, username: identity.username, display_name: identity.display_name, email: identity.email, organization_id: identity.organization_id, urls: identity.urls }; } catch (error) { throw new Error(`Failed to get user info: ${this.formatSalesforceError(error)}`); } } /** * Format Salesforce error messages for better readability */ formatSalesforceError(error) { if (error.name === 'INVALID_FIELD') { return `Invalid field: ${error.message}`; } if (error.name === 'REQUIRED_FIELD_MISSING') { return `Required field missing: ${error.message}`; } if (error.name === 'FIELD_CUSTOM_VALIDATION_EXCEPTION') { return `Validation rule failed: ${error.message}`; } if (error.name === 'INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY') { return `Insufficient permissions: ${error.message}`; } if (error.name === 'INVALID_SESSION_ID') { return 'Session expired - please re-authenticate'; } return error.message || 'Unknown Salesforce error'; } /** * Check if an error is authentication-related */ isAuthenticationError(error) { const authErrorIndicators = [ 'INVALID_SESSION_ID', 'Session expired', 'invalid_grant', 'Authentication failure', 'Unauthorized', 'Invalid token', 'Token expired', 'Not authenticated', 'Authentication required', 'No access token available', 'refresh token is invalid', 'Session has expired' ]; const errorMessage = error.message || ''; const errorString = error.toString() || ''; return authErrorIndicators.some(indicator => errorMessage.includes(indicator) || errorString.includes(indicator) ); } /** * Test connection health */ async testConnection() { try { await this.ensureValidConnection(); const identity = await this.connection.identity(); return { connected: true, user: identity.username, organization: identity.organization_id }; } catch (error) { return { connected: false, error: error.message }; } } }