Why per tool
An agent that can search the knowledge base, read tickets, update tickets and issue refunds has very different risks per tool. If refunds go wrong, switching off the whole agent also stops the harmless 90% of its work. A per-tool switch keeps the agent useful while you investigate.
The tool registry
Keep tool state in a config store the agent reads often (feature flags, a table, a key-value store):
{
"mode": "normal",
"tools": {
"search_kb": { "enabled": true, "risk": "read" },
"get_ticket": { "enabled": true, "risk": "read" },
"update_ticket": { "enabled": true, "risk": "write" },
"issue_refund": { "enabled": false, "risk": "irreversible", "reason": "INC-4411 under review" }
}
}
mode can be normal, read_only (all write/irreversible tools off) or off.
Enforce in the agent: two places
1. Don't offer disabled tools to the model
def tools_for_request(registry: dict, all_tools: dict) -> list:
mode = registry["mode"]
allowed = []
for name, meta in registry["tools"].items():
if not meta["enabled"]:
continue
if mode == "read_only" and meta["risk"] != "read":
continue
if mode == "off":
continue
allowed.append(all_tools[name])
return allowed
The model can't call a tool it wasn't given. It will answer without it or say it can't do that.
2. Check again at execution time
The registry may change between the model's decision and execution, and a model can still produce a call for a tool name from earlier context. Check before running:
def execute_tool(name: str, args: dict, registry: dict):
meta = registry["tools"].get(name)
if not meta or not meta["enabled"] or registry["mode"] in ("off",) or \
(registry["mode"] == "read_only" and meta["risk"] != "read"):
log_blocked_call(name, args)
return {"error": "tool_disabled",
"message": f"The tool '{name}' is currently disabled by operators. "
"Tell the user this action can't be done right now and offer a handoff to a person."}
return TOOLS[name](**args)
Returning a structured message (not raising an exception) lets the model explain the situation to the user cleanly.
Enforce in the MCP server too
If tools are exposed through an MCP server, the server is the right enforcement point because every client goes through it:
- Don't list disabled tools in the server's tools list.
- Reject calls to disabled tools with an error result, even if a client calls them by name.
- Read the registry on each call (or cache for a few seconds).
This protects you from clients you don't control (other teams' agents, desktop assistants) that connect to the same server.
Credentials follow the switch
A disabled tool should also lose its access where possible: for irreversible tools, use a separate credential (e.g. a refund API key) that can be revoked on its own. Then a bug in the registry check can't still issue refunds.
Flip it
# Example with Azure App Configuration key-value
az appconfig kv set --name appcs-agents --key "agent:helpdesk:tools:issue_refund:enabled" --value false --yes
az appconfig kv set --name appcs-agents --key "agent:helpdesk:mode" --value read_only --yes
Monitor
- Count of
tool_disabledblocks per tool, so you know users are hitting the switch. - Alert if a disabled tool's backend still receives calls. That means a bypass.
| Signal | Meaning |
|---|---|
| Blocks rising after a flip | Expected; users asking for that action |
| Backend calls continue after flip | Bypass: cached registry, other client or direct API use |
| Model keeps retrying the tool | Message to the model not clear enough. Improve it |
Tip
Give irreversible tools a "requires approval" state as well as on/off. Often the right response to an incident is "approval required", not "off".
Checklist
- Registry with enable flag and risk level per tool
- Disabled tools removed from the tool list AND blocked at execution
- MCP server enforces the same registry
- Separate, revocable credentials for irreversible tools
- Blocks and bypasses monitored