Dispatch

Inside the MCP Protocol: Sessions, Tools and What the Gateway Sees

One LLM API request at a time, Zuplo's gateway showed us that MCP's notable feature was not the tool calls. It was the surrounding lifecycle: initialization, capability negotiation, session state, and tool calls that may run for seconds. That differs from familiar gateway concerns such as rate limiting and authentication, so we traced what the protocol actually sends.

The MCP Session Model

MCP (Model Context Protocol) is stateful. Unlike a REST API, where requests stand alone, an MCP session follows a defined lifecycle:

  1. Initialize: The client sends an initialize request with its protocol version and capabilities. The server returns its capabilities and a session ID.
  2. Initialized notification: The client confirms that it received the server's capabilities. The session becomes active.
  3. Exchange phase: The client can call tools, read resources, and use prompts. The server can also request actions from the client, including sampling and roots.
  4. Shutdown: Either participant can end the session.

The transport is either HTTP with Server-Sent Events for streaming or stdio for local processes. For internet-facing MCP servers, HTTP+SSE is the practical choice.

This session model is what makes MCP traffic unusual for a gateway. Instead of isolated requests, the gateway sees one session. The initialize request establishes identity and negotiates capabilities. Later requests should remain tied to that identity. Inspecting requests without knowing whether they belong to an authenticated session leaves gaps.

What the Protocol Puts on the Wire

MCP messages use JSON-RPC 2.0. Each message includes a jsonrpc field, a method for requests and notifications, an id for requests expecting replies, and a params object.

A tool call on the wire looks like this:

{
  "jsonrpc": "2.0",
  "id": "call-1",
  "method": "tools/call",
  "params": {
    "name": "web_search",
    "arguments": {
      "query": "current SOFR rate"
    }
  }
}

The reply:

{
  "jsonrpc": "2.0",
  "id": "call-1",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "The current SOFR rate is..."
      }
    ],
    "isError": false
  }
}

Notifications are fire-and-forget: they have no id and expect no reply. The client's initialized notification after the handshake is one example.

Where the Gateway Sits in This

In a proxied MCP setup, the gateway sits between the MCP client (typically the LLM host or agent runtime) and the MCP server (the tool provider). Every JSON-RPC message passes through it.

The main control points are:

  • Session initialization: Enforce authentication here. The initialize request has no earlier context, so the gateway should check an API key or bearer token before forwarding it. If authentication fails, it returns a -32600 (Invalid Request) error or a 401 at the HTTP layer, then closes the connection.
  • Tool invocation: Each tools/call is a separately metered event. Per-call rate limiting belongs here. The gateway can read the method field and select the right rate limit bucket.
  • Resource access: resources/read calls retrieve external resources and form a separate category. They may need separate limits when resource fetches are expensive.
  • Streaming responses: With SSE transports, the gateway carries a long-lived HTTP response. Timeout settings must allow for a legitimate tool call that runs for 30+ seconds.

Authentication at the Session Layer

The MCP spec does not require one authentication method. It delegates that choice to the transport. With HTTP+SSE, the usual pattern is a bearer token or API key in the HTTP headers of the initial POST that opens the session.

Zuplo treats the MCP endpoint like any other HTTP route and applies the same API key validation policy. The difference from a simple REST endpoint comes afterward: the gateway must preserve the authenticated identity throughout the session, not only for the first request.

{
  "path": "/mcp",
  "methods": ["POST"],
  "policies": [
    {
      "name": "api-key-auth",
      "handler": "api-key-auth-inbound",
      "options": {
        "header": "Authorization",
        "prefix": "Bearer ",
        "forward_identity": true
      }
    },
    {
      "name": "mcp-session-rate-limit",
      "handler": "rate-limit-inbound",
      "options": {
        "key": "api-key",
        "window": "1m",
        "max": 60
      }
    }
  ]
}

Rate Limiting MCP Traffic

Tool calls differ in cost. A search tool that reaches an external API is not the same as one reading a local config file. One shared limit can over-restrict cheap work or under-restrict expensive work.

Our practical approach is two-tier limiting: a session limit for total tool calls in a time window per API key, plus a per-tool limit for expensive tools or those with external side effects. The session limit is the broad backstop, while the per-tool limit shields high-cost operations.

REST rate limiting is simpler because MCP requires the gateway to parse the JSON-RPC body to identify the tool. Header inspection alone is not enough. A body-parsing policy runs before the limiter and extracts params.name from tool calls.

What the Gateway Cannot See

Two limits deserve an honest explanation. First is MCP's bidirectional design. Servers can send requests to clients for sampling or root information. A proxy gateway can intercept these opposite-direction messages, but standard gateway tools are not built to inspect server-originating requests. We instead log the complete bidirectional stream at session level rather than apply per-message policies to server-initiated traffic.

Second is the meaning of tool arguments and results. The gateway knows that web_search received a query string, but not whether the query fits the API's intended use. Semantic enforcement belongs closer to the application layer. The gateway handles structural controls: authentication, rate limits, timeout bounds, and audit logging. Those four areas are within its reliable reach.

For teams deploying MCP, the pattern we see is to make the gateway the security perimeter and observability layer. Track every opened session, its credentials, the tools called, and their call counts. Leave tool semantics to the application layer or the model. This keeps each layer focused on its actual role.

Build your API gateway in minutes

Rate limiting, authentication, and LLM routing in one routes.json file. Free tier, no credit card required.