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

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