🤖 How to Build an AI Agent Using MCP and Connect It to Salesforce (Step-by-Step Guide)

AI agents are changing how developers build applications. Instead of hardcoding every step, we can build systems where AI decides what to do, when to do it, and which tools to use.
In this post I’ll walk you through building a simple AI agent using MCP (Model Context Protocol) and connecting it to Salesforce to fetch real data. The agent will:
Discover and call tools (Salesforce queries, notifications, updates)
Execute tool functions when needed
Return structured, actionable results
🧠 What is MCP?
MCP (Model Context Protocol) is a design approach where the model is given:
A list of available tools (name, description, input schema)
Context (conversation + environment data)
A protocol for asking to call tools and receiving the results
The flow becomes:
AI understands the user goal
AI chooses a tool and (optionally) constructs structured arguments
System executes the tool
Tool output is fed back into the model for final response or next step
This helps you keep the agent small, auditable, and safe.
⚙️ Prerequisites
Node.js (v18+)
Basic JavaScript / Node knowledge
OpenAI API key
Salesforce Developer org (or any org with API access)
dotenv for env variables
🏗️ Project Setup
mkdir mcp-agent
cd mcp-agent
npm init -y
npm install express openai axios dotenv
Create a .env:
OPENAI_API_KEY=your_openai_key
SF_CLIENT_ID=your_client_id
SF_CLIENT_SECRET=your_client_secret
SF_REFRESH_TOKEN=your_refresh_token
SF_INSTANCE_URL=https://your-instance.salesforce.com
PORT=3000
Notes:
Use OAuth with a refresh token (offline access) so your service can refresh access tokens without interactive login.
Store secrets securely (vault/secret manager) in production, not plain
.env.
🔗 Connect to Salesforce (recommended approach)
In Salesforce: Setup → App Manager → New Connected App
Enable OAuth
Callback URL: http://localhost:3000/callback (for dev)
Scopes: api, refresh_token, offline_access (and others only if needed)
Use the refresh token to obtain short-lived access tokens. Example token refresh helper:
// sfAuth.js
import axios from "axios";
export async function getAccessToken() {
const params = new URLSearchParams();
params.append("grant_type", "refresh_token");
params.append("client_id", process.env.SF_CLIENT_ID);
params.append("client_secret", process.env.SF_CLIENT_SECRET);
params.append("refresh_token", process.env.SF_REFRESH_TOKEN);
const res = await axios.post(
`${process.env.SF_INSTANCE_URL}/services/oauth2/token`,
params
);
return res.data.access_token;
}
(Adjust URL to token endpoint if using a different Salesforce instance domain.)
🔧 Creating Salesforce Tools (MCP)
Tools are plain functions your agent can call. Keep them small, idiomatic, and idempotent where possible.
Example: fetch Accounts.
// tools.js
import axios from "axios";
import { getAccessToken } from "./sfAuth.js";
export async function getAccounts(limit = 10) {
const accessToken = await getAccessToken();
const soql = `SELECT Id, Name, Type, Industry, LastModifiedDate FROM Account ORDER BY LastModifiedDate DESC LIMIT ${Number(limit)}`;
const encoded = encodeURIComponent(soql);
const url = `\({process.env.SF_INSTANCE_URL}/services/data/v59.0/query/?q=\){encoded}`;
const res = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
// Return minimal fields and count
return {
records: res.data.records,
totalSize: res.data.totalSize,
};
}
export async function getAccountById(id) {
const accessToken = await getAccessToken();
const url = `\({process.env.SF_INSTANCE_URL}/services/data/v59.0/sobjects/Account/\){id}`;
const res = await axios.get(url, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return res.data;
}
Add other tools similarly: query opportunities, create tasks, update fields, post Chatter messages, etc. Each tool should return structured JSON.
🧩 Registering Tools (tool metadata for the model)
Expose metadata the model can use to decide which tool to call. When you use function-calling (or a simple MCP pattern), the metadata helps the model produce structured calls.
// mcp.js
import { getAccounts, getAccountById } from "./tools.js";
export const tools = [
{
name: "getAccounts",
description: "Fetch recent Salesforce accounts. Args: { limit: number }",
function: getAccounts,
// If using function-calling features, include a JSON Schema for args:
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max number of accounts to fetch" },
},
required: [],
},
},
{
name: "getAccountById",
description: "Fetch a single Account by Salesforce Id. Args: { id: string }",
function: getAccountById,
parameters: {
type: "object",
properties: {
id: { type: "string" },
},
required: ["id"],
},
},
];
🤖 Building the Agent Orchestrator
Pattern used here (MCP loop):
Send user input + tool metadata to the model.
If the model returns a function/tool call, run that function locally.
Return the tool output to the model as a new message and ask for the final answer.
Repeat if the model requests additional tools.
Example agent using the OpenAI function-calling pattern (pseudo-real code for the official Node SDK):
// agent.js
import OpenAI from "openai";
import { tools } from "./mcp.js";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// small helper to map tool metadata for the model's function parameter
function buildFunctionDefs(tools) {
return tools.map(t => ({
name: t.name,
description: t.description,
parameters: t.parameters || { type: "object" },
}));
}
export async function runAgent(userInput) {
// 1) Ask the model what to do
const initial = await client.chat.completions.create({
model: "gpt-4o", // pick a model in your account that supports function-calling
messages: [
{
role: "system",
content:
"You are an assistant that can call tools. When you want to call a tool, respond with a function call using JSON arguments matching the declared schema.",
},
{ role: "user", content: userInput },
],
functions: buildFunctionDefs(tools),
function_call: "auto",
});
const message = initial.choices[0].message;
// 2) If the model wants to call a function, execute it
if (message.function_call) {
const { name, arguments: argsStr } = message.function_call;
let args = {};
try {
args = argsStr ? JSON.parse(argsStr) : {};
} catch (err) {
// Bad JSON from model — tell it to reformat
return {
error: "Model returned invalid JSON for function call arguments",
detail: err.message,
};
}
// find the tool function and run it
const tool = tools.find(t => t.name === name);
if (!tool) {
return { error: `Unknown tool: ${name}` };
}
let toolOutput;
try {
toolOutput = await tool.function(...Object.values(args));
} catch (err) {
toolOutput = { error: err.message };
}
// 3) Send the tool output back to the model and ask for finalization
const followUp = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: "You are an assistant that can call tools." },
{ role: "user", content: userInput },
message, // original model function call
{
role: "function",
name,
content: JSON.stringify(toolOutput),
},
{
role: "user",
content: "Based on the tool output, provide a concise summary and next steps.",
},
],
});
const final = followUp.choices[0].message.content;
return { result: final, toolOutput };
} else {
// Model didn't call a tool — just return its text
return { result: message.content };
}
}
Notes:
The above uses the Chat Completions function-calling flow. If you're using the newer Responses API, adapt accordingly to send tool metadata and handle tool calls similarly.
Validate model-returned JSON and guard against unexpected inputs.
🖥️ Example Express Server
// server.js
import express from "express";
import dotenv from "dotenv";
import { runAgent } from "./agent.js";
dotenv.config();
const app = express();
app.use(express.json());
app.post("/agent", async (req, res) => {
try {
const { input } = req.body;
const out = await runAgent(input);
res.json(out);
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Agent server running on ${port}`));
✅ Practical Patterns & Tips
Start with a small set of tools (read-only queries first), then expand to mutate data (create/update) with caution.
Limit scopes in your connected app. Grant only what you need.
Log function calls with correlation IDs for auditing.
Sanitize and validate any model-provided arguments before executing tools.
Add rate limiting and retries when calling external APIs (Salesforce/OpenAI).
Return structured results (JSON) from tools so the model can reason about data reliably.
Implement a “dry-run” or “preview” mode where the agent suggests actions but does not execute them unless explicitly approved.
🛡️ Security & Compliance
Never embed long-lived credentials in code. Use refresh-token + client secret flow and replace secrets using a secure secret store.
Rate-limit model and API usage, and monitor costs.
Add RBAC and approvals for destructive operations (e.g., mass-updates).
Keep sensitive data out of prompts when possible; redact or transform before sending to OpenAI if needed.
🧪 Testing & Iteration
Start with unit tests for each tool (mock Salesforce responses).
Test the agent with typical and adversarial prompts to see how it selects tools.
Add guardrails: deterministic schemas, explicit allowed-values lists, and user confirmations for risky actions.
📈 Next Steps / Ideas
Add human-in-the-loop approvals for any write operations.
Expand tools to query related records, compute metrics, or create tasks.
Build a UI that visualizes the agent’s chosen tool-calls and outputs for auditability.
Record conversations and actions for compliance and debugging.
Final Thoughts
Using MCP lets you design agents that are flexible yet auditable: the model chooses tools and the system executes them in a controlled environment. Start small, instrument heavily, and gradually add capabilities and safety checks. With a minimal set of tools and a solid orchestration loop, you can automate meaningful Salesforce tasks and free up time for higher-value work.
