EDUCATIONOverviewStudentsTeachersSchoolsCurriculumProject Library
GUIDED PROJECT·Advanced·~4 weeks·17 min read

Build an AI Agent

Build an AI agent that doesn't just chat — it uses tools to take real actions in a loop until a task is done. You'll implement the agent loop, give the model typed tools it can call, connect real capabilities via MCP, and build a way to evaluate whether it actually works.

WHAT YOU'LL WALK AWAY WITH
  • A working agent that plans, calls tools, and loops until a real task is complete
  • A clear understanding of the agent loop — the actual difference between a chatbot and an agent
  • Tool use done properly: typed tool definitions, execution, and feeding results back to the model
  • Real capabilities wired in via MCP, plus a way to evaluate whether the agent genuinely succeeds
TIER
Advanced
TIME
~4 weeks
SKILLS
Tool use, agent loops, evaluation
STACK
TypeScript, LLM APIs, MCP
PORTFOLIO
Very high — the frontier of the field
REQUIRES
AI Chatbot

Every company that shipped a chatbot last year is trying to ship an agent this year — something that doesn't just talk about a task but goes and does it. That shift, from answering to acting, is the most important thing happening in applied AI right now, and the engineering underneath it is smaller and more understandable than the hype suggests. It's a loop, a few typed tools, and a short list of guardrails that keep the loop from hurting you.

This walkthrough builds that from scratch. It assumes you've done the AI Chatbot project — you know why the API key lives on the server and never in the browser, you've sent message history to an LLM, and you've streamed a response back. This project reuses all of that and adds the two things that turn a chatbot into an agent: tools and the loop that runs them. If you haven't built the chatbot, build it first; everything here stands on it.

Info

Estimated time: ~4 weeks, part-time. The loop itself is maybe forty lines. The time goes into the parts most tutorials skip: giving the model real tools via MCP, and — the differentiator — actually evaluating whether your agent works. Rushing gets you a demo that works once on camera. Going slow gets you something you'd trust to run unattended.

What you're building

A command-line agent (or a small web UI — your choice at the end) that takes a goal in plain English and works toward it by calling tools. Ask it "what should I pack for Tokyo this week?" and instead of guessing, it calls a weather tool, reads the result, and then answers. Ask it something that needs three lookups and it does three, in order, deciding each step for itself.

The distinction that defines the whole project: a chatbot answers — one turn in, one turn out. An agent loops — it calls a tool, observes the result, decides what to do next, and repeats until the task is complete. That loop is the entire difference, and building it by hand is how you truly understand every agent framework you'll ever touch.

Stack: TypeScript, an LLM API (this guide uses Claude via the Anthropic SDK, but any tool-calling model works the same way), and MCP — the Model Context Protocol — as the standard way to plug real-world tools into your agent. As in the chatbot, every call to the model goes through your server so your API key never reaches the client.

Milestone 0 — Chatbot vs. agent: what the loop adds

Before writing anything, get the mental model exact, because every later milestone is a consequence of it.

A chatbot is one function call: you send the conversation, you get text back, you're done. An agent wraps that call in a loop and gives the model an escape hatch — a way to say "I can't answer yet; run this tool for me first." Here's the shape, in pseudocode:

messages = [the user's goal]
loop:
    response = call the model with (messages + the tools it may use)
    if the model asked to run a tool:
        result = run that tool yourself
        add the model's request AND your result to messages
        continue the loop        # go ask the model again
    else:
        return the model's answer # no tool needed — it's done

Sit with this until it clicks. The model never runs anything itself — it can only ask. Your code is the hands: it executes the tool and reports back. The model is the planner: it decides what to do with the result and whether it needs another step. That division — model plans, your code acts, results flow back — is the agent, full stop. The rest of this project is filling in each line.

Milestone 1 — Defining a tool the model can call (a typed schema)

A tool is three things: a name, a description (so the model knows when to reach for it), and a JSON-schema for its input (so the model knows how to call it). Define one:

const tools = [
  {
    name: "get_weather",
    description:
      "Get the current weather for a city. Call this whenever the user's " +
      "question depends on real, current weather.",
    input_schema: {
      type: "object",
      properties: {
        city: { type: "string", description: "City name, e.g. Tokyo" },
      },
      required: ["city"],
    },
  },
];

// The actual code that runs when the model calls the tool:
function runTool(name: string, input: any): string {
  if (name === "get_weather") {
    // A real version would call a weather API here (see the Weather App project).
    return `It is 18°C and clear in ${input.city}.`;
  }
  return `Unknown tool: ${name}`;
}

What to understand: the tool definition is a contract you hand the model, and the model reads it like documentation. The description is not decoration — it's how the model decides whether this tool is relevant to the question. The input_schema is how it knows to send a city string and not something else. And crucially, runTool — the code that actually does the work — is entirely yours. The model proposes; your function disposes.

Tip

Write the description as if for a new teammate: say when to use the tool, not just what it does. "Get the current weather" is weaker than "Call this whenever the user's question depends on real, current weather." Vague descriptions are the number-one reason an agent calls the wrong tool or skips one it needed.

Milestone 2 — The agent loop: call the model, run the tool, feed the result back, repeat

This is the milestone. Everything before it was setup; everything after it is extension. Build the loop:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment

const messages: any[] = [
  { role: "user", content: "What should I wear in Tokyo today?" },
];

for (let step = 0; step < 10; step++) {
  const response = await client.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    tools,
    messages,
  });

  // Record what the model said (which may include a request to call a tool).
  messages.push({ role: "assistant", content: response.content });

  if (response.stop_reason !== "tool_use") {
    break; // No tool requested — this is the final answer. We're done.
  }

  // The model asked for one or more tools. Run each, collect the results.
  const toolResults = response.content
    .filter((block: any) => block.type === "tool_use")
    .map((block: any) => ({
      type: "tool_result",
      tool_use_id: block.id,
      content: runTool(block.name, block.input),
    }));

  // Hand the results back as a user turn, then loop — the model decides what's next.
  messages.push({ role: "user", content: toolResults });
}

Read this line by line — it's the whole project.

  • stop_reason is the fork in the road. When the model wants a tool, it stops with stop_reason: "tool_use" and its reply contains a tool_use block naming the tool and its input. When it's done, it stops with something else (end_turn) and gives you plain text. That single field is how your loop knows whether to run again or finish.
  • Every tool_result carries a tool_use_id. The model may ask for several tools at once; the id is how each result gets matched back to the request that asked for it. Drop it and the conversation breaks.
  • You append both sides every turn — the model's request and your result — because the model is stateless and rebuilds its understanding from the message history each call. This is the exact same "send the whole conversation every time" lesson from the chatbot, now doing real work.
  • The loop ends on its own when the model produces an answer with no tool call. That's the model saying "I have what I need."
Heads up

Two ways this milestone bites, and both are real, unsolved problems in the field. First: an agent can loop forever, or rack up a huge bill, if the model keeps calling tools. The for loop's step < 10 cap is not optional — it's a hard stop that guarantees the loop ends. Always cap iterations, and decide what "give up" looks like when you hit the cap. Second, and more serious: never give an agent a tool that does something destructive or irreversible — deleting files, sending money, running shell commands — without a guard or a human confirmation step. The model will eventually call a tool at the wrong moment. A weather lookup being wrong is a shrug; a delete_account tool being wrong is a catastrophe. Treating tool access as a security boundary, not a convenience, is the difference between a toy and something you'd let near production.

Milestone 3 — Multiple tools, and letting the model choose which to call

One tool is a demo. The moment there are two, the model has to choose — and that choice, made well, is what makes an agent feel intelligent. Add a second tool and let the model pick:

const tools = [
  {
    name: "get_weather",
    description: "Get current weather for a city.",
    input_schema: {
      type: "object",
      properties: { city: { type: "string" } },
      required: ["city"],
    },
  },
  {
    name: "search_flights",
    description: "Find flights between two cities on a given date.",
    input_schema: {
      type: "object",
      properties: {
        from: { type: "string" },
        to: { type: "string" },
        date: { type: "string", description: "ISO date, e.g. 2026-08-01" },
      },
      required: ["from", "to", "date"],
    },
  },
];

function runTool(name: string, input: any): string {
  switch (name) {
    case "get_weather":
      return `It is 18°C and clear in ${input.city}.`;
    case "search_flights":
      return `Found 3 flights from ${input.from} to ${input.to} on ${input.date}.`;
    default:
      return `Unknown tool: ${name}`;
  }
}

What just changed, and why it matters: you didn't write any "if the question is about weather, use the weather tool" logic — and you shouldn't. The model reads all the tool descriptions and decides which one (or which sequence) fits the goal. Ask it to "plan a trip to Osaka next Friday" and it might call search_flights, read the result, then call get_weather for Osaka — two tools, chosen and ordered by the model, with your loop from Milestone 2 running each. Your job shifts from writing the logic to describing the tools well enough that the model's logic is good. That's the real skill of agent-building.

Milestone 4 — Connecting the real world with MCP

Hand-writing every tool doesn't scale — and you shouldn't have to write a GitHub tool, a Slack tool, and a filesystem tool from scratch when others already have. MCP (the Model Context Protocol) is the standard way to plug pre-built, real-world tools into an agent. An MCP server exposes a set of tools over a common protocol; your agent connects to it and those tools become available to the model exactly like the ones you defined by hand.

Conceptually, you point your agent at a server and its tools join the toolset:

// With the Anthropic MCP connector, you declare the server and the model
// can call its tools through the same loop you already built.
const response = await client.beta.messages.create({
  model: "claude-sonnet-5",
  max_tokens: 1024,
  betas: ["mcp-client-2025-11-20"],
  mcp_servers: [
    { type: "url", name: "filesystem", url: "https://your-mcp-server.example.com/mcp" },
  ],
  tools: [{ type: "mcp_toolset", mcp_server_name: "filesystem" }],
  messages,
});

Why this is worth a whole milestone: MCP is the reason the ecosystem is exploding right now. Instead of every app reinventing "how do I let the model touch my database / my files / my calendar," MCP standardizes it — write a server once, and any MCP-aware agent can use it. Understanding that your hand-rolled tools array and an MCP server are the same idea at two levels of scale is the insight here. You built the mechanism by hand in Milestones 1–3; MCP is that mechanism, standardized, with a growing library of servers you can plug in.

Heads up

Every caution from Milestone 2 applies double to MCP tools, because you didn't write them and may not have read their code. A third-party MCP server can expose tools that write files, hit APIs, or spend money. Before you connect one to an agent that runs unattended, know exactly what its tools can do — and keep the destructive ones behind a confirmation step. "I trusted the server" is not a defense your future self will accept.

Milestone 5 — Evaluation: does it actually work? (test cases and tracing)

Here's the milestone almost every agent tutorial skips, and it's the one that separates you from everyone who "built an agent" that works once and breaks silently forever after. How do you know your agent works? Not "it looked right that one time" — actually know. The answer is the same as for any other software: test cases with expected outcomes, plus tracing so you can see what happened inside a run.

Start with a tiny eval harness — a list of goals and what a correct run should do:

const cases = [
  { goal: "Weather in Tokyo?", expectTool: "get_weather" },
  { goal: "Flights from NYC to Osaka on 2026-08-01?", expectTool: "search_flights" },
  { goal: "What is 2 + 2?", expectTool: null }, // should answer directly, no tool
];

for (const test of cases) {
  const trace: string[] = [];
  const answer = await runAgent(test.goal, trace); // your loop, logging each step into `trace`

  const usedTool = trace.find((t) => t.startsWith("tool:"));
  const pass = test.expectTool
    ? usedTool === `tool:${test.expectTool}`
    : usedTool === undefined;

  console.log(`${pass ? "PASS" : "FAIL"} — ${test.goal}`);
  if (!pass) console.log(trace.join("\n")); // print the full trace when it fails
}

What to understand: an agent is non-deterministic — the same goal can produce different steps on different runs — which is exactly why you can't eyeball it. You need two things. Test cases pin down "for this goal, a correct run calls this tool (or none) and reaches this kind of answer." Tracing — logging every loop iteration: which tool the model chose, what input it sent, what result came back — is how you debug a failure, because the bug is almost never in the final answer; it's three steps back, in a tool call that returned garbage the model then reasoned from. Build the trace into your loop from the start. When an agent does something baffling, the trace is the difference between a five-minute fix and an afternoon of guessing.

Tip

This is the milestone to talk about in an interview. Anyone can wire up a loop. "How do you know your agent is correct, and how do you debug it when it isn't?" is the question that reveals whether you actually understand what you built — and most people who cloned a tutorial have no answer.

Milestone 6 — Ship it

An agent nobody can run is a screenshot. Give it a front door — a CLI is the fastest honest option:

  1. Wrap your loop in a script that reads a goal from the command line (process.argv) and prints the final answer, with an optional --trace flag that dumps the step-by-step log.
  2. Keep the model call — and your API key — on the server side, exactly as the chatbot taught. If you build a small web UI instead of a CLI, the browser sends the goal to your backend, and the backend runs the loop. The key never ships to the client.
  3. Set sensible defaults for the two guards from Milestone 2: a max-iteration cap and a confirmation prompt before any destructive tool runs.

Run it against a goal that genuinely needs two tools, watch the trace, and confirm the loop starts, calls tools, and stops on its own. That — a program that plans, acts, observes, and finishes — is an agent you built and understand end to end.

Stuck? Study a production build

When something won't click, read real code — but after you've built your own loop, so you can see what each project added and why, not just copy an answer:

  • modelcontextprotocol/servers — reference MCP servers. This is the exact pattern for giving an agent real-world tools; read one server to see how the tool definitions you wrote by hand look at production scale.
  • anthropics/anthropic-sdk-typescript — the typed client this build uses. Its tool-use examples map directly onto the loop in Milestone 2.
  • langchain-ai/langchainjs — a full agent framework. Build yours from scratch first, then read this to see which of your hand-rolled pieces — the loop, the tool dispatch, the tracing — it wraps in abstractions. Now the abstractions will make sense instead of hiding the mechanics.

The order is the whole point: build first, read second. Reading a framework before you've written the loop teaches you its API. Writing the loop first teaches you what the framework is for.

Build it with Cartara

Here's the honest part, and it's sharper for this project than any other in the library. You could paste every block above and get an agent running without understanding the loop, the stop_reason fork, or why the iteration cap exists — and increasingly, an AI assistant will have written large parts of it for you. Building AI with AI is the fastest way there is to end up with a working system you cannot explain: it runs, but you couldn't change it, debug a runaway loop, or say why an agent called the wrong tool — and that gap is invisible from the outside until it bites.

Closing that gap is exactly what Cartara is for. As you build:

  • Point Cartara at your repo. It watches the code that lands — including anything an AI assistant generated — and turns each meaningful change into a checkpoint for understanding, not just a commit.
  • Explain your changes back. When you add the loop, a new tool, or the iteration guard, Cartara asks you the why and the trade-offs — the same questions a reviewer or interviewer would, on the exact code you (or your AI pair) just wrote.
  • See your comprehension, not just your output. You get an honest read on which parts of your own agent you actually understand — the loop, the tool contracts, the eval harness — so you can shore up the weak spots before they surface in a review.

That's the loop this whole library is built around: build something real, then prove to yourself you understand it. The agent is the project. The understanding is the point — and when you're building AI with AI, it's the only thing that makes the work truly yours.

Build an agent you can actually explain

Point Cartara at your agent repo and it captures what you genuinely learned — the honest way to make an AI-assisted project on the frontier of the field truly your own.

Join the waitlist

Reference implementations

Production code worth studying — afteryou've built your own version.

modelcontextprotocol/servers
Reference MCP servers — the exact pattern for giving an agent real-world tools in Milestone 4.
anthropics/anthropic-sdk-typescript
The typed client whose tool-use examples map directly onto the agent loop you'll build.
langchain-ai/langchainjs
A full agent framework. Build yours from scratch first, then read this to see which of your hand-rolled pieces it abstracts.
← All projects

Build it with Cartara

Point Cartara at your repo as you build this project. It captures what you actually learned from every change — including the code an AI assistant wrote — so an AI-assisted build becomes understanding you can prove.

Join the waitlist