The Model Context Protocol (MCP) allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction.
This Apex SDK implements the MCP specification, making it easy to:
- Create MCP servers that expose resources and tools
- Use standard Streamable HTTP transport
- Handle MCP protocol messages
Demo Context: For this example, we'll imagine a car service company named AutoCare Plus. The organization has a custom object
CarService__c
that stores information about car services and a custom objectCarServiceAppointment__c
that stores appointment information.
Let's create a simple MCP server that exposes tools to get a list of available car services and book appointments.
First, create a REST resource class that will handle all MCP requests, then create the server instance.
Note: For this demo, we assume the MCP server is publicly accessible via a URL like
{instance}/services/apexrest/mcp/
. To set up a REST Resource for public access, please refer to the Salesforce documentation.
@RestResource(UrlMapping='/mcp/*')
global with sharing class DemoServer {
@HttpPost
global static void post() {
ctx.Server server = new ctx.Server('1.0.0', 'autocare-plus-mcp-server');
server.run();
}
}
That's it! The server is now valid and ready to accept requests from MCP clients. However, it doesn't provide any functionality yet.
Tools allow servers to expose executable functions that can be invoked by language models.
Create a tool that allows clients to retrieve the list of all available car services. To do this, create a class that extends ctx.Tool
and implements the call
method:
public with sharing class GetAvailableCarServicesTool extends ctx.Tool {
public GetAvailableCarServicesTool() {
super('get-available-car-services-tool', 'Get available car services');
}
public override String call(Map<String, Object> input) {
List<CarService__c> carServices = [SELECT Id, Name FROM CarService__c];
return JSON.serialize(carServices);
}
}
Create a tool that allows clients to book appointments. This tool accepts parameters for the service, customer information, and preferred time:
public with sharing class BookAppointmentTool extends ctx.Tool {
public BookAppointmentTool() {
super('book-appointment-tool', 'Book an appointment for a car service');
ctx.Tool.Property carServiceId = new ctx.Tool.Property(
'carServiceId',
'string',
'The Salesforce record ID for the car service',
true
);
ctx.Tool.Property customerPhone = new ctx.Tool.Property(
'customerPhone',
'string',
'The phone number of the customer',
true
);
ctx.Tool.Property preferredTime = new ctx.Tool.Property(
'preferredTime',
'string',
'The preferred time for the appointment in ISO 8601 format',
true
);
this.addProperty(carServiceId);
this.addProperty(customerPhone);
this.addProperty(preferredTime);
}
public override String call(Map<String, Object> input) {
// Get input parameters
Id carServiceId = (Id) input.get('carServiceId');
String customerPhone = (String) input.get('customerPhone');
Datetime preferredTime = (Datetime) JSON.deserialize((String) input.get('preferredTime'), Datetime.class);
// Create a new appointment record
insert new CarServiceAppointment__c(
CarService__c = carServiceId,
CustomerPhone__c = customerPhone,
PreferredTime__c = preferredTime
);
return JSON.serialize(true);
}
}
Now that we've implemented our tools, we need to register them with the server:
@RestResource(UrlMapping='/mcp/*')
global with sharing class DemoServer {
@HttpPost
global static void post() {
ctx.Server server = new ctx.Server('1.0.0', 'AutoCare Plus MCP Server');
server.registerTool(new GetAvailableCarServicesTool());
server.registerTool(new BookAppointmentTool());
server.run();
}
}
That's it! We've successfully implemented our first MCP server 🎉
To test the server, we'll use the MCP Inspector tool.
- Node.js v22.7.5 or higher
If you have Node.js installed, run the following command:
npx @modelcontextprotocol/inspector
- Select Transport Type: Streamable HTTP
- Enter your server URL
- Click Connect
After a successful connection, you should see the list of available resources and tools, as shown in the screenshot below:
If you see the interface above, your server is working correctly! You're now ready to integrate with Claude Desktop or other MCP clients.
Check out short demo video how to add MCP server to Claude Desktop and use it:
The are several ways to create mcp server
ctx.Server server = new ctx.Server('1.0.0', 'autocare-plus-mcp-server');
String version = '1.0.0';
String serverName = 'autocare-plus-mcp-server';
String serverTitle = 'AutoCare Plus MCP Server';
String instructions = 'AutoCare Plus car service who exposes list of available services and too to book appointment.';
ctx.Server server = new ctx.Server(version, serverName, serverTitle, instructions);
ctx.Server server = new ctx.Server('1.0.0', 'autocare-plus-mcp-server')
.setInstructions(instructions)
.setTitle(serverTitle);
public with sharing class GetAvailableCarServicesTool extends ctx.Tool {
public static final String toolName = 'get-available-car-services-tool';
public static final String toolDescription = 'Retrieves all available car services with salesforce record id.';
public GetAvailableCarServicesTool() {
super(toolName, toolDescription);
}
//...
}
public with sharing class GetAvailableCarServicesTool extends ctx.Tool {
public static final String toolName = 'get-available-car-services-tool';
public static final String toolTitle = 'Get available car services';
public static final String toolDescription = 'Retrieves all available car services with salesforce record id.';
public GetAvailableCarServicesTool() {
super(toolName, toolTitle, toolDescription);
}
//...
}
public with sharing class WeatherTool extends ctx.Tool {
public WeatherTool() {
super('get-weather-details', 'Get weather details for specific city');
ctx.Tool.Property cityName = new ctx.Tool.Property(
'cityName', // Property name
'string', // Property type
'The name of the city', // Property description
true // true if property is Required, false if Optional
);
this.addProperty(cityName);
}
//...
}
public with sharing class WeatherTool extends ctx.Tool {
//...
public override String call(Map<String, Object> input) {
String cityName = (String) input.get('cityName');
// Do some logic here..
String result = 'The weather in ' + cityName + ' is perfect!';
return result;
}
}
public with sharing class CarServiceResource extends ctx.Resource {
public static final String uri = '@server://services';
public static final String resourceName = 'service-catalog';
public CarServiceResource() {
super(uri, resourceName);
}
// ..
}
public with sharing class CarServiceResource extends ctx.Resource {
public static final String uri = '@server://services';
public static final String resourceName = 'service-catalog';
public static final String resourceTitle = 'Catalog of services';
public static final String resourceDescription = 'List of all available car services';
public static final String resourceMimeType = 'application/json';
public static Long resourceSize = 65536;
public CarServiceResource() {
super(uri, resourceName, resourceTitle, resourceDescription, resourceMimeType, resourceSize);
}
// ..
}
public with sharing class CarServiceResource extends ctx.Resource {
public static final String uri = '@server://services';
public static final String resourceName = 'service-catalog';
public static final String resourceTitle = 'Catalog of services';
public static final String resourceDescription = 'List of all available car services';
public static final String resourceMimeType = 'application/json';
public static Long resourceSize = 65536;
public CarServiceResource() {
super(uri, resourceName);
this.setTitle(resourceTitle)
.setDescription(resourceDescription)
.setMimeType(resourceMimeType)
.setSize(resourceSize);
}
// ..
}
public with sharing class CarServiceResource extends ctx.Resource {
// ..
public override List<ctx.Resource.Content> read() {
// The method MUST retrieve List<ctx.Content>
List<String> services = new List<String>{ 'Oil Change', 'Battery Check' };
List<ctx.Resource.Content> result = new List<ctx.Resource.Content>();
for (String service : services) {
result.add(new ctx.Resource.Content(this.uri, service));
}
return result;
}
}
ctx.Resource.Content content = new ctx.Resource.Content('uri', 'text-data');
String uri = '@server://service/oil-change';
String name = 'oil-change';
String title = 'Oil Change';
String text = 'Oil Change Service';
ctx.Resource.Content content = new ctx.Resource.Content(uri, name, title, text);
String uri = '@server://service/oil-change';
String name = 'oil-change';
String title = 'Oil Change';
String text = 'Oil Change Service';
ctx.Resource.Content content = new ctx.Resource.Content(uri, text)
.setTitle(title)
.setMimeType('plain/text')
.setText(text);
String uri = '@file:///example.png';
String name = 'example.png';
String title = 'Example Image';
ctx.Resource.Content content = new ctx.Resource.Content(uri)
.setName(name)
.setTitle(title)
.setMimeType('image/png')
.setBlobData('base64-encoded-data');
public with sharing class CarServiceResource extends ctx.Resource {
public static final String uri = '@server://services';
public static final String resourceName = 'service-catalog';
public CarServiceResource() {
super(uri, resourceName);
List<String> audience = new List<String>{ 'user', 'assistant' }; // "user" or "assistant" or both
Decimal priority = 0.8; // between 0 and 1
Datetime lastModified = System.now(); // ISO 8601 formatted timestamp
ctx.Resource.Annotations annotations = new ctx.Resource.Annotations(audience, priority, lastModified);
this.setAnnotations(annotations);
}
// ..
}