Skip to main content
Glama
index.ts62.3 kB
#!/usr/bin/env node import "dotenv/config"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { createLogger } from './utils/logger.js'; import { getConfig } from './config/index.js'; import { validateBaseArguments, validateDoorstromerArguments, validateBestaandeHypotheek } from './validation/schemas.js'; import { ValidationError, normalizeEnergielabel, APIError, ErrorCode } from './types/index.js'; import { getApiClient } from './api/client.js'; import { enforceRateLimit } from './middleware/rate-limiter.js'; import { normalizeDoorstromerArgs, normalizeOpzetDoorstromerArgs, } from './adapters/field-normalizer.js'; import { recordToolCall, recordValidationError } from './metrics/exporter.js'; import { listResources, readResource } from './resources/index.js'; import { getPrompt, listPrompts } from './prompts/index.js'; const config = getConfig(); // API URLs uit config const REPLIT_API_URL_BEREKENEN = config.replitApiUrlBerekenen; const REPLIT_API_URL_OPZET = config.replitApiUrlOpzet; const REPLIT_API_URL_RENTES = config.replitApiUrlRentes; const API_KEY = config.replitApiKey; // Oude check niet meer nodig - getConfig() doet dit al // Type definitions voor de arguments interface BaseArguments { session_id?: string; // OPTIONEEL - Sessie ID van de gebruiker uit n8n chat trigger: "When chat message received" inkomen_aanvrager: number; geboortedatum_aanvrager: string; heeft_partner: boolean; inkomen_partner?: number; geboortedatum_partner?: string; verplichtingen_pm?: number; } interface Leningdeel { huidige_schuld: number; huidige_rente: number; resterende_looptijd_in_maanden: number; rentevasteperiode_maanden: number; hypotheekvorm: string; } interface BestaandeHypotheek { leningdelen: Leningdeel[]; } interface DoorstromerArguments extends BaseArguments { waarde_huidige_woning: number; bestaande_hypotheek: BestaandeHypotheek; } interface NieuweHypotheek { looptijd_maanden?: number; rentevaste_periode_maanden?: number; rente?: number; hypotheekvorm?: string; energielabel?: string; nhg?: boolean; ltv?: number; } interface UitgebreidArguments extends BaseArguments { is_doorstromer?: boolean; waarde_huidige_woning?: number; bestaande_hypotheek?: BestaandeHypotheek; nieuwe_hypotheek?: NieuweHypotheek; } // Type definitions voor opzet hypotheek interface OpzetBaseArguments { session_id?: string; // OPTIONEEL - Sessie ID van de gebruiker uit n8n chat trigger: "When chat message received" inkomen_aanvrager: number; geboortedatum_aanvrager: string; heeft_partner: boolean; inkomen_partner?: number; geboortedatum_partner?: string; verplichtingen_pm?: number; eigen_vermogen?: number; } interface NieuweWoning { waarde_woning: number; bedrag_verbouwen?: number; bedrag_verduurzamen?: number; kosten_percentage?: number; energielabel?: string; } interface OpzetStarterArguments extends OpzetBaseArguments { nieuwe_woning: NieuweWoning; } 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 baseIntakeProperties = { inkomen_aanvrager: { type: "number", description: "Bruto jaarinkomen hoofdaanvrager in euro's.", }, geboortedatum_aanvrager: { type: "string", description: "Geboortedatum hoofdaanvrager (YYYY-MM-DD).", }, 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 geboortedatum partner (YYYY-MM-DD).", }, verplichtingen_pm: { type: "number", description: "Optionele maandelijkse verplichtingen in euro's.", default: 0, }, }; const baseIntakeRequired = ["inkomen_aanvrager", "geboortedatum_aanvrager", "heeft_partner"]; 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"], }; const bestaandeHypotheekSchema = { type: "object", description: `Bestaande leningdelen voor doorstromer (detailuitleg: ${OPZET_GUIDE_URI}).`, properties: { leningdelen: { type: "array", description: "Minimaal één leningdeel met restschuld en rente.", 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 requireArguments<T>(request: any): T { if (!request.params?.arguments) { throw new ValidationError( ErrorCode.INVALID_INPUT, 'Arguments zijn verplicht', 'arguments' ); } return request.params.arguments as unknown as T; } function extractSessionId(args: unknown): string | undefined { if (args && typeof args === 'object' && 'session_id' in (args as Record<string, unknown>)) { return (args as Record<string, unknown>).session_id as string | undefined; } 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 mapOpzetAanvrager(args: OpzetBaseArguments) { 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, eigen_vermogen: args.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 args = requireArguments<OpzetStarterArguments>(request); const logger = createLogger(args.session_id); validateBaseArguments(args as BaseArguments); enforceRateLimit(args.session_id); const payload: any = { aanvrager: mapOpzetAanvrager(args), nieuwe_woning: { waarde_woning: args.nieuwe_woning.waarde_woning, bedrag_verbouwen: args.nieuwe_woning.bedrag_verbouwen ?? 0, bedrag_verduurzamen: args.nieuwe_woning.bedrag_verduurzamen ?? 0, kosten_percentage: args.nieuwe_woning.kosten_percentage ?? 0.05, energielabel: normalizeEnergielabel(args.nieuwe_woning.energielabel || ''), }, }; if (args.session_id) { payload.session_id = args.session_id; } const apiClient = getApiClient(); const { data } = await apiClient.post( REPLIT_API_URL_OPZET, payload, { correlationId: args.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); validateBaseArguments(normalizedArgs as BaseArguments); validateBestaandeHypotheek(normalizedArgs.bestaande_hypotheek); enforceRateLimit(normalizedArgs.session_id); const payload: any = { aanvrager: mapOpzetAanvrager(normalizedArgs), 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")); } async function handleOpzetUitgebreid(request: any): Promise<ToolResponse> { const rawArgs = requireArguments<OpzetUitgebreidArguments>(request); const normalizedArgs = rawArgs.is_doorstromer ? (normalizeOpzetDoorstromerArgs(rawArgs) as OpzetUitgebreidArguments) : 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 = { aanvrager: mapOpzetAanvrager(normalizedArgs), 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.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_OPZET, payload, { correlationId: normalizedArgs.session_id } ); logger.info('Toolcall succesvol', { tool: 'opzet_hypotheek_uitgebreid' }); return successResponse(formatResponse(data, "opzet_hypotheek_uitgebreid")); } const TOOL_HANDLERS: Record<string, ToolHandler> = { bereken_hypotheek_starter: handleBerekenStarter, bereken_hypotheek_doorstromer: handleBerekenDoorstromer, bereken_hypotheek_uitgebreid: handleBerekenUitgebreid, haal_actuele_rentes_op: handleActueleRentes, opzet_hypotheek_starter: handleOpzetStarter, opzet_hypotheek_doorstromer: handleOpzetDoorstromer, opzet_hypotheek_uitgebreid: handleOpzetUitgebreid, }; // Lijst met beschikbare tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Tool 1: Starters - Simpele berekening { name: "bereken_hypotheek_starter", description: "Berekent de maximale hypotheek voor starters. Output: maximaal leenbedrag, maandlast en NHG-vergelijking.", inputSchema: { type: "object", description: `Gebruik basisintakevelden; zie ${OPZET_GUIDE_URI} voor detaildefinities.`, properties: { ...baseIntakeProperties, session_id: { type: "string", description: "Optioneel sessie-ID vanuit n8n (voor logging).", }, }, required: baseIntakeRequired, }, }, // Tool 2: Doorstromers - Met bestaande hypotheek { name: "bereken_hypotheek_doorstromer", description: "Berekent de maximale hypotheek voor doorstromers. Output: nieuw leenbedrag, maandlast en overwaarde-indicatie.", inputSchema: { type: "object", description: `Gebruik basisintakevelden plus huidige woninginformatie; zie ${OPZET_GUIDE_URI} voor detaildefinities.`, properties: { ...baseIntakeProperties, waarde_huidige_woning: { type: "number", description: "Huidige marktwaarde van de bestaande woning.", }, bestaande_hypotheek: { ...bestaandeHypotheekSchema, }, session_id: { type: "string", description: "Optioneel sessie-ID vanuit n8n (voor logging).", }, }, required: [ ...baseIntakeRequired, "waarde_huidige_woning", "bestaande_hypotheek", ], }, }, // Tool 3: Uitgebreid - Alle parameters configureerbaar { name: "bereken_hypotheek_uitgebreid", description: "Gebruik dit voor maatwerk (rente, looptijd, energielabel). Output: maatwerk leenbedrag met maandlast en NHG-inschatting.", inputSchema: { type: "object", description: `Alle velden zijn optioneel bovenop de basisintake; zie ${OPZET_GUIDE_URI} voor velduitleg en defaults.`, properties: { ...baseIntakeProperties, eigen_vermogen: { type: "number", description: "Optioneel beschikbaar eigen geld in euro's.", default: 0, }, is_doorstromer: { type: "boolean", description: "Geeft aan of de aanvrager een doorstromer is.", }, waarde_huidige_woning: { type: "number", description: "Optionele huidige woningwaarde in euro's.", }, bestaande_hypotheek: { ...bestaandeHypotheekSchema, }, nieuwe_woning: { ...nieuweWoningSchema, }, nieuwe_hypotheek: { type: "object", description: `Optionele maatwerk leningparameters (looptijd, rentevast, rente). Detailuitleg: ${OPZET_GUIDE_URI}.`, }, nieuwe_lening: { type: "object", description: `Optionele structuur voor looptijd/rentevast/NHG en renteklassen (detailuitleg: ${OPZET_GUIDE_URI}).`, }, session_id: { type: "string", description: "Optioneel sessie-ID vanuit n8n (voor logging).", }, }, required: baseIntakeRequired, }, }, // Tool 4: Actuele rentes ophalen { name: "haal_actuele_rentes_op", description: "Haalt actuele hypotheekrentes op per rentevaste periode. Output: overzicht met NHG- en niet-NHG-tarieven.", inputSchema: { type: "object", properties: {}, required: [], }, }, // Tool 5: Opzet hypotheek - Starters { name: "opzet_hypotheek_starter", description: "Berekent de hypotheekopzet voor starters. Output: totaal benodigd bedrag, financieringsoverzicht en maandlast.", inputSchema: { type: "object", description: `Gebruik basisintake plus woninginfo; zie ${OPZET_GUIDE_URI} voor detailvelden en defaults.`, properties: { ...baseIntakeProperties, eigen_vermogen: { type: "number", description: "Optioneel beschikbaar eigen geld in euro's.", default: 0, }, nieuwe_woning: { ...nieuweWoningSchema, }, session_id: { type: "string", description: "Optioneel sessie-ID vanuit n8n (voor logging).", }, }, required: [ ...baseIntakeRequired, "nieuwe_woning", ], }, }, // Tool 6: Opzet hypotheek - Doorstromers { name: "opzet_hypotheek_doorstromer", description: "Berekent de hypotheekopzet voor doorstromers met bestaande woning. Output: benodigd bedrag, financiering per component en maandlasten (bestaand versus nieuw).", inputSchema: { type: "object", description: `Gebruik basisintake, huidige woning en bestaande leningdelen; zie ${OPZET_GUIDE_URI} voor detailvelden en defaults.`, properties: { ...baseIntakeProperties, eigen_vermogen: { type: "number", description: "Optioneel beschikbaar eigen geld in euro's.", default: 0, }, 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: [ ...baseIntakeRequired, "waarde_huidige_woning", "bestaande_hypotheek", "nieuwe_woning", ], }, }, // Tool 7: Opzet hypotheek - Uitgebreid { name: "opzet_hypotheek_uitgebreid", description: `GEAVANCEERDE opzet hypotheek berekening met VOLLEDIGE controle over alle parameters. Geschikt voor zowel starters als doorstromers. **Output bevat alles van de starter/doorstromer tools, plus:** - Mogelijkheid om elk leningdeel handmatig te definiëren - Custom rentepercentages, looptijden en rentevast periodes - NHG, energielabel en verbouwing/duurzaamheidsbudget in één scenario - Volledige balans check en praktische toelichtingen Gebruik deze tool alleen wanneer afwijkende parameters nodig zijn; anders de specifieke starter/doorstromer varianten gebruiken.`, inputSchema: { type: "object", properties: { inkomen_aanvrager: { type: "number", description: "Bruto jaarinkomen van de hoofdaanvrager in euro's", }, geboortedatum_aanvrager: { type: "string", description: "Geboortedatum aanvrager in formaat YYYY-MM-DD", }, heeft_partner: { type: "boolean", description: "Heeft de aanvrager een partner die mee aanvraagt?", }, inkomen_partner: { type: "number", description: "OPTIONEEL - Bruto jaarinkomen van de partner in euro's.", }, geboortedatum_partner: { type: "string", description: "OPTIONEEL - Geboortedatum partner in formaat YYYY-MM-DD.", }, verplichtingen_pm: { type: "number", description: "Maandelijkse verplichtingen in euro's.", default: 0, }, eigen_vermogen: { type: "number", description: "Eigen geld beschikbaar in euro's.", default: 0, }, is_doorstromer: { type: "boolean", description: "Is dit een doorstromer met bestaande woning en hypotheek?", }, waarde_huidige_woning: { type: "number", description: "OPTIONEEL - Alleen voor doorstromers: huidige woningwaarde in euro's", }, bestaande_hypotheek: { type: "object", description: "OPTIONEEL - Alleen voor doorstromers: gegevens van de bestaande hypotheek.", properties: { leningdelen: { type: "array", items: { type: "object", properties: { huidige_schuld: { type: "number", description: "Restschuld in euro's", }, huidige_rente: { type: "number", description: "Rente als decimaal (bijv. 0.041 voor 4.1%)", }, 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: "Type hypotheek", enum: ["annuiteit", "lineair", "aflossingsvrij"], }, }, required: ["huidige_schuld", "huidige_rente", "resterende_looptijd_in_maanden", "rentevasteperiode_maanden", "hypotheekvorm"], }, }, }, required: ["leningdelen"], }, nieuwe_woning: { type: "object", description: "Gegevens van de nieuwe woning die gekocht wordt", properties: { waarde_woning: { type: "number", description: "Koopsom van de nieuwe woning in euro's", }, bedrag_verbouwen: { type: "number", description: "OPTIONEEL - Geschatte kosten voor verbouwing/meerwerk in euro's.", default: 0, }, bedrag_verduurzamen: { type: "number", description: "OPTIONEEL - Geschatte kosten voor verduurzaming in euro's.", default: 0, }, kosten_percentage: { type: "number", description: "OPTIONEEL - Koperkosten als decimaal (bijv. 0.05 voor 5%). Standaard: 0.05", default: 0.05, }, energielabel: { type: "string", description: "OPTIONEEL - Energielabel van de nieuwe woning.", enum: ["A++++ (met garantie)", "A++++", "A+++", "A++", "A+", "A", "B", "C", "D", "E", "F", "G"], }, }, required: ["waarde_woning"], }, nieuwe_lening: { type: "object", description: "OPTIONEEL - Specifieke parameters voor de nieuwe lening. Gebruik deze sectie om looptijd, rentevast periode, NHG of renteklassen aan te passen.", properties: { looptijd_jaren: { type: "number", description: "Looptijd van de hypotheek in JAREN. Standaard: 30 jaar. Voorbeelden: 20, 25, 30", default: 30, }, rentevast_periode_jaren: { type: "number", description: "Rentevaste periode in JAREN. Standaard: 10 jaar. Voorbeelden: 5, 10, 15, 20", default: 10, }, nhg: { type: "boolean", description: "Nationale Hypotheek Garantie aanvragen? Standaard: false", default: false, }, renteklassen: { type: "array", description: "OPTIONEEL - Custom renteklassen met specifieke LTV-grenzen en rentepercentages. Alleen invullen als je specifieke renteklassen wilt definiëren.", items: { type: "object", properties: { naam: { type: "string", description: "Naam van de renteklasse (bijv. 'NHG 0-200', 'Niet-NHG 75-90')", }, lowerbound_ltv_pct: { type: "number", description: "Ondergrens LTV in procenten (bijv. 0.0, 75.0)", }, higherbound_ltv_pct: { type: "number", description: "Bovengrens LTV in procenten (bijv. 75.0, 200.0)", }, nhg: { type: "boolean", description: "Is dit een NHG renteklasse?", }, rente_jaarlijks_pct: { type: "number", description: "Rentepercentage als getal (bijv. 3.2 voor 3.2%, 4.0 voor 4.0%)", }, }, required: ["naam", "lowerbound_ltv_pct", "higherbound_ltv_pct", "nhg", "rente_jaarlijks_pct"], }, }, }, }, session_id: { type: "string", description: "OPTIONEEL - Sessie ID voor het traceren van de conversatie. Haal deze waarde uit de n8n chat trigger: 'When chat message received' -> sessionId variabele.", }, }, required: [ "inkomen_aanvrager", "geboortedatum_aanvrager", "heeft_partner", "nieuwe_woning", ], }, }, ], }; }); server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: listResources(), })); server.setRequestHandler(ReadResourceRequestSchema, async (request) => ({ contents: [readResource(request.params.uri)], })); server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: listPrompts(), })); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const args = request.params.arguments ? { ...request.params.arguments } : undefined; const prompt = getPrompt(request.params.name, args as Record<string, unknown> | undefined); return { description: prompt.description, messages: prompt.messages, }; }); // Functie om response mooi te formatteren function formatResponse(data: any, toolName: string): string { let output = ""; if (toolName === "bereken_hypotheek_starter") { output += "🏠 **HYPOTHEEKBEREKENING VOOR STARTER**\n\n"; if (data.resultaat && Array.isArray(data.resultaat)) { data.resultaat.forEach((resultaat: any, index: number) => { const scenario = resultaat.resultaat_omschrijving || `Scenario ${index + 1}`; const hypotheekData = resultaat.gebruikte_hypotheekgegevens?.opzet_nieuwe_hypotheek?.[0]; output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `📊 **${scenario}**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; output += `💰 **Maximale hypotheek:** €${resultaat.maximaal_bedrag?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `📈 **Maandlast:** €${resultaat.bruto_maandlasten_nieuwe_lening?.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 2}) || 'N/A'}\n`; if (hypotheekData) { output += `🏦 **Hypotheekvorm:** ${hypotheekData.hypotheekvorm || 'N/A'}\n`; output += `⏱️ **Looptijd:** ${hypotheekData.looptijd_maanden ? (hypotheekData.looptijd_maanden / 12).toFixed(0) + ' jaar' : 'N/A'}\n`; output += `🔒 **Rentevaste periode:** ${hypotheekData.rentevastperiode_maanden ? (hypotheekData.rentevastperiode_maanden / 12).toFixed(0) + ' jaar' : 'N/A'}\n`; output += `📊 **Rentepercentage:** ${hypotheekData.rente ? (hypotheekData.rente * 100).toFixed(2) + '%' : 'N/A'}\n`; } output += `⚡ **Energielabel:** ${resultaat.gebruikte_hypotheekgegevens?.energielabel || 'N/A'}\n`; output += `🛡️ **NHG:** ${resultaat.gebruikte_hypotheekgegevens?.nhg_toegepast ? 'Ja' : 'Nee'}\n\n`; }); } // Voeg energielabel info toe als beschikbaar if (data.energielabel_verschil) { output += `\n💡 **Energielabel impact:**\n`; output += `${data.energielabel_verschil.opmerking}\n\n`; if (data.energielabel_verschil.verschil_per_label) { output += `Verschil per energielabel:\n`; Object.entries(data.energielabel_verschil.verschil_per_label).forEach(([label, bedrag]: [string, any]) => { output += `• ${label}: €${bedrag?.toLocaleString('nl-NL') || '0'} ${bedrag > 0 ? 'extra' : ''}\n`; }); } } } else if (toolName === "bereken_hypotheek_doorstromer") { output += "🏠 **HYPOTHEEKBEREKENING VOOR DOORSTROMER**\n\n"; if (data.resultaat && Array.isArray(data.resultaat)) { data.resultaat.forEach((resultaat: any, index: number) => { const scenario = resultaat.resultaat_omschrijving || `Scenario ${index + 1}`; const hypotheekData = resultaat.gebruikte_hypotheekgegevens?.opzet_nieuwe_hypotheek?.[0]; output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `📊 **${scenario}**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; output += `💰 **Maximale nieuwe hypotheek:** €${resultaat.maximaal_bedrag?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `📈 **Nieuwe maandlast:** €${resultaat.bruto_maandlasten_nieuwe_lening?.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 2}) || 'N/A'}\n`; output += `💵 **Overwaarde:** €${resultaat.overwaarde?.toLocaleString('nl-NL') || 'N/A'}\n`; if (hypotheekData) { output += `🏦 **Hypotheekvorm:** ${hypotheekData.hypotheekvorm || 'N/A'}\n`; output += `⏱️ **Looptijd:** ${hypotheekData.looptijd_maanden ? (hypotheekData.looptijd_maanden / 12).toFixed(0) + ' jaar' : 'N/A'}\n`; output += `🔒 **Rentevaste periode:** ${hypotheekData.rentevastperiode_maanden ? (hypotheekData.rentevastperiode_maanden / 12).toFixed(0) + ' jaar' : 'N/A'}\n`; output += `📊 **Rentepercentage:** ${hypotheekData.rente ? (hypotheekData.rente * 100).toFixed(2) + '%' : 'N/A'}\n`; } output += `⚡ **Energielabel:** ${resultaat.gebruikte_hypotheekgegevens?.energielabel || 'N/A'}\n`; output += `🛡️ **NHG:** ${resultaat.gebruikte_hypotheekgegevens?.nhg_toegepast ? 'Ja' : 'Nee'}\n\n`; if (resultaat.bestaande_situatie) { output += `\n**🏠 Huidige situatie:**\n`; output += `• Woningwaarde: €${resultaat.bestaande_situatie.woningwaarde?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `• Totale restschuld: €${resultaat.bestaande_situatie.totale_restschuld?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `• Huidige maandlast: €${resultaat.bestaande_situatie.huidige_maandlast?.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 2}) || 'N/A'}\n\n`; } }); } // Voeg energielabel info toe als beschikbaar if (data.energielabel_verschil) { output += `\n💡 **Energielabel impact:**\n`; output += `${data.energielabel_verschil.opmerking}\n\n`; if (data.energielabel_verschil.verschil_per_label) { output += `Verschil per energielabel:\n`; Object.entries(data.energielabel_verschil.verschil_per_label).forEach(([label, bedrag]: [string, any]) => { output += `• ${label}: €${bedrag?.toLocaleString('nl-NL') || '0'} ${bedrag > 0 ? 'extra' : ''}\n`; }); } } } else if (toolName === "bereken_hypotheek_uitgebreid") { output += "🏠 **UITGEBREIDE HYPOTHEEKBEREKENING**\n\n"; if (data.resultaat && Array.isArray(data.resultaat)) { data.resultaat.forEach((resultaat: any, index: number) => { const scenario = resultaat.resultaat_omschrijving || `Scenario ${index + 1}`; const hypotheekData = resultaat.gebruikte_hypotheekgegevens?.opzet_nieuwe_hypotheek?.[0]; output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `📊 **${scenario}**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; output += `💰 **Maximale hypotheek:** €${resultaat.maximaal_bedrag?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `📈 **Maandlast:** €${resultaat.bruto_maandlasten_nieuwe_lening?.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 2}) || 'N/A'}\n`; if (resultaat.overwaarde !== undefined) { output += `💵 **Overwaarde:** €${resultaat.overwaarde?.toLocaleString('nl-NL') || 'N/A'}\n`; } if (hypotheekData) { output += `🏦 **Hypotheekvorm:** ${hypotheekData.hypotheekvorm || 'N/A'}\n`; output += `⏱️ **Looptijd:** ${hypotheekData.looptijd_maanden ? (hypotheekData.looptijd_maanden / 12).toFixed(0) + ' jaar' : 'N/A'}\n`; output += `🔒 **Rentevaste periode:** ${hypotheekData.rentevastperiode_maanden ? (hypotheekData.rentevastperiode_maanden / 12).toFixed(0) + ' jaar' : 'N/A'}\n`; output += `📊 **Rentepercentage:** ${hypotheekData.rente ? (hypotheekData.rente * 100).toFixed(2) + '%' : 'N/A'}\n`; } output += `⚡ **Energielabel:** ${resultaat.gebruikte_hypotheekgegevens?.energielabel || 'N/A'}\n`; output += `🛡️ **NHG:** ${resultaat.gebruikte_hypotheekgegevens?.nhg_toegepast ? 'Ja' : 'Nee'}\n\n`; if (resultaat.bestaande_situatie) { output += `\n**🏠 Huidige situatie:**\n`; output += `• Woningwaarde: €${resultaat.bestaande_situatie.woningwaarde?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `• Totale restschuld: €${resultaat.bestaande_situatie.totale_restschuld?.toLocaleString('nl-NL') || 'N/A'}\n`; output += `• Huidige maandlast: €${resultaat.bestaande_situatie.huidige_maandlast?.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 2}) || 'N/A'}\n\n`; } }); } // Voeg energielabel info toe als beschikbaar if (data.energielabel_verschil) { output += `\n💡 **Energielabel impact:**\n`; output += `${data.energielabel_verschil.opmerking}\n\n`; if (data.energielabel_verschil.verschil_per_label) { output += `Verschil per energielabel:\n`; Object.entries(data.energielabel_verschil.verschil_per_label).forEach(([label, bedrag]: [string, any]) => { output += `• ${label}: €${bedrag?.toLocaleString('nl-NL') || '0'} ${bedrag > 0 ? 'extra' : ''}\n`; }); } } } // Formattering voor opzet hypotheek tools if (toolName.startsWith("opzet_hypotheek_")) { const toolType = toolName.replace("opzet_hypotheek_", "").toUpperCase(); const isDoorstromer = toolName.includes("doorstromer"); output += `🏠 **OPZET HYPOTHEEK - ${toolType}**\n\n`; if (data.resultaat) { const resultaat = data.resultaat; // ======================================================================== // SECTIE 1: BENODIGD BEDRAG // ======================================================================== output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `📊 **TOTAAL BENODIGD BEDRAG**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; if (resultaat.Benodigd_bedrag) { output += `🏡 Koopsom woning: €${resultaat.Benodigd_bedrag.Woning_koopsom?.toLocaleString('nl-NL') || 'N/A'}\n`; if (resultaat.Benodigd_bedrag.Verbouwingskosten_meerwerk > 0) { output += `🔨 Verbouwing/meerwerk: €${resultaat.Benodigd_bedrag.Verbouwingskosten_meerwerk?.toLocaleString('nl-NL') || 'N/A'}\n`; } if (resultaat.Benodigd_bedrag.Verduurzamingskosten > 0) { output += `♻️ Verduurzaming: €${resultaat.Benodigd_bedrag.Verduurzamingskosten?.toLocaleString('nl-NL') || 'N/A'}\n`; } output += `💼 Kosten koper: €${resultaat.Benodigd_bedrag.Kosten?.toLocaleString('nl-NL') || 'N/A'}\n`; // Bereken totaal (fallback als API het niet geeft) const totaalBenodigd = resultaat.Benodigd_bedrag.Totaal_benodigd || ((resultaat.Benodigd_bedrag.Woning_koopsom || 0) + (resultaat.Benodigd_bedrag.Verbouwingskosten_meerwerk || 0) + (resultaat.Benodigd_bedrag.Verduurzamingskosten || 0) + (resultaat.Benodigd_bedrag.Kosten || 0)); output += `${'─'.repeat(45)}\n`; output += `💰 **TOTAAL BENODIGD: €${totaalBenodigd.toLocaleString('nl-NL')}**\n\n`; } // ======================================================================== // SECTIE 2: FINANCIERING (met breakdown voor doorstromers) // ======================================================================== output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `💵 **FINANCIERING**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; if (resultaat.Financiering) { // Voor doorstromers: toon bestaande hypotheek if (isDoorstromer && resultaat.Financiering.Bestaande_hypotheek) { const bestaand = resultaat.Financiering.Bestaande_hypotheek; output += `🔄 Bestaande hypotheek (over te sluiten): €${bestaand.Totaal_schuld?.toLocaleString('nl-NL') || 'N/A'}\n`; } else if (isDoorstromer) { // Fallback: bereken uit opzet_nieuwe_hypotheek let bestaandeSchuld = 0; if (resultaat.gebruikte_hypotheekgegevens?.opzet_nieuwe_hypotheek) { resultaat.gebruikte_hypotheekgegevens.opzet_nieuwe_hypotheek.forEach((deel: any) => { if (deel.type === 'bestaand_leningdeel') { bestaandeSchuld += deel.hypotheekbedrag || 0; } }); } if (bestaandeSchuld > 0) { output += `🔄 Bestaande hypotheek (over te sluiten): €${bestaandeSchuld.toLocaleString('nl-NL')}\n`; } } // Nieuwe hypotheek const nieuweHypotheek = resultaat.Financiering.Nieuwe_hypotheek || resultaat.Financiering.Hypotheek || 0; if (isDoorstromer && nieuweHypotheek > 0) { output += `🆕 Nieuwe hypotheek (extra): €${nieuweHypotheek.toLocaleString('nl-NL')}\n`; } else { output += `🏦 Hypotheek: €${nieuweHypotheek.toLocaleString('nl-NL')}\n`; } // Overwaarde if (resultaat.Financiering.Overwaarde !== undefined && resultaat.Financiering.Overwaarde > 0) { output += `📈 Overwaarde huidige woning: €${resultaat.Financiering.Overwaarde?.toLocaleString('nl-NL')}\n`; } // Eigen geld const eigenGeld = resultaat.Financiering.Eigen_geld || 0; if (eigenGeld > 0) { output += `💎 Eigen geld: €${eigenGeld.toLocaleString('nl-NL')}\n`; } // Bereken totaal financiering (fallback als API het niet geeft) let totaalFinanciering = resultaat.Financiering.Totaal_financiering; if (!totaalFinanciering) { const bestaandBedrag = resultaat.Financiering.Bestaande_hypotheek?.Totaal_schuld || 0; totaalFinanciering = bestaandBedrag + nieuweHypotheek + (resultaat.Financiering.Overwaarde || 0) + eigenGeld; } output += `${'─'.repeat(45)}\n`; output += `💵 **TOTAAL FINANCIERING: €${totaalFinanciering.toLocaleString('nl-NL')}**\n\n`; // Balans check if (resultaat.Benodigd_bedrag) { const totaalBenodigd = resultaat.Benodigd_bedrag.Totaal_benodigd || ((resultaat.Benodigd_bedrag.Woning_koopsom || 0) + (resultaat.Benodigd_bedrag.Verbouwingskosten_meerwerk || 0) + (resultaat.Benodigd_bedrag.Verduurzamingskosten || 0) + (resultaat.Benodigd_bedrag.Kosten || 0)); const verschil = Math.abs(totaalFinanciering - totaalBenodigd); if (verschil < 1) { output += `✅ **Balans: Financiering dekt benodigd bedrag** ✓\n\n`; } else if (totaalFinanciering < totaalBenodigd) { output += `⚠️ **Let op: Tekort van €${verschil.toLocaleString('nl-NL')}**\n`; output += ` → Meer eigen geld of hogere hypotheek nodig\n\n`; } else { output += `ℹ️ **Overschot van €${verschil.toLocaleString('nl-NL')}**\n`; output += ` → Kan als buffer/reserve dienen\n\n`; } } } // ======================================================================== // SECTIE 3: MAANDLASTEN (met breakdown voor doorstromers) // ======================================================================== output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `📊 **MAANDLASTEN**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; // Check of API nieuwe Maandlasten object heeft if (resultaat.Maandlasten) { // NIEUW: API geeft breakdown if (isDoorstromer) { output += `🔄 Bestaande hypotheek: €${Math.round(resultaat.Maandlasten.Bestaande_hypotheek || 0).toLocaleString('nl-NL')}/maand\n`; output += `🆕 Nieuwe hypotheek (extra): €${Math.round(resultaat.Maandlasten.Nieuwe_hypotheek || 0).toLocaleString('nl-NL')}/maand\n`; output += `${'─'.repeat(45)}\n`; output += `💰 **TOTAAL MAANDLAST: €${Math.round(resultaat.Maandlasten.Totaal).toLocaleString('nl-NL')}/maand**\n\n`; const verschil = resultaat.Maandlasten.Verschil || 0; if (verschil > 0) { output += `📈 **Stijging maandlast: +€${Math.round(verschil).toLocaleString('nl-NL')}/maand**\n\n`; } else if (verschil < 0) { output += `📉 **Daling maandlast: -€${Math.round(Math.abs(verschil)).toLocaleString('nl-NL')}/maand**\n\n`; } else { output += `➡️ **Maandlast blijft gelijk**\n\n`; } } else { // Starter: alleen totaal output += `💰 **Bruto maandlast: €${Math.round(resultaat.Maandlasten.Totaal).toLocaleString('nl-NL')}/maand**\n\n`; } } else { // FALLBACK: oude API response zonder breakdown if (isDoorstromer && resultaat.gebruikte_hypotheekgegevens?.opzet_nieuwe_hypotheek) { // Bereken breakdown handmatig let bestaandeMaandlast = 0; let nieuweMaandlast = 0; resultaat.gebruikte_hypotheekgegevens.opzet_nieuwe_hypotheek.forEach((deel: any) => { const bedrag = deel.hypotheekbedrag || 0; const rente = deel.rente || 0; const geschatteMaandlast = (bedrag * rente) / 12; if (deel.type === 'bestaand_leningdeel') { bestaandeMaandlast += geschatteMaandlast; } else { nieuweMaandlast += geschatteMaandlast; } }); output += `🔄 Bestaande hypotheek: €${Math.round(bestaandeMaandlast).toLocaleString('nl-NL')}/maand (geschat)\n`; output += `🆕 Nieuwe hypotheek (extra): €${Math.round(nieuweMaandlast).toLocaleString('nl-NL')}/maand (geschat)\n`; output += `${'─'.repeat(45)}\n`; output += `💰 **TOTAAL MAANDLAST: €${(resultaat.bruto_maandlasten_nieuwe_lening || 0).toLocaleString('nl-NL', {minimumFractionDigits: 2})}/maand**\n\n`; const verschil = (resultaat.bruto_maandlasten_nieuwe_lening || 0) - Math.round(bestaandeMaandlast); if (verschil > 50) { output += `📈 **Stijging maandlast: +€${Math.round(verschil).toLocaleString('nl-NL')}/maand** (geschat)\n\n`; } } else { // Starter: alleen totaal output += `💰 **Bruto maandlast: €${(resultaat.bruto_maandlasten_nieuwe_lening || 0).toLocaleString('nl-NL', {minimumFractionDigits: 2})}/maand**\n\n`; } } // ======================================================================== // SECTIE 4: HYPOTHEEKDETAILS // ======================================================================== if (resultaat.gebruikte_hypotheekgegevens) { output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `🔍 **HYPOTHEEKDETAILS**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; output += `⚡ Energielabel: ${resultaat.gebruikte_hypotheekgegevens.energielabel || 'N/A'}`; if (resultaat.gebruikte_hypotheekgegevens.energielabel_toeslag > 0) { output += ` (+€${resultaat.gebruikte_hypotheekgegevens.energielabel_toeslag?.toLocaleString('nl-NL')} extra leencapaciteit)`; } output += `\n`; output += `🛡️ NHG: ${resultaat.gebruikte_hypotheekgegevens.nhg_toegepast ? 'Ja (lagere rente!)' : 'Nee'}\n\n`; if (resultaat.gebruikte_hypotheekgegevens.opzet_nieuwe_hypotheek && Array.isArray(resultaat.gebruikte_hypotheekgegevens.opzet_nieuwe_hypotheek)) { output += `**📋 Opzet hypotheek:** `; resultaat.gebruikte_hypotheekgegevens.opzet_nieuwe_hypotheek.forEach((deel: any, index: number) => { const deelType = deel.type === 'bestaand_leningdeel' ? '🔄 Bestaand deel' : '🆕 Nieuw deel'; output += ` ${deelType} ${index + 1}: `; output += ` • Bedrag: €${deel.hypotheekbedrag?.toLocaleString('nl-NL') || 'N/A'} `; output += ` • Rente: ${deel.rente ? (deel.rente * 100).toFixed(2) + '%' : 'N/A'} `; output += ` • Type: ${deel.hypotheekvorm || 'N/A'} `; if (deel.type === 'bestaand_leningdeel') { output += ` • Resterende looptijd: ${deel.resterende_looptijd_maanden ? (deel.resterende_looptijd_maanden / 12).toFixed(0) + ' jaar' : 'N/A'} `; output += ` • Nog rentevast: ${deel.rentevastperiode_maanden ? (deel.rentevastperiode_maanden / 12).toFixed(0) + ' jaar' : 'Variabel'} `; } else { output += ` • Looptijd: ${deel.looptijd_maanden ? (deel.looptijd_maanden / 12).toFixed(0) + ' jaar' : 'N/A'} `; output += ` • Rentevast: ${deel.rentevastperiode_maanden ? (deel.rentevastperiode_maanden / 12).toFixed(0) + ' jaar' : 'Variabel'} `; } }); output += ` `; } } // ======================================================================== // SECTIE 5: PRAKTISCHE TOELICHTING // ======================================================================== output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `💡 **PRAKTISCHE TOELICHTING**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; // Context-aware feedback if (isDoorstromer) { if (resultaat.Financiering?.Overwaarde && resultaat.Financiering.Overwaarde > 50000) { output += `✓ U heeft een substantiële overwaarde van €${resultaat.Financiering.Overwaarde.toLocaleString('nl-NL')}. Dit geeft u ruimte voor de nieuwe woning of als buffer.\n`; } const totaalMaandlast = resultaat.Maandlasten?.Totaal || resultaat.bruto_maandlasten_nieuwe_lening || 0; if (totaalMaandlast > 2000) { output += `⚠️ Nieuwe maandlast is substantieel (€${Math.round(totaalMaandlast).toLocaleString('nl-NL')}). Zorg dat dit binnen uw budget past.\n`; } const verschil = resultaat.Maandlasten?.Verschil || 0; if (verschil > 500) { output += `ℹ️ Maandlast stijgt met €${Math.round(verschil).toLocaleString('nl-NL')}. Check of dit duurzaam is op lange termijn.\n`; } } else { // Starter specifieke tips if (resultaat.Financiering?.Nieuwe_hypotheek && resultaat.Benodigd_bedrag?.Woning_koopsom) { const hypotheek = resultaat.Financiering.Nieuwe_hypotheek || resultaat.Financiering.Hypotheek; const ltv = (hypotheek / resultaat.Benodigd_bedrag.Woning_koopsom) * 100; if (ltv > 100) { output += `⚠️ U financiert ${ltv.toFixed(0)}% (boven de woningwaarde). Dit betekent geen NHG. Overweeg meer eigen geld in te brengen.\n`; } else if (ltv > 95) { output += `ℹ️ U financiert ${ltv.toFixed(0)}% van de woningwaarde. Hoge financiering betekent vaak hogere rente.\n`; } else if (ltv < 90) { output += `✓ U financiert ${ltv.toFixed(0)}% - dit is gunstig voor uw rente.\n`; } } const eigenGeld = resultaat.Financiering?.Eigen_geld || 0; if (eigenGeld < 10000) { output += `ℹ️ Met meer eigen geld kunt u vaak een betere rente krijgen. Overweeg eventuele spaargeld of giften.\n`; } } // Energielabel tip if (resultaat.gebruikte_hypotheekgegevens?.energielabel) { const label = resultaat.gebruikte_hypotheekgegevens.energielabel; if (label === 'D' || label === 'E' || label === 'F' || label === 'G') { output += `💡 Tip: Met verduurzaming naar label A++ of hoger kunt u tot €30.000 extra lenen tegen een lagere rente!\n`; } else if (label.startsWith('A')) { output += `✓ Uitstekend energielabel! Dit geeft u extra leencapaciteit.\n`; } } output += `\n`; } // Disclaimers if (data.extra_informatie && data.extra_informatie.disclaimers) { output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `⚠️ **DISCLAIMERS**\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; data.extra_informatie.disclaimers.forEach((disclaimer: string) => { output += `• ${disclaimer}\n`; }); output += `\n`; } } return output; } // Handler voor tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const startTime = Date.now(); const toolName = request.params?.name ?? 'unknown_tool'; const handler = TOOL_HANDLERS[toolName]; const sessionId = extractSessionId(request.params?.arguments); if (!handler) { const error = new ValidationError(ErrorCode.INVALID_INPUT, `Onbekende tool: ${toolName}`, 'tool'); recordToolCall(toolName, Date.now() - startTime, false); try { recordValidationError(error.code); } catch { /* ignore metrics failure */ } return errorResponse(error, sessionId); } try { const response = await handler(request); recordToolCall(toolName, Date.now() - startTime, true); return response; } catch (error) { recordToolCall(toolName, Date.now() - startTime, false); if (error instanceof ValidationError) { try { recordValidationError(error.code); } catch { /* ignore metrics failure */ } } return errorResponse(error, sessionId); } }); // Start de server met stdio transport async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error(`Hypotheek MCP Server v${config.serverVersion} klaar voor gebruik (stdio).`); console.error(`Beschikbare tools: ${Object.keys(TOOL_HANDLERS).join(', ')}`); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });

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/Test'

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