Tools are how AI agents interact with the world. A well-designed tool is the difference between an agent that works and one that hallucinates, fails silently, or costs 10x more tokens than necessary. This skill covers tool design from schema to error handling.
50
39%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/antigravity-agent-tool-builder/SKILL.mdTools are how AI agents interact with the world. A well-designed tool is the difference between an agent that works and one that hallucinates, fails silently, or costs 10x more tokens than necessary.
This skill covers tool design from schema to error handling. JSON Schema best practices, description writing that actually helps the LLM, validation, and the emerging MCP standard that's becoming the lingua franca for AI tools.
Key insight: Tool descriptions are more important than tool implementations. The LLM never sees your code - it only sees the schema and description.
Creating clear, unambiguous JSON Schema for tools
When to use: Defining any new tool for an agent
""" BAD - Too vague: { "name": "get_stock_price", "description": "Gets stock price", "input_schema": { "type": "object", "properties": { "ticker": {"type": "string"} } } }
GOOD - Comprehensive: { "name": "get_stock_price", "description": "Retrieves the current stock price for a given ticker symbol. The ticker symbol must be a valid symbol for a publicly traded company on a major US stock exchange like NYSE or NASDAQ. Returns the latest trade price in USD. Use when the user asks about current or recent stock prices. Does NOT provide historical data, company info, or predictions.", "input_schema": { "type": "object", "properties": { "ticker": { "type": "string", "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." } }, "required": ["ticker"] } } """
""" Every parameter needs:
{ "location": { "type": "string", "description": "City and state/country. Format: 'City, State' for US (e.g., 'San Francisco, CA') or 'City, Country' for international (e.g., 'Tokyo, Japan'). Do not use ZIP codes or coordinates." }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit. Defaults to user's locale if not specified. Use 'fahrenheit' for US users, 'celsius' for others." } } """
""" Enums constrain the LLM to valid values:
"priority": { "type": "string", "enum": ["low", "medium", "high", "critical"], "description": "Task priority level" }
"action": { "type": "string", "enum": ["create", "read", "update", "delete"], "description": "The CRUD operation to perform" } """
""" Be explicit about what's required:
{ "type": "object", "properties": { "query": {...}, // Required "limit": {...}, // Optional with default "offset": {...} // Optional }, "required": ["query"], "additionalProperties": false // Strict mode } """
Using examples to guide LLM tool usage
When to use: Complex tools with nested objects or format-sensitive inputs
""" Examples show Claude concrete patterns that schemas can't express. Improves accuracy from 72% to 90% on complex operations. """
{ "name": "create_calendar_event", "description": "Creates a calendar event with optional attendees and reminders", "input_schema": { "type": "object", "properties": { "title": {"type": "string", "description": "Event title"}, "start_time": { "type": "string", "description": "ISO 8601 datetime, e.g. 2024-03-15T14:00:00Z" }, "duration_minutes": {"type": "integer", "description": "Event duration"}, "attendees": { "type": "array", "items": {"type": "string"}, "description": "Email addresses of attendees" } }, "required": ["title", "start_time", "duration_minutes"] }, "input_examples": [ { "title": "Team Standup", "start_time": "2024-03-15T09:00:00Z", "duration_minutes": 30, "attendees": ["alice@company.com", "bob@company.com"] }, { "title": "Quick Chat", "start_time": "2024-03-15T14:00:00Z", "duration_minutes": 15 }, { "title": "Project Review", "start_time": "2024-03-15T16:00:00-05:00", "duration_minutes": 60, "attendees": ["team@company.com"] } ] }
Returning errors that help the LLM recover
When to use: Any tool that can fail
""" BAD: {"error": "Failed"} {"error": true}
GOOD: { "error": true, "error_type": "not_found", "message": "Location 'Atlantis' not found in weather database. Please provide a real city name like 'San Francisco, CA'.", "suggestions": ["San Francisco, CA", "Los Angeles, CA"] } """
""" { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Location 'Atlantis' not found in weather database. Please provide a real city name like 'San Francisco, CA'.", "is_error": true } """
"""
Input Validation Errors
External Service Errors
Business Logic Errors
Internal Errors
""" from dataclasses import dataclass from typing import Union
@dataclass class ToolResult: success: bool content: str error_type: str = None suggestions: list[str] = None
def to_response(self) -> dict:
if self.success:
return {"content": self.content}
return {
"content": f"Error ({self.error_type}): {self.content}",
"is_error": True
}def get_weather(location: str) -> ToolResult: # Validate input if not location or len(location) < 2: return ToolResult( success=False, content="Location must be at least 2 characters", error_type="validation_error" )
try:
data = weather_api.fetch(location)
return ToolResult(
success=True,
content=f"Temperature: {data.temp}°F, Conditions: {data.conditions}"
)
except LocationNotFound:
return ToolResult(
success=False,
content=f"Location '{location}' not found",
error_type="not_found",
suggestions=weather_api.suggest_locations(location)
)
except RateLimitError:
return ToolResult(
success=False,
content="Weather service rate limit exceeded. Try again in 60 seconds.",
error_type="rate_limit"
)
except Exception as e:
return ToolResult(
success=False,
content=f"Unexpected error: {str(e)}",
error_type="internal_error"
)"""
Building tools using Model Context Protocol
When to use: Creating reusable, cross-platform tools
""" MCP (Model Context Protocol) is Anthropic's open standard for connecting AI agents to external systems. Build once, use everywhere. """
""" import { Server } from "@modelcontextprotocol/sdk/server"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
const server = new Server({ name: "weather-server", version: "1.0.0" });
// Define tools server.setRequestHandler("tools/list", async () => ({ tools: [ { name: "get_weather", description: "Get current weather for a location. Returns temperature, conditions, and humidity. Use for weather queries about specific cities.", inputSchema: { type: "object", properties: { location: { type: "string", description: "City and state, e.g. 'San Francisco, CA'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], default: "fahrenheit" } }, required: ["location"] } } ] }));
// Handle tool calls server.setRequestHandler("tools/call", async (request) => { const { name, arguments: args } = request.params;
if (name === "get_weather") {
try {
const weather = await fetchWeather(args.location, args.unit);
return {
content: [
{
type: "text",
text: JSON.stringify(weather)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: Error: ${error.message}
}
],
isError: true
};
}
}
throw new Error(Unknown tool: ${name});
});
// Start server const transport = new StdioServerTransport(); await server.connect(transport); """
"""
Using SDK tool runners for automatic handling
When to use: Building tool loops without manual management
""" The tool runner handles the tool call loop automatically:
""" import anthropic from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool def get_weather(location: str, unit: str = "fahrenheit") -> str: '''Get the current weather in a given location.
Args:
location: The city and state, e.g. San Francisco, CA
unit: Temperature unit, either 'celsius' or 'fahrenheit'
'''
# Implementation
return json.dumps({"temperature": "72°F", "conditions": "Sunny"})@beta_tool def search_web(query: str) -> str: '''Search the web for information.
Args:
query: The search query
'''
# Implementation
return json.dumps({"results": [...]})runner = client.beta.messages.tool_runner( model="claude-sonnet-4-5", max_tokens=1024, tools=[get_weather, search_web], messages=[ {"role": "user", "content": "What's the weather in Paris?"} ] )
for message in runner: print(message.content[0].text)
final = runner.until_done() """
""" import { Anthropic } from '@anthropic-ai/sdk'; import { betaZodTool } from '@anthropic-ai/sdk/helpers/beta/zod'; import { z } from 'zod';
const anthropic = new Anthropic();
const getWeatherTool = betaZodTool({ name: 'get_weather', description: 'Get the current weather in a given location', inputSchema: z.object({ location: z.string().describe('City and state, e.g. San Francisco, CA'), unit: z.enum(['celsius', 'fahrenheit']).default('fahrenheit') }), run: async (input) => { // Type-safe input! return JSON.stringify({temperature: '72°F'}); } });
const runner = anthropic.beta.messages.toolRunner({ model: 'claude-sonnet-4-5', max_tokens: 1024, tools: [getWeatherTool], messages: [{ role: 'user', content: "What's the weather in Paris?" }] });
for await (const message of runner) { console.log(message.content[0].text); } """
Running multiple tools simultaneously
When to use: Independent tool calls that can run in parallel
""" By default, Claude can call multiple tools in one response. This dramatically reduces latency for independent operations. """
"""
response.content = [ {"type": "text", "text": "I'll check both locations..."}, {"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {"location": "San Francisco, CA"}}, {"type": "tool_use", "id": "toolu_02", "name": "get_weather", "input": {"location": "New York, NY"}}, {"type": "tool_use", "id": "toolu_03", "name": "get_time", "input": {"timezone": "America/Los_Angeles"}}, {"type": "tool_use", "id": "toolu_04", "name": "get_time", "input": {"timezone": "America/New_York"}} ]
import asyncio
async def execute_tools_parallel(tool_uses): tasks = [execute_tool(t) for t in tool_uses] return await asyncio.gather(*tasks)
results = await execute_tools_parallel(tool_uses)
tool_results = [ {"type": "tool_result", "tool_use_id": "toolu_01", "content": "72°F, Sunny"}, {"type": "tool_result", "tool_use_id": "toolu_02", "content": "45°F, Cloudy"}, {"type": "tool_result", "tool_use_id": "toolu_03", "content": "2:30 PM PST"}, {"type": "tool_result", "tool_use_id": "toolu_04", "content": "5:30 PM EST"} ]
messages.append({"role": "user", "content": tool_results})
"""
""" Add to system prompt: "For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially." """
""" response = client.messages.create( model="claude-sonnet-4-5", tools=tools, tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=messages ) """
Severity: WARNING
Tool descriptions should be at least 100 characters
Message: Tool description is too short. Add details about when to use it, parameters, and return values.
Severity: WARNING
Every parameter should have a description
Message: Parameter missing description. Describe what it is and the expected format.
Severity: INFO
Explicitly define which fields are required
Message: Schema doesn't specify required fields. Add 'required' array.
Severity: ERROR
Tool functions should handle exceptions
Message: Tool function without try/except block. Add error handling.
Severity: WARNING
When returning errors, set is_error to true
Message: Error result without is_error flag. Add 'is_error': true.
Severity: WARNING
Return JSON string, not dict/object
Message: Returning dict instead of string. Use json.dumps() or JSON.stringify().
Severity: WARNING
Validate LLM-provided inputs before execution
Message: Tool function without visible input validation. Validate before execution.
Severity: ERROR
Never concatenate user input into SQL
Message: SQL query appears to use string concatenation. Use parameterized queries.
Severity: WARNING
HTTP requests and external calls should have timeouts
Message: External API call without timeout. Add timeout parameter.
Severity: ERROR
All MCP tools require inputSchema
Message: MCP tool definition missing inputSchema.
Works well with: multi-agent-orchestration, api-designer, llm-architect, backend
636b862
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.