DEV Community

Abhishek Banerjee
Abhishek Banerjee

Posted on Originally published at deeper-in-tech.hashnode.dev on

Defensive Tool API Design: Building Interfaces AI Agents Can’t Abuse

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│
└────────────────────────┘ └────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

The three most common agent tool failures I fix during client audits:

  1. Unbounded Free-Text Arguments: Asking an agent to supply a filter or query string without strict structural constraints leading to accidental full-table scans.

  2. Silent Failure / Non-Descriptive Errors: Returning generic HTTP 500 Internal Server Error responses, causing the agent to repeat the exact same failing tool call until its token budget expires.

  3. Over-Privileged Tool Operations: Exposing raw exec() or bulk DELETE primitives 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>;

Enter fullscreen mode Exit fullscreen mode

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 }
  };
}

Enter fullscreen mode Exit fullscreen mode

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 deleteUnusedFiles to all=true if 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
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Top comments (0)