Last updated: July 2026. Verified against MCP spec revision 2025-06-18 and the November 2025 auth revision.
TL;DR: A production Model Context Protocol server for Salesforce needs four things the demos skip: OAuth 2.1 with audience-bound tokens (RFC 8707), tenant isolation at the connection layer, immutable audit logs, and a Streamable HTTP transport that survives load balancers. We walk through each piece with real code, honest trade-offs between the TypeScript and Python SDKs, and the operational gotchas we found deploying to Cloudflare Workers and AWS Fargate.
Why Production MCP Is Different From the Hello World
Most MCP tutorials stop at a stdio server that lists your local filesystem. That's fine for a demo. It's not fine when your MCP server is the bridge between Claude Desktop and 400 Salesforce orgs, each belonging to a different customer, each subject to SOC 2 and probably HIPAA or PCI on top.
The gap between the reference server and something you'd actually run in production is wide. Auth is the biggest piece. Multi-tenancy is the second. And the third — the one that bites every team about six weeks in — is audit logging that a compliance auditor won't laugh at.
The MCP spec has grown up fast
When Anthropic released MCP in November 2024, the auth story was a footnote. By the March 2025 revision it was still hand-wavy. The June 2025 revision (and the November 2025 clarifications) rewrote it into a proper OAuth 2.1 resource-server model with mandatory audience binding, PKCE for public clients, and dynamic client registration.
If you built against the early spec, you already need to migrate. If you're building today, you can skip a lot of scar tissue by starting from the current auth model.
What we mean by production
Throughout this piece, production means: multi-region deploy, at least 99.9% availability, per-tenant rate limits, immutable audit trails, secret rotation without downtime, and an on-call rotation that gets paged when tools start failing. If you're building an internal tool for one team, some of this is overkill. If you're shipping MCP to customers, none of it is.
Why Salesforce specifically
Salesforce is a useful worked example because it forces every hard problem into the open. It has its own OAuth server, its own per-org rate limits, PII everywhere, and API version churn. If your MCP server can survive Salesforce, it can survive most SaaS backends.
Architecture: The Six Boxes That Actually Matter
Strip away the marketing diagrams and a production MCP server is six components. Get the boundaries between them right and everything else is refactoring.
The transport layer
MCP defines two transports: stdio (local, one process per client) and Streamable HTTP (remote, over the wire). Every production deployment uses Streamable HTTP. Stdio is for Claude Desktop plugins and local dev.
The protocol layer
This is where the SDK earns its keep. It handles JSON-RPC framing, capability negotiation, tool discovery, and resource enumeration. You almost never touch it directly — you register tools and the SDK routes calls.
The auth boundary
Every request carries a bearer token. The server validates it, extracts tenant identity and scopes, and either rejects or forwards. This boundary is where 90% of your security work lives.
The tenant-context layer
Once auth passes, you need to load the right Salesforce credentials for this tenant, pick the right database shard, and enforce the right rate limits. Doing this per-request rather than per-connection is what makes horizontal scaling work.
The upstream client
The actual Salesforce API client. Connection pooled, per-tenant, with circuit breakers around the flaky endpoints (Bulk API 2.0, we're looking at you).
The audit sink
Every tool call gets logged before the response returns. Append-only. Tamper-evident. We'll cover the specific storage choices below.
OAuth 2.1: The Actual Implementation
The June 2025 MCP auth revision aligned with the OAuth 2.1 draft (draft-ietf-oauth-v2-1-11). If you already run OAuth-protected APIs, this is familiar territory with three important twists.
Audience binding is not optional
Tokens must be bound to the MCP server's resource URI using the resource parameter (RFC 8707). This prevents a token issued for your app's REST API from being replayed at your MCP endpoint. If your identity provider doesn't support the resource parameter, you can't be spec-compliant. Auth0, Okta, and Descope all support it. Cognito support arrived in late 2024. Firebase Auth does not.
Public clients need PKCE, period
Claude Desktop, Cursor, Windsurf, and Cline are all public clients — they can't hold a client secret. They authenticate with PKCE (RFC 7636) using S256 code challenges. Your authorization server must reject the plain method. This is default behavior in Auth0 and Okta but you should verify it explicitly.
Dynamic client registration for MCP hosts
The spec recommends RFC 7591 dynamic client registration so MCP hosts can enroll themselves without a human in the loop. If you can't turn that on for compliance reasons, publish a static client_id per known host and document it. Every major MCP host respects WWW-Authenticate discovery.
const app = express(); app.use(express.json()); app.post("/mcp", async (req, res) => { // OAuth 2.1 resource-server check (RFC 8707 audience-bound) const token = req.headers.authorization?.replace("Bearer ", ""); const claims = await verifyAccessToken(token, { audience: process.env.MCP_RESOURCE_URI!, issuer: process.env.OAUTH_ISSUER!, }); if (!claims) return res.status(401).json({ error: "invalid_token" }); const server = new Server({ name: "sf-mcp", version: "1.4.0" }, { capabilities: { tools: {} } }); server.tool("sf.query", { soql: z.string().max(20_000) }, async ({ soql }) => { requireScope(claims, "sf:read"); await auditLog({ tenant: claims.tenant_id, actor: claims.sub, action: "sf.query", soql }); const rows = await salesforceClient(claims.tenant_id).query(soql); return { content: [{ type: "text", text: JSON.stringify(rows) }] }; }); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); app.listen(8787, () => console.log("MCP server on :8787")); // server.ts — Minimal production MCP server skeleton with OAuth 2.1
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import express from "express";
import { verifyAccessToken, requireScope, auditLog } from "./auth.js";
import { salesforceClient } from "./salesforce.js";
Pro Tip: Cache JWKS keys for at least an hour but honor the
Cache-Controlheader from your IdP's JWKS endpoint. We saw a 400ms tail latency spike in production traced to per-request JWKS fetches on cold Lambda instances. Warm cache dropped p99 from 480ms to 62ms.
Multi-Tenancy Without the Landmines
Every SaaS engineer thinks they know multi-tenancy until they build one. MCP adds two new failure modes: shared connection pools that leak credentials across tenants, and tool schemas that inadvertently expose one tenant's field names to another.
Choose your isolation model early
Three patterns dominate. Shared-everything with tenant IDs on every row. Shared-app-per-tenant-database. Fully siloed deployments. For an MCP server proxying Salesforce, we recommend shared-app with per-tenant Salesforce credentials stored in a KMS-encrypted vault, keyed on the OAuth token's tenant_id claim.
Connection pool discipline
Salesforce's REST API allows about 5 concurrent requests per user session before you start seeing REQUEST_LIMIT_EXCEEDED. Pool at the tenant level, not the process level. A single global pool means tenant A can starve tenant B during a big export.
Schema leakage is subtle
If you expose Salesforce object metadata as MCP resources, remember that custom objects and fields differ per org. A naive implementation lists every field the server-side connection can see — which might be a superset of what the current tenant should see. Always filter resource enumeration by the requesting tenant's connection, not the server's.
Audit Logging That Passes a SOC 2 Review
The auditor doesn't care about your Elasticsearch cluster's beautiful dashboards. They care about four things: completeness, integrity, retention, and access controls. Design for those and everything else falls out.
What to log, exactly
Every tool invocation. Every resource read. Every prompt fetch. Every auth failure. For each event, capture at minimum: timestamp (server clock, UTC), tenant ID, actor (the sub claim from the token), tool name, redacted input hash, output byte count, latency, outcome, and a correlation ID that ties this event to the upstream Salesforce API call.
Immutability and integrity
Append-only storage is the baseline. AWS S3 Object Lock in compliance mode, Azure Immutable Blob Storage, or Google Cloud Storage bucket lock — all work. Chain each record's hash into the next (a lightweight Merkle log) so tampering is detectable. If that sounds like overkill, know that HIPAA and PCI both explicitly require tamper-evident audit trails.
Retention and access
SOC 2 Type II typically wants 12 months of retained logs; HIPAA wants 6 years for PHI-related events. Segregate audit access by role. The engineers who read logs to debug should not be the same set who can delete them. Ideally, nobody can delete them.
Did You Know? The MCP specification's November 2025 revision added an optional
_metafield on every JSON-RPC message specifically to help audit-log correlation. You can stamp a request ID into_meta.requestIdand it propagates through tool responses. Not every SDK exposes this yet — the TypeScript SDK added support in 1.4.0, the Python SDK in 1.6.
Choosing an SDK: TypeScript vs Python vs Building Your Own
Anthropic ships first-party SDKs for TypeScript and Python. There are community SDKs in Go, Rust, C#, Java, and Kotlin at varying levels of maturity. Here's how the two main ones actually compare in production, as of MCP spec revision 2025-06-18.
| Feature | TypeScript SDK | Python SDK |
|---|---|---|
| Latest version (Jul 2026) | 1.4.x | 1.6.x |
| Streamable HTTP transport | Stable | Stable |
| OAuth 2.1 resource server helpers | Built-in since 1.2 | Built-in since 1.5 |
| Async runtime | Native (Promises) | asyncio required |
| Type-safety story | Excellent (Zod) | Good (Pydantic) |
| Cold-start time (Cloudflare Workers) | ~45ms | N/A |
| Cold-start time (AWS Lambda) | ~120ms | ~380ms |
| Bundle size | ~180KB | ~4MB with deps |
| Best for | Edge, serverless, TS shops | ML pipelines, data teams |
When we reach for TypeScript
Cloudflare Workers deployments. Anything edge-native. Existing Node.js codebases. Cases where the ecosystem (Zod, Hono, Auth.js) already covers half the work. The TypeScript SDK is where the reference implementations land first.
When Python makes more sense
The tools you're exposing are already Python: LangChain agents, HuggingFace pipelines, pandas transforms, or scientific-computing stacks. Bridging those to a TypeScript MCP server via subprocess is possible but joyless. Also: FastMCP, a higher-level wrapper, is Python-only and genuinely nice.
Rolling your own SDK
Don't. The wire protocol is well-specified, but you'll rebuild capability negotiation, streaming responses, session resumption, and progress notifications for no benefit. The one exception is if you're writing an MCP host (like a new IDE integration) rather than a server — hosts have more custom needs.
Deployment: Cloudflare Workers, Fargate, or Fly
Where you run an MCP server matters more than for most APIs, because MCP is chatty. A single Claude session might fire 40 tool calls in a minute. Cold starts and per-request overhead compound fast.
Cloudflare Workers
Our current default for stateless MCP servers. Cold starts around 5ms, global anycast routing, D1 for tenant metadata, R2 for audit log staging. Two caveats: CPU time per invocation is capped (50ms free tier, 30s paid), which forces you to stream long-running tool responses; and the fetch adapter to Salesforce needs custom retry logic because Workers don't have Node's default socket-level TCP retries.
AWS Fargate
The right choice when you need persistent connections to on-prem Salesforce, long-running WebSocket sessions, or specific compliance regions (GovCloud). Costs more, cold starts are slower, but the operational model is familiar to any AWS shop.
Fly.io and Railway
Good middle ground. Regional deploys, persistent VMs, generous CPU. Fly's Machines API is particularly nice for spinning up a per-tenant MCP instance when you need hard isolation.
Pro Tip: Whatever platform you choose, put the MCP server behind a real API gateway (Cloudflare, Kong, AWS API Gateway) — not naked on the internet. You'll want the DDoS protection, per-tenant rate limits, and the ability to shed load without redeploying the server itself.
Operating in Production: Pitfalls, Observability, Security
Every team we've worked with hit at least four of these. Learn from other people's outages.
Assuming clients handle progress correctly
The spec has progress notifications for long-running tools. Claude Desktop respects them beautifully. Cursor ignored them until early 2026. Windsurf still buffers everything. If your tool takes more than 15 seconds, chunk the response and hope for the best.
Trusting the client's tool schema cache
Clients cache your tools/list response aggressively. When you add a tool, existing sessions won't see it. Use notifications/tools/list_changed to invalidate — most clients now respect it.
Underestimating token expiry pain
Salesforce OAuth tokens expire in an hour by default. If your MCP server holds a session open for eight hours, you need silent refresh logic that doesn't drop in-flight requests. Get this wrong and users see mysterious 401s every 60 minutes.
Forgetting about the initialize handshake
Every MCP connection starts with an initialize call that negotiates protocol version and capabilities. If your load balancer drops the connection mid-handshake, some clients retry silently, others fail hard. Set your idle timeout above 30 seconds.
Observability: Metrics that predict outages
You can log everything and still fly blind. These four metrics catch 80% of incidents before users notice.
Per-tool latency percentiles
Not aggregate. Per tool. A p95 spike on sf.query while sf.describe is fine tells you Salesforce's SOQL endpoint is degraded, not your server. Split by tenant too when you scale past 50 customers.
Auth failure rate by IdP
Auth failures cluster. When your IdP has a bad deploy, you'll see the same error signature across tenants for a few minutes. A simple ratio (401s / total requests) alerted at 2% catches this fast.
Tool call error budget
Budget: 99.5% of tool calls succeed. Track burn rate. Alert when you'll exhaust the monthly budget in the next 6 hours. This is standard SRE work but MCP servers often skip it because they feel like glue code.
Session length distribution
A sudden shift in session length (all sessions ending at 5 minutes when they used to average 30) means clients are hitting a timeout they didn't have yesterday. Usually a load balancer config change on your side.
Security hardening beyond OAuth
OAuth handles authentication. It doesn't handle everything else. These are the additional controls you'll need before your security team signs off.
Prompt injection through tool responses
An attacker who can write to a Salesforce field can inject instructions into a tool response that the LLM then follows. This is not theoretical — we've seen it in the wild. Mitigate with output sanitization, tool-response labeling, and by making destructive tools require explicit human confirmation on the client side.
Scope minimization on OAuth tokens
Don't request full scope from Salesforce because it's convenient. Request the minimum: api for read, api refresh_token if you need offline access. If a customer's Salesforce admin sees your app requesting full, they'll bounce your integration through IT review for a month.
Secret rotation without downtime
Support at least two active signing keys and two active client secrets simultaneously. Rotate one at a time. This lets you rotate on a schedule without a maintenance window. AWS Secrets Manager and HashiCorp Vault both support versioned secrets natively.
Heads up: If you build tools that write to Salesforce (create leads, update opportunities), require the client to send a confirmation token that came from a user gesture. LLMs will happily call destructive tools if you let them. The MCP spec's
elicitationfeature (added in the 2025-06-18 revision) is the mechanism for this.
Testing without losing your mind
Testing MCP servers is harder than testing REST APIs because the client behavior is part of the contract. Here's the layered approach that actually works.
Unit tests on tool handlers
Each tool is a function. Test it in isolation with a mock Salesforce client. This catches 60% of bugs. Fast, cheap, run on every commit.
Integration tests against MCP Inspector
Anthropic's MCP Inspector is a browser-based test client that speaks the full protocol. Run your server locally and drive it from Inspector for tool discovery, resource enumeration, and error paths. Catches SDK misuse fast.
End-to-end tests with real clients
Once a week, run a scripted Claude Desktop session (via the CLI now that Anthropic has one) that exercises your top 5 tool paths. This catches client-server compatibility drift, which changes every few weeks as clients update.
Frequently Asked Questions
What is MCP and why should a Fortune 500 architect care?
The Model Context Protocol is an open standard from Anthropic (released November 2024, current spec revision 2025-06-18) for connecting LLMs to tools and data sources. Enterprise architects care because it decouples the tool integration from the model. Build one MCP server for Salesforce and every MCP-compatible client (Claude, Cursor, Windsurf, Cline, ChatGPT Enterprise as of early 2026) can use it. That's the same argument that made LSP win for editor tooling.
Does the MCP spec mandate OAuth 2.1 or is it optional?
For remote (HTTP-based) servers, the June 2025 auth revision made OAuth 2.1 the required auth model. Stdio servers running locally can rely on process boundaries and don't need OAuth. If you're deploying to customers over the internet and skipping OAuth, you're not spec-compliant and your MCP server will fail interop tests with newer clients.
Can I use MCP with a self-hosted LLM instead of Claude?
Yes. MCP is client-agnostic. Any host that speaks the protocol works. Ollama has an MCP bridge, LM Studio added native support in early 2026, and open-source hosts like OpenWebUI expose MCP tools to any backing model. The server doesn't know or care which model the client is running.
How do I handle Salesforce's per-org API limits from an MCP server?
Track API usage per tenant using Salesforce's Sforce-Limit-Info response header and implement a token bucket that slows requests before you hit the wall. Reserve about 20% of the org's daily quota as headroom for other integrations. Also cache describeSObjects results aggressively — those calls are cheap individually but frequent enough to blow through quota during a busy Claude session.
What's the difference between MCP and function calling?
Function calling is a model-specific mechanism: you send tool definitions in each request, the model returns a call, you execute it and send the result back. MCP is a network protocol: a server exposes tools that any compliant client can discover and invoke. Function calling lives inside your app; MCP lives on the wire and enables cross-vendor tool ecosystems. Compare architectural implications in our companion piece on agent skills vs MCP.
Should I run one MCP server per tenant or one shared server?
Almost always one shared server with per-request tenant context. Per-tenant deployments are viable when you have fewer than 20 large customers with hard isolation requirements (defense, healthcare on-prem). Above that, the operational cost of managing N deployments crushes any perceived security benefit. A well-designed shared server with proper OAuth and connection isolation is more secure in practice than 200 loosely-managed per-tenant instances.
What audit-log fields do SOC 2 auditors specifically look for?
In our experience: timestamp with source (server clock), authenticated actor identity (not just IP), resource accessed, action performed, outcome (success or failure with reason code), and correlation to downstream systems. Auditors also ask for evidence that logs cannot be modified — an append-only store with cryptographic chaining or bucket immutability settings satisfies this. Retention should meet or exceed your customers' contractual requirements, typically 12 to 24 months.
How do I test my MCP server against multiple clients?
Start with MCP Inspector for protocol correctness. Add Claude Desktop and Cursor for the two biggest client surfaces. Windsurf and Cline share enough behavior with Cursor that testing one usually covers both. Every 30 days, walk through a checklist of tool discovery, tool invocation, resource listing, prompt fetching, and error handling on each client. Client behavior drifts fast — what worked last month may not work today.
Is there a managed MCP hosting service I can use instead of building my own?
As of mid-2026, Cloudflare has a Workers-based MCP hosting product, Vercel supports MCP servers as first-class deploy targets, and several dedicated startups (Composio, Arcade, others) offer managed hosting with pre-built integrations. If your use case is generic (GitHub, Notion, Slack), managed is probably faster. For Salesforce with strict enterprise requirements, most teams still self-host to keep the audit and compliance surface under their control.
Where can I find production-quality reference implementations?
The official modelcontextprotocol GitHub organization has reference servers in TypeScript and Python. Anthropic maintains examples for filesystems, GitHub, Postgres, Slack, and Google Drive. The official protocol docs also link to a curated list of community servers, though quality varies widely. For deeper learning paths, our MCP server skills hub catalogues verified skills by task type.
What to build next
If you've read this far, you're probably about to write code. A few closing suggestions before you do.
Start with the OAuth boundary. Get that right first, before you write a single tool handler. Everything else compounds on top of it. Then pick one Salesforce object (Contact is a good choice), expose read-only tools for it, and get the full audit-log pipeline running end-to-end. Only then add writes and additional objects.
For patterns beyond MCP itself, our companion write-ups on MCP server skills and LLM evaluation skills cover the adjacent tooling most teams need next. If you're weighing MCP against alternatives, the agent skills versus MCP comparison lays out where each fits.
The protocol will keep moving. Bookmark the Anthropic MCP announcement page and the spec site. Both are updated when the reference implementations ship changes worth knowing about.












