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

251 lines (242 loc) • 11.3 kB
import Database from "@ansvar/mcp-sqlite"; import assert from "node:assert/strict"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import test from "node:test"; import { BUILTIN_ADAPTERS } from "../src/adapters/index.js"; import { resetGermanLawDatabaseCache } from "../src/db/german-law-db.js"; import { LawMcpShell } from "../src/shell/shell.js"; test("de adapter uses sqlite-backed corpus when configured", async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "german-law-mcp-")); const dbPath = path.join(tempDir, "test.db"); const db = new Database(dbPath); db.exec(` CREATE TABLE law_documents ( id TEXT PRIMARY KEY, country TEXT NOT NULL, statute_id TEXT, section_ref TEXT, kind TEXT NOT NULL, title TEXT NOT NULL, citation TEXT, source_url TEXT, effective_date TEXT, text_snippet TEXT, metadata_json TEXT, updated_at TEXT ); CREATE VIRTUAL TABLE law_documents_fts USING fts5( title, citation, text_snippet, content='law_documents', content_rowid='rowid', tokenize='unicode61' ); CREATE TRIGGER law_documents_ai AFTER INSERT ON law_documents BEGIN INSERT INTO law_documents_fts(rowid, title, citation, text_snippet) VALUES (new.rowid, new.title, COALESCE(new.citation, ''), COALESCE(new.text_snippet, '')); END; CREATE TABLE case_law_documents ( id TEXT PRIMARY KEY, country TEXT NOT NULL, case_id TEXT NOT NULL UNIQUE, ecli TEXT, court TEXT, decision_date TEXT, file_number TEXT, decision_type TEXT, title TEXT NOT NULL, citation TEXT, source_url TEXT NOT NULL, text_snippet TEXT, metadata_json TEXT, updated_at TEXT ); CREATE VIRTUAL TABLE case_law_documents_fts USING fts5( title, citation, text_snippet, content='case_law_documents', content_rowid='rowid', tokenize='unicode61' ); CREATE TRIGGER case_law_documents_ai AFTER INSERT ON case_law_documents BEGIN INSERT INTO case_law_documents_fts(rowid, title, citation, text_snippet) VALUES (new.rowid, new.title, COALESCE(new.citation, ''), COALESCE(new.text_snippet, '')); END; CREATE TABLE preparatory_works ( id TEXT PRIMARY KEY, country TEXT NOT NULL, dip_id TEXT NOT NULL UNIQUE, title TEXT NOT NULL, statute_id TEXT, statute_citation TEXT, work_type TEXT, publication_date TEXT, source_url TEXT NOT NULL, text_snippet TEXT, metadata_json TEXT, updated_at TEXT ); CREATE VIRTUAL TABLE preparatory_works_fts USING fts5( title, statute_citation, text_snippet, content='preparatory_works', content_rowid='rowid', tokenize='unicode61' ); CREATE TRIGGER preparatory_works_ai AFTER INSERT ON preparatory_works BEGIN INSERT INTO preparatory_works_fts(rowid, title, statute_citation, text_snippet) VALUES (new.rowid, new.title, COALESCE(new.statute_citation, ''), COALESCE(new.text_snippet, '')); END; `); db.prepare(` INSERT INTO law_documents ( id, country, statute_id, section_ref, kind, title, citation, source_url, effective_date, text_snippet, metadata_json, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) `).run("bdsg:1", "de", "bdsg", "§ 1", "statute", "Bundesdatenschutzgesetz - Zweck und Anwendungsbereich", "§ 1 BDSG", "https://www.gesetze-im-internet.de/bdsg_2018/", "2018-05-25", "Dieses Gesetz dient der Durchführung der Verordnung (EU) 2016/679.", JSON.stringify({ source: "fixture" })); db.prepare(` INSERT INTO law_documents ( id, country, statute_id, section_ref, kind, title, citation, source_url, effective_date, text_snippet, metadata_json, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) `).run("bdsg:10", "de", "bdsg", "§ 10", "statute", "Bundesdatenschutzgesetz - Aufsicht", "§ 10 BDSG", "https://www.gesetze-im-internet.de/bdsg_2018/", "2018-05-25", "Die Aufsicht hat auch Bezüge zu § 1 BDSG.", JSON.stringify({ source: "fixture" })); db.prepare(` INSERT INTO case_law_documents ( id, country, case_id, ecli, court, decision_date, file_number, decision_type, title, citation, source_url, text_snippet, metadata_json, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) `).run("case:jure999999001", "de", "JURE999999001", "ECLI:DE:BGH:2024:0201.IXZR1.23.0", "BGH IX. Zivilsenat", "2024-02-01", "IX ZR 1/23", "Urteil", "BGH zur Haftung nach § 823 BGB", "IX ZR 1/23", "https://example.invalid/case-1", "Der BGH konkretisiert die Voraussetzungen des § 823 Abs. 1 BGB.", JSON.stringify({ source: "fixture" })); db.prepare(` INSERT INTO preparatory_works ( id, country, dip_id, title, statute_id, statute_citation, work_type, publication_date, source_url, text_snippet, metadata_json, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) `).run("prep:310586", "de", "310586", "Medizinforschungsgesetz", "bdsg", "BDSG", "Gesetzgebung", "2024-07-01", "https://example.invalid/prep-1", "Gesetzentwurf mit Datenschutzbezug zum BDSG.", JSON.stringify({ source: "fixture" })); db.close(); const originalEnv = process.env.GERMAN_LAW_DB_PATH; process.env.GERMAN_LAW_DB_PATH = dbPath; resetGermanLawDatabaseCache(); try { const shell = LawMcpShell.fromAdapters(BUILTIN_ADAPTERS); const searchResult = await shell.handleToolCall({ name: "search_legislation", arguments: { query: "datenschutz", limit: 5 }, }); assert.equal(searchResult.ok, true); const payload = searchResult.data; assert.equal(payload.total >= 1, true); assert.equal(payload.documents.some((doc) => doc.id === "bdsg:1"), true); const getResult = await shell.handleToolCall({ name: "get_provision", arguments: { id: "bdsg:1" }, }); assert.equal(getResult.ok, true); assert.equal(getResult.data.id, "bdsg:1"); const citationSearchResult = await shell.handleToolCall({ name: "search_legislation", arguments: { query: "§ 1 BDSG", limit: 2 }, }); assert.equal(citationSearchResult.ok, true); const citationSearchPayload = citationSearchResult.data; assert.equal(citationSearchPayload.documents[0]?.id, "bdsg:1"); const validCitationResult = await shell.handleToolCall({ name: "validate_citation", arguments: { citation: "§ 1 BDSG" }, }); assert.equal(validCitationResult.ok, true); assert.equal(validCitationResult.data.valid, true); const missingCitationResult = await shell.handleToolCall({ name: "validate_citation", arguments: { citation: "§ 999 BDSG" }, }); assert.equal(missingCitationResult.ok, true); assert.equal(missingCitationResult.data.valid, false); const caseLawResult = await shell.handleToolCall({ name: "search_case_law", arguments: { query: "haftung", limit: 5 }, }); assert.equal(caseLawResult.ok, true); const caseLawPayload = caseLawResult.data; assert.equal(caseLawPayload.documents[0]?.id, "case:jure999999001"); assert.equal(caseLawPayload.documents[0]?.kind, "case"); const preparatoryWorksResult = await shell.handleToolCall({ name: "get_preparatory_works", arguments: { citation: "§ 1 BDSG", limit: 5 }, }); assert.equal(preparatoryWorksResult.ok, true); const preparatoryPayload = preparatoryWorksResult.data; assert.equal(preparatoryPayload.documents[0]?.id, "prep:310586"); assert.equal(preparatoryPayload.documents[0]?.kind, "preparatory_work"); const formattedCitationResult = await shell.handleToolCall({ name: "format_citation", arguments: { citation: "§ 1 Absatz 1 bdsg", style: "short", }, }); assert.equal(formattedCitationResult.ok, true); assert.equal(formattedCitationResult.data.formatted, "§ 1 BDSG"); const currencyResult = await shell.handleToolCall({ name: "check_currency", arguments: { statuteId: "bdsg" }, }); assert.equal(currencyResult.ok, true); assert.equal(currencyResult.data.status, "likely_in_force"); const legalStanceResult = await shell.handleToolCall({ name: "build_legal_stance", arguments: { query: "datenschutz", limit: 5 }, }); assert.equal(legalStanceResult.ok, true); const legalStancePayload = legalStanceResult.data; assert.equal(legalStancePayload.statutes.some((document) => document.id === "bdsg:1"), true); const euBasisResult = await shell.handleToolCall({ name: "get_eu_basis", arguments: { statuteId: "bdsg", limit: 10 }, }); assert.equal(euBasisResult.ok, true); const euBasisPayload = euBasisResult.data; assert.equal(euBasisPayload.references.some((reference) => reference.euId.includes("2016/679")), true); const euSearchResult = await shell.handleToolCall({ name: "search_eu_implementations", arguments: { query: "datenschutz", limit: 10 }, }); assert.equal(euSearchResult.ok, true); const euSearchPayload = euSearchResult.data; assert.equal(euSearchPayload.results.some((result) => result.euId.includes("2016/679") && result.statutes.includes("bdsg")), true); const euNationalResult = await shell.handleToolCall({ name: "get_german_implementations", arguments: { euId: "EU 2016/679", limit: 10 }, }); assert.equal(euNationalResult.ok, true); const euNationalPayload = euNationalResult.data; assert.equal(euNationalPayload.results.some((result) => result.euId.includes("2016/679") && result.statutes.includes("bdsg")), true); const provisionBasisResult = await shell.handleToolCall({ name: "get_provision_eu_basis", arguments: { documentId: "bdsg:1", limit: 10 }, }); assert.equal(provisionBasisResult.ok, true); const provisionBasisPayload = provisionBasisResult.data; assert.equal(provisionBasisPayload.references.some((reference) => reference.euId.includes("2016/679")), true); const euComplianceResult = await shell.handleToolCall({ name: "validate_eu_compliance", arguments: { euId: "EU 2016/679", statuteId: "bdsg" }, }); assert.equal(euComplianceResult.ok, true); assert.equal(euComplianceResult.data.status, "mapped"); } finally { if (originalEnv === undefined) { delete process.env.GERMAN_LAW_DB_PATH; } else { process.env.GERMAN_LAW_DB_PATH = originalEnv; } resetGermanLawDatabaseCache(); fs.rmSync(tempDir, { recursive: true, force: true }); } });