Klippy can talk, but it can't look anything up. Ask it about the weather and the best it can do is guess. In this lesson you'll write its first tool, getWeather, which fetches live weather from a free service, and let Claude decide when to call it.
Verified ai 7.0.162zod 4.6.7agents 0.33.0wrangler 4.157.0
Start from b-01 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout b-01
1. One new package
Claude needs to know what arguments a tool takes. You'll describe them with zod, a library for describing the shape of data in TypeScript. The AI SDK turns a zod description into JSON Schema, the format Claude reads.
pnpm --filter agent add --save-exact zod@4.6.72. Describe the tool
A tool has three parts: a description in plain English, an input schema for its arguments, and an execute function that does the work. Claude only ever sees the first two. Make apps/agent/src/tools/weather.ts, starting with the description and the schema:
tool({...})comes from the AI SDK and bundles the three parts.The description is short on purpose for now. In lesson 4 you'll see why a better one changes what Claude does.
z.object({ place: z.string() })says the input is an object with one string,place..describe(...)is a note for Claude about that field, with an example of the format you want.
3. Do the work
Now the part that runs when Claude asks. Add execute inside the tool({...}), after inputSchema:
Open-Meteo is free and needs no key. It works in two steps: turn a place name into coordinates (geocoding), then ask for the weather there.
A place it can't find returns
{ error: ... }as a normal result. Claude reads it and tells the user, instead of the request failing.The result is a small object with just what's needed: Claude reads every field you return, and every field costs tokens.
Two helpers finish the file. Add them at the bottom:
getJson fetches a URL and reads the JSON. describeWeather turns Open-Meteo's numeric weather code into words. Lesson 7 makes getJson honest about failures.
4. Hand Klippy the tool
In apps/agent/src/ask.ts, import the tool and give it to generateText:
tools: { getWeather }sends the tool's description and schema to the model with every request. The key,getWeather, is the name Claude uses.stopWhen: isStepCount(2)is the important new line. With a tool, one question can take two model calls, called steps: in step 1, Claude asks for the tool and the SDK runs it; in step 2, Claude answers with the result. Then it stops, whatever happens, so a confused model can't loop and run up your bill. (Older code calls itstepCountIs; inai7 that's an alias ofisStepCount.)
5. Ask about the weather
With pnpm dev running, ask with your token (from Your first Worker, in apps/agent/.dev.vars):
curl -s -X POST http://localhost:8787/ask -H "Authorization: Bearer $KLIPPY_TOKEN" -d '{"question":"What is the weather in Berlin right now?"}'Run export KLIPPY_TOKEN=... with your token first, or paste it in place of $KLIPPY_TOKEN. Here's ours:
Those numbers are real: they came from Open-Meteo on 3 November 2026. The answers were written by the free fallback model (see model), since our gateway has no Anthropic key. With yours, Claude writes them.
If the answer guesses instead of giving numbers, check that tools: {
getWeather } is in ask.ts and that the dev server reloaded. If it's empty, the model ran out of steps: check isStepCount(2).
6. Tests that don't call anyone
Klippy's tests fake two things: the model, which "decides" to call getWeather and then answers, and Open-Meteo, so no request leaves your machine. They check that the tool's result is what goes back to the model. Copy test/weather.spec.ts and the updated test/fake-model.ts from the repo, then run pnpm test. Ours: 16 passed.
One habit for the whole course: each lesson's tests live in the repo at that lesson's tag, and earlier test files sometimes grow too. The simplest way to stay in step is to copy the whole apps/agent/test/ folder from the tag at the end of each lesson.
Commit. Your code matches tag b-02 (changes from b-01).