Salesforce Org via MCP
global Apex visibility, the Agent Action wrapper, the exact callback URL — are the stable part and unlikely to change. The precise click-path through Salesforce's UI may shift as they continue shipping updates to this feature.Prerequisites
- A free Salesforce Developer Edition org — sign up at developer.salesforce.com
- Claude Desktop installed (claude.com/download)
Create the Demo Object
In Setup → Object Manager → Create → Custom Object, create:
- Label:
Quote Demo, Plural:Quote Demos, Object Name:Quote_Demo - Record Name:
Quote Demo Name, Data Type: Auto Number, Format:QD-{0000}
Then add these fields (Fields & Relationships → New):
| Field Label | API Name | Type |
|---|---|---|
| Status | Status__c | Picklist (Pending Approval, Approved, Rejected) |
| Amount | Amount__c | Currency |
| Territory | Territory__c | Text(255) |
| Rejection Reason | Rejection_Reason__c | Long Text Area |
Seed 5-10 records with varied amounts and territories — for example, QD-0001 at $750,000 in the West territory — so a query later returns a meaningful subset instead of everything or nothing.
Deploy the Apex Class
Create this class in Setup → Apex Classes → New. Paste the code below and save.
@InvocableVariable field must be declared global, not public. A public class will not appear in the MCP tool picker in Step 5./**
* QuoteDemoMCPAction.cls
* -----------------------------------------------------------
* Demo Apex Invocable Action for the "Headless 360 for Claude
* Users" course — Module 4/5 hands-on project.
*
* Uses a standalone demo object (Quote_Demo__c) so this can be
* deployed to any free Salesforce Developer Edition org, with
* zero dependency on any real company's data model.
*
* v2: Changed to 'global' visibility on the class, inner
* classes, and their variables — testing whether MCP tool
* registration requires global scope to discover the action
* in the Add Tools picker.
*
* Deploy this to a free Developer Edition org. Never point a
* demo MCP connection at a production or company org.
* -----------------------------------------------------------
*/
global with sharing class QuoteDemoMCPAction {
global class QuoteDemoActionRequest {
@InvocableVariable(required=true label='Quote Demo Id')
global Id quoteDemoId;
@InvocableVariable(required=true label='Action (Approve or Reject)')
global String action;
@InvocableVariable(label='Reason (required for Reject)')
global String reason;
}
global class QuoteDemoActionResult {
@InvocableVariable(label='Success')
global Boolean success;
@InvocableVariable(label='Message')
global String message;
@InvocableVariable(label='New Status')
global String newStatus;
}
@InvocableMethod(
label='Approve or Reject Demo Quote'
description='Approves or rejects a pending Quote_Demo__c record by Id. Intended to be exposed as an MCP tool so agents like Claude can act on quote approvals directly.'
category='Quote Demo Management'
)
global static List<QuoteDemoActionResult> approveOrRejectQuoteDemo(List<QuoteDemoActionRequest> requests) {
List<QuoteDemoActionResult> results = new List<QuoteDemoActionResult>();
List<Id> demoIds = new List<Id>();
for (QuoteDemoActionRequest req : requests) {
demoIds.add(req.quoteDemoId);
}
Map<Id, Quote_Demo__c> demoMap = new Map<Id, Quote_Demo__c>(
[SELECT Id, Status__c, Amount__c, Territory__c
FROM Quote_Demo__c
WHERE Id IN :demoIds]
);
List<Quote_Demo__c> toUpdate = new List<Quote_Demo__c>();
for (QuoteDemoActionRequest req : requests) {
QuoteDemoActionResult result = new QuoteDemoActionResult();
Quote_Demo__c q = demoMap.get(req.quoteDemoId);
if (q == null) {
result.success = false;
result.message = 'Quote_Demo__c not found: ' + req.quoteDemoId;
results.add(result);
continue;
}
if (req.action != 'Approve' && req.action != 'Reject') {
result.success = false;
result.message = 'Action must be "Approve" or "Reject".';
results.add(result);
continue;
}
if (req.action == 'Reject' && String.isBlank(req.reason)) {
result.success = false;
result.message = 'A reason is required to reject a demo quote.';
results.add(result);
continue;
}
q.Status__c = (req.action == 'Approve') ? 'Approved' : 'Rejected';
if (req.action == 'Reject') {
q.Rejection_Reason__c = req.reason;
}
toUpdate.add(q);
result.success = true;
result.newStatus = q.Status__c;
result.message = 'Quote_Demo__c ' + req.quoteDemoId + ' set to ' + q.Status__c + '.';
results.add(result);
}
if (!toUpdate.isEmpty()) {
update toUpdate;
}
return results;
}
}
Wrap It as an Agent Action
Go to Setup → Einstein → Agentforce Studio → Agentforce Asset Library → Actions tab → New Agent Action.
- Reference Action Type: Apex
- Reference Action Category: Invocable Method
- Reference Action: select your method (Label and API Name auto-populate)
- Fill in a Description for every input and output parameter
- Enable "Require user confirmation" — this makes Claude pause and check with you before executing any write, confirmed working in Module 5
- Click Finish
global, will not appear there.Create the External Client App
Setup → External Client App Manager → New External Client App.
- Callback URL:
https://claude.ai/api/mcp/auth_callback— use this exact URL - OAuth Flow: OAuth 2.0 with PKCE
- Scopes: "Access Salesforce hosted MCP servers (mcp_api)" and "Perform requests at any time (refresh_token, offline_access)"
- Check: Require Proof Key for Code Exchange (PKCE) Extension
- Check: Issue JSON Web Token (JWT)-based access tokens for named users
Save, then copy the Consumer Key and Consumer Secret. Next, go to the Policies tab → OAuth Policies → Edit:
- Permitted Users: All users may self-authorize
- IP Relaxation: Relax IP restrictions
Create the Custom MCP Server
Setup → search "MCP Server" → open API Catalog → MCP Servers (not "Agentforce Registry → MCP Servers," which is a different feature).
- Click Add MCP Server → Create Salesforce MCP Server, fill in Label/Name/Description, Create
- While the server is still Inactive, click Add Server Assets → Add Tools
- Switch the category dropdown to "Apex actions"
- Select your action, click "+ Add Tool," confirm the sidebar counter updates, then click Save
Now click Activate on the server. Once active, the Details tab reveals the real Server URL, following this pattern:
https://api.salesforce.com/platform/mcp/v1/custom/{YourServerAPIName}
Connect Claude Desktop
In Claude Desktop, click the "+" in the message box → Connectors → Manage Connectors → Add → Add custom connector.
- Remote MCP server URL: the Server URL from Step 5
- Advanced settings → OAuth Client ID: your Consumer Key
- Advanced settings → OAuth Client Secret: your Consumer Secret
- Click Add, then log into Salesforce and authorize when prompted
Test It
Find a real record's Salesforce ID (format a0X..., not the display name). In a new chat, enable the connector and ask Claude to act on it.
Troubleshooting
global (Step 2) and wrapped as an Agent Action (Step 3) — both are required.Key Points
- Apex classes exposed as MCP tools must be
globaland wrapped as an Agent Action — both are required together - The callback URL must be exactly
https://claude.ai/api/mcp/auth_callback - The real Server URL lives on
api.salesforce.com, not your org's My Domain - Tools can only be added while the server is Inactive
- A connector showing "Connected" doesn't guarantee its tools attached — check for the warning icon
New interview questions every week
Follow for fresh Salesforce Q&A, free courses, and real interview experiences — straight from the trenches.
Follow Us ↗