~/revenueeng/blog/salesforce-hosted-mcp-multi-client
12 min read
// blog / salesforce + ai

Wire Salesforce Hosted MCP Into Claude Code, Cursor, Codex, and ChatGPT

>One External Client App. Four AI clients. Same prompt, same five rows back from your Salesforce org.

//byWarren Walters··companion video on YouTube

Salesforce's hosted MCP servers are generally available, and every modern AI client now speaks streamable HTTP over OAuth. Which means the same Salesforce side that wires up Claude Code also wires up Cursor, Codex CLI, and ChatGPT. You build the External Client App once. The only thing that changes per client is the callback URL and the format of the config file.

This post is the paste-ready companion to the video. Configs are verbatim from what I run locally. Replace the placeholders, work top to bottom, and you'll have all four clients reading from the same Salesforce org by the end.

## 01_why_one_eca.md

Why one External Client App works for all four clients

The shift in 2026 is that hosted MCP is just an HTTP endpoint with OAuth in front of it. Every modern AI client (ChatGPT custom connectors, Claude Code, Cursor, Codex CLI) talks to MCP over streamable HTTP. That collapses what used to be four different integration stories into one.

  • One External Client App (ECA) in Salesforce, with one set of OAuth scopes.
  • One MCP URL per server you want to expose (sobject-all, flows, invocable-actions, etc.).
  • One callback URL per client, all pasted into the same ECA.
  • A config file per client. The shape changes, the URL doesn't.

The Salesforce side gets set up once. The client side is copy-paste from here on.

## 02_salesforce_side.md

Set up the Salesforce side, once

Two ways to do this: deploy a pre-built ECA from metadata, or hand-create one in Setup. The deploy path is faster if you have the metadata files; Setup is fine if you're building from scratch.

Path A: deploy the pre-built ECA

The deployable metadata lives in salesforce-content/force-app/main/default/ and covers four file types: the External Client App, the global OAuth settings, the OAuth settings, and the OAuth policies. Deploy them together:

terminal
cd salesforce-content/
sf project deploy start \
  --metadata ExternalClientApplication:Salesforce_Hosted_MCP \
             ExtlClntAppGlobalOauthSettings:Salesforce_Hosted_MCP_glbloauth \
             ExtlClntAppOauthSettings:Salesforce_Hosted_MCP_oauth \
             ExtlClntAppOauthConfigurablePolicies:Salesforce_Hosted_MCP_oauthPlcy \
  --target-org <alias>

After deploy, three things happen in this order:

  1. Wait 5 to 10 minutes for OAuth propagation. Trying to connect immediately gives you invalid_client_id and a confusing afternoon.
  2. Activate the hosted MCP server. Setup, then Hosted MCP Servers, then pick the server you want (sobject-all is the most flexible for demos), Activate, and link it to Salesforce_Hosted_MCP. Without this step, OAuth will succeed and the endpoint will still refuse you.
  3. Grab the Consumer Key and Consumer Secret if you need them. Only the mcp-remote shim path (Cursor fallback) needs the Consumer Key. Setup, then External Client App Manager, then Salesforce Hosted MCP, then Settings, then OAuth Settings, then Manage Consumer Details. Salesforce will ask for identity verification.

One thing worth calling out: a re-deploy to a different org generates a new Consumer Key. If you have client configs that hardcode it, update them.

Path B: hand-create the ECA

If you're building it from scratch in Setup, here are the settings that matter:

ECA configuration
LabelSalesforce Hosted MCP
API NameSalesforce_Hosted_MCP
Contact Emailyour email
Distribution StateLocal
OAuth enabledYes
OAuth scopesAccess MCP servers (mcp_api), Perform requests at any time (refresh_token, offline_access)
Callback URLsone per line (see below)
Require PKCEON
Require Secret for Web Server FlowOFF
Require Secret for Refresh Token FlowOFF
IP RelaxationRelax IP restrictions
Permitted UsersAllSelfAuthorized for v1, AdminPreApproved for team setup
Refresh Token PolicySpecific Lifetime, 365 days (or shorter)

Callback URLs (paste all of these)

All callback URLs go on the same ECA, one per line. ChatGPT generates a sixth callback URL after you create the custom connector. You'll add that one live during the ChatGPT section.

callback_urls.txt
https://claude.ai/api/mcp/auth_callback
cursor://anysphere.cursor-mcp/oauth/callback
http://localhost:8080/oauth/callback
http://localhost:3334/oauth/callback
http://localhost:1455/callback
http://localhost:8080/callback

What each one is for:

  • https://claude.ai/api/mcp/auth_callback: the Claude.ai web custom connector.
  • cursor://anysphere.cursor-mcp/oauth/callback: Cursor on the native HTTP MCP path.
  • http://localhost:8080/oauth/callback and http://localhost:3334/oauth/callback: the mcp-remote shim, default and alt ports. Versions shift, so both are worth registering.
  • http://localhost:1455/callback: Codex CLI's codex mcp login default port.
  • http://localhost:8080/callback: Claude Code when you pin it with --callback-port 8080.
## 03_urls.md

The MCP URL pattern (and the one trap)

Production and sandbox URLs share the same host. The difference is one path segment, and missing it is the number-one cause of nothing works and I don't know why.

prod
https://api.salesforce.com/platform/mcp/v1/platform/sobject-all
sandbox_or_scratch
https://api.salesforce.com/platform/mcp/v1/sandbox/platform/sobject-all

Sandbox and scratch orgs sit behind /sandbox/in the path. Production does not. If you're running a demo on a scratch org (most people are), every client config in the rest of this post needs the sandbox URL.

Other servers, swap the last path segment: sobject-reads, sobject-mutations, sobject-deletes, flows, invocable-actions, api-catalog, custom-servers, data-cloud-sql, tableau-next, prompt-builder.

## 04_chatgpt.md

Client 1: ChatGPT (custom connector)

No config file for this one. Everything happens in the browser at chatgpt.com. The thing to internalize here is that ChatGPT generates its own callback URL after you create the connector, and you have to round-trip back to the Salesforce ECA to register it. That round-trip is the mental model for every other client too: each client owns its callback, the ECA collects all of them.

  1. Settings, then Connectors, then New Custom Connector.
  2. Name: Salesforce Hosted MCP.
  3. MCP URL: paste the sandbox URL above (or prod if that's where you're going).
  4. Enable OAuth.
  5. Save. ChatGPT generates a callback URL specific to this connector. Copy it.
  6. Open the Salesforce ECA, add that callback URL to the callback URLs list, Save.
  7. Back in ChatGPT, complete the OAuth flow.
  8. New chat, enable the Salesforce connector, run the demo prompt at the bottom of this post.
## 05_claude_code.md

Client 2: Claude Code

The best dev-tool DX of the four, in my experience. Anthropic handles the OAuth callback infrastructure for you, so there's no shim to install. One command to add the server, one slash command to trigger OAuth.

terminal
claude mcp add --transport http salesforce <SANDBOX_URL>

Three scopes are available. Pick whichever fits how you want the connection to follow you around:

  • local (default): just this machine, just you.
  • --scope project: writes .mcp.json at your project root. Check it into git so teammates pick up the same connection.
  • --scope user: all your projects on this machine.

In a Claude Code session, trigger OAuth with:

claude-code-session
/mcp

For a check-in-able project config, drop this into .mcp.json at the project root:

.mcp.json
{
    "mcpServers": {
        "salesforce": {
            "type": "http",
            "url": "<SANDBOX_URL>"
        }
    }
}

If you run multiple Claude Code sessions and they clash on ephemeral ports during OAuth, pin the callback port:

terminal
claude mcp add --transport http salesforce <SANDBOX_URL> --callback-port 8080

http://localhost:8080/callbackis already in the callback URL list above, so you don't need to touch the ECA for this.

## 06_cursor.md

Client 3: Cursor

Cursor has two paths: native HTTP (try this first), and the mcp-remote shim (fallback if native trips on dynamic client registration). The config file is .cursor/mcp.json at the project root, or ~/.cursor/mcp.json globally.

Path A: native HTTP

.cursor/mcp.json
{
    "mcpServers": {
        "salesforce": {
            "url": "<SANDBOX_URL>"
        }
    }
}

Path B: the mcp-remote shim

If native trips, fall back to mcp-remote. This is also where the Consumer Key comes in (the shim needs it for static client info):

.cursor/mcp.json
{
    "mcpServers": {
        "salesforce": {
            "command": "npx",
            "args": [
                "-y",
                "mcp-remote@latest",
                "<SANDBOX_URL>",
                "8080",
                "--static-oauth-client-info",
                "{\"client_id\":\"<CONSUMER_KEY>\",\"client_secret\":\"\"}"
            ]
        }
    }
}

Two gotchas worth catching before they bite. First, a missing comma anywhere in .cursor/mcp.json kills every server on the file, silently. Validate the JSON before wondering why nothing connects:

terminal
cat ~/.cursor/mcp.json | python3 -m json.tool

Second, after any misconfig with mcp-remote, clear the cached OAuth state or you'll chase ghost errors:

terminal
rm -rf ~/.mcp-auth
npm cache clean --force

To reach the UI: Settings, then Tools and MCP, then New MCP Server. The file path above is where Cursor actually persists what you configure there.

## 07_codex_cli.md

Client 4: Codex CLI

Codex CLI uses TOML, not JSON. Config lives at ~/.codex/config.toml (user-level) or .codex/config.toml (project, trusted dirs only, keep reading).

Native OAuth block

~/.codex/config.toml
[mcp_servers.salesforce]
url = "<SANDBOX_URL>"

Trigger OAuth:

terminal
codex mcp login salesforce

Optional: trim the tool surface for autonomous runs

sobject-allis the most flexible server, which also means it's the most surface. For autonomous Codex runs, filtering keeps the agent focused:

~/.codex/config.toml
[mcp_servers.salesforce]
url = "<SANDBOX_URL>"
enabled_tools = ["soql_query", "find", "getObjectSchema"]
# or:
# disabled_tools = ["deleteSobjectRecord", "deleteRelatedRecord"]

Override OAuth callback port

If port 1455 is in use on your machine:

~/.codex/config.toml
mcp_oauth_callback_port = 1455
# or full URL override:
# mcp_oauth_callback_url = "http://localhost:1455/callback"

[mcp_servers.salesforce]
url = "<SANDBOX_URL>"

Fallback: bearer token via env var

If codex mcp logindoesn't play nicely with Salesforce's PKCE flow, drop to a bearer token from an environment variable:

~/.codex/config.toml
[mcp_servers.salesforce]
url = "<SANDBOX_URL>"
bearer_token_env_var = "SF_MCP_TOKEN"
## 08_side_by_side.md

Side-by-side comparison

All four work. They're differentiated by where the config lives, how OAuth feels, and what you'd reach for them for:

DimensionChatGPTClaude CodeCursorCodex CLI
setup time~3 min~2 min~3 min~3 min
oauth uxin-app, round-trip back to ECAnative MCP OAuth, browser tabbrowser tab, or via mcp-remotecodex mcp login + browser
config scopeaccount-level (no file)local, user, or .mcp.json.cursor/mcp.json~/.codex/config.toml
sandbox urlpaste verbatim, watch the /sandbox/ segmentsame trap, same fixsame trap, same fixsame trap, same fix
write-tool frictionper-tool approval each sessionapprove once, allow-this-pattern afterper-tool approval, inlinetool allowlists per-server in TOML

One recommendation per use case, if you're picking one:

  • ChatGPT: non-dev stakeholders. The browser UI wins.
  • Claude Code: best dev-tool DX. Reach for it when you're pair-coding the integration itself.
  • Cursor: inline coding flow with the connection living next to the project.
  • Codex CLI: autonomous runs where you want to trim the tool surface up front.
## 09_gotchas.md

Gotchas (the ones that actually bit during setup)

  • Sandbox URL has /sandbox/ in the path. Production has /platform/. Easy to miss when copy-pasting between clients.
  • Consumer Key propagation. Fresh ECAs throw invalid_client_id for 2 to 10 minutes after creation or deploy. Wait, then retry. Salesforce docs allow up to 30 minutes.
  • Hosted MCP server activation is a separate step from creating the ECA. Setup, then Hosted MCP Servers, activate the server you want, link it to your ECA. Without this, OAuth works and the MCP endpoint refuses you.
  • Five callback URLs on the same ECA. Plus ChatGPT generates a sixth after connector creation. Keep them straight, paste all of them, save once.
  • mcp-remote cache poisoning. After any misconfig, rm -rf ~/.mcp-auth before retrying. The shim caches OAuth state and ghost errors will follow you otherwise.
  • Cursor JSON parse failures fail silently. One missing comma kills every server on the file. Validate with python3 -m json.tool first.
  • Codex project-scope config needs a trusted dir. First run uses user-level config until you accept the trust prompt for the project.
  • FLS, sharing rules, and object permissions still apply in all four clients. The ECA is not a superuser. Whatever the running user can see in Salesforce is what the AI sees, no more, no less. Worth restating to anyone who assumes one client is more powerful than another.
  • Never paste the Consumer Key into a checked-in config file. Use env interpolation: ${env:SF_CLIENT_ID} in Cursor, ${SF_CLIENT_ID} in Codex headers or bearer_token_env_var, and the --header flag in Claude Code (never .mcp.json).
## 10_demo_prompt.md

The same prompt for all four

Once you've got at least one client wired up, run this in a fresh chat or session:

demo_prompt.txt
List my top 5 open opportunities by amount. Show name, stage, account name, owner, and close date.

Why this one: it hits sobject-all only (no Flows or Invocables), one cross-object relationship (Account.Name), needs ORDER BY + LIMITin the SOQL the agent plans, and the result fits on one screen. Across four clients on the same seeded org, you get the same five rows. That's the visual payoff.

## 11_video.md

Watch the full walkthrough

The video shows the four-client tour live, with the round-trip to the ECA, OAuth flows, and the same demo prompt across all four. About 15 minutes.

## 12_resources.md

Resources

## last_call.sh

Building at the intersection of Salesforce and AI?

If you got this working across all four clients, I'd love to hear which one you're going to live in day-to-day. Drop a comment on the YouTube video. I read every one.

Revenue Engineer is the community for working Salesforce professionals leveling up on AI-augmented development and modern GTM tooling. Weekly drops, live sessions, and a podcast in the works. Join the waitlist, you'll be first in line when the founding cohort opens.

> join the waitlist

// one email, no spam, first in line.

← back to ~/revenueengsigned: Warren Walters