Waarom tool-definitie zo belangrijk is
Het AI-model beslist zelf welke tool het aanroept op basis van de naam en beschrijving die je opgeeft. Een vage beschrijving leidt tot onjuist gebruik of helemaal geen gebruik. Een te brede beschrijving leidt juist tot overmatig tool-gebruik. Goede tool-definities zijn daarmee net zo belangrijk als de implementatie zelf.
Het model leest je beschrijvingen
Het model ziet tool-namen, beschrijvingen en parameter-beschrijvingen als onderdeel van zijn context. Schrijf ze alsof je een collega instrueert, niet alsof je code documenteert.
Basis tool-registratie
In de actuele MCP TypeScript SDK registreer je een tool met server.registerTool(naam, metadata, handler). De oudere server.tool(...)-vorm bestaat nog voor compatibiliteit, maar is gemarkeerd als deprecated. Gebruik daarom registerTool.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "tools-demo", version: "1.0.0" });
server.registerTool(
"search_knowledge_base",
{
title: "Kennisbank doorzoeken",
description:
"Doorzoek de interne kennisbank op een trefwoord of vraag. Gebruik dit als de gebruiker informatie nodig heeft over interne processen, producten of beleid.",
inputSchema: z.object({
query: z.string().min(1).describe("De zoekterm of vraag om op te zoeken"),
max_results: z
.number()
.int()
.min(1)
.max(20)
.default(5)
.describe("Maximaal aantal resultaten (standaard 5)"),
category: z
.enum(["beleid", "technisch", "product", "alle"])
.default("alle")
.describe("Filter op categorie"),
}),
annotations: { readOnlyHint: true },
},
async ({ query, max_results, category }) => {
const results = await searchKB(query, max_results, category);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);
Het tweede argument is een metadata-object: title is de leesbare naam voor clients, description stuurt het model aan, inputSchema is een volledig Zod-object (niet langer een losse shape) en annotations geeft optionele gedragshints.
Beschrijvingen die werken
Een goede tool-beschrijving beantwoordt drie vragen voor het model:
- Wat doet de tool?
- Wanneer moet je hem gebruiken?
- Wanneer moet je hem juist NIET gebruiken?
Vergelijk een zwakke en een sterke beschrijving voor dezelfde tool:
| Beschrijving | Waarom | |
|---|---|---|
| Zwak | "search_docs" met "Zoek in documenten" |
Te vaag. Het model weet niet wanneer dit beter is dan een andere zoektool. |
| Sterk | "search_internal_docs" met "Doorzoek de interne documentatie en beleidshandboeken. Gebruik dit voor vragen over interne processen, HR-beleid en compliance. Gebruik dit NIET voor publiek beschikbare informatie." |
Duidelijk doel, expliciet wanneer wel en wanneer niet. |
Beschrijf ook elke parameter
Voeg aan elk veld een .describe() toe. Die tekst belandt in het JSON Schema dat het model ziet en bepaalt hoe goed het de juiste waarden invult.
Parameter-schema's met Zod
Zod geeft je type-safe parameter-validatie en automatische JSON Schema-generatie. De SDK ondersteunt Zod v4 en blijft compatibel met Zod v3.25 en hoger.
const CreateEventSchema = z.object({
title: z.string().min(1).max(100).describe("Titel van het event"),
start_time: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
.describe("Starttijd in ISO 8601 formaat: 2026-01-15T14:00:00"),
duration_minutes: z
.number()
.int()
.min(15)
.max(480)
.describe("Duur in minuten (15-480)"),
attendees: z
.array(z.string().email())
.max(50)
.describe("E-mailadressen van deelnemers"),
online: z
.boolean()
.default(true)
.describe("True voor een Google Meet-link, false voor fysiek"),
});
server.registerTool(
"create_calendar_event",
{
title: "Agenda-item aanmaken",
description:
"Maak een nieuw agenda-item aan in Google Calendar van de gebruiker.",
inputSchema: CreateEventSchema,
annotations: { readOnlyHint: false },
},
async (params) => {
const event = await createEvent(params);
return {
content: [{ type: "text", text: `Event aangemaakt: ${event.htmlLink}` }],
};
}
);
De SDK valideert de invoer van het model automatisch tegen je inputSchema. Stuurt het model een ongeldige waarde, dan wordt de aanroep afgewezen voordat je handler draait.
Return-types
Een tool-handler retourneert een object met een content-array. Elk item heeft een type: text, image of resource.
return {
content: [
{ type: "text", text: "Tekstuele inhoud" },
{
type: "image",
data: base64String,
mimeType: "image/png",
},
{
type: "resource",
resource: {
uri: "file:///pad/naar/bestand.pdf",
text: "Inhoud van het bestand",
mimeType: "application/pdf",
},
},
],
};
Je kunt meerdere content-items combineren in een enkele return. Handig als je een afbeelding combineert met een tekstuele toelichting.
Gestructureerde output met outputSchema
Wil je naast vrije tekst ook machinaal leesbare data teruggeven, definieer dan een outputSchema en vul structuredContent. Clients kunnen die data dan betrouwbaar parsen.
server.registerTool(
"calculate_bmi",
{
title: "BMI berekenen",
description: "Bereken de Body Mass Index uit gewicht en lengte.",
inputSchema: z.object({
weight_kg: z.number(),
height_m: z.number(),
}),
outputSchema: z.object({ bmi: z.number() }),
},
async ({ weight_kg, height_m }) => {
const output = { bmi: weight_kg / (height_m * height_m) };
return {
content: [{ type: "text", text: JSON.stringify(output) }],
structuredContent: output,
};
}
);
Foutafhandeling
Voor verwachte fouten (zoals een mislukte HTTP-call of een niet gevonden record) retourneer je een normaal resultaat met isError: true. Het model ziet de foutmelding dan als tool-uitvoer en kan erop reageren.
const res = await fetch(url);
if (!res.ok) {
return {
content: [{ type: "text", text: `HTTP ${res.status}: ${res.statusText}` }],
isError: true,
};
}
Voor protocolfouten (zoals een ongeldige aanroep) gooi je een McpError met een specifieke foutcode. Gooi geen kale Error met technische details: dat lekt implementatiedetails en geeft de client een generieke melding.
Tool-annotaties
Annotaties geven clients extra context over het gedrag van een tool. Er zijn vier hints, en de namen eindigen allemaal op Hint:
readOnlyHint: de tool wijzigt niets, alleen lezen of zoeken.destructiveHint: de tool kan onomkeerbare wijzigingen maken, zoals verwijderen.idempotentHint: dezelfde invoer levert hetzelfde resultaat, herhalen is veilig.openWorldHint: de tool praat met externe systemen buiten de controle van de server.
server.registerTool(
"delete_file",
{
title: "Bestand verwijderen",
description: "Verwijder een bestand permanent.",
inputSchema: z.object({ path: z.string() }),
annotations: {
readOnlyHint: false,
destructiveHint: true,
},
},
async ({ path }) => {
await fs.unlink(path);
return { content: [{ type: "text", text: `Bestand ${path} verwijderd` }] };
}
);
Defaults zijn streng
Laat je annotaties weg, dan gaan hosts ervan uit dat een tool destructief is en met de buitenwereld praat (destructiveHint en openWorldHint standaard waar). Dat levert onnodige bevestigingsdialogen op. Zet readOnlyHint: true op je zoek- en lees-tools om die frictie weg te nemen.
Tools dynamisch beheren
registerTool geeft een handle terug waarmee je een tool op runtime kunt aan- en uitzetten, bijwerken of verwijderen. Dat vervangt het oudere patroon waarbij je zelf een ListToolsRequest-handler registreerde. Bij elke wijziging brengt de server verbonden clients automatisch op de hoogte.
const sendChat = server.registerTool(
"send_chat",
{
title: "Bericht versturen",
description: "Stuur een chatbericht namens de ingelogde gebruiker.",
inputSchema: z.object({ to: z.string(), text: z.string() }),
},
async ({ to, text }) => sendMessage(to, text)
);
sendChat.disable();
server.registerTool(
"authenticate",
{
title: "Inloggen",
description: "Log in om beveiligde tools te ontgrendelen.",
inputSchema: z.object({ token: z.string() }),
},
async ({ token }) => {
await login(token);
sendChat.enable();
return { content: [{ type: "text", text: "Ingelogd. Je kunt nu berichten sturen." }] };
}
);
De handle heeft ook update() om de definitie te wijzigen en remove() om de tool helemaal te verwijderen. Let op: capabilities (zoals tools) moeten al bij het aanmaken van de server zijn aangekondigd, anders kun je na het verbinden geen tools meer toevoegen.
Veelgemaakte fouten
| Fout | Gevolg | Oplossing |
|---|---|---|
Nog server.tool() gebruiken |
Werkt, maar deprecated | Stap over op server.registerTool() |
| Beschrijving te kort | Model gebruikt tool niet of verkeerd | Schrijf 2-3 zinnen met context en een NIET-gebruiken-regel |
| Geen parameter-beschrijvingen | Model geeft verkeerde waarden | Voeg .describe() toe aan elk Zod-veld |
destructive als annotatie |
Wordt genegeerd, geen waarschuwing | Gebruik destructiveHint (en de andere *Hint-velden) |
Handler gooit kale Error |
Client krijgt generieke foutmelding | Retourneer isError: true of gooi een McpError |
| Te veel parameters in één tool | Model mist vereiste velden | Houd tools gericht, splits op in meerdere kleine tools |
Moet ik nog server.tool() gebruiken of registerTool?
Gebruik registerTool. De server.tool()-vorm is in de TypeScript SDK gemarkeerd als deprecated. registerTool heeft een duidelijker metadata-object en geeft een handle terug waarmee je de tool dynamisch kunt beheren.
Wat is het verschil tussen inputSchema als shape en als Zod-object?
In de actuele SDK geef je een compleet Zod-object door, bijvoorbeeld z.object({ ... }). Dat maakt het mogelijk om ook unions en intersections te gebruiken. De oude vorm met een losse shape hoort bij de verouderde server.tool()-API.
Welke Zod-versie heb ik nodig?
De SDK werkt met Zod v4 en blijft compatibel met Zod v3.25 en hoger. Controleer je package.json en pin de versie zodat parameter-validatie zich voorspelbaar gedraagt.
Hoeveel tools mag een server aanbieden?
Technisch onbeperkt. Praktisch verwateren te veel tools de aandacht van het model. Boven de twintig tools loont het om te groeperen of om tools dynamisch te filteren met enable() en disable() op basis van context of rechten.
Kan een tool andere tools aanroepen?
Nee, een tool-handler draait server-side en heeft geen toegang tot de MCP-client. Wil je tool-chaining, dan implementeer je dat op client-niveau of laat je het model de tools achter elkaar aanroepen.
Hoe ga ik om met versiebeheer van tools?
Verander bestaande tool-namen niet, want dat breekt clients. Voeg optionele parameters toe voor uitbreidingen en gebruik update() om een definitie aan te passen. Wil je iets uitfaseren, vermeld dat dan in de beschrijving en schakel de tool eventueel uit met disable().