Skip to main content

Command Palette

Search for a command to run...

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

Published
8 min read
🤖 How to Build an AI Agent Using MCP and Connect It to Salesforce (Step-by-Step Guide)
R
Salesforce Developer | Building AI Agents & Automation Tools | Exploring Node.js & OpenAI | Sharing real-world projects

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:

  1. AI understands the user goal

  2. AI chooses a tool and (optionally) constructs structured arguments

  3. System executes the tool

  4. 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.


  1. In Salesforce: Setup → App Manager → New Connected App

  2. 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):

  1. Send user input + tool metadata to the model.

  2. If the model returns a function/tool call, run that function locally.

  3. Return the tool output to the model as a new message and ask for the final answer.

  4. 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.

AI Agent

Part 1 of 1