Skip to main content
Glama
index.ts18.6 kB
#!/usr/bin/env node /** * Swiss Health MCP Server * * Ein MCP-Server für Schweizer Krankenkassen-Prämien (2016-2026) * Datenquelle: BAG Priminfo (Bundesamt für Gesundheit) * * @author Remo Prinz * @license MIT */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { createClient, SupabaseClient } from "@supabase/supabase-js"; // ============================================ // VERSICHERER-NAMEN MAPPING (Fallback) // ============================================ const INSURER_NAMES: Record<string, string> = { '0008': 'CSS', '0032': 'Concordia', '0134': 'Visana', '0194': 'Atupri', '0246': 'Aquilana', '0290': 'Galenos', '0312': 'Helsana', '0343': 'Intras', '0360': 'Sanitas', '0376': 'KPT', '0455': 'ÖKK', '0509': 'Progrès', '0881': 'Sympany', '0923': 'Swica', '0941': 'Vivao', '0966': 'Wincare', '1040': 'EGK', '1113': 'Groupe Mutuel', '1318': 'Assura', '1322': 'Helsana Plus', '1384': 'Groupe Mutuel', '1386': 'KPT', '1401': 'Groupe Mutuel', '1479': 'Helsana', '1507': 'CSS', '1509': 'Swica', '1535': 'Assura', '1542': 'KPT', '1555': 'Groupe Mutuel', '1560': 'KPT', '1562': 'Assura', '1568': 'Helsana', '0062': 'AMB', '0057': 'Entremont', '0182': 'Sodalis', '0558': 'SLKK', '0762': 'Lumnezia', '0774': 'Luzerner Hinterland', '0780': 'Rhenusana', '0820': 'Sanitas', '0829': 'Avenir', '0901': 'Vallée de Joux', '0994': 'Assura-Basis', '1060': 'Sodalis', '1142': 'Intras', '1147': 'Agrisano', '1328': 'Agrisano', '1529': 'Accorda', '1565': 'Agrisano', '1566': 'Sodalis', '1569': 'Sodalis', '1570': 'Intras', '1573': 'Agrisano', '1575': 'Entremont', '1577': 'Vallée de Joux' }; function getInsurerName(insurerId: string): string { // Normalisiere ID auf 4 Stellen mit führenden Nullen const normalizedId = insurerId.toString().padStart(4, '0'); return INSURER_NAMES[normalizedId] || `Versicherer ${normalizedId}`; } // Haupt-IDs für bekannte Versicherer (bevorzugt bei Suche) const PRIMARY_INSURER_IDS: Record<string, string> = { 'helsana': '0312', 'css': '0008', 'swica': '0923', 'assura': '1318', 'kpt': '0376', 'groupe mutuel': '1113', 'sanitas': '0360', 'concordia': '0032', 'visana': '0134', 'ökk': '0455', 'sympany': '0881', 'atupri': '0194', 'egk': '1040', 'aquilana': '0246', 'galenos': '0290', 'agrisano': '1147' }; function findInsurerIdByName(searchName: string): string | undefined { const search = searchName.toLowerCase().trim(); // 1. Prüfe zuerst die Haupt-IDs (exakter Match) if (PRIMARY_INSURER_IDS[search]) { return PRIMARY_INSURER_IDS[search]; } // 2. Prüfe Haupt-IDs mit "enthält" for (const [name, id] of Object.entries(PRIMARY_INSURER_IDS)) { if (name.includes(search) || search.includes(name)) { return id; } } // 3. Fallback: Suche im vollen Mapping for (const [id, name] of Object.entries(INSURER_NAMES)) { if (name.toLowerCase() === search) { return id; // Exakter Match bevorzugt } } for (const [id, name] of Object.entries(INSURER_NAMES)) { if (name.toLowerCase().includes(search) || search.includes(name.toLowerCase())) { return id; } } return undefined; } // Findet ALLE IDs für einen Versicherer (für vollständige Daten) function findAllInsurerIds(searchName: string): string[] { const search = searchName.toLowerCase().trim(); const ids: string[] = []; for (const [id, name] of Object.entries(INSURER_NAMES)) { if (name.toLowerCase().includes(search) || search.includes(name.toLowerCase())) { ids.push(id); } } return ids; } // ============================================ // DISCLAIMER - Wird jeder Response angehängt // ============================================ const DISCLAIMER = ` 📋 HAFTUNGSAUSSCHLUSS: Diese Daten stammen vom BAG Priminfo (Bundesamt für Gesundheit) und dienen nur zur Information. Für verbindliche Prämien kontaktieren Sie bitte direkt die Krankenkasse oder besuchen Sie priminfo.admin.ch `; // ============================================ // Supabase Client // ============================================ let supabase: SupabaseClient; function getSupabase(): SupabaseClient { if (!supabase) { const url = process.env.SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!url || !key) { throw new Error("SUPABASE_URL und SUPABASE_SERVICE_ROLE_KEY müssen gesetzt sein"); } supabase = createClient(url, key); } return supabase; } // ============================================ // Tool-Definitionen // ============================================ const TOOLS = [ { name: "get_cheapest_insurers", description: "Findet die günstigsten Krankenkassen für ein bestimmtes Profil. Gibt die Top 5 zurück.", inputSchema: { type: "object" as const, properties: { canton: { type: "string", description: "Kanton (2-Buchstaben-Code, z.B. 'ZH', 'BE', 'GE')" }, year: { type: "number", description: "Jahr (2016-2026)" }, age_band: { type: "string", enum: ["child", "young_adult", "adult"], description: "Altersgruppe: child (0-18), young_adult (19-25), adult (26+)" }, franchise_chf: { type: "number", enum: [0, 100, 200, 300, 400, 500, 600, 1000, 1500, 2000, 2500], description: "Franchise in CHF" }, model_type: { type: "string", enum: ["standard", "hmo", "telmed", "family_doctor", "diverse"], description: "Versicherungsmodell (optional, default: standard)" }, accident_covered: { type: "boolean", description: "Unfalldeckung inkludiert (optional, default: true)" } }, required: ["canton", "year", "age_band", "franchise_chf"] } }, { name: "compare_insurers", description: "Vergleicht mehrere Versicherer für ein bestimmtes Profil.", inputSchema: { type: "object" as const, properties: { insurer_names: { type: "array", items: { type: "string" }, description: "Liste von Versicherer-Namen (z.B. ['CSS', 'Helsana', 'Swica'])" }, canton: { type: "string", description: "Kanton (2-Buchstaben-Code)" }, year: { type: "number", description: "Jahr (2016-2026)" }, age_band: { type: "string", enum: ["child", "young_adult", "adult"], description: "Altersgruppe" }, franchise_chf: { type: "number", description: "Franchise in CHF" } }, required: ["insurer_names", "canton", "year", "age_band", "franchise_chf"] } }, { name: "get_price_history", description: "Zeigt die Preisentwicklung eines Versicherers über mehrere Jahre.", inputSchema: { type: "object" as const, properties: { insurer_name: { type: "string", description: "Name des Versicherers (z.B. 'CSS', 'Helsana')" }, canton: { type: "string", description: "Kanton (2-Buchstaben-Code)" }, age_band: { type: "string", enum: ["child", "young_adult", "adult"], description: "Altersgruppe" }, franchise_chf: { type: "number", description: "Franchise in CHF" }, start_year: { type: "number", description: "Startjahr (optional, default: 2016)" }, end_year: { type: "number", description: "Endjahr (optional, default: 2026)" } }, required: ["insurer_name", "canton", "age_band", "franchise_chf"] } }, { name: "get_database_stats", description: "Zeigt Statistiken zur Datenbank (Anzahl Einträge, verfügbare Jahre, Versicherer).", inputSchema: { type: "object" as const, properties: {}, required: [] } } ]; // ============================================ // Tool-Implementierungen // ============================================ async function getCheapestInsurers(params: { canton: string; year: number; age_band: string; franchise_chf: number; model_type?: string; accident_covered?: boolean; }): Promise<string> { const db = getSupabase(); const { canton, year, age_band, franchise_chf, model_type = "standard", accident_covered = true } = params; // Suche günstigste Prämien const { data, error } = await db .from("premiums") .select("insurer_id, monthly_premium_chf, tariff_name") .eq("canton", canton.toUpperCase()) .eq("year", year) .eq("age_band", age_band) .eq("franchise_chf", franchise_chf) .eq("model_type", model_type) .eq("accident_covered", accident_covered) .order("monthly_premium_chf", { ascending: true }) .limit(10); if (error) { return `❌ Fehler: ${error.message}`; } if (!data || data.length === 0) { return `⚠️ Keine Prämien gefunden für: ${canton}, ${year}, ${age_band}, CHF ${franchise_chf} Franchise, ${model_type}`; } // Gruppiere nach Versicherer-NAME (nicht ID), damit jeder Versicherer nur einmal erscheint const bestByName = new Map<string, { premium: number; tariff: string; id: string }>(); for (const item of data) { const name = getInsurerName(item.insurer_id); const existing = bestByName.get(name); if (!existing || item.monthly_premium_chf < existing.premium) { bestByName.set(name, { premium: item.monthly_premium_chf, tariff: item.tariff_name || "", id: item.insurer_id }); } } // Sortiere nach Preis const sorted = [...bestByName.entries()] .sort((a, b) => a[1].premium - b[1].premium) .slice(0, 5); // Formatiere Ergebnis let result = `🏆 Top 5 günstigste Krankenkassen\n`; result += `📍 ${canton} | ${year} | ${age_band} | CHF ${franchise_chf} Franchise | ${model_type}\n\n`; sorted.forEach(([name, data], index) => { result += `${index + 1}. ${name}: CHF ${data.premium.toFixed(2)}/Monat\n`; }); result += DISCLAIMER; return result; } async function compareInsurers(params: { insurer_names: string[]; canton: string; year: number; age_band: string; franchise_chf: number; }): Promise<string> { const db = getSupabase(); const { insurer_names, canton, year, age_band, franchise_chf } = params; // Finde ALLE Versicherer-IDs für jeden Namen (z.B. Helsana hat 0312, 1322, 1479, 1568) const matchedInsurers: { searchName: string; ids: string[]; primaryId: string | undefined }[] = insurer_names.map(searchName => ({ searchName, ids: findAllInsurerIds(searchName), primaryId: findInsurerIdByName(searchName) })); // Sammle alle IDs und tracke welche zu welchem Namen gehören const allIds: string[] = []; const idToSearchName = new Map<string, string>(); for (const m of matchedInsurers) { for (const id of m.ids) { allIds.push(id); idToSearchName.set(id, m.searchName); } } const notFound = matchedInsurers.filter(m => m.ids.length === 0).map(m => m.searchName); if (allIds.length === 0) { return `⚠️ Keine der Versicherer gefunden: ${insurer_names.join(", ")}\n\nVerfügbare Versicherer: CSS, Helsana, Swica, Assura, Concordia, Sanitas, KPT, ÖKK, Visana, Groupe Mutuel, Sympany, Atupri, EGK, Aquilana, Galenos`; } // Hole Prämien für ALLE IDs const { data: premiums, error } = await db .from("premiums") .select("insurer_id, monthly_premium_chf, model_type") .eq("canton", canton.toUpperCase()) .eq("year", year) .eq("age_band", age_band) .eq("franchise_chf", franchise_chf) .in("insurer_id", allIds); if (error || !premiums || premiums.length === 0) { return `❌ Keine Prämien gefunden für: ${canton}, ${year}\n\nGesuchte IDs: ${allIds.join(", ")}`; } // Gruppiere nach SUCHNAME (nimm günstigsten Tarif pro Versicherer) const bestByName = new Map<string, { premium: number; model: string; id: string }>(); for (const p of premiums) { const searchName = idToSearchName.get(p.insurer_id) || p.insurer_id; const existing = bestByName.get(searchName); if (!existing || p.monthly_premium_chf < existing.premium) { bestByName.set(searchName, { premium: p.monthly_premium_chf, model: p.model_type, id: p.insurer_id }); } } // Formatiere Ergebnis let result = `📊 Versicherungsvergleich\n`; result += `📍 ${canton} | ${year} | ${age_band} | CHF ${franchise_chf} Franchise\n\n`; const sorted = [...bestByName.entries()] .sort((a, b) => a[1].premium - b[1].premium); sorted.forEach(([searchName, data], index) => { const displayName = getInsurerName(data.id); result += `${index + 1}. ${displayName}: CHF ${data.premium.toFixed(2)}/Monat (${data.model})\n`; }); if (notFound.length > 0) { result += `\n⚠️ Nicht gefunden: ${notFound.join(", ")}\n`; } if (sorted.length >= 2) { const diff = sorted[sorted.length - 1][1].premium - sorted[0][1].premium; result += `\n💰 Differenz günstigste/teuerste: CHF ${diff.toFixed(2)}/Monat\n`; } result += DISCLAIMER; return result; } async function getPriceHistory(params: { insurer_name: string; canton: string; age_band: string; franchise_chf: number; start_year?: number; end_year?: number; }): Promise<string> { const db = getSupabase(); const { insurer_name, canton, age_band, franchise_chf, start_year = 2016, end_year = 2026 } = params; // Finde ALLE Versicherer-IDs (z.B. CSS hat 0008 und 1507) const insurerIds = findAllInsurerIds(insurer_name); const primaryId = findInsurerIdByName(insurer_name); if (insurerIds.length === 0 || !primaryId) { return `⚠️ Versicherer "${insurer_name}" nicht gefunden\n\nVerfügbare Versicherer: CSS, Helsana, Swica, Assura, Concordia, Sanitas, KPT, ÖKK, Visana, Groupe Mutuel, Sympany, Atupri, EGK, Aquilana, Galenos`; } const insurerDisplayName = getInsurerName(primaryId); // Hole Prämien über die Jahre für ALLE IDs des Versicherers const { data: premiums, error } = await db .from("premiums") .select("year, monthly_premium_chf, model_type, insurer_id") .in("insurer_id", insurerIds) .eq("canton", canton.toUpperCase()) .eq("age_band", age_band) .eq("franchise_chf", franchise_chf) .gte("year", start_year) .lte("year", end_year) .order("year", { ascending: true }); if (error || !premiums || premiums.length === 0) { return `❌ Keine Daten für ${insurerDisplayName} in ${canton} (IDs: ${insurerIds.join(", ")})`; } // Gruppiere nach Jahr (günstigster Tarif pro Jahr) const bestByYear = new Map<number, number>(); for (const p of premiums) { const existing = bestByYear.get(p.year); if (!existing || p.monthly_premium_chf < existing) { bestByYear.set(p.year, p.monthly_premium_chf); } } // Formatiere Ergebnis let result = `📈 Preisentwicklung: ${insurerDisplayName}\n`; result += `📍 ${canton} | ${age_band} | CHF ${franchise_chf} Franchise\n\n`; const sortedYears = [...bestByYear.entries()].sort((a, b) => a[0] - b[0]); sortedYears.forEach(([year, premium]) => { result += `${year}: CHF ${premium.toFixed(2)}/Monat\n`; }); if (sortedYears.length >= 2) { const first = sortedYears[0][1]; const last = sortedYears[sortedYears.length - 1][1]; const change = ((last - first) / first * 100).toFixed(1); result += `\n📊 Veränderung ${sortedYears[0][0]}-${sortedYears[sortedYears.length - 1][0]}: ${change}%\n`; } result += DISCLAIMER; return result; } async function getDatabaseStats(): Promise<string> { const db = getSupabase(); // Hole Statistiken const [premiumsCount, insurersCount, locationsCount] = await Promise.all([ db.from("premiums").select("*", { count: "exact", head: true }), db.from("insurers").select("*", { count: "exact", head: true }), db.from("locations").select("*", { count: "exact", head: true }) ]); // Prüfe welche Jahre Daten haben (effizienter als alle Zeilen zu holen) const yearsToCheck = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]; const yearChecks = await Promise.all( yearsToCheck.map(year => db.from("premiums").select("year", { count: "exact", head: true }).eq("year", year) ) ); const availableYears = yearsToCheck.filter((year, index) => yearChecks[index].count && yearChecks[index].count > 0 ); // Hole Anzahl unique Versicherer aus premiums (statt insurers-Tabelle) const { data: insurerSample } = await db .from("premiums") .select("insurer_id") .limit(10000); const uniqueInsurers = new Set(insurerSample?.map(p => p.insurer_id) || []); let result = `📊 Datenbank-Statistiken\n\n`; result += `📋 Tabellen:\n`; result += ` • premiums: ${premiumsCount.count?.toLocaleString("de-CH")} Einträge\n`; result += ` • insurers: ${uniqueInsurers.size} aktive Versicherer\n`; result += ` • locations: ${locationsCount.count?.toLocaleString("de-CH")} PLZ-Einträge\n\n`; result += `📅 Verfügbare Jahre: ${availableYears.join(", ")}\n\n`; result += `🔗 Datenquelle: BAG Priminfo (priminfo.admin.ch)\n`; return result; } // ============================================ // MCP Server Setup // ============================================ const server = new Server( { name: "swiss-health-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Handle: Liste alle Tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS }; }); // Handle: Tool ausführen server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result: string; switch (name) { case "get_cheapest_insurers": result = await getCheapestInsurers(args as any); break; case "compare_insurers": result = await compareInsurers(args as any); break; case "get_price_history": result = await getPriceHistory(args as any); break; case "get_database_stats": result = await getDatabaseStats(); break; default: result = `❌ Unbekanntes Tool: ${name}`; } return { content: [{ type: "text", text: result }], }; } catch (error) { return { content: [{ type: "text", text: `❌ Fehler: ${error}` }], isError: true, }; } }); // ============================================ // Server starten // ============================================ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("🏥 Swiss Health MCP Server läuft..."); } main().catch(console.error);

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/remoprinz/swiss-health-mcp'

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