Model Context Protocol (MCP) is one of those technologies that sounds abstract until you build something with it. Then it clicks: it's a standardized way for AI agents to call external tools, with a governance layer that lets you control exactly which tools the agent can access.
If you're a Salesforce developer working with Agentforce, MCP matters because it replaces one-off REST integrations with a protocol that's consistent across tools. You register a server, pick the tools you want, and they show up as agent actions — managed the same way as your Flows and Apex actions.
In this guide, I'll walk through building a working MCP server from scratch, connecting it to Agentforce, and explaining the governance patterns that make it production-ready. The server itself is pure TypeScript — nothing Salesforce-specific — so the skills transfer to any MCP-compatible agent platform.
What MCP Actually Is (In 60 Seconds)
MCP is an open protocol — originally created by Anthropic — that defines how AI agents discover and call external tools. It has two sides:
The server advertises a list of tools, each with a name, description, and JSON schema for inputs and outputs. It handles execution when a tool is called.
The client (your agent platform — Agentforce, Claude, LangChain, etc.) discovers available tools, presents them to the LLM as callable actions, and routes tool calls to the server.
The protocol handles discovery, invocation, and response formatting. You focus on the tool logic.
Why not just build a REST API? You can — and for simple integrations, you probably should. MCP adds value when you have multiple tools that an AI agent selects between dynamically, or when you want a governance layer (allowlists) between the tools and the agent. For a single-purpose integration, REST is simpler. For a tool ecosystem, MCP scales better.
What We're Building
A mock "payer policy lookup" MCP server with three tools:
- lookup_formulary_coverage — Given a payer, plan, and drug, return coverage status and formulary tier
- get_pa_requirements — Return whether prior authorization is required and the criteria
- get_utilization_rules — Return step therapy and quantity limit rules
These simulate the kind of external data an AI agent in Life Sciences would need during benefits verification. In production, they'd call a real payer API or clearinghouse. For this tutorial, they're backed by a JSON file.
Step 1: Set Up the Project
mkdir mcp-payer-server && cd mcp-payer-server
npm init -y
npm install @modelcontextprotocol/sdk zod
Create a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
Step 2: Create the Mock Data
Create src/payer_rules.json:
{
"AcmeHealth|Gold Plus|RX-OMNI-10mg": {
"coverage_status": "Covered",
"formulary_tier": "Tier 4",
"prior_auth_required": true,
"criteria_summary": "Confirm labeled indication. Document prior therapy failure. Baseline labs required.",
"step_therapy": "Must try Drug A before RX-OMNI",
"quantity_limit": "30 tablets / 30 days",
"restrictions": "Specialty pharmacy required"
},
"BlueCross|Standard PPO|CardioMax-25mg": {
"coverage_status": "Covered",
"formulary_tier": "Tier 2",
"prior_auth_required": false,
"criteria_summary": "No prior authorization required for Tier 2 formulary drugs.",
"step_therapy": "None",
"quantity_limit": "90 tablets / 90 days",
"restrictions": "None"
},
"MediPlan|Bronze Essential|RX-OMNI-10mg": {
"coverage_status": "Not Covered",
"formulary_tier": "Non-formulary",
"prior_auth_required": false,
"criteria_summary": "Drug not on formulary. Patient may appeal for exception.",
"step_therapy": "N/A",
"quantity_limit": "N/A",
"restrictions": "Non-formulary — appeal process available"
}
}
The key format is payer|plan|drug. Three records give you enough to demo a covered/PA-required case, a clean coverage case, and a not-covered case.
Step 3: Build the MCP Server
Create src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";
// Load mock data
const rules: Record<string, any> = JSON.parse(
fs.readFileSync(
path.join(import.meta.dirname, "payer_rules.json"),
"utf-8"
)
);
function key(payer: string, plan: string, drug: string): string {
return `${payer}|${plan}|${drug}`;
}
// Create server
const server = new McpServer({
name: "payer-policy-server",
version: "1.0.0",
});
// Tool 1: Formulary coverage lookup
server.tool(
"lookup_formulary_coverage",
"Check whether a drug is covered under a specific payer and plan. Returns coverage status, formulary tier, and restrictions.",
{
payer: z.string().describe("Payer organization name"),
plan: z.string().describe("Plan name"),
drug: z.string().describe("Drug name and strength"),
},
async ({ payer, plan, drug }) => {
const row = rules[key(payer, plan, drug)];
const result = row
? {
coverage_status: row.coverage_status,
formulary_tier: row.formulary_tier,
restrictions: row.restrictions,
}
: {
coverage_status: "Unknown",
formulary_tier: "Unknown",
restrictions: "No matching plan/drug rule found.",
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
// Tool 2: Prior authorization requirements
server.tool(
"get_pa_requirements",
"Check whether prior authorization is required and return the criteria summary.",
{
payer: z.string().describe("Payer organization name"),
plan: z.string().describe("Plan name"),
drug: z.string().describe("Drug name and strength"),
},
async ({ payer, plan, drug }) => {
const row = rules[key(payer, plan, drug)];
const result = {
prior_auth_required: row?.prior_auth_required ?? false,
criteria_summary:
row?.criteria_summary ?? "No PA criteria found for this combination.",
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
// Tool 3: Utilization management rules
server.tool(
"get_utilization_rules",
"Return step therapy requirements and quantity limits for a drug under a specific plan.",
{
payer: z.string().describe("Payer organization name"),
plan: z.string().describe("Plan name"),
drug: z.string().describe("Drug name and strength"),
},
async ({ payer, plan, drug }) => {
const row = rules[key(payer, plan, drug)];
const result = {
step_therapy: row?.step_therapy ?? "None documented.",
quantity_limit: row?.quantity_limit ?? "None documented.",
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Payer Policy MCP Server running on stdio");
}
main().catch(console.error);
Build and test it:
npx tsc
node dist/server.js
That's it. You have a working MCP server with three tools. Each tool has a name, a description (the LLM reads this to decide when to use it), typed input parameters via Zod, and a handler that returns structured JSON.
Step 4: Connect to Agentforce
This is the Salesforce-specific part. If you're using a different MCP client (Claude Desktop, LangChain, Cursor), skip to the next section — the server is the same regardless of client.
In Salesforce Setup:
Register the MCP server. Navigate to the MCP server setup, add a new server, and provide the connection details (URL if hosted remotely, or the stdio command if running locally during development).
Allowlist your tools. This is the governance step. The server exposes three tools, and you explicitly select which ones the agent can use. If you only want the agent to check coverage but not utilization rules, you allowlist
lookup_formulary_coverageandget_pa_requirements, and leaveget_utilization_rulesoff the list.Tools appear in the Asset Library. Each allowlisted tool becomes an action, managed alongside your Flow actions, Apex actions, and Prompt Template actions. Same governance model, same testing approach.
Add to an agent topic. Open your agent in Agentforce Builder, navigate to the relevant topic, and add the MCP actions from the Asset Library. They chain with your other actions like any other step.
Test in Plan Canvas. Run a test prompt and watch the agent discover the tools, select the right one based on the user's request, pass the parameters, and process the response. Plan Canvas shows the full reasoning trace.
Step 5: Test Without Agentforce (MCP Inspector)
If you don't have Agentforce MCP Support access yet, or you just want to test the server independently, use the MCP Inspector:
npx @modelcontextprotocol/inspector node dist/server.js
This opens a browser UI where you can see the tools list, call each tool with test inputs, and inspect the responses. It's the fastest way to validate your server before connecting it to any agent platform.
Why Allowlists Matter More Than You Think
The allowlist isn't just a configuration step — it's the governance mechanism that makes MCP production-ready.
Consider this scenario: your MCP server exposes 10 tools because it serves multiple agent use cases. One of those tools writes data back to an external system. Without an allowlist, every agent that connects to this server can invoke every tool — including the write operation.
With allowlists, you control this per agent topic:
-
Benefits verification agent gets:
lookup_formulary_coverage,get_pa_requirements,get_utilization_rules(read-only) -
Enrollment agent gets:
lookup_formulary_coverage,submit_pa_request(read + write, with human confirmation gate) -
Reporting agent gets:
get_utilization_rulesonly
The principle: give each agent the minimum set of tools it needs. Smaller toolset = more predictable behavior = fewer surprises in production. This matters especially in regulated industries where every tool invocation is potentially auditable, but it's good practice regardless of domain.
Adapting for Other MCP Clients
The server you just built works with any MCP-compatible client. Here are two common alternatives to Agentforce:
Claude Desktop: Add the server to your claude_desktop_config.json:
{
"mcpServers": {
"payer-policy": {
"command": "node",
"args": ["/path/to/dist/server.js"]
}
}
}
Claude will discover the tools automatically and use them when relevant to the conversation.
VS Code (via Copilot or Cline): Add to .vscode/mcp.json:
{
"servers": {
"payer-policy": {
"command": "node",
"args": ["dist/server.js"]
}
}
}
The server code doesn't change. The tool contracts don't change. Only the client configuration changes. That's the whole point of a protocol.
Deploying Beyond Local Development
For a POC, running the server locally is fine. For production or shared demos, you need to host it somewhere accessible. Heroku is the common choice for Salesforce-connected MCP servers (Salesforce explicitly supports this via AppLink), but any Node.js host works — AWS Lambda, Railway, Fly.io, a plain VM.
The key deployment consideration: if you're switching from stdio transport (local) to HTTP/SSE transport (remote), you'll need to swap the transport layer in your server code. The tool logic stays identical.
What You've Built
In about 30 minutes, you've built:
- A working MCP server with three tools, typed inputs, and structured JSON outputs
- Mock data that covers three test scenarios (covered, not covered, PA required)
- A connection path to Agentforce (or Claude, or VS Code)
- A governance model based on allowlists that controls which tools each agent can access
The pattern scales. When you need a fourth tool — maybe check_appeal_status or get_copay_accumulator — you add another server.tool() block with its schema and handler. The protocol handles discovery and invocation. You focus on the logic.
And when the mock data needs to become real, you replace the JSON file reads with actual API calls to your payer system or clearinghouse. The tool contracts (names, inputs, outputs) stay the same, so nothing in your agent configuration breaks.
That's what a good integration protocol buys you: the ability to evolve the implementation without changing the interface.
Top comments (0)