At 2:17 AM on a Thursday, a support engineer in our group chat posted a line no one wants to read: “Why did the billing agent close 43 active tickets as duplicates?” The model had done exactly what we asked, just not what we meant. It saw two similarly named tools, inferred the wrong one, and fired it with full write permissions.
That night changed how I design AI agent tool calling. The fix was not a new model. It was better boundaries: strict schemas, permission tiers, safer tool descriptions, and human approval where blast radius is high.
If you are moving from chatbot demos to production agents in 2026, this is the shift that matters most. Accuracy is now partly an application architecture problem, not just a model quality problem.
The uncomfortable truth: tool calls are your real attack surface
When an agent can call tools, the risk profile changes:
- The model can trigger side effects, not just generate text.
- Prompt injection can arrive through user input, documents, web pages, or connectors.
- “Mostly correct” is not good enough when one wrong call mutates production data.
The MCP specification explicitly calls out user consent, tool safety, and trust boundaries as first-class concerns, and it is right to do so. Treat tool metadata as untrusted unless it comes from a trusted server, and design your host for explicit approvals on sensitive actions.
What actually works in production
Here is the architecture that has held up best for me:
- Schema-strict tools so arguments are parseable and bounded.
- Capability tiers (read-only, low-risk write, privileged write).
- Policy gate between model output and real tool execution.
- Human-in-the-loop for high-impact actions.
- Audit trail with request, decision, and final side effect.
Tradeoff: this adds latency and engineering effort, but dramatically lowers silent failure risk. In my experience, teams accept 200 to 600 ms extra latency faster than they accept one bad destructive action.
Pattern 1: strict tool contracts before execution
Use schema validation as an execution precondition, not as a logging step after the fact.
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
class CloseTicketInput(BaseModel):
ticket_id: str = Field(min_length=6, max_length=32)
reason: Literal["duplicate", "resolved", "spam"]
actor: Literal["agent", "human"]
def validate_tool_call(raw_input: dict) -> CloseTicketInput:
try:
return CloseTicketInput.model_validate(raw_input)
except ValidationError as e:
raise RuntimeError(f"blocked_invalid_tool_args: {e}")
# Before any side-effecting call:
# parsed = validate_tool_call(tool_input)
# execute_close_ticket(parsed)
This aligns with modern structured-output guidance: keep output typed and machine-checkable so you can fail closed when payloads drift.
Pattern 2: enforce permission and intent in a policy gate
A model deciding what to do should not also decide whether it is allowed to do it.
const POLICY = {
"ticket.read": { risk: "low", approval: false },
"ticket.comment": { risk: "medium", approval: false },
"ticket.close": { risk: "high", approval: true },
"billing.refund": { risk: "critical", approval: true }
};
function authorizeToolCall({ toolName, args, userRole, sessionTrust }) {
const rule = POLICY[toolName];
if (!rule) return { allow: false, reason: "unknown_tool" };
if (sessionTrust === "untrusted_context" && rule.risk !== "low") {
return { allow: false, reason: "untrusted_context_high_risk" };
}
if (rule.approval && userRole !== "human_approved") {
return { allow: false, reason: "approval_required" };
}
return { allow: true };
}
// Execution path:
// 1) schema validate
// 2) authorizeToolCall
// 3) execute or block with audit log
Notice the sequence. Validation first, authorization second, execution last. Keep this deterministic and auditable.
Where teams usually get burned
Most incidents come from one of these patterns:
- Overloaded tools: one tool with many optional fields that means different things in different contexts.
- Ambiguous descriptions: two tools with overlapping natural-language descriptions.
- No context labeling: external content is mixed with trusted policy text, making indirect prompt injection easier.
- Silent retries: wrappers auto-retry failed calls and accidentally repeat side effects.
OWASP’s 2025 prompt-injection guidance maps to this reality well: constrain behavior, validate output format, enforce least privilege, and require human approval for high-risk actions.
Troubleshooting: when your agent keeps choosing the wrong tool
1) Wrong tool selected between similar actions
- Symptom: model picks
ticket.closewhen user intendedticket.comment. - Fix: rename tools with explicit verbs and risk hints, e.g.,
ticket.close_requires_approval. - Prevention: add an intermediate “intent classification” step that outputs one allowed tool from a shortlist.
2) Valid JSON, unsafe semantics
- Symptom: schema passes, but business rule violated (closing VIP ticket without escalation).
- Fix: add domain guardrails after schema validation, before execution.
- Prevention: maintain a central policy engine, not scattered inline checks.
3) Prompt injection in retrieved documents
- Symptom: model follows hidden instructions from external content.
- Fix: mark retrieved content as untrusted and strip execution-like directives before planning.
- Prevention: keep planner prompts and policy prompts isolated from raw external text.
Practical implementation checklist
- Define a JSON schema for each tool and reject unknown fields.
- Tag every session context as trusted or untrusted.
- Map each tool to risk tier and approval requirement.
- Add idempotency keys for write operations.
- Log the model proposal, policy decision, and final tool result in one trace ID.
How to measure whether this design is actually improving safety
Do not rely on gut feel. Track these four numbers weekly:
- Blocked high-risk calls: how often policy prevented an unsafe execution.
- Approval reversal rate: how often humans reject model-proposed privileged actions.
- Schema failure rate: percent of tool calls rejected before execution due to invalid arguments.
- Incident severity after rollout: compare pre and post architecture changes.
Tradeoff note: a higher blocked-call count is not automatically bad. Early on, it usually means your guardrails are finally visible. The goal is to reduce harmful executed actions over time while keeping useful low-risk automation fast.
FAQ
Do strict schemas reduce hallucinations?
They reduce format hallucinations and argument drift, which removes a large class of execution errors. They do not guarantee factual correctness, so you still need policy and business-rule checks.
Should I let the model call high-risk tools directly?
No, not without a policy gate and human approval path. Direct calls are fine for low-risk reads, but writes with financial, legal, or customer impact should require stronger controls.
Is MCP enough to secure agent workflows by itself?
MCP standardizes how context and tools are exposed, which is great for interoperability. Security still depends on host implementation: consent UX, authorization logic, and operational monitoring.
Actionable takeaways you can ship this week
- Pick one high-risk tool and move it behind an explicit approval gate.
- Convert one fuzzy tool interface into a strict schema with enum constraints.
- Split trusted policy instructions from untrusted retrieved content.
- Add one dashboard panel for blocked tool calls and top block reasons.
- Run one red-team drill focused on indirect prompt injection through documents.
Related reads on 7Tech
- The Internal Tool That Became an Attack Path
- The Stale Answer Problem in AI-Assisted WordPress Content
- Node.js Systems Playbook for Real Reliability
- SQL Playbook for Verifiable Data Lineage
Sources reviewed
- Model Context Protocol Specification (2025-03-26)
- OpenAI Structured Outputs Guide
- Anthropic Tool Use Overview
- OWASP GenAI LLM01:2025 Prompt Injection
- NIST AI RMF: Generative AI Profile
Primary keyword: AI agent tool calling. Secondary keywords: structured outputs, MCP security, prompt injection mitigation.

Leave a Reply