Skip to main content
Glama
gokeel

sf-mcp-proxy

by gokeel

sf-mcp-proxy (Python)

Disclaimer. Independent, unofficial project — not affiliated with, endorsed by, or sponsored by Salesforce, Inc. "Salesforce" and related marks are trademarks of Salesforce, Inc.

Salesforce's Hosted MCP servers (sobject-reads, sobject-all, flows, invocable-actions, …) sit behind OAuth 2.0, and Salesforce does not support Dynamic Client Registration — so every client must be pre-wired with a Consumer Key from a Salesforce External Client App and run the full Authorization Code + PKCE browser flow before it can call a single tool.

sf-mcp-proxy does that once and then gives you:

  1. an importable library — an authenticated, in-process mcp.ClientSession that LangChain, LangGraph, and Google ADK can build agents on with no subprocess and no second OAuth layer;

  2. a CLI to log in, inspect, and call tools;

  3. a proxy (sf-mcp-proxy proxy) that re-exposes one Salesforce MCP server as a plain MCP server (stdio or Streamable HTTP), optionally behind its own OAuth;

  4. a chat loop — an LLM in a tool-use loop over one server's tools.

This is a Python port of the TypeScript sf-mcp-proxy. The token cache directory, cache-file naming, StoredSession JSON shape, and encryption-at-rest envelope are byte-compatible with the TS CLI, so the two share ~/.sf-mcp-proxy/.

The OAuth machinery (RFC 9728 discovery, PKCE, the resource indicator, token exchange/refresh) is handled by the official mcp SDK; this package supplies a Salesforce-specific OAuthClientProvider.

API scope. Salesforce owns the Hosted MCP API and can change it without notice; behaviour may drift on later releases and support is best-effort.


Install

pip install sf-mcp-proxy                 # library + CLI + proxy
pip install "sf-mcp-proxy[langchain]"    # + LangChain / LangGraph adapters
pip install "sf-mcp-proxy[adk]"          # + Google ADK toolset
pip install "sf-mcp-proxy[chat]"         # + `sf-mcp-proxy chat`
pip install "sf-mcp-proxy[all]"

Python 3.10–3.13.


Related MCP server: SFCC MCP Server

1. Create a Salesforce External Client App (ECA)

Do this once, in the org you want to connect to.

  1. Setup → External Client App Manager → New External Client App.

  2. Basic Information — name it, set a contact email, Distribution State Local.

  3. API (Enable OAuth Settings)Enable OAuth:

    • Callback URL: http://localhost:8000/callback (must exactly match OAUTH_REDIRECT_URI).

    • OAuth Scopes: Access MCP servers (mcp_api) and Perform requests at any time (refresh_token).

    • Check Require PKCE.

    • Uncheck Require secret for Web Server Flow / Refresh Token Flow to run as a public client (PKCE only). If you leave them checked, set SF_CONSUMER_SECRET.

  4. Policies → OAuth Policies: set Permitted Users, and Issue JWT-based access tokens for named users (so whoami can decode claims).

  5. Settings → OAuth Settings → Consumer Key — copy it.

  6. Wait ~30 min for the ECA to propagate before first use.


2. Configure

Everything can be passed to the library explicitly, or read from the environment / a .env file in the working directory.

Variable

Notes

SF_CONSUMER_KEY

Required. Consumer Key from the ECA.

SF_CONSUMER_SECRET

Only if the ECA still requires a secret (→ client_secret_post).

SF_ENV

platform (production, default) or sandbox.

OAUTH_REDIRECT_URI

Must match an ECA Callback URL. Default http://localhost:8000/callback.

OAUTH_CALLBACK_PORT

Loopback port. Default 8000.

SF_LOGIN_URL

Discovery fallback. Default https://login.salesforce.com.

SF_AUTH_SERVER_URL

Your My Domain host (e.g. https://acme.my.salesforce.com) — bypasses discovery.

SF_TOKEN_PASSPHRASE

Encrypt the token cache at rest (scrypt + AES-256-GCM).

HOST PORT PUBLIC_URL SF_PROXY_AUTH_TOKEN

proxy --http defaults.

SFCHAT_*, ANTHROPIC_API_KEY, …

chat (see the provider table).

servers.json (searched in the working directory, then ~/.sf-mcp-proxy/servers.json). The reliable way is to copy the exact URL from Salesforce Setup → MCP servers into url:

{
  "servers": [
    { "name": "sobject-reads", "url": "https://api.salesforce.com/platform/mcp/v1/sandbox/platform/sobject-reads" },
    { "name": "flows", "env": "platform" }
  ]
}

If url is omitted the URL is built as https://api.salesforce.com/platform/mcp/v1/{group}/{name} (production) or …/mcp/v1/sandbox/{group}/{name} (sandbox); group defaults to platform. When absent entirely, a built-in default list is used (sobject-reads, sobject-all, flows, …). -s/--server also accepts a full URL directly.


3. CLI

sf-mcp-proxy login -s sobject-reads -e sandbox   # browser OAuth, caches tokens
sf-mcp-proxy tools -s sobject-reads
sf-mcp-proxy call query -s sobject-reads -a '{"soql":"SELECT Id, Name FROM Account LIMIT 3"}'

Command

What it does

servers

list configured servers + resolved URLs

login -s <name|url> [-e platform|sandbox]

browser OAuth + PKCE; prints the cache path

logout -s …

delete the cached session file

whoami -s …

decode & print the cached access token's JWT claims

tools -s … [--json]

list tools, or raw tool defs as JSON

toolspec -s …

print { "tools": [...] } (warns on stderr past 10 KB)

call <tool> -s … -a '<json>' [--json]

call one tool, print the result

repl -s …

interactive: tools, schema <tool>, call <tool> [json], resources, read <uri>, prompts, help, quit

chat -s … [-p <provider>] [-m <model>] [--base-url <url>] [-y] [--confirm-all]

LLM tool-use loop

proxy -s … [--http] [--host …] [--port …] [--auth-token …] [--oauth] [--public-url …] [--no-login]

re-expose one server as MCP

oauth-clients -s … [--revoke <id>]

list / revoke clients registered against proxy --oauth

-s/--server may be omitted if exactly one server is configured. -e/--env overrides the tier for that run.


4. Library

import asyncio
from sf_mcp_proxy import SalesforceMCP


async def main():
    # One-time interactive login (opens a browser, caches tokens).
    await SalesforceMCP.login("sobject-reads", env="sandbox")

    # Authenticated session. Uses the cached token, auto-refreshes, and by
    # default does NOT open a browser (interactive=False).
    async with SalesforceMCP.connect("sobject-reads", env="sandbox") as sf:
        tools = await sf.list_tools()  # list[mcp.types.Tool]
        result = await sf.call_tool("query", {"soql": "SELECT Id FROM Account LIMIT 1"})
        session = sf.session  # raw mcp.ClientSession


asyncio.run(main())

connect() accepts the same server spec as the CLI (name | bare name | full URL), plus env=, group=, and explicit consumer_key= / consumer_secret= / settings= / passphrase= overrides so a library caller never needs a .env file. list_tools / call_tool transparently reconnect once on an authorization failure (a port of the TS withReauth). SalesforceMCP.connect_sync(...) is a blocking facade for notebooks and the CLI.


5. Framework integrations

LangChain / LangGraph (sf-mcp-proxy[langchain])

from sf_mcp_proxy.integrations.langchain import SalesforceToolkit

async with SalesforceToolkit("sobject-reads", env="sandbox") as tools:
    # `tools` is list[langchain_core.tools.BaseTool] — pass to any agent
    ...
from sf_mcp_proxy.integrations.langgraph import create_salesforce_agent

agent = await create_salesforce_agent(
    model="anthropic:claude-opus-5",
    server="sobject-reads",
    env="sandbox",
)
async for step in agent.astream({"messages": [("user", "how many open opps?")]}):
    ...
await agent.aclose()

load_salesforce_tools(sf) wraps a live session you already own. Tier B — salesforce_stdio_connection(...) — returns a langchain-mcp-adapters connection pointed at a spawned sf-mcp-proxy proxy.

Google ADK (sf-mcp-proxy[adk])

from google.adk.agents import LlmAgent
from sf_mcp_proxy.integrations.adk import SalesforceToolset

toolset = SalesforceToolset(server="sobject-reads", env="sandbox")
agent = LlmAgent(model="gemini-2.5-pro", name="sf", tools=[toolset])
...
await toolset.close()

Tier B — salesforce_stdio_connection_params(...) returns StdioConnectionParams for ADK's own McpToolset.

Runnable examples are in examples/.


6. Proxy mode

sf-mcp-proxy proxy re-exposes one Salesforce MCP server as a plain MCP server. The proxy owns the Salesforce OAuth (the token cached by login), so downstream clients need no Salesforce auth of their own. Every request runs as the Salesforce user who authorized the proxy (one identity per proxy process).

stdio — for clients that spawn a subprocess:

{
  "mcpServers": {
    "salesforce-sobject-reads": {
      "command": "sf-mcp-proxy",
      "args": ["proxy", "-s", "sobject-reads", "-e", "sandbox", "--no-login"]
    }
  }
}

Streamable HTTP — for clients that take a URL:

sf-mcp-proxy proxy -s sobject-reads -e sandbox --no-login \
  --http --port 9000 --auth-token "$(openssl rand -hex 16)"
# → endpoint: http://127.0.0.1:9000/mcp   (send: Authorization: Bearer <token>)

--host/--port/--public-url/--auth-token fall back to $HOST/$PORT/$PUBLIC_URL/$SF_PROXY_AUTH_TOKEN. For a remote client, bind --host 0.0.0.0 and put a TLS reverse proxy in front (the proxy speaks plain HTTP), and always set --auth-token or --oauth.

--oauth

Runs a minimal, spec-shaped OAuth 2.0 Authorization Server + Resource Server on the same port: RFC 9728 / RFC 8414 discovery, RFC 7591 Dynamic Client Registration (POST /register), Authorization Code + PKCE (S256), and an Approve/Deny page on every /authorize request. A spec-compliant client discovers everything from the 401 it gets hitting /mcp. Manage clients with sf-mcp-proxy oauth-clients -s … [--revoke <id>]. Behind a reverse proxy, pass --public-url https://your.host so the discovery documents advertise the right address. This layer only gates who reaches the proxy — every approved client still acts as the one identity that ran login.

Docker

A Dockerfile runs proxy --http --no-login and reads $HOST, $PORT, $PUBLIC_URL, $SF_PROXY_AUTH_TOKEN from the environment.

docker build -t sf-mcp-proxy .
docker run --rm -p 8080:8080 \
  -e SF_CONSUMER_KEY=... -e SF_PROXY_AUTH_TOKEN=... -e SF_TOKEN_PASSPHRASE=... \
  -v ~/.sf-mcp-proxy:/home/app/.sf-mcp-proxy \
  sf-mcp-proxy -s sobject-reads -e sandbox

There's no browser in the container, so --no-login is on: run sf-mcp-proxy login once on your machine and make the token cache available to the container. On a stateless host (Cloud Run, etc.) mount a persistent volume at /home/app/.sf-mcp-proxy (the Salesforce refresh token rotates and is written back), set SF_TOKEN_PASSPHRASE from a secret, and pin --max-instances 1 --min-instances 1 (a Streamable HTTP session is held in memory by the instance that created it).


7. Chat mode (sf-mcp-proxy[chat])

sf-mcp-proxy chat -s sobject-reads
you> how many open opportunities are closing this quarter?
⚙  soqlQuery({"soql":"SELECT COUNT() FROM Opportunity WHERE IsClosed = false AND ..."})
   run soqlQuery? [y/N] y

Tool calls whose name looks like a write (create/update/delete/run/…) prompt for confirmation. -y approves everything; --confirm-all prompts for every call. In-chat commands: /tools, /reset, /quit.

--provider

Base URL

API key env

Default model

anthropic (default)

ANTHROPIC_API_KEY

claude-opus-5

openai

api.openai.com/v1

OPENAI_API_KEY

gpt-4.1

deepseek

api.deepseek.com

DEEPSEEK_API_KEY

deepseek-chat

kimi / moonshot

api.moonshot.ai/v1

MOONSHOT_API_KEY

kimi-k2-0711-preview

qwen

dashscope-intl.aliyuncs.com/compatible-mode/v1

DASHSCOPE_API_KEY

qwen-plus

openrouter

openrouter.ai/api/v1

OPENROUTER_API_KEY

— (set --model)

custom

--base-url / SFCHAT_BASE_URL

SFCHAT_API_KEY

— (set --model)

SFCHAT_API_KEY works as the key for any provider; SFCHAT_MODEL / SFCHAT_BASE_URL persist per-run overrides. The model must support tool calling.


How auth works

  1. First request to the MCP server returns 401. The SDK discovers https://api.salesforce.com/.well-known/oauth-protected-resource/…, which names the org's authorization server.

  2. It builds the authorize URL with PKCE (S256), the mcp_api refresh_token scopes, and resource=<server URL> (RFC 8707), then opens the browser.

  3. Salesforce redirects to http://localhost:<port>/callback with a code; a one-shot loopback server catches it and the SDK exchanges it for tokens.

  4. Tokens are written to ~/.sf-mcp-proxy/<sha256(url)[:32]>.json (dir 0700, files 0600; encrypted if SF_TOKEN_PASSPHRASE is set). Later runs reuse and silently refresh the access token.

SF_AUTH_SERVER_URL (a My Domain host) bypasses discovery and uses {base}/services/oauth2/{authorize,token,revoke} directly.

Troubleshooting

Symptom

Fix

redirect_uri_mismatch

The ECA Callback URL must byte-for-byte equal OAUTH_REDIRECT_URI.

invalid_client_id right after creating the ECA

Wait ~30 min for propagation.

Browser shows the wrong org / login

Set SF_AUTH_SERVER_URL to your My Domain, or SF_LOGIN_URL=https://test.salesforce.com for sandboxes.

Port 8000 is not available

Change OAUTH_CALLBACK_PORT and the ECA Callback URL.

No cached token … from connect()

Run sf-mcp-proxy login -s <name> first — non-interactive mode never opens a browser.

Stuck / bad token

sf-mcp-proxy logout -s <name>, then login again.


Development

uv sync --all-extras --group dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run mypy sf_mcp_proxy

License

MIT © Ocky Harliansyah — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server for Salesforce that exposes CLI, REST, Connect, Data 360, Bulk 2.0, and Einstein Models APIs as tools for any MCP-compatible client to manage orgs, data, and metadata.
    Apache 2.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol server that connects AI assistants to Salesforce orgs, enabling querying, searching, creating, updating, and managing Salesforce data through natural language via any MCP-compatible client.
    29 npm
    MIT
  • F
    license
    C
    quality
    C
    maintenance
    A Model Context Protocol server for Salesforce development workflows, enabling org lookup, metadata deploy/retrieve, SOQL queries, Apex tests, and permission set assignment.
    72
    -