Lesson 3: Giving Agents Tools
Course: Building AI Agents
Estimated time: 25 minutes
Difficulty: Intermediate Builder
A language model without tools is a very smart calculator that can only talk to you. It can reason, plan, write, and analyze - but it can't do anything. It can't check your email, browse the web, call an API, or modify a file. It can only generate text.
Tools are what change that. The moment you give an agent a web search tool, it stops being a text generator and becomes something that can gather real information and act on it. Add a file read/write tool and it has memory that persists. Add an API call tool and it can trigger actions in the real world. The agent's capabilities are exactly the sum of its tools, nothing more.
This lesson covers how tools work, what's available, and how to build the right tool set for your agent.
How Tools Work (Mechanically)
At the API level, tools are function signatures with descriptions. You send the API a list of available tools. The model reads them and, when it decides to use one, returns a structured response instead of text - a JSON object that names the tool and specifies the inputs.
Your code reads that response, executes the function, and sends the result back to the model as the next message. The model reads the result and continues.
The model does not execute the tool directly. It cannot. The model lives in a server somewhere. Your tools run in your code. The API is the bridge. The model says "call this function with these arguments," your code runs the function, and the model gets the output. This is important because it means tool security is entirely your responsibility. Whatever your tools can access, your agent can access. Design accordingly.
The Core Tool Categories
Web Tools
These give agents access to information that isn't in their training data.
Web search. Let the agent query a search engine and get results. Useful for any task where current information matters: competitor research, news monitoring, market data, documentation lookup. The simplest implementation calls the Brave Search API or Serper.dev. You get back a list of results with titles, URLs, and snippets.
URL fetching. Retrieve the content of a specific URL. Different from search - search finds pages, fetch reads them. You'll use this when the agent needs to read a full page, not just a snippet. The main challenge is HTML is noisy; you'll want to strip tags or use a Markdown converter to make the content readable.
Browser control. For pages that require JavaScript to render or user interaction to navigate - clicking buttons, filling forms, handling authentication. Tools like Playwright give agents a full browser they can control. This is heavier than a simple fetch but necessary for some tasks.
File and Storage Tools
These give agents the ability to read and write persistent state.
Read file. The agent reads a file from disk or a storage bucket. Useful for loading data, reading stored context, checking previous state.
Write file. The agent writes output to disk. The most important tool for giving agents persistent memory without a database. A file that stores what the agent knew at the end of last session is enough for most use cases.
List directory. The agent sees what files exist. Useful for agents that need to process a folder of documents or check what data is available.
Append to file. For logging, journaling, or accumulating results without overwriting. Better than write for memory files because you don't accidentally destroy previous state.
API Tools
These connect agents to everything else.
The pattern for an API tool is simple: wrap the API call in a function, handle errors, return structured output. You can build a Slack tool in 10 lines. A GitHub tool in 30. A Stripe tool in 50.
Common API tools that are worth building once and reusing:
- Slack: post message, read channel history, search messages
- Gmail/email: send message, read inbox, search
- GitHub: create issue, read PR, search code
- Notion: read page, create page, update properties
- Airtable or any database: read records, create records, update records
- Calendar: read events, create events
The rule of thumb: if your agent is going to touch a service more than occasionally, build a tool for it. Don't have the agent construct raw HTTP requests in a tool_use response - that's fragile and hard to debug.
Code Execution Tools
These let agents run code, not just write it.
A code execution tool takes code as input, runs it in a sandbox, and returns the output (stdout, stderr, return value). This is powerful because it means the agent can compute things, process data, generate charts, and test its own outputs.
The safety consideration here is significant. A code execution tool is essentially arbitrary code execution. If your agent is generating and running Python, make sure it runs in a container or sandbox with limited permissions. Don't point it at production systems without safeguards.
MCP: Model Context Protocol
MCP (Model Context Protocol) is an open standard that makes tool integration more portable. Instead of writing a custom tool for every agent that needs Slack access, you write one MCP server for Slack and any MCP-compatible agent can use it.
The way it works: an MCP server is a small service (usually a local process or API endpoint) that exposes a list of tools with consistent schemas. Your agent runtime connects to the MCP server, discovers what tools are available, and can use them without you writing the integration code.
The MCP community is growing fast. There are already MCP servers for GitHub, Slack, databases, browsers, file systems, and dozens of other services. If you're building on an MCP-compatible runtime (like Claude with computer use, or OpenClaw), you can add capabilities in minutes by connecting an existing MCP server instead of building a tool from scratch.
For custom integrations where no MCP server exists, you still write the tool yourself. But for common services, check the MCP directory first before building.
Practical use: if you're building an agent that needs to interact with GitHub, don't write the GitHub tool from scratch. Point your agent runtime at the official GitHub MCP server. You get create_issue, list_prs, read_file, and a dozen other operations immediately.
Building a Custom Tool
When you need a tool that doesn't exist, building it is straightforward. Three steps:
Step 1: Write the function.
Keep it focused. One tool, one job. If you find yourself writing a tool that does three different things depending on a parameter, split it into three tools. Models do better with many specific tools than with few multi-purpose ones.
def get_github_issue(repo: str, issue_number: int) -> dict:
"""Fetch a GitHub issue by repo and number."""
token = os.environ.get("GITHUB_TOKEN")
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
f"https://api.github.com/repos/{repo}/issues/{issue_number}",
headers=headers,
timeout=10
)
if response.status_code != 200:
return {"error": f"GitHub API returned {response.status_code}"}
data = response.json()
return {
"number": data["number"],
"title": data["title"],
"body": data["body"],
"state": data["state"],
"labels": [l["name"] for l in data["labels"]],
"comments": data["comments"]
}
Notice: error handling returns a structured error dict, not an exception. The model can read and respond to a dict. An unhandled exception breaks the loop.
Step 2: Write the schema.
The schema is how the model knows what the tool does and what inputs to provide. Write descriptions that would make sense to a non-technical reader, because the model is reading them as natural language, not code.
{
"name": "get_github_issue",
"description": "Fetch a specific GitHub issue by repository and issue number. Returns the title, body, state, and labels.",
"input_schema": {
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository in 'owner/repo' format, e.g. 'anthropics/anthropic-sdk-python'"
},
"issue_number": {
"type": "integer",
"description": "The issue number to fetch"
}
},
"required": ["repo", "issue_number"]
}
}
Step 3: Handle the tool call in your loop.
Add a branch in your tool dispatch logic:
if block.name == "get_github_issue":
result = get_github_issue(
repo=block.input["repo"],
issue_number=block.input["issue_number"]
)
# Convert dict to string for the tool_result content
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
That's the full pattern. Function, schema, dispatch. Repeat for each tool.
Tool Design Principles
A few things that separate good tool sets from bad ones:
Return what the model needs, not everything you have. If your API returns 50 fields but the model only needs 5, filter down to 5 before returning. The model has to read every token in the tool result. Cleaner results lead to better decisions.
Be explicit about errors. Return error messages as structured data, not exceptions. The model can reason about "Error: rate limit exceeded, try again in 60 seconds" and decide to wait. It can't reason about a Python traceback.
Name tools like verbs. get_slack_message, send_email, search_web, create_github_issue. Not slack_tool or email_helper. The model picks tools based on their names and descriptions - clear verb-first names make the right choice obvious.
One tool per action. Resist the urge to build a Swiss Army knife tool that handles multiple modes. It confuses the model and makes the schema harder to describe. Separate read and write operations. Separate search and fetch.
Test tools standalone before testing them in agents. If your tool function has a bug, the agent fails in mysterious ways. Write a quick test that calls the function directly with known inputs and verifies the output before you wire it into the agent.
Exercise: Add Three Tools to Your Agent
Take the website monitor from Lesson 2 and extend it with three new tools.
Tool 1: Search for context. Add a web search tool using the Brave Search API or Serper.dev. When the monitor detects a change on a pricing page, have the agent search for recent news about that company to understand why prices might have changed. Update your system prompt to use search when a change is detected.
Tool 2: Summarize and format. This one doesn't call an external service - it's a tool that calls the model itself to produce a nicely formatted summary. Or keep it simpler: add a format_slack_message tool that takes raw diff data and structures it as a clean Slack block.
Tool 3: Log the detection. Add an append_to_log tool that writes a timestamped entry to a log file every time the monitor runs. This gives you an audit trail: what changed, when it changed, what the agent reported.
After adding all three, run the agent on a real URL. Read the logs. Verify the tool calls are happening in the right order. Verify the alert message makes sense.
If something's wrong, look at the raw message history (log the messages array between loop iterations). The tool call sequence is all there.
Key Takeaway
Agent capabilities equal tool capabilities, exactly. Design your tool set before you write your system prompt. The right tools with the right schemas turn a mediocre agent into a reliable one.
What's Next
You've built a tool-using agent. Lesson 4 is about the next level: what happens when one agent isn't enough, and how to build multi-agent systems where specialists handle different parts of the work.