├── .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 |
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 |
--------------------------------------------------------------------------------