@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
130 lines (129 loc) • 5.78 kB
JavaScript
/**
* Free-tier database builder for German Law MCP server.
*
* SUBTRACTIVE — copies the full database, then removes case law,
* preparatory works, and paid-tier stub tables using the native
* sqlite3 CLI (WASM wrapper can struggle with large databases).
*
* Kept tables:
* - law_documents + law_documents_fts (core statutes)
* - db_metadata (tier info)
* - ingestion_runs (if present)
*
* Dropped tables:
* - case_law_documents + case_law_documents_fts
* - preparatory_works + preparatory_works_fts
* - case_law_documents_full, preparatory_works_full
* - agency_guidance, agency_guidance_fts
*
* Usage: npm run build:db:free
*/
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
const FULL_DB = path.resolve(process.cwd(), 'data', 'database.db');
const FREE_DB = path.resolve(process.cwd(), 'data', 'database-free.db');
function sql(dbPath, query) {
return execFileSync('sqlite3', [dbPath, query], {
encoding: 'utf-8',
timeout: 600_000, // 10 minutes for VACUUM on large DBs
}).trim();
}
// Tables to drop for the free tier (order matters: FTS/dependents first)
const TABLES_TO_DROP = [
'case_law_documents_fts',
'case_law_documents',
'preparatory_works_fts',
'preparatory_works',
'case_law_documents_full',
'preparatory_works_full',
'agency_guidance_fts',
'agency_guidance',
];
function buildFreeTier() {
console.log('Building free-tier database for German Law MCP...\n');
if (!fs.existsSync(FULL_DB)) {
console.error(`ERROR: No full database found at ${FULL_DB}\n` +
`Run ingestion first to create the full database.`);
process.exit(1);
}
// Verify sqlite3 is available
try {
execFileSync('sqlite3', ['--version'], { encoding: 'utf-8' });
}
catch {
console.error('ERROR: sqlite3 CLI not found. Install it first.');
process.exit(1);
}
const fullSize = fs.statSync(FULL_DB).size;
console.log(` Source: ${FULL_DB} (${(fullSize / 1024 / 1024).toFixed(1)} MB)`);
// Copy full DB to free-tier path
if (fs.existsSync(FREE_DB))
fs.unlinkSync(FREE_DB);
fs.copyFileSync(FULL_DB, FREE_DB);
// Also remove any WAL/journal files from the copy
for (const suffix of ['-wal', '-shm', '-journal']) {
const f = FREE_DB + suffix;
if (fs.existsSync(f))
fs.unlinkSync(f);
}
console.log(` Copied to: ${FREE_DB}`);
// Switch to DELETE journal mode (WAL can cause locking issues)
sql(FREE_DB, 'PRAGMA journal_mode = DELETE;');
sql(FREE_DB, 'PRAGMA foreign_keys = OFF;');
// Get existing tables
const tableList = sql(FREE_DB, "SELECT name FROM sqlite_master WHERE type IN ('table', 'view');");
const existingTables = new Set(tableList.split('\n').filter(Boolean));
// Drop tables
console.log('\n Dropping tables:');
for (const table of TABLES_TO_DROP) {
if (existingTables.has(table)) {
sql(FREE_DB, `DROP TABLE IF EXISTS "${table}";`);
console.log(` Dropped: ${table}`);
}
else {
console.log(` Skipped (not present): ${table}`);
}
}
// Drop triggers related to dropped tables
const triggerList = sql(FREE_DB, "SELECT name || '|' || tbl_name FROM sqlite_master WHERE type = 'trigger';");
const droppedTableSet = new Set(TABLES_TO_DROP);
for (const line of triggerList.split('\n').filter(Boolean)) {
const parts = line.split('|');
const triggerName = parts[0];
const tblName = parts[1];
if (tblName && droppedTableSet.has(tblName)) {
sql(FREE_DB, `DROP TRIGGER IF EXISTS "${triggerName}";`);
console.log(` Dropped trigger: ${triggerName}`);
}
}
// Ensure db_metadata table exists and update tier
sql(FREE_DB, "CREATE TABLE IF NOT EXISTS db_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);");
sql(FREE_DB, `INSERT INTO db_metadata (key, value) VALUES ('tier', 'free') ON CONFLICT(key) DO UPDATE SET value = excluded.value;`);
sql(FREE_DB, `INSERT INTO db_metadata (key, value) VALUES ('schema_version', '1') ON CONFLICT(key) DO UPDATE SET value = excluded.value;`);
sql(FREE_DB, `INSERT INTO db_metadata (key, value) VALUES ('built_at', '${new Date().toISOString()}') ON CONFLICT(key) DO UPDATE SET value = excluded.value;`);
sql(FREE_DB, `INSERT INTO db_metadata (key, value) VALUES ('builder', 'build-db-free.ts') ON CONFLICT(key) DO UPDATE SET value = excluded.value;`);
// Report remaining data
const lawCount = sql(FREE_DB, 'SELECT COUNT(*) FROM law_documents;');
console.log(`\n Remaining data:`);
console.log(` Law documents: ${parseInt(lawCount).toLocaleString()}`);
// VACUUM to reclaim space
console.log('\n Running VACUUM (this may take a while)...');
sql(FREE_DB, 'VACUUM;');
sql(FREE_DB, 'ANALYZE;');
const freeSize = fs.statSync(FREE_DB).size;
const reduction = ((1 - freeSize / fullSize) * 100).toFixed(1);
console.log(`\nFree-tier build complete.` +
`\n Size: ${(fullSize / 1024 / 1024).toFixed(1)} MB -> ${(freeSize / 1024 / 1024).toFixed(1)} MB (${reduction}% reduction)` +
`\n Tier: free` +
`\n Output: ${FREE_DB}`);
// Warn if too large for Vercel
const VERCEL_LIMIT = 250 * 1024 * 1024;
if (freeSize > VERCEL_LIMIT) {
console.warn(`\n WARNING: Free-tier database is ${(freeSize / 1024 / 1024).toFixed(0)} MB.` +
`\n This may be too large for Vercel Hobby plan (512 MB /tmp limit).` +
`\n Consider further data trimming if deployment fails.`);
}
}
buildFreeTier();