Production-grade, read-only MCP server for Salesforce (SOQL queries, record reads, full-text search, and schema discovery)
Project description
sfdc-mcp-server
A production-grade, read-only MCP server for Salesforce: SOQL queries, record
reads, full-text search, and schema discovery through the Salesforce REST API, from
Claude Desktop, Claude Code, or any MCP client — with auth done properly (OAuth 2.0
device-code and client-credentials flows) and API-limit awareness (Salesforce's daily
request-limit usage is surfaced, and throttling is retried honoring Retry-After),
which the existing servers in this space skip.
Read-only by construction, not by convention. The HTTP client this server is built
on exposes only GET and POST — there is no write call to the Salesforce data
API — the only POST anywhere is the OAuth token exchange, which creates no records — so
no tool here could write to Salesforce even by accident. SOQL queries are additionally
guarded: soql_query rejects anything whose first keyword isn't SELECT before a
request is ever sent.
sfdc-mcp-server is not affiliated with, endorsed by, or sponsored by Salesforce, Inc.
Quickstart
Requires a Salesforce Connected App or External Client App (see below) and uv. Don't have a Salesforce org to test against? Sign up for a free Developer Edition org — no credit card required.
uvx sfdc-mcp-server
That starts the server over stdio. Until sfdc-mcp-server is published to PyPI, run it
from a local checkout instead:
uv run --directory <path-to-checkout> sfdc-mcp-server
In practice you'll point an MCP client at it instead of running it directly — for Claude Code:
Note: until sfdc-mcp-server is published to PyPI, replace uvx sfdc-mcp-server
in the command below with the from-source form
uvx --from git+https://github.com/inogen-ai/sfdc-mcp-server sfdc-mcp-server (see the
run-from-checkout note above).
claude mcp add salesforce -e SFDC_MCP_CLIENT_ID=<your-consumer-key> -- uvx sfdc-mcp-server
For Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"salesforce": {
"command": "uvx",
"args": ["sfdc-mcp-server"],
"env": {
"SFDC_MCP_CLIENT_ID": "<your-consumer-key>"
}
}
}
}
The first tool call you make (e.g. soql_query) won't have a cached token yet — it
returns device-code sign-in instructions (a URL and a short code) as the tool result
instead of data. Open the URL, enter the code, sign in. The server finishes the login
in the background as soon as you do, so the next tool call succeeds without any extra
steps on your end.
Connected App setup
Salesforce's Spring '26 release disabled creating new classic Connected Apps in most orgs — use an External Client App instead (Salesforce's own successor to Connected Apps; existing Connected Apps still work, but a fresh Developer Edition org signed up today can't create a new one through the UI). The steps below use External Client Apps; if your org still has classic Connected App creation enabled, the same settings exist under Setup → App Manager → New Connected App instead.
Both auth modes start the same way: Setup → Quick Find → External Client App Manager → New External Client App → give it a name, API name, and contact email, then in the app's OAuth Settings check Enable OAuth.
Device flow (interactive — for a human signing in)
- Under OAuth Settings, check Enable for Device Flow. If a Callback URL field is
required to save the form, any placeholder works
(
https://login.salesforce.com/services/oauth2/successis a common one) — it's never used by the device flow, which has no browser redirect. - Under OAuth Scopes, add
api(data access) andrefresh_token(its aliasoffline_accessworks too) — move both to Selected OAuth Scopes. Withoutrefresh_token, the server can't renew a session silently and you'll be asked to sign in again every couple of hours. - Important: leave Require Secret for Refresh Token Flow unchecked. Device
flow is a public client — it has no secret to send — and checking this box makes
Salesforce reject the server's silent refresh-token renewal with
invalid_client, forcing a fresh interactive login every time the access token expires. - Save. On the app's detail page, reveal and copy the Consumer Key — this is
SFDC_MCP_CLIENT_ID. Device flow is a public client, so there's no secret to copy andSFDC_MCP_CLIENT_SECRETstays unset. - Set
SFDC_MCP_AUTH=device_code(the default — this variable can be omitted) andSFDC_MCP_CLIENT_ID=<the consumer key from step 4>.
Client credentials (unattended — for service/automation use)
Read the warning under Auth modes before using this mode — it sees everything the run-as user is granted, with no per-caller access control.
- Under OAuth Settings, check Enable Client Credentials Flow and accept the
security-risk warning. Under OAuth Scopes, add
apiand move it to Selected OAuth Scopes. - Save, then from the app's Manage page → Edit Policies → under Client Credentials Flow, set Run As to an integration user with exactly the object- and field-level permissions you want this server to have — see the warning above.
- On the app's detail page, reveal and copy the Consumer Key and Consumer
Secret — these are
SFDC_MCP_CLIENT_IDandSFDC_MCP_CLIENT_SECRET. - Client credentials flow requires a My Domain login URL —
login.salesforce.comandtest.salesforce.comaren't accepted for this flow. Find yours under Setup → My Domain (it looks likehttps://your-domain.my.salesforce.com); setSFDC_MCP_LOGIN_URLto it. - Set
SFDC_MCP_AUTH=client_credentials,SFDC_MCP_CLIENT_ID,SFDC_MCP_CLIENT_SECRET, andSFDC_MCP_LOGIN_URLfrom the steps above.
Tools
| Tool | Parameters | Returns |
|---|---|---|
soql_query |
query: str, limit: int = 25 |
Up to limit records from a read-only SOQL SELECT. Anything else (UPDATE, DELETE, leading comments hiding a non-SELECT statement, etc.) is rejected before any request is sent. |
get_record |
sobject: str, record_id: str, fields: str = "" |
One record by Id, optionally limited to a comma-separated fields list. |
search |
term: str, sobjects: str = "", limit: int = 25 |
Cross-object full-text search (name/email/phone fields), optionally scoped to a comma-separated sobjects list. |
describe_sobject |
sobject: str |
Every field's name, type, and label, with up to 10 picklist values shown per picklist field. |
list_sobjects |
(none) | Every queryable object (standard and custom) the signed-in identity can see, as name — label pairs. |
One line of SOQL to get you started: SELECT Id, Name FROM Account WHERE CreatedDate = LAST_N_DAYS:7 LIMIT 10. list_sobjects and describe_sobject are good starting
points for exploring an org you haven't queried before.
Auth modes
device_code (interactive) |
client_credentials (unattended) |
|
|---|---|---|
| Who signs in | A human, via browser device-code login | Nobody — the Connected/External Client App itself |
| Effective access | Whatever the signed-in user can see — object/field-level security and sharing rules respected by construction | Whatever the run-as user configured on the app can see, with no per-caller access control — the server reads what that one identity reads, regardless of who's asking through the MCP client |
| Required env vars | SFDC_MCP_CLIENT_ID |
SFDC_MCP_CLIENT_ID, SFDC_MCP_CLIENT_SECRET, SFDC_MCP_LOGIN_URL (a My Domain URL) |
| Use for | Interactive use — Claude Desktop, Claude Code, a human at a terminal | Service/automation scenarios only, where that run-as user's fixed, broad view of the org is an accepted tradeoff |
client_credentials bypasses per-caller access control entirely — every tool call
sees exactly what the run-as user configured on the app can see, whoever is actually
asking through the MCP client. Scope that user's permission set as narrowly as the
integration allows, and use this mode only for service scenarios, never as a
convenience shortcut for interactive use.
Environment variables
All settings are prefixed SFDC_MCP_ and can be set in the environment or a .env
file (see .env.example).
| Variable | Default | Purpose |
|---|---|---|
SFDC_MCP_AUTH |
device_code |
Auth mode: device_code or client_credentials. |
SFDC_MCP_LOGIN_URL |
https://login.salesforce.com |
OAuth login endpoint. Sandboxes use https://test.salesforce.com; client_credentials requires a My Domain URL instead of either. |
SFDC_MCP_CLIENT_ID |
(unset, required) | Connected/External Client App's consumer key. |
SFDC_MCP_CLIENT_SECRET |
(unset) | Consumer secret; required for client_credentials only. |
SFDC_MCP_API_VERSION |
62.0 |
Salesforce REST API version. |
SFDC_MCP_ITEM_LIMIT |
25 |
Default max records/results returned per call before "showing N of M" is reported. |
SFDC_MCP_TIMEOUT_SECONDS |
30.0 |
HTTP timeout (seconds) per Salesforce request. |
SFDC_MCP_TOKEN_CACHE_PATH |
~/.sfdc-mcp/token_cache.json |
Where the device-code token cache is persisted (mode 600). |
The Salesforce instance URL is never a setting here — it comes back from the OAuth
token response (instance_url) once signed in.
API limits
Salesforce enforces a per-org daily API request allocation. This server tracks the
Sforce-Limit-Info header off every response and appends a usage note to a tool's
result once daily usage crosses 90% (e.g. Salesforce API usage: 14500/15000 daily calls.), so a client sees the ceiling coming rather than hitting it mid-session. A
429/503 is retried honoring Retry-After (clamped to at most 60s, up to 3
retries, exponential 1→2→4s backoff otherwise); a 403 with
REQUEST_LIMIT_EXCEEDED (the daily allocation already exhausted) returns an
actionable sentence about the 24-hour reset window, not a stack trace.
Security notes
- Read-only by construction. The HTTP client exposes
GETandPOST, but there's no write call to the Salesforce data API for a tool to call even by mistake — the only POST anywhere is the OAuth token exchange (/services/oauth2/token), which creates no records.soql_queryadditionally rejects any query whose first keyword isn'tSELECT. - Token cache on disk. The device-code token cache (it holds a refresh token, not
just an access token) is written to
~/.sfdc-mcp/token_cache.jsonat mode600, created with that mode rather than chmod'd after the fact. - API-limit awareness. See API limits — usage is surfaced before the daily ceiling is hit, never as a stack trace.
- Not affiliated with, endorsed by, or sponsored by Salesforce, Inc.
Development
Requires Python 3.11+ and uv.
uv sync
uv run pytest -q
uv run ruff check .
No live Salesforce org is needed for the test suite — the REST API is faked at the
httpx.MockTransport boundary for unit tests, and a real stdio round-trip runs against
an in-process fake Salesforce API in tests/integration/. See
docs/manual-verification.md for the live-org check a
maintainer runs before releases.
Contributing
Contributions welcome — see CONTRIBUTING.md for dev setup and PR expectations, and SECURITY.md for reporting vulnerabilities privately.
Not affiliated with, endorsed by, or sponsored by Salesforce, Inc.
Part of InoGen's open-source portfolio: kilnworks (self-hostable RAG assistant) and the read-only MCP connectors m365, servicenow, salesforce, and hubspot.
Built and maintained by InoGen.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sfdc_mcp_server-0.1.0.tar.gz.
File metadata
- Download URL: sfdc_mcp_server-0.1.0.tar.gz
- Upload date:
- Size: 1.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.29 {"installer":{"name":"uv","version":"0.9.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8191cd45d9af9f8a367e5a7c8999ada185b98cc38435ca140328a4e6b44a766
|
|
| MD5 |
79fd6540b5e06c8a633a6f02bed5201b
|
|
| BLAKE2b-256 |
5c3879ed93ba1585b7ef6b546026d5dc3a1687fb247c9d1c9a9ebc7eb4148f92
|
File details
Details for the file sfdc_mcp_server-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sfdc_mcp_server-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.29 {"installer":{"name":"uv","version":"0.9.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa369dad9cb36afcaa06657459552d067e5e4957c206a178552d32aa7af6929c
|
|
| MD5 |
c3df394832b4570efd2af7d62e476028
|
|
| BLAKE2b-256 |
717dea2268e09f6ff8f74263d2cda9df41d10a6fd0beea23e0eec270dd2eb6fe
|