Day 5/15 — OAuth 2.0, PKCE & External Client App Setup
Salesforce's hosted MCP servers only accept users who sign in through an External Client App with OAuth 2.0 and PKCE. Today you create that app step by step, learn what each scope and checkbox does, and build a PKCE helper you will reuse on Day 15.

Neha has a call with Priya Sharma, the VP of Sales at Acme Global Tech, and she wants Claude to brief her from Salesforce first. On Day 4 her admin, Arjun, switched on the sobject-all and sobject-reads hosted Model Context Protocol (MCP) servers. Nothing can use them yet. A hosted server only answers requests that carry a token for a user who signed in, and in Salesforce Headless 360 (now called AIforce) that token comes from one place: an External Client App.
Older tutorials tell you to create a Connected App, add the api scope and paste a client secret into your AI tool. For the generally available hosted MCP servers, the first two steps are wrong and the third is usually unnecessary. Connected Apps aren't supported, api belongs to the beta, and Claude doesn't need a secret unless your app requires one, because it proves who it is in a different way: PKCE.
By the end of today you will have an External Client App ready for Claude, with exactly the right scopes and security settings, and a tested TypeScript helper that creates the PKCE values every OAuth client sends to Salesforce.
A quick recap: on Day 1 you saw a hosted server's two scopes, mcp_api and refresh_token, and on Day 4 you activated the servers. Today you create the app that issues the tokens; on Day 6 you connect Claude to it.
Why an External Client App, not a Connected App
Salesforce Help defines external client apps as "packageable frameworks to enable a third-party application to integrate with Salesforce using APIs and security protocols." For MCP, the hosted server documentation is short and absolute: "Use an External Client App to connect an MCP client to a Salesforce org. Connected Apps aren't supported."
The rest of the platform is moving the same way. Since Spring '26, "The ability to create new connected apps is disabled by default for all Salesforce orgs." A Salesforce knowledge article adds that creating them through the Metadata API "is blocked in the same way as the UI" and that Salesforce "will be announcing End-of-Support (EOS) for existing Connected Apps soon." If you learn one app model this year, make it this one.
Three more facts shape the setup:
- There is no automatic registration. Some MCP clients can register themselves with a server through Dynamic Client Registration. Salesforce's answer: "Dynamic client registration is not supported. Administrators must explicitly create and configure External Client Apps". The app you create today is Claude's identity in your org.
- One app per client. Salesforce recommends "creating a dedicated ECA per MCP client (one for Claude, one for ChatGPT, one for Cursor, etc.)". Separate apps mean separate policies and a clean way to cut off one client.
- You need admin rights, and not a scratch org. Creating the app takes "System Administrator or equivalent permissions", scratch orgs can't create External Client Apps in Setup, and local apps "aren't copied to a new sandbox when you clone or refresh a sandbox." A Developer Edition org avoids all three problems.
OAuth 2.0 with PKCE, in plain words
Hosted MCP supports one way to sign in. Salesforce's security guide: "The system uses OAuth authorization code flow exclusively, maintaining human accountability for all transactions." A person signs in, approves the app, and the client receives tokens that act as that person.

Read the diagram from the top:
- The client creates a secret for this sign-in only. It generates a random code verifier and derives a code challenge from it: the SHA-256 hash of the verifier, base64url-encoded. That transformation is called
S256. - It sends the user to Salesforce with its client ID (the External Client App's consumer key), the callback URL, the scopes and the challenge. The verifier stays behind.
- The user logs in and approves access. This is the human in "human accountability".
- Salesforce redirects back with a short-lived authorization code.
- The client exchanges the code for tokens, and this time it sends the verifier. Salesforce hashes it and compares the result with the challenge from step 2.
- Salesforce returns an access token and a refresh token.
Why the extra steps? Because a desktop app can't keep a secret, and Salesforce's hardening guide names "Desktop applications" under "When to avoid" for client secrets. PKCE gives a public client the protection a secret would: a code intercepted in step 4 is useless without the verifier, which never left the client. Claude's documentation describes exactly this mode: "If a user leaves an optional client secret blank, Claude uses the client ID as a public client."
For hosted MCP, PKCE is not optional: "PKCE required. Proof Key for Code Exchange (PKCE) is mandatory for all OAuth flows." The method is S256. RFC 7636, the PKCE standard, says a client that can use S256 "MUST use" it, and the OpenID configuration published at login.salesforce.com lists S256 as the only supported challenge method.
Two more details complete the picture.
How a client knows where to sign in. The MCP specification says clients "MUST use OAuth 2.0 Protected Resource Metadata for authorization server discovery." That is the metadata you read on Day 1. For production and Developer Edition server URLs, authorization_servers is https://login.salesforce.com. For the /sandbox/ form of the same URL it is https://test.salesforce.com, and for the My Domain form it is your org's own My Domain login URL. When a sign-in lands on the wrong login page, this field tells you why.
Why the access token must be a JWT. On Day 1 the server answered an anonymous request with JWT Token is required. Salesforce's wiki explains the design: "We require JWT-based access tokens (not opaque tokens). JWTs are self-contained ... This allows the MCP server to validate tokens without making a separate callout to Salesforce on every request". You switch this on with one checkbox in a moment; don't skip it.
Step by step: create the External Client App
These steps create the app Claude uses on Day 6: claude.ai, Claude Desktop, Claude Code and, for testing, MCP Inspector. They follow Salesforce's developer documentation, with the security settings from Salesforce's Claude blog and Headless 360 workshop. If a label in your org differs, trust your screen and keep the intent.
Setup → Quick Find "external client" → External Client App Manager → New External Client App.
Under Basic Information, enter:
- External Client App Name:
Claude MCP Test - Contact Email: your email address
- Distribution State: Local (label may differ in your org)
- External Client App Name:
Expand API (Enable OAuth Settings) and check Enable OAuth.
In Callback URL, enter one URL per line:
https://claude.ai/api/mcp/auth_callback http://localhost:38000/callback http://localhost:6274/oauth/callbackThe first is Claude's connector callback for claude.ai and Claude Desktop. The second is the one Salesforce's blog gives for Claude Code. The third is optional, for MCP Inspector opened at
http://localhost:6274(Day 3). In my Developer Edition org on 2026-10-03, one app accepted all three without an error.In OAuth Scopes, add exactly two scopes and nothing else:
- Access Salesforce hosted MCP servers (mcp_api)
- Perform requests at any time (refresh_token, offline_access)
Leave every Flow Enablement option unchecked (label may differ in your org).
Under Security:
- Uncheck Require secret for Web Server Flow.
- Uncheck Require secret for Refresh Token Flow.
- Check Require Proof Key for Code Exchange (PKCE) extension for Supported Authorization Flows.
- Check Issue JSON Web Token (JWT)-based access tokens for named users.
- Leave every other box unchecked. In my org these were exactly the boxes on the page that needed a change: both secret boxes off, PKCE and JWT on, nothing else.
Click Create and note the time. Salesforce warns: "The External Client App can take up to 30 minutes to become available and operational for use with your MCP client. (The delay is similar to registering a new domain with DNS.)"
Open the app, click Settings, then Consumer Key and Secret under OAuth Settings. Salesforce asks for a verification code, which it emails you (it did in my org); enter it, then copy the Consumer Key into your password manager. Claude doesn't need the secret, so don't copy it anywhere.



Salesforce's pages don't use the same labels
The scope labels above are exactly what Setup showed in my Developer Edition org on 2026-10-03; some Salesforce pages print shorter versions. The developer docs' security step also names only the JWT box, without mentioning PKCE, but the Claude blog, the workshop and the wiki all check it, and the security guide makes PKCE mandatory. Choose scopes by the value in parentheses, not by the wording.
Salesforce documents a callback for each common client:
| Client | Callback URL | Documented by |
|---|---|---|
| claude.ai and Claude Desktop | https://claude.ai/api/mcp/auth_callback |
Salesforce and Anthropic |
| Claude Code | http://localhost:38000/callback |
Salesforce blog, May 2026 |
| Postman | https://oauth.pstmn.io/v1/callback (web version: https://oauth.pstmn.io/v1/browser-callback) |
Salesforce |
| Cursor | http://localhost:8787/callback (some versions: cursor://anysphere.cursor-mcp/oauth/callback) |
Salesforce |
| ChatGPT | copy it from ChatGPT's Advanced settings | Salesforce |
| MCP Inspector (testing) | http://localhost:6274/oauth/callback |
Not in Salesforce's docs; works when you open the Inspector at localhost, because Salesforce refuses http://127.0.0.1 callbacks |
Postman, Cursor and ChatGPT would each get their own app. I keep Claude's surfaces in one app because they are one vendor's client; MCP Inspector joins only as a test tool. Registering both Claude callbacks leaves both of Day 6's routes open. There is a cost to sharing one app, though, and I hit it during testing.
Five approvals per user, per app
Each time you click Allow for Claude MCP Test, Salesforce records an approval for your user on that app: the claude.ai connector, each server you add to Claude Code with the same consumer key, each MCP Inspector login, and each reconnect. When I approved a sixth, Salesforce showed: "Caution: You have granted access to this application 5 times, which is the limit. Approving this request automatically revokes your oldest approval. To avoid revoking your oldest approval, deny this request or manually revoke an approval in your personal settings."
Approve it, and the oldest connection stops working, with no warning in the client that used it. Keep one app per client, as Salesforce recommends, or fewer servers per app (Day 7 connects more). If a connection breaks, revoke approvals you no longer use (Setup → External Client Apps → OAuth Usage, then click the user count), then sign in again from that client: Connect on the claude.ai connector, or /mcp → Authenticate in Claude Code.

Optional: decide who may use the app
In a real org, restrict the app to the people who should use it. Open the app and go to Policies → Edit → OAuth Policies. Change Permitted Users to Admin approved users are pre-authorized, then select a permission set that only those people have. Salesforce's workshop uses one called MCP Client User. Salesforce Help is firm about the alternative: "Don't select a self-authorization option: it skips the permission set gate and allows users access the ECA without explicit authorization." It also warns that without a permission set, "every user in the org gets access to the ECA."
While you are in the policies, look at the refresh-token settings. Salesforce recommends a refresh token validity of "30 days or less" with refresh token rotation. In my org the heading was "Refresh Token Policy (Idle Expiration Time Limit Enforced)", with three options: "Immediately expire refresh token", "Expire refresh token after specific time" and "Expire refresh token if not used for specific time". There was no "valid until revoked" choice. My capstone app (Headless Assistant Local, below) uses the third, with 7 days.

Scopes
A scope is a permission the app asks for when a user signs in. Hosted MCP needs exactly two, and the servers say so themselves: the metadata you read on Day 1 lists "scopes_supported": ["mcp_api", "refresh_token"].
| Scope | Label you may see in Setup | What it allows | Use it for hosted MCP? |
|---|---|---|---|
mcp_api |
Access Salesforce hosted MCP servers (mcp_api) | Access to Salesforce hosted MCP servers, but not to the REST, Tooling or Metadata APIs | Yes, required |
refresh_token |
Perform requests at any time (refresh_token, offline_access) | A refresh token, so the client can get new access tokens without asking the user to sign in again | Yes, required |
api |
Manage user data via APIs (api) | Full access to the Platform APIs: REST, Tooling, Metadata and more | No. A beta-era scope that doesn't work with the GA servers |
sfap_api, einstein_gpt_api |
(beta scopes) | Part of the beta's scope set | No. They don't work with the GA servers |
mcp_api exists to keep the damage from a leaked token small. Salesforce's security blog: "We've released this scope to avoid exposing the "Manage user data via APIs (api)" scope that grants full access to the Platform APIs (REST, Tooling, Metadata, etc.)." A token taken from an MCP client can call MCP tools as that user, but it can't be replayed against the REST API.
If you followed the beta. Beta apps used api, sfap_api, refresh_token and einstein_gpt_api, and "tokens issued during beta are not valid for the GA service." Change the app's scopes to the two above, then sign in again from each client.
Where offline_access fits. It appears only inside the refresh-token label. There is nothing extra to select.
What mcp_api can't do. It covers hosted MCP as a whole, not one server. Salesforce says so plainly: "You cannot restrict access to a specific MCP server through the ECA configuration, but you control access to the tools that compose those servers." You choose servers in Setup and limit tools through the user's permissions, which is the subject of Day 11.
Hands-on: a second app and a PKCE helper
Claude does the OAuth work for you on Day 6. The Next.js assistant you build on Day 15 has to do it itself, so today you build and test the piece it needs: a PKCE helper in TypeScript, copied from the companion repository's web/lib/pkce.ts.
Step 1: create an app for your own code
One app per client applies to your own code too. Create a second External Client App exactly like the first, with two differences:
- External Client App Name:
Headless Assistant Local - Callback URL: only
http://localhost:3000/api/auth/salesforce/callback, the companion app's default (change the port if you run the app on another one)
Use the same two scopes and the same Security settings. When the companion repository's Headless Assistant User permission set (Headless_Assistant_User) is in your org, set this app's Permitted Users to Admin approved users are pre-authorized and select it, as in the optional step above. Copy this app's consumer key as well: the capstone reads it from SF_CLIENT_ID.
Step 2: check which login host your server uses
Your code has to send users to the right login host, and the server's metadata names it. Run the Day 1 check against the URL you copied from Setup. For a sandbox URL it looks like this:
curl https://api.salesforce.com/.well-known/oauth-protected-resource/platform/mcp/v1/sandbox/platform/sobject-all
# "authorization_servers": ["https://test.salesforce.com"]
Step 3: the PKCE helper
// web/lib/pkce.ts
/**
* PKCE (RFC 7636) and OAuth state helpers.
* Salesforce hosted MCP servers require PKCE with the S256 method; the org's
* /.well-known/openid-configuration lists code_challenge_methods_supported: ["S256"].
*/
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
/** RFC 7636 section 4.1: 43-128 characters from the unreserved set. */
const VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]{43,128}$/;
export function generateCodeVerifier(byteLength = 32): string {
if (byteLength < 32 || byteLength > 96) {
throw new RangeError("byteLength must be between 32 and 96 bytes (43-128 characters)");
}
return randomBytes(byteLength).toString("base64url");
}
export function isValidCodeVerifier(verifier: string): boolean {
return VERIFIER_PATTERN.test(verifier);
}
/** code_challenge = BASE64URL(SHA256(ASCII(code_verifier))), method "S256". */
export function codeChallengeS256(verifier: string): string {
if (!isValidCodeVerifier(verifier)) {
throw new Error("Invalid PKCE code_verifier");
}
return createHash("sha256").update(verifier, "ascii").digest("base64url");
}
/** Opaque, unguessable value that ties the callback to the browser that started login. */
export function generateState(): string {
return randomBytes(32).toString("base64url");
}
/** Constant-time string comparison for state values. */
export function safeEqual(a: string, b: string): boolean {
const left = Buffer.from(a, "utf8");
const right = Buffer.from(b, "utf8");
return left.length === right.length && timingSafeEqual(left, right);
}
What each function does:
generateCodeVerifierbase64url-encodes 32 random bytes, which gives exactly 43 characters. RFC 7636 asks for a "high-entropy cryptographic random STRING" of 43 to 128 characters.codeChallengeS256is all ofS256: SHA-256 over the verifier's ASCII bytes, base64url-encoded without padding. It refuses a malformed verifier.generateStatemakes thestatevalue. PKCE protects the code exchange;stateprotects the callback, so your app only accepts a redirect it started.safeEqualcompares the returnedstatewith the stored one in constant time.
Step 4: prove it against the standard
Never trust crypto code you haven't tested against a known answer. RFC 7636 includes one in Appendix B: a fixed verifier and the challenge it must produce.
// check-pkce.ts: prove the helper against RFC 7636 Appendix B, then print a fresh pair.
// Run with Node.js 22.19+: npx tsx check-pkce.ts
import { codeChallengeS256, generateCodeVerifier, generateState } from "./pkce";
const RFC_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const RFC_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
const actual = codeChallengeS256(RFC_VERIFIER);
if (actual !== RFC_CHALLENGE) {
throw new Error(`S256 mismatch: expected ${RFC_CHALLENGE}, got ${actual}`);
}
console.log("RFC 7636 Appendix B: OK");
const verifier = generateCodeVerifier();
console.log("code_verifier: ", verifier, `(${verifier.length} chars)`);
console.log("code_challenge:", codeChallengeS256(verifier));
console.log("state: ", generateState());
Put both files in one folder and run npx tsx check-pkce.ts. The first line must say RFC 7636 Appendix B: OK. The other three change on every run; the verifier is always 43 characters long:
RFC 7636 Appendix B: OK
code_verifier: <43_RANDOM_CHARACTERS> (43 chars)
code_challenge: <43_CHARACTERS_DERIVED_FROM_THE_VERIFIER>
state: <43_RANDOM_CHARACTERS>
The companion repository runs the same check as a unit test (web/tests/pkce.test.ts), so a future edit can't quietly break the hash.
Step 5: see where the values go
Here is the sign-in half of the flow, condensed from the capstone's web/lib/salesforce-oauth.ts. The endpoints come from the OpenID configuration Salesforce publishes at https://login.salesforce.com/.well-known/openid-configuration.
// authorize-url.ts: where the PKCE pair goes (condensed from web/lib/salesforce-oauth.ts).
import { codeChallengeS256, generateCodeVerifier, generateState } from "./pkce";
const LOGIN_URL = "https://login.salesforce.com"; // https://test.salesforce.com for sandboxes
const CLIENT_ID = "<EXTERNAL_CLIENT_APP_CONSUMER_KEY>";
const CALLBACK_URL = "http://localhost:3000/api/auth/salesforce/callback"; // must match the app exactly
// 1. Before the redirect: keep the verifier and state server-side (the capstone seals them in a cookie).
const verifier = generateCodeVerifier();
const state = generateState();
const authorize = new URL(`${LOGIN_URL}/services/oauth2/authorize`);
authorize.search = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: CALLBACK_URL,
scope: "mcp_api refresh_token",
state,
code_challenge: codeChallengeS256(verifier),
code_challenge_method: "S256",
}).toString();
console.log(authorize.toString());
// 2. In the callback: check state, then exchange the code once, with the original verifier.
export async function exchangeCode(code: string): Promise<Response> {
return fetch(`${LOGIN_URL}/services/oauth2/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: CALLBACK_URL,
code_verifier: verifier,
client_id: CLIENT_ID,
}),
});
}
Run it and you get a long URL containing scope=mcp_api+refresh_token and code_challenge_method=S256. Don't open it yet: nothing listens on port 3000 until Day 15. Three details matter most: the redirect_uri must match the app's callback exactly in both requests, the token request sends the verifier (never the challenge), and each sign-in gets a fresh pair.
Python version
"""PKCE (RFC 7636, S256) helpers for a Salesforce External Client App. Python 3.10+, standard library only."""
import base64
import hashlib
import re
import secrets
from urllib.parse import urlencode
VERIFIER_PATTERN = re.compile(r"^[A-Za-z0-9\-._~]{43,128}$")
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def generate_code_verifier(byte_length: int = 32) -> str:
if not 32 <= byte_length <= 96:
raise ValueError("byte_length must be between 32 and 96 bytes (43-128 characters)")
return _b64url(secrets.token_bytes(byte_length))
def code_challenge_s256(verifier: str) -> str:
if not VERIFIER_PATTERN.fullmatch(verifier):
raise ValueError("Invalid PKCE code_verifier")
return _b64url(hashlib.sha256(verifier.encode("ascii")).digest())
def generate_state() -> str:
return _b64url(secrets.token_bytes(32))
if __name__ == "__main__":
rfc_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
assert code_challenge_s256(rfc_verifier) == "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
print("RFC 7636 Appendix B: OK")
verifier = generate_code_verifier()
query = urlencode({
"response_type": "code",
"client_id": "<EXTERNAL_CLIENT_APP_CONSUMER_KEY>",
"redirect_uri": "http://localhost:3000/api/auth/salesforce/callback",
"scope": "mcp_api refresh_token",
"state": generate_state(),
"code_challenge": code_challenge_s256(verifier),
"code_challenge_method": "S256",
})
print(f"https://login.salesforce.com/services/oauth2/authorize?{query}")
What can go wrong
Almost every External Client App problem shows up on the first sign-in, so these are the failures you will meet on Day 6 and how to trace them back to today's settings.
| Symptom | Likely cause | Fix |
|---|---|---|
| Salesforce's login page says the app or client ID is invalid, right after you created it | The app hasn't propagated yet (up to 30 minutes), or you copied the wrong key | Wait the full 30 minutes, then copy the Consumer Key again from Settings → OAuth Settings |
| Salesforce shows a redirect URI error instead of the login page | The client's callback doesn't match one registered on the app | The callback in your client must exactly match the app: scheme, host, port and path. localhost is not 127.0.0.1, and Salesforce won't save an http://127.0.0.1 callback ("Cannot be an HTTP URL"), so open MCP Inspector at http://localhost:6274 |
Sign-in works, then every MCP call fails with 401 or the client asks you to log in again |
The JWT box is unchecked, the scopes are wrong, or the token comes from a beta-era app | Scopes exactly mcp_api and refresh_token, JWT and PKCE checked; then sign in again |
| The token request fails after a successful login | The PKCE pair doesn't match: a new verifier was generated, the challenge was sent instead of the verifier, or the method isn't S256 |
Keep one verifier per sign-in and send the original in the token request (Step 5) |
| A sandbox user can't sign in, or the token is refused | A production URL was used for a sandbox org | Use the /sandbox/ server URL, whose metadata points to test.salesforce.com, or the My Domain URL from Day 4 |
| No New External Client App option in a scratch org | Scratch orgs can't create External Client Apps in Setup | Create the app in your Dev Hub org, package it and install the package, or use a Developer Edition org |
Security note: what this app decides, and what it doesn't
The app decides who may sign in, through which client, for how long and with which scopes. What the agent can then do is the user's own access: "Every MCP tool call runs with the same permissions as the user who authorized the connection." Keep the app narrow (one per client, admin-approved users, refresh-token expiry and rotation) and the users narrow too.
Keep the consumer secret out of chats, tickets and screenshots, and know where the off switch is: "go to Setup, search for OAuth Usage, select your ECA, and revoke individual tokens or run a bulk revoke operation."
Today's checklist
- I created
Claude MCP Testas an External Client App, not a Connected App. - Its callback URLs match my clients exactly: claude.ai, Claude Code and, optionally, MCP Inspector.
- It has exactly two scopes,
mcp_apiandrefresh_token. - PKCE and JWT are checked, and both "Require secret" boxes are unchecked.
- I copied the consumer key into my password manager and noted when I created the app.
- I created a second app,
Headless Assistant Local, for my own code. -
check-pkce.tsprintsRFC 7636 Appendix B: OK.
Frequently asked questions
Can I reuse a Connected App I already have?
Not for hosted MCP. Salesforce's documentation says Connected Apps aren't supported for connecting an MCP client, so create an External Client App with the mcp_api and refresh_token scopes.
Why doesn't Claude need the consumer secret?
Claude connects as a public client, and PKCE proves that the app redeeming the authorization code is the one that requested it. Salesforce advises against requiring client secrets for desktop applications. Leave both "Require secret" boxes unchecked for Claude's app.
Do I need one External Client App per MCP server?
No. The mcp_api scope covers hosted MCP as a whole, and Salesforce says you can't restrict an app to one server. What you need is one app per client: one for Claude, another for Cursor, another for your own web app.
What's next
Your app exists, and the 30-minute clock is running. Tomorrow, on Day 6, we connect Claude to it: a custom connector in claude.ai that carries over to Claude Desktop, a direct connection from Claude Code on port 38000, and the reason you pick "Use your own OAuth client" every time. Then Neha asks her first real question about Acme Global Tech, and we look at exactly what happened between Claude and Salesforce.
Sources
Verified against the sources below on September 30, 2026. Salesforce ships Headless 360 changes often: check the linked docs if a screen looks different.
- Create an External Client App (Salesforce Hosted MCP Servers) — steps, callback URLs, scopes, hardening options
- Security Best Practices (Salesforce Hosted MCP Servers)
- Configuring an External Client App (forcedotcom/mcp-hosted wiki) — why JWT, beta vs GA scopes
- Connect Claude with Salesforce Hosted MCP Servers — Salesforce Developers blog, May 26, 2026
- How to Secure Salesforce Hosted MCP Servers — Salesforce Developers blog, June 30, 2026
- Creation of New Connected Apps Is Disabled by Default — Spring '26 release note
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clients — S256 method and the Appendix B test vector
Everything from today on one page. Tap to zoom, or download it for later.
All 15 days in this series
- Day 05OAuth 2.0, PKCE & External Client App Setup
- Day 06Connect an AI Client to Salesforce via MCP
- Day 07Explore Salesforce MCP Tools & sobject-all




Comments
Loading comments...