Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe

Build a Stateless MCP Gateway Workflow with Header-Based Routing & Cacheable Tool Lists in 2026

MCP 2026-07-28 went stateless: requests carry method and tool names in headers, list results are cacheable, and any request can land on any instance. Build the gateway that routes, caches and meters a stateless MCP fleet.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 14, 2026 Published
|
Aug 14, 2026 Updated
|
11 Minutes Reading Time
Core Takeaways for Founders & Builders
  • MCP 2026-07-28 removed sessions: every request is self-describing and any instance can serve it behind a round-robin load balancer.
  • Route on Mcp-Method and Mcp-Name headers, never by parsing the JSON body, so edge infrastructure can meter the traffic.
  • Cache tools/list responses with ttlMs and cacheScope hints — agent fleets stop hammering upstream on every reconnect.
  • MRTR (input_required) is the approval channel: the gateway ferries human answers and retries the original call with inputResponses.
  • Retry transient failures with idempotency keys so a retried MRTR approval is never applied twice.

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

The 2026-07-28 Model Context Protocol specification ended the era of the session. MCP went from a bidirectional stateful protocol to a request/response stateless core: every request is self-describing, method and tool names travel in the Mcp-Method and Mcp-Name HTTP headers, list responses carry cache hints, and any request can land on any server instance behind a plain round-robin load balancer. That single change turns MCP from something you babysit into something you can operate like the rest of the web — and it is the difference between a fleet of MCP servers and an actual MCP platform. The protocol's maintainers report nearly half a billion SDK downloads a month, with the TypeScript and Python SDKs each crossing a billion total downloads, because statelessness is what production teams asked for.

This guide builds the piece that makes a stateless MCP fleet work: a stateless MCP gateway workflow that routes tools/call requests by header to the right server pool, serves cached tools/list catalogs with ttlMs and cacheScope hints, handles multi-round-trip requests (MRTR) where a tool needs an answer before it can act, and meters every call for rate limits and billing. It is the routing and caching layer for the servers catalogued in the MCP directory, and the operational patterns it encodes are part of our AI workflows library.

Architecture Overview

graph TD
  A[MCP Client] --> B[Round-Robin LB]
  B --> G[Stateless Gateway]
  G --> C{Header: Mcp-Method}
  C -- tools/list --> D[Catalog Cache]
  D --> G
  C -- tools/call --> E[Route by Mcp-Name]
  E --> P1[Server Pool A]
  E --> P2[Server Pool B]
  E --> P3[Server Pool C]
  P1 --> G
  P2 --> G
  P3 --> G
  G --> F[MRTR Handler: input_required]
  F --> G

A stateless gateway is a pure function from request to response: parse the Mcp-Method and Mcp-Name headers, decide the destination pool, apply cache and rate-limit policy, forward, and return. No session stickiness, no shared storage, no state to drain on deploy. That is the entire point — and it is why the same gateway can scale horizontally behind a load balancer without a single coordination point.

Part 1 — Configuration

.env

LISTEN_PORT=8443
UPSTREAM_POOLS=./pools.json
TOOLS_LIST_TTL_MS=60000
TOOLS_LIST_CACHE_SCOPE=shared
RATE_LIMIT_PER_NAME=100
MRTR_ENABLED=true
OAUTH_INTROSPECT_URL=https://auth.example.com/introspect
IDEMPOTENCY_TTL_SECONDS=300

pools.json

{
  "search": {"servers": ["https://mcp-a.internal:9443", "https://mcp-b.internal:9443"]},
  "docs":    {"servers": ["https://docs-mcp.internal:9443"]},
  "billing": {"servers": ["https://billing-mcp.internal:9443"], "rate_limit": 20}
}

The pools file is the routing table: every tool name maps to a pool of identical server instances. Because the protocol is stateless, any instance can serve any request in its pool — round-robin distribution just works. Pools can carry their own rate limits, which is how a billing tool gets a stricter budget than a search tool without any special-case code.

Part 2 — Routing by header

gateway.py

import os, json, httpx, time

async def route_request(headers: dict, body: dict) -> dict:
    method = headers.get("mcp-method", body.get("method"))
    name = headers.get("mcp-name") or (body.get("params") or {}).get("name")

    if method == "tools/list":
        return await serve_catalog(headers)
    if method == "tools/call":
        return await forward_call(name, headers, body)
    if method == "server/discover":
        return await discover(headers)
    return {"jsonrpc": "2.0", "id": body.get("id"),
            "error": {"code": -32601, "message": "method not found"}}

def pick_instance(pool_name: str) -> str:
    servers = POOLS[pool_name]["servers"]
    return servers[int(time.time()) % len(servers)]  # round-robin

async def forward_call(name: str, headers: dict, body: dict) -> dict:
    pool = POOLS.get(name) or POOLS.get(default_pool(name))
    if pool and not rate_ok(name):
        return {"jsonrpc": "2.0", "id": body.get("id"),
                "error": {"code": -429, "message": "rate limit exceeded"}}
    upstream = pick_instance(name)
    async with httpx.AsyncClient() as c:
        r = await c.post(f"{upstream}/mcp", headers=headers, json=body)
        return r.json()

Header-based routing is the spec's gift to operators: the gateway never parses the JSON body to decide where a request goes. Mcp-Method and Mcp-Name arrive as plain HTTP headers, so a WAF, a rate limiter, or an API gateway can route and meter on them directly — and the gateway itself stays a thin, fast passthrough. The round-robin instance picker needs no shared state, so the gateway scales by adding replicas.

Part 3 — Cacheable tool catalogs

cache.py

import time, json

_CATALOG = {}

def get_catalog(key: str):
    entry = _CATALOG.get(key)
    if entry and entry["expires"] > time.time():
        return entry["value"]
    return None

async def serve_catalog(headers: dict) -> dict:
    # Deterministic order + cache hints per SEP-2549
    cached = get_catalog("tools/list")
    if cached:
        return cached
    upstream = pick_instance("catalog")
    async with httpx.AsyncClient() as c:
        r = await c.post(f"{upstream}/mcp", headers=headers,
                         json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
    result = r.json()
    result["result"]["_meta"] = {
        "ttlMs": int(os.environ["TOOLS_LIST_TTL_MS"]),
        "cacheScope": os.environ["TOOLS_LIST_CACHE_SCOPE"],
        "ordered": True,
    }
    _CATALOG["tools/list"] = {"value": result,
                               "expires": time.time() + int(os.environ["TOOLS_LIST_TTL_MS"]) / 1000}
    return result

Catalog caching is where stateless MCP gets its cost win. tools/list results now carry ttlMs and cacheScope hints and a deterministic order, so the gateway can serve the tool catalog from cache instead of fanning every tools/list to every server. A fleet of agents re-discovering tools on reconnect stops hammering upstream — the catalog is served in microseconds from the cache layer, and cacheScope: shared tells every client the same catalog is safe to reuse.

Part 4 — MRTR and the human-in-the-loop

mrtr.py

async def handle_mrtr(headers: dict, body: dict, server_response: dict) -> dict:
    # SEP-2322: server returns input_required with pending requests
    if server_response.get("result", {}).get("resultType") == "input_required":
        pending = server_response["result"]["requests"]
        # Route the elicitation to the human-facing channel (approval UI, chat, email)
        answers = await elicit(pending)          # blocks until human answers
        body["params"]["inputResponses"] = answers
        return await forward_call(
            headers.get("mcp-name") or body["params"]["name"], headers, body)
    return server_response

MRTR replaces the old held-open bidirectional stream for server-to-client requests. When a tool needs a confirmation mid-call — approve a payment, provide a missing parameter — the server returns resultType: "input_required" with the unanswered requests, and the client retries the original call with the answers attached in inputResponses. The gateway's job is to ferry that round trip: the elicit step is where the human approval UI plugs in, which is exactly the governance pattern we detail in our AI workflows library. Retry rules are explicit: MRTR round trips retry up to twice with backoff, and an idempotency key on the original request guarantees a retried approval is never applied twice.

Part 5 — Running the gateway

main.py

import os
from fastapi import FastAPI, Request, Response
from gateway import route_request

app = FastAPI()

@app.post("/mcp")
async def mcp_endpoint(request: Request):
    headers = {k.lower(): v for k, v in request.headers.items()}
    body = await request.json()
    result = await route_request(headers, body)
    return Response(content=json.dumps(result),
                    media_type="application/json")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=int(os.environ["LISTEN_PORT"]))

The gateway is a single POST /mcp endpoint; everything else is policy. Deploy it as a stateless service, put it behind the load balancer, and scale replicas to meet demand — there is nothing to drain because there is no session state anywhere in the path. Upgrade your servers to the 2026-07-28 spec and the same gateway keeps working with the headers the new SDKs send.

Production checklist

  1. Route on headers, never on body JSON. Mcp-Method and Mcp-Name keep the gateway fast and let edge infrastructure meter the traffic.
  2. Cache tool catalogs with hints. Honor ttlMs/cacheScope; serve tools/list from cache with deterministic order.
  3. Pools are the routing table. Every tool maps to a pool of identical stateless instances; round-robin needs no shared state.
  4. MRTR is the approval channel. input_required round trips plug the human approval UI into the request path.
  5. Retry with idempotency. Retry transient 5xx with backoff; never apply the same MRTR approval twice.

Frequently Asked Questions

Q: What does "stateless MCP" mean for operators?

A: The initialize handshake and Mcp-Session-Id are gone. Every request carries its protocol version, client identity, and capabilities, so any instance behind a load balancer can serve any request without shared storage — deploys become instant and scaling is trivial.

Q: How does the gateway know which server handles which tool?

A: The Mcp-Name header names the tool, and pools.json maps tool names to server pools. The gateway never parses the body to route; the headers are the routing table.

Q: Why are tool lists cacheable now?

A: tools/list responses carry ttlMs and cacheScope hints with deterministic ordering (SEP-2549), so clients and gateways can cache catalogs and stop re-fetching on every reconnect — a large cost win for agent fleets.

Q: How do approvals work without sessions?

A: Via MRTR: the server returns resultType: input_required with pending requests, the client (or gateway) gets the answers from the human, and retries the original call with inputResponses attached. No held-open stream needed.

Q: Does this gateway work with older MCP servers?

A: Yes. The spec gives a twelve-month deprecation window, so a stateless gateway can front mixed fleets while older servers migrate — but new deployments should target the 2026-07-28 spec directly.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
The initialize handshake and Mcp-Session-Id are gone. Every request carries its protocol version, client identity, and capabilities, so any instance behind a load balancer can serve any request without shared storage — deploys become instant and scaling is trivial.
The Mcp-Name header names the tool, and pools.json maps tool names to server pools. The gateway never parses the body to route; the headers are the routing table.
tools/list responses carry ttlMs and cacheScope hints with deterministic ordering (SEP-2549), so clients and gateways can cache catalogs and stop re-fetching on every reconnect — a large cost win for agent fleets.
Via MRTR: the server returns resultType input_required with pending requests, the client or gateway gets the answers from the human, and retries the original call with inputResponses attached. No held-open stream needed.
Yes. The spec gives a twelve-month deprecation window, so a stateless gateway can front mixed fleets while older servers migrate — but new deployments should target the 2026-07-28 spec directly.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Research Breakdown AI Workflows

The Step-by-Step Guide to Automating Meeting Tasks with Whisper

You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...

Deepak Bagada Deepak Bagada
9m read
Research Breakdown AI Workflows

Lovable AI UI-to-Code Pipeline: 2026 Tutorial

Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...

Deepak Bagada Deepak Bagada
8m read
Breaking AI Workflows

Claude Code's New Browser: 5 Workflows That Save Hours Daily

Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...

Deepak Bagada Deepak Bagada
12m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc