Skip to content

How to Build MCP Servers for AI Agents: 2026 Hands-On Architecture Guide

A definitive, code-first guide to building MCP servers for AI agents in 2026. Learn how to auto-generate tools, handle flat namespaces, and manage rate limits.

Uday Gajavalli Uday Gajavalli · · 19 min read
How to Build MCP Servers for AI Agents: 2026 Hands-On Architecture Guide

If you are a senior product manager or engineering leader at a B2B SaaS company evaluating how to connect your application to external AI agents, the initial question has been answered for you. Building custom, point-to-point API connectors for every new AI framework is a massive engineering write-off. The market has standardized entirely on the Model Context Protocol (MCP).

We will skip the marketing surface and walk through the actual JSON-RPC handshake, ship working TypeScript, cover dynamic tool generation from OpenAPI, and address the two gotchas that quietly break most production MCP deployments: the flat input namespace and rate-limit propagation.

The shift toward MCP is structural and permanent. By the end of 2026, Gartner projects that 40% of enterprise applications will feature task-specific AI agents, up from less than 5% in 2025. Over 57% of enterprises already have AI agents in production today, evolving from basic chat interfaces into operational systems capable of multi-step reasoning. To support this, MCP experienced a 4,750% growth rate in just 16 months, reaching over 97 million monthly SDK downloads. It is now the default interface between agents and external data.

MCP is no longer a research protocol. The 2026-07-28 release candidate is the largest revision since MCP's launch and delivers a stateless core that scales on ordinary HTTP infrastructure, extensions like MCP Apps and Tasks, and authorization aligned with OAuth and OpenID Connect deployments. If your architecture still assumes stateful sessions and per-request Mcp-Session-Id headers, you are already behind the curve.

Your choice is no longer whether to support MCP, but how to architect your infrastructure. Do you build and host your own custom MCP servers per integration, or do you rely on a managed platform? This guide provides a highly technical, code-level walkthrough on how to build production-grade MCP servers in 2026.

The 2026 Shift: Why Custom Point-to-Point AI Connectors Are Dead

Before MCP, exposing your platform to AI agents meant writing custom function-calling schemas for OpenAI, Anthropic, Google, and open-source models. You had to maintain separate integration code for LangChain, LlamaIndex, AutoGen, and CrewAI. Every time an upstream SaaS vendor changed an API endpoint, you updated five different schemas.

Anthropic identified this as the "N × M integration problem" that arises when integrating N tools (such as Slack, GitHub, or databases) with M model front-ends (like ChatGPT, Gemini, or Claude). MCP collapses that matrix into N+M. You define your tools once on the server, and any compliant client translates them into its native function-calling format.

Adopting MCP reduces multi-tool AI agent integration development time by 60-70% because it eliminates the per-provider integration tax. The ecosystem has caught on rapidly: Slack, GitHub, Google, Salesforce, Stripe, HubSpot, Shopify, Notion, Linear, Sentry, Figma, Webflow, Cloudflare, Postman, WooCommerce, and many others have built official or community-maintained servers, and official SDKs exist for TypeScript and Python.

However, building an MCP server is not just about wrapping a REST API in a new JSON schema. You are building infrastructure for autonomous machines. These machines will aggressively poll endpoints, invent query parameters that do not exist, and fail to understand basic pagination unless explicitly instructed. Your MCP server must be defensively engineered to handle these realities.

Understanding MCP Server Architecture for B2B SaaS

At its core, an MCP server is a JSON-RPC 2.0 interface. The wire format is the same lightweight protocol used in many developer tools. It exposes three primitives to LLM clients: tools (functions the model can call), resources (readable content), and prompts (reusable templates).

While the protocol supports local communication, B2B SaaS applications require the HTTP-Streamable transport layer. This allows remote AI agents to connect over standard web protocols. The transport landscape has evolved significantly:

  • stdio: Local subprocess communication, primarily useful for developer tooling running on the same machine (like IDE integrations).
  • Streamable HTTP (2025-03-26): Introduced to replace earlier HTTP+SSE transports, using a single MCP endpoint supporting POST and GET, with optional Server-Sent Events. Sessions were tracked with the Mcp-Session-Id header.
  • Stateless HTTP (2026-07-28 RC): This release candidate changes the layer significantly by making the transport stateless. It removes protocol-level sessions and the Mcp-Session-Id header, meaning the same request can be answered by any server instance behind ordinary HTTP infrastructure.

For enterprise SaaS, stateless serverless deployments are heavily preferred. Instead of maintaining persistent WebSocket connections or sticky sessions, each request to the /mcp endpoint is authenticated, processed, and closed. This allows you to horizontally scale MCP behind a standard load balancer. Multi-tenant routing becomes trivial because every request carries its own auth context.

sequenceDiagram
    participant Agent as "MCP Client (Claude/ChatGPT)"
    participant Server as "MCP Server"
    participant API as "Upstream API (Salesforce/HubSpot)"
    Agent->>Server: POST /mcp (initialize)
    Server-->>Agent: Capabilities (tools, resources, protocolVersion)
    Agent->>Server: POST /mcp (notifications/initialized)
    Agent->>Server: POST /mcp (tools/list)
    Server-->>Agent: Array of JSON Schemas
    Agent->>Server: POST /mcp (tools/call name=create_contact)
    Server->>API: HTTP POST /contacts
    API-->>Server: HTTP 201 Created
    Server-->>Agent: Tool execution result {content:[{type:text,text:...}]}

For a foundational overview of what an MCP server actually is, see our architecture guide for SaaS PMs.

Hands-On Code: Building Your First MCP Server

To build a production-grade MCP server, you need a web framework (like Express, Hono, or Fastify) and the official @modelcontextprotocol/sdk. We will use the official TypeScript SDK and implement a stateless approach, meaning the server initializes a fresh McpServer instance for every request. This is highly advantageous for edge runtimes and serverless environments.

1. Scaffold the Project

mkdir mcp-server && cd mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod express
npm install -D typescript @types/node @types/express tsx
npx tsc --init

2. Define the Server and Register a Tool

Here we define a basic Express server. We register a statically typed tool using Zod to represent the expected input from the AI agent.

// src/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';
 
const server = new McpServer({
  name: 'acme-crm-mcp',
  version: '1.0.0',
});
 
// Register a static tool
server.tool(
  'create_contact',
  'Creates a new contact record in the Acme CRM.',
  {
    first_name: z.string().describe("The contact's first name"),
    last_name: z.string().describe("The contact's last name"),
    email: z.string().email().describe("The contact's email address"),
    company: z.string().optional().describe("Optional company name"),
  },
  async ({ first_name, last_name, email, company }) => {
    const res = await fetch('https://api.acme.example/contacts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.ACME_TOKEN}`,
      },
      body: JSON.stringify({ first_name, last_name, email, company }),
    });
 
    if (res.status === 429) {
      // Surface the rate limit directly; do not silently retry.
      return {
        isError: true,
        content: [{
          type: 'text',
          text: `Rate limited. Retry-After: ${res.headers.get('retry-after')}`,
        }],
      };
    }
 
    const body = await res.json();
    return {
      content: [{ type: 'text', text: JSON.stringify(body) }],
    };
  }
);

3. Wire Up the Stateless HTTP Transport

const app = express();
app.use(express.json());
 
app.post('/mcp', async (req, res) => {
  // Stateless mode: sessionIdGenerator is undefined
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });
  
  res.on('close', () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});
 
app.listen(3000, () => console.log('MCP up on :3000'));

Stateless mode is the 2026 default. Each POST /mcp request creates a fresh server instance, avoiding cross-request state and letting you scale horizontally without sticky routing.

4. Test the JSON-RPC Handshake

A raw request to list the available tools looks like this:

curl -X POST http://localhost:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }'

For interactive debugging, use the official MCP Inspector (npx @modelcontextprotocol/inspector). It provides a UI for listing tools, testing invocations, and inspecting JSON-RPC traffic without involving an LLM.

Tip

Tool errors belong in the result, not the transport. Tool errors should be reported within the result object, not as MCP protocol-level errors. This allows the LLM to see and potentially handle the error. Return { isError: true, content: [...] } instead of throwing an unhandled exception.

Auto-Generating MCP Tools from API Documentation

Hardcoding tools like create_contact works well for internal tools or a quick demo. However, if you are building an integration platform that connects to hundreds of SaaS APIs (like Salesforce, HubSpot, or Jira), hardcoding tools using Zod schemas is a losing battle. It falls over the moment you have 200 endpoints across 40 integrations, each with its own schema evolution.

The scalable pattern is documentation-driven tool generation. You must build a pipeline that reads OpenAPI specifications or internal documentation records and transforms them into MCP-compliant JSON schemas dynamically on every tools/list or tools/call request.

A minimal generator using SwaggerParser looks like this:

import SwaggerParser from '@apidevtools/swagger-parser';
import { snakeCase } from 'lodash';
 
type OpenAPIOperation = {
  operationId?: string;
  summary?: string;
  parameters?: any[];
  requestBody?: any;
};
 
async function registerToolsFromSpec(server: McpServer, specUrl: string) {
  const api: any = await SwaggerParser.dereference(specUrl);
 
  for (const [path, methods] of Object.entries<any>(api.paths)) {
    for (const [httpMethod, op] of Object.entries<OpenAPIOperation>(methods)) {
      const name = snakeCase(op.operationId ?? `${httpMethod}_${path}`);
      const inputSchema = buildJsonSchema(op);
 
      server.tool(
        name,
        op.summary ?? name,
        inputSchema,
        async (args) => callUpstream(httpMethod, path, args)
      );
    }
  }
}

buildJsonSchema walks parameters and requestBody.content ['application/json'].schema and merges them into a single flat input object.

To do this reliably in production, you must adhere to four design principles:

1. Never Cache Generated Tools Statically

Regenerate tools on every tools/list request so schema changes propagate immediately. Tools should always reflect the latest state of the integration's schemas.

2. The Documentation Gate

Do not expose every single endpoint of an upstream API to an LLM. Agents get confused by massive tool lists. If a specific method lacks a human-readable description in your documentation database, skip it entirely. Bad docs produce bad tool calls. Documentation acts as the quality gate for AI readiness.

3. Generate Deterministic Tool Names

LLMs rely heavily on tool names to understand intent. Generate highly descriptive, snake_case strings using the integration label, the resource name, and the method. list_all_hubspot_contacts outperforms hubspotContactsList for LLM tool selection accuracy.

4. Schema Building and Instruction Injection

When parsing your documentation into JSON Schema, you must inject explicit instructions for the LLM. AI agents are notoriously bad at handling pagination cursors. For list operations, automatically append a next_cursor property to the query schema with this exact description:

"The cursor to fetch the next set of records. Always send back exactly the cursor value you received without decoding, modifying, or parsing it. This can be found in the response of the previous tool invocation."

5. Tag Filtering

Enterprise deployments often require scoped access. You might want an AI agent to only access "support" tools (tickets, articles) and not "sales" tools (deals, pipelines). During the tools/list generation phase, intersect the requested tags with the resource tags defined in your integration configuration. If there is no overlap, drop the tool from the list.

Info

Architectural Note: Truto dynamically generates MCP tools on the fly from API documentation on every request, ensuring tools are never statically cached and always reflect the latest integration schemas. Learn more in our auto-generated MCP tools guide.

Handling Authentication and the Flat Input Namespace

Once your tools are generating dynamically, you will hit two major architectural hurdles that break more MCP servers than any other issue: parsing the agent's arguments and securing the endpoints.

The Flat Input Namespace Problem

When an MCP client calls a tool via tools/call, it passes all arguments in a single, flat JSON object. The agent does not know the difference between a URL path parameter, a query string parameter, or a JSON body field.

For example, if the agent wants to update a contact's first name, it will send:

{
  "id": "cont_123",
  "first_name": "Alice",
  "include_metadata": "true"
}

Your proxy API expects id in the path, include_metadata in the query string, and first_name in the request body. To solve this, your MCP server must intelligently split the caller arguments based on the JSON schema property keys you generated earlier.

function splitParams(
  args: Record<string, unknown>,
  querySchema: any,
  bodySchema: any
) {
  const queryKeys = Object.keys(querySchema?.properties ?? {});
  const bodyKeys = Object.keys(bodySchema?.properties ?? {});
 
  const query: Record<string, unknown> = {};
  const body: Record<string, unknown> = {};
 
  for (const [k, v] of Object.entries(args)) {
    if (queryKeys.includes(k)) query[k] = v;
    else if (bodyKeys.includes(k)) body[k] = v;
  }
  return { query, body };
}

Edge case: If a property name appears in both schemas (e.g., an id in the query and an id in the body), pick one and document the precedence. Query-wins is the pragmatic default because path/query parameters are usually the primary identifier fields.

Dual-Layer Authentication

By default, most tutorials show an MCP URL containing a secure token in the path (e.g., https://api.example.com/mcp/a1b2c3d4...). That is fine for local prototypes and terrible for production. In enterprise environments where URLs might be logged in CI/CD pipelines or monitoring tools, this bearer-token-in-URL approach is insufficient. Enterprise buyers will not accept possession-of-URL as the sole trust boundary.

You must implement a dual-layer authentication system:

  1. MCP token in the URL: Identifies the tenant and scopes tool access (methods allowed, resources exposed, expiry).
  2. Bearer API token in Authorization: Proves the caller is an authenticated user of your platform.
app.post('/mcp/:token', async (req, res) => {
  const { token } = req.params;
  const config = await lookupToken(token);
  if (!config) return res.status(401).json({ error: 'invalid_token' });
 
  if (config.require_api_token_auth) {
    const auth = req.headers.authorization;
    if (!auth?.startsWith('Bearer ') || !(await verifyApiToken(auth.slice(7)))) {
      return res.status(401).json({ error: 'api_token_required' });
    }
  }
  // ... continue to MCP handling
});

Other hardening you should ship on day one:

  • Never store raw tokens: Hash with HMAC before writing to any database or key-value store.
  • TTL every token: Support an expires_at timestamp and enforce cleanup via a scheduled background job or alarm.
  • Cap the tool surface per token: Read-only, write-only, or tag-scoped. Expose configuration that keeps the blast radius small.

For deeper multi-tenant patterns, see our multi-tenant MCP architecture guide.

Managing Rate Limits and Edge Cases

When AI agents execute loops, they can generate hundreds of API requests in seconds. This will inevitably trigger HTTP 429 Too Many Requests errors from upstream SaaS providers. This is where most homegrown MCP servers quietly ruin their agent workloads.

A common mistake developers make is attempting to absorb these rate limits. They write middleware that catches the 429, applies exponential backoff, and retries the request while keeping the HTTP connection open to the LLM.

Do not do this.

LLM clients have strict timeout windows. If your MCP server pauses for 30 seconds to wait out a rate limit, the LLM client will drop the connection, assume the tool failed, and potentially hallucinate a fallback response. Furthermore, silent internal retries burn through rate-limit budgets invisibly, causing cascading 429s across unrelated tenants.

Instead, your MCP server must pass HTTP 429 errors directly back to the caller immediately. The agent itself (or the framework orchestrating it, like LangGraph or CrewAI) knows the full context of the task and is responsible for managing tool retry logic.

To do this correctly, you must normalize the upstream rate limit information into standardized headers per the IETF RateLimit specification. Regardless of whether the upstream API uses X-RateLimit-Remaining or Retry-After, your MCP server should map them to standard names:

function normalizeRateLimitHeaders(upstream: Headers): Record<string, string> {
  const out: Record<string, string> = {};
  const limit = upstream.get('x-ratelimit-limit') ?? upstream.get('ratelimit-limit');
  const remaining = upstream.get('x-ratelimit-remaining') ?? upstream.get('ratelimit-remaining');
  const reset = upstream.get('x-ratelimit-reset') ?? upstream.get('retry-after');
  
  if (limit) out['ratelimit-limit'] = limit;
  if (remaining) out['ratelimit-remaining'] = remaining;
  if (reset) out['ratelimit-reset'] = reset;
  
  return out;
}

Truto strictly adheres to the IETF rate limit spec, normalizing headers and passing HTTP 429 errors directly to the caller without applying hidden retries or exponential backoff. This gives the agent full control over its execution state.

Other Edge Cases Worth Planning For

  • Long-running operations: The 2026-07-28 RC's Tasks extension handles this natively. Until it stabilizes fully across all clients, return a job ID and expose a separate get_job_status tool.
  • OAuth token refresh: Refresh OAuth tokens shortly before they expire, not reactively on a 401. Agents will interpret transient auth failures as tool errors and abandon the workflow entirely.
  • CORS for browser-based agents: Ensure you allow the MCP transport headers (Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID) in your Access-Control-Allow-Headers configuration.

Production Deployment Guide for MCP Servers

Getting a stateless MCP server to run on your laptop is a weekend project. Getting it to survive an autonomous agent hitting tools/call in a tight loop across hundreds of tenants is where the real work starts. This section covers the deployment topology, observability, and scaling patterns that separate a demo from a system you can put in front of enterprise buyers.

Reference Deployment Topology

A production MCP server for AI agents in 2026 should look like a normal stateless HTTP service: the 2026-07-28 spec drops session state for a stateless core, unlocking horizontal scaling and standard HTTP routing. That means no sticky sessions, no in-memory session tables, no shared session store for protocol state.

flowchart LR
    Agents["AI Agents<br>(Claude, ChatGPT, LangGraph)"] --> LB["Load Balancer<br>(round-robin, TLS terminated)"]
    LB --> N1["MCP Instance 1"]
    LB --> N2["MCP Instance 2"]
    LB --> N3["MCP Instance N"]
    N1 --> KV["Token + Config Store<br>(hashed tokens, TTL)"]
    N2 --> KV
    N3 --> KV
    N1 --> Upstream["Upstream SaaS APIs<br>(CRM, HRIS, Ticketing)"]
    N2 --> Upstream
    N3 --> Upstream
    N1 --> Otel["OpenTelemetry Collector"]
    N2 --> Otel
    N3 --> Otel

A few non-negotiables for this topology:

  • Disable client affinity at the load balancer. With the new spec, any MCP request can land on any instance, so the sticky routing and shared session stores that horizontal deployments needed before aren't required at the protocol layer. Turn affinity cookies off.
  • TLS terminates at the edge. The MCP instance behind it only speaks plain HTTP inside your VPC. This keeps cert rotation out of your MCP process.
  • All shared state lives outside the request path. Token records, config, and rate-limit counters live in a key-value store with TTL support. No process-local caches for anything that needs to survive a pod restart.

Container and Runtime Configuration

Whether you deploy to Kubernetes, a managed container platform, or an edge runtime, the runtime shape is the same:

# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

Expose two operational endpoints alongside /mcp:

  • GET /health - liveness. Returns 200 if the process is up. No dependencies checked.
  • GET /ready - readiness. Returns 200 only if the token store and upstream credential cache are reachable. This is what your load balancer should probe before adding an instance to the pool.
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
 
app.get('/ready', async (_req, res) => {
  try {
    await tokenStore.ping();
    return res.json({ status: 'ready' });
  } catch (err) {
    return res.status(503).json({ status: 'not_ready', error: String(err) });
  }
});

Observability: Per-Tool Tracing Is Non-Negotiable

Standard HTTP request logs are not enough. When an agent chains 40 tool calls and the seventh one produces a wrong answer, you need a trace that shows exactly which tool ran, what arguments arrived, which upstream endpoint was hit, and how long each hop took. The 2026 spec explicitly points teams toward OpenTelemetry for structured cloud observability instead of ad-hoc logging.

Wrap every tool invocation in a span with attributes that matter for agent debugging:

import { trace, SpanStatusCode } from '@opentelemetry/api';
 
const tracer = trace.getTracer('mcp-server');
 
async function invokeTool(name: string, args: unknown, handler: Function) {
  return tracer.startActiveSpan(`mcp.tool.${name}`, async (span) => {
    span.setAttribute('mcp.tool.name', name);
    span.setAttribute('mcp.tool.args_size', JSON.stringify(args).length);
    try {
      const result = await handler(args);
      span.setAttribute('mcp.tool.is_error', Boolean(result.isError));
      return result;
    } catch (err) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
      throw err;
    } finally {
      span.end();
    }
  });
}

The minimum metric set to alert on:

Metric Why it matters
mcp_tool_latency_p95_ms (per tool name) Detects a specific tool degrading before agents time out
mcp_tool_error_rate (per tool + tenant) Isolates a broken integration from a broken tool
mcp_upstream_429_rate (per integration) Signals when to raise per-tenant rate-limit budgets
mcp_tokens_active Capacity planning for the token store
mcp_cold_start_ms Serverless health; regressions here hurt agent UX first

Idempotency and Safe Retries

Agents retry. Frameworks retry. Load balancers retry. If create_a_hubspot_contact runs three times, you should not create three contacts. For any write tool, accept an idempotency key and forward it to the upstream API when supported. When it is not supported, generate a deterministic key from the tool name plus a hash of the arguments and dedupe at the proxy layer with a short TTL cache.

function idempotencyKey(toolName: string, args: unknown): string {
  const hash = crypto
    .createHash('sha256')
    .update(toolName + JSON.stringify(args))
    .digest('hex');
  return `${toolName}:${hash.slice(0, 32)}`;
}

Secrets, Config, and Zero-Downtime Deploys

  • Load secrets at startup from a secrets manager, not env files. Rotate without redeploying.
  • Ship a config version in the /health response. Trivially answers "is the new config live on every pod yet?"
  • Use rolling deploys with a warmed-up staging slot. Agents actively drive your tool surface, so a 30-second cold cutover shows up as tool failures in production traces.

Scaling AI Agent Servers Under Load

Agent traffic does not look like normal API traffic. A single user session can fan out into a burst of 50-200 tool calls in a few seconds, followed by long idle periods while the model reasons. Your MCP layer must absorb the bursts without becoming a bottleneck for the upstream APIs it wraps.

Scale Out, Not Up

With the stateless transport, scaling is arithmetic. Load balancers distribute requests across instances using any algorithm because every instance handles every request type identically. Prefer many small instances over a few large ones: it gives you finer-grained autoscaling and limits the blast radius of a single pod crash.

Sensible autoscaling signals for MCP workloads:

  • CPU utilization (60-70% target) as the primary signal.
  • In-flight requests per instance as a leading indicator for burst load. Agent bursts spike concurrency before they spike CPU.
  • Upstream latency p95 as a scale-out brake. If your upstreams are slow, adding more MCP pods just piles on more concurrent connections to a struggling API.

Connection Pooling to Upstream APIs

Each MCP instance should maintain a persistent HTTP/2 connection pool to every upstream SaaS API it fronts. Building a new TLS connection for every tools/call is the fastest way to double your tail latency.

import { Agent, setGlobalDispatcher } from 'undici';
 
setGlobalDispatcher(new Agent({
  connections: 128,          // per origin
  pipelining: 1,
  keepAliveTimeout: 30_000,
  keepAliveMaxTimeout: 60_000,
}));

Size the pool per origin, not globally. A single tenant hammering HubSpot should not exhaust the sockets your other tenants need for Salesforce.

Per-Tenant Rate Limit Budgets

Agents are noisy neighbors by default. Enforce a per-token rate limit inside your MCP layer before any request reaches the upstream. This is separate from the IETF header pass-through described earlier: that surfaces upstream limits to the agent, this protects your infrastructure from a runaway agent. As one production write-up puts it, teams deploying MCP servers should implement rate limiting per-token in addition to per-IP to prevent compromised tokens from overwhelming servers.

A token bucket keyed on token_id + upstream_integration works well. Store counters in the same key-value store as your tokens with a short TTL.

Circuit Breakers on Upstream Integrations

When an upstream API starts returning 5xx errors, do not let every tool call queue behind it. Trip a circuit breaker per (tenant, integration) pair after a threshold of failures, and fast-fail subsequent calls with a clear error the agent can reason about:

return {
  isError: true,
  content: [{
    type: 'text',
    text: 'Upstream integration is currently unavailable. Retry in 30 seconds.',
  }],
};

Half-open the breaker after a cool-down window and let one probe request test recovery. This pattern keeps a single failing integration from consuming worker threads across your entire fleet.

OAuth Refresh Ahead of Expiry

Do not refresh OAuth tokens reactively on a 401. Schedule refreshes to run shortly before expiry, so the credential is always warm when an agent calls a tool. Reactive refresh under agent bursts causes thundering-herd refresh storms against upstream identity providers, which then rate-limit you, which then breaks every tool for every tenant using that integration.

Multi-Region and Cold Start Considerations

If you are deploying to an edge runtime or serverless platform, the stateless spec makes multi-region trivial: MCP servers can run as serverless functions and spin down to zero when idle, drastically reducing costs. The trade-off is cold starts on the first request in a region. Two mitigations that pay for themselves:

  1. Bundle small. Every megabyte in the deploy artifact adds cold-start time. Strip dev dependencies and lazy-load integration modules.
  2. Keep-alive pings from a lightweight scheduler. A cheap way to keep at least one warm instance per region for tenants who care about p99.

Load Test Like An Agent, Not Like A User

Standard REST load-testing tools generate the wrong traffic shape. Real agent load looks like: burst of 30 tools/list and tools/call in 2 seconds, then 45 seconds of silence, then another burst. Model this with a k6 or Locust script that fires a full initialize + tools/list + tools/call × N sequence per virtual user, and measure p95 latency for the tool call itself, not the aggregate.

Info

How Truto handles this in production: Truto runs its MCP layer as a stateless HTTP service so any request can land on any instance. Tools are generated per-request from the latest integration documentation, tokens are hashed and TTL-scoped, OAuth credentials are refreshed ahead of expiry, and upstream 429s are surfaced to the agent with normalized IETF RateLimit headers so the caller keeps control of retry logic.

Build vs. Buy: Managed MCP Platforms vs. Custom Hosting

As you evaluate your multi-tenant MCP architecture, the build vs. buy calculation becomes unavoidable. As covered in our MCP buyer's checklist, a basic hardcoded MCP server takes an afternoon to build, but a production-grade, multi-tenant MCP layer across 40+ integrations takes 12-18 months and a dedicated engineering squad. That is the honest math.

What you are actually signing up to build:

Concern Complexity
Per-tenant OAuth (refresh, revocation, rotation) High
Dynamic tool generation from live docs High
Method + tag scoping per MCP token Medium
Flat-namespace argument routing Medium
Rate limit normalization (IETF) Medium
Streamable HTTP transport (stateful → stateless migration) Medium
Token hashing, TTL, revocation Medium
Observability (per-tool latency, error rates, agent tracing) Medium
Compliance (SOC 2, GDPR, EU residency, ZDR) High

The competitive landscape offers several alternatives. The official GitHub reference server provides a basic standard but is frequently criticized for parameter complexity and OAuth challenges. Platforms like Arcade.dev position themselves as MCP runtimes but force you to adopt their specific Python framework and gateway, locking you into their execution model. Kong pitches API gateways as essential for governing MCP, which solves routing but does nothing for dynamic tool generation or schema maintenance.

If you are shipping a single-integration MCP server (your own product only), the code above plus a small OAuth layer will get you to production. But if you are shipping MCP across dozens of third-party integrations (CRM, HRIS, ATS, ticketing, accounting), the maintenance surface is where teams burn 6-9 months of engineering time and still fall behind vendor API changes.

If your goal is to give AI agents secure, standardized access to your customers' SaaS data without dedicating an engineering squad to connector maintenance, a managed pass-through unified API is the superior architectural choice. You get auto-generated tools, normalized rate limits, flat-namespace routing, and dual-layer authentication out of the box, allowing your team to focus on building the agent's reasoning logic rather than debugging protocol transports.

FAQ

What is the current MCP specification version in 2026?
The stable production version is 2025-11-25, and the 2026-07-28 release candidate introduces a stateless transport core, an Extensions framework, Tasks for long-running operations, and tighter OAuth alignment. Most production servers today are migrating to the stateless HTTP transport.
Should my MCP server retry on HTTP 429 rate limit errors?
No. Pass 429 responses directly to the caller and normalize the headers to the IETF `ratelimit-limit`, `ratelimit-remaining`, and `ratelimit-reset` standard. The agent has the full task context and should own the retry decision—hidden server-side retries cause timeouts and cascading failures.
How do I handle the flat argument object in MCP tools/call?
MCP delivers all tool arguments in a single flat JSON object. You must split them into separate query parameters and body payloads by matching keys against your predefined query schema and body schema properties. If a key appears in both, use a documented precedence like query-wins.
Should I hardcode MCP tools or generate them dynamically?
Generate tools dynamically from OpenAPI specs or documentation records on every `tools/list` request. Static tool caches drift out of sync with upstream API changes. Gate generated tools on documentation quality so undocumented endpoints never surface to the LLM.
Is a single MCP token enough authentication for enterprise deployments?
No. You should layer an API bearer token requirement on top of the URL token for enterprise scenarios. Possession-of-URL fails security reviews. You must also hash tokens before storage, enforce TTLs, and cap the tool surface per token via method and tag scoping.

More from our Blog