A read-only Model Context Protocol (MCP) server for Salesforce — SOQL queries, SOSL full-text search, saved report execution, schema discovery, and bulk record fetch — deployable on Google Cloud Run with Google OAuth.
salesforce-mcp exposes your Salesforce org as a set of structured read-only tools that any MCP-compatible client (Claude Desktop, Claude.ai remote connectors, etc.) can call conversationally.
Design principles:
- Strictly read-only.
SalesforceClientexposes zero write methods.sanitizeSOQLblocksINSERT,UPDATE,DELETE,MERGE,UPSERT, andUNDELETEat the validation layer, and the integration user's Salesforce profile enforces read-only permissions as the primary control. No amount of prompt engineering can trigger a write. - Dedicated integration user. The server authenticates via Salesforce's OAuth Client Credentials flow using a named integration user with a read-only profile — not a shared admin account. This limits blast radius, enables per-user audit trails in Salesforce, and makes it easy to revoke access.
- SOC 2-style audit logging. Every tool call logs the caller's email, the tool name, relevant query details (with string literals redacted to prevent PII in logs), and duration. Bulk record access (
get_records) emits dedicated audit entries. - Dual transport. Runs as a local stdio server for Claude Desktop or as an authenticated HTTP server on Cloud Run for shared/remote deployments.
| Tool | Description |
|---|---|
query_records |
Execute a SOQL SELECT query. Supports pagination via cursor (up to 5,000 records per page). |
search_records |
SOSL full-text search across Salesforce objects. Returns up to 200 records per object. |
get_record |
Fetch a single record by SObject type and ID, with optional field selection. |
get_records |
Batch-fetch up to 200 records by ID in one call. Missing IDs return stub errors rather than failing the call. |
list_objects |
List all queryable SObject types in the org (e.g. Account, Contact, Opportunity). |
describe_object |
Inspect an SObject's fields, types, relationships, and picklist values. |
list_reports |
Discover saved reports the integration user can access, with name/folder filters. |
describe_report |
Inspect a report's columns and current filter criteria before running it. |
run_report |
Execute a saved report by ID. Supports filterOverrides to run with different filter values without editing the saved report. Detail rows capped at 2,000. |
get_current_user |
Show the authenticated Salesforce integration user's info and org ID. |
get_api_limits |
Show current Salesforce API usage and daily limits. |
MCP Client (Claude Desktop / Claude.ai)
│
│ stdio (local) ─── or ─── HTTPS Bearer (remote)
│
┌───────▼───────────────────────────────────────────────────┐
│ salesforce-mcp │
│ │
│ Transport layer │
│ ├── StdioServerTransport (local dev / Claude Desktop) │
│ └── StreamableHTTPServerTransport (Cloud Run) │
│ ├── Google OAuth token verification │
│ │ ├── Domain check (@yourdomain.com only) │
│ │ └── Audience check (aud === GOOGLE_CLIENT_ID) │
│ ├── Per-user rate limiting (60 req/min) │
│ └── CORS (claude.ai origins only) │
│ │
│ MCP Server (per-request in HTTP mode) │
│ └── 11 read-only tools │
│ │
│ SalesforceClient (jsforce wrapper) │
│ ├── Client Credentials OAuth (server-to-server) │
│ ├── Token refresh with mutex (no thundering herd) │
│ ├── Exponential backoff retry (429/5xx) │
│ ├── 30s request timeout │
│ └── TTL caches (schema: 30 min, records: 2 min) │
└───────────────────────────────────────────────────────────┘
│
│ REST API (SOQL / SOSL / Reports / Metadata)
│
┌───────▼───────┐
│ Salesforce │
│ (read-only │
│ integration │
│ user) │
└───────────────┘
In Cloud Run mode every request must carry a Google OAuth Bearer token. verifyGoogleToken in src/auth.ts validates it via Google's tokeninfo endpoint, enforcing three checks:
- Email domain — only
@yourdomain.comaccounts (configured viaALLOWED_DOMAIN). - Audience (
aud) — the token must have been issued for this specific OAuth client (GOOGLE_CLIENT_ID), not reused from another Google-integrated app. - Email verified — Google must confirm the email is verified.
Tokens are cached by SHA-256 hash (not the raw token) for up to 60 seconds, so rapid MCP sub-calls within a single tool invocation don't hammer Google, while revoked tokens (e.g. offboarded employees) expire quickly.
Uses OAuth 2.0 Client Credentials flow — a server-to-server flow requiring no user interaction or certificate rotation. The Connected App in Salesforce must have "Enable Client Credentials Flow" enabled and the integration user's profile assigned.
The /register endpoint implements RFC 7591 so MCP clients (like Claude.ai) can self-register. Registrations are origin-checked (claude.ai only) and rate-limited (10/minute globally).
| Layer | Technology |
|---|---|
| Language | TypeScript 5.x, Node.js 22 |
| MCP SDK | @modelcontextprotocol/sdk 1.x |
| HTTP server | Express 5 |
| Salesforce client | jsforce 3.x |
| Validation | Zod 4, custom SOQL/SOSL sanitizers |
| Testing | Vitest |
| Container | Docker (multi-stage, non-root runtime) |
| Deploy | Google Cloud Build + Cloud Run |
| Auth | Google OAuth (HTTP mode), SF Client Credentials |
- Node.js 22+
- A Salesforce org (sandbox or production)
- A Salesforce Connected App configured for Client Credentials flow with a read-only integration user
- (HTTP mode) A Google Cloud project with a configured OAuth 2.0 client
git clone https://github.com/micahyee415/salesforce-mcp-server
cd salesforce-mcp-server
npm installCopy .env.example to .env and fill in your values:
cp .env.example .env| Variable | Required | Description |
|---|---|---|
SF_CLIENT_ID |
Yes | Consumer Key from the Salesforce Connected App |
SF_CLIENT_SECRET |
Yes | Consumer Secret from the Salesforce Connected App |
SF_INSTANCE_URL |
Yes | Your Salesforce instance URL (e.g. https://your-org.my.salesforce.com) |
SF_LOGIN_URL |
Yes | https://login.salesforce.com (prod) or https://test.salesforce.com (sandbox) |
SF_API_VERSION |
No | Salesforce API version, defaults to 62.0 |
PORT |
HTTP mode | Set to 8080 to enable HTTP mode (Cloud Run sets this automatically) |
ALLOWED_DOMAIN |
HTTP mode | Email domain to restrict access, e.g. example.com |
SERVER_URL |
HTTP mode | Public URL of this service, e.g. https://your-service.example.com |
GOOGLE_CLIENT_ID |
HTTP mode | Google OAuth 2.0 client ID |
GOOGLE_CLIENT_SECRET |
HTTP mode | Google OAuth 2.0 client secret |
The server also looks for a user-space config at ~/.config/salesforce-mcp/.env — useful for local development without modifying the project root.
- In Salesforce Setup, go to App Manager → New Connected App.
- Enable OAuth settings, add the
apiscope. - Enable Client Credentials Flow and assign the integration user.
- Create or reuse a Salesforce user with a read-only profile (System Administrator is not required and not recommended). Assign it to the Connected App as the "Run As" user.
npm run build
npm startIn stdio mode, SF_USERNAME is used as the audit log caller identity. No PORT variable should be set.
Connect via Claude Desktop by adding to your claude_desktop_config.json:
{
"mcpServers": {
"salesforce": {
"command": "node",
"args": ["/absolute/path/to/salesforce-mcp/dist/index.js"]
}
}
}PORT=8080 npm startVerify the server is up:
curl http://localhost:8080/healthnpm run deployThis runs cloudbuild.yaml via gcloud builds submit, which:
- Runs
npm audit --audit-level=high(fails the build on critical/high CVEs) - Builds the Docker image
- Pushes to Container Registry
- Deploys to Cloud Run (
us-central1) - Restores the
allUsersinvoker binding (Cloud Run resets IAM on every deploy)
Set your GCP project in deploy.sh:
PROJECT="your-gcp-project"Once deployed, connect any MCP-compatible client to:
https://your-service.example.com/mcp
For Claude.ai remote connectors, use the MCP URL above. The server implements RFC 9728 OAuth discovery at /.well-known/oauth-protected-resource and RFC 7591 dynamic client registration at /register — Claude.ai handles the OAuth flow automatically.
- Read-only by design. The
SalesforceClientclass exposes no create/update/delete methods. SOQL input is validated to reject DML keywords and semicolons before any API call. A test asserts no write methods exist on the client. - Integration user with minimal permissions. The Connected App runs as a named user with a read-only Salesforce profile — not a shared admin credential.
- OAuth domain + audience enforcement. In HTTP mode, tokens must come from a verified
@yourdomain.comGoogle account and must have been issued specifically for this OAuth client (audience check). Tokens issued for other apps are rejected even if the email domain matches. - Token cache uses SHA-256 hashes. Raw OAuth tokens are never stored in memory; only their hash is used as the cache key.
- Audit logging. Every tool invocation logs the caller's email, tool name, and relevant metadata. SOQL/SOSL string literals are redacted before logging to prevent PII leakage. Bulk record access (
get_records) emits dedicated audit log entries. - Per-user rate limiting. 60 requests/minute per authenticated user in HTTP mode.
- Security headers.
Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options, andCache-Control: no-storeon all responses. - Non-root container. The Docker image runs as a non-root user to limit blast radius if the container is compromised.
- Dependency scanning.
npm audit --audit-level=highruns in Cloud Build and fails the pipeline on critical or high CVEs.
See SECURITY.md for the vulnerability reporting policy.
No license file is present in this repository. All rights reserved unless otherwise stated.