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:
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.04At 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:
| Field | Value |
|---|---|
| job_id | eval-17 |
| owner | Ana |
| status | pending |
| completed_actions | [] |
Scroll horizontally to view columns.
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".

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.
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 contextPredict 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.
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 context | Requested job | Expected result |
|---|---|---|
Ana on c1 | Her eval job | pending |
Ana on c2 | Same job | pending |
| Bo on either connection | Ana's job | denied |
| No authenticated caller | Any job | Denied before returning job data |
| Authenticated caller | Unknown or inaccessible job | The same denial |
| Expired credentials on a reused connection | Any protected job | Authentication rejects the request |
Scroll horizontally to view columns.
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.

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.
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.
Move the job record out of any session object and into a store that survives a restart and handles concurrent writers.
Take caller identity from the verified request context, then check job-specific permission before returning data or acting.
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.
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.
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.

