E6 · Check the kit
5 minRun the check the tools block.
Done when: node teste-ferramentas.mjs → 0 falhas and saída: 0.
Lesson 3 · 20 min
The model picks the tool by reading its name, description, and schema. The client decides whether to ask for approval by reading the annotations. A badly designed tool gets called at the wrong time or with the wrong argument. In this lesson, you use a script to check that your tools say what they do and what they change.
Start the lesson
Goal: write the name, description, schema, and annotations that let the agent choose correctly and let the client ask for confirmation when there are consequences.
Click each part of a tool's registration.
Semantics and defaults of the annotations follow the ToolAnnotations type in the 2026-07-28 specification (Tools page of the specification; types in the official SDK repository, read on 10/19/2026).
The specification's defaults apply when you omit the annotation.
true = the tool doesn't change the environment. A host can approve a read-only tool on its own.
Without the annotation, the client should assume it changes something.
Only makes sense when the tool is not read-only. true = can delete or overwrite; false = only adds (like a new booking).
If you leave it out on a tool that changes something, the client treats it as destructive.
true = repeating the call with the same arguments has no extra effect. It matters because clients retry calls (the network dropped, MRTR in lesson 5).
reservar_vaga is not idempotent: two calls, two seats.
true = talks to the outside world (web search, third-party API). false = closed domain, like the kit's in-memory catalog.
An open-world tool is an entry point for untrusted content: course 4 covers this.
Excerpt from kit/servidor.mjs. Note the description that warns about the consequence, and the different annotations.
server.registerTool(
'buscar_turmas',
{
title: 'Buscar turmas',
description: 'Lista turmas cujo título contém o termo. Só leitura: não reserva nada.',
inputSchema: z.object({
termo: z.string().min(2).describe('Parte do título, ex.: "mcp"'),
limite: z.int().min(1).max(20).optional().describe('Máximo de turmas (padrão 5)')
}),
outputSchema: z.object({ turmas: z.array(Turma) }),
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
},
async ({ termo, limite }) => { /* … */ }
);server.registerTool(
'reservar_vaga',
{
title: 'Reservar vaga',
description: 'Reserva uma vaga na turma para o e-mail informado. Tem consequência: pede confirmação antes.',
inputSchema: z.object({
turma_id: z.string().describe('Id da turma, ex.: "mcp-01"'),
email: z.email().describe('E-mail de quem vai fazer a turma')
}),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
},
async ({ turma_id, email }, ctx) => { /* … aula 5 … */ }
);Lists the tools, checks the description and annotation, calls with a good argument and with a bad one.
cd ~/projetos/formacao-webmcp-mcp-v72/kit node teste-ferramentas.mjs; echo "saída: $?"
// teste-ferramentas.mjs — checks the tools' design: annotations, valid argument,
// invalid argument (the SDK rejects it before your code runs) and structured result.
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { createServer } from './servidor.mjs';
const handler = createMcpHandler(createServer);
const cliente = new Client({ name: 'teste-ferramentas', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await cliente.connect(new StreamableHTTPClientTransport(new URL('http://teste.local/mcp'),
{ fetch: (url, init) => handler.fetch(new Request(url, init)) }));
let falhas = 0;
const confere = (ok, msg) => { console.log(`${ok ? 'ok ' : 'FALHA'} ${msg}`); if (!ok) falhas++; };
const { tools } = await cliente.listTools();
for (const t of tools) {
confere(t.description?.length >= 20, `${t.name}: descrição com 20+ caracteres`);
confere(typeof t.annotations?.readOnlyHint === 'boolean', `${t.name}: diz se é só leitura`);
}
const ok = await cliente.callTool({ name: 'buscar_turmas', arguments: { termo: 'mcp', limite: 1 } });
confere(ok.structuredContent?.turmas?.length === 1, 'buscar_turmas devolve structuredContent com 1 turma');
const ruim = await cliente.callTool({ name: 'buscar_turmas', arguments: { termo: 'm' } });
confere(ruim.isError === true, `argumento inválido vira isError: ${ruim.content[0].text.slice(0, 70)}…`);
console.log(`${falhas} falhas`);
await cliente.close();
await handler.close();
process.exitCode = falhas ? 1 : 0;ok buscar_turmas: descrição com 20+ caracteres ok buscar_turmas: diz se é só leitura ok reservar_vaga: descrição com 20+ caracteres ok reservar_vaga: diz se é só leitura ok buscar_turmas devolve structuredContent com 1 turma ok argumento inválido vira isError: Input validation error: Invalid arguments for tool buscar_turmas: term… 0 falhas saída: 0
The termo "m" has 1 character and the schema asks for 2: the SDK rejected it and returned the message as a result with isError, which the model reads and fixes.
Rehearsal and the first tool for your service.
Run the check the tools block.
Done when: node teste-ferramentas.mjs → 0 falhas and saída: 0.
Delete the annotations line from buscar_turmas and run it again. Then put the line back.
Done when: without the line: FALHA buscar_turmas: diz se é só leitura and saída: 1; with the line back: 0 falhas.
Pick a tool from your mapa-mcp.md (lesson 1) and register it in servidor.mjs with a description, a schema with limits, and the four annotations.
Done when: node teste-ferramentas.mjs → 0 falhas with your tool listed in the ok lines.
Question 1 of 4
You registered a tool that deletes records and forgot the annotations. How should the client treat it?skill
Question 2 of 4
The model calls buscar_turmas with termo "m". What happens?skill
isError, which the model reads.isError, which the model reads.Review: The two tools side by sideQuestion 3 of 4
Why does reservar_vaga have idempotentHint false?
Question 4 of 4
Which of these is the best description for a tool?
Pick an option. The explanation appears after you try.