A remote tool server my agent depends on started timing out. Most calls went through. Some hung and failed, and the agent carried on as if the tool had answered. I opened the MCP spec to find where retries are configured, because I assumed a protocol for tool calls would have a retry setting.
It has none, and that is by design. The spec tells me how a failed call is reported. It says clients should set timeouts and should pass tool errors back to the model so it can correct itself. Whether to retry, and how many times, is left to the application. My agent had been dropping those errors on the floor, and the fix belonged in my harness.
MCP defines the call and stops there
The protocol is small on purpose. A server offers three things: tools the model can call, resources the application can read and pass to the model, and prompts the server shares as templates. Messages travel as JSON-RPC 2.0 over stdio for local servers or Streamable HTTP for remote ones.
MCP's architecture docs draw the boundary themselves. They say MCP "focuses solely on the protocol for context exchange" and "does not dictate how AI applications use LLMs or manage the provided context."
I had read that line before without thinking about what it meant for retries. Deciding when to call a tool again is part of how my application uses the model, so the retry logic was always going to be mine to write.
Retries, stopping and memory are harness code
Once I stopped looking in the protocol, the list of what my harness owns got clear.
It runs the loop. On each turn it decides whether the model wants a tool, which tool, and whether the task is done.
It makes the retry call. When a tool fails, it decides whether to try again, try something else, or stop and say so.
It manages memory, trimming the context so a long session still fits the window. And it checks guardrails before a risky action runs.
In the tool-call part of that cycle, MCP touches two steps. It defines tools/list so my harness can discover what a server offers, and it defines tools/call, which the server runs when my harness decides to call a tool. My harness decides when to call a tool, and MCP carries the request to the server.
My timeout fix ended up as a few lines in the harness. It retries once on a timeout. If the second try fails too, it hands the error to the model as a failed result, so the agent can no longer act as if the call worked.
Approval steps live in my harness too
The same split applies to security. The spec says MCP "cannot enforce these security principles at the protocol level," meaning user consent, privacy and tool safety. It asks clients to prompt the user for confirmation on sensitive operations.
So if I want a human to approve a write before it happens, my harness has to pause and ask. The server can refuse a call it is not allowed to serve, but it will not ask a human for me.
In MCP's own terms, my harness is the Host
MCP's architecture docs name three roles: a Host, one or more Clients, and the Servers they connect to. The Host is "the AI application that coordinates and manages one or multiple MCP clients." It creates one client per server.
Reading that again, I realized I had been running a Host the whole time. My harness creates one client per server. It merges every server's tool list with my native tools into one catalog for the model. Then it routes each tool call to the right client. MCP gives my harness a standard way to hold those connections, and the harness still does everything else.
The July spec update gave my harness more decisions
The MCP spec version dated 2026-07-28 moved even more onto the Host side. Three changes affected my harness directly.
The initialize handshake is gone, and MCP is now stateless. Each request carries its own protocol version and client capabilities. A new server/discover call lets a client check a server's versions and capabilities up front.
Protocol-level sessions are gone too. A server that needs state across calls now hands out an explicit handle, and the client passes it back as an ordinary tool argument.
Results from tools/list now carry two new fields. ttlMs is a freshness hint for caching. cacheScope says whether a shared cache may store the result. My old harness fetched the tool list once at startup and kept it forever. Now it reads the hint and refreshes the list when the cached copy goes stale.
The same release marked the Roots, Sampling and Logging features for removal. For Roots, the suggested replacement is to pass directories through tool parameters, resource URIs or server configuration. For Sampling, it is to call the model provider's API directly.
When I do not need a harness at all
Not every setup needs one. A script that asks the model one question, makes one call to a filesystem server, and prints the answer is complete on its own. It has no loop, no memory, and no need for a permission gate.
The harness shows up the moment the work becomes a loop. Say my script has to read a repo, edit files, run tests, and retry on failure. Now it needs something to pick the next step, keep the context, and gate the writes. The server is the same in both cases, but the runtime now carries all of that work.
What I would tell myself before that timeout
- Looking for retry logic in the protocol. The spec defines how a failed call is reported. Whether to retry, give up, or hand the error to the model is your harness's decision.
- Expecting the server to ask the user. The spec puts user confirmation on the client side. Put the approval step in the Host.
-
Fetching the tool list once and keeping it forever. Under the July spec,
tools/listresults come with a freshness hint. Read it. - Keeping state in the connection. Sessions are gone from the protocol. Pass the server's handle as a tool argument, or keep the state in your harness.
If you want the deeper version, this piece walks through the full split of work between a harness and MCP. It also covers the Host and Client roles and the call sequence a harness runs against an MCP server.
Which part of your agent did you expect MCP to handle before you found out it was yours? For me it was retries, and the fix was a few lines in the harness once I stopped searching the spec for them.
Top comments (7)
You landed on the right boundary, and it's one a lot of people get wrong because "protocol" sounds like it should own reliability. MCP tells you how a failure is reported; what you do about it is the harness's job, same as
tools/listdiscovery vs. your loop deciding when to call.The subtle trap in your "agent carried on as if the tool answered" bug: the real defect wasn't the missing retry, it was that a timeout got silently swallowed instead of surfaced to the model as a tool error. Once the error actually reaches the model, you often don't even need a retry — the model will pick a different path. We treat it as a rule now: a tool call either returns a result or returns an explicit error object, never nothing.
One distinction worth making in your harness: retry on transport failures (timeout, connection reset — safe to retry if the call is idempotent) is very different from retry on tool-reported errors (the tool ran and said no — retrying usually just repeats the failure). Blanket retry counts conflate the two and mask real logic bugs.
The MCP spec lets a server mark a tool with readOnlyHint, destructiveHint and idempotentHint. Retry on a timeout only when readOnlyHint is set, or when idempotentHint is set and the call carries an idempotency key. Treat those hints as the server's claim, not a guarantee.
Does your retry-once rule know which tools have side effects? We hit this on the Auten team with screen-control tools: a click that "timed out" had often already landed, so a blind retry sent the message twice.
What worked for us: before retrying a side-effecting call, read the current state again and retry only if the expected change is missing. Reads stay blind-retry safe.
I watched the healthy version of this today. An agent on the Apify MCP server fired 14 Actor runs in parallel, and 13 of them came back as tool results saying the account allows five concurrent runs. Because the error reached the model as text instead of being dropped, it re-planned by itself: poll the one run that started, feed the rest in small batches. There was no retry code anywhere in the harness.
The case in your post is the mirror image: an error that never reaches the model leaves it nothing to correct from. One thing I'd add on the server side of that split: say what the limit is, not just that the call failed. "Five concurrent runs" is something a model can plan around. "Request failed" only invites a blind retry.
The existing comments cover duplicate effects well. Another harness case I'd add is overlap: when the first call times out locally, does it actually stop at the server, or can it keep running while the retry starts? A timeout and a cancellation acknowledgment are different observations.
For a synthetic slow tool, I'd record the operation ID, attempt IDs, start/end events and the maximum number of simultaneously active attempts. If cancellation cannot be confirmed, keep that uncertainty visible rather than treating the first attempt as finished.
The retry should also consume the original task's total time/call budget. Otherwise a bounded 'retry once' policy at each nested layer can still multiply work. Does your harness have one shared retry owner and deadline across those layers?
MCP having no retry setting by design is the part that catches people.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.