Skip to main content
Glama
steffensbola

Salesforce MCP Server

by steffensbola
salesforce-client.ts11.2 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import { SalesforceConfig, SalesforceQueryResult, SalesforceRecord, SalesforceObjectMetadata, SalesforceCreateResult, SalesforceField, } from '../types/salesforce.js'; import { SalesforcePasswordAuth } from '../auth/password-auth.js'; import { getApiUrl } from '../config/config.js'; import { debugLog, errorLog } from '../utils/debug-log.js'; /** * Main Salesforce client that handles operations and caching */ export class SalesforceClient { private readonly config: SalesforceConfig; private authClient: SalesforcePasswordAuth | null = null; private readonly sobjectsCache: Map<string, SalesforceField[]> = new Map(); constructor(config: SalesforceConfig) { this.config = config; } /** * Establishes connection to Salesforce using authentication methods * Priority order: * 1. Environment variables (access token + instance URL) * 2. Username/Password with OAuth credentials */ async connect(): Promise<boolean> { try { return ( (await this.tryAccessTokenAuth()) || (await this.tryUsernamePasswordAuth()) || this.handleAuthFailure() ); } catch (error) { errorLog('Salesforce connection failed:', this.getErrorMessage(error)); return this.tryTokenRefresh(); } } /** * Try authentication using access token from configuration */ private async tryAccessTokenAuth(): Promise<boolean> { if (this.config.accessToken && this.config.instanceUrl) { debugLog('🔑 Using access token from environment variables...'); // Test the token by making a simple API call try { await this.makeRequest('GET', '/sobjects'); debugLog('✅ Access token authentication successful!'); return true; } catch (error) { errorLog('❌ Access token is invalid or expired', error); return false; } } return false; } /** * Try username/password authentication with OAuth flow */ private async tryUsernamePasswordAuth(): Promise<boolean> { if ( this.config.username && this.config.password && this.config.clientId && this.config.clientSecret ) { debugLog('🔐 Using username/password authentication with OAuth flow...'); this.authClient = new SalesforcePasswordAuth(this.config.clientId, this.config.clientSecret); const success = await this.authClient.authenticate( this.config.username, this.config.password, this.config.securityToken ?? '', this.config.sandbox ); if (success) { // Update config with the obtained tokens this.config.accessToken = this.authClient.accessToken!; this.config.instanceUrl = this.authClient.instanceUrl!; return true; } errorLog('❌ Username/password authentication failed'); return false; } return false; } /** * Handle the case when no authentication method is available */ private handleAuthFailure(): boolean { errorLog('❌ No valid authentication method found. Please set one of:'); errorLog('1. SALESFORCE_ACCESS_TOKEN + SALESFORCE_INSTANCE_URL'); errorLog( '2. SALESFORCE_USERNAME + SALESFORCE_PASSWORD + SALESFORCE_CLIENT_ID + SALESFORCE_CLIENT_SECRET' ); return false; } /** * Try refreshing token if OAuth was used */ private async tryTokenRefresh(): Promise<boolean> { if (this.authClient?.refreshToken) { debugLog('🔄 Attempting to refresh access token...'); if (await this.authClient.refreshAccessToken(this.config.sandbox)) { this.config.accessToken = this.authClient.accessToken!; this.config.instanceUrl = this.authClient.instanceUrl!; return true; } errorLog('❌ Token refresh failed'); } return false; } /** * Make authenticated request to Salesforce API */ async makeRequest<T = unknown>( method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, data?: unknown, params?: Record<string, unknown> ): Promise<T> { if (!this.config.accessToken || !this.config.instanceUrl) { throw new Error('Not authenticated. Call connect() first.'); } const url = getApiUrl(this.config.instanceUrl, path); const config: AxiosRequestConfig = { method, url, headers: { Authorization: `Bearer ${this.config.accessToken}`, 'Content-Type': 'application/json', }, params, data, }; try { const response = await axios(config); return response.data; } catch (error) { //eslint-disable-next-line no-magic-numbers if (axios.isAxiosError(error) && error.response?.status === 401) { // Token might be expired, try to refresh if (await this.tryTokenRefresh()) { // Retry the request with new token config.headers!.Authorization = `Bearer ${this.config.accessToken}`; const retryResponse = await axios(config); return retryResponse.data; } } throw this.handleRequestError(error); } } /** * Execute SOQL query */ async query<T = SalesforceRecord>(query: string): Promise<SalesforceQueryResult<T>> { return this.makeRequest('GET', '/query', undefined, { q: query }); } /** * Execute SOQL query with all records (handles pagination) */ async queryAll<T = SalesforceRecord>(query: string): Promise<SalesforceQueryResult<T>> { const result = await this.query<T>(query); // If there are more records, fetch them while (!result.done && result.nextRecordsUrl) { const nextResult = await this.makeRequest<SalesforceQueryResult<T>>( 'GET', result.nextRecordsUrl.replace(`/services/data/v58.0`, '') ); result.records.push(...nextResult.records); result.done = nextResult.done; result.nextRecordsUrl = nextResult.nextRecordsUrl; result.totalSize = nextResult.totalSize; } return result; } /** * Execute SOSL search */ async search<T = SalesforceRecord>(searchQuery: string): Promise<T[]> { const result = await this.makeRequest<{ searchRecords?: T[] }>('GET', '/search', undefined, { q: searchQuery, }); return result.searchRecords ?? []; } /** * Get object metadata including fields */ async getObjectFields(objectName: string): Promise<SalesforceField[]> { // Check cache first if (this.sobjectsCache.has(objectName)) { return this.sobjectsCache.get(objectName)!; } const metadata = await this.makeRequest<SalesforceObjectMetadata>( 'GET', `/sobjects/${objectName}/describe` ); const filteredFields: SalesforceField[] = metadata.fields.map(field => ({ label: field.label, name: field.name, updateable: field.updateable, type: field.type, length: field.length, picklistValues: field.picklistValues, })); // Cache the result this.sobjectsCache.set(objectName, filteredFields); return filteredFields; } /** * Get a specific record by ID */ async getRecord<T = SalesforceRecord>( objectName: string, recordId: string, fields?: string[] ): Promise<T> { let path = `/sobjects/${objectName}/${recordId}`; if (fields && fields.length > 0) { path += `?fields=${fields.join(',')}`; } return this.makeRequest('GET', path); } /** * Create a new record */ async createRecord( objectName: string, data: Record<string, any> ): Promise<SalesforceCreateResult> { return this.makeRequest('POST', `/sobjects/${objectName}`, data); } /** * Update an existing record */ async updateRecord( objectName: string, recordId: string, data: Record<string, any> ): Promise<boolean> { await this.makeRequest('PATCH', `/sobjects/${objectName}/${recordId}`, data); return true; } /** * Delete a record */ async deleteRecord(objectName: string, recordId: string): Promise<boolean> { await this.makeRequest('DELETE', `/sobjects/${objectName}/${recordId}`); return true; } /** * Execute Tooling API request */ async toolingExecute<T = any>( action: string, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', data?: any ): Promise<T> { if (!this.config.instanceUrl) { throw new Error('Not authenticated. Call connect() first.'); } const url = `${this.config.instanceUrl}/services/data/v58.0/tooling/${action}`; const config: AxiosRequestConfig = { method, url, headers: { Authorization: `Bearer ${this.config.accessToken}`, 'Content-Type': 'application/json', }, data, }; const response = await axios(config); return response.data; } /** * Execute Apex REST request */ async apexExecute<T = any>( action: string, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', data?: any ): Promise<T> { if (!this.config.instanceUrl) { throw new Error('Not authenticated. Call connect() first.'); } const cleanAction = action.startsWith('/') ? action : `/${action}`; const url = `${this.config.instanceUrl}/services/apexrest${cleanAction}`; const config: AxiosRequestConfig = { method, url, headers: { Authorization: `Bearer ${this.config.accessToken}`, 'Content-Type': 'application/json', }, data, }; const response = await axios(config); return response.data; } /** * Make a direct REST API call */ async restful<T = any>( path: string, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', params?: Record<string, any>, data?: any ): Promise<T> { return this.makeRequest(method, path, data, params); } /** * Check if client is authenticated */ isAuthenticated(): boolean { return !!(this.config.accessToken && this.config.instanceUrl); } /** * Get the current instance URL */ getInstanceUrl(): string | null { return this.config.instanceUrl ?? null; } /** * Handle request errors */ private handleRequestError(error: unknown): Error { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; if (axiosError.response) { const errorData = axiosError.response.data as any; const message = errorData?.message ?? errorData?.[0]?.message ?? `HTTP ${axiosError.response.status}`; return new Error(`Salesforce API Error: ${message}`); } else if (axiosError.request) { return new Error('Network error: Unable to reach Salesforce API'); } } if (error instanceof Error) { return error; } return new Error('Unknown error occurred'); } /** * Get error message from unknown error type */ private getErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (typeof error === 'string') { return error; } return 'Unknown error occurred'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steffensbola/salesforce-mcp-ts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server