Most MCP guidance assumes a tool call is short: the agent calls, the server answers, the token is still valid the whole time. Real enterprise work is not like that. A report runs for nine minutes. A bulk permission change runs for an hour. A migration job runs overnight. The protocol has grown support for this — long-running operations modelled as tasks that the client creates, polls, and cancels — and every one of those verbs is an authorization decision that first-build servers usually get wrong.
This is a guide to making long-running tool calls survive a security review: who owns a task, what happens when the token that created it expires, how cancellation is authorized, and what the audit trail has to say.
The shape of the problem
A synchronous tool call has one identity check at one moment. A long-running call splits into at least four moments:
- Create. The caller asks for work to be done. A validated access token is present.
- Execute. The work runs, possibly for longer than the token's lifetime, possibly on a different process or worker than the one that accepted the request.
- Poll. The caller — or something claiming to be the caller — asks for status or results, in a separate request with a separate token.
- Terminate. Someone cancels the task, or it fails, or its result is retrieved and discarded.
Every one of those is a request that must be authorized on its own. The common mistake is to authorize step 1 and treat the task identifier as a bearer credential for steps 3 and 4. A task ID is not a credential. It is an object reference, and it needs the same per-user, per-tenant enforcement as any other object your server exposes.
Bind the task to the principal at creation
At creation time you have everything you need: a validated token with a subject, a tenant, a client identifier, and a set of granted scopes. Record all of it on the task, alongside the tool name and the arguments you are about to act on.
That record is the authorization basis for the rest of the task's life. Concretely, store at minimum:
- Subject identifier and tenant identifier from the token claims
- Client application identifier, so a task created by one client is not readable by another
- The scopes granted at creation, and the tool name
- Creation timestamp, requested deadline, and a correlation ID
Do not store the access token itself. If the worker needs to call a downstream system on the user's behalf, it needs a credential obtained for that purpose, which is the subject of the next section.
Status and results are authorized reads
Treat the status endpoint and the result retrieval path as ordinary protected reads on an ordinary object. On every poll:
- Validate the presented token as you would for any tool call: signature, expiry, issuer, and audience bound to your server via the resource indicator.
- Compare the token's subject and tenant against the values recorded on the task. Mismatch is a denial, not a 404-with-details.
- Check that the token still carries the scope the tool requires. A user whose entitlement was removed mid-run should not be able to collect the output.
- Return the same denial shape whether the task does not exist or belongs to someone else. Task identifiers are guessable often enough that existence itself is information.
If your task identifiers are sequential integers, change them before the review. Use unguessable identifiers, but do not rely on unguessability as the control — it is defence in depth behind the ownership check, not a substitute for it.
Tokens expire before the work finishes
This is the design question that actually bites. An access token good for fifteen minutes cannot authorize an hour of work by itself, and refresh tokens belong to the client, not to your server.
Four workable patterns, in rough order of preference:
- Do the privileged work up front. Read what you need while the user's identity is demonstrably live, then spend the remaining time on computation that touches no new data. Many long tools are long because of processing, not access.
- Obtain a downstream credential at creation. Exchange the incoming token for a short-lived downstream token, or for an act-for assertion your own services accept, and use it under the user's identity for the duration. Keep the lifetime as short as the work allows and do not extend it silently.
- Checkpoint and require a fresh token to continue. The task pauses at a boundary and resumes on the next authorized poll. This suits multi-stage jobs and keeps identity live throughout, at the cost of latency.
- Run the tail of the job under a service identity with a recorded delegation. The weakest option, because the audit trail now says "service acting for user" rather than "user". If you use it, bound it: one tenant, one task, one narrow permission, an explicit expiry, and an audit event that names the delegation.
Whichever you choose, write it down in the design document. A reviewer who asks "what identity is this running under at minute forty?" is looking for a specific answer, and any of the four are acceptable answers when they are deliberate.
Revocation has to reach running work
If a user is disabled, or an administrator revokes consent, or a tenant's access is suspended, the in-flight tasks belonging to that principal are still holding a delegation. A server that only checks identity at creation will keep working for an account that no longer exists.
Two mechanisms cover most of it. First, re-check at boundaries: before each stage, before each downstream call, before writing results, confirm the principal is still active and still entitled. Second, make revocation explicit: when your identity layer sees a disabled user or a revoked grant, cancel that principal's open tasks and mark them terminated for that reason. Record the cancellation as an audit event. "We cancel running work within one polling interval of revocation" is a sentence that closes a questionnaire item.
Cancellation, idempotency, and deadlines
Cancellation is a write. Authorize it like one: same ownership check as a status read, plus the scope the tool required, plus a rule for administrators if you want them to be able to stop other users' work. Administrative cancellation should be its own scope and its own audit event type.
Then make cancellation mean something. A task marked cancelled while its worker keeps writing rows is worse than one that cannot be cancelled at all, because the operator believes they stopped it. The worker checks a cancellation flag at every boundary and at every batch, stops writing, and records what it had already done.
Related properties a reviewer will probe:
- Deadlines. Every task has a maximum lifetime, enforced by the server, not by the client's patience. Expired tasks terminate and say so.
- Idempotency. A client that retries a creation request after a timeout must not start a second copy of an hour-long write. Accept an idempotency key, or derive one from the caller, tool, and arguments, and return the existing task.
- Concurrency limits. Per user and per tenant. Long-running tools are the cheapest denial-of-service in your product; an agent in a retry loop can queue hundreds.
- Result retention. Results are stored output, often the most sensitive data your server holds. Give them a short, documented lifetime, delete them after retrieval where the semantics allow, and apply the same minimization and redaction rules you apply to synchronous tool results.
What the audit trail must show
Long-running work multiplies audit events, and that is correct. One event per task lifecycle transition, each carrying the task ID and the same correlation ID:
task.created— subject, tenant, client, tool, redacted argument summary, requested deadlinetask.started, and one event per stage boundary if stages cross trust boundariestask.identity_refreshedortask.delegated— whenever the identity under which work runs changes, naming the old and new principaltask.status_read— at least for result retrieval; polling status can be sampled if volume is a problem, but retrieval never istask.cancelled— who cancelled it, and whether that was the owner, an administrator, or the revocation pathtask.completedortask.failed— outcome class, duration, downstream systems touched, and the size or row count of the result
Denials belong in the same stream: a poll from the wrong subject is one of the few signals that tells a customer's security team something real is happening. Make sure the event says "denied" with a reason code rather than disappearing into an HTTP access log.
Testing it before handover
The adversarial pass for tasks is short and worth doing explicitly:
- Create a task as user A, poll and retrieve it as user B in the same tenant, then as a user in another tenant. Both must be denied, identically.
- Create a task with one client, poll it with a different client holding a valid token for the same user.
- Let the creating token expire, then poll. Confirm the documented behaviour, and confirm no privileged work continues past the point your design says it stops.
- Disable the user mid-run and confirm the task terminates within one boundary.
- Cancel a task and confirm no further writes land.
- Fire the same creation request twenty times and confirm you get one task.
- Iterate task identifiers and confirm the response is indistinguishable from a non-existent one.
Write down what happened. That record belongs in the evidence pack next to the threat model and the audit sample, because "long-running tools" is a question enterprise reviewers now ask directly, and the answer that passes is a specific one.
If you are adding long-running operations to a server that is heading into a customer's review, a security audit will tell you which of these checks are missing before the customer does, and we design task lifecycles this way on every server build.