@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
JavaScript
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),
};
}