Skip to content

How to Build a Custom MCP Server for Claude to Access SaaS APIs

A pragmatic engineering guide to building a custom MCP server that connects Claude to any SaaS API—covering JSON-RPC, OAuth, tool generation, and rate limits.

Roopendra Talekar Roopendra Talekar · · 11 min read
How to Build a Custom MCP Server for Claude to Access SaaS APIs

Building a custom Model Context Protocol (MCP) server for Claude means implementing a JSON-RPC 2.0 endpoint that dynamically exposes third-party SaaS API operations as callable tools, handles OAuth token lifecycles per tenant, and propagates rate limit errors cleanly back to the model. That single sentence hides about six months of engineering work if you build it end-to-end.

If you need to connect Claude to external SaaS APIs like Salesforce, Jira, or HubSpot, native AI connectors and hardcoded API wrappers are no longer viable for production enterprise deployments. They fail security reviews, struggle with dynamic data models, and break under the unpredictable request volume of agentic workflows.

This guide walks through the actual architecture, the non-obvious protocol gotchas, and where the hidden costs live. We will bypass the marketing surface and examine the actual JSON-RPC handshake, explore how to dynamically generate tools from OpenAPI specifications, and address the architectural bottlenecks that silently break most production MCP deployments: multi-tenant OAuth state management and rate limit propagation.

We are targeting engineering and product leads shipping AI features that need to read Salesforce records, mutate Jira issues, query HubSpot pipelines, or call any of the other thousands of SaaS APIs your customers use. If your bar is "works in a demo," native connectors are fine. If your bar is "passes InfoSec review and survives a Monday morning of agent traffic," keep reading.

Why Build a Custom MCP Server for Claude?

The architectural shift toward the Model Context Protocol is permanent. MCP is now the de facto interface between LLMs and external systems. Anthropic's MCP 2026-07-28 specification represented the most substantial revision of the standard since its introduction, arriving as MCP surpassed 400 million monthly SDK downloads. Once OpenAI adopted MCP in early 2025 and Google followed in early 2026, the standards war was effectively over. If you want a deeper primer on the protocol itself, see our 2026 guide to MCP for SaaS PMs.

A custom MCP server gives you three things a native Claude connector cannot:

  • Full API surface area. Native connectors expose a curated handful of operations. Your server can expose every endpoint your customers actually pay for—custom Salesforce objects, threaded Slack metadata, complex Jira transitions.
  • Per-tenant identity. Each customer's Claude session hits the SaaS API using that specific customer's OAuth token, not a shared global service account.
  • Governance. You control exactly which methods are exposed (read-only vs. write), which resources are visible, and which AI agents get access.

Building custom, point-to-point API connectors for Claude is an engineering write-off. As we've noted in our guide to building MCP servers for AI agents, MCP standardizes the interface. Claude queries the server for available tools, understands their JSON Schema definitions, and invokes them natively. However, you are now responsible for a distributed system that touches customer credentials, third-party rate limits, and an evolving protocol spec.

Core Architecture of an MCP Server

An MCP server is, at its core, a JSON-RPC 2.0 endpoint that answers a fixed set of protocol methods. (If you are writing this from scratch, our hands-on architecture guide provides a deeper code-level walkthrough). The wire format is boring on purpose—MCP owes a lot of its adoption to the fact that the protocol itself is simple: it uses JSON-RPC over standard transports that any language and any runtime can support.

The core methods you must implement:

Method Purpose
initialize Handshake phase. The client announces itself, and the server returns its protocol version and capabilities (confirming tool support).
notifications/initialized Client confirms the handshake is complete. The server should return an HTTP 202 with no body.
tools/list Returns the tool catalog. Called on connect and after a notifications/tools/list_changed event.
tools/call Executes a single tool. Arguments are passed as a flat JSON object.
ping Standard health check.

For an enterprise B2B SaaS deployment facing Claude, you want HTTP Streamable transport, not stdio. Stdio is for local desktop tools; HTTP is what remote clients like Claude Desktop's custom connectors and enterprise agent orchestrators expect. The 2026-07-28 spec doubled down on this, fundamentally rearchitecting MCP around a stateless core and replacing the previous bidirectional, session-dependent model with a lightweight request/response framework. If your design still assumes long-lived sessions per client, you are already fighting the spec.

The Initialization Handshake

Before Claude can read CRM data or update a ticket, it must understand what the server can do. This happens during the initialize phase. Keep the transport stateless. Every POST to /mcp should be independently authenticatable and independently executable. That gives you horizontal scaling for free and simplifies your incident response.

sequenceDiagram
    participant Claude as Claude (MCP Client)
    participant Server as Custom MCP Server
    participant SaaS as Vendor SaaS API
    
    Claude->>Server: POST /mcp {method: initialize}
    Server-->>Claude: JSON-RPC Result (capabilities, protocolVersion)
    Claude->>Server: POST /mcp {method: notifications/initialized}
    Claude->>Server: POST /mcp {method: tools/list}
    Server-->>Claude: JSON-RPC Result (Array of Tools)
    Claude->>Server: POST /mcp {method: tools/call, name: update_a_contact}
    Note over Server: Resolve flat namespace & inject tenant OAuth token
    Server->>SaaS: PATCH /api/v3/contacts/98765
    SaaS-->>Server: 200 OK (Contact Data)
    Server-->>Claude: JSON-RPC Result (Tool Response)

Following initialization, Claude calls tools/list. Your server returns an array of available tools, each containing a name, description, and a JSON Schema defining the expected arguments. This sounds straightforward, but in practice, hardcoding these tool definitions is a maintenance nightmare.

Challenge 1: Dynamic Tool Generation and the Flat Input Namespace

If you manually write static tool definitions for a SaaS API, you will constantly be updating them as the vendor adds fields or your customers create custom objects. Production-grade MCP servers generate tools dynamically from API documentation or OpenAPI specifications.

Generating Tools from API Schemas

Instead of writing static configurations, your server should parse the upstream API's schema at runtime. Each tool needs a name, a description, and a JSON Schema for arguments. A sensible naming convention keeps Claude's tool selection accurate:

function toolName(vendor: string, resource: string, method: string): string {
  if (method === 'list') return snakeCase(`list all ${vendor} ${resource}`)
  if (method === 'get')  return snakeCase(`get single ${vendor} ${singular(resource)} by id`)
  if (method === 'create') return snakeCase(`create a ${vendor} ${singular(resource)}`)
  // ... update, delete, custom
}
// Produces: list_all_hubspot_contacts, get_single_hubspot_contact_by_id, ...
Tip

Documentation as a Quality Gate: Descriptions are not decorative. They are the primary signal Claude uses to pick a tool. Do not expose every API endpoint blindly. Require a documentation record or description for an endpoint before exposing it as a tool. If an endpoint lacks a description, the LLM will not know how to use it, leading to hallucinations.

Solving the Flat Input Namespace Problem

When Claude decides to invoke a tool, it sends a tools/call request. Here is the architectural quirk you must handle: MCP sends all arguments as a single, flat JSON object.

If a vendor API requires a path parameter (contact_id), a query parameter (include_associations), and a JSON body (first_name), Claude will send them all mixed together in one object:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "update_a_contact",
    "arguments": {
      "contact_id": "98765",
      "include_associations": "true",
      "first_name": "Alice"
    }
  }
}

Your MCP server must untangle this flat namespace before proxying the request to the SaaS API. To do this, your server must maintain the original JSON Schemas for both the query parameters and the request body. You need a splitter that reconstructs the vendor call from the flat input:

function splitArgs(args: Record<string, unknown>, querySchema: JSONSchema, bodySchema: JSONSchema) {
  const queryKeys = Object.keys(querySchema.properties ?? {})
  const bodyKeys  = Object.keys(bodySchema.properties  ?? {})
  return {
    query: pick(args, queryKeys),
    body:  pick(args, bodyKeys),
  }
}

Two edge cases will bite you here. First, list methods need auto-injected limit and next_cursor properties, and your next_cursor description must tell the model to echo cursor values back verbatim—LLMs love to "clean up" base64 strings. Second, for get, update, and delete, inject an id property with a description that includes the word "Required" so Claude asks for it before calling.

For a deeper look at generating tool catalogs from docs rather than hand-writing them, see our auto-generated MCP tools guide.

Challenge 2: Multi-Tenant OAuth and Token Lifecycles

Building a single-tenant MCP server for internal use is trivial. Building a multi-tenant MCP server where hundreds of your customers are connecting their own SaaS accounts is highly complex. Every customer that connects Salesforce gives you an OAuth access token and a refresh token. Multiply by 500 customers and 20 integrations and you have 10,000 tokens to store, rotate, and revoke without ever leaking one into a log.

The OWASP MCP Top 10 lists token mismanagement as MCP01 for good reason. Between January and February 2026 alone, researchers filed over 30 CVEs targeting MCP servers, clients, and tooling. Forty-three percent were shell injections, and token exposure was close behind. Hard-coded credentials, long-lived tokens, and secrets stored in model memory or protocol logs can expose sensitive environments to unauthorized access.

The Production OAuth Checklist

If you manage OAuth yourself, your MCP server must handle the following:

  1. Never store raw credentials. Encrypt tokens at rest with per-tenant keys. Anything less fails a SOC 2 review.
  2. Refresh proactively. Refresh tokens shortly before expiry, not after a 401. Waiting for the 401 creates a race condition between the LLM's tool call and your refresh path.
  3. Serialize refreshes per account. Two concurrent tool calls should not both try to refresh the same token; the second refresh will invalidate the first. A per-account lock solves this.
  4. Handle vendor quirks. Some vendors rotate the refresh token on every use. Some invalidate the old access token immediately, others keep both valid for a grace period. Encode this per-integration.
  5. Reauth signals. When a refresh returns invalid_grant, mark the account needs_reauth and short-circuit future MCP calls with a helpful error instead of a stack trace.

The Per-Account Token URL Pattern

To solve this securely, do not rely on generic API keys or global session state. Instead, use a per-account MCP token URL pattern.

When a customer connects their SaaS account to your platform, generate a unique, cryptographically secure URL specifically for that integration instance (e.g., https://your-api.com/mcp/a1b2c3d4...).

This URL acts as the authentication boundary. When Claude connects to that specific URL, the server hashes the token, looks up the corresponding tenant and integration in a fast key-value store, and automatically applies the correct OAuth credentials to all subsequent proxy requests.

This architecture ensures the MCP server remains completely stateless. The URL itself encodes the tenant context, preventing cross-tenant data bleed entirely. If your MCP server is going to touch regulated data, also read our writeup on zero-data-retention MCP architectures—your storage and logging model is what an InfoSec team will actually audit.

Challenge 3: Handling SaaS API Rate Limits

Rate limits are the single most common production failure mode for AI agents using MCP. When a human uses an application, they might trigger one API call per second. When Claude executes a complex reasoning loop, a single user prompt can fan out into 5–15 sequential tool calls. If your agent is listing contacts, enriching each one, then updating a CRM field, you are already at 20+ calls per user turn. This exhausts per-API budgets in seconds, resulting in HTTP 429 Too Many Requests errors.

Do Not Absorb Rate Limits

When engineers first build an MCP server, they often try to handle rate limits by implementing exponential backoff inside the server. They catch the 429 error, pause the thread, and retry the request.

This is an anti-pattern for LLM integrations.

If your server sleeps for 30 seconds waiting for a rate limit window to clear, the HTTP connection between Claude and your server will likely time out. Even worse, you consume expensive serverless execution time just waiting. Silent retries make the model think a call succeeded when it didn't, or worse, complete a mutation twice.

Propagating Rate Limits to the LLM

Instead of absorbing the error, your MCP server must pass the HTTP 429 directly back to the caller. However, you cannot just throw a generic error. You must normalize the upstream vendor's proprietary rate limit headers into the standardized IETF format.

When the SaaS API returns a 429, your server should return an MCP error response that includes:

  • ratelimit-limit: The total request quota.
  • ratelimit-remaining: The remaining quota (usually 0 in this case).
  • ratelimit-reset: The timestamp (or seconds) when the quota resets.
// Response envelope for a 429 pass-through
res.status(429)
   .header('ratelimit-limit', '1000')
   .header('ratelimit-remaining', '0')
   .header('ratelimit-reset', '42')  // seconds until reset
   .json({ error: 'rate_limit_exceeded', vendor: 'salesforce', retry_after_seconds: 42 })

By passing these normalized headers back to the client, Claude and other well-behaved orchestration layers can intelligently pause their own execution loop, inform the user of the delay, and resume exactly when the reset window opens. Your job is to report reality accurately, not to hide it. Additionally, ensure you circuit-break at the account level, not globally—one noisy tenant should not degrade every other tenant on your MCP server.

The Managed Alternative: Auto-Generated MCP Servers per Account

If your engineering team is evaluating how to connect Claude to external APIs, you face a distinct build vs. buy decision. Building a custom MCP server requires engineering dynamic schema parsers, maintaining multi-tenant OAuth state, and normalizing rate limit headers across dozens of inconsistent vendor APIs. After you have built this for one integration, you have to do it 50 more times for 50 more vendors. That is the honest math on "custom."

For B2B SaaS companies, the alternative is utilizing a managed unified API platform that natively supports MCP. Platforms like Truto completely abstract this infrastructure layer. When your customer connects an integration (like Salesforce or Zendesk), Truto automatically generates a secure, per-account MCP server URL.

Creating one is a single API call:

curl -X POST https://api.truto.one/integrated-account/$ID/mcp \
  -H "Authorization: Bearer $TRUTO_API_TOKEN" \
  -d '{
    "name": "Support-only Zendesk MCP",
    "config": { "methods": ["read"], "tags": ["support"] },
    "expires_at": "2026-09-01T00:00:00Z"
  }'
# → { "url": "https://api.truto.one/mcp/a1b2c3...", ... }

This managed approach provides several architectural advantages:

  • Documentation-Driven Tool Creation: Tools are automatically generated dynamically from each integration's resource definitions and JSON Schema documentation. When you add a new field or endpoint upstream, tools update on the next tools/list call—no redeploy required.
  • Built-in Filtering: You can restrict the generated MCP server to specific operations. If you only want Claude to browse a customer's Zendesk but never delete a ticket, you apply a read-only method filter. Tag filtering groups tools by functional area (support, directory, sales) so one server surfaces a focused toolset instead of 200 undifferentiated tools.
  • Stateless Execution: The platform handles the OAuth token refresh lifecycle and secure credential storage. The MCP execution path remains stateless, passing payloads entirely in-memory to satisfy strict enterprise InfoSec requirements.
  • Standardized Error Handling: Upstream HTTP 429 errors are passed cleanly to the caller with normalized IETF ratelimit-* headers, ensuring predictable retry behavior for your AI agents.
  • Enforced TTLs: Set an expires_at deadline, and both the token store entry and a scheduled cleanup job disappear the server automatically when it is no longer needed.

For a deeper walkthrough of the trade-offs, see our comparison of managed MCP versus custom builds.

Where to Go From Here

A custom MCP server for Claude is a solved engineering problem, but it is not a small one. The protocol is stable, the transports are standardized, and the reference SDKs are solid. What eats teams alive is the surrounding infrastructure: multi-tenant OAuth, per-account rate limit isolation, dynamic tool catalogs, and the security posture required to survive an InfoSec review.

If you are prototyping, start with the official TypeScript or Python SDK, wire up one vendor, and get a stateless HTTP endpoint responding to tools/list and tools/call. That gets you to a working Claude connector in an afternoon.

If you are productizing, decide early whether integration count is going to grow. Building the tenth integration costs more than building the first because you also own the runtime that keeps them alive. By standardizing on managed MCP infrastructure, you can focus on building intelligent agent workflows rather than maintaining the plumbing that connects them to the outside world.

FAQ

What is an MCP server for Claude?
An MCP (Model Context Protocol) server is a JSON-RPC 2.0 endpoint that exposes external tools and APIs to Claude. Claude connects to it, calls `tools/list` to discover available operations, then invokes them with `tools/call`. It is the standard way to give Claude access to SaaS APIs like Salesforce, HubSpot, or Jira.
Do I need to build a custom MCP server if Claude already has native connectors?
Native connectors expose only a small curated slice of a vendor's API. If your use case needs custom fields, admin actions, or the full API surface area, a custom MCP server (or a managed layer that generates one per account) is the only path.
How do I handle OAuth for multiple customers in one MCP server?
Use a per-account token URL pattern. Generate a cryptographically secure URL for each connected integration that acts as the authentication boundary. Encrypt tokens at rest, refresh them proactively before expiry, and serialize refreshes per account to avoid race conditions.
Should my MCP server retry on rate limit errors?
No. Do not absorb rate limits with server-side retries. Pass HTTP 429s directly to the caller with normalized `ratelimit-limit`, `ratelimit-remaining`, and `ratelimit-reset` headers per the IETF draft. Let the client or the model decide the backoff strategy.
Why shouldn't I hardcode tools in my MCP server?
Hardcoding tools creates a massive maintenance burden as vendor APIs change. Production MCP servers dynamically generate tool definitions and JSON Schemas directly from API documentation or OpenAPI specs, mapping the LLM's flat input namespace into the correct path, query, and body parameters.

More from our Blog