@andrew_eragon/mcp-server-salesforce
Version:
A SaaS-ready Salesforce connector MCP Server with connection pooling and multi-user support.
340 lines (339 loc) • 16 kB
JavaScript
import * as dotenv from "dotenv";
import { AGGREGATE_QUERY, handleAggregateQuery } from "./tools/aggregateQuery.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { DESCRIBE_OBJECT, handleDescribeObject } from "./tools/describe.js";
import { DML_RECORDS, handleDMLRecords } from "./tools/dml.js";
import { EXECUTE_ANONYMOUS, handleExecuteAnonymous } from "./tools/executeAnonymous.js";
import { MANAGE_DEBUG_LOGS, handleManageDebugLogs } from "./tools/manageDebugLogs.js";
import { MANAGE_FIELD, handleManageField } from "./tools/manageField.js";
import { MANAGE_FIELD_PERMISSIONS, handleManageFieldPermissions } from "./tools/manageFieldPermissions.js";
import { MANAGE_OBJECT, handleManageObject } from "./tools/manageObject.js";
import { QUERY_RECORDS, handleQueryRecords } from "./tools/query.js";
import { READ_APEX, handleReadApex } from "./tools/readApex.js";
import { READ_APEX_TRIGGER, handleReadApexTrigger } from "./tools/readApexTrigger.js";
import { SEARCH_ALL, handleSearchAll } from "./tools/searchAll.js";
import { SEARCH_OBJECTS, handleSearchObjects } from "./tools/search.js";
import { WRITE_APEX, handleWriteApex } from "./tools/writeApex.js";
import { WRITE_APEX_TRIGGER, handleWriteApexTrigger } from "./tools/writeApexTrigger.js";
import { connectionPool, extractCredentialsFromEnv } from "./utils/connectionPool.js";
import { retry, withTimeout } from "./utils/parallel.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
dotenv.config();
const server = new Server({
name: "salesforce-mcp-server",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
SEARCH_OBJECTS,
DESCRIBE_OBJECT,
QUERY_RECORDS,
AGGREGATE_QUERY,
DML_RECORDS,
MANAGE_OBJECT,
MANAGE_FIELD,
MANAGE_FIELD_PERMISSIONS,
SEARCH_ALL,
READ_APEX,
WRITE_APEX,
READ_APEX_TRIGGER,
WRITE_APEX_TRIGGER,
EXECUTE_ANONYMOUS,
MANAGE_DEBUG_LOGS
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const startTime = Date.now();
try {
const { name, arguments: args } = request.params;
if (!args)
throw new Error('Arguments are required');
// Extract user credentials from environment variables (set per-request by client)
const credentials = extractCredentialsFromEnv();
// Get or create connection from pool with retry logic
const conn = await retry(() => withTimeout(connectionPool.getConnection(credentials), 30000, // 30 second timeout
'Connection timeout: Failed to establish Salesforce connection'), {
maxAttempts: 3,
initialDelay: 1000,
onRetry: (error, attempt) => {
console.error(`🔄 Connection retry ${attempt}/3: ${error.message}`);
}
});
console.error(`⚡ Connection acquired in ${Date.now() - startTime}ms`);
switch (name) {
case "salesforce_search_objects": {
const { searchPattern } = args;
if (!searchPattern)
throw new Error('searchPattern is required');
return await handleSearchObjects(conn, searchPattern);
}
case "salesforce_describe_object": {
const { objectName } = args;
if (!objectName)
throw new Error('objectName is required');
return await handleDescribeObject(conn, objectName);
}
case "salesforce_query_records": {
const queryArgs = args;
if (!queryArgs.objectName || !Array.isArray(queryArgs.fields)) {
throw new Error('objectName and fields array are required for query');
}
// Type check and conversion
const validatedArgs = {
objectName: queryArgs.objectName,
fields: queryArgs.fields,
whereClause: queryArgs.whereClause,
orderBy: queryArgs.orderBy,
limit: queryArgs.limit
};
return await handleQueryRecords(conn, validatedArgs);
}
case "salesforce_aggregate_query": {
const aggregateArgs = args;
if (!aggregateArgs.objectName || !Array.isArray(aggregateArgs.selectFields) || !Array.isArray(aggregateArgs.groupByFields)) {
throw new Error('objectName, selectFields array, and groupByFields array are required for aggregate query');
}
// Type check and conversion
const validatedArgs = {
objectName: aggregateArgs.objectName,
selectFields: aggregateArgs.selectFields,
groupByFields: aggregateArgs.groupByFields,
whereClause: aggregateArgs.whereClause,
havingClause: aggregateArgs.havingClause,
orderBy: aggregateArgs.orderBy,
limit: aggregateArgs.limit
};
return await handleAggregateQuery(conn, validatedArgs);
}
case "salesforce_dml_records": {
const dmlArgs = args;
if (!dmlArgs.operation || !dmlArgs.objectName || !Array.isArray(dmlArgs.records)) {
throw new Error('operation, objectName, and records array are required for DML');
}
const validatedArgs = {
operation: dmlArgs.operation,
objectName: dmlArgs.objectName,
records: dmlArgs.records,
externalIdField: dmlArgs.externalIdField
};
return await handleDMLRecords(conn, validatedArgs);
}
case "salesforce_manage_object": {
const objectArgs = args;
if (!objectArgs.operation || !objectArgs.objectName) {
throw new Error('operation and objectName are required for object management');
}
const validatedArgs = {
operation: objectArgs.operation,
objectName: objectArgs.objectName,
label: objectArgs.label,
pluralLabel: objectArgs.pluralLabel,
description: objectArgs.description,
nameFieldLabel: objectArgs.nameFieldLabel,
nameFieldType: objectArgs.nameFieldType,
nameFieldFormat: objectArgs.nameFieldFormat,
sharingModel: objectArgs.sharingModel
};
return await handleManageObject(conn, validatedArgs);
}
case "salesforce_manage_field": {
const fieldArgs = args;
if (!fieldArgs.operation || !fieldArgs.objectName || !fieldArgs.fieldName) {
throw new Error('operation, objectName, and fieldName are required for field management');
}
const validatedArgs = {
operation: fieldArgs.operation,
objectName: fieldArgs.objectName,
fieldName: fieldArgs.fieldName,
label: fieldArgs.label,
type: fieldArgs.type,
required: fieldArgs.required,
unique: fieldArgs.unique,
externalId: fieldArgs.externalId,
length: fieldArgs.length,
precision: fieldArgs.precision,
scale: fieldArgs.scale,
referenceTo: fieldArgs.referenceTo,
relationshipLabel: fieldArgs.relationshipLabel,
relationshipName: fieldArgs.relationshipName,
deleteConstraint: fieldArgs.deleteConstraint,
picklistValues: fieldArgs.picklistValues,
description: fieldArgs.description,
grantAccessTo: fieldArgs.grantAccessTo
};
return await handleManageField(conn, validatedArgs);
}
case "salesforce_manage_field_permissions": {
const permArgs = args;
if (!permArgs.operation || !permArgs.objectName || !permArgs.fieldName) {
throw new Error('operation, objectName, and fieldName are required for field permissions management');
}
const validatedArgs = {
operation: permArgs.operation,
objectName: permArgs.objectName,
fieldName: permArgs.fieldName,
profileNames: permArgs.profileNames,
readable: permArgs.readable,
editable: permArgs.editable
};
return await handleManageFieldPermissions(conn, validatedArgs);
}
case "salesforce_search_all": {
const searchArgs = args;
if (!searchArgs.searchTerm || !Array.isArray(searchArgs.objects)) {
throw new Error('searchTerm and objects array are required for search');
}
// Validate objects array
const objects = searchArgs.objects;
if (!objects.every(obj => obj.name && Array.isArray(obj.fields))) {
throw new Error('Each object must specify name and fields array');
}
// Type check and conversion
const validatedArgs = {
searchTerm: searchArgs.searchTerm,
searchIn: searchArgs.searchIn,
objects: objects.map(obj => ({
name: obj.name,
fields: obj.fields,
where: obj.where,
orderBy: obj.orderBy,
limit: obj.limit
})),
withClauses: searchArgs.withClauses,
updateable: searchArgs.updateable,
viewable: searchArgs.viewable
};
return await handleSearchAll(conn, validatedArgs);
}
case "salesforce_read_apex": {
const apexArgs = args;
// Type check and conversion
const validatedArgs = {
className: apexArgs.className,
namePattern: apexArgs.namePattern,
includeMetadata: apexArgs.includeMetadata
};
return await handleReadApex(conn, validatedArgs);
}
case "salesforce_write_apex": {
const apexArgs = args;
if (!apexArgs.operation || !apexArgs.className || !apexArgs.body) {
throw new Error('operation, className, and body are required for writing Apex');
}
// Type check and conversion
const validatedArgs = {
operation: apexArgs.operation,
className: apexArgs.className,
apiVersion: apexArgs.apiVersion,
body: apexArgs.body
};
return await handleWriteApex(conn, validatedArgs);
}
case "salesforce_read_apex_trigger": {
const triggerArgs = args;
// Type check and conversion
const validatedArgs = {
triggerName: triggerArgs.triggerName,
namePattern: triggerArgs.namePattern,
includeMetadata: triggerArgs.includeMetadata
};
return await handleReadApexTrigger(conn, validatedArgs);
}
case "salesforce_write_apex_trigger": {
const triggerArgs = args;
if (!triggerArgs.operation || !triggerArgs.triggerName || !triggerArgs.body) {
throw new Error('operation, triggerName, and body are required for writing Apex trigger');
}
// Type check and conversion
const validatedArgs = {
operation: triggerArgs.operation,
triggerName: triggerArgs.triggerName,
objectName: triggerArgs.objectName,
apiVersion: triggerArgs.apiVersion,
body: triggerArgs.body
};
return await handleWriteApexTrigger(conn, validatedArgs);
}
case "salesforce_execute_anonymous": {
const executeArgs = args;
if (!executeArgs.apexCode) {
throw new Error('apexCode is required for executing anonymous Apex');
}
// Type check and conversion
const validatedArgs = {
apexCode: executeArgs.apexCode,
logLevel: executeArgs.logLevel
};
return await handleExecuteAnonymous(conn, validatedArgs);
}
case "salesforce_manage_debug_logs": {
const debugLogsArgs = args;
if (!debugLogsArgs.operation || !debugLogsArgs.username) {
throw new Error('operation and username are required for managing debug logs');
}
// Type check and conversion
const validatedArgs = {
operation: debugLogsArgs.operation,
username: debugLogsArgs.username,
logLevel: debugLogsArgs.logLevel,
expirationTime: debugLogsArgs.expirationTime,
limit: debugLogsArgs.limit,
logId: debugLogsArgs.logId,
includeBody: debugLogsArgs.includeBody
};
return await handleManageDebugLogs(conn, validatedArgs);
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const totalTime = Date.now() - startTime;
console.error(`❌ Request failed after ${totalTime}ms: ${errorMessage}`);
// Log connection pool stats for debugging
const stats = connectionPool.getStats();
console.error(`📊 Pool stats: ${stats.activeConnections} active connections`);
return {
content: [{
type: "text",
text: `Error: ${errorMessage}`,
}],
isError: true,
};
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("🚀 Salesforce MCP Server running on stdio (SaaS-ready with connection pooling)");
// Log initial pool stats
const stats = connectionPool.getStats();
console.error(`📊 Connection pool initialized: ${stats.activeConnections} active connections`);
// Graceful shutdown
process.on('SIGINT', () => {
console.error("🛑 Shutting down gracefully...");
connectionPool.destroy();
process.exit(0);
});
process.on('SIGTERM', () => {
console.error("🛑 Shutting down gracefully...");
connectionPool.destroy();
process.exit(0);
});
}
runServer().catch((error) => {
console.error("❌ Fatal error running server:", error);
connectionPool.destroy();
process.exit(1);
});