UNPKG

@iflow-mcp/ansvar-systems-german-law-mcp

Version:

German legal research MCP server with statutes, case law, and preparatory works from gesetze-im-internet.de

1,097 lines (1,096 loc) • 35.9 kB
import Database from "@ansvar/mcp-sqlite"; import * as fs from "node:fs"; import * as path from "node:path"; import { parseGermanCitation } from "../citation/german-citation.js"; import { buildFtsQueryVariantsLegacy as buildFtsQueryVariants } from "../utils/fts-query.js"; import { detectCapabilities, readDbMetadata } from '../capabilities.js'; const DB_ENV_VAR = "GERMAN_LAW_DB_PATH"; const DEFAULT_DB_PATH = path.resolve(process.cwd(), "data", "database.db"); let dbInstance = null; let dbAvailabilityChecked = false; let dbAvailable = false; let resolvedPathCache = ""; let dbCapabilities = null; let dbMetadata = null; export function searchGermanLawDocuments(query, limit, statuteId) { const db = getDb(); if (!db || !tableExists(db, "law_documents")) { return null; } // Resolve statuteId from title if it does not match a known ID const resolvedStatuteId = statuteId ? resolveDocumentStatuteId(db, statuteId) : undefined; if (statuteId && resolvedStatuteId === null) { return { documents: [], total: 0, _metadata: { note: `No document found matching "${statuteId}"` }, }; } // Fetch extra rows to account for deduplication const clampedLimit = clampLimit(limit); const fetchLimit = clampedLimit * 2; let queryStrategy; const exactRows = findExactCitationRows(db, query, fetchLimit); const mergedRows = []; const seen = new Set(); const filteredExact = resolvedStatuteId ? exactRows.filter((r) => matchesStatuteId(r, resolvedStatuteId)) : exactRows; pushUniqueRows(mergedRows, seen, filteredExact, fetchLimit); const variants = buildFtsQueryVariants(query); if (mergedRows.length < fetchLimit && variants.primary) { if (tableExists(db, "law_documents_fts")) { const primaryRows = runLawFtsQuery(db, variants.primary, fetchLimit * 3); if (primaryRows) { const filtered = resolvedStatuteId ? primaryRows.filter((r) => matchesStatuteId(r, resolvedStatuteId)) : primaryRows; pushUniqueRows(mergedRows, seen, filtered, fetchLimit); } if (mergedRows.length < fetchLimit && variants.fallback) { const fallbackRows = runLawFtsQuery(db, variants.fallback, fetchLimit * 3); if (fallbackRows && fallbackRows.length > 0) { if (!queryStrategy) queryStrategy = "broadened"; const filtered = resolvedStatuteId ? fallbackRows.filter((r) => matchesStatuteId(r, resolvedStatuteId)) : fallbackRows; pushUniqueRows(mergedRows, seen, filtered, fetchLimit); } } } } if (mergedRows.length < fetchLimit) { const likeRows = runLawLikeQuery(db, query, fetchLimit * 3); const filtered = resolvedStatuteId ? likeRows.filter((r) => matchesStatuteId(r, resolvedStatuteId)) : likeRows; if (filtered.length > 0 && mergedRows.length === 0) { queryStrategy = "like_fallback"; } pushUniqueRows(mergedRows, seen, filtered, fetchLimit); } const documents = deduplicateDocuments(mergedRows.map(mapStatuteRowToLawDocument), clampedLimit); const metadata = queryStrategy ? { query_strategy: queryStrategy } : undefined; return { documents, total: documents.length, ...(metadata ? { _metadata: metadata } : {}), }; } export function searchGermanCaseLawDocuments(request) { const db = getDb(); if (!db || !tableExists(db, "case_law_documents")) { return null; } const query = request.query.trim(); if (!query) { return { documents: [], total: 0 }; } const clampedLimit = clampLimit(request.limit ?? 20); const filters = buildCaseFilterSql({ ...(request.court === undefined ? {} : { court: request.court }), ...(request.dateFrom === undefined ? {} : { dateFrom: request.dateFrom }), ...(request.dateTo === undefined ? {} : { dateTo: request.dateTo }), }); const mergedRows = []; const seen = new Set(); const exactRows = findExactCaseRows(db, query, clampedLimit, filters); pushUniqueRows(mergedRows, seen, exactRows, clampedLimit); if (mergedRows.length >= clampedLimit) { return { documents: mergedRows.map(mapCaseLawRowToLawDocument), total: mergedRows.length, }; } const remaining = clampedLimit - mergedRows.length; const variants = buildFtsQueryVariants(query); if (variants.primary && tableExists(db, "case_law_documents_fts")) { const primaryRows = runCaseLawFtsQuery(db, variants.primary, remaining * 3, filters); if (primaryRows) { pushUniqueRows(mergedRows, seen, primaryRows, clampedLimit); } if (mergedRows.length >= clampedLimit) { return { documents: mergedRows.map(mapCaseLawRowToLawDocument), total: mergedRows.length, }; } if (variants.fallback) { const fallbackRows = runCaseLawFtsQuery(db, variants.fallback, remaining * 3, filters); if (fallbackRows && fallbackRows.length > 0) { pushUniqueRows(mergedRows, seen, fallbackRows, clampedLimit); } } if (mergedRows.length >= clampedLimit) { return { documents: mergedRows.map(mapCaseLawRowToLawDocument), total: mergedRows.length, }; } } const likeRows = runCaseLawLikeQuery(db, query, remaining * 3, filters); pushUniqueRows(mergedRows, seen, likeRows, clampedLimit); return { documents: mergedRows.map(mapCaseLawRowToLawDocument), total: mergedRows.length, }; } export function searchGermanPreparatoryWorks(request) { const db = getDb(); if (!db || !tableExists(db, "preparatory_works")) { return null; } const hints = buildPreparatorySearchHints(request); const clampedLimit = clampLimit(request.limit ?? 20); const filters = buildPreparatoryFilterSql(request); const mergedRows = []; const seen = new Set(); if (hints.length > 0 && tableExists(db, "preparatory_works_fts")) { const variants = buildFtsQueryVariants(hints[0] ?? ""); if (variants.primary) { const primaryRows = runPreparatoryFtsQuery(db, variants.primary, clampedLimit * 3, filters); if (primaryRows) { pushUniqueRows(mergedRows, seen, primaryRows, clampedLimit); } if (mergedRows.length >= clampedLimit) { return { documents: mergedRows.map(mapPreparatoryWorkRowToLawDocument), total: mergedRows.length, }; } if (variants.fallback) { const fallbackRows = runPreparatoryFtsQuery(db, variants.fallback, clampedLimit * 3, filters); if (fallbackRows && fallbackRows.length > 0) { pushUniqueRows(mergedRows, seen, fallbackRows, clampedLimit); } } if (mergedRows.length >= clampedLimit) { return { documents: mergedRows.map(mapPreparatoryWorkRowToLawDocument), total: mergedRows.length, }; } } } const likeRows = runPreparatoryLikeQuery(db, hints, clampedLimit * 3, filters); pushUniqueRows(mergedRows, seen, likeRows, clampedLimit); if (mergedRows.length === 0 && hints.length === 0) { const filteredRows = runPreparatoryFilteredQuery(db, clampedLimit, filters); pushUniqueRows(mergedRows, seen, filteredRows, clampedLimit); } return { documents: mergedRows.map(mapPreparatoryWorkRowToLawDocument), total: mergedRows.length, }; } export function getGermanLawDocumentById(id) { const db = getDb(); if (!db || !tableExists(db, "law_documents")) { return undefined; } let row; try { row = db .prepare(` SELECT id, country, kind, title, citation, source_url, effective_date, text_snippet, metadata_json FROM law_documents WHERE id = ? LIMIT 1 `) .get(id); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return undefined; } return row ? mapStatuteRowToLawDocument(row) : null; } export function getGermanDocumentByAnyId(id) { const db = getDb(); if (!db) { return undefined; } if (tableExists(db, "law_documents")) { try { const statuteRow = db .prepare(` SELECT id, country, kind, title, citation, source_url, effective_date, text_snippet, metadata_json FROM law_documents WHERE id = ? LIMIT 1 `) .get(id); if (statuteRow) { return mapStatuteRowToLawDocument(statuteRow); } } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return undefined; } } if (tableExists(db, "case_law_documents")) { try { const caseLawRow = db .prepare(` SELECT id, country, case_id, ecli, court, decision_date, file_number, decision_type, title, citation, source_url, text_snippet, metadata_json FROM case_law_documents WHERE id = ? LIMIT 1 `) .get(id); if (caseLawRow) { return mapCaseLawRowToLawDocument(caseLawRow); } } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return undefined; } } if (tableExists(db, "preparatory_works")) { try { const preparatoryRow = db .prepare(` SELECT id, country, dip_id, title, statute_id, statute_citation, work_type, publication_date, source_url, text_snippet, metadata_json FROM preparatory_works WHERE id = ? LIMIT 1 `) .get(id); if (preparatoryRow) { return mapPreparatoryWorkRowToLawDocument(preparatoryRow); } } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return undefined; } } return null; } export function getGermanLawDocumentsByStatuteId(statuteId, limit = 200) { const db = getDb(); if (!db || !tableExists(db, "law_documents")) { return null; } const normalized = statuteId.trim().toLowerCase(); if (!normalized) { return { documents: [], total: 0 }; } const clampedLimit = clampLimit(limit); try { const rows = db .prepare(` SELECT id, country, kind, title, citation, source_url, effective_date, text_snippet, metadata_json FROM law_documents WHERE lower(statute_id) = ? ORDER BY id LIMIT ? `) .all(normalized, clampedLimit); return { documents: rows.map(mapStatuteRowToLawDocument), total: rows.length, }; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } export function getGermanLawDocumentsByCitation(citation, limit = 200) { const db = getDb(); if (!db || !tableExists(db, "law_documents")) { return null; } const trimmed = citation.trim(); if (!trimmed) { return { documents: [], total: 0 }; } const parsed = parseGermanCitation(trimmed); const candidates = dedupeStrings([ trimmed, parsed?.normalized ?? "", ...(parsed?.lookupCitations ?? []), ]) .map((value) => value.toLowerCase()) .filter((value) => value.length > 0); if (candidates.length === 0) { return { documents: [], total: 0 }; } const preferred = (parsed?.lookupCitations[0] ?? parsed?.normalized ?? trimmed).toLowerCase(); const clampedLimit = clampLimit(limit); const placeholders = candidates.map(() => "?").join(", "); try { const rows = db .prepare(` SELECT id, country, kind, title, citation, source_url, effective_date, text_snippet, metadata_json FROM law_documents WHERE lower(citation) IN (${placeholders}) ORDER BY CASE WHEN lower(citation) = ? THEN 0 ELSE 1 END, id LIMIT ? `) .all(...candidates, preferred, clampedLimit); return { documents: rows.map(mapStatuteRowToLawDocument), total: rows.length, }; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } export function getGermanLawDocumentCount() { const db = getDb(); if (!db || !tableExists(db, "law_documents")) { return null; } try { const row = db .prepare("SELECT COUNT(*) AS count FROM law_documents") .get(); return Number(row.count); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } export function getGermanCaseLawDocumentCount() { const db = getDb(); if (!db || !tableExists(db, "case_law_documents")) { return null; } try { const row = db .prepare("SELECT COUNT(*) AS count FROM case_law_documents") .get(); return Number(row.count); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } export function getGermanPreparatoryWorkCount() { const db = getDb(); if (!db || !tableExists(db, "preparatory_works")) { return null; } try { const row = db .prepare("SELECT COUNT(*) AS count FROM preparatory_works") .get(); return Number(row.count); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } export function citationExistsInGermanLawDatabase(citation) { const db = getDb(); if (!db || !tableExists(db, "law_documents")) { return null; } const parsed = parseGermanCitation(citation); if (!parsed || parsed.lookupCitations.length === 0) { return false; } try { return existsByCitation(db, parsed.lookupCitations); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } export function resolveGermanLawDatabasePath() { return process.env[DB_ENV_VAR]?.trim() || DEFAULT_DB_PATH; } export function resetGermanLawDatabaseCache() { if (dbInstance) { dbInstance.close(); dbInstance = null; } dbAvailabilityChecked = false; dbAvailable = false; resolvedPathCache = ""; } export function getCapabilities() { if (!dbCapabilities) getDb(); return dbCapabilities ?? new Set(); } export function getMetadata() { if (!dbMetadata) getDb(); return dbMetadata ?? { tier: 'unknown', schema_version: '1', built_at: 'unknown', builder: 'unknown' }; } export function getDb() { const resolvedPath = resolveGermanLawDatabasePath(); if (dbAvailabilityChecked && resolvedPath === resolvedPathCache) { return dbAvailable ? dbInstance : null; } if (dbInstance) { dbInstance.close(); dbInstance = null; } resolvedPathCache = resolvedPath; dbAvailabilityChecked = true; dbAvailable = false; if (!fs.existsSync(resolvedPath)) { return null; } try { dbInstance = new Database(resolvedPath, { readonly: true }); dbAvailable = true; // Detect capabilities on first open dbCapabilities = detectCapabilities(dbInstance); dbMetadata = readDbMetadata(dbInstance); console.error(`[german-law-mcp] Database tier: ${dbMetadata.tier}, capabilities: ${[...dbCapabilities].join(', ')}`); return dbInstance; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } function tableExists(db, tableName) { try { const row = db .prepare(` SELECT 1 AS exists_flag FROM sqlite_master WHERE type IN ('table', 'view') AND name = ? LIMIT 1 `) .get(tableName); return Boolean(row?.exists_flag); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return false; } } function runLawFtsQuery(db, ftsQuery, limit) { try { const rows = db .prepare(` SELECT d.id, d.country, d.kind, d.title, d.citation, d.source_url, d.effective_date, d.text_snippet, d.metadata_json FROM law_documents_fts f JOIN law_documents d ON d.rowid = f.rowid WHERE law_documents_fts MATCH ? ORDER BY bm25(law_documents_fts) LIMIT ? `) .all(ftsQuery, limit); return rows; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } function findExactCitationRows(db, query, limit) { const parsed = parseGermanCitation(query); if (!parsed || parsed.lookupCitations.length === 0) { return []; } const citations = dedupeStrings(parsed.lookupCitations).map((value) => value.toLowerCase()); const placeholders = citations.map(() => "?").join(", "); try { return db .prepare(` SELECT id, country, kind, title, citation, source_url, effective_date, text_snippet, metadata_json FROM law_documents WHERE lower(citation) IN (${placeholders}) ORDER BY CASE WHEN lower(citation) = ? THEN 0 ELSE 1 END, id LIMIT ? `) .all(...citations, citations[0] ?? "", limit); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return []; } } function runLawLikeQuery(db, query, limit) { const tokens = query .normalize("NFC") .split(/\s+/) .map((token) => token.trim()) .filter((token) => token.length > 1); const searchTerms = tokens.length > 0 ? tokens : [query.trim()]; const clauses = []; const params = []; for (const term of searchTerms) { const like = `%${term}%`; clauses.push("(title LIKE ? OR citation LIKE ? OR text_snippet LIKE ?)"); params.push(like, like, like); } const whereClause = clauses.length > 0 ? clauses.join(" AND ") : "1 = 0"; try { return db .prepare(` SELECT id, country, kind, title, citation, source_url, effective_date, text_snippet, metadata_json FROM law_documents WHERE ${whereClause} LIMIT ? `) .all(...params, limit); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return []; } } function findExactCaseRows(db, query, limit, filters) { const lower = query.toLowerCase(); const params = [lower, lower, lower, lower, lower, ...filters.params, limit]; try { return db .prepare(` SELECT id, country, case_id, ecli, court, decision_date, file_number, decision_type, title, citation, source_url, text_snippet, metadata_json FROM case_law_documents WHERE ( lower(ecli) = ? OR lower(file_number) = ? OR lower(citation) = ? OR lower(case_id) = ? OR lower(id) = ? ) ${filters.clause} ORDER BY decision_date DESC, id DESC LIMIT ? `) .all(...params); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return []; } } function runCaseLawFtsQuery(db, ftsQuery, limit, filters) { try { const rows = db .prepare(` SELECT c.id, c.country, c.case_id, c.ecli, c.court, c.decision_date, c.file_number, c.decision_type, c.title, c.citation, c.source_url, c.text_snippet, c.metadata_json FROM case_law_documents_fts f JOIN case_law_documents c ON c.rowid = f.rowid WHERE case_law_documents_fts MATCH ? ${filters.clause} ORDER BY bm25(case_law_documents_fts), c.decision_date DESC LIMIT ? `) .all(ftsQuery, ...filters.params, limit); return rows; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } function runCaseLawLikeQuery(db, query, limit, filters) { const tokens = query .normalize("NFC") .split(/\s+/) .map((token) => token.trim()) .filter((token) => token.length > 1); const searchTerms = tokens.length > 0 ? tokens : [query.trim()]; const clauses = []; const params = []; for (const term of searchTerms) { const like = `%${term}%`; clauses.push("(title LIKE ? OR citation LIKE ? OR text_snippet LIKE ? OR file_number LIKE ? OR court LIKE ? OR ecli LIKE ?)"); params.push(like, like, like, like, like, like); } const queryClause = clauses.length > 0 ? clauses.join(" AND ") : "1 = 0"; try { return db .prepare(` SELECT id, country, case_id, ecli, court, decision_date, file_number, decision_type, title, citation, source_url, text_snippet, metadata_json FROM case_law_documents WHERE (${queryClause}) ${filters.clause} ORDER BY decision_date DESC, id DESC LIMIT ? `) .all(...params, ...filters.params, limit); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return []; } } function runPreparatoryFtsQuery(db, ftsQuery, limit, filters) { try { const rows = db .prepare(` SELECT p.id, p.country, p.dip_id, p.title, p.statute_id, p.statute_citation, p.work_type, p.publication_date, p.source_url, p.text_snippet, p.metadata_json FROM preparatory_works_fts f JOIN preparatory_works p ON p.rowid = f.rowid WHERE preparatory_works_fts MATCH ? ${filters.clause} ORDER BY bm25(preparatory_works_fts), p.publication_date DESC LIMIT ? `) .all(ftsQuery, ...filters.params, limit); return rows; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return null; } } function runPreparatoryLikeQuery(db, hints, limit, filters) { const tokens = hints.length > 0 ? tokenizeHints(hints) : []; const clauses = []; const params = []; for (const token of tokens) { const like = `%${token}%`; clauses.push("(p.title LIKE ? OR p.statute_citation LIKE ? OR p.text_snippet LIKE ?)"); params.push(like, like, like); } const queryClause = clauses.length > 0 ? clauses.join(" AND ") : "1 = 0"; try { return db .prepare(` SELECT p.id, p.country, p.dip_id, p.title, p.statute_id, p.statute_citation, p.work_type, p.publication_date, p.source_url, p.text_snippet, p.metadata_json FROM preparatory_works p WHERE (${queryClause}) ${filters.clause} ORDER BY publication_date DESC, id DESC LIMIT ? `) .all(...params, ...filters.params, limit); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return []; } } function runPreparatoryFilteredQuery(db, limit, filters) { try { return db .prepare(` SELECT p.id, p.country, p.dip_id, p.title, p.statute_id, p.statute_citation, p.work_type, p.publication_date, p.source_url, p.text_snippet, p.metadata_json FROM preparatory_works p WHERE 1 = 1 ${filters.clause} ORDER BY publication_date DESC, id DESC LIMIT ? `) .all(...filters.params, limit); } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return []; } } function buildPreparatorySearchHints(request) { const hints = []; if (request.query) { hints.push(request.query); } if (request.statuteId) { hints.push(request.statuteId); } if (request.citation) { hints.push(request.citation); const parsedCitation = parseGermanCitation(request.citation); if (parsedCitation?.normalized) { hints.push(parsedCitation.normalized); } const code = parsedCitation?.parsed.code; if (code && typeof code === "string") { hints.push(code); } } return dedupeStrings(hints); } function buildCaseFilterSql(filters) { const clauses = []; const params = []; if (filters.court?.trim()) { clauses.push("AND court LIKE ?"); params.push(`%${filters.court.trim()}%`); } const dateFrom = normalizeIsoDate(filters.dateFrom); if (dateFrom) { clauses.push("AND decision_date >= ?"); params.push(dateFrom); } const dateTo = normalizeIsoDate(filters.dateTo); if (dateTo) { clauses.push("AND decision_date <= ?"); params.push(dateTo); } return { clause: clauses.length > 0 ? ` ${clauses.join(" ")} ` : "", params, }; } function buildPreparatoryFilterSql(request) { const clauses = []; const params = []; if (request.statuteId?.trim()) { clauses.push("AND lower(p.statute_id) = ?"); params.push(request.statuteId.trim().toLowerCase()); } if (request.citation?.trim()) { const parsed = parseGermanCitation(request.citation); const tokens = dedupeStrings([ request.citation, parsed?.normalized ?? "", parsed?.parsed.code ?? "", ]) .map((token) => token.toLowerCase()) .filter((token) => token.length > 1); if (tokens.length > 0) { const tokenClauses = []; for (const token of tokens) { // Use table-qualified names to avoid ambiguity in FTS JOIN queries tokenClauses.push("(lower(p.statute_citation) LIKE ? OR lower(p.title) LIKE ? OR lower(p.text_snippet) LIKE ?)"); const like = `%${token}%`; params.push(like, like, like); } clauses.push(`AND (${tokenClauses.join(" OR ")})`); } } return { clause: clauses.length > 0 ? ` ${clauses.join(" ")} ` : "", params, }; } function normalizeIsoDate(value) { if (!value) { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return null; } return trimmed; } function tokenizeHints(hints) { const tokens = hints .flatMap((hint) => hint .normalize("NFC") .split(/\s+/) .map((token) => token.trim())) .filter((token) => token.length > 1); return dedupeStrings(tokens); } function existsByCitation(db, citations) { const values = dedupeStrings(citations).map((value) => value.toLowerCase()); if (values.length === 0) { return false; } const placeholders = values.map(() => "?").join(", "); const row = db .prepare(` SELECT 1 AS hit FROM law_documents WHERE lower(citation) IN (${placeholders}) LIMIT 1 `) .get(...values); return Boolean(row?.hit); } function pushUniqueRows(target, seen, rows, limit) { for (const row of rows) { if (seen.has(row.id)) { continue; } seen.add(row.id); target.push(row); if (target.length >= limit) { return; } } } function dedupeStrings(values) { return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; } function mapStatuteRowToLawDocument(row) { return { id: row.id, country: row.country, kind: mapKind(row.kind), title: row.title, ...(row.citation ? { citation: row.citation } : {}), ...(row.source_url ? { sourceUrl: row.source_url } : {}), ...(row.effective_date ? { effectiveDate: row.effective_date } : {}), ...(row.text_snippet ? { textSnippet: row.text_snippet } : {}), ...(row.metadata_json ? { metadata: parseMetadata(row.metadata_json) } : {}), }; } function mapCaseLawRowToLawDocument(row) { const metadata = { ...(row.metadata_json ? parseMetadata(row.metadata_json) : {}), ...(row.case_id ? { case_id: row.case_id } : {}), ...(row.ecli ? { ecli: row.ecli } : {}), ...(row.court ? { court: row.court } : {}), ...(row.file_number ? { file_number: row.file_number } : {}), ...(row.decision_type ? { decision_type: row.decision_type } : {}), }; return { id: row.id, country: row.country, kind: "case", title: row.title, ...(row.citation ? { citation: row.citation } : {}), ...(row.source_url ? { sourceUrl: row.source_url } : {}), ...(row.decision_date ? { effectiveDate: row.decision_date } : {}), ...(row.text_snippet ? { textSnippet: row.text_snippet } : {}), ...(Object.keys(metadata).length > 0 ? { metadata } : {}), }; } function mapPreparatoryWorkRowToLawDocument(row) { const metadata = { ...(row.metadata_json ? parseMetadata(row.metadata_json) : {}), dip_id: row.dip_id, ...(row.statute_id ? { statute_id: row.statute_id } : {}), ...(row.work_type ? { work_type: row.work_type } : {}), }; return { id: row.id, country: row.country, kind: "preparatory_work", title: row.title, ...(row.statute_citation ? { citation: row.statute_citation } : {}), ...(row.source_url ? { sourceUrl: row.source_url } : {}), ...(row.publication_date ? { effectiveDate: row.publication_date } : {}), ...(row.text_snippet ? { textSnippet: row.text_snippet } : {}), metadata, }; } function mapKind(value) { const kind = value.toLowerCase(); if (kind === "statute" || kind === "regulation" || kind === "case" || kind === "preparatory_work") { return kind; } return "other"; } function parseMetadata(raw) { try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return {}; } return parsed; } catch (err) { console.error("[german-law-mcp] DB query error:", err instanceof Error ? err.message : err); return {}; } } /** * Deduplicate LawDocument results by title + citation. * Same provision with different IDs (numeric vs slug) appears as duplicates; * this keeps the first (highest-ranked) occurrence per unique title+citation. */ function deduplicateDocuments(documents, limit) { const seen = new Set(); const deduped = []; for (const doc of documents) { const key = `${doc.title}::${doc.citation ?? ""}`; if (seen.has(key)) continue; seen.add(key); deduped.push(doc); if (deduped.length >= limit) break; } return deduped; } /** * Resolve a statute identifier from user input. * Accepts a statute_id directly, or a title / abbreviation. * Returns the statute_id string if found, or null if no match. */ function resolveDocumentStatuteId(db, input) { const trimmed = input.trim(); if (!trimmed) return null; // Step 1: Direct statute_id match try { const direct = db .prepare("SELECT statute_id FROM law_documents WHERE lower(statute_id) = ? LIMIT 1") .get(trimmed.toLowerCase()); if (direct) return direct.statute_id; } catch { // table/column may not exist } // Step 2: Direct ID match try { const byId = db .prepare("SELECT statute_id FROM law_documents WHERE lower(id) = ? AND statute_id IS NOT NULL LIMIT 1") .get(trimmed.toLowerCase()); if (byId) return byId.statute_id; } catch { // ignore } // Step 3: Title match (exact, case-insensitive) try { const byTitle = db .prepare("SELECT statute_id FROM law_documents WHERE lower(title) = ? AND statute_id IS NOT NULL LIMIT 1") .get(trimmed.toLowerCase()); if (byTitle) return byTitle.statute_id; } catch { // ignore } // Step 4: Substring title match — shortest wins try { const likeRows = db .prepare("SELECT statute_id, title FROM law_documents WHERE lower(title) LIKE ? AND statute_id IS NOT NULL") .all(`%${trimmed.toLowerCase()}%`); if (likeRows.length > 0) { likeRows.sort((a, b) => a.title.length - b.title.length); return likeRows[0]?.statute_id ?? null; } } catch { // ignore } return null; } /** * Check whether a statute row matches the resolved statute_id. */ function matchesStatuteId(row, statuteId) { if (!row.id) return false; const colonIndex = row.id.indexOf(":"); if (colonIndex > 0) { return row.id.slice(0, colonIndex).toLowerCase() === statuteId.toLowerCase(); } return row.id.toLowerCase().startsWith(statuteId.toLowerCase()); } function clampLimit(limit) { if (!Number.isFinite(limit) || Number.isNaN(limit)) { return 20; } if (limit < 1) { return 1; } if (limit > 100) { return 100; } return Math.trunc(limit); }