Skip to main content
Glama
pace8

mcp-hypotheken-berekenen

opzet_hypotheek_doorstromer

Calculate mortgage options for homeowners moving to a new property. Compare existing versus new monthly payments, required financing amounts, and loan components to plan your move effectively.

Instructions

Berekent de hypotheekopzet voor doorstromers met bestaande woning. Output: benodigd bedrag, financiering per component en maandlasten (bestaand versus nieuw).

Invoerbeleid bestaande hypotheek (verplicht expliciet vragen):

  • Stel altijd de vraag: "Wilt u een snelle globale berekening (met een samenvatting van uw hypotheek) of een detailberekening waarbij u alle leningdelen invoert?"

  • Bij snelle globale berekening: laat de gebruiker één samenvattende set waarden geven (totale schuld, gemiddelde rente, resterende looptijd, optioneel huidige maandlast) en vul hiermee één leningdeel.

  • Bij detailberekening: laat de gebruiker alle leningdelen kopiëren/plakken (hoofdsom, rente, looptijd, rentevast, hypotheekvorm) en vul de leningdelen-array één-op-één.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
aanvragerYesGegevens van de (hoofd)aanvrager. Vraag altijd: "Wat is uw leeftijd of geboortedatum?" en gebruik opgegeven leeftijden alleen intern.
waarde_huidige_woningYesMarktwaarde van de huidige woning.
bestaande_hypotheekYesBestaande leningdelen voor doorstromer (detailuitleg: hypotheek://v4/guide/opzet-intake). VRAAG ALTIJD: "Wilt u een snelle globale berekening (met een samenvatting van uw hypotheek) of een detailberekening waarbij u alle leningdelen invoert?"
nieuwe_woningYesKerngegevens nieuwe woning (detailuitleg: hypotheek://v4/guide/opzet-intake).
session_idNoOptioneel sessie-ID vanuit n8n (voor logging).

Implementation Reference

  • Main handler function that processes the tool request: normalizes and validates input, builds API payload for the Replit opzet endpoint, calls the external API, logs success, and returns a formatted response.
    async function handleOpzetDoorstromer(request: any): Promise<ToolResponse> {
      const rawArgs = requireArguments<OpzetDoorstromerArguments>(request);
      const normalizedArgs = normalizeOpzetDoorstromerArgs(rawArgs) as OpzetDoorstromerArguments;
      const logger = createLogger(normalizedArgs.session_id);
    
      const aanvrager = requireOpzetAanvrager(normalizedArgs);
      validateOpzetAanvrager(aanvrager);
      validateBestaandeHypotheek(normalizedArgs.bestaande_hypotheek);
      enforceRateLimit(normalizedArgs.session_id);
    
      const payload: any = {
        aanvrager: mapOpzetAanvrager(aanvrager),
        bestaande_hypotheek: {
          waarde_huidige_woning: normalizedArgs.waarde_huidige_woning,
          leningdelen: normalizedArgs.bestaande_hypotheek.leningdelen,
        },
        nieuwe_woning: {
          waarde_woning: normalizedArgs.nieuwe_woning.waarde_woning,
          bedrag_verbouwen: normalizedArgs.nieuwe_woning.bedrag_verbouwen ?? 0,
          bedrag_verduurzamen: normalizedArgs.nieuwe_woning.bedrag_verduurzamen ?? 0,
          kosten_percentage: normalizedArgs.nieuwe_woning.kosten_percentage ?? 0.05,
          energielabel: normalizeEnergielabel(normalizedArgs.nieuwe_woning.energielabel || ''),
        },
      };
    
      if (normalizedArgs.session_id) {
        payload.session_id = normalizedArgs.session_id;
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.post(
        REPLIT_API_URL_OPZET,
        payload,
        { correlationId: normalizedArgs.session_id }
      );
    
      logger.info('Toolcall succesvol', { tool: 'opzet_hypotheek_doorstromer' });
      return successResponse(formatResponse(data, "opzet_hypotheek_doorstromer"));
    }
  • src/index.ts:903-938 (registration)
    Tool registration in MCP ListToolsRequestHandler: defines name, detailed description with usage instructions, and complete inputSchema with properties and requirements.
          {
            name: "opzet_hypotheek_doorstromer",
            description: `Opzet-berekening voor doorstromers met een CONCRETE nieuwe woning. Gebruik dit zodra er een koopprijs/verbouwing bekend is; hiermee ziet de gebruiker exact hoe bestaand en nieuw samenkomen. Voor algemene verhuis-oriëntatie zonder specifieke woning gebruikt u de maximale-hypotheek tools.
    
    **Invoerbeleid bestaande hypotheek (verplicht expliciet vragen):**
    - Stel altijd de vraag: "Wilt u een snelle globale berekening (met een samenvatting van uw hypotheek) of een detailberekening waarbij u alle leningdelen invoert?"
    - Bij snelle globale berekening: laat de gebruiker één samenvattende set waarden geven (totale schuld, gemiddelde rente, resterende looptijd, optioneel huidige maandlast) en vul hiermee één leningdeel.
    - Bij detailberekening: laat de gebruiker alle leningdelen kopiëren/plakken (hoofdsom, rente, looptijd, rentevast, hypotheekvorm) en vul de leningdelen-array één-op-één.`,
            inputSchema: {
              type: "object",
              description: `Gebruik basisintake, huidige woning en bestaande leningdelen; zie ${OPZET_GUIDE_URI} voor detailvelden en defaults.`,
              properties: {
                aanvrager: aanvragerSchema,
                waarde_huidige_woning: {
                  type: "number",
                  description: "Marktwaarde van de huidige woning.",
                },
                bestaande_hypotheek: {
                  ...bestaandeHypotheekSchema,
                },
                nieuwe_woning: {
                  ...nieuweWoningSchema,
                },
                session_id: {
                  type: "string",
                  description: "Optioneel sessie-ID vanuit n8n (voor logging).",
                },
              },
              required: [
                "aanvrager",
                "waarde_huidige_woning",
                "bestaande_hypotheek",
                "nieuwe_woning",
              ],
            },
          },
  • TypeScript interface defining the expected input structure for the tool arguments.
    interface OpzetDoorstromerArguments extends OpzetBaseArguments {
      nieuwe_woning: NieuweWoning;
      waarde_huidige_woning: number;
      bestaande_hypotheek: BestaandeHypotheek;
    }
    
    interface Renteklasse {
      naam: string;
      lowerbound_ltv_pct: number;
      higherbound_ltv_pct: number;
      nhg: boolean;
      rente_jaarlijks_pct: number;
    }
    
    interface OpzetNieuweLening {
      looptijd_jaren?: number;
      rentevast_periode_jaren?: number;
      nhg?: boolean;
      renteklassen?: Renteklasse[];
    }
    
    interface OpzetUitgebreidArguments extends OpzetBaseArguments {
      nieuwe_woning: NieuweWoning;
      is_doorstromer?: boolean;
      waarde_huidige_woning?: number;
      bestaande_hypotheek?: BestaandeHypotheek;
      nieuwe_lening?: OpzetNieuweLening;
    }
    
    const OPZET_GUIDE_URI = 'hypotheek://v4/guide/opzet-intake';
    
    const DOORSTROMER_OUTPUT_GUIDANCE = `
    **Outputvelden (altijd rechtstreeks gebruiken in de terugkoppeling):**
    - max_woningbudget → woningbudget inclusief overwaarde en extra leencapaciteit
    - overwaarde_bedrag → vrijvallende winst uit de huidige woning
    - huidige_hypotheek_schuld → resterende schuld die moet worden afgelost
    - extra_leencapaciteit → nieuwe hypotheekruimte bovenop de overwaarde
    - maandlast_nu, maandlast_straks en verschil_maandlast → huidige, toekomstige en delta maandlast
    
    **Presentatie richting gebruiker (één compact blok):**
    - Toon het woningbudget centraal onder de titel "Uw woningbudget" en licht toe waaruit dit bedrag bestaat in bullets (overwaarde, huidige hypotheek, extra leencapaciteit).
    - Voeg een tweede blok toe "Uw nieuwe maandlast" met maandlast nu, maandlast straks en het verschil (positief/negatief) op eigen regel.
    - Gebruik alleen MCP-waarden; geen eigen herberekeningen behalve eenvoudige weergave/afronding.
    
    **Invoerkeuze bestaande hypotheek (verplicht expliciet vragen):**
    1. Snelle globale berekening → gebruiker geeft een samenvatting (totale schuld, gemiddelde rente/looptijd, eventuele huidige maandlast). Vul één leningdeel met deze totaalwaarden in.
    2. Detailberekening → gebruiker levert alle leningdelen (hoofdsom, rente, resterende looptijd, hypotheekvorm). Kopieer ze één-op-één in de leningdelen array.
    
    Vraag altijd: "Wilt u een snelle globale berekening (met een samenvatting van uw hypotheek) of een detailberekening waarbij u alle leningdelen invoert?" en volg de gekozen route.`;
    
    // BESLISBOOM VOOR AI-AGENTS
    // 1. Heeft de gebruiker een concrete woning/koopprijs in gedachten?
    //    - Ja → gebruik altijd een OPZET tool (starter, doorstromer of uitgebreid).
    //    - Nee → gebruik een MAXIMALE HYPOTHEEK tool.
    // 2. Is de gebruiker starter (nog geen eigen woning) of doorstromer (heeft al woning/hypotheek)?
    //    - Kies de starter- of doorstromervariant van het gekozen pad (maximaal of opzet).
    // 3. Wil de gebruiker specifiek spelen met looptijd/rente/hypotheekvorm?
    //    - Nee → kies de standaard tool (opzet_hypotheek_starter/doorstromer of bereken_hypotheek_starter/doorstromer).
    //    - Ja → gebruik de uitgebreide variant (opzet_hypotheek_uitgebreid of bereken_hypotheek_uitgebreid) en vraag naar de gewenste parameters.
    
    // Leeftijd/geboortedatum beleid:
    // - Vraag eindgebruikers altijd: "Wat is uw leeftijd of geboortedatum?"
    // - Converteer een opgegeven leeftijd intern naar een geboortedatum in ISO-formaat voor MCP-calls
    // - Rapporteer bij een gegeven leeftijd uitsluitend die leeftijd terug aan de gebruiker (nooit de afgeleide geboortedatum)
    const baseIntakeProperties = {
      inkomen_aanvrager: {
        type: "number",
        description: "Bruto jaarinkomen hoofdaanvrager in euro's.",
      },
      geboortedatum_aanvrager: {
        type: "string",
        description: "Interne geboortedatum hoofdaanvrager (ISO). Vraag de gebruiker altijd: \"Wat is uw leeftijd of geboortedatum?\" en deel bij een leeftijd alleen die leeftijd terug.",
      },
      heeft_partner: {
        type: "boolean",
        description: "Geeft aan of een partner mee leent.",
      },
      inkomen_partner: {
        type: "number",
        description: "Optioneel partnerinkomen in euro's.",
      },
      geboortedatum_partner: {
        type: "string",
        description: "Optionele interne geboortedatum partner (ISO). Vraag ook hier: \"Wat is uw leeftijd of geboortedatum?\" en houd de afgeleide datum intern.",
      },
      verplichtingen_pm: {
        type: "number",
        description: "Optionele maandelijkse verplichtingen in euro's.",
        default: 0,
      },
    };
    
    const baseIntakeRequired = ["inkomen_aanvrager", "geboortedatum_aanvrager", "heeft_partner"];
    
    const aanvragerSchema = {
      type: "object",
      description: 'Gegevens van de (hoofd)aanvrager. Vraag altijd: "Wat is uw leeftijd of geboortedatum?" en gebruik opgegeven leeftijden alleen intern.',
      properties: {
        ...baseIntakeProperties,
        eigen_vermogen: {
          type: "number",
          description: "Beschikbaar eigen geld in euro's (optioneel).",
          default: 0,
        },
      },
      required: [...baseIntakeRequired],
    };
    
    const nieuweWoningSchema = {
      type: "object",
      description: `Kerngegevens nieuwe woning (detailuitleg: ${OPZET_GUIDE_URI}).`,
      properties: {
        waarde_woning: {
          type: "number",
          description: "Koopsom nieuwe woning in euro's.",
        },
        bedrag_verbouwen: {
          type: "number",
          description: "Optionele verbouwingskosten in euro's.",
          default: 0,
        },
        bedrag_verduurzamen: {
          type: "number",
          description: "Optionele verduurzamingskosten in euro's.",
          default: 0,
        },
        kosten_percentage: {
          type: "number",
          description: "Optioneel kostenpercentage koper als decimaal.",
          default: 0.05,
        },
        energielabel: {
          type: "string",
          description: "Optioneel energielabel van de woning.",
          enum: ["A++++ (met garantie)", "A++++", "A+++", "A++", "A+", "A", "B", "C", "D", "E", "F", "G"],
        },
      },
      required: ["waarde_woning"],
    };
    
    // Doorstromer invoerbeleid:
    // - Laat gebruikers kiezen tussen een snelle globale samenvatting of detailinvoer per leningdeel.
    // - Snelle invoer: één "leningdeel" dat totale schuld, gemiddelde rente en resterende looptijd samenvat.
    // - Detailinvoer: meerdere leningdelen rechtstreeks overgenomen uit het hypotheekoverzicht.
    const bestaandeHypotheekSchema = {
      type: "object",
      description: `Bestaande leningdelen voor doorstromer (detailuitleg: ${OPZET_GUIDE_URI}). VRAAG ALTIJD: "Wilt u een snelle globale berekening (met een samenvatting van uw hypotheek) of een detailberekening waarbij u alle leningdelen invoert?"`,
      properties: {
        leningdelen: {
          type: "array",
          description: "Minimaal één leningdeel. Gebruik één samenvattend leningdeel voor een snelle globale berekening of voeg alle afzonderlijke leningdelen toe voor een nauwkeurige detailberekening.",
          items: {
            type: "object",
            properties: {
              huidige_schuld: {
                type: "number",
                description: "Restschuld in euro's.",
              },
              huidige_rente: {
                type: "number",
                description: "Rente als decimaal (bijv. 0.028).",
              },
              resterende_looptijd_in_maanden: {
                type: "number",
                description: "Resterende looptijd in maanden.",
              },
              rentevasteperiode_maanden: {
                type: "number",
                description: "Resterende rentevaste periode in maanden.",
              },
              hypotheekvorm: {
                type: "string",
                description: "Hypotheekvorm van het leningdeel.",
                enum: ["annuiteit", "lineair", "aflossingsvrij"],
              },
            },
            required: ["huidige_schuld", "huidige_rente", "resterende_looptijd_in_maanden", "rentevasteperiode_maanden", "hypotheekvorm"],
          },
        },
      },
      required: ["leningdelen"],
    };
    
    const server = new Server(
      {
        name: "hypotheek-berekening-server",
        version: config.serverVersion,
      },
      {
        capabilities: {
          tools: {},
          resources: {},
          prompts: {},
        },
      }
    );
    
    type ToolResponse = {
      content: Array<{
        type: "text";
        text: string;
      }>;
    };
    
    type ToolErrorResponse = ToolResponse & { isError: true };
    type ToolHandler = (request: any) => Promise<ToolResponse>;
    
    function successResponse(text: string): ToolResponse {
      return {
        content: [
          {
            type: "text",
            text,
          },
        ],
      };
    }
    
    function errorResponse(error: unknown, sessionId?: string): ToolErrorResponse {
      if (error instanceof ValidationError) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(error.toStructured(sessionId), null, 2),
            },
          ],
          isError: true,
        };
      }
    
      if (error instanceof APIError) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(error.toStructured(sessionId), null, 2),
            },
          ],
          isError: true,
        };
      }
    
      const message = error instanceof Error ? error.message : String(error);
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              {
                code: ErrorCode.UNKNOWN_ERROR,
                message,
                correlation_id: sessionId,
              },
              null,
              2
            ),
          },
        ],
        isError: true,
      };
    }
    
    function normalizeSessionIdField(obj: Record<string, unknown>) {
      if (!obj) return;
      if (!obj.session_id && typeof obj.sessionId === 'string' && obj.sessionId.trim().length > 0) {
        obj.session_id = obj.sessionId;
      }
    }
    
    function requireArguments<T>(request: any): T {
      if (!request.params?.arguments) {
        throw new ValidationError(
          ErrorCode.INVALID_INPUT,
          'Arguments zijn verplicht',
          'arguments'
        );
      }
      const args = request.params.arguments as Record<string, unknown>;
      normalizeSessionIdField(args);
      return args as unknown as T;
    }
    
    function extractSessionId(args: unknown): string | undefined {
      if (!args || typeof args !== 'object') {
        return undefined;
      }
      const record = args as Record<string, unknown>;
      if ('session_id' in record && typeof record.session_id === 'string') {
        return record.session_id;
      }
      if ('sessionId' in record && typeof record.sessionId === 'string') {
        return record.sessionId;
      }
      return undefined;
    }
    
    function mapAanvragers(args: {
      inkomen_aanvrager: number;
      geboortedatum_aanvrager: string;
      heeft_partner: boolean;
      inkomen_partner?: number;
      geboortedatum_partner?: string;
      verplichtingen_pm?: number;
    }) {
      return {
        inkomen_aanvrager: args.inkomen_aanvrager,
        geboortedatum_aanvrager: args.geboortedatum_aanvrager,
        heeft_partner: args.heeft_partner,
        inkomen_partner: args.inkomen_partner ?? 0,
        geboortedatum_partner: args.geboortedatum_partner ?? null,
        verplichtingen_pm: args.verplichtingen_pm ?? 0,
      };
    }
    
    function validateOpzetAanvrager(aanvrager: OpzetAanvrager) {
      validateBaseArguments({
        inkomen_aanvrager: aanvrager.inkomen_aanvrager,
        geboortedatum_aanvrager: aanvrager.geboortedatum_aanvrager,
        heeft_partner: aanvrager.heeft_partner,
        inkomen_partner: aanvrager.inkomen_partner,
        geboortedatum_partner: aanvrager.geboortedatum_partner,
        verplichtingen_pm: aanvrager.verplichtingen_pm,
      } as BaseArguments);
    }
    
    function requireOpzetAanvrager(container: { aanvrager?: OpzetAanvrager }): OpzetAanvrager {
      if (!container.aanvrager || typeof container.aanvrager !== 'object') {
        throw new ValidationError(
          ErrorCode.INVALID_INPUT,
          'aanvrager ontbreekt of is onvolledig',
          'aanvrager'
        );
      }
      return container.aanvrager;
    }
    
    function mapOpzetAanvrager(aanvrager: OpzetAanvrager) {
      return {
        inkomen_aanvrager: aanvrager.inkomen_aanvrager,
        geboortedatum_aanvrager: aanvrager.geboortedatum_aanvrager,
        heeft_partner: aanvrager.heeft_partner,
        inkomen_partner: aanvrager.inkomen_partner ?? 0,
        geboortedatum_partner: aanvrager.geboortedatum_partner ?? null,
        verplichtingen_pm: aanvrager.verplichtingen_pm ?? 0,
        eigen_vermogen: aanvrager.eigen_vermogen ?? 0,
      };
    }
    
    function buildNieuweLeningPayload(raw: any): any | undefined {
      if (!raw || typeof raw !== 'object') {
        return undefined;
      }
    
      const payload: Record<string, unknown> = {};
    
      const looptijdMaanden =
        raw.looptijd_maanden ??
        (typeof raw.looptijd_jaren === 'number' ? raw.looptijd_jaren * 12 : undefined);
      if (looptijdMaanden) {
        payload.looptijd_maanden = looptijdMaanden;
      }
    
      const rentevastMaanden =
        raw.rentevaste_periode_maanden ??
        (typeof raw.rentevast_periode_jaren === 'number' ? raw.rentevast_periode_jaren * 12 : undefined);
      if (rentevastMaanden) {
        payload.rentevaste_periode_maanden = rentevastMaanden;
      }
    
      if (raw.rente !== undefined) {
        payload.rente = raw.rente;
      }
    
      if (raw.hypotheekvorm) {
        payload.hypotheekvorm = raw.hypotheekvorm;
      } else if (raw.type) {
        payload.hypotheekvorm = raw.type;
      }
    
      if (raw.energielabel) {
        payload.energielabel = normalizeEnergielabel(raw.energielabel);
      }
    
      if (raw.nhg !== undefined) {
        payload.nhg = raw.nhg;
      }
    
      if (raw.ltv !== undefined) {
        let ltvValue: number | undefined;
        if (typeof raw.ltv === 'string') {
          const parsed = parseFloat(raw.ltv.replace('%', ''));
          ltvValue = Number.isFinite(parsed) ? parsed / 100 : undefined;
        } else if (typeof raw.ltv === 'number') {
          ltvValue = raw.ltv;
        }
        if (ltvValue !== undefined) {
          payload.ltv = ltvValue;
        }
      }
    
      if (Array.isArray(raw.renteklassen) && raw.renteklassen.length > 0) {
        payload.renteklassen = raw.renteklassen;
      }
    
      return Object.keys(payload).length > 0 ? payload : undefined;
    }
    
    async function handleBerekenStarter(request: any): Promise<ToolResponse> {
      const args = requireArguments<BaseArguments>(request);
      const logger = createLogger(args.session_id);
    
      validateBaseArguments(args);
      enforceRateLimit(args.session_id);
    
      const payload: any = {
        aanvragers: mapAanvragers(args),
      };
    
      if (args.session_id) {
        payload.session_id = args.session_id;
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.post(
        REPLIT_API_URL_BEREKENEN,
        payload,
        { correlationId: args.session_id }
      );
    
      logger.info('Toolcall succesvol', { tool: 'bereken_hypotheek_starter' });
      return successResponse(formatResponse(data, "bereken_hypotheek_starter"));
    }
    
    async function handleBerekenDoorstromer(request: any): Promise<ToolResponse> {
      const rawArgs = requireArguments<DoorstromerArguments>(request);
      const normalizedArgs = normalizeDoorstromerArgs(rawArgs) as DoorstromerArguments;
      const logger = createLogger(normalizedArgs.session_id);
    
      validateDoorstromerArguments(normalizedArgs);
      enforceRateLimit(normalizedArgs.session_id);
    
      const payload: any = {
        aanvragers: mapAanvragers(normalizedArgs),
        bestaande_hypotheek: {
          waarde_huidige_woning: normalizedArgs.waarde_huidige_woning,
          leningdelen: normalizedArgs.bestaande_hypotheek.leningdelen,
        },
      };
    
      if (normalizedArgs.session_id) {
        payload.session_id = normalizedArgs.session_id;
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.post(
        REPLIT_API_URL_BEREKENEN,
        payload,
        { correlationId: normalizedArgs.session_id }
      );
    
      logger.info('Toolcall succesvol', { tool: 'bereken_hypotheek_doorstromer' });
      return successResponse(formatResponse(data, "bereken_hypotheek_doorstromer"));
    }
    
    async function handleBerekenUitgebreid(request: any): Promise<ToolResponse> {
      const rawArgs = requireArguments<UitgebreidArguments>(request);
      const normalizedArgs = rawArgs.is_doorstromer
        ? (normalizeDoorstromerArgs(rawArgs) as UitgebreidArguments)
        : rawArgs;
      const logger = createLogger(normalizedArgs.session_id);
    
      validateBaseArguments(normalizedArgs as BaseArguments);
      if (normalizedArgs.is_doorstromer && normalizedArgs.bestaande_hypotheek) {
        validateBestaandeHypotheek(normalizedArgs.bestaande_hypotheek);
      }
    
      enforceRateLimit(normalizedArgs.session_id);
    
      const payload: any = {
        aanvragers: mapAanvragers(normalizedArgs),
      };
    
      if (normalizedArgs.is_doorstromer && normalizedArgs.waarde_huidige_woning && normalizedArgs.bestaande_hypotheek) {
        payload.bestaande_hypotheek = {
          waarde_huidige_woning: normalizedArgs.waarde_huidige_woning,
          leningdelen: normalizedArgs.bestaande_hypotheek.leningdelen,
        };
      }
    
      const maatwerk = (normalizedArgs as any).nieuwe_hypotheek ?? (normalizedArgs as any).nieuwe_lening;
      const nieuweLening = buildNieuweLeningPayload(maatwerk);
      if (nieuweLening) {
        payload.nieuwe_lening = nieuweLening;
      }
    
      if (normalizedArgs.session_id) {
        payload.session_id = normalizedArgs.session_id;
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.post(
        REPLIT_API_URL_BEREKENEN,
        payload,
        { correlationId: normalizedArgs.session_id }
      );
    
      logger.info('Toolcall succesvol', { tool: 'bereken_hypotheek_uitgebreid' });
      return successResponse(formatResponse(data, "bereken_hypotheek_uitgebreid"));
    }
    
    async function handleActueleRentes(request: any): Promise<ToolResponse> {
      const sessionId = extractSessionId(request.params?.arguments);
      if (sessionId) {
        enforceRateLimit(sessionId);
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.get(REPLIT_API_URL_RENTES, { correlationId: sessionId });
      return successResponse(JSON.stringify(data, null, 2));
    }
    
    async function handleOpzetStarter(request: any): Promise<ToolResponse> {
      const rawArgs = requireArguments<OpzetStarterArguments>(request);
      const normalizedArgs = normalizeOpzetAanvragerShape(rawArgs) as OpzetStarterArguments;
      const logger = createLogger(normalizedArgs.session_id);
    
      const aanvrager = requireOpzetAanvrager(normalizedArgs);
      validateOpzetAanvrager(aanvrager);
      enforceRateLimit(normalizedArgs.session_id);
    
      const payload: any = {
        aanvrager: mapOpzetAanvrager(aanvrager),
        nieuwe_woning: {
          waarde_woning: normalizedArgs.nieuwe_woning.waarde_woning,
          bedrag_verbouwen: normalizedArgs.nieuwe_woning.bedrag_verbouwen ?? 0,
          bedrag_verduurzamen: normalizedArgs.nieuwe_woning.bedrag_verduurzamen ?? 0,
          kosten_percentage: normalizedArgs.nieuwe_woning.kosten_percentage ?? 0.05,
          energielabel: normalizeEnergielabel(normalizedArgs.nieuwe_woning.energielabel || ''),
        },
      };
    
      if (normalizedArgs.session_id) {
        payload.session_id = normalizedArgs.session_id;
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.post(
        REPLIT_API_URL_OPZET,
        payload,
        { correlationId: normalizedArgs.session_id }
      );
    
      logger.info('Toolcall succesvol', { tool: 'opzet_hypotheek_starter' });
      return successResponse(formatResponse(data, "opzet_hypotheek_starter"));
    }
    
    async function handleOpzetDoorstromer(request: any): Promise<ToolResponse> {
      const rawArgs = requireArguments<OpzetDoorstromerArguments>(request);
      const normalizedArgs = normalizeOpzetDoorstromerArgs(rawArgs) as OpzetDoorstromerArguments;
      const logger = createLogger(normalizedArgs.session_id);
    
      const aanvrager = requireOpzetAanvrager(normalizedArgs);
      validateOpzetAanvrager(aanvrager);
      validateBestaandeHypotheek(normalizedArgs.bestaande_hypotheek);
      enforceRateLimit(normalizedArgs.session_id);
    
      const payload: any = {
        aanvrager: mapOpzetAanvrager(aanvrager),
        bestaande_hypotheek: {
          waarde_huidige_woning: normalizedArgs.waarde_huidige_woning,
          leningdelen: normalizedArgs.bestaande_hypotheek.leningdelen,
        },
        nieuwe_woning: {
          waarde_woning: normalizedArgs.nieuwe_woning.waarde_woning,
          bedrag_verbouwen: normalizedArgs.nieuwe_woning.bedrag_verbouwen ?? 0,
          bedrag_verduurzamen: normalizedArgs.nieuwe_woning.bedrag_verduurzamen ?? 0,
          kosten_percentage: normalizedArgs.nieuwe_woning.kosten_percentage ?? 0.05,
          energielabel: normalizeEnergielabel(normalizedArgs.nieuwe_woning.energielabel || ''),
        },
      };
    
      if (normalizedArgs.session_id) {
        payload.session_id = normalizedArgs.session_id;
      }
    
      const apiClient = getApiClient();
      const { data } = await apiClient.post(
        REPLIT_API_URL_OPZET,
        payload,
        { correlationId: normalizedArgs.session_id }
      );
    
      logger.info('Toolcall succesvol', { tool: 'opzet_hypotheek_doorstromer' });
      return successResponse(formatResponse(data, "opzet_hypotheek_doorstromer"));
    }
  • Helper function to normalize doorstromer-specific arguments, ensuring aanvrager shape and existing mortgage fields are canonicalized before validation.
    export function normalizeOpzetDoorstromerArgs(args: any): any {
      const normalized = normalizeOpzetAanvragerShape(args);
      if (normalized?.bestaande_hypotheek) {
        normalized.bestaande_hypotheek = normalizeBestaandeHypotheek(normalized.bestaande_hypotheek);
      }
      return normalized;
    }
  • Helper to normalize existing mortgage data, mapping variant field names to canonical keys and recursively normalizing leningdelen array.
    export function normalizeBestaandeHypotheek(input: any): any {
      if (!input || typeof input !== 'object') {
        logger.warn('Invalid bestaande_hypotheek input', { input });
        return input;
      }
      
      const normalized: any = {};
      
      for (const [key, value] of Object.entries(input)) {
        const lowerKey = key.toLowerCase().trim();
        
        const isLeningdelenArray = 
          (lowerKey.includes('lening') && lowerKey.includes('deel')) ||
          (lowerKey.includes('loan') && lowerKey.includes('part')) ||
          lowerKey === 'parts';
        
        if (isLeningdelenArray && Array.isArray(value)) {
          normalized.leningdelen = value.map((deel, idx) => normalizeLeningdeel(deel, idx));
        } else {
          const canonicalKey = BESTAANDE_HYPOTHEEK_MAPPINGS[lowerKey];
          
          if (canonicalKey) {
            normalized[canonicalKey] = value;
          } else {
            normalized[key] = value;
            logger.debug('Unknown field in bestaande_hypotheek', { field: key });
          }
        }
      }
      
      return normalized;
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden of behavioral disclosure. It explains the tool's interactive behavior (mandatory questions to ask users), input handling policies for different calculation types, and output structure. However, it doesn't mention computational limitations, error conditions, or performance characteristics that might be relevant for a complex financial calculation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized but not optimally structured. The first sentence clearly states purpose and output, but the detailed input policy section dominates. While necessary for usage guidance, it creates a front-loaded structure that could be more balanced. Every sentence earns its place, but the flow could be improved.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 5 parameters, 100% schema coverage, no annotations, and no output schema, the description provides strong contextual completeness. It explains the tool's purpose, usage procedures, and behavioral expectations. The main gap is the lack of output format details beyond high-level categories, which would be helpful given the absence of an output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds value by explaining the conceptual approach to handling existing mortgages (global vs detailed calculation) and how this affects the 'leningdelen' array parameter. However, it doesn't provide additional semantic context for other parameters beyond what's in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Berekent de hypotheekopzet voor doorstromers met bestaande woning' (Calculates mortgage setup for movers with existing property). It specifies the exact output format and distinguishes itself from siblings by focusing on 'doorstromers' (movers) with existing properties, unlike starter or uitgebreid variants.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit usage guidance with mandatory questions to ask users: 'Wilt u een snelle globale berekening... of een detailberekening...' (Do you want a quick global calculation... or a detailed calculation...). It clearly explains when to use each approach and how to handle input based on user choice, offering complete procedural guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pace8/mcp-hypotheken-berekenen'

If you have feedback or need assistance with the MCP directory API, please join our Discord server