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 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. 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: Claude for Desktop Node.js Salesforce CLI Claude for Desktop Claude for Desktop Node.js Node.js Salesforce CLI Salesforce CLI 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. sf-mcp-server In VS Code open a terminal and initiate a new npm project by executing the following command: npm init -y npm init -y Install the required dependencies: npm install @modelcontextprotocol/sdk zod npm install -D @types/node typescript 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. src sf-cmp-server Create a new file inside the src folder called index.ts, it should be inside this path ./src/index.ts. src index.ts ./src/index.ts Create a new file called tsconfig.json inside the root of your project folder and populate it with this code: tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } { "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: package.json { "type": "module", "bin": { "sf-mcp-server": "./build/index.js" }, "scripts": { "build": "tsc && chmod 755 build/index.js" }, "files": ["build"] } { "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. 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 . ├── 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: .src/index.ts 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: {}, }, }); 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. sf org list command authorize 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: sf org list --json .src/index.ts 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), }, ], }; }); 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. targetOrg fields executeSoqlQuery sf command to query records using a SOQL query 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), }, ], }; } ); 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: .src/index.ts 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); }); 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. .src/index.ts 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: build sf-cmp-server, along index.js npm run 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: For MacOS/Linux: ~/Library/Application Support/Claude/claude_desktop_config.json ~/Library/Application Support/Claude/claude_desktop_config.json For Windows: For Windows: C:\Users\YOUR_USERNAME\AppData\Roaming\Claude\claude_desktop_config.json C:\Users\YOUR_USERNAME\AppData\Roaming\Claude\claude_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: claude_desktop_config.json { "mcpServers": { "sf-mcp-server": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/sf-mcp-server/build/index.js"] } } } { "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: sf-mcp-server Try writing a prompt to get your connected orgs, for example: list connected orgs 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 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 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 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. GitHub