Breaking down Tools in MCP: how a tool is defined with JSON Schema, registered via tools/list, executed via tools/call, produces structured output, the meaning of tool annotations, and why annotations are not a security guarantee.

After understanding the connection lifecycle in episode 3, we now move to the most frequently used primitive: Tools. These are the "hands" that let a model perform real actions in the external world — writing files, sending emails, running database queries, calling APIs.
This episode dissects the entire lifecycle of a tool: how it's defined, how the model discovers it, how it's executed, what its output looks like, and what tool annotations really mean. By the end of this episode you'll be able to read any tool definition the way you'd read a restaurant menu.
A tool is a function the model can call to perform a specific action. Two important distinguishing properties:
Imagine the server as an office: tools are the employees ready to carry out tasks, resources are the archives you can read, and prompts are the pre-written SOPs. The model is the manager choosing which employee to assign.
Each tool is defined by three main parts: name (a unique name), description (an explanation for the model), and inputSchema (JSON Schema for the parameters). Example tool definition:
{
"tools": [
{
"name": "get_weather",
"description": "Ambil cuaca terkini untuk sebuah kota",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Nama kota, misalnya Jakarta"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city"]
}
}
]
}JSON Schema is a standard parameter description language. Servers can express data types, enums, minimum/maximum values, and other validations. The model uses the description and this schema to decide whether a tool is relevant and how to fill in correct arguments.
The model doesn't know which tools are available until the server tells it. The tools/list method returns the server's tool list:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}Hosts usually call tools/list once at startup (or periodically for caching) and store the tool definitions. The list can also contain a nextCursor for pagination — if the server has many tools, the client fetches the next page until all tools are loaded.
You can also observe the tools/list result directly without writing a manual client — just open the MCP Inspector with npx @modelcontextprotocol/inspector and look at the tools tab.
When the model decides to use a tool, the host calls tools/call with the tool name and arguments:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "Jakarta",
"unit": "celsius"
}
}
}The server executes the tool's logic and returns the result. If execution fails, the server sends a response containing isError: true along with an error message — instead of leaving the client guessing the cause of the failure.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Jakarta: 30 derajat celsius, berawan"
}
],
"isError": false
}
}The content field is an array of typed items — not just raw text. Common types:
| Type | Description |
|---|---|
text | Plain text the model reads |
image | An image (data URL) |
audio | Audio (data URL) |
resource | A reference to a specific resource |
This typed design lets the model receive results in the appropriate form — reading text, or processing an image returned by a tool — without manual JSON parsing. That's what structured output means in the 2026-07-28 specification: tool results aren't just strings, but content with explicit types.
Info
You may have heard of "tool schema & structured output" as a headline feature of the 2026-07-28 specification. In short: tool definitions use strict JSON Schema, and tool output is wrapped in a typed content structure — both of which we've seen directly in this episode.
Servers can add annotations to a tool definition — metadata that gives the model hints about the tool's nature:
{
"name": "delete_user",
"description": "Hapus akun pengguna berdasarkan ID",
"inputSchema": {
"type": "object",
"properties": {
"userId": { "type": "string" }
},
"required": ["userId"]
},
"annotations": {
"destructiveHint": true,
"readOnlyHint": false,
"idempotentHint": false
}
}Some commonly used annotations:
| Annotation | Meaning for the Model |
|---|---|
readOnlyHint | The tool doesn't change state — safe to call repeatedly |
destructiveHint | The tool may destroy data — requires confirmation |
idempotentHint | Repeated calls give the same result |
openWorldHint | The tool interacts with the outside world (network, files) |
titleHint | A short, human-friendly label for the tool |
These hints help the model make better decisions — for example, asking for confirmation before calling a destructive tool, or avoiding repeated calls to a read-only tool.
One important message you must hold onto: annotations are only hints, not security guarantees. Nothing forces a server to be honest about destructiveHint or readOnlyHint. A malicious tool could label itself readOnlyHint: true to deceive the model.
Therefore, security validation must come from other layers: an allowlist of tools that may be called, strict input validation, an execution sandbox, and authorization. Never trust annotations from a server you don't know. We'll cover this hardening comprehensively in episode 14.
In this episode 4 you've understood the entire lifecycle of a tool in MCP: definition with JSON Schema, discovery via tools/list, execution via tools/call, output as typed content, and the role of annotations as hints for the model.
Key takeaways:
name, description, inputSchema).tools/list and executes them via tools/call.text, image, audio, resource.readOnlyHint, destructiveHint, and others) help the model make decisions.In the next episode 5 we'll cover the two remaining primitives: Resources and Prompts — how a resource is read via resources/read, resource template patterns, and how reusable prompt templates work through argument binding. See you in episode 5!