Now-Next

AI-ontwikkeling

Een MCP-server op een bestaande SaaS: 17 tools in productie

Wat we leerden van een MCP-server op Pilot-Next: 14 lezende en 3 schrijvende tools, een toollijst van 11 KB, eerst een voorbeeld, en de datum die AI gokt.

Chris van Eijk · · 10 min lezen

Pilot-Next is een reserverings- en factuursysteem voor vliegclubs, en het heeft een MCP-server — de eerste code daarvoor is van december 2025. Een lid vraagt een assistent zoals Claude wat een vlucht naar Lelystad kost, of het toestel zaterdag vrij is, en laat hem daarna boeken — in hetzelfde gesprek, zonder de app te openen.

Handleidingen over MCP toevoegen aan een SaaS-product beschrijven meestal een server in het algemeen. Dit stuk beschrijft een server die draait, die wij bouwden en die we nog steeds onderhouden. Elk getal hieronder is op 24 september 2026 gemeten aan zijn eigen code.

Dit artikel gaat over het ontwerp van de tools: welke, hoe je ze beschrijft, en hoe je voorkomt dat een assistent handelt op een gok. Toegangsbeheer laten we hier bewust buiten.

Wat voegt een MCP-server toe aan een bestaande SaaS?

Met een MCP-server kan een AI-assistent buiten je product je product gebruiken: iets opzoeken, iets uitrekenen en een handeling uitvoeren namens de gebruiker die met hem praat. Het Model Context Protocol legt vast hoe de assistent die tools vindt en aanroept, dus één server werkt met elke client die het protocol spreekt. Voor een bestaande SaaS is het een nieuwe voordeur, geen nieuw product: de bedrijfslogica blijft waar hij was, en de MCP-server is een dunne laag die een toolaanroep vertaalt naar dezelfde serviceaanroep die je eigen schermen doen.

Hoeveel tools heeft een MCP-server nodig?

Pilot-Next heeft 17 tools: 14 die alleen lezen en 3 die schrijven. De schrijvende tools zijn de drie handelingen die iets veranderen waar een lid om geeft: een reservering maken, haar annuleren, en een gevlogen vlucht afrekenen tot een factuur. Al het andere beantwoordt een vraag.

toolwat hij doetleest of schrijft
get_dispatch_context_and_timevloot, standaardkeuzes, voorkeuren en de datum van vandaagleest
get_weatherMETAR en TAF voor een of meer vliegveldenleest
get_my_balancehet huidige saldoleest
get_my_bookingskomende en recente reserveringenleest
get_unsettled_bookingsgevlogen maar nog niet afgerekendleest
get_pilot_activityvliegactiviteit van een pilootleest
get_aircraft_availabilitywanneer een toestel vrij isleest
find_available_slotseen vrij blok van een gegeven lengteleest
get_aircraft_hoursde actuele Hobbs- en Tacho-standleest
get_maintenance_forecastwanneer onderhoud nodig is, op basis van gebruikleest
get_flight_statisticsvlieguren per toestel per jaarleest
estimate_flight_costwat een vlucht kost vóór hij geboekt isleest
get_booking_settlement_infobeginstanden en tarieven van een vlucht die afgerekend wordtleest
preview_settlementde factuur zoals hij zou worden, zonder op te slaanleest
create_bookingreserveert een toestelschrijft
cancel_bookingannuleert een reserveringschrijft
settle_bookingslaat het vluchtlog op en maakt de factuurschrijft

Naast de tools biedt de server één prompt (een vluchtbriefing) en geen resources. Er was er ooit één: een tekst met “clubreglementen” die in de code bleek te zijn geschreven en aan elke club werd geserveerd alsof het de hunne was. Hij is weggehaald in plaats van gerepareerd, want er bestaan geen reglementsgegevens om te serveren. Een resource die gezaghebbend klinkt en het niet is, is erger dan geen resource: de assistent citeert hem met hetzelfde vertrouwen als een echt saldo.

Het getal dat ertoe doet is niet 17 maar de verhouding. Een assistent is nuttig zodra hij vragen kan beantwoorden, en elke schrijvende tool is een plek waar een verkeerde gok iets kost. Begin met de vragen die gebruikers nu aan je helpdesk stellen, en voeg alleen een schrijvende tool toe voor een handeling waarvan je het gevolg kunt laten zien voordat het gebeurt.

Hoe groot is de toollijst, en waarom doet dat ertoe?

Elke client die verbindt vraagt de toollijst op (tools/list) en geeft die aan het model, zodat het model weet wat het kan aanroepen. Wij hebben die lijst gemeten door de toolregistratie van de server te starten tegen een client in het geheugen en op te slaan wat terugkwam:

deel van de toollijstomvang
volledig tools/list-antwoord, 17 tools11.319 bytes JSON
alle 17 omschrijvingen samen3.572 tekens
alle 17 invoerschema’s samen5.941 tekens
parameters over alle tools44, elk met een eigen omschrijving
langste omschrijvingestimate_flight_cost, 421 tekens
grootste invoerschemasettle_booking, 937 tekens

Twee dingen vallen op. De invoerschema’s wegen zwaarder dan de omschrijvingen — 5.941 tegen 3.572 tekens — omdat elke parameter een eigen type en omschrijving meedraagt. En de twee afreken-tools zijn samen al goed voor 1.852 tekens schema, omdat ze dezelfde acht parameters twee keer vragen: één keer voor het voorbeeld, één keer om op te slaan.

Dat is een bewuste afweging en geen ongeluk. Maar het betekent dat elke parameter die je toevoegt tekst is die het model leest vóór elk antwoord, of de gebruiker het nu over die tool heeft of niet. Een toollijst is een prompt die je niet ziet.

Hoe voorkom je dat een assistent schrijft zonder te vragen?

Met een voorbeeldtool die precies uitrekent wat de schrijvende tool zou doen, en niets opslaat. In Pilot-Next kost een vlucht afrekenen drie stappen:

  1. get_booking_settlement_info geeft de vastgelegde beginstanden. De omschrijving zegt het model de piloot te vragen of die kloppen, en in hetzelfde bericht niets anders te vragen.
  2. preview_settlement bouwt het vluchtlog in het geheugen, haalt het door dezelfde tarief- en factuurberekening die de applicatie voor haar echte facturen gebruikt, en geeft de factuurregels terug — met de vraag “Shall I finalize this invoice?”.
  3. settle_booking slaat het vluchtlog op en maakt de factuur. Zijn omschrijving begint met FINAL STEP en zegt dat hij niet mag worden aangeroepen voordat de piloot het voorbeeld heeft gezien en bevestigd.

De waarde van het voorbeeld is dat de bevestiging iets betekent. “Zal ik deze vlucht afrekenen?” is een vraag waar iedereen ja op zegt. “Dit worden deze drie factuurregels — zal ik hem definitief maken?” is een vraag die een piloot echt nakijkt, omdat de bedragen de bedragen zijn die gefactureerd gaan worden.

Het protocol zegt zelf dat er altijd een mens in de lus hoort te zitten die een toolaanroep kan weigeren, en dat clients om bevestiging horen te vragen bij gevoelige handelingen. Het definieert ook hints zoals readOnlyHint en destructiveHint — maar dezelfde specificatie zegt dat een client die hints als onbetrouwbaar moet behandelen, tenzij de server vertrouwd is. Een hint in een toollijst is geen bevestiging. Een voorbeeld dat de echte uitkomst laat zien wel.

Waarom heeft een assistent een contexttool nodig?

Omdat het model niet weet welke dag het is, en gaat gokken. “Morgen” en “volgende zaterdag” betekenen niets zonder datum, en een model dat er een moet invullen, pakt een jaar uit zijn trainingsdata. Verschillende tool-omschrijvingen van Pilot-Next dragen nog de waarschuwing die daaruit voortkwam: Do NOT guess the year (e.g. 2024).

De oplossing kwam in drie stappen, en juist die geschiedenis is de les:

  • 18 december 2025 — een contexttool die de vloot, het standaardtoestel van de gebruiker, zijn voorkeuren en de actuele datum en tijd teruggeeft. Het idee: de assistent moet verstandige aannames doen in plaats van “robotachtige” vragen te stellen over de datum van vandaag.
  • 22 december 2025 — hernoemd naar get_dispatch_context_and_time, “for better discoverability” in de woorden van de commit. Een naam waar time in staat, wordt gekozen als de vraag over tijd gaat.
  • 25 december 2025 — de contexttool geeft nu een token terug met de datum van vandaag erin, en de tools die zoeken of boeken eisen dat token als argument.

Vandaag zeggen vijf tool-omschrijvingen dat het model eerst de contexttool moet aanroepen, en drie tools zijn niet aan te roepen zonder zijn token. De instructie alleen was niet genoeg; de aanroep een verplichte invoer maken wel. Moet er iets gebeuren voordat een tool draait, zet het dan in het schema, niet alleen in het proza.

Hoort de MCP-server eigen logica te bevatten?

Zo weinig mogelijk, en dat hebben we op de langzame manier geleerd. De tools krijgen de bestaande services van de applicatie mee — reserveringen, tarieven, facturen, saldo’s — en roepen die aan. create_booking slaat op via dezelfde reserveringsservice die de webapp gebruikt; het voorbeeld rekent met dezelfde tarief- en factuurservices als een echte factuur.

Het duidelijkste geval van een tool met een eigen query was het saldo. Die tool las het grootboek zelf, sorteerde net iets anders dan de webapp, en kon daardoor een tussenstand tonen die het scherm nooit liet zien. In september 2026 is dat veranderd naar precies dezelfde aanroep als de webapp, met een opmerking in de code waarom: één bron voor het saldo, zodat de assistent nooit een bedrag noemt dat de gebruiker in de app niet terugvindt. Elke regel logica in de MCP-laag is een tweede implementatie, en een tweede implementatie loopt uit de pas.

Hoe bereiken fouten het model?

Als tekst die het model kan doorgeven. Het protocol onderscheidt protocolfouten (een onbekende tool, ongeldige argumenten) van fouten bij het uitvoeren, die als gewoon resultaat terugkomen met isError: true. Pilot-Next gebruikt die tweede soort op 28 plekken, en de tekst is geschreven voor de gebruiker die hem te horen krijgt: een overlappende reservering of een toestel dat aan zijn inspectie toe is, komt terug als een zin die de assistent kan herhalen, niet als een stacktrace.

Eén fout was helemaal geen fout, en het is het nuttigste van dit artikel. Modellen sturen booleans verrassend vaak als tekst. In Zod 4.1.13 gebruikt z.coerce.boolean() de Boolean() van JavaScript, dus de tekst "false" wordt true — op 24 september 2026 nog eens nagemeten. De server heeft een eigen boolean-schema dat "true" en "false" expliciet vertaalt en al het andere weigert. Bepaalt een vlag in je tool of er een factuur wordt gemaakt, dan is dit geen detail.

Welk transport?

Streamable HTTP op één eindpunt, /mcp, is het huidige HTTP-transport van de specificatie; het verving het HTTP+SSE-transport van protocolversie 2024-11-05. Pilot-Next biedt beide: Streamable HTTP voor de huidige clients, en de oude SSE-eindpunten voor clients die nog niet zijn overgestapt — Home Assistant is het voorbeeld dat de code noemt. De specificatie beschrijft precies dat als de achterwaarts compatibele route.

Twee details uit productie. Is een sessie onbekend — na een herstart, of omdat hij verlopen is — dan antwoordt de server 404, zoals de specificatie voorschrijft, en daarop begint een client een nieuwe sessie. Een 410 Gone zou preciezer lijken, en de opmerking in de code legt vast waarom die niet wordt gebruikt: sommige clients vatten hem op als definitief. En verzoeken binnen één sessie worden strikt op volgorde afgehandeld, behalve de langlopende GET-stroom, die anders elk verzoek erna zou blokkeren.

Hoe lang duurde het?

De eerste code is van 12 december 2025. Van de 163 commits die de MCP-module sindsdien raakten, zijn er 124 gemaakt in december 2025, op negen dagen. Daarna werd het stil: één commit in januari, dan 9, 4 en 3 in maart, april en mei, en 22 in september 2026.

Een eerste versie is dus echt een kwestie van dagen — maar het waren niet de tools die de maand kostten. Een telling op trefwoorden in de 124 commitberichten van december geeft 60 over de verbinding (SSE-stromen, heartbeats, handshakes, de volgorde van verzoeken, een client die de eerste beurt liet vallen) en 35 over datum en context; negen gaan over allebei. Verschillende clients braken op verschillende manieren, en een zelfgebouwd SSE-transport werd voor elk van hen opgelapt tot de server op 22 december 2025 overstapte op de officiële TypeScript-SDK, en later op diens Streamable HTTP-transport. De les die we uit die maand meenemen: gebruik de officiële SDK vanaf de eerste commit, en besteed de dagen die je daarmee wint aan de tool-omschrijvingen.

Vandaag is de module 3.493 regels code, met 73 tests op de tools, de protocolafhandeling en de registervermelding. Eén van die tests faalt zodra de tools die de server registreert afwijken van de lijst die in MCP-registers staat, zodat de omschrijving die een gebruiker leest vóór hij verbindt nooit kan verschillen van de server waarmee hij verbindt.

Wat we iedereen meegeven die begint

  1. Begin met de vragen, niet met de handelingen. Veertien van onze zeventien tools lezen. Daar is de assistent op dag één nuttig, en daar kost een fout niets.
  2. Geef elke schrijvende tool een voorbeeld dat de echte berekening draait en niets opslaat. Een bevestiging is zo goed als wat de gebruiker te zien krijgt.
  3. Zet voorwaarden in het schema. Een instructie in een omschrijving is een verzoek; een verplicht argument is een regel.
  4. Roep je bestaande services aan, geen eigen query’s. Waar we dat niet deden, kon de assistent een ander saldo tonen dan de app.
  5. Gebruik de officiële SDK vanaf dag één. De helft van onze eerste maand ging op aan verbindingsproblemen in een transport dat we zelf hadden gebouwd.
  6. Meet je toollijst. De onze is 11 KB en meer dan de helft daarvan is invoerschema. Elke parameter wordt gelezen vóór elk antwoord.
  7. Test booleans die als tekst binnenkomen. "false" is op meer plekken waar dan je denkt.

Dit is het soort functie dat we bedoelen met AI die werk overneemt: geen chatvenster naast het product, maar het product zelf, te bedienen vanuit het gesprek dat de gebruiker toch al voert. Hoe Pilot-Next verder gebouwd is, staat op de casepagina van Pilot-Next.

Laten we praten

Wat gaan we bouwen?

Eén gesprek is genoeg om te weten of we bij elkaar passen. Vertel wat je voor je ziet — wij zeggen hoe snel dat kan, en of wij daar de juiste partij voor zijn.

Start het gesprek