Production Discipline for a Tool Layer Begins Where API Discipline Ends
The standard advice for taking an agent’s tool layer to production is that you should operate it like any other API service. Authenticate the caller. Version the interface. Rate limit it. Log it, alert on it, give it health checks and timeouts. That advice is correct, and a team that follows it will avoid the first tier of failures. It is also where most guidance stops, and stopping there is what leaves teams surprised when their tool server behaves badly in ways their other services never did.
The reason is a single structural fact, and by itself it is not new. Every one of those controls was designed against a client that is a program, whose behavior is fixed at deploy time, which calls what it was written to call and fails loudly on a contract it does not recognize. A tool layer’s client does none of that. It chooses at runtime, under the influence of inputs nobody on your team controls, and when the contract shifts underneath it, it does not break. It quietly does something else instead.
That much is understood by anyone who has designed a tool interface. What has not been absorbed is the operational consequence. The fact does not merely add a control to the list. It changes what every control already on the list is protecting you from. The floor holds. What sits on top of it is different work.
The Inherited Controls Are the Floor
It is worth being precise about what carries over unchanged, so that the parts that do not carry over stand out.
A tool server exposed over a network is reachable by anyone who can route to it, so it needs authentication for exactly the reason any endpoint does. A tool server that multiple independently deployed clients call needs a compatibility story for exactly the reason any shared interface does. Shared capacity needs limits, because a single heavy consumer will otherwise take the service down for everyone behind it. Production services need logs, latency and error metrics, alerting on thresholds you chose deliberately, a health signal infrastructure can poll without triggering real side effects, graceful behavior when a downstream dependency is unavailable, and bounded execution so a slow dependency cannot hold resources open indefinitely.
None of that is novel and none of it is optional. Take it as given. The interesting question is what each of these controls has to become when the thing on the other side of the boundary is deciding, rather than executing.
Identity Has to Survive Delegation
In a conventional service, authentication answers one question: who is calling. There are two parties, the caller and you, and a credential proves the caller is who it claims. Even when the caller acts for a user, the service usually treats the calling application’s identity as the operative one and lets it assert the user as data.
An agent breaks that into three parties, and the operative identity is the one in the middle. There is a human or an upstream system that initiated the work. There is the agent, which is acting on that initiator’s behalf and choosing its own steps. And there is your tool server, which is being asked to do something consequential and needs to know whose authority that something is being done under. The agent has an identity, but it is the wrong thing to authorize against, because the agent is not the principal. It acts for someone, and it takes its direction from language it did not author.
This is the reason a shared secret in a header fails here, and it fails structurally rather than merely being unfashionable. A static key held by the agent process is ambient authority. It is present on every call the agent makes, regardless of whose request prompted the call, regardless of what the agent was asked to do, and regardless of whether the content that steered the agent toward this call came from the initiating user or from a document the agent was handed to read. A key answers “is this the agent” when the question you needed answered was “did the person this agent is acting for actually authorize this operation.” Those are different questions, and only one of them bounds what a misled agent can reach. Every agent holding a broad shared key is one adversarial input away from exercising that key on someone else’s behalf.
What replaces it is delegated authorization: the initiating principal grants a scoped, short-lived, verifiable credential, the agent carries that credential when it calls you, and you authorize against what the credential permits rather than against the agent’s ambient standing. This is the shape OAuth-style delegation has had for years, and the fact that MCP specifies delegated authorization rather than header keys is not a stylistic preference. It is the only shape that can express the sentence “this specific user allowed this specific agent to do these specific things, for a bounded time.”
Two consequences follow, and the second is the one teams miss.
The first is that the credential’s scopes should map to tool subsets, not to the server as a whole. A credential that permits reading should not be able to invoke a tool that writes. Scope granularity is what keeps the credential from quietly becoming the widest permission in the system.
The second is that authorization must be evaluated at call time against the specific resource, not merely at connection time against the token. An agent’s sequence of calls is not knowable in advance, which is precisely what makes it useful. A model that has just read a record containing an instruction to fetch a different record will happily fetch it, and the credential it carries will still be valid. Only a check that asks whether this principal may touch this object, on this call, catches that. Connection-time authorization assumes the caller’s plan is fixed. The caller does not have a fixed plan.
Schema Compatibility Is Not Semantic Compatibility
Versioning a tool layer looks like versioning any API. Additive changes are safe. Renaming or withdrawing an operation breaks callers, as does tightening the type of a field they already send. Bump a major version, serve both for a migration window, deprecate loudly, retire on a schedule. The reasoning behind that rule is sound and it is worth keeping.
It is also insufficient, because it assumes the consumer parses your interface. A model does not parse your interface. It reads it. The contract a model consumes includes the tool’s name, its description, its argument descriptions, and the entire set of other tools presented alongside it, because the model’s decision is a selection made across that whole set. All of that is contract surface, and almost none of it is covered by a schema diff.
The immediate consequence is that changes which are perfectly compatible by the conventional test can still be breaking in practice. Rewriting a description for clarity changes routing behavior, because the description is the primary signal the model uses to decide whether this tool is the one the request calls for. Tightening a description’s wording can pull calls in from a neighboring tool or push them away. Adding an optional argument adds a decision the model now has to make on every call, and it will make it sometimes and not others. None of these touch a required field. All of them can change which tool fires and with what arguments, which is the only behavior anyone cared about.
The sharpest version of this is the one that most contradicts inherited instinct. Adding a new tool is not a safe additive change. For a program, a new endpoint is invisible until someone writes code against it. For a model, a new tool joins the set that every selection is made over, and if it overlaps semantically with an existing tool, selection accuracy on the existing tool degrades immediately. You shipped an addition. Your consumers experienced a regression in an operation you did not touch. The toolset is a single jointly-optimized artifact, not a collection of independent endpoints, and the compatibility unit is the set, not the tool.
This reframes what a version actually protects. Version the toolset as a whole and pin clients to a version of the set, because that is the object whose behavior they depend on. A description is not prose you are free to polish in place; it is interface text, and it belongs under the same change control as a field name. And accept that the only trustworthy compatibility test is behavioral: hold a fixed suite of representative requests, run them against the old and new toolsets, and compare which tools got selected and with what arguments. A schema diff cannot tell you what you need to know here. An evaluation can.
Deprecation inverts too. The conventional path is to publish a deprecation notice and let consumers act on it. A model will not act on it. It does not read your changelog, and a deprecation warning returned in a response is text the model may summarize, ignore, or explain to a user who cannot do anything about it. Deprecation for a tool layer is enforced by what you advertise: the tool disappears from the set presented to upgraded clients, and the notice goes to the humans who own those clients. The advertised set is the only channel the model actually obeys.
An Agent’s Load Is a Function of Its Reasoning
Rate limits exist to keep shared capacity healthy and to keep one consumer from starving the others. That purpose survives intact. What changes is the shape of the load and the reason you are limiting it.
Traffic from a program is a function of its users. Traffic from an agent is a function of a plan the model just made, which means it is bursty, correlated, and occasionally unbounded. A single request from a single user can expand into a long sequence of tool calls, sometimes a fan-out of parallel ones, and the expansion factor is not something the model committed to in advance. Per-client request counts over a fixed window, the default shape of an API rate limit, model this badly. They are calibrated against a caller whose call volume tracks its own demand, and an agent’s call volume tracks its reasoning.
The shape of the limit has to change with it. Limits should be weighted by cost rather than counted by request, because the calls an agent makes vary enormously in what they consume. A lookup against a warm cache and a tool that runs an expensive query or calls a metered third-party service are the same request to a counter and nothing alike to your infrastructure. And the natural scope for a budget is the run, not the client. A per-run ceiling on calls and on weighted cost gives you something a per-client window cannot: a bound on how much damage a single piece of work can do, which is exactly the bound you want when the entity generating the work is a loop that decides for itself whether it is finished.
That last point is worth stating directly, because it is where the control changes character. For a conventional API, a rate limit is a fairness and capacity mechanism. For a tool layer, a per-run budget is also a safety mechanism. It is the backstop against an agent that has gotten stuck in a cycle of retrying, rephrasing, and retrying again, and it is frequently the only thing standing between a subtly bad plan and a large bill. Enforce it server-side. The case you are defending against is precisely the case where the agent is not behaving as expected, which is the case where asking it to stop has already failed.
Finally, a limit the model can read is a limit the model will try to route around. What it does next may be to reach for the closest available tool that accomplishes something similar. Say plainly, in the response content, that this is a temporary limit and that waiting is the correct response, and make sure no adjacent tool offers an unlimited path to the same side effect. Otherwise the limit does not stop the work. It reroutes it.
Retries Come From Something That Will Not Retry Identically
An agent retries. It retries when a call fails, it retries when a call succeeds but the result was not what it expected, and it will sometimes retry with slightly different arguments because rephrasing is what a model does when something does not work. This is normal behavior, not a bug, and no amount of instruction removes it reliably.
For a read, that is merely wasteful. For anything with a side effect, it is a correctness problem, and it cannot be solved on the client, because the client is the nondeterministic part. Server-side idempotency is the standard answer and it is the right answer here. What an autonomous caller adds is a constraint on how the key is derived, and that is the part that gets missed.
The key has to be scoped to the intent, not to the literal call. If the agent reformulates its arguments on the retry, an idempotency key derived by hashing the arguments will not match, and you will perform the operation twice. The key belongs to the step in the plan, which means it has to be supplied by the layer that owns the run rather than derived from whatever the model happened to emit. That is an integration requirement on the agent runtime, not just a server feature, and it is the kind of requirement that is cheap to build in early and expensive to retrofit after the first duplicate write.
This also means a timeout on a write is a design problem, not just an availability one. A program left unsure whether its write landed has a retry policy and a reconciliation job. A model left unsure will guess, in whichever direction its context makes plausible, and then act on the guess. The idempotency record is what makes the answer knowable: the agent can re-issue the call with the same key and receive the recorded outcome, which is a fact, rather than reasoning its way to a conclusion, which is not.
The Unit of Investigation Is the Run
Observability is where the inherited practice is closest to adequate and still misses the thing you will most want in the middle of an incident.
Per-call logging is table stakes, and the standard fields are the obvious ones. Keep them; they give you error rates by tool and the capacity signal you need for planning. But notice what a per-call log cannot answer. When an agent does something wrong, the failing question is almost never whether the call errored. It is why the call was made at all. A single tool invocation, viewed alone, is a fact without a cause.
So the correlation identifier that matters is the run, and the tool layer’s specific obligation is to accept it from the agent runtime and stamp it on everything it serves. That is an integration requirement, not a logging default, and it is the difference between holding evidence and holding half of it with no way to join it to the other half.
There is also a class of failure that a conventional service does not have and that your standard metrics are blind to. The tool succeeded. It was the wrong tool. Nothing errored, latency was fine, the response was well formed, and the agent did the wrong thing on the user’s behalf. No error-rate dashboard shows this. Catching it requires you to instrument selection itself, which means treating “which tool was chosen for this class of request” as a metric with an expected distribution and alerting when the distribution shifts. Selection share is also the metric that closes the loop with versioning. It is where a behaviorally breaking change finally surfaces in production, after it slipped past a schema diff in review.
One caution attaches to all of this. Tool arguments are model-generated text derived from whatever the model had in context, which can include the contents of user documents, records it retrieved, and pages it read. Your tool logs are therefore both the highest-value debugging artifact you have and a genuine data-handling surface. Redact at the point of logging, decide retention deliberately, and do not let the convenience of full-fidelity argument capture quietly turn your log store into the least-governed copy of your most sensitive data.
A Clean Failure Beats a Plausible One
The reliability practices carry over almost unchanged in mechanism and change meaningfully in emphasis.
Timeouts matter more, because the caller is a loop. A hung tool call does not merely block one request; it stalls a run that may be holding context and budget, and the agent’s response to a stall is frequently to try again, which multiplies a single slow dependency into duplicate work the server is still holding open. Bound handler execution hard, and bound it below the point where the agent’s own patience runs out, so that the failure is one you shaped rather than one that emerged.
Degradation is where the emphasis shifts most. Conventional guidance says to degrade gracefully rather than crash, and to return a structured error the caller can act on. All true, and insufficient. The response this caller handles worst is not an error at all. It is a result that carries no mark of having come from a broken path. When the store behind a tool is unreachable and the tool answers with nothing, the model reads that as a finding. It concludes the record does not exist, the account has no transactions, the search came up empty, and it proceeds on that conclusion and tells the user something false.
What a degraded path owes the model, then, is the difference between “I could not find out” and “there is nothing to find.” The first is a state an agent can handle: it can retry, escalate, or say that it does not know. The second it cannot recover from, because nothing in it signals that recovery is called for. That distinction has to live in the content of the result, in terms specific enough that the model cannot round them off, and it is cheap to express at the moment the degradation happens and impossible to reconstruct anywhere downstream.
Each Control Earns Its Place at a Boundary
None of this argues for putting the full apparatus around every tool server, and a great deal of waste comes from teams that do. The controls are not a checklist to complete. Each one is triggered by a specific boundary, and if the boundary is absent the control is overhead.
A tool server running as a local subprocess for a single operator, in one trust domain, with one consumer, does not need delegated authorization, because there is no delegation and no network. It does not need versioning ceremony, because the consumer and the server ship together. It does not need per-client rate limits, because there is one client and it is the operator. Adding those things buys nothing and costs real complexity.
The triggers are what to watch for. A network boundary, meaning the server is reachable by a caller you did not deploy, is what makes authentication mandatory. Consumers you do not deploy in lockstep with the server is what makes compatibility a contract rather than a refactor. Shared capacity is what makes limits necessary, and an autonomous caller with a budget is what makes them a safety control rather than just a fairness one. Side effects are what make idempotency non-negotiable. Anything an agent does on behalf of someone other than the person running it is what makes per-call authorization and provenance-grade logging worth their cost. Read the list of boundaries your deployment actually crosses, and implement against those. The failure mode on both sides is real: the team that ships a networked tool server with a shared key has a security incident waiting, and the team that puts a full authorization stack around a local development helper has burned a week for nothing.
Every One of These Controls Exists Because the Caller Can Be Wrong
The through line is worth stating plainly, because it is what makes the difference operational rather than academic.
Conventional API operations assumes a client that is faithful and possibly buggy. Its controls protect the service from load, protect data from unauthorized callers, and give operators visibility into failures. The client’s intentions are never in scope, because a program has no intentions, only code. A tool layer’s client has something close enough to intentions that the distinction stops helping. It decides what to call, it can be persuaded by content it reads, it will retry and rephrase and work around and proceed on bad data without noticing, it will not read your changelog, and it will not stop at a ceiling that exists only as an instruction. Every control described here is one idea restated in a different register: the boundary must hold when the caller’s judgment does not, because the caller’s judgment is the part of the system you cannot verify in advance.
That is why authorization has to be bound to the principal rather than the agent, why compatibility has to be tested behaviorally rather than structurally, why budgets have to be enforced server-side and scoped to the run, why idempotency has to live on the server, why the log has to reconstruct the reasoning and not just the call, and why a failure has to be impossible to mistake for a success. Treat the tool layer as an API service and you will get the floor. Treat it as an API service whose client reasons, and you will get the parts that actually keep it standing.
