---
title: "Your First Tool"
description: "Add the lookup_service tool with defineTool and a Zod schema so the dispatcher quotes from the real catalog instead of guessing, and watch the tool loop run."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/your-first-tool"
md_url: "https://vercel.com/academy/building-agents-with-eve/your-first-tool.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-30T07:43:25.866Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Your First Tool

# Your First Tool

Ask the dispatcher what a brake bleed costs right now, and you'll get unpredictable results. You might get some follow up questions, or you might get a confident number that is completely made up. The persona told it to "quote in real dollars from the catalog," but there's no catalog it can actually read. The language model fills the gap with a guess.

That's the moment a tool becomes important. A tool is the agent's hands: a typed function it can call to go *get* a real answer instead of inventing one. What the shop needs is a catalog of services and prices, and a door the model can reach through to read it.

We'll drop in a ready-made catalog (hand-typing a price list teaches nothing), then build the door ourselves. In eve that door is one file, and the file's name *is* the tool's name.

## Outcome

The dispatcher quotes a real price from the catalog by calling a `lookup_service` tool you wrote, instead of guessing.

## Hands-on exercise

Watch the wrong version first. With no tool, ask for a price:

```text
you › what's a hydraulic brake bleed run me?

dispatcher › I'm having a little trouble pulling up the service catalog right now.
```

"I'd estimate." "Usually around." That's a guess dressed up as an answer, and the price isn't ours. Now give it the real thing.

First, give the shop a catalog to read from. Instead of hand-typing a price list, drop in a ready-made one. In a new terminal\*\*, \*\*same directory as the project, run:

```bash
curl -f --create-dirs -o agent/lib/shop.ts https://raw.githubusercontent.com/vercel-labs/bike-shop-agent/main/agent/lib/shop.ts
```

That file is deliberately minimal: an in-memory array of services with prices and time estimates, plus a couple of helpers (`listServices`, `formatUsd`) to read it. It's the one piece you don't hand-build, because a hard-coded price list isn't the interesting part. Reaching it through a typed tool is. Swap it for a real booking system or warehouse later, and the tool you're about to write won't notice the difference.

Now create `agent/tools/lookup_service.ts`. Three pieces make a tool:

- A **`description`** the model reads to decide when to call it. Write it for the model: say what it does *and* when to reach for it.
- An **`inputSchema`**, a Zod object that both validates the call and types the argument you get in `execute`. Here, an optional `query` to filter the catalog.
- An **`execute`** function, your code, running in the app runtime with full access to `agent/lib/`.

The catalog you just dropped in does the data work. Import `listServices` and `formatUsd` from `../lib/shop.js` (note the `.js` extension, that's how Node's module resolution wants the import written even though the file is `.ts`).

\*\*Note: The filename is the tool name\*\*

You won't register this tool anywhere. The file lives at `agent/tools/lookup_service.ts`, so the model sees a tool named `lookup_service`. Rename the file and you rename the tool. That's the filesystem-first idea in one move: the directory *is* the configuration.

Map the catalog rows down to the fields the model actually needs, and turn cents into dollars with `formatUsd` so the agent never has to do math on a price.

## Try It

Restart the dev server and ask the same question:

```bash
npx eve dev
```

```text
you › what's a hydraulic brake bleed run me?

  ⚙ lookup_service  { query: "brake" }
  ↳ [{ id: "brake-bleed", name: "Hydraulic Brake Bleed", description: "Flush the old fluid and bleed the brakes back to a firm lever.", price: "$55.00", estMinutes: 40 }]

dispatcher › A Hydraulic Brake Bleed is $55 and takes about 40 minutes. That
flushes the old fluid and bleeds the brakes back to a firm lever, which is
exactly what a spongy lever usually needs. Want me to check for an opening?
```

Watch the loop in the TUI: the model emitted a `lookup_service` call, eve ran your `execute`, the real row came back as a tool result, and *then* the model answered, with the catalog's number, not its own. You wrote the tool; eve drove the loop.

\*\*Note: It chose the query itself\*\*

Notice you never told it to search for "brake." The model read the customer's question, decided `lookup_service` fit, and picked the query argument on its own. Your job was to describe the tool well enough that it knew when and how to use it.

Seeing `⚙ lookup_service` with no result, or a type error on the import? Two usual suspects: the import path needs the `.js` extension (`../lib/shop.js`), and the file must `export default` the `defineTool(...)` call. Run `eve info` to confirm eve discovered the tool, you should see `lookup_service` in the tools list.

## Done-When

- [ ] `agent/tools/lookup_service.ts` default-exports `defineTool` with a `description` and Zod `inputSchema`.
- [ ] `eve info` lists `lookup_service` among the tools.
- [ ] Asking for a price triggers a visible `lookup_service` call in the TUI.
- [ ] The quoted number matches the catalog in `agent/lib/shop.ts`, not an invented estimate.

## Solution

```ts title="agent/tools/lookup_service.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";
import { listServices, formatUsd } from "../lib/shop.js";

export default defineTool({
  description:
    "Look up the repair services the shop offers, with prices and time estimates. " +
    "Pass a query to filter (e.g. 'brake', 'wheel'); omit it to list everything.",
  inputSchema: z.object({
    query: z.string().optional().describe("Optional keyword to filter services."),
  }),
  async execute({ query }) {
    return listServices(query).map((s) => ({
      id: s.id,
      name: s.name,
      description: s.description,
      price: formatUsd(s.priceCents),
      estMinutes: s.estMinutes,
    }));
  },
});
```

The `inputSchema` does double duty: it validates whatever the model passes, and it types the `{ query }` you destructure in `execute`. That's why Zod 4 matters here, eve uses the schema as the model-facing contract, and a Zod 3 schema won't satisfy it. The catalog in `agent/lib/shop.ts` is the file you curled in, kept deliberately dumb because a hard-coded list of services isn't the interesting part; reaching it through a typed tool is.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
