# Tools

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Core Primitives
Source: https://www.devtutorials.ai/learn/mcp/core-primitives/tools

> How to define MCP tools well: name, title, description, JSON Schema 2020-12 input schemas, atomic single-verb design, strict types, and annotations for side effects.

A good kitchen has labeled drawers. "Knives." "Spoons." "Measuring cups." Anyone who walks in can find what they need, and nobody grabs a knife when they wanted a spoon.

A model choosing an MCP tool is like a cook in a strange kitchen. All it has are the labels: each tool's name, its description and its input schema. If the labels are clear, it picks the right tool and fills in the right arguments. If they are vague, it guesses.

**The main idea:** a tool is an action the model can ask for. Its **name**, **description**, **input schema** and **annotations** are all the model and the host know about it, so they must be clear, exact and honest.

## A Real-World Example

Brightgrid's first help-desk server had one tool called `orders`. Its description said "Order tool." Its input was one text field called `data`.

- The model sent `data: "where is my lamp"`. The tool expected an order id.
- Sometimes the model used `orders` to cancel. Sometimes to look up. Nobody could tell which.
- The host couldn't tell which calls were safe, so it asked the user to approve every single one.

The team split it into small tools: `lookup_order`, `create_ticket`, `cancel_order`. Each got a clear description, a strict schema and honest annotations. Wrong calls dropped, and the host now asks for approval only on `cancel_order` and `create_ticket`.

**In simple words:** one tool, one job, clearly labeled.

## What You Will Learn

In this lesson, you will learn:

1. The parts of a tool definition.
2. How to name tools.
3. How to write descriptions a model can use.
4. How input schemas work, using JSON Schema 2020-12.
5. Why tools should be small, single-verb actions with strict types.
6. How annotations describe side effects.

The **MCP Lab** server now has five tools. Open **Server code** to read how each one is defined with the official SDK. The editor is the same code editor as the lesson code boxes, with a **Format code** button.

Now that you know why tools matter, let's look at the definition the server advertises.

## Tool Definition

### The Parts of a Tool

A tool definition tells the model, the host, and other programs what they need to know about a tool. It describes what the tool is called and does, which arguments it accepts, and how the host should treat it.

A server describes each tool in its `tools/list` answer. A tool has the following parts:

| Field | Required | Who reads it |
| --- | --- | --- |
| `name` | yes | Programs: the model asks for a tool by its name |
| `title` | no | People: a friendly label for the host's screen |
| `description` | no, but always write one | The model: what the tool does and when to use it |
| `inputSchema` | yes | The model and both sides' validators: the arguments |
| `outputSchema` | no | Programs: the shape of structured results (Chapter 4) |
| `annotations` | no | The host: hints about side effects |

```js runnable title="Read a tool definition"
// One tool, as a server describes it in tools/list.
const tool = {
  name: "lookup_order",
  title: "Look up an order",
  description:
    "Get the item, status and delivery date of one order. Use it when the customer gives an order id.",
  inputSchema: {
    $schema: "https://json-schema.org/draft/2020-12/schema",
    type: "object",
    properties: {
      order_id: { type: "string", description: "The order id, like A-1001" },
    },
    required: ["order_id"],
  },
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
};

console.log("name (for programs): ", tool.name);
console.log("title (for people):  ", tool.title);
console.log("arguments:           ", Object.keys(tool.inputSchema.properties).join(", "));
console.log("required:            ", tool.inputSchema.required.join(", "));
console.log("schema dialect:      ", tool.inputSchema.$schema.split("/").slice(-2)[0]);
```

Expected output:

```text
name (for programs):  lookup_order
title (for people):   Look up an order
arguments:            order_id
required:             order_id
schema dialect:       2020-12
```

In the SDK, you define all of this in one call:

```js
server.registerTool(
  "lookup_order",
  {
    title: "Look up an order",
    description: "Get the item, status and delivery date of one order. Use it when the customer gives an order id.",
    inputSchema: z.object({
      order_id: z.string().describe("The order id, like A-1001"),
    }),
    annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
  },
  async ({ order_id }) => {
    // ... look up the order and return content
  },
);
```

The SDK turns the `z.object(...)` schema into JSON Schema for `tools/list` and uses it to validate every call before the tool runs.

Once a tool is defined, the first thing a model sees is its name. A clear name helps the model quickly identify the right action without relying entirely on the description.

## Naming

### Name Rules

A tool's name is often the first piece of information the model sees. A clear and consistent name helps the model quickly identify the correct tool and reduces confusion when multiple tools are available.

The spec says a tool name should be 1 to 128 characters long and use only letters, digits, underscore (`_`), hyphen (`-`) and dot (`.`). Names are case-sensitive, and each name must be unique within one server.

```js runnable title="Check tool names"
// Tool names: 1 to 128 characters, using only letters, digits, _ - and .
const valid = /^[A-Za-z0-9_.-]{1,128}$/;

const names = [
  "lookup_order",
  "orders.cancel",
  "create-ticket",
  "look up order",
  "get_order!",
  "",
  "x".repeat(130),
];
for (const n of names) {
  const shown = n.length > 20 ? n.slice(0, 17) + "..." : JSON.stringify(n);
  console.log(shown.padEnd(22), valid.test(n) ? "ok" : "not allowed");
}
```

Expected output:

```text
"lookup_order"         ok
"orders.cancel"        ok
"create-ticket"        ok
"look up order"        not allowed
"get_order!"           not allowed
""                     not allowed
xxxxxxxxxxxxxxxxx...   not allowed
```

Some hosts are stricter than the spec. A few drop tools whose names contain dots or hyphens. The safest names use only lowercase letters, digits and underscores, like <code>lookup_order</code>.

### Good Names

- Use a **verb and a noun**: `lookup_order`, `create_ticket`, `cancel_order`.
- Be specific: `search_wiki` is better than `search` when a host may connect several servers.
- Keep one style across the server.

A good name helps the model find a tool, but the description helps it decide when to use it.

## Descriptions

### What a Good Description Says

The description is the main thing the model reads when it decides whether to use a tool. A good description says:

1. **What** the tool does, starting with a verb.
2. **When** to use it, and when not to.
3. Any **limits**: "one order at a time", "only orders from the last 90 days".

```js runnable title="Review tool descriptions"
// A quick check for tool descriptions: what does it do, and when should the model use it?
function reviewDescription(d) {
  const notes = [];
  if (d.length < 30) {
    notes.push("too short");
  }
  if (
    !/^(Get|Find|List|Create|Open|Cancel|Send|Update|Delete|Search|Build|Check)\b/.test(d)
  ) {
    notes.push("start with a verb");
  }
  if (!/\b(use it|use this|only when|when the)\b/i.test(d)) {
    notes.push("say when to use it");
  }
  return notes.length ? notes.join(", ") : "good";
}

const descriptions = [
  "Order tool.",
  "Get the status and delivery date of one order.",
  "Get the status and delivery date of one order. Use it when the customer gives an order id.",
  "This tool can be used for many order things.",
];
for (const d of descriptions) {
  console.log(reviewDescription(d).padEnd(40), "|", d.slice(0, 45));
}
```

Expected output:

```text
too short, start with a verb, say when to use it | Order tool.
say when to use it                       | Get the status and delivery date of one order
good                                     | Get the status and delivery date of one order
start with a verb, say when to use it    | This tool can be used for many order things.
```

Each argument can have its own description too, with `.describe(...)` in the SDK. Say what format you expect: "The order id, like A-1001".

Every tool description is sent to the model on every request that includes the tool list. Ten tools with long descriptions can cost hundreds of tokens each time. Be clear, but short. And never put instructions in a description that you wouldn't want the model to follow, like "always call this tool first". Hosts and reviewers treat that as tool poisoning.

After a model chooses a tool, it must provide the correct arguments. An input schema defines what those arguments should look like and helps prevent invalid tool calls.

## Input Schemas

### JSON Schema 2020-12

An input schema is written in **JSON Schema**, a standard way to describe JSON data. MCP tools use the **2020-12** version of JSON Schema. When no other version is named, that is the default, and the SDK writes it out in `$schema`.

The schema of a tool's arguments is always an object:

- `type: "object"`.
- `properties`: one entry per argument, with its type.
- `required`: the arguments that must be there.

### Strict Types

The tighter the schema, the fewer wrong calls. JSON Schema can say:

- `enum`: only these values, like `["low", "high"]`.
- `minLength`, `maxLength`: text length.
- `minimum`, `maximum`: number range.
- `pattern`: a regular expression the text must match.

```js runnable title="Strict types for create_ticket"
// Strict types: the schema for create_ticket, as the SDK sends it.
const schema = {
  type: "object",
  properties: {
    summary: { type: "string", minLength: 5, maxLength: 200 },
    priority: { type: "string", enum: ["low", "high"] },
  },
  required: ["summary", "priority"],
};

for (const [name, rule] of Object.entries(schema.properties)) {
  const limits = [
    rule.enum && `one of ${rule.enum.join("/")}`,
    rule.minLength !== undefined && `at least ${rule.minLength} characters`,
    rule.maxLength !== undefined && `at most ${rule.maxLength} characters`,
  ]
    .filter(Boolean)
    .join(", ");
  console.log(
    `${name}: ${rule.type}${schema.required.includes(name) ? " (required)" : ""} - ${limits}`,
  );
}
```

Expected output:

```text
summary: string (required) - at least 5 characters, at most 200 characters
priority: string (required) - one of low/high
```

Both arguments break the schema: `"Hi"` is too short, and `"urgent"` is not in the list. The server refuses with a tool error that names both problems, so the model can fix them.

A tool can have a good name, a clear description, and a strict schema, but still be difficult for a model to use if it tries to do too many different jobs. When a tool has one clear responsibility, the model is more likely to choose the right tool and provide the correct arguments. This design approach is called atomicity.

## Atomic Tools

Atomic tools are designed to do one clear task and do it well. They make it easier for models to choose the right action, provide the correct arguments, and avoid unexpected behavior.

### One Verb, One Job

An **atomic** tool does one small thing completely. Instead of trying to handle many different actions, it focuses on a single responsibility. Compare:

| Not atomic | Atomic |
| --- | --- |
| `manage_order(action, data)` | `lookup_order(order_id)`, `cancel_order(order_id)` |
| `do_anything(sql)` | `find_orders_by_email(email)` |

Atomic tools are easier for the model to choose, easier to check, and easier to approve. A host can let `lookup_order` run freely and still ask before `cancel_order`. With one big `manage_order` tool, it would have to ask every time.

### Not Too Small Either

Being atomic does not mean creating a separate tool for every tiny action. The goal is clarity, not unnecessary complexity.

Don't split so far that the model needs ten calls for one simple task. If users always need the item, status and date together, one `lookup_order` that returns all three is better than three tools.

## Side Effects and Annotations

A name, description, and schema help the model choose and use a tool. The host also needs to understand the risks of running it.

That's where annotations help. They tell the host whether a tool only reads data, changes data, may be destructive, or reaches outside systems such as the web or email.

### The Four Hints

The protocol defines four annotation hints, each with a default if it is missing:

| Hint | Meaning | Default if missing |
| --- | --- | --- |
| `readOnlyHint` | Only reads, changes nothing | `false` |
| `destructiveHint` | May delete or overwrite things | `true` |
| `idempotentHint` | Calling twice with the same arguments has no extra effect | `false` |
| `openWorldHint` | Reaches outside systems, like the web or email | `true` |

The defaults assume the worst. A tool with no hints is treated as one that may change things, destroy data, and reach the outside world.

```js runnable title="Judge risk from annotations"
// Annotation defaults from the spec: a tool with no hints is treated as the riskiest kind.
const DEFAULTS = {
  readOnlyHint: false,
  destructiveHint: true,
  idempotentHint: false,
  openWorldHint: true,
};

function risk(tool) {
  const a = { ...DEFAULTS, ...tool.annotations };
  if (a.readOnlyHint) {
    return "low: only reads";
  }
  if (a.destructiveHint) {
    return "high: may delete or overwrite";
  }
  return a.idempotentHint
    ? "medium: changes data, safe to repeat"
    : "medium: changes data, don't repeat";
}

const tools = [
  { name: "lookup_order", annotations: { readOnlyHint: true } },
  {
    name: "create_ticket",
    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
  },
  {
    name: "set_address",
    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
  },
  { name: "cancel_order", annotations: { readOnlyHint: false, destructiveHint: true } },
  { name: "mystery_tool" },
];
for (const t of tools) {
  console.log(t.name.padEnd(14), risk(t));
}
```

Expected output:

```text
lookup_order   low: only reads
create_ticket  medium: changes data, don't repeat
set_address    medium: changes data, safe to repeat
cancel_order   high: may delete or overwrite
mystery_tool   high: may delete or overwrite
```

Hosts use these hints to decide when to ask the user. They are **hints**, not promises: a careless or bad server can label a dangerous tool as read-only. Trust them only from servers you trust.

## Common mistakes

- **One giant tool with an `action` argument.** The model and the host can't tell what each call will do. Split it.
- **Vague arguments like `data` or `input`.** Name each argument for what it holds, and describe its format.
- **Free text where a list would do.** `priority: "urgent!!"` slips through a string. An `enum` stops it.
- **Missing annotations.** Without hints, careful hosts treat the tool as destructive and ask every time.
- **Instructions hidden in descriptions.** A description is for the tool's purpose. Telling the model what else to do is a security problem.

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- A tool has a **name**, **title**, **description**, **inputSchema**, optional **outputSchema** and **annotations**.
- Names: 1 to 128 characters of letters, digits, `_`, `-` and `.`. For the widest support, use `lowercase_with_underscores`.
- Descriptions say **what**, **when** and **limits**, in a few short sentences.
- Input schemas use **JSON Schema 2020-12**. Use `enum`, lengths, ranges and patterns.
- Make tools **atomic**: one verb, one job.
- Annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) are hints, and missing hints mean "risky".

Next: **Resources**, data the app can read without changing anything.
