
Posted on · 5 min read
How to Give Your AI Agent Real-Time Data With Free APIs (Claude Tool Use in TypeScript & Python)
On this page · 8 sections
A language model only knows what was in its training data. Ask it whether you need an umbrella in Lagos today and the honest answer is "I don't know": it has no idea what today is, let alone the forecast. Tool calling (also called function calling or tool use) fixes that by letting the model ask your code to call an API, then answer from the real data.
In this tutorial you'll build a small agent that answers weather questions about any place on Earth, using two free APIs that need no key. You'll write it in TypeScript or Python with Anthropic's Claude SDK, and along the way pick up the lessons we learned testing 15 public APIs as agent tools in October 2026.
#How tool calling works
- You describe each tool to the model: a name, a description of what it does and when to use it, and its parameters as a JSON Schema.
- You send the user's question along with the tool descriptions.
- If the model needs data, it replies with a tool call instead of an answer: the tool's name and the arguments it chose, like
geocode_place({ "query": "Yaba, Lagos" }). - Your code runs the function (calling the real API) and sends the result back.
- The model either calls another tool or writes the final answer from the results.
The model never touches the network or your API keys. It only decides what to call and with which arguments; your code does the calling. That's what makes tool calling safe to build on.
#The two APIs
Tool | API | What it does | Key? |
|---|---|---|---|
| Nominatim (OpenStreetMap) | Place name → latitude and longitude | No |
| Coordinates → current weather and today's rain chance | No |
Why two tools? Weather APIs need coordinates, but people say "Yaba" or "Brooklyn". Giving the agent a geocoder lets it chain the calls itself: look up the place, then the weather. That chaining is what makes it an agent rather than a single API call.
#Build it in TypeScript
Install the SDK and Zod (Node.js 20+), and set your API key from the Claude Console:
npm install @anthropic-ai/sdk zod
export ANTHROPIC_API_KEY=your-keyThe SDK's tool runner handles the loop for you: it sends the request, runs whichever tool Claude asks for, sends the result back and repeats until Claude has an answer. Each tool is a Zod schema plus a run function:
// agent.ts: a Claude agent that answers weather questions using two free APIs
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY
// Tool 1: place name → coordinates (OpenStreetMap Nominatim, no key)
export const geocodePlace = betaZodTool({
name: "geocode_place",
description:
"Find the latitude and longitude of a place name or address. Call this before get_weather when the user names a place.",
inputSchema: z.object({
query: z.string().describe("Place name or address, e.g. 'Yaba, Lagos'"),
}),
run: async ({ query }) => {
const url = new URL("https://nominatim.openstreetmap.org/search");
url.searchParams.set("q", query);
url.searchParams.set("format", "json");
url.searchParams.set("limit", "1");
// Nominatim requires a User-Agent that identifies your app.
const res = await fetch(url, { headers: { "User-Agent": "weather-agent-tutorial/1.0 (+https://publicapis.lol)" } });
if (!res.ok) throw new Error(`Geocoding failed: HTTP ${res.status}`);
const [place] = (await res.json()) as { display_name: string; lat: string; lon: string }[];
if (!place) return `No place found for "${query}".`;
return JSON.stringify({ name: place.display_name, latitude: Number(place.lat), longitude: Number(place.lon) });
},
});
// Tool 2: coordinates → current weather and today's rain chance (Open-Meteo, no key)
export const getWeather = betaZodTool({
name: "get_weather",
description:
"Get the current temperature (°C), precipitation (mm) and wind speed (km/h) at a latitude and longitude, plus today's chance of rain (%).",
inputSchema: z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
}),
run: async ({ latitude, longitude }) => {
const url = new URL("https://api.open-meteo.com/v1/forecast");
url.searchParams.set("latitude", String(latitude));
url.searchParams.set("longitude", String(longitude));
url.searchParams.set("current", "temperature_2m,precipitation,wind_speed_10m");
url.searchParams.set("daily", "precipitation_probability_max");
url.searchParams.set("forecast_days", "1");
url.searchParams.set("timezone", "auto");
const res = await fetch(url);
if (!res.ok) throw new Error(`Weather lookup failed: HTTP ${res.status}`);
const data = (await res.json()) as {
current: Record<string, number | string>;
daily: { precipitation_probability_max: number[] };
};
// Return only what the model needs: every token of tool output is billed.
return JSON.stringify({ current: data.current, rain_chance_today: data.daily.precipitation_probability_max[0] });
},
});
export async function ask(question: string) {
const message = await client.beta.messages.toolRunner({
model: "claude-opus-5-5",
max_tokens: 16000,
tools: [geocodePlace, getWeather],
messages: [{ role: "user", content: question }],
// If a request is declined, retry it on a fallback model automatically.
betas: ["server-side-fallback-2026-07-01"],
fallbacks: "default",
});
if (message.stop_reason === "refusal") return "Sorry, I can't help with that.";
return message.content.map((block) => (block.type === "text" ? block.text : "")).join("");
}
if (process.argv[2]) console.log(await ask(process.argv.slice(2).join(" ")));Run it with npx tsx agent.ts "Do I need an umbrella in Yaba, Lagos today?".
#Build it in Python
pip install anthropic requestsIn Python, the @beta_tool decorator turns a typed function into a tool: the parameter types become the JSON Schema and the docstring's Args: section becomes the parameter descriptions.
# agent.py: a Claude agent that answers weather questions using two free APIs
import json
import sys
import anthropic
import requests
from anthropic import beta_tool
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
# Nominatim requires a User-Agent that identifies your app.
HEADERS = {"User-Agent": "weather-agent-tutorial/1.0 (+https://publicapis.lol)"}
@beta_tool
def geocode_place(query: str) -> str:
"""Find the latitude and longitude of a place name or address.
Call this before get_weather when the user names a place.
Args:
query: Place name or address, e.g. 'Yaba, Lagos'.
"""
res = requests.get(
"https://nominatim.openstreetmap.org/search",
params={"q": query, "format": "json", "limit": 1},
headers=HEADERS,
timeout=10,
)
res.raise_for_status()
results = res.json()
if not results:
return f'No place found for "{query}".'
place = results[0]
return json.dumps({"name": place["display_name"], "latitude": float(place["lat"]), "longitude": float(place["lon"])})
@beta_tool
def get_weather(latitude: float, longitude: float) -> str:
"""Get the current temperature (°C), precipitation (mm) and wind speed (km/h) at a location, plus today's chance of rain (%).
Args:
latitude: Latitude in decimal degrees, -90 to 90.
longitude: Longitude in decimal degrees, -180 to 180.
"""
res = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": latitude,
"longitude": longitude,
"current": "temperature_2m,precipitation,wind_speed_10m",
"daily": "precipitation_probability_max",
"forecast_days": 1,
"timezone": "auto",
},
timeout=10,
)
res.raise_for_status()
data = res.json()
# Return only what the model needs: every token of tool output is billed.
return json.dumps({"current": data["current"], "rain_chance_today": data["daily"]["precipitation_probability_max"][0]})
def ask(question: str) -> str:
runner = client.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=16000,
tools=[geocode_place, get_weather],
messages=[{"role": "user", "content": question}],
# If a request is declined, retry it on a fallback model automatically.
betas=["server-side-fallback-2026-07-01"],
fallbacks="default",
)
final = None
for message in runner: # one message per turn; tools run in between
final = message
if final is None or final.stop_reason == "refusal":
return "Sorry, I can't help with that."
return "".join(block.text for block in final.content if block.type == "text")
if __name__ == "__main__":
print(ask(" ".join(sys.argv[1:]) or "Do I need an umbrella in Yaba, Lagos today?"))#What the tools return
We ran both tools against the live APIs for "Yaba, Lagos" (late on 24 October 2026). The geocoder returned:
{"name": "Yaba, Lagos Mainland, Lagos, 102215, Nigeria", "latitude": 6.5068212, "longitude": 3.3754881}And the weather tool, called with those coordinates:
{"current": {"time": "2026-10-25T00:15", "interval": 900, "temperature_2m": 27.8, "precipitation": 0, "wind_speed_10m": 9.6}, "rain_chance_today": 94}With a 94% chance of rain, the right answer was "yes, take an umbrella", and that's exactly the information the agent gets to base it on. The flow is always the same: Claude asks for geocode_place, the runner executes it, Claude reads the coordinates and asks for get_weather, and the final answer is written from both results.
#Lessons from testing 15 APIs as agent tools
Before writing this, we turned 15 popular free APIs into tools and called them the way an agent would. Some things only show up when you do that:
1. Keep tool results small. Every character a tool returns goes into the model's context, and you pay for every token. PokéAPI's response for Pikachu is 309 KB: send that raw and one lookup costs more than the whole conversation. Return only the fields the model needs (the examples above pick out three weather values), and cap anything else. Our AI Tool Generator truncates handler output at 20,000 characters for this reason.
2. Descriptions are the interface. The model chooses tools almost entirely from their names and descriptions. "Call this before get_weather when the user names a place" in the geocoder's description is what makes Claude chain the two tools reliably. Say what the tool does, when to use it, and what each parameter means, with units and examples.
3. Validate inputs in the schema. z.number().min(-90).max(90) costs nothing and stops a bad call before it reaches the API. Models occasionally pass odd values; a schema rejection gives them a clear error to correct.
4. Return errors, don't crash. If an API fails, tell the model what happened (the tool runner turns a thrown error into an error result). It can retry, try another approach or explain the problem, instead of the whole run failing.
5. Respect each API's rules. Nominatim returned 403 Forbidden to our tests until we sent a User-Agent naming the app: its usage policy requires one, and it allows one request per second. An agent can make many calls quickly, so read the limits before you hand an API to one.
6. Keep keys out of the model's view. These two APIs need no key, but most do. Read keys from environment variables inside the tool function; never put them in the prompt or the tool description, where the model could repeat them.
7. Some popular APIs aren't agent-ready right now. On our test day, REST Countries' v3.1 endpoint redirected to a deprecation notice, Open Library returned 503 errors, and NASA's DEMO_KEY returned 500. Check an API's status page before building on it, and handle failures gracefully when you do.
#Add more tools
The same pattern works for any API. A few that make good agent tools, all free and tested:
- Frankfurter: currency conversion.
- Nager.Date: public holidays, so the agent doesn't schedule meetings on them.
- Wikipedia: article summaries for fact-checking.
- World Bank: official economic statistics.
You don't have to write the definitions by hand: paste a curl command into the AI Tool Generator and it writes the Claude or OpenAI tool definition, the handler, and an MCP server, with API keys kept out of the schema. The APIs for AI agents page has ready-made starting points for each API above.
#What's next
You now have the core loop every AI agent runs on: a model that decides, tools that act, and your code in between. From here you can add more tools, give the agent memory of past conversations, or package your tools as an MCP server so any MCP client (Claude Desktop, Claude Code, Cursor) can use them without your code.