Skip to main content

Tools - Defining and Executing Tools

This guide explains the specification for Tools, the core feature of MCP: definitions, execution, error handling, and metadata. Design guidance for tools (granularity, naming, how to write descriptions) is covered in Tool Design Patterns and Anti-patterns; this guide sticks to the formats and behaviors the protocol defines. For where server features fit overall, see Introduction to MCP - What Is the Model Context Protocol. For how a server asks the user for input mid-operation, see Elicitation and Multi Round-Trip Requests.

1. What This Document Covers​

  • tools/list and tools/call
  • The parts of a tool definition
  • Tool name format
  • JSON Schema 2020-12
  • Kinds of results
  • The two layers of errors
  • Annotations
  • Human confirmation
  • list_changed and dynamic tools

tools/list and tools/call​

Tools are a model-controlled feature. The server publishes a list of tools, and the model picks which to call based on the user's request and the context. The protocol does not force a particular user interface, but it does require that humans keep the ability to say no (described below).

A server that exposes tools declares the tools capability. listChanged indicates whether the server sends notifications when the tool list changes.

{
"capabilities": {
"tools": {
"listChanged": true
}
}
}

The client retrieves the available tools with tools/list and runs them with tools/call.

tools/list follows these rules.

  • Pagination: it supports cursor-based pagination with cursor and nextCursor. The server decides the page size (details in Resources, Prompts, and Common Utilities)
  • Caching: the result MUST include ttlMs (milliseconds) and cacheScope (public / private). During that time the client can use the list without fetching it again
  • Deterministic order: servers SHOULD return the tools in the same order as long as the underlying set has not changed. This makes the list easier for clients to cache, and improves prompt cache hits when tool definitions are placed in the model context
  • Do not vary per connection: the list must not change because of the connection or as a side effect of other requests. The only thing allowed to change it is the authorization attached to the request (for example, the granted scopes). Because the 2026-07-28 version has no sessions, credentials are treated as per-request input, not connection state
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"tools": [
{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
}
}
],
"nextCursor": "next-page-cursor",
"ttlMs": 300000,
"cacheScope": "public"
}
}

tools/call sends a name and arguments and receives a result. The result carries resultType: "complete". Only when user input becomes necessary mid-operation may the server return resultType: "input_required" instead of a result (Multi Round-Trip Requests; see Elicitation and Multi Round-Trip Requests). In that case the JSON-RPC id of the retried request must differ from the first request.

The parts of a tool definition​

A tool definition consists of the following fields.

FieldRequiredContent
nameYesUnique identifier of the tool
titleNoHuman-readable name for display
descriptionNoDescription of the functionality. The model's main clue for choosing the tool
iconsNoArray of icons to show in the UI
inputSchemaYesJSON Schema that defines the arguments. type: "object" is required
outputSchemaNoJSON Schema that defines the structured result
annotationsNoHints about the tool's behavior
_metaNoMetadata used by extensions and the like

For a tool with no arguments, the recommended inputSchema is { "type": "object", "additionalProperties": false }, because it states explicitly that only an empty object is accepted. { "type": "object" } accepts any object. null is not allowed.

name is an identifier for programs, and title is the name shown on screen. description is text for the model and goes into its context. Write what the tool does, when to use it, and when not to.

Properties in inputSchema can carry an extension key called x-mcp-header. Over Streamable HTTP it makes the value of that argument be mirrored into an Mcp-Param-{name} HTTP header, so that load balancers and WAFs can route requests without parsing the body. Headers are visible to the network devices in between, so do not put it on arguments that hold passwords, API keys, tokens, or personal information. See Transports - stdio and Streamable HTTP for details.

Tool name format​

The following recommendations apply to tool names (the current specification, which incorporates SEP-986).

  • Length of 1 to 128 characters
  • Case-sensitive
  • Allowed characters: ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.)
  • No spaces, commas, or other special characters
  • Unique within a server

Examples of valid names are getUser, DATA_EXPORT_v2, and admin.tools.list. Dots can express a namespace.

Uniqueness is guaranteed only within a single server. A client or proxy that aggregates tools from multiple servers will run into collisions, for example when two servers each expose search. In that case, disambiguate by prefixing a server identifier or similar. The name in serverInfo is not necessarily unique across servers, so do not rely on it for disambiguation.

note

The initial SEP-986 proposal allowed the slash (/) and set a 64-character limit. This guide follows the current 2026-07-28 Tools page (128 characters, no /).

JSON Schema 2020-12​

inputSchema and outputSchema are treated as JSON Schema 2020-12 when $schema is absent (SEP-2106). To use another dialect, state it with $schema.

  • inputSchema: the root must be type: "object", but you can use 2020-12 keywords such as anyOf, oneOf, allOf, not, if / then / else, $ref, and $defs
  • outputSchema: not limited to objects; any JSON Schema, including arrays and primitives, is allowed

For example, a tool that accepts "either an ID or a name" can express it with oneOf.

{
"type": "object",
"oneOf": [
{ "properties": { "id": { "type": "string" } }, "required": ["id"] },
{ "properties": { "name": { "type": "string" } }, "required": ["name"] }
]
}

The added expressiveness demands defenses from whoever processes the schema. The specification and SEP-2106 require the following.

  • Do not fetch $ref over the network on your own: references other than in-document ones such as #/$defs/Foo (external URIs) must not be resolved automatically as part of normal validation. A malicious tool definition could otherwise be used for server-side request forgery (SSRF) or fetch amplification. Reject a schema that contains an unresolvable external reference instead of treating it permissively
  • Limits on composition keywords: combinations of anyOf, oneOf, allOf, and $defs can blow up validation cost. Set limits on schema depth, total number of subschemas, and time per validation

Kinds of results​

A tool result can carry unstructured content (content) and structured content (structuredContent).

content is an array of items of the following kinds.

typeContent
textText
imageBase64 image data and a mimeType
audioBase64 audio data and a mimeType
resource_linkA link to a resource (uri, name, etc.). The client can use it to fetch or subscribe
resourceThe contents of a resource embedded in the result

A resource that a resource_link points to does not necessarily appear in the result of resources/list. Every item can carry annotations for audience (audience), priority (priority), and last modified time (lastModified).

structuredContent is any JSON value (object, array, string, number, boolean, or null). If an outputSchema exists, the server MUST return a result that conforms to it, and clients SHOULD validate it. For backward compatibility, a tool that returns structured results SHOULD also put the same JSON, serialized as a string, into a text item.

{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "complete",
"content": [
{ "type": "text", "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\", \"humidity\": 65}" }
],
"structuredContent": { "temperature": 22.5, "conditions": "Partly cloudy", "humidity": 65 }
}
}

structuredContent has nothing to do with the LLM "structured outputs" (schema-constrained model generation); it is result data produced by the server.

The two layers of errors​

Errors are returned in two layers. See "Kinds of errors and how to return them" below for details.

Annotations​

annotations are hints about the tool's behavior.

FieldDefaultMeaning
titleNoneHuman-readable title
readOnlyHintfalseIf true, the tool does not modify its environment
destructiveHinttrueIf false, the update is additive only (not destructive). Meaningful only when readOnlyHint is false
idempotentHintfalseIf true, repeating the call with the same arguments has no additional effect. Meaningful only when readOnlyHint is false
openWorldHinttrueIf true, the tool interacts with external, open-ended entities. If false, it deals only with a closed domain

Note that the defaults lean toward "writes, is destructive, interacts with the outside world." Unless a hint is set, treating the tool as dangerous is the safe default for a client.

The specification states that clients MUST consider annotations untrusted unless they come from trusted servers. Annotations are merely the server's self-report. The danger of spoofing, such as putting readOnlyHint: true on a tool that writes, is covered in AI Agent-Specific Threats - Prompt Injection and Tool Poisoning. Use annotations as reference values for how to present the UI and the wording of confirmation dialogs, not as grounds for skipping confirmation.

Human confirmation​

For trust and safety, the specification says that there SHOULD always be a human in the loop with the ability to deny tool invocations. Applications are recommended to:

  • Make clear in the UI which tools are exposed to the model
  • Show a clear indicator when a tool is invoked
  • Ask for confirmation of operations, to ensure a human is in the loop

Before sensitive operations the client asks for confirmation, and it shows the tool input to the user before sending it to the server, to avoid unintended or malicious data exfiltration. On the server side, validating inputs, access control, rate limiting of invocations, and sanitizing outputs are mandatory. Destructive operations (delete, send, deploy, pay) should in principle always go through user confirmation, regardless of the annotation values.

list_changed and dynamic tools​

The set of tools may change over time. A server that declared listChanged: true SHOULD announce changes with notifications/tools/list_changed. The 2026-07-28 version consolidated how notifications are received into subscriptions/listen: the server sends that notification to a client that opened subscriptions/listen with toolsListChanged: true.

A client that receives the notification treats its cached list as stale immediately and fetches tools/list again. The notification can be combined with ttlMs: the TTL avoids unnecessary refetches, and the notification invalidates immediately.

Because the protocol has no sessions, a server cannot show "this tool only to this connection." Stateful tools (a shopping basket, an open browser context, a database transaction) work by having a creation tool return an explicit handle, which later calls take as an argument. The specification's non-normative guidance lists the following points.

  • Authorization: on an authenticated server, a handle is a name, not a capability. On every call, verify that the caller may use that handle. On an unauthenticated server the handle plays the role of a bearer token, so generate it with sufficient entropy and give it a lifetime
  • Opacity: a handle that embeds internal structure invites parsing and guessing
  • Lifetime: state the retention period (for example, "baskets expire after 24 hours of inactivity") in the description of the creation tool
  • Expiry errors: for a call with an expired or unknown handle, return a tool execution error that says so, so the model can recover by creating a new one

2. Kinds of Errors and How to Return Them​

KindExamplesHow to return
Protocol errorUnknown method, malformed JSON-RPC requestA JSON-RPC error object
Tool execution errorAPI call failure, business rule violation, input validation errorisError: true inside a successful response. Include an explanation the LLM can use to self-correct
Authorization errorMissing token, insufficient scopeHTTP 401 / 403 with WWW-Authenticate

A protocol error is a problem with the structure of the request that the model cannot fix. An unknown tool name, a malformed request that does not satisfy the CallToolRequest schema, and an internal server error fall here, and are returned as JSON-RPC errors. For an unknown tool, use -32602 (Invalid params).

{
"jsonrpc": "2.0",
"id": 3,
"error": { "code": -32602, "message": "Unknown tool: invalid_tool_name" }
}

A tool execution error is a runtime failure the model can read, self-correct from, and retry. Return it inside the result with isError: true.

{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": [
{ "type": "text", "text": "Invalid departure date: must be in the future. Current date is 08/08/2025." }
],
"isError": true
}
}

Input validation errors (a wrongly formatted date, an out-of-range value) are returned as tool execution errors, not protocol errors (SEP-1303), so that the model can read the reason and fix the arguments. Clients SHOULD pass tool execution errors to the model, and MAY pass protocol errors, because the model is unlikely to recover from them.

Authorization errors are returned at the HTTP layer, not in JSON-RPC. See MCP Authorization - OAuth 2.1 and Metadata Discovery for details.

3. Common Pitfalls for Developers​

3.1 Returning input validation errors as JSON-RPC errors​

Many implementations return -32602 when argument validation fails. In that case the content of the error does not reach the model, and it tends to repeat the same mistake. Return a wrong value with isError: true, saying what is wrong, why, and how to fix it. Use -32602 for problems the model cannot fix, such as an unknown tool name.

3.2 Declaring outputSchema and returning results that do not conform​

Once you declare an outputSchema, the server must always return a conforming structuredContent. Some implementations fail validation by returning content that does not match the schema in the error case (isError: true). Fields with the wrong type or missing fields become validation errors in the client. Automatic validation in the SDK helps you notice early in tests.

3.3 Returning only structuredContent and leaving content empty​

Older clients may not handle a structuredContent that is an array or a primitive. Also, what is passed to the model is usually content. When returning a structured result, put the same JSON, serialized, into text as well.

3.4 Varying the tool list per connection or state​

The 2026-07-28 version has no per-connection sessions. A list that depends on connection state, such as "more tools appear after login," violates the specification. Varying it by the request's authorization is acceptable, but then set cacheScope to private to prevent sharing the cache across authorization contexts. A cacheScope: "public" list can be shared even between different access tokens. Also, cacheScope is no substitute for authorization of whether a tool may be called; apply access control on every call.

3.5 Trusting annotations​

Some implementations use readOnlyHint: true as grounds for automatic execution without confirmation. Annotations from anything other than a trusted server are self-reported. Also remember that the annotation defaults lean toward "destructive, interacts with the outside world," and do not assume it is fine to publish a tool without setting them.

3.6 Name collisions and character restrictions​

In a client that aggregates multiple servers, generic names such as search or create_issue collide. Give names that are conscious of the server's namespace (such as github.search_issues). Names containing spaces or Japanese characters can be rejected by constraints of clients or model providers.

3.7 Trying to keep state in a session​

The protocol has no sessions, so state between tool calls cannot be held implicitly by the connection. Return a handle explicitly, take it as an argument, and return an easily understood error on expiry.

4. Code Examples​

Defining a tool and returning an input validation error​

This is an example of defining a tool with an outputSchema using the TypeScript SDK (@modelcontextprotocol/sdk). The API changes between SDK versions. This shows the idea; check the documentation of the SDK you use for the actual calls.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "flight-server", version: "1.0.0" });

server.registerTool(
"search_flights",
{
title: "Search Flights",
description:
"Search flights by route and date. Use for questions about availability. Does not book.",
inputSchema: {
from: z.string().length(3).describe("IATA code, e.g. HND"),
to: z.string().length(3).describe("IATA code, e.g. ITM"),
date: z.string().describe("Departure date, YYYY-MM-DD"),
},
outputSchema: {
flights: z.array(
z.object({ id: z.string(), departs: z.string(), priceJpy: z.number() }),
),
},
annotations: { readOnlyHint: true, openWorldHint: true },
},
async ({ from, to, date }) => {
// Return a wrong value as a tool execution error, not a protocol error
if (new Date(date) < new Date()) {
return {
isError: true,
content: [
{
type: "text",
text: `Invalid date ${date}: must be in the future. Use YYYY-MM-DD.`,
},
],
};
}
const flights = await findFlights(from, to, date);
return {
content: [{ type: "text", text: JSON.stringify({ flights }) }],
structuredContent: { flights },
};
},
);

A read-only tool with no arguments​

A tool with no arguments states explicitly that it accepts only an empty object.

{
"name": "get_current_time",
"description": "Returns the current server time",
"inputSchema": { "type": "object", "additionalProperties": false },
"annotations": { "readOnlyHint": true, "openWorldHint": false }
}

A handle for a stateful tool​

{
"name": "create_basket",
"description": "Create a shopping basket. Baskets expire after 24 hours of inactivity.",
"inputSchema": { "type": "object", "additionalProperties": false }
}

The result returns a basket_id, which later tools take as an argument.

{
"name": "add_item",
"arguments": { "basket_id": "bsk_a1b2c3", "sku": "SKU-1001" }
}

On every call, the server verifies the owner of basket_id from the authorization information, and if it has expired, returns a tool execution error such as "Expired. Recreate it with create_basket."

5. Summary​

  • Discover with tools/list and run with tools/call. Return the list in a deterministic order with ttlMs and cacheScope, and do not vary it by connection state
  • inputSchema / outputSchema are JSON Schema 2020-12. Do not fetch external $refs automatically, and set limits on composition keywords
  • Results are content and structuredContent. Add a text item with the same JSON to a structured result
  • Errors have two layers. Return wrong input values as tool execution errors (isError: true) so that the model can fix them
  • Annotations are self-reported. Do not use those from anything other than a trusted server as grounds for skipping confirmation
  • Leave humans room to refuse before execution. Put confirmation before destructive operations
  • Announce tool list changes with subscriptions/listen and notifications/tools/list_changed. Keep state in explicit handles

References​