N
NEXUSCORP
ALERT

How to Add Tools

Give your AI chatbot the ability to call APIs

WHAT IS A TOOL?

A tool is a function your AI chatbot can call. When a user asks a question, the AI decides if it needs a tool, calls it with the right parameters, gets the result, and uses that to answer.

Example: user asks "Who accessed Server Room B last night?" → AI calls your searchAccessLogs tool → tool fetches data from the Arena API → AI reads the result and tells the user.

SETUP

Environment variables

Before you start: copy .env.example to .env.local and fill in these values:

.env.local
# Generate a random secret: openssl rand -base64 32
AUTH_SECRET=your-random-secret-here

# The Arena URL — used by your tools to call the challenge APIs
ARENA_BASE_URL=https://arena-murex.vercel.app

# OpenRouter API key — log in at https://tinyfish-hackerschool.vercel.app to get yours
OPENROUTER_API_KEY=your-openrouter-key-here
  • AUTH_SECRET — run openssl rand -base64 32 in your terminal to generate one
  • ARENA_BASE_URL — this is where the Arena API lives. All your tools call this.
  • OPENROUTER_API_KEY — log in at tinyfish-hackerschool.vercel.app to get your key

Part 1: Create a Tool

TOOL STRUCTURE

Every tool file looks like this

Create a new file in lib/ai/tools/. Every tool has the same structure — 2 imports and 3 parts:

lib/ai/tools/your-tool-name.ts
import { tool } from "ai";
import { z } from "zod";

export const yourToolName = tool({
  description: "...",    // YOU write this — tells the AI when to use the tool

  inputSchema: z.object({   // Copy from the reference below
    // ...
  }),

  execute: async (input) => {   // Copy from the reference below
    // ...
  },
});
  • descriptionYou write this. This is how the AI decides when to use your tool. Be specific — tell the AI what the tool does and what kind of questions it helps answer.
  • inputSchemaDefines what parameters the tool accepts. Copy this from the tool reference sections below.
  • executeThe function that runs when the AI calls the tool. Copy this from the tool reference sections below.

Part 2: Wire It Up

After creating your tool file, you need to connect it to the chatbot. There are 3 files to update. Follow the comments in each file — they say WORKSHOP.

A

Register the type

lib/types.ts

Add 3 lines. Look at how getWeather is already done as an example:

// 1. Import your tool (at the top, next to the getWeather import):
import type { yourToolName } from "./ai/tools/your-tool-name";

// 2. Create a type alias (below the existing weatherTool type):
type yourToolType = InferUITool<typeof yourToolName>;

// 3. Add it to ChatTools (inside the existing type):
export type ChatTools = {
  getWeather: weatherTool;
  yourToolName: yourToolType;  // ← add this line
};
B

Import and add to the chat route

app/(chat)/api/chat/route.ts

Two changes in this file:

// 1. Import your tool (near the top, find the WORKSHOP comment):
import { yourToolName } from "@/lib/ai/tools/your-tool-name";

// 2. In the streamText() call, uncomment the tools block and add yours:
tools: {
  yourToolName: yourToolName,
  // add more tools here as you build them...
},

// 3. Also uncomment this line (right below tools) to let the AI
//    call multiple tools in a row:
stopWhen: stepCountIs(5),
C

Update the system prompt

lib/ai/prompts.ts

Edit the regularPrompt string to tell the AI what tools it has. This helps the AI know when to call them. Example:

export const regularPrompt = `You are a NEXUS Corp security investigator.

You have the following tools:
- searchAccessLogs: Search facility access logs by employee, location, and time range

When asked about who accessed a location or when someone entered a facility,
use the searchAccessLogs tool to look it up.`;

This is just a text string — write whatever makes sense. The more specific you are about when to use each tool, the better the AI will be at using them.

D

Add the tool to the chat UI

components/message.tsx — show tool calls in the conversation

When your AI calls a tool, the chat needs to know how to display it. Open components/message.tsx and find the comment that says WORKSHOP: Add your tool UI rendering here. This is where you tell the chatbot what to show when a tool runs.

Use this Claude Code prompt to do it for you:

PASTE THIS INTO CLAUDE CODE
I just added new tools to my chatbot. Now I need to render them in the chat UI.

In components/message.tsx, there's a comment that says "WORKSHOP: Add your tool UI rendering here" (around line 276). For each tool I've registered in the tools object in route.ts, add a rendering block there.

Use the existing getWeather tool rendering (the "tool-getWeather" block above the workshop comment) as a reference for the pattern. For my new tools, keep it simple — just show the default Tool/ToolHeader/ToolInput/ToolOutput components (no need for custom UI like the Weather component). Make sure to handle the "output-available" state to show results.

Run this prompt every time you add new tools. It adds the rendering code so tool calls show up in the chat with their parameters and results.

THEN TEST IT

Restart your dev server (pnpm dev) and try chatting. Repeat these steps each time you add a new tool.

Part 3: Tool Reference

Copy the inputSchema and execute for each tool you need. You write the description yourself.

1

Round 1 — Find the Insider

1 tool needed

searchAccessLogs

lib/ai/tools/search-access-logs.ts — calls /api/logs

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    employeeId: z
      .string()
      .describe("Filter by employee ID, e.g. EMP-047")
      .optional(),
    location: z
      .string()
      .describe("Filter by location name, e.g. 'Server Room B'")
      .optional(),
    startTime: z
      .string()
      .describe("Filter logs after this ISO timestamp, e.g. 2045-03-15T00:00:00Z")
      .optional(),
    endTime: z
      .string()
      .describe("Filter logs before this ISO timestamp, e.g. 2045-03-16T00:00:00Z")
      .optional(),
  }),


  execute: async (input) => {
    const params = new URLSearchParams();
    if (input.employeeId) params.set("employeeId", input.employeeId);
    if (input.location) params.set("location", input.location);
    if (input.startTime) params.set("startTime", input.startTime);
    if (input.endTime) params.set("endTime", input.endTime);

    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/logs?${params.toString()}`
    );
    const data = await response.json();
    return data;
  },
2

Round 2 — Trace Communications

1 new tool

searchCommunications

lib/ai/tools/search-communications.ts — calls /api/communications

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    senderId: z
      .string()
      .describe("Filter by participant employee ID (matches sender or recipient), e.g. EMP-047")
      .optional(),
    recipientId: z
      .string()
      .describe("Filter by the other participant employee ID")
      .optional(),
    channel: z
      .string()
      .describe("Filter by channel: 'email', 'internal-chat', or 'encrypted'")
      .optional(),
    search: z
      .string()
      .describe("Full-text search in subject and body")
      .optional(),
    startDate: z
      .string()
      .describe("Filter messages after this ISO date")
      .optional(),
    endDate: z
      .string()
      .describe("Filter messages before this ISO date")
      .optional(),
    page: z
      .number()
      .describe("Page number (default: 1)")
      .optional(),
    pageSize: z
      .number()
      .describe("Results per page (default: 20)")
      .optional(),
  }),


  execute: async (input) => {
    const params = new URLSearchParams();
    if (input.senderId) params.set("senderId", input.senderId);
    if (input.recipientId) params.set("recipientId", input.recipientId);
    if (input.channel) params.set("channel", input.channel);
    if (input.search) params.set("search", input.search);
    if (input.startDate) params.set("startDate", input.startDate);
    if (input.endDate) params.set("endDate", input.endDate);
    if (input.page) params.set("page", input.page.toString());
    if (input.pageSize) params.set("pageSize", input.pageSize.toString());

    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/communications?${params.toString()}`
    );
    const data = await response.json();
    return data;
  },
3

Round 3 — Map the Network

1 new tool + reuses searchCommunications

getEmployee

lib/ai/tools/get-employee.ts — calls /api/employees/{id}

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    employeeId: z
      .string()
      .describe("The employee ID, e.g. EMP-023"),
  }),


  execute: async (input) => {
    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/employees/${input.employeeId}`
    );
    const data = await response.json();
    return data;
  },
4

Round 4 — Locate the Threat

3 new tools

getFacilityPolicies

lib/ai/tools/get-facility-policies.ts — calls /api/facilities/policies

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    category: z
      .string()
      .describe("Filter by category: 'access', 'security', 'facilities', or 'emergency'")
      .optional(),
  }),


  execute: async (input) => {
    const params = new URLSearchParams();
    if (input.category) params.set("category", input.category);

    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/facilities/policies?${params.toString()}`
    );
    const data = await response.json();
    return data;
  },

searchBookings

lib/ai/tools/search-bookings.ts — calls /api/facilities/bookings

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    roomId: z
      .string()
      .describe("Filter by room ID, e.g. F-12")
      .optional(),
    bookedBy: z
      .string()
      .describe("Filter by employee ID who made the booking, e.g. EMP-023")
      .optional(),
    startDate: z
      .string()
      .describe("Filter bookings starting after this date")
      .optional(),
    endDate: z
      .string()
      .describe("Filter bookings ending before this date")
      .optional(),
    page: z
      .number()
      .describe("Page number (default: 1)")
      .optional(),
  }),


  execute: async (input) => {
    const params = new URLSearchParams();
    if (input.roomId) params.set("roomId", input.roomId);
    if (input.bookedBy) params.set("bookedBy", input.bookedBy);
    if (input.startDate) params.set("startDate", input.startDate);
    if (input.endDate) params.set("endDate", input.endDate);
    if (input.page) params.set("page", input.page.toString());

    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/facilities/bookings?${params.toString()}`
    );
    const data = await response.json();
    return data;
  },

getRoomDetails

lib/ai/tools/get-room-details.ts — calls /api/facilities/rooms/{id}

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    roomId: z
      .string()
      .describe("The room ID, e.g. F-12"),
  }),


  execute: async (input) => {
    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/facilities/rooms/${input.roomId}`
    );
    const data = await response.json();
    return data;
  },
5

Round 5 — Secure the Server

3 new tools + reuses getEmployee and getRoomDetails

getServerDetails

lib/ai/tools/get-server-details.ts — calls /api/systems/{id}

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    serverId: z
      .string()
      .describe("The server ID, e.g. NX-7042"),
  }),


  execute: async (input) => {
    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/systems/${input.serverId}`
    );
    const data = await response.json();
    return data;
  },

listSecurityPatches

lib/ai/tools/list-security-patches.ts — calls /api/systems/patches

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    severity: z
      .string()
      .describe("Filter by severity: 'critical', 'high', 'medium', or 'low'")
      .optional(),
    systemType: z
      .string()
      .describe("Filter by target system type: 'compute', 'storage', 'network', or 'backup'")
      .optional(),
    targetFirmware: z
      .string()
      .describe("Filter by target firmware version, e.g. '3.2.1'")
      .optional(),
  }),

  execute: async (input) => {
    const params = new URLSearchParams();
    if (input.severity) params.set("severity", input.severity);
    if (input.systemType) params.set("systemType", input.systemType);
    if (input.targetFirmware) params.set("targetFirmware", input.targetFirmware);

    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/systems/patches?${params.toString()}`
    );
    const data = await response.json();
    return data;
  },

applySecurityPatch

lib/ai/tools/apply-security-patch.ts — calls POST /api/systems/control

You write the description — copy the inputSchema and execute below:

  inputSchema: z.object({
    serverId: z
      .string()
      .describe("The server ID to patch, e.g. NX-7042"),
    patchId: z
      .string()
      .describe("The patch ID to apply, e.g. PATCH-2045-0312"),
    authCode: z
      .string()
      .describe("Authorization code in format [Badge Number]-[Room Code], e.g. BDG-1234-F-12"),
    justification: z
      .string()
      .describe("Written justification for applying the patch"),
  }),

  execute: async (input) => {
    const response = await fetch(
      `${process.env.ARENA_BASE_URL}/api/systems/control`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          serverId: input.serverId,
          patchId: input.patchId,
          authCode: input.authCode,
          justification: input.justification,
        }),
      }
    );
    const data = await response.json();
    return data;
  },

TIPS

  • [1]Add tools as you go. You don't need all 9 tools at once. Add the tools for your current round, wire them up, test, then move on.
  • [2]Descriptions matter a lot. The AI uses your description to decide when to call the tool. If the AI isn't using your tool, try rewriting the description to be more specific.
  • [3]The system prompt matters too. Experiment with it. Tell the AI what role it plays, what tools it has, and when to use them. A good system prompt makes the AI much more effective.
  • [4]The AI calls tools automatically. You don't write any code to "call" the tool. The AI decides when to use it based on the user's question, the tool's description, and the system prompt.
  • [5]Check the Challenges page for API details. Each challenge lists the endpoints, parameters, and example responses if you want to understand what the tools do under the hood.