As a freelance technical consultant, I get brought into client codebases when things start blowing up. Over the past year, almost every client engagement has shared a similar pattern: an engineering team gave an AI coding agent access to internal CLI tools, database utilities, or internal REST endpoints, only to watch the agent go off the rails.
I’ve seen autonomous agents run infinite loops that racked up $4,000 in cloud bills overnight, accidentally drop staging database tables because a tool flag was slightly ambiguous, and pass malformed JSON stringified arguments that crashed production microservices.
When an AI agent breaks your infrastructure, it is almost never the LLM’s fault it is an interface design flaw.
We spend years designing clean, RESTful, type-safe APIs for human developers who can read documentation, infer context, and ask questions on Slack when an error occurs. But when your API client is an autonomous LLM executing function calls via Model Context Protocol (MCP) or custom CLI wrappers, traditional API design principles break down completely.
Here is an opinionated, battle-tested guide to designing defensive tool APIs specifically built for AI agents complete with production TypeScript contracts, defensive schema boundaries, and error recovery patterns I use across client projects.
The Core Problem: How Agents Abuse APIs
When an agent interacts with an API tool, it relies on statistical token prediction to construct arguments. If your tool accepts broad types or free-text input, the agent will find edge cases you never tested.
Traditional Human API Defensive Agent Tool API
┌────────────────────────┐ ┌────────────────────────┐
│ - Loose Arguments │ │ - Strict Enums Only │
│ - Free-Text String │ ❌ Agents ❌ │ - Deterministic Enums │
│ - Generic Errors │ ───────────────► │ - Self-Correction Docs│
│ - Assumes Human Context│ │ - Hard Boundary Limits│
└────────────────────────┘ └────────────────────────┘
The three most common agent tool failures I fix during client audits:
Unbounded Free-Text Arguments: Asking an agent to supply a
filterorquerystring without strict structural constraints leading to accidental full-table scans.Silent Failure / Non-Descriptive Errors: Returning generic HTTP
500 Internal Server Errorresponses, causing the agent to repeat the exact same failing tool call until its token budget expires.Over-Privileged Tool Operations: Exposing raw
exec()or bulkDELETEprimitives instead of fine-grained, intent-based actions.
1. Concrete Tool API Contract & Schema
When building tool interfaces for client agentic systems, every parameter must be constrained to explicit, non-overlapping enums wherever possible.
Here is how we build a defensive, agent-facing database migration tool wrapper in TypeScript using Zod and Model Context Protocol (MCP) tool schemas:
// src/tools/db-migration-tool.ts
import { z } from "zod";
/**
* DEFENSIVE PATTERN 1: Rigid Enums over Free-Text Strings
* Do NOT allow free-text SQL or arbitrary string commands.
*/
export const AllowedEnvironments = z.enum(["development", "staging"]); // Production explicitly omitted!
export const AllowedOperations = z.enum([
"CHECK_PENDING_MIGRATIONS",
"APPLY_NEXT_MIGRATION",
"ROLLBACK_LAST_MIGRATION"
]);
export const DbMigrationToolInputSchema = z.object({
environment: AllowedEnvironments,
operation: AllowedOperations,
targetVersion: z.string().regex(/^v\d+\.\d+\.\d+$/, {
message: "targetVersion MUST follow strict semantic versioning format e.g., 'v1.2.0'"
}).optional(),
dryRun: z.boolean().default(true), // Safe default: force explicit false to mutate
maxSteps: z.number().int().min(1).max(3).default(1), // Hard boundary on execution depth
});
export type DbMigrationToolInput = z.infer<typeof DbMigrationToolInputSchema>;
2. Self-Correction Error Payloads (The Remediation Pattern)
When a tool call fails, returning a standard stack trace confuses the agent. Instead, the tool error response itself should act as an instructional prompt that tells the agent exactly how to format its next attempt.
Here is a defensive tool execution wrapper (src/tools/execute-tool.ts) that intercepts runtime errors and formats self-correcting JSON payloads:
// src/tools/execute-tool.ts
import { DbMigrationToolInputSchema, DbMigrationToolInput } from "./db-migration-tool";
export interface ToolResult {
success: boolean;
data?: unknown;
error?: {
code: string;
reason: string;
remediationGuide: string; // The explicit instructions for the LLM's next step
};
}
export async function runMigrationToolSafely(rawInput: unknown): Promise<ToolResult> {
// Step 1: Validate input schema BEFORE hitting domain logic
const parseResult = DbMigrationToolInputSchema.safeParse(rawInput);
if (!parseResult.success) {
const formattedIssues = parseResult.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join("; ");
return {
success: false,
error: {
code: "INVALID_TOOL_ARGUMENTS",
reason: `Input validation failed: ${formattedIssues}`,
remediationGuide: `CRITICAL: Your inputs violated the schema contract.
1. Verify 'environment' is strictly 'development' or 'staging' (production is disallowed).
2. Ensure 'targetVersion' matches format 'vX.Y.Z'.
3. Do NOT invent new parameter fields. Refer to the schema tool definition.`
}
};
}
const input: DbMigrationToolInput = parseResult.data;
// Step 2: Enforce Intent Boundaries at Runtime
if (input.environment === "staging" && !input.dryRun && input.operation === "ROLLBACK_LAST_MIGRATION") {
return {
success: false,
error: {
code: "UNSAFE_MUTATION_BLOCKED",
reason: "Destructive rollbacks on staging require dryRun=true first.",
remediationGuide: "Set 'dryRun: true' in your tool arguments, execute to inspect the SQL plan, and present the plan to the human supervisor before attempting live mutation."
}
};
}
// Execute safe underlying operation...
return {
success: true,
data: { status: "EXECUTED", operation: input.operation, appliedSteps: 1 }
};
}
3. Real-World Client Failure Modes: Where Tool APIs Explode
In my consulting work auditing client agent pipelines, I frequently step in to fix three specific edge cases:
Failure Mode 1: The "Implicit Default" Cascade
What Happened: A client’s internal cleanup tool defaulted
deleteUnusedFilestoall=trueif no target directory was passed. An agent called the tool with{}intending to list files, and wiped an entire S3 bucket directory.How We Fixed It: Zero implicit defaults for destructive operations. In defensive tool design, missing required parameters must trigger explicit schema errors, and mutating flags (
--force,dryRun: false) must be explicitly passed by the caller.
Failure Mode 2: Argument Pollution via Stringified JSON
What Happened: The agent was expected to pass a JSON string inside a CLI argument:
--config '{"timeout": 5000}'. The LLM generated unescaped single quotes inside double quotes, causing bash expansion errors that executed unpredictable shell fragments.How We Fixed It: Completely eliminated shell-interpolated CLI tools for agents. All agent interactions were migrated to native MCP server endpoints or HTTP RPCs using strictly validated JSON payloads over stdio/HTTP.
4. Non-Trivial Terminal Execution & Verification
Here is what it looks like when an agent attempts an invalid tool invocation in terminal logs, receives our defensive remediation payload, and successfully self-corrects:
# 1. Agent attempts an invalid tool call (passes "prod" instead of allowed enums)
$ mcp-tool-runner --tool db_migration --args '{"environment": "production", "operation": "ROLLBACK_LAST_MIGRATION"}'
[TOOL_LOG] Validation Intercepted. Parsing input...
[TOOL_OUTPUT] {
"success": false,
"error": {
"code": "INVALID_TOOL_ARGUMENTS",
"reason": "Input validation failed: environment: Invalid enum value. Expected 'development' | 'staging', received 'production'",
"remediationGuide": "CRITICAL: Your inputs violated the schema contract. 1. Verify 'environment' is strictly 'development' or 'staging' (production is disallowed)."
}
}
# 2. Agent reads the remediationGuide, self-corrects argument to 'staging' with safe dryRun
$ mcp-tool-runner --tool db_migration --args '{"environment": "staging", "operation": "ROLLBACK_LAST_MIGRATION", "dryRun": true}'
[TOOL_LOG] Input validated successfully. Executing in DRY_RUN mode...
[TOOL_OUTPUT] {
"success": true,
"data": {
"status": "DRY_RUN_COMPLETE",
"plannedSql": "DOWN MIGRATION: ALTER TABLE users DROP COLUMN legacy_bio_v1;",
"impactedRows": 1420
}
}
The Verdict
Design Metric
|
Traditional Human REST/CLI API
|
Defensive Agent Tool API
|
|
Type Flexibility
|
High (String parameters allowed)
|
Minimal (Strict Enums & Regex matching)
|
|
Error Handling
|
Human-readable stack traces
|
Machine-actionable Remediation Guides
|
|
Default Safety
|
Convenient defaults (dryRun=false)
|
Paranoid defaults (dryRun=true forced)
|
|
Execution Risk
|
Low (Human sanity-checks)
|
High (Requires strict intent boundaries)
|
My Takeaway as a Consultant: If you are exposing tools, CLIs, or internal APIs to autonomous agents, treat the agent as an untrusted, highly enthusiastic junior developer who moves at 10,000 requests per minute. Restrict argument spaces with strict enums, wrap operations in dry-run defaults, and make your error payloads write the prompt for the agent's next attempt.
### 💡 Need High-Impact Technical Content for Your Engineering Team?
I partner with developer-tooling startups, SaaS platforms, and engineering teams to translate complex infrastructure, agentic systems, and backend architecture into publication-grade technical writing.
Whether you need deep-dive architecture essays, hands-on developer tutorials, or technical counter-narratives:
📩 Email: abhishekninja2018@gmail.com
💼 LinkedIn: linkedin.com/in/abhishekninja
🐦 X (Twitter): @AvishekBanzzov
✍️ Medium: medium.com/@abhishekninja2018
💻 Dev.to: dev.to/abhishekninja_writer
🛠️ Capabilities: Long-form Technical Essays | Hands-On Tutorials | Developer Tooling Deep-Dives | Technical Counter-Narratives
Top comments (0)