---
title: "How to Build MCP Servers for AI Agents: 2026 Hands-On Architecture Guide"
slug: how-to-build-mcp-servers-for-ai-agents-2026-hands-on-architecture-guide
date: 2026-08-19
author: Uday Gajavalli
categories: ["AI & Agents", Guides, Engineering]
excerpt: "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."
tldr: "Building custom AI API connectors is dead. This guide shows how to build and deploy stateless MCP servers that auto-generate tools from OpenAPI, handle dual-layer authentication, and pass through IETF-compliant rate limits."
canonical: https://truto.one/blog/how-to-build-mcp-servers-for-ai-agents-2026-hands-on-architecture-guide/
---

# 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)](https://truto.one/what-is-mcp-model-context-protocol-the-2026-guide-for-saas-pms/).

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.

```mermaid
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](https://truto.one/what-is-an-mcp-server-the-2026-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

```bash
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.

```typescript
// 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

```typescript
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:

```bash
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:

```typescript
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.

> [!NOTE]
> **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](https://truto.one/auto-generated-mcp-tools-for-ai-agents-a-2026-architecture-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:

```json
{
  "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.

```typescript
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.

```typescript
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](https://truto.one/how-to-architect-a-multi-tenant-mcp-server-for-enterprise-b2b-saas/).

## 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:

```typescript
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.

## 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](https://truto.one/mcp-buyers-checklist-and-quick-start-guide-for-b2b-saas-2026/), 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.

> Stop writing custom API connectors for every AI framework. Want an MCP layer that auto-generates tools from live integration docs, routes flat arguments correctly, and hands 429s straight to your agent? Talk to the Truto team about partnering on your AI integration roadmap.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
