When an MCP 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 a stranger who learns the job ID is still refused.
Who this is for: engineers who already run or integrate an MCP server over HTTP and are deciding where session, identity and job state belong. You should be comfortable reading Python and know what an access token is. The example needs only the Python standard library; no MCP SDK, API key or network access.
What you will have at the end: a short authorization check you can run locally, three calls with their expected output (the owner gets pending, another caller gets denied, a missing caller gets its own denial), a table of reconnection cases to test against your real adapter, and a four-step checklist for your own server.
Here is the situation the rest of the article works through. A hypothetical client starts a supplier-order job through an MCP server, then loses its connection. The client reconnects a minute later. The application needs to recover the job's progress and decide whether this caller can still access it. Neither decision should depend on remembering the old network connection.
LangChain's article on stateless MCP and elicitation discusses support for stateless operation. That is a transport and integration choice, not a universal claim that every MCP server has no session state. Here we examine an application using stateless requests and a separate durable job store.
The separation lets a new connection reach the same business record, but it does not make recovery automatic. Persist the operation identity, status and uncertain external outcomes explicitly. A dropped connection may leave a supplier call unresolved even when the job record itself remains available.
Four identities that get flattened into one
Before working the example, it helps to name four things precisely, because sloppy language here is where the misconception takes root.
A request is one message on the wire. A connection carries traffic; a protocol session, when used, is a separate logical context that need not have the same lifetime 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 a connection. A job is the durable unit of business work, like "process supplier order 17," which has its own lifecycle independent of any connection or even any particular caller session.
The MCP authorization specification defines authorization for HTTP-based transports, including resource-bound access tokens. It does not define your application's job ownership rules. Keep token validation and the permission to read or mutate a particular job as separate checks. The HTTP authorization flow also should not be assumed to apply unchanged to a local stdio integration.
The worked case: order-17 across two connections
Picture a durable job record for a supplier order:
| Field | Value |
|---|---|
| job_id | order-17 |
| owner | Ana |
| status | pending |
| completed_actions | [] |
Ana opens transport session c1, starts the job, and the connection drops before it finishes. She reconnects on a new transport session, c2. The job record above lives in application storage, not in either transport session, so it is still sitting there with status: pending when c2 opens.
For this example, every protected request carries authenticated caller context. Validate that context on requests from c2, just as on requests from c1, then check whether the caller may access order-17. We use owner-only access to make the example small. A shared team application could instead use explicit memberships, roles or delegated permissions; knowing the job ID is insufficient in either design.
Now bring in a second caller, Bo. Suppose Bo, through a shared link or a guessed identifier, sends a request referencing order-17 on his own connection. If the server's logic only checks "does this job ID exist," Bo gets the state of Ana's order. If the logic checks "is the authenticated caller the recorded owner," Bo is refused, and refused before any job payload is returned, not after.
The application may read ownership metadata internally to make the decision. The boundary is that it returns no protected job payload to Bo before authorization succeeds. Where job existence is sensitive, use consistent unauthorized/not-found responses as well.
A teaching sketch before any protocol code
Here is a bounded, in-memory sketch that isolates the authorization logic, deliberately before any MCP transport enters the picture. This is standard-library Python, runs in a single process, and proves only what happens inside that process.
jobs = {
"order-17": {"owner": "Ana", "status": "pending"}
}
def resume_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(resume_job("Ana", "order-17")) # expected: pending
print(resume_job("Bo", "order-17")) # expected: denied
print(resume_job(None, "order-17")) # expected: denied: no caller contextPredict the three outputs before running this mentally: Ana gets pending, Bo gets denied, and a missing caller gets its own explicit denial rather than silently falling through to an allow. Notice what happens if you rename the transport connection variable anywhere in a real system, from c1 to c2 say, and change nothing else: the jobs dictionary is untouched, so the answers do not change. The transport identifier is not a key in this authorization decision. The caller strings are stand-ins for an identity already authenticated elsewhere; accepting a user-supplied string such as "Ana" would defeat the check.
This dictionary is not a database. It has no concurrent-write protection, no durability across a process restart, and no query language. Treat it as a model of the authorization decision, not as a production state store. A real system needs a database or equivalent durable store precisely because an in-memory counter or dictionary proves nothing about what happens when two processes touch the same job at once, or when the process restarts.
If your resumption logic accepts any caller who supplies a valid job ID, you have built a bearer-token system out of an identifier that was never designed to be secret. Job IDs often appear in logs, URLs, and error messages. Ownership must be checked against authenticated caller identity, not against knowledge of the ID.
Test reconnection without widening access
Use a table of expected outcomes before implementing the transport adapter. These controls concern the owner-only policy above, not every possible collaboration model.
| Request context | Requested job | Expected result |
|---|---|---|
Ana on c1 | Ana's order | pending |
Ana on c2 | Same order | pending |
| Bo on either connection | Ana's order | denied |
| No authenticated caller | Any job | Denied before returning job data |
| Authenticated caller | Unknown or inaccessible job | Consistent denial |
| Expired credentials on reused connection | Any protected job | Authentication rejects the request |
The local helper can test the first five application decisions using supplied caller context. It cannot test token expiry, signatures or MCP request handling. Exercise those through the actual adapter with controlled credentials, and verify that the authentication result, not a field from request JSON, supplies caller.
The second diagram follows a different concern: what survives reconnection. A new connection can read the same authorized job record, including an uncertain external action. It must not reset that action to pending merely because the transport is new.

Test a dropped response after an external effect separately. Persisting only completed actions leaves a gap when the receiver commits but the local confirmation is lost. Record an attempted operation as uncertain before sending, use the same operation ID across retries, and reconcile against the receiver's contract. A reconnect by itself provides no evidence that resending is safe.
Write down, for one real job type you handle, what the request, the connection, the caller, and the job actually are in your code. If two of these are the same variable, that is the seam to fix first.
Move the job record out of any in-memory session object and into a store that survives a process restart. Choose a store with explicit atomic-write, recovery and concurrency behavior. Writing a dictionary to a file does not by itself establish those guarantees.
Do not let "the job ID matched" stand in for "the caller is authorized." Obtain caller identity from the verified request context, then check job-specific permission before returning protected data or performing an action.
Before resending, inspect the operation record and its receipt. A confirmed matching effect should not be repeated. An uncertain effect needs reconciliation or a retry authorized by the receiver's idempotency contract; absence from a completed-actions list is not proof it never happened.
Check the decision
Where this leaves the design decision
The practical version of this lesson is a checklist you can run against your own MCP-based system: identify where job ownership is stored, confirm it is not the transport layer, confirm identity and job permission are checked on every protected request, and confirm completed external actions are logged somewhere that a retry will consult before acting again. None of that requires abandoning a stateless transport. Stateless operation can simplify distributing requests among server instances, provided durable business state and authorization remain available to whichever instance receives the request. The mistake is assuming that simplification extends upward into the business layer, where it does not belong and was never claimed to.
The teaching lesson on persistent operational state and typed handoffs works through a related case in more depth, covering what should survive when one autonomous actor hands work to another, and how to validate a handoff's claims rather than trusting its prose. That lesson's core distinction, that saving a narrative summary is not the same as preserving verifiable job state, is the same distinction this article applies specifically to protocol reconnection.
If the caller-versus-owner check in this article made sense, the course's lesson on memory handoffs and permissions extends the same durable-state discipline to handoffs between different autonomous actors.

