This article will guide you through building a simple yet functional MCP server that integrates with Salesforce, enabling Claude Desktop to directly query and interact with your Salesforce data.
What are we going to build
We’ll create a Node.js-based MCP server that enables Claude to:
- List all connected Salesforce organizations from your Salesforce CLI
- Execute SOQL queries through natural language prompts
- Retrieve and display Salesforce data in a conversational format
Full code for this project is available on GitHub.
What it looks like:
What is MCP
MCP is a protocol developed by Anthropic that allows AI models to extend their capabilities by accessing external systems. In our case, it enables Claude to interact with Salesforce orgs, execute queries, and process data – all through simple conversation.
Let’s build it!
Prerequisites
Before proceeding with the article, make sure you have the following tools installed on your computer:
Also, it’s assumed you have a basic experience with LLMs, like ChatGPT, Gemini, Claude, etc.
Project setup
Create a folder for the project anywhere on your computer. Let’s name it, for example, sf-mcp-server. Open the folder in VS Code.
In VS Code open a terminal and initiate a new npm project by executing the following command:
npm init -y
Install the required dependencies:
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
Create a new folder called src inside the root of your project folder, which is the sf-cmp-server one.
Create a new file inside the src folder called index.ts, it should be inside this path ./src/index.ts.
Create a new file called tsconfig.json inside the root of your project folder and populate it with this code:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
There is the package.json file in the root of your project folder, adjust its content to make sure it contains the following code:
{
"type": "module",
"bin": {
"sf-mcp-server": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": ["build"]
}
Full code of this file is available in the GitHub repository.
By the end of this part your project folder should have the following structure:
.
├── node_modules
├── src/
│ └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json
The coding part
Let’s start with importing the packages we are going to use, and setting up the server. Add the following code in the top of the .src/index.ts
file:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Importing exec from node:child_process to execute shell commands
import { exec } from "node:child_process";
const server = new McpServer({
name: "sf-mcp-server",
version: "1.0.0",
capabilities: {
tools: {},
},
});
Now, let’s add some code for fetching the connected Salesforce Orgs. It’s better to create a connected app and set up an authentication flow if you’re planning to work with a single org, but we will use the connected orgs from the Salesforce CLI for simplicity.
Run the sf org list
command in your terminal and check whether you have any connected orgs available. If not, then authorize an org which you can use going further.
This code tells the MCP server that you have a tool which can execute the sf org list --json
command in shell and pass the result to Claude so it can understand what orgs you have authenticated to. Add this code below to the .src/index.ts
file:
const listConnectedSalesforceOrgs = async () => {
return new Promise((resolve, reject) => {
exec("sf org list --json", (error, stdout, stderr) => {
if (error) {
return reject(error);
}
if (stderr) {
return reject(new Error(stderr));
}
try {
const result = JSON.parse(stdout);
resolve(result);
} catch (parseError) {
reject(parseError);
}
});
});
};
server.tool("list_connected_salesforce_orgs", {}, async () => {
const orgList = await listConnectedSalesforceOrgs();
return {
content: [
{
type: "text",
text: JSON.stringify(orgList, null, 2),
},
],
};
});
Great! The next step is to add the code for executing SOQL queries in one of the connected orgs.
This code accepts an input schema from the prompt you send to Claude in the Claude for Desktop app, parses it for the separate entities like targetOrg
or fields
to query and sends this information to the executeSoqlQuery
function, which executes the sf command to query records using a SOQL query. After the later function finishes executing, its result is being sent to the Claude, which parses the result and responds to you in a pretty way in the chat.
Now add this code after the previously appended one:
const executeSoqlQuery = async (
targetOrg: string,
sObject: string,
fields: string,
where?: string,
orderBy?: string,
limit?: number
) => {
let query = `SELECT ${fields} FROM ${sObject}`;
if (where) query += " WHERE " + where;
if (limit) query += " LIMIT " + limit;
if (orderBy) query += " ORDER BY " + orderBy;
const sfCommand = `sf data query --target-org ${targetOrg} --query "${query}" --json`;
return new Promise((resolve, reject) => {
exec(sfCommand, (error, stdout, stderr) => {
if (error) {
return reject(error);
}
if (stderr) {
return reject(new Error(stderr));
}
try {
const result = JSON.parse(stdout);
resolve(result.result.records || []);
} catch (parseError) {
reject(parseError);
}
});
});
};
server.tool(
"query_records",
"Execute a SOQL query in Salesforce Org",
{
input: z.object({
targetOrg: z
.string()
.describe("Target Salesforce Org to execute the query against"),
sObject: z.string().describe("Salesforce SObject to query from"),
fields: z
.string()
.describe("Comma-separated list of fields to retrieve"),
where: z
.string()
.optional()
.describe("Optional WHERE clause for the query"),
orderBy: z
.string()
.optional()
.describe("Optional ORDER BY clause for the query"),
limit: z
.number()
.optional()
.describe("Optional limit for the number of records returned"),
}),
},
async ({ input }) => {
const { targetOrg, sObject, fields, where, orderBy, limit } = input;
const result = await executeSoqlQuery(
targetOrg,
sObject,
fields,
where,
orderBy,
limit
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
We’re done with the code for basic functionality of this MCP server, now let’s add the code to initialize server and setup connection. Append this code to the end of the .src/index.ts
file:
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Salesforce MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
The whole .src/index.ts
file should look like this.
Making sure it works
Let’s start testing the MCP server by building our project, it’s an important step, don’t skip it. A build folder will be created inside the sf-cmp-server, along with the index.js file, which will be used by MCP server. Execute the following command in your terminal to perform the build:
npm run build
Now, the Claude for Desktop app should be configured to work with the MCP it’s going to be used as a client. In your computer, navigate to the path where the Claude’s config file is located. Create it if it’s not present.
For MacOS/Linux:
~/Library/Application Support/Claude/claude_desktop_config.json
For Windows:
C:UsersYOUR_USERNAMEAppDataRoamingClaudeclaude_desktop_config.json
Open the claude_desktop_config.json file in VS Code and add the following code to make the MCP server be displayed in the Claude for Desktop app’s UI:
{
"mcpServers": {
"sf-mcp-server": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/sf-mcp-server/build/index.js"]
}
}
}
Save the file and restart the Claude for Desktop app. You should see the the sf-mcp-server in the tools UI in the app:
Try writing a prompt to get your connected orgs, for example:
list connected orgs
You should see a similar result:
Copy org’s alias or username and write the next prompt to actually query the records, for example:
query 5 account names and websites from the ORG_YOU_COPIED org
Then you will see something like this:
Conclusion
Congratulations! You’ve successfully built an MCP server that connects Claude Desktop with Salesforce. You can now query your Salesforce data using natural language, making data exploration more intuitive and efficient.
What we’ve accomplished
- Built a Node.js MCP server with TypeScript
- Implemented tools to list Salesforce orgs and execute SOQL queries
- Configured Claude Desktop to use the custom server
- Tested the integration with real queries
Next steps
While this tutorial used Salesforce CLI for simplicity, you can unlock the full power of Salesforce by implementing proper authentication with Connected Apps and directly using Salesforce APIs. This approach enables:
- OAuth 2.0 authentication flows
- Direct REST, SOAP, and Bulk API access
- Real-time streaming with Platform Events
- Metadata API for configuration management
- Complete CRUD operations and complex business logic
The skills you’ve learned here apply to integrating Claude with any system or API. Whether building internal tools or automating workflows, MCP provides a solid foundation for creating AI-powered experiences.
Complete code for this project is available on GitHub.