An MCP Server for Your Service|INEMA.CLUB-PRO

Lesson 3 · 20 min

Designing a good tool: name, description, JSON Schema, idempotency, read-only × with consequences

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
A dark workbench with precision tools lined up, each with a blank label, one with a red lock.

In this lesson you will

  1. 1One intent per tool, with a name and description that state it
  2. 2An input schema that rejects the wrong argument before your code runs
  3. 3The four annotations, and the default when you say nothing
  4. 4Text and structured results; an error the model can read

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.

01What the model and the client read

Click each part of a tool's registration.

Click each block in the diagram to see what it does.

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

02Annotations: what saying nothing means

The specification's defaults apply when you omit the annotation.

1. readOnlyHint

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.

03The two tools side by side

Excerpt from kit/servidor.mjs. Note the description that warns about the consequence, and the different annotations.

Excerpt · buscar_turmas
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 }) => { /* … */ }
);
Excerpt · reservar_vaga
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 … */ }
);

04Check the design with a script

Lists the tools, checks the description and annotation, calls with a good argument and with a bad one.

Terminal · check the tools
cd ~/projetos/formacao-webmcp-mcp-v72/kit
node teste-ferramentas.mjs; echo "saída: $?"
File · kit/teste-ferramentas.mjs
// 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;
Example from the repository
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.

05Practice

Rehearsal and the first tool for your service.

E6 · Check the kit

5 min

Run the check the tools block.

Done when: node teste-ferramentas.mjs → 0 falhas and saída: 0.

E7 · Make it fail on purpose

5 min

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.

E8 · A tool for your service

20 min

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.

06Lesson quiz

Question 1 of 4

You registered a tool that deletes records and forgot the annotations. How should the client treat it?skill

07In three sentences

IntentOne per tool; a name with a verb; a description that warns about the consequence.
SchemaLimits in Zod: the SDK rejects before your code runs.
AnnotationsState all four; omitting = destructive to the client.

Going further

Previous lessonNext lesson