# Tools definiëren in MCP [[TOC]] ## 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. :::info title="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`. ```typescript 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: 1. Wat doet de tool? 2. Wanneer moet je hem gebruiken? 3. 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. | :::tip title="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. ```typescript 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`. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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` }] }; } ); ``` :::warn title="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. ```typescript 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 | :::faq ### 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()`. :::