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

509 lines (508 loc) • 22.4 kB
import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { upgradeMessage } from "../capabilities.js"; const PREMIUM_UPGRADE_MESSAGE = "Version tracking is available in the Ansvar Intelligence Portal. Contact hello@ansvar.ai for access."; import { getGermanLawDocumentCount, getGermanCaseLawDocumentCount, getGermanPreparatoryWorkCount, getMetadata, } from "../db/german-law-db.js"; import { AdapterRegistry } from "./adapter-registry.js"; import { ShellError, toShellError } from "./errors.js"; import { TOOL_DEFINITIONS } from "./tool-contract.js"; let SERVER_VERSION = "0.0.0"; try { const __dirname = dirname(fileURLToPath(import.meta.url)); const pkgPath = join(__dirname, "..", "..", "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); SERVER_VERSION = pkg.version; } catch { // Fallback: version stays 0.0.0 if package.json cannot be read } export class LawMcpShell { registry; handlers; constructor(registry) { this.registry = registry; this.handlers = { "search_legislation": this.searchDocuments.bind(this), "search_case_law": this.searchCaseLaw.bind(this), "get_preparatory_works": this.getPreparatoryWorks.bind(this), "format_citation": this.formatCitation.bind(this), "check_currency": this.checkCurrency.bind(this), "build_legal_stance": this.buildLegalStance.bind(this), "get_eu_basis": this.getEuBasis.bind(this), "search_eu_implementations": this.searchEuImplementations.bind(this), "get_german_implementations": this.getNationalImplementations.bind(this), "get_provision_eu_basis": this.getProvisionEuBasis.bind(this), "validate_eu_compliance": this.validateEuCompliance.bind(this), "get_provision": this.getDocument.bind(this), "parse_citation": this.parseCitation.bind(this), "validate_citation": this.validateCitation.bind(this), "list_sources": this.listSources.bind(this), "about": this.about.bind(this), "get_provision_history": this.getProvisionHistory.bind(this), "diff_provision": this.diffProvision.bind(this), "get_recent_changes": this.getRecentChanges.bind(this), }; } static fromAdapters(adapters) { const registry = new AdapterRegistry(); registry.registerMany(adapters); return new LawMcpShell(registry); } getToolDefinitions() { return TOOL_DEFINITIONS; } async handleToolCall(call) { const args = call.arguments ?? {}; try { const handler = this.handlers[call.name]; const data = await handler(args); return { tool: call.name, ok: true, data, }; } catch (error) { const normalizedError = toShellError(error); return { tool: call.name, ok: false, error: { code: normalizedError.code, message: normalizedError.message, details: normalizedError.details, }, }; } } async searchDocuments(args) { const adapter = this.requireDocumentsAdapter(); const query = requireString(args, "query"); const limit = optionalNumber(args, "limit"); const documentId = optionalString(args, "document_id"); const request = { query, ...(limit === undefined ? {} : { limit }), ...(documentId === undefined ? {} : { document_id: documentId }), }; return adapter.searchDocuments(request); } async getDocument(args) { const adapter = this.requireDocumentsAdapter(); const id = requireString(args, "id"); return adapter.getDocument(id); } async searchCaseLaw(args) { const adapter = this.requireCaseLawAdapter(); const gateResult = requireDbCapability(adapter, "basic_case_law", "Case law search"); if (gateResult !== null) { return gateResult; } const query = requireString(args, "query"); const limit = optionalNumber(args, "limit"); const court = optionalString(args, "court"); const dateFrom = optionalString(args, "dateFrom"); const dateTo = optionalString(args, "dateTo"); const request = { query, ...(limit === undefined ? {} : { limit }), ...(court === undefined ? {} : { court }), ...(dateFrom === undefined ? {} : { dateFrom }), ...(dateTo === undefined ? {} : { dateTo }), }; return adapter.searchCaseLaw(request); } async getPreparatoryWorks(args) { const adapter = this.requirePreparatoryWorksAdapter(); const gateResult = requireDbCapability(adapter, "full_preparatory_works", "Preparatory works"); if (gateResult !== null) { return gateResult; } const citation = optionalString(args, "citation"); const statuteId = optionalString(args, "statuteId"); const query = optionalString(args, "query"); const limit = optionalNumber(args, "limit"); if (citation === undefined && statuteId === undefined && query === undefined) { throw new ShellError("invalid_arguments", "Expected at least one of: citation, statuteId, query"); } const request = { ...(citation === undefined ? {} : { citation }), ...(statuteId === undefined ? {} : { statuteId }), ...(query === undefined ? {} : { query }), ...(limit === undefined ? {} : { limit }), }; return adapter.getPreparatoryWorks(request); } async parseCitation(args) { const adapter = this.requireCitationsAdapter(); const citation = requireString(args, "citation"); return adapter.parseCitation(citation); } async formatCitation(args) { const adapter = this.requireFormattingAdapter(); const citation = requireString(args, "citation"); const styleValue = optionalString(args, "style"); const style = styleValue === undefined ? undefined : parseCitationStyle(styleValue); const request = style === undefined ? { citation } : { citation, style }; return adapter.formatCitation(request); } async checkCurrency(args) { const adapter = this.requireCurrencyAdapter(); const citation = optionalString(args, "citation"); const statuteId = optionalString(args, "statuteId"); const asOfDate = optionalString(args, "asOfDate"); if (citation === undefined && statuteId === undefined) { throw new ShellError("invalid_arguments", "Expected at least one of: citation, statuteId"); } const request = { ...(citation === undefined ? {} : { citation }), ...(statuteId === undefined ? {} : { statuteId }), ...(asOfDate === undefined ? {} : { asOfDate }), }; return adapter.checkCurrency(request); } async buildLegalStance(args) { const adapter = this.requireLegalStanceAdapter(); const query = requireString(args, "query"); const limit = optionalNumber(args, "limit"); const documentId = optionalString(args, "document_id"); let includeCaseLaw = optionalBoolean(args, "includeCaseLaw"); let includePreparatoryWorks = optionalBoolean(args, "includePreparatoryWorks"); // Gracefully suppress case law / preparatory works when the DB lacks // those tables, and collect upgrade notices for the response. const notices = []; const dbCaps = adapter.getDbCapabilities?.(); if (dbCaps !== undefined) { if ((includeCaseLaw === undefined || includeCaseLaw) && !dbCaps.has("basic_case_law")) { includeCaseLaw = false; notices.push(upgradeMessage("Case law search")); } if ((includePreparatoryWorks === undefined || includePreparatoryWorks) && !dbCaps.has("full_preparatory_works")) { includePreparatoryWorks = false; notices.push(upgradeMessage("Preparatory works")); } } const request = { query, ...(limit === undefined ? {} : { limit }), ...(documentId === undefined ? {} : { document_id: documentId }), ...(includeCaseLaw === undefined ? {} : { includeCaseLaw }), ...(includePreparatoryWorks === undefined ? {} : { includePreparatoryWorks }), }; const result = await adapter.buildLegalStance(request); if (notices.length > 0 && result && typeof result === "object") { return { ...result, tierNotices: notices }; } return result; } async getEuBasis(args) { const adapter = this.requireEuAdapter(); const citation = optionalString(args, "citation"); const statuteId = optionalString(args, "statuteId"); const documentId = optionalString(args, "documentId"); const limit = optionalNumber(args, "limit"); if (citation === undefined && statuteId === undefined && documentId === undefined) { throw new ShellError("invalid_arguments", "Expected at least one of: citation, statuteId, documentId"); } const request = { ...(citation === undefined ? {} : { citation }), ...(statuteId === undefined ? {} : { statuteId }), ...(documentId === undefined ? {} : { documentId }), ...(limit === undefined ? {} : { limit }), }; return adapter.getEuBasis(request); } async searchEuImplementations(args) { const adapter = this.requireEuAdapter(); const query = requireString(args, "query"); const limit = optionalNumber(args, "limit"); const request = limit === undefined ? { query } : { query, limit }; return adapter.searchEuImplementations(request); } async getNationalImplementations(args) { const adapter = this.requireEuAdapter(); const euId = requireString(args, "euId"); const limit = optionalNumber(args, "limit"); const request = limit === undefined ? { euId } : { euId, limit }; return adapter.getNationalImplementations(request); } async getProvisionEuBasis(args) { const adapter = this.requireEuAdapter(); const documentId = requireString(args, "documentId"); const limit = optionalNumber(args, "limit"); return adapter.getProvisionEuBasis(limit === undefined ? { documentId } : { documentId, limit }); } async validateEuCompliance(args) { const adapter = this.requireEuAdapter(); const euId = requireString(args, "euId"); const citation = optionalString(args, "citation"); const statuteId = optionalString(args, "statuteId"); const request = { euId, ...(citation === undefined ? {} : { citation }), ...(statuteId === undefined ? {} : { statuteId }), }; return adapter.validateEuCompliance(request); } async validateCitation(args) { const adapter = this.requireCitationsAdapter(); const citation = requireString(args, "citation"); return adapter.validateCitation(citation); } async listSources() { const adapter = this.registry.get("de"); return { country: adapter.country.code, sources: [ { name: "Gesetze im Internet", url: "https://www.gesetze-im-internet.de", description: "Official portal of the German Federal Ministry of Justice. " + "Provides consolidated texts of all German federal statutes and regulations.", scope: "All consolidated German federal statutes (Bundesgesetze)", limitations: "Does not include state (Länder) legislation.", format: "XML download", updateFrequency: "Daily", authority: "Federal Ministry of Justice (BMJ)", }, { name: "Rechtsprechung im Internet", url: "https://www.rechtsprechung-im-internet.de", description: "Official portal for published decisions of German federal courts.", scope: "BVerfG, BGH, BVerwG, BAG, BSG, BFH, BPatG decisions", limitations: "Not all decisions are published. Lower court (Landesgerichte) decisions are not included.", format: "XML download", updateFrequency: "Daily", authority: "German Federal Courts", }, { name: "DIP Bundestag", url: "https://dip.bundestag.de", description: "Documentation and Information System of the German Bundestag. " + "Provides legislative preparatory works including Drucksachen and Plenarprotokolle.", scope: "Drucksachen and Plenarprotokolle for Wahlperioden 19 and 20", limitations: "Bundesrat documents may be incomplete. Earlier Wahlperioden not yet ingested.", format: "REST API", updateFrequency: "Daily", authority: "German Bundestag", }, ], }; } async about() { const dbMeta = getMetadata(); const statuteCount = getGermanLawDocumentCount(); const caseCount = getGermanCaseLawDocumentCount(); const prepCount = getGermanPreparatoryWorkCount(); return { server: "german-law-mcp", version: SERVER_VERSION, description: "German legal research MCP server providing access to federal statutes, " + "court decisions, and legislative preparatory works from official German government sources.", jurisdiction: "DE", language: "de", tier: dbMeta.tier, database: { schema_version: dbMeta.schema_version, built_at: dbMeta.built_at, builder: dbMeta.builder, }, statistics: { statutes: statuteCount, case_law_decisions: caseCount, preparatory_works: prepCount, }, data_sources: [ "gesetze-im-internet.de", "rechtsprechung-im-internet.de", "dip.bundestag.de", ], transports: ["stdio", "streamable-http"], repository: "https://github.com/Ansvar-Systems/German-law-mcp", }; } // --------------------------------------------------------------------------- // Premium: version tracking handlers // --------------------------------------------------------------------------- async getProvisionHistory(args) { if (!process.env.PREMIUM_ENABLED) { return { premium: false, message: PREMIUM_UPGRADE_MESSAGE }; } const adapter = this.requireVersionTrackingAdapter(); const lawIdentifier = requireString(args, "law_identifier"); const article = requireString(args, "article"); return adapter.getProvisionHistory(lawIdentifier, article); } async diffProvision(args) { if (!process.env.PREMIUM_ENABLED) { return { premium: false, message: PREMIUM_UPGRADE_MESSAGE }; } const adapter = this.requireVersionTrackingAdapter(); const lawIdentifier = requireString(args, "law_identifier"); const article = requireString(args, "article"); const fromDate = requireString(args, "from_date"); const toDate = optionalString(args, "to_date"); return adapter.diffProvision(lawIdentifier, article, fromDate, toDate); } async getRecentChanges(args) { if (!process.env.PREMIUM_ENABLED) { return { premium: false, message: PREMIUM_UPGRADE_MESSAGE }; } const adapter = this.requireVersionTrackingAdapter(); const since = requireString(args, "since"); const limit = optionalNumber(args, "limit"); return adapter.getRecentChanges(since, limit); } requireVersionTrackingAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.versionTracking || !adapter.getProvisionHistory || !adapter.diffProvision || !adapter.getRecentChanges) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support version tracking`); } return adapter; } requireDocumentsAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.documents || !adapter.searchDocuments || !adapter.getDocument) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support document search`); } return adapter; } requireCaseLawAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.caseLaw || !adapter.searchCaseLaw) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support case law search`); } return adapter; } requirePreparatoryWorksAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.preparatoryWorks || !adapter.getPreparatoryWorks) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support preparatory works retrieval`); } return adapter; } requireCitationsAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.citations || !adapter.parseCitation || !adapter.validateCitation) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support citation parsing`); } return adapter; } requireFormattingAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.formatting || !adapter.formatCitation) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support citation formatting`); } return adapter; } requireCurrencyAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.currency || !adapter.checkCurrency) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support currency checks`); } return adapter; } requireLegalStanceAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.legalStance || !adapter.buildLegalStance) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support legal stance building`); } return adapter; } requireEuAdapter() { const adapter = this.requireCountry(); if (!adapter.capabilities.eu || !adapter.getEuBasis || !adapter.searchEuImplementations || !adapter.getNationalImplementations || !adapter.getProvisionEuBasis || !adapter.validateEuCompliance) { throw new ShellError("unsupported_capability", `Country ${adapter.country.code} does not support EU linkage`); } return adapter; } requireCountry() { return this.registry.get("de"); } } function parseCitationStyle(value) { const normalized = value.trim().toLowerCase(); if (normalized === "default" || normalized === "short" || normalized === "pinpoint") { return normalized; } throw new ShellError("invalid_arguments", "Expected style to be one of: default, short, pinpoint"); } function requireString(args, key) { const value = args[key]; if (typeof value !== "string" || value.trim().length === 0) { throw new ShellError("invalid_arguments", `Expected non-empty string: ${key}`); } return value; } function optionalString(args, key) { const value = args[key]; if (value === undefined) { return undefined; } if (typeof value !== "string") { throw new ShellError("invalid_arguments", `Expected string: ${key}`); } return value; } function optionalNumber(args, key) { const value = args[key]; if (value === undefined) { return undefined; } if (typeof value !== "number" || Number.isNaN(value)) { throw new ShellError("invalid_arguments", `Expected number: ${key}`); } return value; } function optionalBoolean(args, key) { const value = args[key]; if (value === undefined) { return undefined; } if (typeof value !== "boolean") { throw new ShellError("invalid_arguments", `Expected boolean: ${key}`); } return value; } /** * Check whether the adapter's runtime DB has the required capability. * * Returns `null` when the capability is present (caller should proceed) or * an upgrade-message payload when the capability is missing (caller should * return this payload directly). * * When the adapter does not expose `getDbCapabilities` (professional tier, * or non-DB adapters), the check is skipped and `null` is returned so that * the normal code path runs. */ function requireDbCapability(adapter, capability, featureLabel) { const dbCaps = adapter.getDbCapabilities?.(); // No runtime capability detection → assume full access (professional tier). if (dbCaps === undefined) { return null; } if (dbCaps.has(capability)) { return null; } return { documents: [], total: 0, upgradeRequired: true, message: upgradeMessage(featureLabel), }; }