Skip to main content
Glama
index.ts20.9 kB
import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { z } from 'zod'; import { McpError, ErrorCode as McpErrorCode, ResourceSchema, TextResourceContentsSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ErrorCode } from '../types/index.js'; const MARKDOWN_MIME = 'text/markdown; charset=utf-8; lang=nl-NL'; const require = createRequire(import.meta.url); const { version: packageVersion } = require('../../package.json') as { version: string }; type Resource = z.infer<typeof ResourceSchema>; type TextResourceContents = z.infer<typeof TextResourceContentsSchema>; const projectRoot = resolve(fileURLToPath(new URL('../..', import.meta.url))); export interface ErrorGuideEntry { title: string; typicalCause: string; resolutionSteps: string[]; badExample: string; goodExample: string; } export const ERROR_GUIDE: Record<ErrorCode, ErrorGuideEntry> = { [ErrorCode.INVALID_INPUT]: { title: 'Algemene invoerfout', typicalCause: 'Ontbrekende verplichte velden of combinaties die niet door validatie komen.', resolutionSteps: [ 'Herlees de foutmelding en identificeer welk veld of combinatie ontbreekt.', 'Vraag de gebruiker om alle verplichte velden opnieuw te bevestigen.', 'Controleer of er geen onbekende of extra velden worden meegestuurd.' ], badExample: '{ "heeft_partner": true }', goodExample: '{ "heeft_partner": false, "geboortedatum": "1990-05-15", "bruto_inkomen": 42000 }' }, [ErrorCode.INVALID_DATE_FORMAT]: { title: 'Onjuist datumformaat', typicalCause: 'Datum is aangeleverd in DD-MM-YYYY of natuurlijke taal in plaats van ISO-formaat.', resolutionSteps: [ 'Vraag de gebruiker expliciet om de geboortedatum in het formaat YYYY-MM-DD.', 'Herhaal het juiste formaat en geef een bestaand voorbeeld.', 'Controleer of de datum geen toekomst ligt en een geldige kalenderdatum is.' ], badExample: '"15-05-1990"', goodExample: '"1990-05-15"' }, [ErrorCode.AGE_OUT_OF_RANGE]: { title: 'Leeftijd buiten toegestane bandbreedte', typicalCause: 'De berekende leeftijd is jonger dan 18 of ouder dan 75 jaar op het moment van aanvragen.', resolutionSteps: [ 'Controleer of de geboortedatum correct is omgerekend vanuit de opgegeven leeftijd.', 'Vraag of de gebruiker zeker weet dat hij/zij voldoet aan de leeftijdseisen van de kredietverstrekker.', 'Indien foutief omgezet: bereken opnieuw met een geboortedatum waarbij de gebruiker morgen jarig is.' ], badExample: '"2010-01-01" (15 jaar)', goodExample: '"1990-05-15" (35 jaar)' }, [ErrorCode.INCOME_OUT_OF_RANGE]: { title: 'Inkomen buiten bandbreedte', typicalCause: 'Bruto jaarinkomen is negatief, onrealistisch hoog of ingevoerd in honderden i.p.v. euro’s.', resolutionSteps: [ 'Vraag het bruto jaarinkomen opnieuw en benadruk dat het bedrag in euro’s per jaar moet zijn.', 'Noem de onderste (0) en bovenste (1.000.000) grenzen duidelijk.', 'Indien er een partner is: check of beide inkomens gescheiden zijn opgegeven.' ], badExample: 'inkomen: -500', goodExample: 'inkomen: 42000' }, [ErrorCode.RENTEVAST_EXCEEDS_LOOPTIJD]: { title: 'Rentevaste periode langer dan looptijd', typicalCause: 'Rentevast-periode in maanden is groter dan de resterende looptijd in maanden.', resolutionSteps: [ 'Leg uit dat de rentevasteperiode altijd korter of gelijk moet zijn aan de looptijd.', 'Vraag naar de resterende looptijd en gebruik die als bovengrens.', 'Normaliseer invoer naar maanden (bijv. 10 jaar = 120 maanden).' ], badExample: 'looptijd: 120, rentevast: 180', goodExample: 'looptijd: 240, rentevast: 120' }, [ErrorCode.INVALID_HYPOTHEEKVORM]: { title: 'Onbekende hypotheekvorm', typicalCause: 'Hypotheekvorm bevat accenten of varianten die niet ondersteund zijn.', resolutionSteps: [ 'Noem de drie toegestane waarden en hun schrijfwijze: annuiteit, lineair, aflossingsvrij.', 'Verwijder accenten of kapitalisatie voor je het veld invult.', 'Bevestig met de gebruiker welke vorm bedoeld is voordat je normaliseert.' ], badExample: '"Annuïteit"', goodExample: '"annuiteit"' }, [ErrorCode.INVALID_ENERGIELABEL]: { title: 'Energielabel onbekend', typicalCause: 'Label is in lowercase, mist plus-tekens of bevat tekst tussen haakjes die niet ondersteund is.', resolutionSteps: [ 'Som de toegestane waarden op en benadruk hoofdletters.', 'Voeg ontbrekende plus-tekens toe of verwijder extra tekst behalve bij "A++++ (met garantie)".', 'Vraag desnoods naar de letter én of er plus-tekens bij horen.' ], badExample: '"a+++"', goodExample: '"A+++"' }, [ErrorCode.PARTNER_DATA_INCOMPLETE]: { title: 'Partnergegevens onvolledig', typicalCause: 'heeft_partner staat op true maar inkomen of geboortedatum van partner ontbreekt.', resolutionSteps: [ 'Vraag expliciet naar het bruto partnerinkomen in euro per jaar.', 'Vraag naar geboortedatum partner in YYYY-MM-DD.', 'Indien geen partner meedoet: zet heeft_partner op false en verwijder partner velden.' ], badExample: 'heeft_partner: true, inkomen_partner: ontbreekt', goodExample: 'heeft_partner: true, inkomen_partner: 38000, geboortedatum_partner: "1994-09-12"' }, [ErrorCode.TOO_MANY_LENINGDELEN]: { title: 'Te veel leningdelen aangeleverd', typicalCause: 'Meer dan 10 leningdelen in bestaande hypotheek of duplicaten.', resolutionSteps: [ 'Beperk het aantal leningdelen tot maximaal 10.', 'Combineer delen die identieke rente en looptijd hebben tot één aggregaat.', 'Vraag welke delen essentieel zijn voor de berekening en verwijder rest.' ], badExample: 'leningdelen: 14 items', goodExample: 'leningdelen: 3 items (samengevoegd per type)' }, [ErrorCode.WONING_VALUE_OUT_OF_RANGE]: { title: 'Woningwaarde buiten bandbreedte', typicalCause: 'Woningwaarde lager dan €50.000 of hoger dan €5.000.000 of opgegeven in duizenden.', resolutionSteps: [ 'Vraag de exacte woningwaarde in euro’s (zonder punten of komma’s voor duizendtallen).', 'Herhaal de onderste en bovenste grens en vraag of er een taxatie beschikbaar is.', 'Normaliseer bedragen die vermoedelijk in duizenden zijn opgegeven door ×1000 te doen.' ], badExample: 'woningwaarde: 280 (bedoeld €280.000)', goodExample: 'woningwaarde: 280000' }, [ErrorCode.API_TIMEOUT]: { title: 'Backend-timeout', typicalCause: 'Replit API reageerde niet binnen de ingestelde timeout.', resolutionSteps: [ 'Informeer de gebruiker dat de backend traag reageert.', 'Wacht minimaal 30 seconden voor een retry.', 'Herstart indien probleem aanhoudt en controleer statuspagina.' ], badExample: 'Direct opnieuw dezelfde call spammen', goodExample: 'Een retry met exponential backoff en duidelijke melding aan gebruiker' }, [ErrorCode.API_RATE_LIMIT]: { title: 'Rate limit bereikt', typicalCause: 'Meer dan 100 requests per sessie per minuut.', resolutionSteps: [ 'Communiceer de wachttijd (60 seconden) naar de gebruiker.', 'Gebruik hetzelfde session_id en plan calls zodat de limiet niet opnieuw geraakt wordt.', 'Cache resultaten indien dezelfde vraag direct opnieuw wordt gesteld.' ], badExample: 'Onmiddellijk opnieuw proberen zonder vertraging', goodExample: 'Gebruiker informeren en na 60 seconden opnieuw proberen' }, [ErrorCode.API_ERROR]: { title: 'Algemene backendfout', typicalCause: 'De externe hypotheek-API gaf een 5xx respons of onverwachte payload.', resolutionSteps: [ 'Log correlation_id en statuscode zonder gevoelige gegevens.', 'Probeer het verzoek na een korte wachttijd opnieuw (maximaal 3 pogingen).', 'Escalatie: controleer backend-status of neem contact op met het platformteam.' ], badExample: 'Herhaaldelijk direct opnieuw proberen zonder logging', goodExample: '"Backend gaf 502 terug. Ik probeer het over 15 seconden opnieuw (poging 2/3)."' }, [ErrorCode.UNKNOWN_ERROR]: { title: 'Onbekende fout', typicalCause: 'Onverwachte situatie (bijv. nieuwe API-respons, parsingfout).', resolutionSteps: [ 'Log de correlation_id en relevante metadata (zonder PII).', 'Geef een vriendelijke melding dat het team op de hoogte wordt gebracht.', 'Vraag de gebruiker om eventueel ontbrekende context te delen.' ], badExample: '“Geen idee wat er mis ging”', goodExample: '“Er ging iets mis buiten onze verwachting. Ik heb het incident gelogd (ID: ...).”' }, [ErrorCode.CONFIGURATION_ERROR]: { title: 'Configuratieprobleem', typicalCause: 'Ontbrekende environment variables, verkeerde API-sleutel of verouderde versie.', resolutionSteps: [ 'Controleer of alle vereiste environment variables zijn ingevuld (zie README).', 'Vergelijk de draaiende versie met de laatste release en voer zo nodig een update uit.', 'Herstart de service na het corrigeren van configuratiewaarden.' ], badExample: 'Productie draait zonder REQUIRED_API_KEY', goodExample: 'Alle configuratievariabelen ingevuld en service herstart met succesmelding' } }; const QUICK_REF_ERROR_CODES: ErrorCode[] = [ ErrorCode.INVALID_DATE_FORMAT, ErrorCode.AGE_OUT_OF_RANGE, ErrorCode.INVALID_ENERGIELABEL, ErrorCode.INVALID_HYPOTHEEKVORM, ErrorCode.RENTEVAST_EXCEEDS_LOOPTIJD ]; const TOP_FIVE_MISTAKES: { title: string; fix: string }[] = [ { title: 'Rente opgegeven als percentage of string', fix: 'Vraag de rente opnieuw als decimaal (bijv. 0.0372 voor 3,72%) en herhaal het voorbeeld.' }, { title: 'Looptijd in jaren in plaats van maanden', fix: 'Vermenigvuldig jaren met 12 en bevestig de conversie met de gebruiker.' }, { title: 'Energielabel niet exact of lowercase', fix: 'Gebruik hoofdletters en de exacte notatie uit de lijst (bijv. "A++++").' }, { title: 'Partner aangemeld zonder partnerdata', fix: 'Vraag naar inkomen en geboortedatum partner of zet heeft_partner terug naar false.' }, { title: 'Leningdelen bevatten onbekende sleutel of verkeerde mapping', fix: 'Gebruik canonical velden: huidige_schuld, huidige_rente, resterende_looptijd_in_maanden, rentevasteperiode_maanden, hypotheekvorm.' } ]; const FORMAT_RULES: Array<{ parameter: string; format: string; good: string; bad: string; rationale: string }> = [ { parameter: 'Rente', format: 'Decimaal (bijv. 0.0372)', good: '0.025', bad: '"2,5%"', rationale: 'Backend verwacht een float; percentages veroorzaken 100× hogere waarden.' }, { parameter: 'Looptijd', format: 'Maanden (integer)', good: '240', bad: '20', rationale: '20 wordt gelezen als 20 maanden. Converteer 20 jaar → 240 maanden.' }, { parameter: 'Geboortedatum', format: 'YYYY-MM-DD', good: '1990-05-15', bad: '15-05-1990', rationale: 'ISO-formaat voorkomt ambiguïteit en matchingproblemen in validatie.' }, { parameter: 'Hypotheekvorm', format: 'Exacte string (annuiteit | lineair | aflossingsvrij)', good: 'annuiteit', bad: 'Annuïteit', rationale: 'Accenten en hoofdletters worden geweigerd door normalisatie en validatie.' }, { parameter: 'Energielabel', format: 'Exact uit lijst', good: 'A+++', bad: 'a+++', rationale: 'Beperk tot vooraf gedefinieerde waarden zodat back-end de toeslag goed toepast.' } ]; const STARTER_CASES = [ { title: 'Starter alleenstaand', context: 'Bruto inkomen €42.000, 28 jaar, geen verplichtingen.', highlight: 'Gebruik bereken_hypotheek_starter voor NHG vs non-NHG scenario’s.' }, { title: 'Starter met partner', context: 'Combinatie-inkomen €103.000, NHG tegen limiet aan.', highlight: 'Informeer over NHG-grens (€435.000) en kosten koper buffer.' }, { title: 'Starter met verduurzamingsbudget', context: 'Spaargeld + verduurzamingskosten, energielabel impact bespreken.', highlight: 'Vraag naar verbouwingsbudget en energielabel om toeslagen mee te nemen.' } ]; const DOORSTROMER_CASES = [ { title: 'Doorstromer met één leningdeel', context: 'Restschuld €180.000, woningwaarde €350.000.', highlight: 'Benadruk overwaarde en nieuwe maximale hypotheek.' }, { title: 'Doorstromer met meerdere delen', context: 'Annuïteit + aflossingsvrij, verschillende rentepercentages.', highlight: 'Gebruik normalizer om alle leningdelen naar canonical velden te zetten.' }, { title: 'Doorstromer met partner en verbouwing', context: 'Gezamenlijk inkomen, bestaande woning en renovatiebudget.', highlight: 'Combineer overwaarde, nieuwe woningkosten en partnerdata voor opzet.' } ]; interface ResourceDefinition { metadata: Resource; version: string; buildText: () => string; } const resourceDefinitions: ResourceDefinition[] = [ { metadata: { name: 'guide-playbook', title: 'AI Agent Playbook', description: 'Volledige playbook met 10 voorbeelden, best practices en troubleshooting.', uri: 'hypotheek://v4/guide/playbook', mimeType: MARKDOWN_MIME }, version: packageVersion, buildText: () => readFileRelative('docs/AI_AGENT_PLAYBOOK.md') }, { metadata: { name: 'guide-quick-ref', title: 'Quick Reference', description: 'Tool selectie, formatregels, fouten en errorcodes in één pagina.', uri: 'hypotheek://v4/guide/quick-ref', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildQuickReference }, { metadata: { name: 'guide-opzet-intake', title: 'Opzet Hypotheek Intake Guide', description: 'Checklist voor intakevelden, defaults en doorstromer-specifieke gegevens.', uri: 'hypotheek://v4/guide/opzet-intake', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildOpzetIntakeGuide }, { metadata: { name: 'guide-output-formatting', title: 'Output Formatting Guide', description: 'Best practices voor het presenteren van hypotheek berekeningen aan eindgebruikers.', uri: 'hypotheek://v4/guide/output-formatting', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildOutputFormattingGuide }, { metadata: { name: 'examples-starter', title: 'Startercases', description: 'Top 3 starter scenario’s met intake-tip en toolkeuze.', uri: 'hypotheek://v4/examples/starter', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildStarterExamples }, { metadata: { name: 'examples-doorstromer', title: 'Doorstromercases', description: 'Top 3 doorstromer scenario’s met aandachtspunten.', uri: 'hypotheek://v4/examples/doorstromer', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildDoorstromerExamples }, { metadata: { name: 'ops-error-recovery', title: 'Error Recovery Plan', description: 'Resolutie-stappen per errorcode met voorbeelden.', uri: 'hypotheek://v4/ops/error-recovery', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildErrorRecoveryGuide }, { metadata: { name: 'rules-format', title: 'Formatregels', description: 'Formele lijst van verplichte formats met rationale.', uri: 'hypotheek://v4/rules/format', mimeType: MARKDOWN_MIME }, version: '1.0.0', buildText: buildFormatRules } ].sort((a, b) => a.metadata.uri.localeCompare(b.metadata.uri)); function readFileRelative(relativePath: string): string { const absolutePath = resolve(projectRoot, relativePath); return readFileSync(absolutePath, 'utf-8'); } function hashContent(content: string): string { return createHash('sha256').update(content).digest('hex'); } function buildOpzetIntakeGuide(): string { return readFileRelative('docs/OPZET_INTAKE.md'); } function buildOutputFormattingGuide(): string { return readFileRelative('docs/OUTPUT_FORMATTING.md'); } function buildQuickReference(): string { const toolMatrix = `| Situatie | Tool | Reden | |----------|------|-------| | Starter zonder bestaande hypotheek | \`bereken_hypotheek_starter\` | Simpelste route, levert NHG en non-NHG scenario\'s | | Doorstromer met bestaande woning | \`bereken_hypotheek_doorstromer\` | Overwaarde + leningdelen analyseren | | Specifieke rente/looptijd gevraagd | \`bereken_hypotheek_uitgebreid\` | Volledige controle over parameters | | Starter wil woningtoets | \`opzet_hypotheek_starter\` | Laat financieringsmix en maandlast zien | | Doorstromer wil woningtoets | \`opzet_hypotheek_doorstromer\` | Combineert overwaarde, nieuwe woning en leningdelen | | Geavanceerde woningtoets | \`opzet_hypotheek_uitgebreid\` | Voor renteklassen of custom looptijd | | Alleen rentestanden nodig | \`haal_actuele_rentes_op\` | Toont actuele top-5 rentes |`; const formatTableRows = FORMAT_RULES.map(rule => `| ${rule.parameter} | ${rule.format} | ${rule.good} | ${rule.bad} |`).join('\n'); const formatTable = `| Parameter | Format | ✅ Goed | ❌ Fout | |-----------|--------|--------|--------| ${formatTableRows}`; const mistakes = TOP_FIVE_MISTAKES.map((item, index) => `${index + 1}. **${item.title}:** ${item.fix}`).join('\n'); const errorLines = QUICK_REF_ERROR_CODES.map(code => { const entry = ERROR_GUIDE[code]; return `- **${code}** — ${entry.title}: ${entry.typicalCause}`; }).join('\n'); const outputGuidance = ` ## Output Best Practices ### Opzet Hypotheek - ✅ Toon VOLLEDIGE tool output (heeft al balans checks + breakdown) - ✅ Voeg korte intro/context toe (max 2 zinnen) - ❌ Herschrijf of vat niet samen - ❌ Laat geen secties weg ### Maximale Hypotheek - ✅ Toon beide scenario's (NHG + non-NHG) - ✅ Benadruk verschil tussen scenario's - ✅ Verwijs naar energielabel impact indien relevant `; return `# Hypotheek MCP Quick Reference ## Tool selectie matrix ${toolMatrix} ## Kritieke format regels ${formatTable} ## Top 5 veelgemaakte fouten ${mistakes} ## Error code quick reference ${errorLines}${outputGuidance} `; } function buildStarterExamples(): string { const items = STARTER_CASES.map(caseItem => `### ${caseItem.title} - **Context:** ${caseItem.context} - **Aanpak:** Gebruik \`bereken_hypotheek_starter\`. - **Let op:** ${caseItem.highlight} `).join('\n'); return `# Startercases Gebruik deze referenties om intakegesprekken te versnellen. ${items}`; } function buildDoorstromerExamples(): string { const items = DOORSTROMER_CASES.map(caseItem => `### ${caseItem.title} - **Context:** ${caseItem.context} - **Aanpak:** Gebruik \`bereken_hypotheek_doorstromer\` of \`opzet_hypotheek_doorstromer\` afhankelijk van de vraag. - **Let op:** ${caseItem.highlight} `).join('\n'); return `# Doorstromercases Belangrijk: vraag altijd naar huidige woningwaarde én volledige leningdeel informatie. ${items}`; } function buildErrorRecoveryGuide(): string { const sections = Object.entries(ERROR_GUIDE).map(([code, entry]) => { const steps = entry.resolutionSteps.map(step => `1. ${step}`).join('\n'); return `## ${code} — ${entry.title} **Typische oorzaak:** ${entry.typicalCause} **Stappen om op te lossen:** ${steps} **Fout voorbeeld:** ${entry.badExample} **Correct voorbeeld:** ${entry.goodExample} `; }).join('\n'); return `# Error Recovery Plan Gebruik dit document om validatie en API-fouten snel te herstellen zonder PII te loggen. ${sections}`; } function buildFormatRules(): string { const rows = FORMAT_RULES.map(rule => `- **${rule.parameter}** → ${rule.format} - ✅ ${rule.good} - ❌ ${rule.bad} - _Waarom:_ ${rule.rationale}`).join('\n'); return `# Formele Formatregels Hanteer deze regels om afwijzingen te voorkomen. ${rows}`; } function toResourceContents(definition: ResourceDefinition): TextResourceContents { const text = definition.buildText(); return { uri: definition.metadata.uri, mimeType: definition.metadata.mimeType, text, etag: hashContent(text), version: definition.version }; } export function listResources(): Resource[] { return resourceDefinitions.map(def => def.metadata); } export function readResource(uri: string): TextResourceContents { const definition = resourceDefinitions.find(item => item.metadata.uri === uri); if (!definition) { throw new McpError(McpErrorCode.InvalidParams, `Onbekende resource: ${uri}`, { httpStatus: 404, code: 'NOT_FOUND' }); } return toResourceContents(definition); }

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