Tools

By the Syncfusion DevTutorials team · Last updated

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.

Each part of a tool definition has a reader: programs use the name, people the title, the model the description, both sides the schema, and the host the annotations.

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

FieldRequiredWho reads it
nameyesPrograms: the model asks for a tool by its name
titlenoPeople: a friendly label for the host's screen
descriptionno, but always write oneThe model: what the tool does and when to use it
inputSchemayesThe model and both sides' validators: the arguments
outputSchemanoPrograms: the shape of structured results (Chapter 4)
annotationsnoThe host: hints about side effects

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:

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:

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.

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:

"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 lookup_order.

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".

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:

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.

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:

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 atomicAtomic
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:

HintMeaningDefault if missing
readOnlyHintOnly reads, changes nothingfalse
destructiveHintMay delete or overwrite thingstrue
idempotentHintCalling twice with the same arguments has no extra effectfalse
openWorldHintReaches outside systems, like the web or emailtrue

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.

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:

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.