├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── claude_desktop_config.json ├── package.json ├── src ├── index.ts ├── test-connection.ts ├── tools │ ├── describe.ts │ ├── dml.ts │ ├── manageField.ts │ ├── manageObject.ts │ ├── query.ts │ ├── search.ts │ └── searchAll.ts ├── types │ ├── metadata.ts │ └── salesforce.ts ├── typings.d.ts └── utils │ ├── connection.ts │ └── errorHandler.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Salesforce Instance URL (Optional) 2 | # Default: https://login.salesforce.com 3 | # For sandbox use: https://test.salesforce.com 4 | SALESFORCE_INSTANCE_URL=https://login.salesforce.com 5 | 6 | # Method 1: Username/Password Authentication 7 | SALESFORCE_USERNAME=your.email@example.com 8 | SALESFORCE_PASSWORD=your_password_here 9 | SALESFORCE_TOKEN=your_security_token_here 10 | 11 | # Method 2: OAuth2 Authentication 12 | # Uncomment and fill these values to use OAuth2 instead of username/password auth 13 | # Note: Username and password are still required even with OAuth2 14 | #SALESFORCE_CONSUMER_KEY=your_connected_app_consumer_key 15 | #SALESFORCE_CONSUMER_SECRET=your_connected_app_consumer_secret 16 | 17 | # Note: 18 | # 1. Rename this file to .env to use it 19 | # 2. Never commit your actual .env file to version control 20 | # 3. Get your security token from Salesforce Setup -> Reset Security Token 21 | # 4. For OAuth2, create a Connected App in Salesforce Setup to get consumer key/secret -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Build output 6 | dist/ 7 | 8 | # Environment variables 9 | .env 10 | 11 | # IDE and OS files 12 | .DS_Store 13 | .vscode/ 14 | .idea/ 15 | 16 | # Logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tapas Mukherjee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce MCP Server 2 | 3 | An MCP (Model Context Protocol) server implementation that integrates Claude with Salesforce, enabling natural language interactions with your Salesforce data and metadata. This server allows Claude to query, modify, and manage your Salesforce objects and records using everyday language. 4 | 5 | 6 | Salesforce Server MCP server 7 | 8 | 9 | ## Features 10 | 11 | * **Object and Field Management**: Create and modify custom objects and fields using natural language 12 | * **Smart Object Search**: Find Salesforce objects using partial name matches 13 | * **Detailed Schema Information**: Get comprehensive field and relationship details for any object 14 | * **Flexible Data Queries**: Query records with relationship support and complex filters 15 | * **Data Manipulation**: Insert, update, delete, and upsert records with ease 16 | * **Cross-Object Search**: Search across multiple objects using SOSL 17 | * **Intuitive Error Handling**: Clear feedback with Salesforce-specific error details 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm install -g @surajadsul02/mcp-server-salesforce 23 | ``` 24 | 25 | ## Setup 26 | 27 | ### Salesforce Authentication 28 | You can authenticate with Salesforce using one of two methods: 29 | 30 | #### 1. Username/Password Authentication 31 | 1. Set up your Salesforce credentials 32 | 2. Get your security token (Reset from Salesforce Settings) 33 | 3. Configure the environment variables as shown in the configuration section 34 | 35 | #### 2. OAuth2 Authentication with Consumer Key/Secret 36 | 1. Set up a Connected App in Salesforce 37 | 2. Get the Consumer Key and Consumer Secret 38 | 3. Configure the environment variables as shown in the configuration section 39 | 40 | ### IDE Integration 41 | 42 | #### Cursor IDE Setup 43 | 44 | 1. Install the package globally: 45 | ```bash 46 | npm install -g @surajadsul02/mcp-server-salesforce 47 | ``` 48 | 49 | 2. Configure the MCP server in Cursor IDE `.cursor/mcp.json`: 50 | 51 | ##### Using env Command 52 | ```json 53 | { 54 | "mcpServers": { 55 | "salesforce": { 56 | "command": "env", 57 | "args": [ 58 | "SALESFORCE_USERNAME=your.actual.email@example.com", 59 | "SALESFORCE_PASSWORD=YourActualPassword123", 60 | "SALESFORCE_TOKEN=YourActualSecurityToken123", 61 | "SALESFORCE_INSTANCE_URL=https://login.salesforce.com", 62 | "npx", 63 | "-y", 64 | "@surajadsul02/mcp-server-salesforce" 65 | ] 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ##### For OAuth2 Authentication in Cursor 72 | ```json 73 | { 74 | "mcpServers": { 75 | "salesforce": { 76 | "command": "env", 77 | "args": [ 78 | "SALESFORCE_USERNAME=your.actual.email@example.com", 79 | "SALESFORCE_PASSWORD=YourActualPassword123", 80 | "SALESFORCE_TOKEN=YourActualSecurityToken123", 81 | "SALESFORCE_INSTANCE_URL=https://login.salesforce.com", 82 | "SALESFORCE_CONSUMER_KEY=YourConsumerKey", 83 | "SALESFORCE_CONSUMER_SECRET=YourConsumerSecret", 84 | "npx", 85 | "-y", 86 | "@surajadsul02/mcp-server-salesforce" 87 | ] 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | #### Claude Desktop Setup 94 | 95 | 1. Install the package globally (if not already installed): 96 | ```bash 97 | npm install -g @surajadsul02/mcp-server-salesforce 98 | ``` 99 | 100 | 2. Add to your `claude_desktop_config.json`: 101 | 102 | ##### For Username/Password Authentication 103 | ```json 104 | { 105 | "mcpServers": { 106 | "salesforce": { 107 | "command": "npx", 108 | "args": ["-y", "@surajadsul02/mcp-server-salesforce"], 109 | "env": { 110 | "SALESFORCE_USERNAME": "your_username", 111 | "SALESFORCE_PASSWORD": "your_password", 112 | "SALESFORCE_TOKEN": "your_security_token", 113 | "SALESFORCE_INSTANCE_URL": "https://login.salesforce.com" 114 | } 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | ##### For OAuth2 Authentication 121 | ```json 122 | { 123 | "mcpServers": { 124 | "salesforce": { 125 | "command": "npx", 126 | "args": ["-y", "@surajadsul02/mcp-server-salesforce"], 127 | "env": { 128 | "SALESFORCE_USERNAME": "your_username", 129 | "SALESFORCE_PASSWORD": "your_password", 130 | "SALESFORCE_CONSUMER_KEY": "your_consumer_key", 131 | "SALESFORCE_CONSUMER_SECRET": "your_consumer_secret", 132 | "SALESFORCE_INSTANCE_URL": "https://login.salesforce.com" 133 | } 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | 3. Configuration File Location: 140 | - macOS: `~/Library/Application Support/Claude Desktop/claude_desktop_config.json` 141 | - Windows: `%APPDATA%\Claude Desktop\claude_desktop_config.json` 142 | - Linux: `~/.config/Claude Desktop/claude_desktop_config.json` 143 | 144 | ### Required Environment Variables 145 | 146 | For Username/Password Authentication: 147 | - `SALESFORCE_USERNAME`: Your Salesforce username/email 148 | - `SALESFORCE_PASSWORD`: Your Salesforce password 149 | - `SALESFORCE_TOKEN`: Your Salesforce security token 150 | - `SALESFORCE_INSTANCE_URL`: Your Salesforce instance URL (Optional, default: https://login.salesforce.com) 151 | 152 | For OAuth2 Authentication: 153 | - `SALESFORCE_USERNAME`: Your Salesforce username/email 154 | - `SALESFORCE_PASSWORD`: Your Salesforce password 155 | - `SALESFORCE_CONSUMER_KEY`: Your Connected App's consumer key 156 | - `SALESFORCE_CONSUMER_SECRET`: Your Connected App's consumer secret 157 | - `SALESFORCE_INSTANCE_URL`: Your Salesforce instance URL (Optional, default: https://login.salesforce.com) 158 | 159 | ## Example Usage 160 | 161 | ### Searching Objects 162 | ``` 163 | "Find all objects related to Accounts" 164 | "Show me objects that handle customer service" 165 | "What objects are available for order management?" 166 | ``` 167 | 168 | ### Getting Schema Information 169 | ``` 170 | "What fields are available in the Account object?" 171 | "Show me the picklist values for Case Status" 172 | "Describe the relationship fields in Opportunity" 173 | ``` 174 | 175 | ### Querying Records 176 | ``` 177 | "Get all Accounts created this month" 178 | "Show me high-priority Cases with their related Contacts" 179 | "Find all Opportunities over $100k" 180 | ``` 181 | 182 | ### Managing Custom Objects 183 | ``` 184 | "Create a Customer Feedback object" 185 | "Add a Rating field to the Feedback object" 186 | "Update sharing settings for the Service Request object" 187 | ``` 188 | 189 | ### Searching Across Objects 190 | ``` 191 | "Search for 'cloud' in Accounts and Opportunities" 192 | "Find mentions of 'network issue' in Cases and Knowledge Articles" 193 | "Search for customer name across all relevant objects" 194 | ``` 195 | 196 | ## Development 197 | 198 | ### Building from source 199 | ```bash 200 | # Clone the repository 201 | git clone https://github.com/surajadsul02/mcp-server-salesforce.git 202 | 203 | # Navigate to directory 204 | cd mcp-server-salesforce 205 | 206 | # Install dependencies 207 | npm install 208 | 209 | # Build the project 210 | npm run build 211 | ``` 212 | 213 | ## Troubleshooting 214 | 215 | 1. **Authentication Errors** 216 | - Verify your credentials are correct 217 | - For username/password auth: ensure security token is correct 218 | - For OAuth2: verify consumer key and secret 219 | 220 | 2. **Connection Issues** 221 | - Check your Salesforce instance URL 222 | - Verify network connectivity 223 | - Ensure proper API access permissions 224 | 225 | 3. **Cursor IDE Integration** 226 | - Restart Cursor IDE after configuration changes 227 | - Check Developer Tools (Help > Toggle Developer Tools) for error messages 228 | - Verify the package is installed globally 229 | 230 | 4. **Claude Desktop Integration** 231 | - Verify configuration file location 232 | - Check file permissions 233 | - Restart Claude Desktop after configuration changes 234 | - Ensure environment variables are properly set 235 | 236 | ## Contributing 237 | Contributions are welcome! Feel free to submit a Pull Request. 238 | 239 | ## License 240 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 241 | 242 | ## Issues and Support 243 | If you encounter any issues or need support, please file an issue on the GitHub repository. -------------------------------------------------------------------------------- /claude_desktop_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "salesforce": { 4 | "command": "SALESFORCE_USERNAME=your.actual.email@example.com SALESFORCE_PASSWORD=YourActualPassword123 SALESFORCE_TOKEN=YourActualSecurityToken123 SALESFORCE_INSTANCE_URL=https://login.salesforce.com npx", 5 | "args": ["-y", "@surajadsul02/mcp-server-salesforce"], 6 | "env": { 7 | "SALESFORCE_USERNAME": "your.actual.email@example.com", 8 | "SALESFORCE_PASSWORD": "YourActualPassword123", 9 | "SALESFORCE_TOKEN": "YourActualSecurityToken123", 10 | "SALESFORCE_INSTANCE_URL": "https://login.salesforce.com", 11 | "NODE_ENV": "production" 12 | }, 13 | "cwd": "/Users/surajadsul/code/AI Projects/mcp-server-salesforce" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@surajadsul02/mcp-server-salesforce", 3 | "version": "0.0.2", 4 | "description": "A Salesforce connector MCP Server.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "bin": { 9 | "salesforce-connector": "dist/index.js" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "tsc && shx chmod +x dist/*.js", 16 | "prepare": "npm run build", 17 | "watch": "tsc --watch", 18 | "test:connection": "npm run build && node dist/test-connection.js" 19 | }, 20 | "keywords": [ 21 | "mcp", 22 | "salesforce", 23 | "claude", 24 | "ai" 25 | ], 26 | "author": "surajadsul02", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@modelcontextprotocol/sdk": "0.5.0", 30 | "dotenv": "^16.3.1", 31 | "jsforce": "^1.11.0" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^22.10.1", 35 | "typescript": "^5.7.2", 36 | "shx": "^0.3.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import * as dotenv from "dotenv"; 10 | 11 | import { createSalesforceConnection } from "./utils/connection.js"; 12 | import { SEARCH_OBJECTS, handleSearchObjects } from "./tools/search.js"; 13 | import { DESCRIBE_OBJECT, handleDescribeObject } from "./tools/describe.js"; 14 | import { QUERY_RECORDS, handleQueryRecords, QueryArgs } from "./tools/query.js"; 15 | import { DML_RECORDS, handleDMLRecords, DMLArgs } from "./tools/dml.js"; 16 | import { MANAGE_OBJECT, handleManageObject, ManageObjectArgs } from "./tools/manageObject.js"; 17 | import { MANAGE_FIELD, handleManageField, ManageFieldArgs } from "./tools/manageField.js"; 18 | import { SEARCH_ALL, handleSearchAll, SearchAllArgs, WithClause } from "./tools/searchAll.js"; 19 | 20 | dotenv.config(); 21 | 22 | const server = new Server( 23 | { 24 | name: "salesforce-mcp-server", 25 | version: "1.0.0", 26 | }, 27 | { 28 | capabilities: { 29 | tools: {}, 30 | }, 31 | }, 32 | ); 33 | 34 | // Tool handlers 35 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 36 | tools: [ 37 | SEARCH_OBJECTS, 38 | DESCRIBE_OBJECT, 39 | QUERY_RECORDS, 40 | DML_RECORDS, 41 | MANAGE_OBJECT, 42 | MANAGE_FIELD, 43 | SEARCH_ALL 44 | ], 45 | })); 46 | 47 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 48 | try { 49 | const { name, arguments: args } = request.params; 50 | if (!args) throw new Error('Arguments are required'); 51 | 52 | const conn = await createSalesforceConnection(); 53 | 54 | switch (name) { 55 | case "salesforce_search_objects": { 56 | const { searchPattern } = args as { searchPattern: string }; 57 | if (!searchPattern) throw new Error('searchPattern is required'); 58 | return await handleSearchObjects(conn, searchPattern); 59 | } 60 | 61 | case "salesforce_describe_object": { 62 | const { objectName } = args as { objectName: string }; 63 | if (!objectName) throw new Error('objectName is required'); 64 | return await handleDescribeObject(conn, objectName); 65 | } 66 | 67 | case "salesforce_query_records": { 68 | const queryArgs = args as Record; 69 | if (!queryArgs.objectName || !Array.isArray(queryArgs.fields)) { 70 | throw new Error('objectName and fields array are required for query'); 71 | } 72 | // Type check and conversion 73 | const validatedArgs: QueryArgs = { 74 | objectName: queryArgs.objectName as string, 75 | fields: queryArgs.fields as string[], 76 | whereClause: queryArgs.whereClause as string | undefined, 77 | orderBy: queryArgs.orderBy as string | undefined, 78 | limit: queryArgs.limit as number | undefined 79 | }; 80 | return await handleQueryRecords(conn, validatedArgs); 81 | } 82 | 83 | case "salesforce_dml_records": { 84 | const dmlArgs = args as Record; 85 | if (!dmlArgs.operation || !dmlArgs.objectName || !Array.isArray(dmlArgs.records)) { 86 | throw new Error('operation, objectName, and records array are required for DML'); 87 | } 88 | const validatedArgs: DMLArgs = { 89 | operation: dmlArgs.operation as 'insert' | 'update' | 'delete' | 'upsert', 90 | objectName: dmlArgs.objectName as string, 91 | records: dmlArgs.records as Record[], 92 | externalIdField: dmlArgs.externalIdField as string | undefined 93 | }; 94 | return await handleDMLRecords(conn, validatedArgs); 95 | } 96 | 97 | case "salesforce_manage_object": { 98 | const objectArgs = args as Record; 99 | if (!objectArgs.operation || !objectArgs.objectName) { 100 | throw new Error('operation and objectName are required for object management'); 101 | } 102 | const validatedArgs: ManageObjectArgs = { 103 | operation: objectArgs.operation as 'create' | 'update', 104 | objectName: objectArgs.objectName as string, 105 | label: objectArgs.label as string | undefined, 106 | pluralLabel: objectArgs.pluralLabel as string | undefined, 107 | description: objectArgs.description as string | undefined, 108 | nameFieldLabel: objectArgs.nameFieldLabel as string | undefined, 109 | nameFieldType: objectArgs.nameFieldType as 'Text' | 'AutoNumber' | undefined, 110 | nameFieldFormat: objectArgs.nameFieldFormat as string | undefined, 111 | sharingModel: objectArgs.sharingModel as 'ReadWrite' | 'Read' | 'Private' | 'ControlledByParent' | undefined 112 | }; 113 | return await handleManageObject(conn, validatedArgs); 114 | } 115 | 116 | case "salesforce_manage_field": { 117 | const fieldArgs = args as Record; 118 | if (!fieldArgs.operation || !fieldArgs.objectName || !fieldArgs.fieldName) { 119 | throw new Error('operation, objectName, and fieldName are required for field management'); 120 | } 121 | const validatedArgs: ManageFieldArgs = { 122 | operation: fieldArgs.operation as 'create' | 'update', 123 | objectName: fieldArgs.objectName as string, 124 | fieldName: fieldArgs.fieldName as string, 125 | label: fieldArgs.label as string | undefined, 126 | type: fieldArgs.type as string | undefined, 127 | required: fieldArgs.required as boolean | undefined, 128 | unique: fieldArgs.unique as boolean | undefined, 129 | externalId: fieldArgs.externalId as boolean | undefined, 130 | length: fieldArgs.length as number | undefined, 131 | precision: fieldArgs.precision as number | undefined, 132 | scale: fieldArgs.scale as number | undefined, 133 | referenceTo: fieldArgs.referenceTo as string | undefined, 134 | relationshipLabel: fieldArgs.relationshipLabel as string | undefined, 135 | relationshipName: fieldArgs.relationshipName as string | undefined, 136 | deleteConstraint: fieldArgs.deleteConstraint as 'Cascade' | 'Restrict' | 'SetNull' | undefined, 137 | picklistValues: fieldArgs.picklistValues as Array<{ label: string; isDefault?: boolean }> | undefined, 138 | description: fieldArgs.description as string | undefined 139 | }; 140 | return await handleManageField(conn, validatedArgs); 141 | } 142 | 143 | case "salesforce_search_all": { 144 | const searchArgs = args as Record; 145 | if (!searchArgs.searchTerm || !Array.isArray(searchArgs.objects)) { 146 | throw new Error('searchTerm and objects array are required for search'); 147 | } 148 | 149 | // Validate objects array 150 | const objects = searchArgs.objects as Array>; 151 | if (!objects.every(obj => obj.name && Array.isArray(obj.fields))) { 152 | throw new Error('Each object must specify name and fields array'); 153 | } 154 | 155 | // Type check and conversion 156 | const validatedArgs: SearchAllArgs = { 157 | searchTerm: searchArgs.searchTerm as string, 158 | searchIn: searchArgs.searchIn as "ALL FIELDS" | "NAME FIELDS" | "EMAIL FIELDS" | "PHONE FIELDS" | "SIDEBAR FIELDS" | undefined, 159 | objects: objects.map(obj => ({ 160 | name: obj.name as string, 161 | fields: obj.fields as string[], 162 | where: obj.where as string | undefined, 163 | orderBy: obj.orderBy as string | undefined, 164 | limit: obj.limit as number | undefined 165 | })), 166 | withClauses: searchArgs.withClauses as WithClause[] | undefined, 167 | updateable: searchArgs.updateable as boolean | undefined, 168 | viewable: searchArgs.viewable as boolean | undefined 169 | }; 170 | 171 | return await handleSearchAll(conn, validatedArgs); 172 | } 173 | 174 | default: 175 | return { 176 | content: [{ type: "text", text: `Unknown tool: ${name}` }], 177 | isError: true, 178 | }; 179 | } 180 | } catch (error) { 181 | return { 182 | content: [{ 183 | type: "text", 184 | text: `Error: ${error instanceof Error ? error.message : String(error)}`, 185 | }], 186 | isError: true, 187 | }; 188 | } 189 | }); 190 | 191 | async function runServer() { 192 | const transport = new StdioServerTransport(); 193 | await server.connect(transport); 194 | console.error("Salesforce MCP Server running on stdio"); 195 | } 196 | 197 | runServer().catch((error) => { 198 | console.error("Fatal error running server:", error); 199 | process.exit(1); 200 | }); -------------------------------------------------------------------------------- /src/test-connection.ts: -------------------------------------------------------------------------------- 1 | import { createSalesforceConnection } from './utils/connection.js'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | // Load environment variables from .env file 5 | dotenv.config(); 6 | 7 | async function testConnection() { 8 | try { 9 | console.log('Testing Salesforce connection...'); 10 | 11 | // Try to establish connection 12 | const conn = await createSalesforceConnection(); 13 | console.log('✅ Successfully connected to Salesforce!'); 14 | 15 | // Get some basic info about the connection 16 | console.log('\nConnection Details:'); 17 | console.log('Instance URL:', conn.instanceUrl); 18 | console.log('Access Token:', conn.accessToken ? '✅ Received' : '❌ Missing'); 19 | console.log('Auth Method:', process.env.SALESFORCE_CONSUMER_KEY ? 'OAuth2' : 'Username/Password'); 20 | 21 | // Try to query a simple object to verify API access 22 | console.log('\nTesting API access...'); 23 | const result = await conn.query('SELECT Id, Name FROM Account LIMIT 1'); 24 | console.log('✅ Successfully queried Account object!'); 25 | console.log(`Found ${result.totalSize} account(s)`); 26 | 27 | if (result.records.length > 0) { 28 | console.log('\nSample Account:'); 29 | console.log('ID:', result.records[0].Id); 30 | console.log('Name:', result.records[0].Name); 31 | } 32 | 33 | } catch (error) { 34 | console.error('\n❌ Connection test failed:'); 35 | console.error(error instanceof Error ? error.message : error); 36 | process.exit(1); 37 | } 38 | } 39 | 40 | testConnection(); -------------------------------------------------------------------------------- /src/tools/describe.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { SalesforceField, SalesforceDescribeResponse } from "../types/salesforce"; 3 | 4 | export const DESCRIBE_OBJECT: Tool = { 5 | name: "salesforce_describe_object", 6 | description: "Get detailed schema metadata including all fields, relationships, and field properties of any Salesforce object. Examples: 'Account' shows all Account fields including custom fields; 'Case' shows all Case fields including relationships to Account, Contact etc.", 7 | inputSchema: { 8 | type: "object", 9 | properties: { 10 | objectName: { 11 | type: "string", 12 | description: "API name of the object (e.g., 'Account', 'Contact', 'Custom_Object__c')" 13 | } 14 | }, 15 | required: ["objectName"] 16 | } 17 | }; 18 | 19 | export async function handleDescribeObject(conn: any, objectName: string) { 20 | const describe = await conn.describe(objectName) as SalesforceDescribeResponse; 21 | 22 | // Format the output 23 | const formattedDescription = ` 24 | Object: ${describe.name} (${describe.label})${describe.custom ? ' (Custom Object)' : ''} 25 | Fields: 26 | ${describe.fields.map((field: SalesforceField) => ` - ${field.name} (${field.label}) 27 | Type: ${field.type}${field.length ? `, Length: ${field.length}` : ''} 28 | Required: ${!field.nillable} 29 | ${field.referenceTo && field.referenceTo.length > 0 ? `References: ${field.referenceTo.join(', ')}` : ''} 30 | ${field.picklistValues && field.picklistValues.length > 0 ? `Picklist Values: ${field.picklistValues.map((v: { value: string }) => v.value).join(', ')}` : ''}` 31 | ).join('\n')}`; 32 | 33 | return { 34 | content: [{ 35 | type: "text", 36 | text: formattedDescription 37 | }], 38 | isError: false, 39 | }; 40 | } -------------------------------------------------------------------------------- /src/tools/dml.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { DMLResult } from "../types/salesforce"; 3 | 4 | export const DML_RECORDS: Tool = { 5 | name: "salesforce_dml_records", 6 | description: `Perform data manipulation operations on Salesforce records: 7 | - insert: Create new records 8 | - update: Modify existing records (requires Id) 9 | - delete: Remove records (requires Id) 10 | - upsert: Insert or update based on external ID field 11 | Examples: Insert new Accounts, Update Case status, Delete old records, Upsert based on custom external ID`, 12 | inputSchema: { 13 | type: "object", 14 | properties: { 15 | operation: { 16 | type: "string", 17 | enum: ["insert", "update", "delete", "upsert"], 18 | description: "Type of DML operation to perform" 19 | }, 20 | objectName: { 21 | type: "string", 22 | description: "API name of the object" 23 | }, 24 | records: { 25 | type: "array", 26 | items: { type: "object" }, 27 | description: "Array of records to process" 28 | }, 29 | externalIdField: { 30 | type: "string", 31 | description: "External ID field name for upsert operations", 32 | optional: true 33 | } 34 | }, 35 | required: ["operation", "objectName", "records"] 36 | } 37 | }; 38 | 39 | export interface DMLArgs { 40 | operation: 'insert' | 'update' | 'delete' | 'upsert'; 41 | objectName: string; 42 | records: Record[]; 43 | externalIdField?: string; 44 | } 45 | 46 | export async function handleDMLRecords(conn: any, args: DMLArgs) { 47 | const { operation, objectName, records, externalIdField } = args; 48 | 49 | let result: DMLResult | DMLResult[]; 50 | 51 | switch (operation) { 52 | case 'insert': 53 | result = await conn.sobject(objectName).create(records); 54 | break; 55 | case 'update': 56 | result = await conn.sobject(objectName).update(records); 57 | break; 58 | case 'delete': 59 | result = await conn.sobject(objectName).destroy(records.map(r => r.Id)); 60 | break; 61 | case 'upsert': 62 | if (!externalIdField) { 63 | throw new Error('externalIdField is required for upsert operations'); 64 | } 65 | result = await conn.sobject(objectName).upsert(records, externalIdField); 66 | break; 67 | default: 68 | throw new Error(`Unsupported operation: ${operation}`); 69 | } 70 | 71 | // Format DML results 72 | const results = Array.isArray(result) ? result : [result]; 73 | const successCount = results.filter(r => r.success).length; 74 | const failureCount = results.length - successCount; 75 | 76 | let responseText = `${operation.toUpperCase()} operation completed.\n`; 77 | responseText += `Processed ${results.length} records:\n`; 78 | responseText += `- Successful: ${successCount}\n`; 79 | responseText += `- Failed: ${failureCount}\n\n`; 80 | 81 | if (failureCount > 0) { 82 | responseText += 'Errors:\n'; 83 | results.forEach((r: DMLResult, idx: number) => { 84 | if (!r.success && r.errors) { 85 | responseText += `Record ${idx + 1}:\n`; 86 | if (Array.isArray(r.errors)) { 87 | r.errors.forEach((error) => { 88 | responseText += ` - ${error.message}`; 89 | if (error.statusCode) { 90 | responseText += ` [${error.statusCode}]`; 91 | } 92 | if (error.fields && error.fields.length > 0) { 93 | responseText += `\n Fields: ${error.fields.join(', ')}`; 94 | } 95 | responseText += '\n'; 96 | }); 97 | } else { 98 | // Single error object 99 | const error = r.errors; 100 | responseText += ` - ${error.message}`; 101 | if (error.statusCode) { 102 | responseText += ` [${error.statusCode}]`; 103 | } 104 | if (error.fields) { 105 | const fields = Array.isArray(error.fields) ? error.fields.join(', ') : error.fields; 106 | responseText += `\n Fields: ${fields}`; 107 | } 108 | responseText += '\n'; 109 | } 110 | } 111 | }); 112 | } 113 | 114 | return { 115 | content: [{ 116 | type: "text", 117 | text: responseText 118 | }], 119 | isError: false, 120 | }; 121 | } -------------------------------------------------------------------------------- /src/tools/manageField.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { FieldMetadataInfo } from "../types/metadata"; 3 | 4 | export const MANAGE_FIELD: Tool = { 5 | name: "salesforce_manage_field", 6 | description: `Create new custom fields or modify existing fields on any Salesforce object: 7 | - Field Types: Text, Number, Date, Lookup, Master-Detail, Picklist etc. 8 | - Properties: Required, Unique, External ID, Length, Scale etc. 9 | - Relationships: Create lookups and master-detail relationships 10 | Examples: Add Rating__c picklist to Account, Create Account lookup on Custom Object 11 | Note: Changes affect metadata and require proper permissions`, 12 | inputSchema: { 13 | type: "object", 14 | properties: { 15 | operation: { 16 | type: "string", 17 | enum: ["create", "update"], 18 | description: "Whether to create new field or update existing" 19 | }, 20 | objectName: { 21 | type: "string", 22 | description: "API name of the object to add/modify the field" 23 | }, 24 | fieldName: { 25 | type: "string", 26 | description: "API name for the field (without __c suffix)" 27 | }, 28 | label: { 29 | type: "string", 30 | description: "Label for the field", 31 | optional: true 32 | }, 33 | type: { 34 | type: "string", 35 | enum: ["Checkbox", "Currency", "Date", "DateTime", "Email", "Number", "Percent", 36 | "Phone", "Picklist", "MultiselectPicklist", "Text", "TextArea", "LongTextArea", 37 | "Html", "Url", "Lookup", "MasterDetail"], 38 | description: "Field type (required for create)", 39 | optional: true 40 | }, 41 | required: { 42 | type: "boolean", 43 | description: "Whether the field is required", 44 | optional: true 45 | }, 46 | unique: { 47 | type: "boolean", 48 | description: "Whether the field value must be unique", 49 | optional: true 50 | }, 51 | externalId: { 52 | type: "boolean", 53 | description: "Whether the field is an external ID", 54 | optional: true 55 | }, 56 | length: { 57 | type: "number", 58 | description: "Length for text fields", 59 | optional: true 60 | }, 61 | precision: { 62 | type: "number", 63 | description: "Precision for numeric fields", 64 | optional: true 65 | }, 66 | scale: { 67 | type: "number", 68 | description: "Scale for numeric fields", 69 | optional: true 70 | }, 71 | referenceTo: { 72 | type: "string", 73 | description: "API name of the object to reference (for Lookup/MasterDetail)", 74 | optional: true 75 | }, 76 | relationshipLabel: { 77 | type: "string", 78 | description: "Label for the relationship (for Lookup/MasterDetail)", 79 | optional: true 80 | }, 81 | relationshipName: { 82 | type: "string", 83 | description: "API name for the relationship (for Lookup/MasterDetail)", 84 | optional: true 85 | }, 86 | deleteConstraint: { 87 | type: "string", 88 | enum: ["Cascade", "Restrict", "SetNull"], 89 | description: "Delete constraint for Lookup fields", 90 | optional: true 91 | }, 92 | picklistValues: { 93 | type: "array", 94 | items: { 95 | type: "object", 96 | properties: { 97 | label: { type: "string" }, 98 | isDefault: { type: "boolean", optional: true } 99 | } 100 | }, 101 | description: "Values for Picklist/MultiselectPicklist fields", 102 | optional: true 103 | }, 104 | description: { 105 | type: "string", 106 | description: "Description of the field", 107 | optional: true 108 | } 109 | }, 110 | required: ["operation", "objectName", "fieldName"] 111 | } 112 | }; 113 | 114 | export interface ManageFieldArgs { 115 | operation: 'create' | 'update'; 116 | objectName: string; 117 | fieldName: string; 118 | label?: string; 119 | type?: string; 120 | required?: boolean; 121 | unique?: boolean; 122 | externalId?: boolean; 123 | length?: number; 124 | precision?: number; 125 | scale?: number; 126 | referenceTo?: string; 127 | relationshipLabel?: string; 128 | relationshipName?: string; 129 | deleteConstraint?: 'Cascade' | 'Restrict' | 'SetNull'; 130 | picklistValues?: Array<{ label: string; isDefault?: boolean }>; 131 | description?: string; 132 | } 133 | 134 | export async function handleManageField(conn: any, args: ManageFieldArgs) { 135 | const { operation, objectName, fieldName, type, ...fieldProps } = args; 136 | 137 | try { 138 | if (operation === 'create') { 139 | if (!type) { 140 | throw new Error('Field type is required for field creation'); 141 | } 142 | 143 | // Prepare base metadata for the new field 144 | const metadata: FieldMetadataInfo = { 145 | fullName: `${objectName}.${fieldName}__c`, 146 | label: fieldProps.label || fieldName, 147 | type, 148 | ...(fieldProps.required && { required: fieldProps.required }), 149 | ...(fieldProps.unique && { unique: fieldProps.unique }), 150 | ...(fieldProps.externalId && { externalId: fieldProps.externalId }), 151 | ...(fieldProps.description && { description: fieldProps.description }) 152 | }; 153 | 154 | // Add type-specific properties 155 | switch (type) { 156 | case 'MasterDetail': 157 | case 'Lookup': 158 | if (fieldProps.referenceTo) { 159 | metadata.referenceTo = fieldProps.referenceTo; 160 | metadata.relationshipName = fieldProps.relationshipName; 161 | metadata.relationshipLabel = fieldProps.relationshipLabel || fieldProps.relationshipName; 162 | if (type === 'Lookup' && fieldProps.deleteConstraint) { 163 | metadata.deleteConstraint = fieldProps.deleteConstraint; 164 | } 165 | } 166 | break; 167 | 168 | case 'TextArea': 169 | metadata.type = 'LongTextArea'; 170 | metadata.length = fieldProps.length || 32768; 171 | metadata.visibleLines = 3; 172 | break; 173 | 174 | case 'Text': 175 | if (fieldProps.length) { 176 | metadata.length = fieldProps.length; 177 | } 178 | break; 179 | 180 | case 'Number': 181 | if (fieldProps.precision) { 182 | metadata.precision = fieldProps.precision; 183 | metadata.scale = fieldProps.scale || 0; 184 | } 185 | break; 186 | 187 | case 'Picklist': 188 | case 'MultiselectPicklist': 189 | if (fieldProps.picklistValues) { 190 | metadata.valueSet = { 191 | valueSetDefinition: { 192 | sorted: true, 193 | value: fieldProps.picklistValues.map(val => ({ 194 | fullName: val.label, 195 | default: val.isDefault || false, 196 | label: val.label 197 | })) 198 | } 199 | }; 200 | } 201 | break; 202 | } 203 | 204 | // Create the field 205 | const result = await conn.metadata.create('CustomField', metadata); 206 | 207 | if (result && (Array.isArray(result) ? result[0].success : result.success)) { 208 | return { 209 | content: [{ 210 | type: "text", 211 | text: `Successfully created custom field ${fieldName}__c on ${objectName}` 212 | }], 213 | isError: false, 214 | }; 215 | } 216 | } else { 217 | // For update, first get existing metadata 218 | const existingMetadata = await conn.metadata.read('CustomField', [`${objectName}.${fieldName}__c`]); 219 | const currentMetadata = Array.isArray(existingMetadata) ? existingMetadata[0] : existingMetadata; 220 | 221 | if (!currentMetadata) { 222 | throw new Error(`Field ${fieldName}__c not found on object ${objectName}`); 223 | } 224 | 225 | // Prepare update metadata 226 | const metadata: FieldMetadataInfo = { 227 | ...currentMetadata, 228 | ...(fieldProps.label && { label: fieldProps.label }), 229 | ...(fieldProps.required !== undefined && { required: fieldProps.required }), 230 | ...(fieldProps.unique !== undefined && { unique: fieldProps.unique }), 231 | ...(fieldProps.externalId !== undefined && { externalId: fieldProps.externalId }), 232 | ...(fieldProps.description !== undefined && { description: fieldProps.description }), 233 | ...(fieldProps.length && { length: fieldProps.length }), 234 | ...(fieldProps.precision && { precision: fieldProps.precision, scale: fieldProps.scale || 0 }) 235 | }; 236 | 237 | // Special handling for picklist values if provided 238 | if (fieldProps.picklistValues && 239 | (currentMetadata.type === 'Picklist' || currentMetadata.type === 'MultiselectPicklist')) { 240 | metadata.valueSet = { 241 | valueSetDefinition: { 242 | sorted: true, 243 | value: fieldProps.picklistValues.map(val => ({ 244 | fullName: val.label, 245 | default: val.isDefault || false, 246 | label: val.label 247 | })) 248 | } 249 | }; 250 | } 251 | 252 | // Update the field 253 | const result = await conn.metadata.update('CustomField', metadata); 254 | 255 | if (result && (Array.isArray(result) ? result[0].success : result.success)) { 256 | return { 257 | content: [{ 258 | type: "text", 259 | text: `Successfully updated custom field ${fieldName}__c on ${objectName}` 260 | }], 261 | isError: false, 262 | }; 263 | } 264 | } 265 | 266 | return { 267 | content: [{ 268 | type: "text", 269 | text: `Failed to ${operation} custom field ${fieldName}__c` 270 | }], 271 | isError: true, 272 | }; 273 | 274 | } catch (error) { 275 | return { 276 | content: [{ 277 | type: "text", 278 | text: `Error ${operation === 'create' ? 'creating' : 'updating'} custom field: ${error instanceof Error ? error.message : String(error)}` 279 | }], 280 | isError: true, 281 | }; 282 | } 283 | } -------------------------------------------------------------------------------- /src/tools/manageObject.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { MetadataInfo } from "../types/metadata"; 3 | 4 | export const MANAGE_OBJECT: Tool = { 5 | name: "salesforce_manage_object", 6 | description: `Create new custom objects or modify existing ones in Salesforce: 7 | - Create: New custom objects with fields, relationships, and settings 8 | - Update: Modify existing object settings, labels, sharing model 9 | Examples: Create Customer_Feedback__c object, Update object sharing settings 10 | Note: Changes affect metadata and require proper permissions`, 11 | inputSchema: { 12 | type: "object", 13 | properties: { 14 | operation: { 15 | type: "string", 16 | enum: ["create", "update"], 17 | description: "Whether to create new object or update existing" 18 | }, 19 | objectName: { 20 | type: "string", 21 | description: "API name for the object (without __c suffix)" 22 | }, 23 | label: { 24 | type: "string", 25 | description: "Label for the object" 26 | }, 27 | pluralLabel: { 28 | type: "string", 29 | description: "Plural label for the object" 30 | }, 31 | description: { 32 | type: "string", 33 | description: "Description of the object", 34 | optional: true 35 | }, 36 | nameFieldLabel: { 37 | type: "string", 38 | description: "Label for the name field", 39 | optional: true 40 | }, 41 | nameFieldType: { 42 | type: "string", 43 | enum: ["Text", "AutoNumber"], 44 | description: "Type of the name field", 45 | optional: true 46 | }, 47 | nameFieldFormat: { 48 | type: "string", 49 | description: "Display format for AutoNumber field (e.g., 'A-{0000}')", 50 | optional: true 51 | }, 52 | sharingModel: { 53 | type: "string", 54 | enum: ["ReadWrite", "Read", "Private", "ControlledByParent"], 55 | description: "Sharing model for the object", 56 | optional: true 57 | } 58 | }, 59 | required: ["operation", "objectName"] 60 | } 61 | }; 62 | 63 | export interface ManageObjectArgs { 64 | operation: 'create' | 'update'; 65 | objectName: string; 66 | label?: string; 67 | pluralLabel?: string; 68 | description?: string; 69 | nameFieldLabel?: string; 70 | nameFieldType?: 'Text' | 'AutoNumber'; 71 | nameFieldFormat?: string; 72 | sharingModel?: 'ReadWrite' | 'Read' | 'Private' | 'ControlledByParent'; 73 | } 74 | 75 | export async function handleManageObject(conn: any, args: ManageObjectArgs) { 76 | const { operation, objectName, label, pluralLabel, description, nameFieldLabel, nameFieldType, nameFieldFormat, sharingModel } = args; 77 | 78 | try { 79 | if (operation === 'create') { 80 | if (!label || !pluralLabel) { 81 | throw new Error('Label and pluralLabel are required for object creation'); 82 | } 83 | 84 | // Prepare metadata for the new object 85 | const metadata = { 86 | fullName: `${objectName}__c`, 87 | label, 88 | pluralLabel, 89 | nameField: { 90 | label: nameFieldLabel || `${label} Name`, 91 | type: nameFieldType || 'Text', 92 | ...(nameFieldType === 'AutoNumber' && nameFieldFormat ? { displayFormat: nameFieldFormat } : {}) 93 | }, 94 | deploymentStatus: 'Deployed', 95 | sharingModel: sharingModel || 'ReadWrite' 96 | } as MetadataInfo; 97 | 98 | if (description) { 99 | metadata.description = description; 100 | } 101 | 102 | // Create the object using Metadata API 103 | const result = await conn.metadata.create('CustomObject', metadata); 104 | 105 | if (result && (Array.isArray(result) ? result[0].success : result.success)) { 106 | return { 107 | content: [{ 108 | type: "text", 109 | text: `Successfully created custom object ${objectName}__c` 110 | }], 111 | isError: false, 112 | }; 113 | } 114 | } else { 115 | // For update, first get existing metadata 116 | const existingMetadata = await conn.metadata.read('CustomObject', [`${objectName}__c`]); 117 | const currentMetadata = Array.isArray(existingMetadata) ? existingMetadata[0] : existingMetadata; 118 | 119 | if (!currentMetadata) { 120 | throw new Error(`Object ${objectName}__c not found`); 121 | } 122 | 123 | // Prepare update metadata 124 | const metadata = { 125 | ...currentMetadata, 126 | label: label || currentMetadata.label, 127 | pluralLabel: pluralLabel || currentMetadata.pluralLabel, 128 | description: description !== undefined ? description : currentMetadata.description, 129 | sharingModel: sharingModel || currentMetadata.sharingModel 130 | } as MetadataInfo; 131 | 132 | // Update the object using Metadata API 133 | const result = await conn.metadata.update('CustomObject', metadata); 134 | 135 | if (result && (Array.isArray(result) ? result[0].success : result.success)) { 136 | return { 137 | content: [{ 138 | type: "text", 139 | text: `Successfully updated custom object ${objectName}__c` 140 | }], 141 | isError: false, 142 | }; 143 | } 144 | } 145 | 146 | return { 147 | content: [{ 148 | type: "text", 149 | text: `Failed to ${operation} custom object ${objectName}__c` 150 | }], 151 | isError: true, 152 | }; 153 | 154 | } catch (error) { 155 | return { 156 | content: [{ 157 | type: "text", 158 | text: `Error ${operation === 'create' ? 'creating' : 'updating'} custom object: ${error instanceof Error ? error.message : String(error)}` 159 | }], 160 | isError: true, 161 | }; 162 | } 163 | } -------------------------------------------------------------------------------- /src/tools/query.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | export const QUERY_RECORDS: Tool = { 4 | name: "salesforce_query_records", 5 | description: `Query records from any Salesforce object using SOQL, including relationship queries. 6 | 7 | Examples: 8 | 1. Parent-to-child query (e.g., Account with Contacts): 9 | - objectName: "Account" 10 | - fields: ["Name", "(SELECT Id, FirstName, LastName FROM Contacts)"] 11 | 12 | 2. Child-to-parent query (e.g., Contact with Account details): 13 | - objectName: "Contact" 14 | - fields: ["FirstName", "LastName", "Account.Name", "Account.Industry"] 15 | 16 | 3. Multiple level query (e.g., Contact -> Account -> Owner): 17 | - objectName: "Contact" 18 | - fields: ["Name", "Account.Name", "Account.Owner.Name"] 19 | 20 | 4. Related object filtering: 21 | - objectName: "Contact" 22 | - fields: ["Name", "Account.Name"] 23 | - whereClause: "Account.Industry = 'Technology'" 24 | 25 | Note: When using relationship fields: 26 | - Use dot notation for parent relationships (e.g., "Account.Name") 27 | - Use subqueries in parentheses for child relationships (e.g., "(SELECT Id FROM Contacts)") 28 | - Custom relationship fields end in "__r" (e.g., "CustomObject__r.Name")`, 29 | inputSchema: { 30 | type: "object", 31 | properties: { 32 | objectName: { 33 | type: "string", 34 | description: "API name of the object to query" 35 | }, 36 | fields: { 37 | type: "array", 38 | items: { type: "string" }, 39 | description: "List of fields to retrieve, including relationship fields" 40 | }, 41 | whereClause: { 42 | type: "string", 43 | description: "WHERE clause, can include conditions on related objects", 44 | optional: true 45 | }, 46 | orderBy: { 47 | type: "string", 48 | description: "ORDER BY clause, can include fields from related objects", 49 | optional: true 50 | }, 51 | limit: { 52 | type: "number", 53 | description: "Maximum number of records to return", 54 | optional: true 55 | } 56 | }, 57 | required: ["objectName", "fields"] 58 | } 59 | }; 60 | 61 | export interface QueryArgs { 62 | objectName: string; 63 | fields: string[]; 64 | whereClause?: string; 65 | orderBy?: string; 66 | limit?: number; 67 | } 68 | 69 | // Helper function to validate relationship field syntax 70 | function validateRelationshipFields(fields: string[]): { isValid: boolean; error?: string } { 71 | for (const field of fields) { 72 | // Check for parent relationship syntax (dot notation) 73 | if (field.includes('.')) { 74 | const parts = field.split('.'); 75 | // Check for empty parts 76 | if (parts.some(part => !part)) { 77 | return { 78 | isValid: false, 79 | error: `Invalid relationship field format: "${field}". Relationship fields should use proper dot notation (e.g., "Account.Name")` 80 | }; 81 | } 82 | // Check for too many levels (Salesforce typically limits to 5) 83 | if (parts.length > 5) { 84 | return { 85 | isValid: false, 86 | error: `Relationship field "${field}" exceeds maximum depth of 5 levels` 87 | }; 88 | } 89 | } 90 | 91 | // Check for child relationship syntax (subqueries) 92 | if (field.includes('SELECT') && !field.match(/^\(SELECT.*FROM.*\)$/)) { 93 | return { 94 | isValid: false, 95 | error: `Invalid subquery format: "${field}". Child relationship queries should be wrapped in parentheses` 96 | }; 97 | } 98 | } 99 | 100 | return { isValid: true }; 101 | } 102 | 103 | // Helper function to format relationship query results 104 | function formatRelationshipResults(record: any, field: string, prefix = ''): string { 105 | if (field.includes('.')) { 106 | const [relationship, ...rest] = field.split('.'); 107 | const relatedRecord = record[relationship]; 108 | if (relatedRecord === null) { 109 | return `${prefix}${field}: null`; 110 | } 111 | return formatRelationshipResults(relatedRecord, rest.join('.'), `${prefix}${relationship}.`); 112 | } 113 | 114 | const value = record[field]; 115 | if (Array.isArray(value)) { 116 | // Handle child relationship arrays 117 | return `${prefix}${field}: [${value.length} records]`; 118 | } 119 | return `${prefix}${field}: ${value !== null && value !== undefined ? value : 'null'}`; 120 | } 121 | 122 | export async function handleQueryRecords(conn: any, args: QueryArgs) { 123 | const { objectName, fields, whereClause, orderBy, limit } = args; 124 | 125 | try { 126 | // Validate relationship field syntax 127 | const validation = validateRelationshipFields(fields); 128 | if (!validation.isValid) { 129 | return { 130 | content: [{ 131 | type: "text", 132 | text: validation.error! 133 | }], 134 | isError: true, 135 | }; 136 | } 137 | 138 | // Construct SOQL query 139 | let soql = `SELECT ${fields.join(', ')} FROM ${objectName}`; 140 | if (whereClause) soql += ` WHERE ${whereClause}`; 141 | if (orderBy) soql += ` ORDER BY ${orderBy}`; 142 | if (limit) soql += ` LIMIT ${limit}`; 143 | 144 | const result = await conn.query(soql); 145 | 146 | // Format the output 147 | const formattedRecords = result.records.map((record: any, index: number) => { 148 | const recordStr = fields.map(field => { 149 | // Handle special case for subqueries (child relationships) 150 | if (field.startsWith('(SELECT')) { 151 | const relationshipName = field.match(/FROM\s+(\w+)/)?.[1]; 152 | if (!relationshipName) return ` ${field}: Invalid subquery format`; 153 | const childRecords = record[relationshipName]; 154 | return ` ${relationshipName}: [${childRecords?.length || 0} records]`; 155 | } 156 | return ' ' + formatRelationshipResults(record, field); 157 | }).join('\n'); 158 | return `Record ${index + 1}:\n${recordStr}`; 159 | }).join('\n\n'); 160 | 161 | return { 162 | content: [{ 163 | type: "text", 164 | text: `Query returned ${result.records.length} records:\n\n${formattedRecords}` 165 | }], 166 | isError: false, 167 | }; 168 | } catch (error) { 169 | // Enhanced error handling for relationship queries 170 | const errorMessage = error instanceof Error ? error.message : String(error); 171 | let enhancedError = errorMessage; 172 | 173 | if (errorMessage.includes('INVALID_FIELD')) { 174 | // Try to identify which relationship field caused the error 175 | const fieldMatch = errorMessage.match(/(?:No such column |Invalid field: )['"]?([^'")\s]+)/); 176 | if (fieldMatch) { 177 | const invalidField = fieldMatch[1]; 178 | if (invalidField.includes('.')) { 179 | enhancedError = `Invalid relationship field "${invalidField}". Please check:\n` + 180 | `1. The relationship name is correct\n` + 181 | `2. The field exists on the related object\n` + 182 | `3. You have access to the field\n` + 183 | `4. For custom relationships, ensure you're using '__r' suffix`; 184 | } 185 | } 186 | } 187 | 188 | return { 189 | content: [{ 190 | type: "text", 191 | text: `Error executing query: ${enhancedError}` 192 | }], 193 | isError: true, 194 | }; 195 | } 196 | } -------------------------------------------------------------------------------- /src/tools/search.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { SalesforceObject } from "../types/salesforce"; 3 | 4 | export const SEARCH_OBJECTS: Tool = { 5 | name: "salesforce_search_objects", 6 | description: "Search for Salesforce standard and custom objects by name pattern. Examples: 'Account' will find Account, AccountHistory; 'Order' will find WorkOrder, ServiceOrder__c etc.", 7 | inputSchema: { 8 | type: "object", 9 | properties: { 10 | searchPattern: { 11 | type: "string", 12 | description: "Search pattern to find objects (e.g., 'Account Coverage' will find objects like 'AccountCoverage__c')" 13 | } 14 | }, 15 | required: ["searchPattern"] 16 | } 17 | }; 18 | 19 | export async function handleSearchObjects(conn: any, searchPattern: string) { 20 | // Get list of all objects 21 | const describeGlobal = await conn.describeGlobal(); 22 | 23 | // Process search pattern to create a more flexible search 24 | const searchTerms = searchPattern.toLowerCase().split(' ').filter(term => term.length > 0); 25 | 26 | // Filter objects based on search pattern 27 | const matchingObjects = describeGlobal.sobjects.filter((obj: SalesforceObject) => { 28 | const objectName = obj.name.toLowerCase(); 29 | const objectLabel = obj.label.toLowerCase(); 30 | 31 | // Check if all search terms are present in either the API name or label 32 | return searchTerms.every(term => 33 | objectName.includes(term) || objectLabel.includes(term) 34 | ); 35 | }); 36 | 37 | if (matchingObjects.length === 0) { 38 | return { 39 | content: [{ 40 | type: "text", 41 | text: `No Salesforce objects found matching "${searchPattern}".` 42 | }], 43 | isError: false, 44 | }; 45 | } 46 | 47 | // Format the output 48 | const formattedResults = matchingObjects.map((obj: SalesforceObject) => 49 | `${obj.name}${obj.custom ? ' (Custom)' : ''}\n Label: ${obj.label}` 50 | ).join('\n\n'); 51 | 52 | return { 53 | content: [{ 54 | type: "text", 55 | text: `Found ${matchingObjects.length} matching objects:\n\n${formattedResults}` 56 | }], 57 | isError: false, 58 | }; 59 | } -------------------------------------------------------------------------------- /src/tools/searchAll.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | export const SEARCH_ALL: Tool = { 4 | name: "salesforce_search_all", 5 | description: `Search across multiple Salesforce objects using SOSL (Salesforce Object Search Language). 6 | 7 | Examples: 8 | 1. Basic search across all objects: 9 | { 10 | "searchTerm": "John", 11 | "objects": [ 12 | { "name": "Account", "fields": ["Name"], "limit": 10 }, 13 | { "name": "Contact", "fields": ["FirstName", "LastName", "Email"] } 14 | ] 15 | } 16 | 17 | 2. Advanced search with filters: 18 | { 19 | "searchTerm": "Cloud*", 20 | "searchIn": "NAME FIELDS", 21 | "objects": [ 22 | { 23 | "name": "Account", 24 | "fields": ["Name", "Industry"], 25 | "orderBy": "Name DESC", 26 | "where": "Industry = 'Technology'" 27 | } 28 | ], 29 | "withClauses": [ 30 | { "type": "NETWORK", "value": "ALL NETWORKS" }, 31 | { "type": "SNIPPET", "fields": ["Description"] } 32 | ] 33 | } 34 | 35 | Notes: 36 | - Use * and ? for wildcards in search terms 37 | - Each object can have its own WHERE, ORDER BY, and LIMIT clauses 38 | - Support for WITH clauses: DATA CATEGORY, DIVISION, METADATA, NETWORK, PRICEBOOKID, SNIPPET, SECURITY_ENFORCED 39 | - "updateable" and "viewable" options control record access filtering`, 40 | inputSchema: { 41 | type: "object", 42 | properties: { 43 | searchTerm: { 44 | type: "string", 45 | description: "Text to search for (supports wildcards * and ?)" 46 | }, 47 | searchIn: { 48 | type: "string", 49 | enum: ["ALL FIELDS", "NAME FIELDS", "EMAIL FIELDS", "PHONE FIELDS", "SIDEBAR FIELDS"], 50 | description: "Which fields to search in", 51 | optional: true 52 | }, 53 | objects: { 54 | type: "array", 55 | items: { 56 | type: "object", 57 | properties: { 58 | name: { 59 | type: "string", 60 | description: "API name of the object" 61 | }, 62 | fields: { 63 | type: "array", 64 | items: { type: "string" }, 65 | description: "Fields to return for this object" 66 | }, 67 | where: { 68 | type: "string", 69 | description: "WHERE clause for this object", 70 | optional: true 71 | }, 72 | orderBy: { 73 | type: "string", 74 | description: "ORDER BY clause for this object", 75 | optional: true 76 | }, 77 | limit: { 78 | type: "number", 79 | description: "Maximum number of records to return for this object", 80 | optional: true 81 | } 82 | }, 83 | required: ["name", "fields"] 84 | }, 85 | description: "List of objects to search and their return fields" 86 | }, 87 | withClauses: { 88 | type: "array", 89 | items: { 90 | type: "object", 91 | properties: { 92 | type: { 93 | type: "string", 94 | enum: ["DATA CATEGORY", "DIVISION", "METADATA", "NETWORK", 95 | "PRICEBOOKID", "SNIPPET", "SECURITY_ENFORCED"] 96 | }, 97 | value: { 98 | type: "string", 99 | description: "Value for the WITH clause", 100 | optional: true 101 | }, 102 | fields: { 103 | type: "array", 104 | items: { type: "string" }, 105 | description: "Fields for SNIPPET clause", 106 | optional: true 107 | } 108 | }, 109 | required: ["type"] 110 | }, 111 | description: "Additional WITH clauses for the search", 112 | optional: true 113 | }, 114 | updateable: { 115 | type: "boolean", 116 | description: "Return only updateable records", 117 | optional: true 118 | }, 119 | viewable: { 120 | type: "boolean", 121 | description: "Return only viewable records", 122 | optional: true 123 | } 124 | }, 125 | required: ["searchTerm", "objects"] 126 | } 127 | }; 128 | 129 | export interface SearchObject { 130 | name: string; 131 | fields: string[]; 132 | where?: string; 133 | orderBy?: string; 134 | limit?: number; 135 | } 136 | 137 | export interface WithClause { 138 | type: "DATA CATEGORY" | "DIVISION" | "METADATA" | "NETWORK" | 139 | "PRICEBOOKID" | "SNIPPET" | "SECURITY_ENFORCED"; 140 | value?: string; 141 | fields?: string[]; 142 | } 143 | 144 | export interface SearchAllArgs { 145 | searchTerm: string; 146 | searchIn?: "ALL FIELDS" | "NAME FIELDS" | "EMAIL FIELDS" | "PHONE FIELDS" | "SIDEBAR FIELDS"; 147 | objects: SearchObject[]; 148 | withClauses?: WithClause[]; 149 | updateable?: boolean; 150 | viewable?: boolean; 151 | } 152 | 153 | function buildWithClause(withClause: WithClause): string { 154 | switch (withClause.type) { 155 | case "SNIPPET": 156 | return `WITH SNIPPET (${withClause.fields?.join(', ')})`; 157 | case "DATA CATEGORY": 158 | case "DIVISION": 159 | case "NETWORK": 160 | case "PRICEBOOKID": 161 | return `WITH ${withClause.type} = ${withClause.value}`; 162 | case "METADATA": 163 | case "SECURITY_ENFORCED": 164 | return `WITH ${withClause.type}`; 165 | default: 166 | return ''; 167 | } 168 | } 169 | 170 | export async function handleSearchAll(conn: any, args: SearchAllArgs) { 171 | const { searchTerm, searchIn = "ALL FIELDS", objects, withClauses, updateable, viewable } = args; 172 | 173 | try { 174 | // Validate the search term 175 | if (!searchTerm.trim()) { 176 | throw new Error('Search term cannot be empty'); 177 | } 178 | 179 | // Construct the RETURNING clause with object-specific clauses 180 | const returningClause = objects 181 | .map(obj => { 182 | let clause = `${obj.name}(${obj.fields.join(',')}` 183 | 184 | // Add object-specific clauses if present 185 | if (obj.where) clause += ` WHERE ${obj.where}`; 186 | if (obj.orderBy) clause += ` ORDER BY ${obj.orderBy}`; 187 | if (obj.limit) clause += ` LIMIT ${obj.limit}`; 188 | 189 | return clause + ')'; 190 | }) 191 | .join(', '); 192 | 193 | // Build WITH clauses if present 194 | const withClausesStr = withClauses 195 | ? withClauses.map(buildWithClause).join(' ') 196 | : ''; 197 | 198 | // Add updateable/viewable flags if specified 199 | const accessFlags = []; 200 | if (updateable) accessFlags.push('UPDATEABLE'); 201 | if (viewable) accessFlags.push('VIEWABLE'); 202 | const accessClause = accessFlags.length > 0 ? 203 | ` RETURNING ${accessFlags.join(',')}` : ''; 204 | 205 | // Construct complete SOSL query 206 | const soslQuery = `FIND {${searchTerm}} IN ${searchIn} 207 | ${withClausesStr} 208 | RETURNING ${returningClause} 209 | ${accessClause}`.trim(); 210 | 211 | // Execute search 212 | const result = await conn.search(soslQuery); 213 | 214 | // Format results by object 215 | let formattedResults = ''; 216 | objects.forEach((obj, index) => { 217 | const objectResults = result.searchRecords.filter((record: any) => 218 | record.attributes.type === obj.name 219 | ); 220 | 221 | formattedResults += `\n${obj.name} (${objectResults.length} records found):\n`; 222 | 223 | if (objectResults.length > 0) { 224 | objectResults.forEach((record: any, recordIndex: number) => { 225 | formattedResults += ` Record ${recordIndex + 1}:\n`; 226 | obj.fields.forEach(field => { 227 | const value = record[field]; 228 | formattedResults += ` ${field}: ${value !== null && value !== undefined ? value : 'null'}\n`; 229 | }); 230 | // Add metadata or snippet info if requested 231 | if (withClauses?.some(w => w.type === "METADATA")) { 232 | formattedResults += ` Metadata:\n Last Modified: ${record.attributes.lastModifiedDate}\n`; 233 | } 234 | if (withClauses?.some(w => w.type === "SNIPPET")) { 235 | formattedResults += ` Snippets:\n${record.snippets?.map((s: any) => 236 | ` ${s.field}: ${s.snippet}`).join('\n') || ' None'}\n`; 237 | } 238 | }); 239 | } 240 | 241 | if (index < objects.length - 1) { 242 | formattedResults += '\n'; 243 | } 244 | }); 245 | 246 | return { 247 | content: [{ 248 | type: "text", 249 | text: `Search Results:${formattedResults}` 250 | }], 251 | isError: false, 252 | }; 253 | } catch (error) { 254 | // Enhanced error handling for SOSL queries 255 | const errorMessage = error instanceof Error ? error.message : String(error); 256 | let enhancedError = errorMessage; 257 | 258 | if (errorMessage.includes('MALFORMED_SEARCH')) { 259 | enhancedError = `Invalid search query format. Common issues:\n` + 260 | `1. Search term contains invalid characters\n` + 261 | `2. Object or field names are incorrect\n` + 262 | `3. Missing required SOSL syntax elements\n` + 263 | `4. Invalid WITH clause combination\n\n` + 264 | `Original error: ${errorMessage}`; 265 | } else if (errorMessage.includes('INVALID_FIELD')) { 266 | enhancedError = `Invalid field specified in RETURNING clause. Please check:\n` + 267 | `1. Field names are correct\n` + 268 | `2. Fields exist on the specified objects\n` + 269 | `3. You have access to all specified fields\n` + 270 | `4. WITH SNIPPET fields are valid\n\n` + 271 | `Original error: ${errorMessage}`; 272 | } else if (errorMessage.includes('WITH_CLAUSE')) { 273 | enhancedError = `Error in WITH clause. Please check:\n` + 274 | `1. WITH clause type is supported\n` + 275 | `2. WITH clause value is valid\n` + 276 | `3. You have permission to use the specified WITH clause\n\n` + 277 | `Original error: ${errorMessage}`; 278 | } 279 | 280 | return { 281 | content: [{ 282 | type: "text", 283 | text: `Error executing search: ${enhancedError}` 284 | }], 285 | isError: true, 286 | }; 287 | } 288 | } -------------------------------------------------------------------------------- /src/types/metadata.ts: -------------------------------------------------------------------------------- 1 | export interface MetadataInfo { 2 | fullName: string; 3 | label: string; 4 | pluralLabel?: string; 5 | nameField?: { 6 | type: string; 7 | label: string; 8 | displayFormat?: string; 9 | }; 10 | deploymentStatus?: 'Deployed' | 'InDevelopment'; 11 | sharingModel?: 'ReadWrite' | 'Read' | 'Private' | 'ControlledByParent'; 12 | enableActivities?: boolean; 13 | description?: string; 14 | } 15 | 16 | export interface ValueSetDefinition { 17 | sorted?: boolean; 18 | value: Array<{ 19 | fullName: string; 20 | default?: boolean; 21 | label: string; 22 | }>; 23 | } 24 | 25 | export interface FieldMetadataInfo { 26 | fullName: string; 27 | label: string; 28 | type: string; 29 | required?: boolean; 30 | unique?: boolean; 31 | externalId?: boolean; 32 | length?: number; 33 | precision?: number; 34 | scale?: number; 35 | visibleLines?: number; 36 | referenceTo?: string; 37 | relationshipLabel?: string; 38 | relationshipName?: string; 39 | deleteConstraint?: 'Cascade' | 'Restrict' | 'SetNull'; 40 | valueSet?: { 41 | valueSetDefinition: ValueSetDefinition; 42 | }; 43 | defaultValue?: string | number | boolean; 44 | description?: string; 45 | } -------------------------------------------------------------------------------- /src/types/salesforce.ts: -------------------------------------------------------------------------------- 1 | export interface SalesforceObject { 2 | name: string; 3 | label: string; 4 | custom: boolean; 5 | } 6 | 7 | export interface SalesforceField { 8 | name: string; 9 | label: string; 10 | type: string; 11 | nillable: boolean; 12 | length?: number; 13 | picklistValues: Array<{ value: string }>; 14 | defaultValue: string | null; 15 | referenceTo: string[]; 16 | } 17 | 18 | export interface SalesforceDescribeResponse { 19 | name: string; 20 | label: string; 21 | fields: SalesforceField[]; 22 | custom: boolean; 23 | } 24 | 25 | export interface SalesforceError { 26 | statusCode: string; 27 | message: string; 28 | fields?: string[]; 29 | } 30 | 31 | export interface DMLResult { 32 | success: boolean; 33 | id?: string; 34 | errors?: SalesforceError[] | SalesforceError; 35 | } -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jsforce'; 2 | -------------------------------------------------------------------------------- /src/utils/connection.ts: -------------------------------------------------------------------------------- 1 | import jsforce from 'jsforce'; 2 | 3 | export async function createSalesforceConnection() { 4 | const loginUrl = process.env.SALESFORCE_INSTANCE_URL || 'https://login.salesforce.com'; 5 | 6 | // Check which authentication method to use 7 | const useOAuth = process.env.SALESFORCE_CONSUMER_KEY && process.env.SALESFORCE_CONSUMER_SECRET; 8 | 9 | try { 10 | if (useOAuth) { 11 | // OAuth2 authentication with consumer key and secret 12 | const conn = new jsforce.Connection({ 13 | oauth2: { 14 | loginUrl, 15 | clientId: process.env.SALESFORCE_CONSUMER_KEY!, 16 | clientSecret: process.env.SALESFORCE_CONSUMER_SECRET! 17 | } 18 | }); 19 | 20 | // For OAuth2, we still need username and password for JWT bearer flow 21 | if (!process.env.SALESFORCE_USERNAME || !process.env.SALESFORCE_PASSWORD) { 22 | throw new Error('Username and password are required even when using OAuth2'); 23 | } 24 | 25 | await conn.login( 26 | process.env.SALESFORCE_USERNAME!, 27 | process.env.SALESFORCE_PASSWORD! 28 | ); 29 | 30 | return conn; 31 | } else { 32 | // Username/password authentication with security token 33 | if (!process.env.SALESFORCE_USERNAME || !process.env.SALESFORCE_PASSWORD || !process.env.SALESFORCE_TOKEN) { 34 | throw new Error('Username, password, and security token are required for password authentication'); 35 | } 36 | 37 | const conn = new jsforce.Connection({ 38 | loginUrl 39 | }); 40 | 41 | await conn.login( 42 | process.env.SALESFORCE_USERNAME!, 43 | process.env.SALESFORCE_PASSWORD! + process.env.SALESFORCE_TOKEN! 44 | ); 45 | 46 | return conn; 47 | } 48 | } catch (error) { 49 | console.error('Error connecting to Salesforce:', error); 50 | throw error; 51 | } 52 | } -------------------------------------------------------------------------------- /src/utils/errorHandler.ts: -------------------------------------------------------------------------------- 1 | interface ErrorResult { 2 | success: boolean; 3 | fullName?: string; 4 | errors?: Array<{ message: string; statusCode?: string; fields?: string | string[]; }> | 5 | { message: string; statusCode?: string; fields?: string | string[]; }; 6 | } 7 | 8 | export function formatMetadataError(result: ErrorResult | ErrorResult[], operation: string): string { 9 | let errorMessage = `Failed to ${operation}`; 10 | const saveResult = Array.isArray(result) ? result[0] : result; 11 | 12 | if (saveResult && saveResult.errors) { 13 | if (Array.isArray(saveResult.errors)) { 14 | errorMessage += ': ' + saveResult.errors.map((e: { message: string }) => e.message).join(', '); 15 | } else if (typeof saveResult.errors === 'object') { 16 | const error = saveResult.errors; 17 | errorMessage += `: ${error.message}`; 18 | if (error.fields) { 19 | errorMessage += ` (Field: ${error.fields})`; 20 | } 21 | if (error.statusCode) { 22 | errorMessage += ` [${error.statusCode}]`; 23 | } 24 | } else { 25 | errorMessage += ': ' + String(saveResult.errors); 26 | } 27 | } 28 | 29 | return errorMessage; 30 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2022", 5 | "moduleResolution": "bundler", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "rootDir": "./src" 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | --------------------------------------------------------------------------------