Skip to main content
Lesson 6 of 7 15 min

Agent-Native Tools

Designing tools for AI agents, not humans. Different inputs, different outputs.

What I Wish Someone Had Told Me

When I first built tools for AI agents, I made the same mistakes everyone makes. I designed them like CLI tools for humans:

  • Friendly error messages ("Oops! Something went wrong!")
  • Flexible input formats ("Enter a task title, or leave blank to cancel")
  • Pretty formatted output ("✓ Task added successfully!")

None of that helps an AI agent. In fact, it makes things worse.

Here's what I learned the hard way.


Agents Read Schemas, Not Documentation

When an AI agent sees your tool, it doesn't read a README. It reads the schema — the structured definition of what the tool accepts and returns.

// This schema IS your documentation
{
  name: 'task_add',
  description: 'Add a new task to the task list',
  inputSchema: {
    type: 'object',
    properties: {
      title: { 
        type: 'string', 
        description: 'The task title (required)' 
      }
    },
    required: ['title']
  }
}

The agent looks at this and knows: "I need to pass an object with a title string property."

If your schema is vague, the agent will guess. And it will guess wrong.

Bad Schema (Don't Do This)

{
  name: 'task_add',
  description: 'Add a task',  // What kind of task? What format?
  inputSchema: { type: 'object' }  // Properties? Required fields?
}

The agent has no idea what to pass. It might try { task: 'review PR' } or { name: 'review PR' } or just 'review PR'. All wrong.


Return Data, Not Messages

Here's the biggest mistake I made: returning human-friendly messages instead of structured data.

Bad Output (What I Used To Write)

return { content: [{ type: 'text', text: 'Task added!' }] };

The agent sees "Task added!" but can't do anything with it. What's the task ID? What's the status? If the next step needs the ID, the agent is stuck.

Good Output (What Actually Works)

return { 
  content: [{ 
    type: 'text', 
    text: JSON.stringify({ 
      task: { id: 'abc123', title: 'Review PR', status: 'todo' } 
    }) 
  }] 
};

Now the agent can parse the response and use the ID for follow-up operations.

Rule: Return JSON. Always. The agent needs to parse your response, not read it.


Errors Are Data Too

When something goes wrong, your instinct is to throw an error:

// ❌ This is useless to an agent
throw new Error('Something went wrong');

The agent gets a stack trace. It can't recover. It can't explain the failure to the user.

Instead, return structured error data:

// ✓ This helps the agent recover
return { 
  content: [{ 
    type: 'text', 
    text: JSON.stringify({ 
      error: 'Task not found',
      requestedId: id,
      suggestion: 'Use task_list to see available task IDs'
    }) 
  }] 
};

Now the agent can tell the user exactly what went wrong and suggest a fix.


One Tool, One Job

It's tempting to build a "do everything" tool:

// ❌ One tool that does too much
task_manage({ 
  action: 'add' | 'remove' | 'update' | 'list',
  title?: string,
  id?: string,
  status?: string
})

This schema is confusing. The agent has to figure out which combination of parameters to use for each action. It will make mistakes.

Split it into separate tools:

// ✓ Four tools with clear purposes
task_add({ title })       // Create
task_list({ status? })    // Read
task_complete({ id })     // Update
task_remove({ id })       // Delete

Each tool has one job. The agent picks the right tool for the task. Simple.


Your Prompts Are an Interface Too

We've been talking about designing tools for agents. But there's another interface in this system: the prompts you write.

When you talk to Claude Code, your natural language prompt is an interface. And the same principles apply — in reverse.

Apply the Same Rules

Clear boundaries:

❌ "Fix the bugs"
✓ "Fix the type errors in src/lib/auth.ts — don't touch other files"

Structured expectations:

❌ "Do the thing"
✓ "Add the task_remove tool. Return the task ID so I can verify it worked."

One request at a time:

❌ "Add task_remove, update the README, write tests, and deploy"
✓ "Add task_remove. We'll do the rest after."

Why This Matters

A vague prompt is like a vague schema. The agent guesses. It guesses wrong.

A clear prompt is like a detailed schema. The agent knows exactly what you need. It delivers.

The symmetry: You design tools for agents to use. You design prompts for agents to understand. Same principles, same discipline, same results.

This is the Triad at work — not just in the code you write, but in how you collaborate with the automation layer itself.


The Four Tools You'll Build

Here's the complete interface for your Task Tracker:

Tool Input Output What It Does
task_add { title: string } { task: Task } Creates a new task
task_list { status?: string } { tasks: Task[] } Lists tasks (optionally filtered)
task_complete { id: string } { task: Task } or { error } Marks a task done
task_remove { id: string } { success: boolean } Deletes a task

Notice the patterns:

  • Consistent output structure: Always an object with named properties
  • Consistent error handling: Errors are data, not exceptions
  • Clear boundaries: Each tool does exactly one thing

The MCP Pattern

MCP (Model Context Protocol) is the standard for how AI agents discover and use tools. You'll implement it in the capstone. Here's the shape:

// 1. Define your tool (the schema)
{
  name: 'task_add',
  description: 'Add a new task to the task list',
  inputSchema: {
    type: 'object',
    properties: {
      title: { type: 'string', description: 'Task title' }
    },
    required: ['title']
  }
}

// 2. Handle the tool call
case 'task_add': {
  const { title } = args as { title: string };
  const task = addTask(title);
  return { content: [{ type: 'text', text: JSON.stringify({ task }) }] };
}

The agent reads the schema, understands the tool, and calls it correctly. That's the whole pattern.


The Triad Applied

Question How It Applies
DRY One schema pattern across all tools. Consistent structure.
Rams Four tools. No more. Each earns its existence.
Heidegger Tools serve the agent's workflow: "manage tasks through conversation"

Summary: What Makes Tools Agent-Friendly

  1. Detailed schemas — The agent reads these, not your docs
  2. Structured output — Return JSON, not human messages
  3. Structured errors — Errors are data the agent can use
  4. Clear boundaries — One tool, one job

The best agent tools are invisible. The agent doesn't struggle with inputs or parse cryptic outputs. It just works.

That's what you're building in the capstone.


Try This in Claude Code

Copy this prompt and paste it into Claude Code:

I want to design a tool schema for a task_add function. The tool should:
- Accept a task title (required)
- Accept an optional priority (low, medium, high)
- Return the created task with its ID, title, priority, status, and creation date

Write the MCP tool schema (inputSchema and example output). Make it 
agent-friendly — detailed descriptions, clear types, structured output.

What you're practicing: Designing for agents, not humans. The schema IS the documentation.

What you should see: Claude Code should produce a detailed JSON schema with property descriptions, an enum for priority, required fields marked, and a structured JSON output example.

Push further: Ask "What would a bad version of this schema look like?" Compare the two.


Lesson Complete

You've learned:

  • ✓ Agents read schemas, not documentation
  • ✓ Return data (JSON), not messages ("Task added!")
  • ✓ Errors are data too — structured, recoverable
  • ✓ One tool, one job — clear boundaries
  • ✓ Your prompts are an interface too

The goal you achieved: You can design a tool schema that an AI agent will use correctly on the first try.