# MCP-server aanmaken [[TOC]] ## Wat je gaat bouwen In dit artikel bouw je een werkende MCP-server met de officiele TypeScript SDK. De server geeft toegang tot een eenvoudig ticketsysteem in het geheugen: je kunt tickets opvragen, aanmaken en een specifiek ticket ophalen. Onderweg leer je het verschil tussen tools en resources, hoe je fouten netjes teruggeeft en hoe je de server koppelt aan Claude Desktop. MCP (Model Context Protocol) is de open standaard waarmee AI-clients zoals Claude Desktop, Gemini en agent-frameworks op een uniforme manier praten met externe tools en data. Eenmaal gebouwd werkt je server met elke MCP-compatibele client, niet alleen met Claude. ## Voorbereiding Zorg dat je beschikt over Node.js 18 of hoger (Node 16 is het officiele minimum, maar 18+ wordt aangeraden) en basiskennis van TypeScript. ```bash mkdir mcp-ticketserver cd mcp-ticketserver npm init -y npm install @modelcontextprotocol/sdk zod npm install --save-dev typescript @types/node tsx ``` Maak een `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "strict": true }, "include": ["src/**/*"] } ``` :::info title="Gebruik de juiste SDK-versie" Dit artikel gebruikt `@modelcontextprotocol/sdk` versie 1.x (1.29 ten tijde van schrijven, juni 2026). De API veranderde flink tussen 0.x en 1.x. Belangrijk: vanaf 1.x zijn `server.tool()` en `server.resource()` vervangen door `server.registerTool()` en `server.registerResource()`. Oude voorbeelden met `.tool()` werken niet meer op recente versies. ::: ## De serverstructuur opzetten Maak `src/index.ts`: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "ticket-server", version: "1.0.0", }); ``` De `McpServer`-klasse beheert de protocol-communicatie. Je registreert tools en resources op de instantie, en de server leidt automatisch af welke capabilities hij aanbiedt. Pas daarna verbind je een transport. ## Tools registreren Een tool heeft een naam, een configuratie-object met een `description` en een `inputSchema`, en een handler-functie. Het `inputSchema` is een object van Zod-velden die de parameters beschrijven: ```typescript const tickets = new Map(); server.registerTool( "list_tickets", { description: "Geef alle openstaande tickets terug", inputSchema: {}, }, async () => { const list = Array.from(tickets.values()); return { content: [ { type: "text", text: JSON.stringify(list, null, 2), }, ], }; } ); server.registerTool( "create_ticket", { description: "Maak een nieuw support-ticket aan", inputSchema: { title: z.string().describe("Korte omschrijving van het probleem"), priority: z .enum(["low", "medium", "high"]) .describe("Urgentie van het ticket"), }, }, async ({ title, priority }) => { const id = `TICKET-${Date.now()}`; tickets.set(id, { id, title, status: `open-${priority}` }); return { content: [ { type: "text", text: `Ticket aangemaakt: ${id}`, }, ], }; } ); ``` De handler retourneert altijd een object met een `content`-array. Elk item heeft een `type` (`text`, `image` of `resource`) en bijbehorende data. Beschrijf je velden goed met `.describe()`: dat is precies wat het AI-model gebruikt om te bepalen wanneer en hoe het de tool aanroept. :::tip title="Beschrijvingen zijn de interface naar het model" De `description` van een tool en de `.describe()` op elk veld vormen samen het contract met het model. Schrijf ze alsof je een collega uitlegt wat de tool doet en wanneer hij hem moet gebruiken. Vage beschrijvingen leiden tot tools die op het verkeerde moment of met verkeerde parameters worden aangeroepen. ::: ## Resources registreren Resources zijn read-only data die het model in zijn context kan laden. Waar een tool een actie uitvoert, levert een resource alleen gegevens. Gebruik `server.registerResource()` met een naam, een vaste URI, metadata en een handler: ```typescript server.registerResource( "open-tickets", "tickets://open", { title: "Open tickets", description: "Alle open tickets als gestructureerde data", mimeType: "application/json", }, async (uri) => { const open = Array.from(tickets.values()).filter((t) => t.status.startsWith("open") ); return { contents: [ { uri: uri.href, text: JSON.stringify(open, null, 2), mimeType: "application/json", }, ], }; } ); ``` Een resource-handler retourneert een `contents`-array (let op het meervoud), waarin elk item minstens een `uri` en `text` bevat. ## Foutafhandeling Gooi een `McpError` met de juiste error-code voor verwachte fouten. Zo krijgt de client een nette, gestructureerde foutmelding in plaats van een crash: ```typescript import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; server.registerTool( "get_ticket", { description: "Haal een specifiek ticket op", inputSchema: { id: z.string().describe("Het ID van het ticket, bijvoorbeeld TICKET-123"), }, }, async ({ id }) => { const ticket = tickets.get(id); if (!ticket) { throw new McpError( ErrorCode.InvalidParams, `Ticket ${id} bestaat niet` ); } return { content: [{ type: "text", text: JSON.stringify(ticket) }], }; } ); ``` Gebruik `ErrorCode.InvalidParams` voor ongeldige invoer, `ErrorCode.InternalError` voor onverwachte fouten en `ErrorCode.MethodNotFound` als iets niet bestaat. Voor fouten die het model zelf moet kunnen herstellen kun je in plaats van een `McpError` ook een gewoon resultaat met `isError: true` teruggeven, zodat het model de melding leest en het opnieuw kan proberen. ## De server starten ```typescript async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Ticket MCP-server gestart"); } main().catch((err) => { console.error("Fatale fout:", err); process.exit(1); }); ``` :::warn title="Gebruik console.error, nooit console.log" Bij stdio-transport is stdout volledig gereserveerd voor het MCP-protocol (JSON-RPC-berichten). Eén enkele `console.log` mengt zich tussen die berichten en breekt de hele verbinding. Log daarom altijd naar stderr via `console.error`. ::: ## Testen Voeg een script toe aan `package.json`: ```json { "scripts": { "start": "tsx src/index.ts" } } ``` Test de server met de MCP Inspector, een grafische tool waarmee je elke tool en resource handmatig kunt aanroepen zonder een AI-client: ```bash npx @modelcontextprotocol/inspector npm start ``` De Inspector opent een web-UI waar je alle geregistreerde tools en resources ziet en direct kunt uitproberen. Dit is de snelste manier om je server te valideren voordat je hem aan een client koppelt. ## Verbinden met Claude Desktop Bouw eerst naar JavaScript en verwijs naar het gecompileerde bestand. Voeg toe aan `claude_desktop_config.json` (op macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`, op Windows: `%AppData%\Claude\claude_desktop_config.json`): ```json { "mcpServers": { "tickets": { "command": "node", "args": ["/absoluut/pad/naar/mcp-ticketserver/dist/index.js"] } } } ``` Herstart Claude Desktop volledig na het aanpassen van de configuratie. De tools verschijnen dan onder het tools-icoon in het gespreksvenster. :::warn title="Altijd een absoluut pad gebruiken" Gebruik altijd een absoluut pad in de configuratie. Relatieve paden werken niet betrouwbaar, omdat de working directory waaruit Claude Desktop het commando start per platform verschilt en niet je projectmap is. ::: :::faq ### Moet ik nog `server.tool()` of `server.registerTool()` gebruiken? Gebruik `server.registerTool()`. De oudere methodes `server.tool()`, `server.resource()` en `server.prompt()` zijn deprecated en in recente 1.x-versies van de SDK verwijderd. De nieuwe methodes nemen een configuratie-object met `description` en `inputSchema`. ### Kan ik de server ook in Python schrijven? Ja, gebruik de officiele `mcp` Python-package. De structuur is vergelijkbaar: je maakt een serverobject, registreert tools en resources via decorators, en gebruikt `stdio_server()` als transport. ### Hoe debug ik fouten in het MCP-protocol? Begin met de MCP Inspector, die elk JSON-RPC-bericht toont. In Claude Desktop vind je de serverlogs op macOS in de map `~/Library/Logs/Claude` en op Windows onder `%AppData%\Claude\logs`. Vergeet niet dat al je eigen logregels naar stderr moeten gaan. ### Kan mijn server state bijhouden? Ja, zoals in het voorbeeld met de `tickets`-Map. Houd er wel rekening mee dat bij stdio-transport elke clientverbinding een nieuw serverproces start, dus de state leeft alleen zolang dat proces draait. Wil je data behouden tussen sessies, schrijf dan naar een database of bestand. ### Hoe voeg ik authenticatie toe? Bij stdio-transport draait de server lokaal als hetzelfde proces dat de client start, dus je leunt op de rechten van die gebruiker en op secrets in omgevingsvariabelen. Bied je de server aan over het netwerk via HTTP-transport (Streamable HTTP), dan implementeer je OAuth 2.0 of een Bearer-tokencontrole op elk verzoek. ### Werkt deze server ook met andere clients dan Claude? Ja. MCP is een open standaard, dus dezelfde server werkt met elke MCP-compatibele client, waaronder andere AI-assistenten en agent-frameworks. Je hoeft niets aan de server te veranderen, alleen de manier waarop de betreffende client het startcommando registreert verschilt. :::