mcp-salesforce-server
Provides tools for interacting with Salesforce Contacts, including listing and fetching contacts and proposing/confirming updates with a guard that prevents overwriting concurrent changes.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-salesforce-serverPropose updating Jane Doe's email to jane@acme.com"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-salesforce-server
An MCP server for Salesforce Contacts where irreversible CRM writes get a verify-before-apply step, backed by a server-held "before" snapshot the client can't tamper with.
Why it's built this way
Letting an AI agent write straight to a live CRM record is the kind of irreversible action that deserves a verification step rather than a single hopeful tool call. Two things go wrong with the naive design:
The agent's information goes stale. It read a field a few minutes ago; someone else has since changed it in Salesforce. Writing blindly discards that person's edit with no trace.
There's no record of what changed. A CRM write with no audit trail is hard to review, hard to debug, and hard to trust.
So the write is split in two:
propose_updatefetches the current live values for the fields being changed, computes a before/after diff, appends a dry-run entry to an append-only audit log, and stores the proposal server-side under a generated id. It never writes to Salesforce.confirm_updatetakes only that proposal id — nothing else. The "before" snapshot stays server-side, so a caller cannot supply it, forge it, or quietly adjust it to make the check pass. The server re-fetches the live record, and only if every field still matches what the diff was computed against does the write actually happen. If anything changed in between, the whole confirm is rejected as stale and nothing is applied.
That re-verification step is the point of the project. The implementation is src/guard.ts; the tests that prove it are src/guard.test.ts.
Related MCP server: Salesforce MCP Server
Tools
Tool | Reads/writes | What it does |
| — | Smoke-test tool; returns |
| read | List Contacts, optionally filtered by a search term against Name/Email. |
| read | Fetch one Contact by Id. |
| read + dry-run log | Diff a proposed change against live values; never writes. |
| write (guarded) | Apply a proposal, only if the record hasn't changed since. |
Writable fields are deliberately limited to a small allowlist: Email, Phone, Title, Department.
Quickstart
npm install
cp .env.example .env # then fill in your Salesforce Connected App credentials
npm run build
npx @modelcontextprotocol/inspector node dist/index.jsThe Inspector opens a UI for calling the tools against your org; --cli --method tools/list does the same non-interactively.
Salesforce setup
Create a Developer Edition org at developer.salesforce.com.
In Setup → App Manager, create a Connected App: enable OAuth, and under its OAuth policies enable Client Credentials Flow with a Run As user configured (the flow acts as that user).
Copy
.env.exampleto.envand fill in the Consumer Key/Secret and your org's login URL..envis gitignored and never committed.
Worked examples
Both transcripts below are from real runs against a live Salesforce Developer Edition org, with the audit-log lines quoted verbatim from data/audit-log.jsonl.
The normal path: propose, then confirm
// propose_update({ contactId: "003bm00001qvbvhAAA", changes: { Title: "Test Update" } })
{
"proposalId": "086812c3-c4cc-4101-a7ab-949a873b79f9",
"contactId": "003bm00001qvbvhAAA",
"fields": { "Title": { "before": "SVP, Operations", "after": "Test Update" } },
"expiresAt": "2026-09-09T20:33:06.222Z"
}
// confirm_update({ proposalId: "086812c3-c4cc-4101-a7ab-949a873b79f9" })
{
"status": "applied",
"proposalId": "086812c3-c4cc-4101-a7ab-949a873b79f9",
"contactId": "003bm00001qvbvhAAA",
"fields": { "Title": { "before": "SVP, Operations", "after": "Test Update" } },
"appliedAt": "2026-09-09T20:20:19.526Z"
}The audit log captures both halves — the dry run and the write that followed it:
{"event":"proposed","timestamp":"2026-09-09T20:18:06.227Z","proposalId":"086812c3-c4cc-4101-a7ab-949a873b79f9","contactId":"003bm00001qvbvhAAA","fields":{"Title":{"before":"SVP, Operations","after":"Test Update"}}}
{"event":"confirmed","timestamp":"2026-09-09T20:20:19.526Z","proposalId":"086812c3-c4cc-4101-a7ab-949a873b79f9","contactId":"003bm00001qvbvhAAA","fields":{"Title":{"before":"SVP, Operations","after":"Test Update"}},"appliedAt":"2026-09-09T20:20:19.526Z"}The blocked path: the record changed underneath the proposal
Here a change to a different Contact was proposed, and then — to genuinely simulate a concurrent edit rather than stage one in code — the record's Title was edited by hand in the Salesforce UI, outside this tool entirely, before the confirm was issued.
// propose_update({ contactId: "003bm00001qvbvpAAA", changes: { Title: "Pending Change" } })
{
"proposalId": "75bd1ff0-b223-48e0-9485-f82172c19307",
"contactId": "003bm00001qvbvpAAA",
"fields": { "Title": { "before": "CEO", "after": "Pending Change" } }
}
// ...Title edited directly in the Salesforce UI, from "CEO" to "Interim CEO"...
// confirm_update({ proposalId: "75bd1ff0-b223-48e0-9485-f82172c19307" })
{
"status": "rejected",
"reason": "stale",
"contactId": "003bm00001qvbvpAAA",
"fields": { "Title": { "proposedBefore": "CEO", "currentValue": "Interim CEO" } }
}{"event":"proposed","timestamp":"2026-09-09T20:39:05.647Z","proposalId":"75bd1ff0-b223-48e0-9485-f82172c19307","contactId":"003bm00001qvbvpAAA","fields":{"Title":{"before":"CEO","after":"Pending Change"}}}
{"event":"rejected_stale","timestamp":"2026-09-09T20:49:19.283Z","proposalId":"75bd1ff0-b223-48e0-9485-f82172c19307","contactId":"003bm00001qvbvpAAA","fields":{"Title":{"proposedBefore":"CEO","currentValue":"Interim CEO"}}}Nothing was written to Salesforce here — on the stale path patchContact is never called, and the hand-made edit ("Interim CEO") survived untouched. Note the two timestamps: the confirm came about ten minutes after the proposal, comfortably inside the 15-minute TTL. This was rejected because the value changed, not because the proposal expired — those are separate rejection reasons (stale vs expired), and this is the first one.
A proposal also can't be confirmed twice — it's consumed on first read — and a failed Salesforce write retries transient 5xx/network errors up to three times with backoff while failing fast on a 4xx. DECISIONS.md covers the reasoning behind each of those.
Evidence, not assertions
src/guard.test.ts is the proof the guard works, rather than a claim that it does. It covers a stale write being rejected, a confirm with no prior proposal being rejected, a correct before/after diff reaching the audit log, a clean confirm actually applying, a proposal not being confirmable twice, and the retry/fail-fast behavior on write failures.
Tests that never fail prove nothing, so the guard was checked against deliberate breakage: with the stale-value comparison in guard.ts disabled, the suite was rerun and the stale-rejection test failed exactly as it should have, then the check was restored. That's what establishes the test is exercising the guard rather than just confirming the code runs.
npm testDesign decisions
Every non-obvious fork in this build — SDK version, transport, auth flow, object choice, proposal storage, audit log format, retry policy — is recorded in DECISIONS.md in Chose / Over / Because form.
Project layout
src/
guard.ts propose/confirm orchestration — the tested core
sfClient.ts Salesforce REST client (auth, query, get, patch)
proposalStore.ts file-backed, single-use proposal store
auditLog.ts append-only JSON Lines audit log
tools/ one file per MCP tool
test-utils/ fake Salesforce client used by guard.test.tsLicense
MIT — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read and write a CRM built for agents. Every change carries who asserted it and how.
- kanonikOAuthai.kanonik
Governance runtime for compliance: verified, human-approved writes to a tamper-evident record.
Project-scoped marketing ops for agents: Bearer dpk_/dpa_ or OAuth, read and supervised write.
Read SMS, WhatsApp, email, contacts and audiences from your Bird workspace, plus safe CRM writes.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to perform secure Salesforce Lead management operations including CRUD actions, status updates, and idempotent syncing. It features TRD-compliant audit logging and OAuth 2.0 authentication to ensure reliable CRM interactions.-
- FlicenseNot gradedqualityNot gradedmaintenanceEnables interaction with Salesforce through REST, Bulk API, and SOQL for comprehensive CRM management across objects like Leads, Accounts, and Opportunities. It provides a structured interface for performing CRUD operations and executing complex queries via the Model Context Protocol.-
- FlicenseNot gradedqualityCmaintenanceEnables lead management and permission set operations on Salesforce through the MCP protocol.-
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to manage contacts with full CRUD, dedup, merge, import/export, sync with Google/Apple/CardDAV, and git-backed rollback.4 npmAGPL 3.0