If you’re still getting oriented on what the Model Context Protocol actually covers and what it leaves to you, start there — this post goes deep on one of the things it leaves to you. A human who calls your delete endpoint by accident does it once, sees the error or the confirmation dialog, and stops. A model doesn’t stop. It gets a 500, decides the call didn’t go through, and tries again. It misreads the schema, calls cancelSubscription when it meant pauseSubscription, gets an ambiguous response, and calls it three more times to be sure. The whole reason agents are useful is that they retry, branch, and persist without a human in the loop. That’s also the whole reason they’re dangerous pointed at a write.

So here’s the question nobody asks until after the incident: when you expose an operation as an MCP tool, what tells the agent’s host that this call is the one to stop and confirm? On most setups, nothing does. Every tool looks the same to the model. The read that lists orders and the call that refunds them are both just functions in a schema.

The MCP risk class is the one setting that makes a destructive tool look different from a safe one before it runs.

Why the platform can’t decide this for you

You might expect the integration platform to figure this out automatically. It can’t, and the reason is worth being clear about, because it’s a real limitation rather than a missing feature.

A platform sees an HTTP method and a path. It does not know your domain. A POST might create a draft (harmless) or wire money (not). A DELETE might soft-delete a row you restore in one click, or it might fire an irreversible downstream cancellation at a payment processor. GET is usually safe, until it’s a GET that triggers a charge because someone built a side effect into a read years ago and nobody wrote it down. The danger of an operation lives in your business semantics, and your business semantics are not in the URL.

This is why side-effect classification has to be a human decision made at exposure time. The person publishing the tool knows that refundOrder is the one to fence off and getOrderStatus is fine to call in a loop. Nobody else does, and no amount of method-sniffing recovers that knowledge.

The risk of a call lives in your business logic, not its HTTP verb — so a person has to say so, once, when the tool is created.

On a Koodisi MCP server, that’s exactly when you say it. When you expose an operation from a published spec as a tool, you set its Risk Class, and that classification is backed by the scopes a caller must hold to invoke it. The scope vocabulary isn’t a fixed list handed to you; you author it on your Security Profile, whatever your APIs actually need, from subscriber:read to billing:refund. So the Risk Class sits alongside the claims that get forwarded, and identity and blast radius are decided together, per operation, in your own vocabulary, rather than waved through at the server level.

You classify each operation’s side effect at exposure time, because that knowledge exists only in your head, not in the HTTP verb.

The label has to travel, or it does nothing

Three risk classes routed differently by the host: read passes, write scoped, destructive gated

Classifying the tool is half the job. The half that actually protects you is what happens to that label next.

A risk class that lives only in your platform’s database is a comment. The model never sees it, the host never sees it, and nothing changes at call time. For the classification to mean anything, it has to leave the server and reach the place where the decision to invoke actually happens: the client and the model’s host. That’s the point where a confirmation gate can fire, where a destructive call can pause for a human, where a host policy can say “writes need approval in this environment.”

So the Risk Class travels with the tool definition. When a client discovers your tools, each one carries its classification and the scopes behind it. The host can then do something the platform never could, because the platform doesn’t sit in the invocation path and the host does. It can let low-risk reads through freely, allow scoped operations within the scope the caller was granted, and gate the highest-risk calls behind a confirmation, all without understanding a single thing about what refundOrder means. It doesn’t need to. It needs one signal, and the signal arrived attached to the tool.

This is the part that turns a label into a control:

Risk ClassWhat the host can doExample operation
low-risk readcall freelylook up an order
scoped writecall within the granted scopeupdate a record
high-risk actiongate behind a confirmationcancel, delete, refund

Those rows are the common shape, not a fixed menu. Because the classes are backed by the scopes you authored on the Security Profile, you can carve the gradient as finely as your APIs need, a billing:refund that always confirms, a subscriber:read that never does. The model still chooses which tool to call. What changes is that its host now has the information to interrupt the dangerous choice before it executes, instead of finding out from your logs that it already did.

A classification that never leaves the server protects nothing; the label is a control only because it travels to the host that sits in the call path.

This is what stops the loop

An agent's retry loop arrested at a confirmation gate on the first attempt

Come back to the failure mode from the top. An agent in a retry loop against an unguarded destructive endpoint is one of the genuinely bad ways an agent project ends. Not because the model is malicious, but because retrying is correct behavior for a model and catastrophic behavior against a refund.

Risk Class is the thing that breaks the loop without you having to predict every way a model might misbehave. You don’t write rules for “if the agent calls refund more than twice.” You don’t try to teach the model your cancellation policy. You classify the operation once, behind a scope you defined, and from then on every host that talks to your server treats it as a call that needs a gate. The loop hits the gate on the first attempt and stops there, waiting for a human, instead of running thirty times while everyone’s asleep.

That last point is where this connects to the rest of a governed server. The gate is one layer. Underneath it, the same operation still requires the scopes the caller was granted and still forwards the verified claims that scope it to one tenant’s data, so even an approved destructive call can only reach what that caller was ever allowed to touch. And when it does run, it’s a span in OpenTelemetry, on every Koodisi tier including the free one, so the destructive call you approved is also the destructive call you can later find and explain. Risk class decides whether the call pauses; identity decides what it can reach; tracing decides whether you can reconstruct it after. The lifecycle that wires all three together is the longer argument — this post is about the one label that handles the pause.

You don’t predict the model’s misbehavior; you mark the operation once, and the gate fires on the first dangerous attempt instead of the thirtieth.

Where this isn’t enough on its own

Honesty about the boundary keeps the rest of it credible. A risk class is a signal to a cooperating host, not a server-side firewall. A well-behaved client respects the destructive label and gates the call; the protection assumes the host on the other end honors what it’s told. For tools facing clients you don’t control, you don’t lean on the label alone. You lean on the layers underneath it, the scopes and forwarded claims that constrain what any call can reach no matter what the host does with the risk signal. Classification makes good hosts safer and makes intent explicit. It doesn’t replace the scoping that holds when the host won’t cooperate.

It also governs whether a call should pause, not which records it may touch. Risk Class and its backing scopes decide that an operation needs the right permission and maybe a confirmation; they don’t encode which customer’s row or which fields are in bounds. That granularity comes from the claims you forward, not the Risk Class. The classification answers one question well: should this call pause before it runs, and does the caller hold the scope to make it at all? For per-record boundaries, it hands off to the claims layer.

Start by classifying one tool

If you’re standing up agent access to real APIs, the smallest useful step is also the one that prevents the worst outcome. Take the operation on your server that you’d least want a model to call in a loop. Define a scope for it on your Security Profile, expose it as a tool behind that scope, and watch a client gate it.

You can do that on Koodisi’s free Community tier, which carries the OpenTelemetry tracing you’ll want for watching those calls, with no per-execution billing and no overage invoices on the paid tiers as your usage grows. Classify one tool, point an agent at it, and try to make it do the dangerous thing. The gate is the answer to the 2 a.m. question you don’t want to ask after the fact.