Skip to content
Harness Engineering2026-09-108 min readRev. 2026-09-30

Where State and Identity Live Behind a Stateless MCP Server

A stateless MCP transport lets any server instance take any request, and it leaves job state and authorization to your application. Keep both outside the connection, and check the caller on every protected request.

Key takeaways

  • A stateless transport does not preserve work in progress; the application must persist job state and reconcile external actions a dropped connection left uncertain.
  • At scale, reconnects are routine: with an illustrative drop rate of 0.1 per hour, about 3 of 64 parallel half-hour eval episodes see at least one.
  • A job identifier is not a credential; anyone who learns an eval job's ID must still be refused unless the authenticated caller may access it.
  • Validate the caller and the job permission on every protected request, including after a reconnect; a reused connection carries no authority.
  • The in-memory example models the authorization decision, not a production store with durability and concurrency guarantees.

Rod Rivera

Author

Where State and Identity Live Behind a Stateless MCP Server

Rod's note: read with a pencil; the margins are for you.

When a connection drops, the job and the caller must survive without it

If your MCP server runs stateless, keep two things outside the connection: the job's progress, in durable storage, and the decision about who may touch that job, made again on every protected request. A reconnecting client then resumes from the stored record, and someone who merely learns the job ID is refused.

Who this is for: engineers who run or integrate an MCP server over HTTP, including anyone building eval or agent infrastructure on one. You should read Python comfortably and know what an access token is. The example uses only the standard library; no MCP SDK, API key or network access.

The book's Chapter 14 builds a bounded MCP client over stdio from the protocol up: the handshake, one line per message, discovery, and a client that decides what runs. This article covers what that chapter leaves out: a server reached over HTTP, where connections come and go and several callers share one server.

Why reconnection is routine, not rare

Picture an MCP server that exposes start_eval_job and get_eval_job. An eval harness runs 64 agent episodes in parallel against it, each lasting about 30 minutes. Suppose connections drop at an illustrative rate of λ = 0.1 per hour; your real rate depends on your network, load balancer and client. If drops arrive independently at a constant rate, the chance that an episode of length T sees at least one is:

text
P(at least one drop) = 1 − e^(−λT) = 1 − e^(−0.1 × 0.5) = 1 − e^(−0.05) ≈ 0.049
expected episodes with a drop = 64 × 0.049 ≈ 3.1
P(no episode drops) = (1 − 0.049)^64 ≈ 0.04

At this scale, a run where no episode reconnects is the unusual case, about one in 25. If episode state lives in the connection, three episodes a run come back with a corrupted or reset trajectory, and your eval scores include infrastructure noise you did not measure.

LangChain's article on stateless MCP discusses support for stateless operation. That is a transport and integration choice, not a claim that every MCP server holds no session state. Here we examine an application that uses stateless requests and a separate durable job store.

Four identities that get flattened into one

A request is one message. A connection carries traffic; a protocol session, when one is used, is a separate logical context that need not live as long as one connection. A caller is the authenticated party making requests, established by the authentication layer; its credentials can expire or be revoked independently of any connection. A job is the durable unit of work, such as "eval job eval-17", with a lifecycle of its own.

The MCP authorization specification defines authorization for HTTP-based transports, including access tokens bound to a resource. It does not define your application's rules about who owns which job. Keep token validation and the permission to read or change a particular job as separate checks. And do not assume the HTTP authorization flow applies unchanged to a local stdio integration like Chapter 14's.

The worked case: eval-17 across two connections

A durable job record for one eval run:

FieldValue
job_ideval-17
ownerAna
statuspending
completed_actions[]

Ana's agent opens transport session c1 and starts the job, and the connection drops before the job finishes. The agent reconnects on a new session, c2. The record lives in application storage, not in either session, so it is still there with status: pending when c2 opens.

Every protected request carries authenticated caller context. Validate it on requests from c2 exactly as on c1, then check whether the caller may access eval-17. We use owner-only access to keep the example small; a shared team setup could use memberships, roles or delegated permissions instead. Knowing the job ID is not enough in any of those designs.

Now a second researcher, Bo, sees eval-17 in a shared dashboard link and asks for it from his own agent. If the server only checks "does this job ID exist", Bo gets Ana's run, including its config and partial results. If it checks "may this authenticated caller access this job", Bo is refused before any job data is returned. Where job existence itself is sensitive, return the same response for "not yours" and "not found".

Each protected request checks caller and job permission. Request on connection c2; Validate authenticated caller; Read job ownership internally; Check job permission; Return permitted state or deny
Each protected request checks caller and job permission

A sketch of the decision, before any protocol code

This in-memory sketch isolates the authorization decision. It is standard-library Python in one process, and it proves only what happens inside that process.

python
jobs = {
    "eval-17": {"owner": "Ana", "status": "pending"}
}


def get_eval_job(caller, job_id):
    # caller must come from trusted authentication middleware, not request JSON.
    if caller is None:
        return "denied: no caller context"
    job = jobs.get(job_id)
    if job is None or caller != job["owner"]:
        return "denied"
    return job["status"]


print(get_eval_job("Ana", "eval-17"))   # expected: pending
print(get_eval_job("Bo", "eval-17"))    # expected: denied
print(get_eval_job(None, "eval-17"))    # expected: denied: no caller context

Predict the three outputs, then run it: Ana gets pending, Bo gets denied, and a missing caller gets its own denial instead of falling through to an allow. Rename the transport session from c1 to c2 anywhere in a real system and nothing here changes, because the session is not an input to the decision. The caller strings stand in for an identity authenticated elsewhere; accepting a caller-supplied string such as "Ana" would defeat the check.

This dictionary is not a database. It has no protection against concurrent writes and no durability across a restart. Treat it as a model of the authorization decision. A real server needs a store with explicit atomic writes, recovery and concurrency behavior, because 64 episodes will touch it at once.

A job ID is not a credential

If your server resumes any job for any caller who supplies a valid ID, you have built a bearer-token system from an identifier that was never meant to be secret. Job IDs appear in logs, dashboards, URLs and error messages. Check ownership against the authenticated caller.

Test reconnection without widening access

Write the expected outcomes before implementing the transport adapter. These controls assume the owner-only policy above.

Request contextRequested jobExpected result
Ana on c1Her eval jobpending
Ana on c2Same jobpending
Bo on either connectionAna's jobdenied
No authenticated callerAny jobDenied before returning job data
Authenticated callerUnknown or inaccessible jobThe same denial
Expired credentials on a reused connectionAny protected jobAuthentication rejects the request

The local sketch can test the first five decisions with supplied caller context. It cannot test token expiry, signatures or MCP request handling. Exercise those through the real adapter with controlled credentials, and confirm that caller comes from the authentication result, never from a field in the request body.

Requests on two connections resolve to the same authorized durable job and operation record, including any unresolved external effect.. Ana request on c1; Authorized job lookup; Ana request on c2; Same job and stable operation ID; Confirmed outcome or pending reconciliation
Requests on two connections resolve to the same authorized durable job and operation record, including any unresolved external effect.

A new connection can read the same authorized record, including an external action whose outcome is uncertain. It must not reset that action to pending because the transport is new. Suppose the job submitted work to a GPU cluster and the confirmation was lost when the connection dropped. Record the attempt as uncertain before sending, reuse the same operation ID on any retry, and reconcile against the receiving service's rules. A reconnect is no evidence that resending is safe.

Name the four identities in your own server

For one real job type, write down what the request, connection, caller and job are in your code. If two of them are the same variable, fix that seam first.

Put ownership and status in durable storage

Move the job record out of any session object and into a store that survives a restart and handles concurrent writers.

Authenticate and authorize every protected request

Take caller identity from the verified request context, then check job-specific permission before returning data or acting.

Reconcile recorded operations before retrying

Before resending, read the operation record and its receipt. A confirmed effect is not repeated; an uncertain one is reconciled or retried under the receiver's idempotency rules.

Quick check — An eval harness runs 64 half-hour episodes against a stateless MCP server, and one episode's connection drops and reconnects with a new session ID. What should the server do before resuming that episode's job?

Check the decision

Why this matters in a lab

Agent and eval infrastructure in a research team increasingly talks to tools over MCP, and it runs many episodes at once. At that scale the rare case in a single-user demo becomes a few episodes in every run. A team will expect you to put state and authorization where a dropped connection cannot touch them, and to know that infrastructure faults leak into eval scores unless you design them out. An interviewer might ask, "Your agent eval runs 500 episodes against a tool server behind a load balancer. What breaks?" A strong answer computes the expected number of reconnects, moves episode state into a durable store keyed by job, checks the caller on every request, and reconciles uncertain side effects. For how traces from those episodes can feed evals without copying user data, see capturing trajectories for evals.

What would prove this wrong

If drops are not independent, for example when a load balancer restart drops every connection at once, the per-episode formula understates the damage: all 64 episodes reconnect together, and your store must handle that burst. If your server is intentionally stateful and pins each client to one instance, reconnects may be rarer, but the job record still has to survive that instance's restart. And the owner-only rule is a teaching choice; a team policy with shared access changes the permission check, not the principle that the check happens on every request.

Read Chapter 14: the protocol connects, the application decides

Measure what each offered tool costs in tokens and routing accuracy, then build a bounded MCP client and break it on purpose, with notebooks for both.

Updated . New opening: the answer, who it is for, and what you will have at the end.

Learn the mechanism. Test the claim.

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.