Tutoriel · Salesforce · MCP

Brancher un agent IA sur Salesforce avec MCP

Exposer à un agent des outils Salesforce en lecture seule (requête SOQL, description d’objet) via un serveur MCP, avec un utilisateur d’intégration aux droits minimaux.

MCP en bref

Le Model Context Protocol (MCP) est un protocole ouvert qui standardise la façon dont un agent IA découvre et appelle des outils externes. Un serveur MCP expose des outils (nom, description, schéma des paramètres) ; un client MCP, intégré à l’agent, les présente au modèle et relaie ses appels.

Intérêt : on écrit l’intégration une fois, et elle fonctionne avec tous les agents compatibles (agents de code, applications de bureau, agents maison), au lieu d’un connecteur par outil.

Salesforce publie ses propres serveurs MCP, notamment pour le développement (Salesforce DX). Cet article montre le principe en construisant un petit serveur, ce qui aide aussi à évaluer ceux qu’on installe.

Choisir le périmètre

Un agent est d’autant plus fiable qu’il dispose de peu d’outils, bien décrits. Pour aider un développeur à explorer une org de démonstration, deux outils en lecture suffisent :

  • describe_object : les champs d’un objet, pour écrire des requêtes justes ;
  • soql_query : une requête SOQL, résultat plafonné.

Pas d’outil d’écriture : une création ou une suppression passe par un humain, ou par un outil dédié, étroit et confirmé.

Authentification : un utilisateur d’intégration

Le serveur n’utilise jamais le compte d’un développeur. On crée un utilisateur d’intégration dédié, avec un permission set qui n’ouvre que les objets et champs utiles, en lecture.

Le jeton s’obtient par une External Client App (ou une Connected App) avec le flux JWT Bearer ou Client Credentials : aucun mot de passe stocké, et un accès révocable d’un clic. Le jeton est passé au serveur par variable d’environnement, jamais écrit dans le code ni dans un fichier versionné.

Le serveur

En TypeScript, avec le SDK officiel MCP et le transport stdio (l’agent lance le serveur comme un processus local) :

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const INSTANCE = process.env.SF_INSTANCE_URL!;   // https://acme-demo.my.salesforce.com
const TOKEN = process.env.SF_ACCESS_TOKEN!;      // jeton de l'utilisateur d'intégration
const API = `${INSTANCE}/services/data/v62.0`;
const MAX_ROWS = 200;

async function sf(path: string) {
  const res = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${TOKEN}` } });
  if (!res.ok) throw new Error(`Salesforce ${res.status} : ${await res.text()}`);
  return res.json();
}

const server = new McpServer({ name: "salesforce-lecture", version: "1.0.0" });

server.registerTool(
  "soql_query",
  {
    description:
      "Exécute une requête SOQL en lecture sur l'org de démonstration et renvoie au plus 200 lignes. " +
      "Toujours préciser les champs (pas de FIELDS(ALL)) et ajouter un LIMIT.",
    inputSchema: { query: z.string().describe("Requête SOQL, ex. : SELECT Id, Name FROM Account LIMIT 10") },
  },
  async ({ query }) => {
    // Défense en profondeur : SOQL est déjà en lecture seule, mais on refuse ce qui n'est pas un SELECT.
    if (!/^\s*select\s/i.test(query)) {
      return { content: [{ type: "text", text: "Seules les requêtes SELECT sont acceptées." }], isError: true };
    }
    const data = await sf(`/query?q=${encodeURIComponent(query)}`);
    const rows = data.records.slice(0, MAX_ROWS);
    const note = data.totalSize > rows.length ? `\n(${data.totalSize} lignes au total, ${rows.length} renvoyées)` : "";
    return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) + note }] };
  }
);

server.registerTool(
  "describe_object",
  {
    description: "Liste les champs d'un objet Salesforce (nom d'API, libellé, type) pour écrire une requête juste.",
    inputSchema: { objectName: z.string().regex(/^[A-Za-z0-9_]+$/).describe("Nom d'API, ex. : Case") },
  },
  async ({ objectName }) => {
    const data = await sf(`/sobjects/${objectName}/describe`);
    const fields = data.fields.map((f: any) => `${f.name} (${f.type}) — ${f.label}`);
    return { content: [{ type: "text", text: fields.join("\n") }] };
  }
);

await server.connect(new StdioServerTransport());

Les descriptions des outils sont des consignes pour le modèle : elles disent quoi faire (« préciser les champs », « ajouter un LIMIT ») et donnent un exemple. Les erreurs sont renvoyées avec isError, pour que le modèle corrige sa requête au lieu d’abandonner.

Brancher le serveur sur un agent

Dans un agent de code comme Claude Code, un fichier .mcp.json à la racine du projet déclare le serveur pour toute l’équipe :

{
  "mcpServers": {
    "salesforce-lecture": {
      "command": "node",
      "args": ["dist/server.js"],
      "env": {
        "SF_INSTANCE_URL": "https://acme-demo.my.salesforce.com",
        "SF_ACCESS_TOKEN": "${SF_ACCESS_TOKEN}"
      }
    }
  }
}

La syntaxe ${SF_ACCESS_TOKEN} reprend la variable de l’environnement du développeur : le fichier se versionne sans secret. On peut ensuite demander : « Combien de demandes ouvertes ont plus de 30 jours ? » L’agent décrit l’objet Case, écrit la requête, l’exécute et répond.

Sécurité

  • Moindre privilège : la vraie protection est le permission set de l’utilisateur d’intégration. Le filtre SELECT du code n’est qu’une ceinture de sécurité.
  • Pas d’org de production pour un usage exploratoire : une sandbox avec des données anonymisées ou fictives.
  • Injection de prompt : les données lues (description d’une demande, commentaire) peuvent contenir du texte qui ressemble à une consigne. Un serveur en lecture seule limite les dégâts possibles ; c’est une raison de plus pour ne pas exposer d’écriture.
  • Volume : plafonner les lignes renvoyées protège à la fois le contexte de l’agent et les limites d’API de l’org.
  • Serveurs tiers : un serveur MCP exécute du code avec les droits qu’on lui donne. Lire son code ou s’en tenir à des éditeurs de confiance.
🍪 Gestion des cookies
Ce site utilise des cookies pour mesurer l'audience (Google Analytics) et lire des vidéos (YouTube). Vous pouvez personnaliser votre choix.
Mentions légales
Réglages des cookies
Essentiels Nécessaires au fonctionnement du site — ne peuvent pas être désactivés.
Google Analytics Analyse d'audience (pages vues, sources de trafic). Aucune donnée personnelle identifiante.
YouTube Permet de lire les vidéos YouTube intégrées sur cette page.