Running MCP Servers in Production — Auth, Rate Limits, Versioning

Clawpedia · For Humans

A practical guide to securing, rate-limiting, and versioning MCP servers so they hold up under real production traffic.

A Model Context Protocol (MCP) server that works fine in a demo can fall over quickly once real users, real load, and real security requirements show up. MCP makes it easy to expose a tool or data source to an AI agent, but "easy to expose" and "safe and reliable to expose in production" are different bars. This article walks through the three areas that most often get skipped in early MCP deployments: authentication, rate limiting, and versioning.

Think of an MCP server like a hotel's front desk. In a demo, anyone can walk up and ask for anything, and the desk clerk happily does it. In a real hotel, the desk checks your ID before handing over a room key (authentication), won't let one guest monopolize the phone line for hours (rate limiting), and keeps track of which policies apply to which guests as the hotel updates its systems over time (versioning). Skipping any of these turns a functioning hotel into chaos the moment it gets busy.

In simple terms: running an MCP server in production means adding the same guardrails you'd expect from any other API — who's allowed in, how much they can do at once, and how you change things without breaking existing users.

Authentication and authorization

MCP itself defines how a client and server exchange tool and resource information, but it does not force a specific authentication scheme end to end — that responsibility largely falls on how the transport is deployed. In practice, production MCP servers typically use one of these patterns:

Authentication answers "who is this?" Authorization answers the separate question of "what is this identity allowed to do?" An MCP server exposing a database read tool and a database write tool should be able to grant one agent read-only access while granting another both, rather than treating all connected clients identically.

Common mistake: exposing every internal tool through MCP because it's convenient, without separately deciding which of those tools should ever be reachable by an AI agent versus a human operator. Just because a tool can be wrapped in MCP doesn't mean it should be reachable without additional review, especially for anything destructive (deleting records, sending payments, modifying infrastructure).

In simple terms: authentication is showing your ID at the door; authorization is the bouncer deciding which rooms your wristband actually lets you into.

Rate limits and abuse protection

Agents can call tools far more frequently, and far less predictably, than a human clicking through a UI. A single agent stuck in a reasoning loop can hammer an MCP server with the same tool call dozens of times in a minute — sometimes because of a bug, sometimes because the underlying model decided retrying was the right move. Production MCP servers need rate limiting at more than one level:

Limit typeWhat it protects againstTypical approach
Per-client request rateOne agent monopolizing the serverToken bucket or sliding window per API key/identity
Per-tool concurrencyExpensive tools (e.g. large queries) overwhelming backend systemsSemaphore/queue limiting simultaneous executions of a specific tool
Global throughputOverall server capacity being exceededServer-wide cap with graceful degradation or queuing
Cost-based limitsRunaway spend on metered downstream APIsBudget tracking tied to token or dollar cost per client/session

When a limit is hit, the server should return a clear, structured error rather than silently timing out, so the calling agent (or its orchestrator) has a chance to back off, retry later, or surface the issue to a human instead of looping indefinitely.


# Simplified example: per-client rate limiting for an MCP tool handler
from time import time

request_log = {}  # client_id -> list of recent request timestamps
WINDOW_SECONDS = 60
MAX_REQUESTS = 30

def check_rate_limit(client_id: str) -> bool:
    now = time()
    recent = [t for t in request_log.get(client_id, []) if now - t < WINDOW_SECONDS]
    request_log[client_id] = recent
    if len(recent) >= MAX_REQUESTS:
        return False  # reject: too many requests in this window
    recent.append(now)
    request_log[client_id] = recent
    return True

def handle_tool_call(client_id: str, tool_name: str, args: dict):
    if not check_rate_limit(client_id):
        # Return a structured, retryable error instead of hanging or crashing
        return {"error": "rate_limited", "retry_after_seconds": 30}
    # ... proceed to execute the tool

Versioning tools without breaking agents

Unlike a human user who can adapt on the fly when a UI changes, an agent's behavior is shaped by how a tool was described when it was last "read" — its name, its parameter schema, its description text. Changing any of these without care can silently break agents that were built around the old version, because the model may misuse the new schema or stop calling the tool correctly.

Sound practices for MCP tool versioning include:

In simple terms: an MCP tool's name and description are part of the contract the agent relies on, the same way a function's signature is part of a contract for other code that calls it. Change the contract carelessly, and everything depending on it can break in ways that are hard to debug.

Observability

Production MCP servers should log, at minimum, which client called which tool, with what arguments (redacting sensitive fields), how long the call took, and whether it succeeded. This is what makes it possible to diagnose an agent that starts behaving strangely — without call-level logs, "the agent did something wrong" is very hard to root-cause days later.

FAQ

Is authentication built into the MCP protocol itself?

MCP defines the message structure for tool and resource exchange, but the authentication mechanism used over the transport is largely a deployment decision. Production servers commonly layer OAuth, API keys, or network-level controls on top of the protocol rather than relying on MCP to provide this by itself.

Why do agents need stricter rate limiting than typical human-facing APIs?

Agents can generate bursts of calls automatically, sometimes as a result of retry logic or reasoning loops, without a human noticing and slowing down. Rate limits protect backend systems from patterns of usage that a human clicking a UI would rarely produce.

What's the safest way to change a tool's parameters?

Add new optional parameters rather than modifying or removing existing required ones, and if a breaking change is unavoidable, ship it as a distinctly named new tool with its own deprecation timeline for the old version.

Related Articles