
Creating an Apex Rest API and using it as a custom tool in custom mcp server
Here is an end-to-end implementation that:
✅ Exposes a Salesforce custom REST API that returns object field metadata
✅ Calls it from your custom MCP stdio server (Node.js)
✅ Makes it available to Copilot Agent
This solution uses Apex REST for object describe and jsforce in your MCP server to authenticate & fetch the results.
1) Apex REST Endpoint (Salesforce)
Create this Apex class in your Salesforce org. It returns a list of field metadata (name, label, type) for any object.
Save as ObjectFieldsController.cls:
@RestResource(urlMapping='/ObjectFields/*') global with sharing class ObjectFieldsController { global class FieldInfo { public String name; public String label; public String type; } @HttpGet global static List<FieldInfo> getObjectFields() { String sObjectName = RestContext.request.requestURI.substring( RestContext.request.requestURI.lastIndexOf('/') + 1 ); if (String.isBlank(sObjectName)) { return new List<FieldInfo>(); } Map<String, Schema.SObjectField> fieldMap = Schema.getGlobalDescribe().get(sObjectName).getDescribe().fields.getMap(); List<FieldInfo> output = new List<FieldInfo>(); for (String key : fieldMap.keySet()) { Schema.DescribeFieldResult d = fieldMap.get(key).getDescribe(); FieldInfo info = new FieldInfo(); info.name = d.getName(); info.label = d.getLabel(); info.type = String.valueOf(d.getType()); output.add(info); } return output; } }
This returns an array of simple objects with field name, label, and type. Apex REST requires a concrete return type (not Object) so we define FieldInfo.
2) MCP Server (Node.js)
This is your MCP stdio server that Copilot can call.
Install Required Packages:
npm install mcp-use jsforce
server.js#!/usr/bin/env node import { createMCPServer } from "mcp-use/server"; import jsforce from "jsforce"; // Salesforce connection info — set via environment variables const { SF_LOGIN_URL, SF_USERNAME, SF_PASSWORD, SF_CLIENT_ID, SF_CLIENT_SECRET, SF_REFRESH_TOKEN } = process.env; // Define your custom tool const tools = { getObjectFields: async ({ input }) => { try { const { objectName } = input; if (!objectName) { return { result: { error: "Input must include objectName" } }; } const conn = new jsforce.Connection({ loginUrl: SF_LOGIN_URL || "https://login.salesforce.com", clientId: SF_CLIENT_ID, clientSecret: SF_CLIENT_SECRET, refreshToken: SF_REFRESH_TOKEN }); if (SF_REFRESH_TOKEN) { await conn.refreshToken(SF_REFRESH_TOKEN); } else { await conn.login(SF_USERNAME, SF_PASSWORD); } const result = await conn.apex.get( `/ObjectFields/${encodeURIComponent(objectName)}` ); if (!Array.isArray(result)) { return { result: { error: "Unexpected response format from Salesforce" } }; } const summary = `Object ${objectName} has ${result.length} fields: ` + result.map(f => `${f.name} (${f.type})`).join(", "); return { result: { text: summary, fields: result } }; } catch (err) { return { result: { error: err.message } }; } } }; // Start stdio MCP server createMCPServer({ tools }); // keep alive setInterval(() => {}, 1e9);
3) Create a Connected App (New External Client App) in Salesforce
-
Log in to Salesforce Setup (gear icon → Setup).
-
In Quick Find, search for App Manager.
-
Click New External Client App (top-right).
-
Fill in:
External Client App Name
API Name
Contact Email
Check Enable OAuth Settings.
Set Callback URL (for now you can use something like):
https://login.salesforce.com/services/oauth2/success
(It’s required, even if you won’t use it directly.)
-
In Selected OAuth Scopes, add:
-
Access and manage your data (api)
-
Perform requests on your behalf at any time (refresh_token, offline_access)
-
These scopes let your app call Salesforce APIs and get a refresh token.
-
Save the connected app and wait ~10 minutes for it to fully propagate.
-
Once saved, open the app’s detail page.
Get SF_CLIENT_ID and SF_CLIENT_SECRET
On your connected app’s detail screen:
✔ Consumer Key → this is your SF_CLIENT_ID
✔ Consumer Secret → this is your SF_CLIENT_SECRET
Salesforce labels them Consumer Key and Consumer Secret respectively.
3) Get a Refresh Token (SF_REFRESH_TOKEN)
Use Postman (easier for many users)
-
In Postman, create a new http request.
-
Go to the Authorization tab.
-
Make sure no collection is selected in collections tab
-
Select OAuth 2.0 → Get New Access Token.
-
Fill in:
-
Token Name: Personal API
-
Grant Type: Authorization Code(With PKCE)
-
Callback URL: same as in your connected app or use Authorize using browser
-
Auth URL: https://login.salesforce.com/services/oauth2/authorize
-
Access Token URL: https://login.salesforce.com/services/oauth2/token
-
Client ID: your Consumer Key
-
Client Secret: your Consumer Secret
-
Code Challenge Method: SHA-256
-
Scope: api refresh_token
Click Get New Access Token.
Log in and authorize.
-
Postman will show you the Refresh Token which you can copy.
This method uses Salesforce’s OAuth and gives you both tokens (access and refresh) in a GUI.
4) Set Environment Variables
Store your Salesforce credentials securely in user settings:
| Env Var | Description |
|---|---|
SF_LOGIN_URL | https://login.salesforce.com |
SF_USERNAME | your Salesforce username |
SF_PASSWORD | password + security token |
SF_CLIENT_ID | Connected App Consumer Key |
SF_CLIENT_SECRET | Connected App Secret |
SF_REFRESH_TOKEN | refresh token (recommended) |
Update .vscode/mcp.json
mcp.json{ "servers": { "Custom MCP": { "type": "stdio", "command": "node", "args": ["server.js"], "cwd": "${workspaceFolder}/my-mcp-server" } } }
✔ Copilot/Agentforce will spawn your custom MCP server via stdio
✔ Ensure you restart VS Code after saving this
5) Execute this command in Copilot/Agentforce to test the API
Call the tool getObjectFields for Account

🎉 Result
You now have a fully working custom Salesforce API + MCP tool that:
✨ Calls an Apex REST endpoint
✨ Returns Salesforce field metadata
✨ Responds in natural language via Copilot/Agentforce