Naar inhoud
lightbulb Welkom op de nieuwe kennisbank | We hebben de docs volledig vernieuwd met meer dan 160 features. Bekijk wat nieuw isarrow_forward

MCP-server aanmaken

Stap voor stap een eigen MCP-server bouwen met de officiele TypeScript SDK, inclusief tools, resources, foutafhandeling en koppeling aan Claude Desktop.

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.

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:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"]
}
info

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:

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:

const tickets = new Map<string, { id: string; title: string; status: string }>();

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.

lightbulb

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:

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:

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

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);
});
warning

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:

{
  "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:

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

{
  "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.

warning

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.

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.