DEV Community

Sudhakar
Sudhakar

Posted on

Custom-built MCP Server for Salesforce

A custom-built, self-hosted MCP server that lets AI agents (Claude Desktop,
Claude Code, the MCP Inspector, or any other MCP client) connect to
Salesforce — to query, search, and modify data in an org.

Not a Salesforce product. This is an independent, personal learning
project — not affiliated with, endorsed by, or supported by Salesforce,
Inc. Full explanation: docs/ARCHITECTURE.md.

New to MCP? If "server," "client," and "tool call" aren't already
familiar terms, read docs/MCP_PRIMER.md first — five
minutes, and everything else here will make more sense.

Quickstart

You'll need: a Salesforce org with an External Client App set up — a
free Developer Edition org works
fine — and its Consumer Key in hand. docs/SETUP.md walks
through creating that (10–15 min); do it first, then come back here.

Three ways to get a running server — pick whichever fits:

Option A — uvx (fastest; no clone, no venv; requires uv):

export SF_LOGIN_URL=https://your-domain.my.salesforce.com
export SF_CLIENT_ID=your-client-id
uvx --from sf-mcp-server sf-mcp-login   # one-time interactive login — opens your browser
uvx sf-mcp-server
Enter fullscreen mode Exit fullscreen mode

(sf-mcp-login needs the explicit --from sf-mcp-server — uv only infers
the package name from a bare command when they match, and this package
provides two commands. sf-mcp-server matches its own package name, so it
doesn't need --from.)

Or drop straight into an MCP client's config (Claude Desktop's
claude_desktop_config.json, Claude Code's .mcp.json) with "command":
"uvx", "args": ["sf-mcp-server"]
and the same env vars, plus
SF_PKCE_TOKEN_CACHE set to an absolute path
(the client launches the
server from its own working directory, not wherever you ran
sf-mcp-login, so the default relative cache path won't be found
otherwise) — see docs/USAGE.md for the full config
example. That login step is only needed once — see
docs/AUTHENTICATION.md for what it does and why,
and for the alternative Client Credentials Flow (SF_CLIENT_SECRET, no
login step) if you'd rather use a fixed service identity instead.

Option B — Python from source (for contributing, or if you'd rather not
use uv):

git clone <this-repo-url> && cd salesforce-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env   # fill in SF_LOGIN_URL / SF_CLIENT_ID
python -m salesforce_mcp.login   # one-time interactive login — opens your browser
python -m salesforce_mcp.server
Enter fullscreen mode Exit fullscreen mode

Option C — Docker (no Python setup needed; requires Docker installed
and running — check with docker info):

git clone <this-repo-url> && cd salesforce-mcp-server
docker build -t salesforce-mcp-server .
docker run --rm -i \
  -e SF_LOGIN_URL=https://your-domain.my.salesforce.com \
  -e SF_CLIENT_ID=your-client-id \
  -e SF_CLIENT_SECRET=your-client-secret \
  -e SF_AUTH_FLOW=client_credentials \
  -e MCP_TRANSPORT=stdio \
  salesforce-mcp-server
Enter fullscreen mode Exit fullscreen mode

Docker explicitly pins SF_AUTH_FLOW=client_credentials here rather than
using the default interactive login — there's no browser or display inside
a container for that flow to use. See
docs/AUTHENTICATION.md if you actually want PKCE
in a container anyway (mount a pre-existing .salesforce_pkce_token.json
from the host).

Whichever you pick, that's it running. Next: point the
MCP Inspector or Claude
Desktop at it and actually try a tool — see docs/USAGE.md.

Quick note on that -e MCP_TRANSPORT=stdio flag in Option C: Python vs.
Docker and stdio vs. HTTP are two separate choices, not tied together
—
Python defaults to stdio and Docker's image defaults to HTTP purely for
convenience, but all four combinations actually work. See
docs/MCP_PRIMER.md#the-two-transports-stdio-and-streamable-http
for what each transport actually is and why. For hosting this on a network
instead of running it locally, see docs/DEPLOYMENT.md.

What it can do

  • Query & search — sf_query (SOQL, auto-paginated), sf_search (SOSL)
  • Record CRUD — sf_get_record, sf_create_record, sf_update_record, sf_upsert_record (by external ID), sf_delete_record
  • Bulk API 2.0 — sf_bulk_query, sf_bulk_load, for record volumes too large for the one-record-per-call REST tools above
  • Composite — sf_composite, to bundle several sub-requests into one atomic call
  • Describe/discovery — sf_describe_object, sf_list_objects (trimmed fields + optional name_contains/custom_only filters, so it doesn't dump 800+ objects' full raw metadata), also available as MCP Resources (salesforce://objects, salesforce://schema/{sobject})
  • Ops — sf_api_usage (quick API-limit check), sf_org_health (fuller report: org info, all limits, and license seat usage)
  • Custom APIs — sf_call_apex_rest calls any custom Apex REST endpoint (@RestResource) your org exposes, no code changes needed — see docs/USAGE.md
  • Prompts — ready-made task templates for common requests: summarize_account, draft_followup_email, data_hygiene_check — see docs/USAGE.md
  • Platform events / Change Data Capture — sf_subscribe_platform_event replays a bounded batch of events from a platform event or CDC channel, honoring Salesforce's 72-hour Pub/Sub API retention window — see docs/USAGE.md
  • Elicitation — confirms before an unscoped sf_query/sf_search or any delete (sf_delete_record, sf_bulk_load(operation="delete")); disable with SF_ELICITATION_ENABLED=false — see docs/USAGE.md
  • Two auth options — the default interactive "Login with Salesforce" (OAuth Authorization Code + PKCE, per-user), via python -m salesforce_mcp.login or the in-session sf_login tool, or the OAuth Client Credentials Flow (one fixed service identity) for headless/shared use, switched with SF_AUTH_FLOW=client_credentials — see docs/AUTHENTICATION.md
  • Resilient by default — retries transient (5xx / REQUEST_LIMIT_EXCEEDED) Salesforce errors automatically; every other error comes back as a clean, readable message instead of a stack trace

Every tool above talks to a standard Salesforce API out of the box — none
of them are specific to any one org. Two ways to add your own: call
sf_call_apex_rest (works today, zero code) or add a first-class tool of
your own — docs/EXTENDING.md is a step-by-step guide.

Running it remotely (cloud)

The same server also runs as a container behind a network-reachable
Streamable HTTP endpoint, for when you want an agent that isn't on the same
machine to reach it. This has been built and run locally with Docker and
confirmed working — it has not yet been deployed to a real cloud account.

docs/DEPLOYMENT.md has the full picture, including that
caveat up front, plus two ready-to-try recipes (GCP Cloud Run, AWS App
Runner).

Tests

pytest tests/ -v      # all Salesforce calls are mocked with respx — no live org needed
ruff check src tests
Enter fullscreen mode Exit fullscreen mode

This is the automated suite — fast, no Salesforce org or Docker required.
There are two other, manual checks, each testing something different:
Inspector-against-a-real-org (functional — see docs/USAGE.md)
and Docker-build-and-curl (plumbing only — see docs/DEPLOYMENT.md).

Documentation

Read in this order if you're getting started:

# Doc For
1 docs/MCP_PRIMER.md New to MCP — what a server/client/tool call actually is
2 docs/SETUP.md Creating the Salesforce org + integration, .env config
3 docs/AUTHENTICATION.md Both auth flows side by side — Client Credentials vs. "Login with Salesforce" (PKCE)
4 docs/USAGE.md Running it — Claude Desktop, Claude Code, MCP Inspector, example prompts
5 docs/DEPLOYMENT.md Hosting it in the cloud instead of locally
6 docs/EXTENDING.md Adding your own tool for a custom API
7 docs/RELEASING.md Maintainers only — how a PyPI release actually gets published
8 docs/ARCHITECTURE.md Optional — how and why it was built this way

License

MIT

Top comments (0)