Skip to main content
Glama
simple-salesforce-mcp

simple-salesforce-mcp

by JustParent

simple-salesforce-mcp

Tests

A lightweight Salesforce MCP server. Talks straight to the Salesforce REST API over stdio with two runtime dependencies (mcp, httpx) — built as a fast-cold-start replacement for the official @salesforce/mcp DX server, whose dependency tree is too heavy for sandboxed environments.

Tools

Tool

Writes?

Notes

run_soql_query

no

Raw SOQL escape hatch with pagination (next_url)

search_records

no

Cross-object full-text search (parameterized search API)

get_record

no

Fetch one record by Id, optional field list

create_record

yes

Create a record

update_record

yes, confirm-gated

Requires literal confirm=true

delete_record

yes, confirm-gated

Requires literal confirm=true

describe_object

no

Trimmed object metadata (fields, types, required, picklists)

list_objects

no

Org objects, system noise filtered by default

get_org_info

no

Current user, org, instance URL, API version

list_record_files

no

Files, legacy Attachments, and (for Cases) email attachments on a record

download_file

no

Streams a file to disk and returns a link or path, never the bytes

prepare_file_upload

yes

Hosted sandboxes only: reserves a single-use upload slot and returns a link to PUT the file to

upload_file

yes, local_path is confirm-gated

Attaches a file to a record as a Salesforce File; the bytes never pass through the call

Every tool carries MCP annotations (readOnlyHint / destructiveHint / idempotentHint). update_record and delete_record refuse to act unless the call includes confirm: true, and the refusal message instructs the model to present the exact change to the user and get approval first. upload_file is gated the same way when given a local_path, since that reads a model-chosen path on the user's machine. SOQL itself cannot modify data, so the escape hatch stays read-only.

Related MCP server: Salesforce MCP Server

Authentication

The server performs no OAuth flow itself; it consumes an existing access token, resolved on every tool call (so an external process may rotate the token at any time):

  1. Salesforce CLI auth store under $HOME:

    • ~/.sf/config.json → {"target-org": "<username>"} (or legacy ~/.sfdx/sfdx-config.json → {"defaultusername": ...})

    • ~/.sfdx/<username>.json → {"accessToken": "...", "instanceUrl": "https://..."}

    This is the shape written by sf org login, and the shape the JustParent platform materialises into sandboxes (with the platform refreshing the access token before each call). Refresh tokens and client secrets in these files are ignored — never read into memory, stored, or logged.

  2. Environment variables (fallback, for local/standalone use): SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL.

On a rejected token (HTTP 401 / INVALID_SESSION_ID) the server re-reads the auth store once and retries, then reports that the connection needs to be re-established.

The REST API version defaults to 62.0; override with SALESFORCE_API_VERSION.

Downloading files

download_file never puts file content in a tool result, so any file size works and tool-call logs only ever hold a small JSON envelope (file_name, content_type, size, sha256, plus where to get the file). It accepts ContentVersion (068…), ContentDocument (069…, the latest version is used) and Attachment (00P…) Ids, and streams the blob to disk in chunks.

  • Hosted sandbox (Harriet). When HARRIET_FILE_OUTBOX_DIR is set, the file is streamed into a hidden .staging-* directory with a .meta.json alongside, then renamed to <outbox>/<token>/<file name> once complete. token is <expires_at>.<random>: the Unix expiry time (now + HARRIET_FILE_OUTBOX_TTL_SECONDS, default 120) and 256 random bits. The result carries download_url = <base>/files/<token> (with <base> read from <outbox>/.base_url) and expires_at. The sandbox bridge serves that URL once, then deletes the file. It refuses the link once the expiry in the token has passed or the file on disk is older than the TTL, since each check works without the other, and it sweeps expired files in the background. The client fetches the link straight into its workspace, e.g. curl -fsSL -o scan.jpg "<download_url>".

  • Local (standalone). Otherwise the file is saved under SALESFORCE_DOWNLOAD_DIR (default <tmp>/simple-salesforce-mcp) and the result carries local_path.

Downloads above SALESFORCE_MAX_DOWNLOAD_BYTES (default 500 MB) are refused.

Uploading files

upload_file attaches a file (PDF, Word document, image, ...) to a record such as an Account or Case. It creates a Salesforce File (a ContentVersion) with FirstPublishLocationId set to the record, so the file appears under the record's Files with no Apex, Flow or Connected App involved. As with downloads, file content never travels through a tool call: the file streams from disk to Salesforce as multipart/form-data, and the result is a small JSON envelope (content_version_id, file_name, size, linked_to). Files above SALESFORCE_MAX_UPLOAD_BYTES (default 100 MB) are refused, and a very large file may not finish inside the platform's per-tool-call timeout.

  • Hosted sandbox (Harriet). The model's workspace is a different machine from the sandbox, so the file goes through a single-use inbox, the mirror image of the download outbox. Set HARRIET_FILE_INBOX_DIR, and the flow is:

    1. prepare_file_upload(file_name) creates <inbox>/<token>/ holding .meta.json ({"file_name": ..., "max_bytes": ...}), built under a hidden .staging-* name and renamed into place. token is <expires_at>.<random> like the outbox's, valid for HARRIET_FILE_INBOX_TTL_SECONDS (default 600). The result carries upload_token and upload_url = <base>/uploads/<token> (with <base> read from <inbox>/.base_url, which must be https://). Expired slots are swept whenever this tool runs.

    2. The client PUTs the file from its workspace, e.g. curl -fsS -T report.pdf "<upload_url>".

    3. upload_file(record_id, upload_token) streams <inbox>/<token>/<file name> to Salesforce and deletes the slot. A failed upload keeps the slot, so it can be retried until the token expires.

    The sandbox bridge must implement the PUT side (it lives outside this repo). Accept exactly one PUT /uploads/<token> per slot, refusing it if the expiry in the token has passed, if <inbox>/<token>/.meta.json does not exist, or if the slot already holds a file. Stream the body into a hidden file in the slot (refuse and discard anything above max_bytes from .meta.json), and only once it is complete rename it to <inbox>/<token>/<file_name>, taking file_name from .meta.json. upload_file only ever reads that final name, so it never sees a partial upload. Without HARRIET_FILE_INBOX_DIR a sandbox reports that uploads are not available yet.

  • Local (standalone). Otherwise the server runs on the user's machine, and upload_file reads local_path directly. Because that is a model-chosen path, the call needs confirm: true after the user has approved the exact file and record. local_path is refused whenever HARRIET_FILE_INBOX_DIR or HARRIET_FILE_OUTBOX_DIR is set, since a sandbox's disk holds the platform-written Salesforce credentials.

Running

# Standalone with env vars (get a token via: sf org display --json)
SALESFORCE_ACCESS_TOKEN=... \
SALESFORCE_INSTANCE_URL=https://yourorg.my.salesforce.com \
uvx --from git+https://github.com/JustParent/simple-salesforce-mcp simple-salesforce-mcp

Claude Desktop / generic MCP client config:

{
  "mcpServers": {
    "salesforce": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/JustParent/simple-salesforce-mcp", "simple-salesforce-mcp"],
      "env": {
        "SALESFORCE_ACCESS_TOKEN": "...",
        "SALESFORCE_INSTANCE_URL": "https://yourorg.my.salesforce.com"
      }
    }
  }
}

Smoke test (no network, prints version, credential status, and tools; exits 0):

uvx --from . simple-salesforce-mcp --test

Development

uv run pytest                    # unit tests, no network needed
uv run ruff check .              # lint
uv run ruff format .             # format (--check to verify only)
uv run mypy                      # type check (src and tests)
uv run simple-salesforce-mcp --test
npx @modelcontextprotocol/inspector uv run simple-salesforce-mcp   # interactive

CI (.github/workflows/tests.yml) runs on every push and pull request: the test suite on Python 3.10–3.13, ruff lint and format checks plus mypy, and a packaging job that installs the package the way sandboxes do (uvx --from .) to catch packaging breakage.

Security notes

  • Access-token only: the server never requests, reads, or persists refresh tokens.

  • Destructive operations (update/delete) are gated behind an explicit confirm=true argument with model-facing guidance to obtain user approval first.

  • SOQL pagination cursors (next_url) are validated to be /services/data/... paths, and object/record identifiers are validated before being placed in URLs.

  • Responses are size-capped with explicit truncation notices.

  • File downloads never travel through a tool result; hosted download links are single-use, short-lived, and unguessable, and file names are reduced to a safe single path component.

  • File uploads never carry bytes in a tool call either. In a sandbox only a file the bridge placed in a server-created, single-use slot is read (tokens must match a strict format and symlinks are refused), and a model-chosen local_path is refused there and confirm-gated everywhere else.

Available Tools

9 tools
create_recordA

Create a new Salesforce record. Use describe_object first to find the required fields and valid picklist values.

Inputs: data (required), object_type (required).

delete_recordA
DestructiveIdempotent

Permanently delete a Salesforce record. Requires confirm=true: first present the deletion to the user and get their approval.

Inputs: confirm (required), record_id (required), object_type (required).

describe_objectA
Read-onlyIdempotent

Get a Salesforce object's fields: API names, types, required flags, and picklist values. Use before writing SOQL or creating/updating records.

Inputs: object_type (required), detail.

get_org_infoA
Read-onlyIdempotent

Current Salesforce user, org id/name/type, instance URL, and API version in use. Call this first to establish context.

Inputs: no named parameters.

get_recordA
Read-onlyIdempotent

Fetch a single Salesforce record by Id.

Inputs: record_id (required), object_type (required), fields.

list_objectsA
Read-onlyIdempotent

List the org's Salesforce objects (API name and label). System noise (Share/History/Feed/ChangeEvent tables, non-queryable objects) is excluded by default.

Inputs: filter, include_system.

run_soql_queryA
Read-onlyIdempotent

Run a raw SOQL query against the Salesforce org. Read-only — SOQL cannot modify data. Use this as the escape hatch for anything the other tools don't cover: filtering, joins via relationship fields, aggregates, ORDER BY. Always include a LIMIT for exploratory queries. If a field or object name errors, check the exact API names with describe_object.

Inputs: query, next_url.

search_recordsA
Read-onlyIdempotent

Full-text search across Salesforce records (name, email, phone, and other searchable fields). Good for 'find the record for X' when you don't know the object or Id. For precise filtering use run_soql_query instead.

Inputs: search_term (required), limit, fields, object_types.

update_recordA
DestructiveIdempotent

Update fields on an existing Salesforce record. Requires confirm=true: first present the exact change to the user and get their approval.

Inputs: data (required), confirm (required), record_id (required), object_type (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 9 tools

    Disambiguation5/5

    Each tool targets a distinct operation: SOQL query, full-text search, record CRUD by Id, object metadata, and org context. The descriptions explicitly contrast run_soql_query with search_records, and get_record with run_soql_query, leaving no ambiguity.

    Naming Consistency5/5

    All tool names follow a clear verb_noun pattern in snake_case: run_soql_query, search_records, get_record, create_record, update_record, delete_record, describe_object, list_objects, get_org_info. The naming is perfectly uniform and predictable.

    Tool Count5/5

    Nine tools is a well-scoped size for a Salesforce MCP server, covering query, search, CRUD, schema introspection, and org context. Every tool has a clear purpose with no redundant or filler tools.

    Completeness5/5

    The set provides complete CRUD lifecycle coverage (create, get, update, delete), plus two read/search mechanisms, object metadata discovery, and org context. It covers the full range of common Salesforce interactions an agent would need, with no glaring dead ends.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      A focused MCP server for Salesforce CRM that exposes read/write access to standard CRM objects via six tools, designed for multi-tenant gateway deployments.
      39 npm
      MIT
    • F
      license
      B
      quality
      D
      maintenance
      A customizable MCP server for integrating Salesforce APIs with GenAI applications, supporting SOQL queries, record CRUD, metadata access, and more.
      13
      8
      -
    • A
      license
      A
      quality
      C
      maintenance
      Enables SOQL queries, SOSL search, and record CRUD operations on a single Salesforce org via MCP, with optional read-only mode.
      9
      209 npm
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Enables AI agents to interact with Salesforce through MCP, supporting queries, records, metadata, and bulk operations with flexible OAuth authentication.
      16
      MIT