feat: production MCP server with Israeli legislation (multi-source)

Complete production implementation with shell+adapter architecture,
13 MCP tools, SQLite FTS5 search, and multi-source ingestion pipeline.

Ingestion fetches from UCI mirror, UNODC SHERLOC PDFs, and Knesset
mobile PDFs (135 provisions, 33 definitions). 3 acts with full text,
7 acts metadata-only due to gov.il/nevo.co.il access restrictions.
Knesset OData API used for metadata enrichment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mortalus
2026-02-19 20:40:01 +01:00
parent 21aa81d2b0
commit 1e28f8a6b1
41 changed files with 9136 additions and 51 deletions

64
src/capabilities.ts Normal file
View File

@@ -0,0 +1,64 @@
/**
* Runtime capability detection for Israel Law MCP.
* Detects which database tables are available to enable/disable features.
*/
import type Database from '@ansvar/mcp-sqlite';
export type Capability =
| 'core_legislation'
| 'eu_references'
| 'case_law'
| 'preparatory_works';
const TABLE_MAP: Record<Capability, string[]> = {
core_legislation: ['legal_documents', 'legal_provisions', 'provisions_fts'],
eu_references: ['eu_documents', 'eu_references'],
case_law: ['case_law'],
preparatory_works: ['preparatory_works'],
};
export function detectCapabilities(db: InstanceType<typeof Database>): Set<Capability> {
const caps = new Set<Capability>();
const tables = new Set(
(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[])
.map(r => r.name)
);
for (const [cap, required] of Object.entries(TABLE_MAP)) {
if (required.every(t => tables.has(t))) {
caps.add(cap as Capability);
}
}
return caps;
}
export interface DbMetadata {
tier: string;
schema_version: string;
built_at?: string;
builder?: string;
}
export function readDbMetadata(db: InstanceType<typeof Database>): DbMetadata {
const meta: Record<string, string> = {};
try {
const rows = db.prepare('SELECT key, value FROM db_metadata').all() as { key: string; value: string }[];
for (const row of rows) {
meta[row.key] = row.value;
}
} catch {
// db_metadata table may not exist
}
return {
tier: meta.tier ?? 'free',
schema_version: meta.schema_version ?? '1.0',
built_at: meta.built_at,
builder: meta.builder,
};
}
export function upgradeMessage(feature: string): string {
return `The "${feature}" feature requires a professional-tier database. Contact hello@ansvar.ai for access.`;
}

6
src/constants.ts Normal file
View File

@@ -0,0 +1,6 @@
export const SERVER_NAME = 'israel-law-mcp';
export const SERVER_VERSION = '1.0.0';
export const SERVER_LABEL = 'Israel Law MCP';
export const PACKAGE_NAME = '@ansvar/israel-law-mcp';
export const REPOSITORY_URL = 'https://github.com/Ansvar-Systems/israel-law-mcp';
export const DB_ENV_VAR = 'ISRAEL_LAW_DB_PATH';

102
src/index.ts Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env node
/**
* Israel Law MCP Server -- stdio entry point.
*
* Provides Israeli legislation search via Model Context Protocol.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import Database from '@ansvar/mcp-sqlite';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import { registerTools, type AboutContext } from './tools/registry.js';
import { detectCapabilities, readDbMetadata } from './capabilities.js';
import {
DB_ENV_VAR,
SERVER_NAME,
SERVER_VERSION,
} from './constants.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
function resolveDbPath(): string {
if (process.env[DB_ENV_VAR]) {
return process.env[DB_ENV_VAR];
}
return join(__dirname, '..', 'data', 'database.db');
}
let db: InstanceType<typeof Database> | null = null;
function getDb(): InstanceType<typeof Database> {
if (!db) {
const dbPath = resolveDbPath();
db = new Database(dbPath, { readonly: true });
db.pragma('foreign_keys = ON');
const caps = detectCapabilities(db);
const meta = readDbMetadata(db);
console.error(`[${SERVER_NAME}] DB opened: tier=${meta.tier}, caps=[${[...caps].join(',')}]`);
}
return db;
}
function computeAboutContext(): AboutContext {
const dbPath = resolveDbPath();
let fingerprint = 'unknown';
let dbBuilt = 'unknown';
try {
const buf = readFileSync(dbPath);
fingerprint = createHash('sha256').update(buf).digest('hex').slice(0, 12);
} catch {
// DB might not exist in dev
}
try {
const database = getDb();
const row = database.prepare("SELECT value FROM db_metadata WHERE key = 'built_at'").get() as { value: string } | undefined;
if (row) dbBuilt = row.value;
} catch {
// Ignore
}
return { version: SERVER_VERSION, fingerprint, dbBuilt };
}
async function main() {
const database = getDb();
const aboutContext = computeAboutContext();
const server = new Server(
{ name: SERVER_NAME, version: SERVER_VERSION },
{ capabilities: { tools: {} } }
);
registerTools(server, database, aboutContext);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[${SERVER_NAME}] Server running on stdio`);
const cleanup = () => {
if (db) {
db.close();
db = null;
}
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}
main().catch((err) => {
console.error(`[${SERVER_NAME}] Fatal error:`, err);
process.exit(1);
});

55
src/tools/about.ts Normal file
View File

@@ -0,0 +1,55 @@
/**
* about -- Server metadata, dataset statistics, and provenance.
*/
import type Database from '@ansvar/mcp-sqlite';
import { detectCapabilities, readDbMetadata } from '../capabilities.js';
import { SERVER_NAME, SERVER_VERSION, REPOSITORY_URL } from '../constants.js';
export interface AboutContext {
version: string;
fingerprint: string;
dbBuilt: string;
}
function safeCount(db: InstanceType<typeof Database>, sql: string): number {
try {
const row = db.prepare(sql).get() as { count: number } | undefined;
return row ? Number(row.count) : 0;
} catch {
return 0;
}
}
export function getAbout(db: InstanceType<typeof Database>, context: AboutContext) {
const caps = detectCapabilities(db);
const meta = readDbMetadata(db);
return {
server: SERVER_NAME,
version: context.version,
repository: REPOSITORY_URL,
database: {
fingerprint: context.fingerprint,
built_at: context.dbBuilt,
tier: meta.tier,
schema_version: meta.schema_version,
capabilities: [...caps],
},
statistics: {
documents: safeCount(db, 'SELECT COUNT(*) as count FROM legal_documents'),
provisions: safeCount(db, 'SELECT COUNT(*) as count FROM legal_provisions'),
definitions: safeCount(db, 'SELECT COUNT(*) as count FROM definitions'),
eu_documents: safeCount(db, 'SELECT COUNT(*) as count FROM eu_documents'),
eu_references: safeCount(db, 'SELECT COUNT(*) as count FROM eu_references'),
},
data_source: {
name: 'Knesset Legislation Database',
authority: 'The Knesset (Israeli Parliament)',
url: 'https://main.knesset.gov.il/Activity/Legislation',
license: 'Government Open Data',
jurisdiction: 'IL',
languages: ['he', 'en'],
},
};
}

View File

@@ -0,0 +1,72 @@
/**
* build_legal_stance -- Build a comprehensive set of citations for a legal question.
*/
import type Database from '@ansvar/mcp-sqlite';
import { buildFtsQueryVariants, sanitizeFtsInput } from '../utils/fts-query.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface BuildLegalStanceInput {
query: string;
document_id?: string;
limit?: number;
}
export interface LegalStanceResult {
document_id: string;
document_title: string;
provision_ref: string;
section: string;
title: string | null;
snippet: string;
relevance: number;
}
export async function buildLegalStance(
db: InstanceType<typeof Database>,
input: BuildLegalStanceInput,
): Promise<ToolResponse<LegalStanceResult[]>> {
if (!input.query || input.query.trim().length === 0) {
return { results: [], _metadata: generateResponseMetadata(db) };
}
const limit = Math.min(Math.max(input.limit ?? 5, 1), 20);
const queryVariants = buildFtsQueryVariants(sanitizeFtsInput(input.query));
for (const ftsQuery of queryVariants) {
let sql = `
SELECT
lp.document_id,
ld.title as document_title,
lp.provision_ref,
lp.section,
lp.title,
snippet(provisions_fts, 0, '>>>', '<<<', '...', 48) as snippet,
bm25(provisions_fts) as relevance
FROM provisions_fts
JOIN legal_provisions lp ON lp.id = provisions_fts.rowid
JOIN legal_documents ld ON ld.id = lp.document_id
WHERE provisions_fts MATCH ?
`;
const params: (string | number)[] = [ftsQuery];
if (input.document_id) {
sql += ' AND lp.document_id = ?';
params.push(input.document_id);
}
sql += ' ORDER BY relevance LIMIT ?';
params.push(limit);
try {
const rows = db.prepare(sql).all(...params) as LegalStanceResult[];
if (rows.length > 0) {
return { results: rows, _metadata: generateResponseMetadata(db) };
}
} catch {
continue;
}
}
return { results: [], _metadata: generateResponseMetadata(db) };
}

View File

@@ -0,0 +1,71 @@
/**
* check_currency -- Check whether an Israeli statute is currently in force.
*/
import type Database from '@ansvar/mcp-sqlite';
import { resolveDocumentId } from '../utils/statute-id.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface CheckCurrencyInput {
document_id: string;
provision_ref?: string;
as_of_date?: string;
}
export interface CheckCurrencyResult {
document_id: string;
title: string;
status: string;
issued_date: string | null;
in_force_date: string | null;
warnings: string[];
}
export async function checkCurrency(
db: InstanceType<typeof Database>,
input: CheckCurrencyInput,
): Promise<ToolResponse<CheckCurrencyResult>> {
const resolvedId = resolveDocumentId(db, input.document_id);
if (!resolvedId) {
return {
results: {
document_id: input.document_id,
title: 'Unknown',
status: 'not_found',
issued_date: null,
in_force_date: null,
warnings: [`Document not found: "${input.document_id}"`],
},
_metadata: generateResponseMetadata(db),
};
}
const doc = db.prepare(
'SELECT id, title, status, issued_date, in_force_date FROM legal_documents WHERE id = ?'
).get(resolvedId) as {
id: string;
title: string;
status: string;
issued_date: string | null;
in_force_date: string | null;
};
const warnings: string[] = [];
if (doc.status === 'repealed') {
warnings.push('This statute has been repealed and is no longer in force.');
} else if (doc.status === 'not_yet_in_force') {
warnings.push('This statute has not yet entered into force.');
}
return {
results: {
document_id: doc.id,
title: doc.title,
status: doc.status,
issued_date: doc.issued_date,
in_force_date: doc.in_force_date,
warnings,
},
_metadata: generateResponseMetadata(db),
};
}

View File

@@ -0,0 +1,55 @@
/**
* format_citation -- Format an Israeli legal citation per standard conventions.
*
* Formats:
* - "full": "Section N, [Law Name Year]"
* - "short": "Section N, [Law Name]"
* - "pinpoint": "\u00a7N"
*/
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
import type Database from '@ansvar/mcp-sqlite';
export interface FormatCitationInput {
citation: string;
format?: 'full' | 'short' | 'pinpoint';
}
export interface FormatCitationResult {
original: string;
formatted: string;
format: string;
}
export async function formatCitationTool(
input: FormatCitationInput,
): Promise<FormatCitationResult> {
const format = input.format ?? 'full';
const trimmed = input.citation.trim();
// Parse "Section N, <law>" or "Section N <law>"
const sectionFirst = trimmed.match(/^Section\s+(\d+[A-Za-z]*(?:\(\d+\))?)[,;]?\s+(.+)$/i);
// Parse "<law>, Section N" or "<law> Section N"
const sectionLast = trimmed.match(/^(.+?)[,;]?\s*Section\s+(\d+[A-Za-z]*(?:\(\d+\))?)$/i);
// Parse "\u00a7N <law>"
const paraFirst = trimmed.match(/^\u00a7\s*(\d+[A-Za-z]*(?:\(\d+\))?)[,;]?\s+(.+)$/);
const section = sectionFirst?.[1] ?? sectionLast?.[2] ?? paraFirst?.[1];
const law = sectionFirst?.[2] ?? sectionLast?.[1] ?? paraFirst?.[2] ?? trimmed;
let formatted: string;
switch (format) {
case 'short':
formatted = section ? `Section ${section}, ${law.split('(')[0].trim()}` : law;
break;
case 'pinpoint':
formatted = section ? `\u00a7${section}` : law;
break;
case 'full':
default:
formatted = section ? `Section ${section}, ${law}` : law;
break;
}
return { original: input.citation, formatted, format };
}

81
src/tools/get-eu-basis.ts Normal file
View File

@@ -0,0 +1,81 @@
/**
* get_eu_basis -- Get EU legal basis for an Israeli statute.
*/
import type Database from '@ansvar/mcp-sqlite';
import { resolveDocumentId } from '../utils/statute-id.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface GetEUBasisInput {
document_id: string;
include_articles?: boolean;
reference_types?: string[];
}
export interface EUBasisResult {
eu_document_id: string;
eu_document_type: string;
eu_document_title: string | null;
reference_type: string;
reference_count: number;
implementation_status: string | null;
articles?: string[];
}
export async function getEUBasis(
db: InstanceType<typeof Database>,
input: GetEUBasisInput,
): Promise<ToolResponse<EUBasisResult[]>> {
const resolvedId = resolveDocumentId(db, input.document_id);
if (!resolvedId) {
return { results: [], _metadata: generateResponseMetadata(db) };
}
// Check if EU reference tables exist
try {
db.prepare('SELECT 1 FROM eu_references LIMIT 1').get();
} catch {
return {
results: [],
_metadata: {
...generateResponseMetadata(db),
...{ note: 'EU references not available in this database tier' },
},
};
}
let sql = `
SELECT
er.eu_document_id,
ed.type as eu_document_type,
COALESCE(ed.title, ed.short_name) as eu_document_title,
er.reference_type,
COUNT(*) as reference_count,
MAX(er.implementation_status) as implementation_status
FROM eu_references er
LEFT JOIN eu_documents ed ON ed.id = er.eu_document_id
WHERE er.document_id = ?
`;
const params: string[] = [resolvedId];
if (input.reference_types && input.reference_types.length > 0) {
const placeholders = input.reference_types.map(() => '?').join(', ');
sql += ` AND er.reference_type IN (${placeholders})`;
params.push(...input.reference_types);
}
sql += ' GROUP BY er.eu_document_id, er.reference_type ORDER BY reference_count DESC';
const rows = db.prepare(sql).all(...params) as EUBasisResult[];
if (input.include_articles) {
for (const row of rows) {
const articles = db.prepare(
'SELECT DISTINCT eu_article FROM eu_references WHERE document_id = ? AND eu_document_id = ? AND eu_article IS NOT NULL'
).all(resolvedId, row.eu_document_id) as { eu_article: string }[];
row.articles = articles.map(a => a.eu_article);
}
}
return { results: rows, _metadata: generateResponseMetadata(db) };
}

View File

@@ -0,0 +1,71 @@
/**
* get_israeli_implementations -- Find Israeli statutes implementing a specific EU directive/regulation.
*
* Note: Israel is not an EU member but has an EU adequacy decision for data protection.
* Israeli laws may align with or reference EU directives/regulations, particularly
* in data protection (GDPR adequacy) and electronic signatures.
*/
import type Database from '@ansvar/mcp-sqlite';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface GetIsraeliImplementationsInput {
eu_document_id: string;
primary_only?: boolean;
in_force_only?: boolean;
}
export interface IsraeliImplementationResult {
document_id: string;
document_title: string;
status: string;
reference_type: string;
implementation_status: string | null;
is_primary: boolean;
reference_count: number;
}
export async function getIsraeliImplementations(
db: InstanceType<typeof Database>,
input: GetIsraeliImplementationsInput,
): Promise<ToolResponse<IsraeliImplementationResult[]>> {
try {
db.prepare('SELECT 1 FROM eu_references LIMIT 1').get();
} catch {
return {
results: [],
_metadata: {
...generateResponseMetadata(db),
...{ note: 'EU references not available in this database tier' },
},
};
}
let sql = `
SELECT
ld.id as document_id,
ld.title as document_title,
ld.status,
er.reference_type,
MAX(er.implementation_status) as implementation_status,
MAX(er.is_primary_implementation) as is_primary,
COUNT(*) as reference_count
FROM eu_references er
JOIN legal_documents ld ON ld.id = er.document_id
WHERE er.eu_document_id = ?
`;
const params: (string | number)[] = [input.eu_document_id];
if (input.primary_only) {
sql += ' AND er.is_primary_implementation = 1';
}
if (input.in_force_only) {
sql += " AND ld.status = 'in_force'";
}
sql += ' GROUP BY ld.id, er.reference_type ORDER BY is_primary DESC, reference_count DESC';
const rows = db.prepare(sql).all(...params) as IsraeliImplementationResult[];
return { results: rows, _metadata: generateResponseMetadata(db) };
}

View File

@@ -0,0 +1,71 @@
/**
* get_provision_eu_basis -- Get EU legal basis for a specific provision.
*/
import type Database from '@ansvar/mcp-sqlite';
import { resolveDocumentId } from '../utils/statute-id.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface GetProvisionEUBasisInput {
document_id: string;
provision_ref: string;
}
export interface ProvisionEUBasisResult {
eu_document_id: string;
eu_document_type: string;
eu_document_title: string | null;
eu_article: string | null;
reference_type: string;
reference_context: string | null;
full_citation: string | null;
}
export async function getProvisionEUBasis(
db: InstanceType<typeof Database>,
input: GetProvisionEUBasisInput,
): Promise<ToolResponse<ProvisionEUBasisResult[]>> {
const resolvedId = resolveDocumentId(db, input.document_id);
if (!resolvedId) {
return { results: [], _metadata: generateResponseMetadata(db) };
}
try {
db.prepare('SELECT 1 FROM eu_references LIMIT 1').get();
} catch {
return {
results: [],
_metadata: {
...generateResponseMetadata(db),
...{ note: 'EU references not available in this database tier' },
},
};
}
// Find the provision
const ref = input.provision_ref.trim();
const provision = db.prepare(
"SELECT id FROM legal_provisions WHERE document_id = ? AND (provision_ref = ? OR provision_ref = ? OR section = ?)"
).get(resolvedId, ref, `sec${ref}`, ref) as { id: number } | undefined;
if (!provision) {
return { results: [], _metadata: generateResponseMetadata(db) };
}
const rows = db.prepare(`
SELECT
er.eu_document_id,
ed.type as eu_document_type,
COALESCE(ed.title, ed.short_name) as eu_document_title,
er.eu_article,
er.reference_type,
er.reference_context,
er.full_citation
FROM eu_references er
LEFT JOIN eu_documents ed ON ed.id = er.eu_document_id
WHERE er.provision_id = ?
ORDER BY er.reference_type, er.eu_document_id
`).all(provision.id) as ProvisionEUBasisResult[];
return { results: rows, _metadata: generateResponseMetadata(db) };
}

126
src/tools/get-provision.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* get_provision -- Retrieve specific provision(s) from an Israeli statute.
*/
import type Database from '@ansvar/mcp-sqlite';
import { resolveDocumentId } from '../utils/statute-id.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface GetProvisionInput {
document_id: string;
section?: string;
provision_ref?: string;
as_of_date?: string;
}
export interface ProvisionResult {
document_id: string;
document_title: string;
provision_ref: string;
chapter: string | null;
section: string;
title: string | null;
content: string;
article_number?: string;
url?: string;
}
export async function getProvision(
db: InstanceType<typeof Database>,
input: GetProvisionInput,
): Promise<ToolResponse<ProvisionResult[]>> {
const resolvedId = resolveDocumentId(db, input.document_id);
if (!resolvedId) {
return {
results: [],
_metadata: {
...generateResponseMetadata(db),
...{ note: `No document found matching "${input.document_id}"` },
},
};
}
const docRow = db.prepare(
'SELECT id, title, url FROM legal_documents WHERE id = ?'
).get(resolvedId) as { id: string; title: string; url: string | null } | undefined;
if (!docRow) {
return { results: [], _metadata: generateResponseMetadata(db) };
}
// Specific provision lookup
const ref = input.provision_ref ?? input.section;
if (ref) {
const refTrimmed = ref.trim();
// Try direct provision_ref match
let provision = db.prepare(
'SELECT * FROM legal_provisions WHERE document_id = ? AND provision_ref = ?'
).get(resolvedId, refTrimmed) as Record<string, unknown> | undefined;
// Try with "sec" prefix (e.g., "1" -> "sec1")
if (!provision) {
provision = db.prepare(
'SELECT * FROM legal_provisions WHERE document_id = ? AND provision_ref = ?'
).get(resolvedId, `sec${refTrimmed}`) as Record<string, unknown> | undefined;
}
// Try section column match
if (!provision) {
provision = db.prepare(
'SELECT * FROM legal_provisions WHERE document_id = ? AND section = ?'
).get(resolvedId, refTrimmed) as Record<string, unknown> | undefined;
}
// Try LIKE match for flexible input
if (!provision) {
provision = db.prepare(
"SELECT * FROM legal_provisions WHERE document_id = ? AND (provision_ref LIKE ? OR section LIKE ?)"
).get(resolvedId, `%${refTrimmed}%`, `%${refTrimmed}%`) as Record<string, unknown> | undefined;
}
if (provision) {
return {
results: [{
document_id: resolvedId,
document_title: docRow.title,
provision_ref: String(provision.provision_ref),
chapter: provision.chapter as string | null,
section: String(provision.section),
title: provision.title as string | null,
content: String(provision.content),
article_number: String(provision.provision_ref).replace(/^sec/, ''),
url: docRow.url ?? undefined,
}],
_metadata: generateResponseMetadata(db),
};
}
return {
results: [],
_metadata: {
...generateResponseMetadata(db),
...{ note: `Provision "${ref}" not found in document "${resolvedId}"` },
},
};
}
// Return all provisions for the document
const provisions = db.prepare(
'SELECT * FROM legal_provisions WHERE document_id = ? ORDER BY id'
).all(resolvedId) as Record<string, unknown>[];
return {
results: provisions.map(p => ({
document_id: resolvedId,
document_title: docRow.title,
provision_ref: String(p.provision_ref),
chapter: p.chapter as string | null,
section: String(p.section),
title: p.title as string | null,
content: String(p.content),
article_number: String(p.provision_ref).replace(/^sec/, ''),
url: docRow.url ?? undefined,
})),
_metadata: generateResponseMetadata(db),
};
}

77
src/tools/list-sources.ts Normal file
View File

@@ -0,0 +1,77 @@
/**
* list_sources -- Return provenance metadata for all data sources.
*/
import type Database from '@ansvar/mcp-sqlite';
import { readDbMetadata } from '../capabilities.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface SourceInfo {
name: string;
authority: string;
url: string;
license: string;
coverage: string;
languages: string[];
}
export interface ListSourcesResult {
sources: SourceInfo[];
database: {
tier: string;
schema_version: string;
built_at?: string;
document_count: number;
provision_count: number;
};
}
function safeCount(db: InstanceType<typeof Database>, sql: string): number {
try {
const row = db.prepare(sql).get() as { count: number } | undefined;
return row ? Number(row.count) : 0;
} catch {
return 0;
}
}
export async function listSources(
db: InstanceType<typeof Database>,
): Promise<ToolResponse<ListSourcesResult>> {
const meta = readDbMetadata(db);
return {
results: {
sources: [
{
name: 'Knesset Legislation Database',
authority: 'The Knesset (Israeli Parliament)',
url: 'https://main.knesset.gov.il/Activity/Legislation',
license: 'Government Open Data',
coverage:
'All Israeli primary legislation (Chukkim), Basic Laws (Chukei Yesod), ' +
'and selected regulatory frameworks published in Sefer HaChukkim (Book of Laws)',
languages: ['he', 'en'],
},
{
name: 'Israeli Government Legal Information',
authority: 'Government of Israel',
url: 'https://www.gov.il/en/departments/legalinfo',
license: 'Government Publication',
coverage:
'English translations of major Israeli laws including Basic Laws, Privacy Protection Law, ' +
'Companies Law, and key regulatory frameworks',
languages: ['en'],
},
],
database: {
tier: meta.tier,
schema_version: meta.schema_version,
built_at: meta.built_at,
document_count: safeCount(db, 'SELECT COUNT(*) as count FROM legal_documents'),
provision_count: safeCount(db, 'SELECT COUNT(*) as count FROM legal_provisions'),
},
},
_metadata: generateResponseMetadata(db),
};
}

406
src/tools/registry.ts Normal file
View File

@@ -0,0 +1,406 @@
/**
* Tool registry for Israel Law MCP Server.
* Shared between stdio (index.ts) and HTTP (api/mcp.ts) entry points.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import type Database from '@ansvar/mcp-sqlite';
import { searchLegislation, type SearchLegislationInput } from './search-legislation.js';
import { getProvision, type GetProvisionInput } from './get-provision.js';
import { validateCitationTool, type ValidateCitationInput } from './validate-citation.js';
import { buildLegalStance, type BuildLegalStanceInput } from './build-legal-stance.js';
import { formatCitationTool, type FormatCitationInput } from './format-citation.js';
import { checkCurrency, type CheckCurrencyInput } from './check-currency.js';
import { getEUBasis, type GetEUBasisInput } from './get-eu-basis.js';
import { getIsraeliImplementations, type GetIsraeliImplementationsInput } from './get-israeli-implementations.js';
import { searchEUImplementations, type SearchEUImplementationsInput } from './search-eu-implementations.js';
import { getProvisionEUBasis, type GetProvisionEUBasisInput } from './get-provision-eu-basis.js';
import { validateEUCompliance, type ValidateEUComplianceInput } from './validate-eu-compliance.js';
import { listSources } from './list-sources.js';
import { getAbout, type AboutContext } from './about.js';
import { detectCapabilities, upgradeMessage } from '../capabilities.js';
export type { AboutContext } from './about.js';
const ABOUT_TOOL: Tool = {
name: 'about',
description:
'Server metadata, dataset statistics, freshness, and provenance. ' +
'Call this to verify data coverage, currency, and content basis before relying on results.',
inputSchema: { type: 'object', properties: {} },
};
const LIST_SOURCES_TOOL: Tool = {
name: 'list_sources',
description:
'Returns detailed provenance metadata for all data sources used by this server, ' +
'including the Knesset Legislation Database and gov.il English translations. ' +
'Use this to understand what data is available, its authority, coverage scope, and known limitations. ' +
'Also returns dataset statistics (document counts, provision counts) and database build timestamp. ' +
'Call this FIRST when you need to understand what Israeli legal data this server covers.',
inputSchema: { type: 'object', properties: {} },
};
export const TOOLS: Tool[] = [
{
name: 'search_legislation',
description:
'Search Israeli statutes and regulations by keyword using full-text search (FTS5 with BM25 ranking). ' +
'Returns matching provisions with document context, snippets with >>> <<< markers around matched terms, and relevance scores. ' +
'Supports FTS5 syntax: quoted phrases ("exact match"), boolean operators (AND, OR, NOT), and prefix wildcards (term*). ' +
'Results are primarily in English (from official translations) with Hebrew authoritative text where available. ' +
'Default limit is 10 results. For broad topics, increase the limit. ' +
'Do NOT use this for retrieving a known provision -- use get_provision instead.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description:
'Search query in English or Hebrew. Supports FTS5 syntax: ' +
'"privacy" for exact term, "data protection" for phrase, term* for prefix.',
},
document_id: {
type: 'string',
description: 'Optional: filter results to a specific statute by its document ID.',
},
status: {
type: 'string',
enum: ['in_force', 'amended', 'repealed'],
description: 'Optional: filter by legislative status.',
},
limit: {
type: 'number',
description: 'Maximum results to return (default: 10, max: 50).',
default: 10,
},
},
required: ['query'],
},
},
{
name: 'get_provision',
description:
'Retrieve the full text of a specific provision (section) from an Israeli statute. ' +
'Specify a document_id (law name, title, or internal ID) and optionally a section or provision_ref. ' +
'Omit section/provision_ref to get ALL provisions in the statute (use sparingly -- can be large). ' +
'Returns provision text, chapter, section number, and metadata. ' +
'Supports law name references (e.g., "Privacy Protection Law 1981"), abbreviations, and full titles. ' +
'Use this when you know WHICH provision you want. For discovery, use search_legislation instead.',
inputSchema: {
type: 'object',
properties: {
document_id: {
type: 'string',
description:
'Statute identifier: law name + year (e.g., "Privacy Protection Law 1981"), ' +
'full title (e.g., "Protection of Privacy Regulations (Data Security) 2017"), or internal document ID.',
},
section: {
type: 'string',
description: 'Section number (e.g., "1", "7", "2A"). Omit to get all provisions.',
},
provision_ref: {
type: 'string',
description: 'Direct provision reference (e.g., "sec1"). Alternative to section parameter.',
},
},
required: ['document_id'],
},
},
{
name: 'validate_citation',
description:
'Validate an Israeli legal citation against the database -- zero-hallucination check. ' +
'Parses the citation, checks that the document and provision exist, and returns warnings about status ' +
'(repealed, amended). Use this to verify any citation BEFORE including it in a legal analysis. ' +
'Supports formats: "Section N, Privacy Protection Law 1981", "\u00a7N Privacy Protection Law", "\u05e1\u05e2\u05d9\u05e3 N".',
inputSchema: {
type: 'object',
properties: {
citation: {
type: 'string',
description: 'Citation string to validate. Examples: "Section 1, Privacy Protection Law 1981", "\u00a71 Computer Law 1995".',
},
},
required: ['citation'],
},
},
{
name: 'build_legal_stance',
description:
'Build a comprehensive set of citations for a legal question by searching across all Israeli statutes simultaneously. ' +
'Returns aggregated results from multiple relevant provisions, useful for legal research on a topic. ' +
'Use this for broad legal questions like "What are the data security requirements in Israel?" ' +
'rather than looking up a specific known provision.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Legal question or topic to research (e.g., "data security", "privacy protection").',
},
document_id: {
type: 'string',
description: 'Optional: limit search to one statute by document ID.',
},
limit: {
type: 'number',
description: 'Max results per category (default: 5, max: 20).',
default: 5,
},
},
required: ['query'],
},
},
{
name: 'format_citation',
description:
'Format an Israeli legal citation per standard conventions. ' +
'Three formats: "full" (formal, e.g., "Section 1, Privacy Protection Law 1981"), ' +
'"short" (abbreviated), "pinpoint" (section reference only, e.g., "\u00a71").',
inputSchema: {
type: 'object',
properties: {
citation: { type: 'string', description: 'Citation string to format.' },
format: {
type: 'string',
enum: ['full', 'short', 'pinpoint'],
description: 'Output format (default: "full").',
default: 'full',
},
},
required: ['citation'],
},
},
{
name: 'check_currency',
description:
'Check whether an Israeli statute or provision is currently in force, amended, repealed, or not yet in force. ' +
'Returns the document status, issued date, in-force date, and warnings. ' +
'Essential before citing any provision -- always verify currency.',
inputSchema: {
type: 'object',
properties: {
document_id: {
type: 'string',
description: 'Statute identifier (law name, abbreviation, or title).',
},
provision_ref: {
type: 'string',
description: 'Optional: provision reference to check a specific section.',
},
},
required: ['document_id'],
},
},
{
name: 'get_eu_basis',
description:
'Get the EU legal basis that an Israeli statute aligns with or references. ' +
'Israel has an EU adequacy decision for data protection (Privacy Protection Law aligns with GDPR). ' +
'Returns EU document identifiers, reference types, and implementation status.',
inputSchema: {
type: 'object',
properties: {
document_id: { type: 'string', description: 'Israeli statute identifier.' },
include_articles: {
type: 'boolean',
description: 'Include specific EU article references (default: false).',
default: false,
},
},
required: ['document_id'],
},
},
{
name: 'get_israeli_implementations',
description:
'Find all Israeli statutes that align with or reference a specific EU directive or regulation. ' +
'Given an EU document ID (e.g., "regulation:2016/679" for GDPR), returns matching Israeli statutes. ' +
'Note: Israel implements EU law through adequacy recognition and voluntary alignment, not transposition.',
inputSchema: {
type: 'object',
properties: {
eu_document_id: {
type: 'string',
description: 'EU document ID (e.g., "regulation:2016/679" for GDPR, "directive:2016/1148" for NIS).',
},
primary_only: {
type: 'boolean',
description: 'Return only primary aligning statutes (default: false).',
default: false,
},
in_force_only: {
type: 'boolean',
description: 'Return only currently in-force statutes (default: false).',
default: false,
},
},
required: ['eu_document_id'],
},
},
{
name: 'search_eu_implementations',
description:
'Search for EU directives and regulations that have Israeli aligning legislation. ' +
'Search by keyword, type (directive/regulation), or year range.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Keyword search across EU document titles.' },
type: { type: 'string', enum: ['directive', 'regulation'], description: 'Filter by EU document type.' },
year_from: { type: 'number', description: 'Filter by year (from).' },
year_to: { type: 'number', description: 'Filter by year (to).' },
has_israeli_implementation: {
type: 'boolean',
description: 'If true, only return EU documents with Israeli aligning legislation.',
},
limit: { type: 'number', description: 'Max results (default: 20, max: 100).', default: 20 },
},
},
},
{
name: 'get_provision_eu_basis',
description:
'Get the EU legal basis for a SPECIFIC provision within an Israeli statute. ' +
'More granular than get_eu_basis (which operates at the statute level). ' +
'Use this for pinpoint EU compliance checks at the provision level.',
inputSchema: {
type: 'object',
properties: {
document_id: { type: 'string', description: 'Israeli statute identifier.' },
provision_ref: { type: 'string', description: 'Provision reference (e.g., "sec1" or "1").' },
},
required: ['document_id', 'provision_ref'],
},
},
{
name: 'validate_eu_compliance',
description:
'Check EU alignment status for an Israeli statute or provision. ' +
'Detects references to EU directives, adequacy status, and alignment gaps. ' +
'Israel has an EU adequacy decision for data protection. ' +
'Returns compliance status (compliant, partial, unclear, not_applicable) with warnings.',
inputSchema: {
type: 'object',
properties: {
document_id: { type: 'string', description: 'Israeli statute identifier.' },
provision_ref: { type: 'string', description: 'Optional: check for a specific provision.' },
eu_document_id: { type: 'string', description: 'Optional: check against a specific EU document.' },
},
required: ['document_id'],
},
},
];
export function buildTools(
db?: InstanceType<typeof Database>,
context?: AboutContext,
): Tool[] {
const tools = [...TOOLS, LIST_SOURCES_TOOL];
if (db) {
try {
db.prepare('SELECT 1 FROM definitions LIMIT 1').get();
// Could add a get_definitions tool here when definitions table exists
} catch {
// definitions table doesn't exist
}
}
if (context) {
tools.push(ABOUT_TOOL);
}
return tools;
}
export function registerTools(
server: Server,
db: InstanceType<typeof Database>,
context?: AboutContext,
): void {
const allTools = buildTools(db, context);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: allTools };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: unknown;
switch (name) {
case 'search_legislation':
result = await searchLegislation(db, args as unknown as SearchLegislationInput);
break;
case 'get_provision':
result = await getProvision(db, args as unknown as GetProvisionInput);
break;
case 'validate_citation':
result = await validateCitationTool(db, args as unknown as ValidateCitationInput);
break;
case 'build_legal_stance':
result = await buildLegalStance(db, args as unknown as BuildLegalStanceInput);
break;
case 'format_citation':
result = await formatCitationTool(args as unknown as FormatCitationInput);
break;
case 'check_currency':
result = await checkCurrency(db, args as unknown as CheckCurrencyInput);
break;
case 'get_eu_basis':
result = await getEUBasis(db, args as unknown as GetEUBasisInput);
break;
case 'get_israeli_implementations':
result = await getIsraeliImplementations(db, args as unknown as GetIsraeliImplementationsInput);
break;
case 'search_eu_implementations':
result = await searchEUImplementations(db, args as unknown as SearchEUImplementationsInput);
break;
case 'get_provision_eu_basis':
result = await getProvisionEUBasis(db, args as unknown as GetProvisionEUBasisInput);
break;
case 'validate_eu_compliance':
result = await validateEUCompliance(db, args as unknown as ValidateEUComplianceInput);
break;
case 'list_sources':
result = await listSources(db);
break;
case 'about':
if (context) {
result = getAbout(db, context);
} else {
return {
content: [{ type: 'text' as const, text: 'About tool not configured.' }],
isError: true,
};
}
break;
default:
return {
content: [{ type: 'text' as const, text: `Error: Unknown tool "${name}".` }],
isError: true,
};
}
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text' as const, text: `Error: ${message}` }],
isError: true,
};
}
});
}

View File

@@ -0,0 +1,91 @@
/**
* search_eu_implementations -- Search EU directives/regulations with Israeli aligning legislation.
*/
import type Database from '@ansvar/mcp-sqlite';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface SearchEUImplementationsInput {
query?: string;
type?: 'directive' | 'regulation';
year_from?: number;
year_to?: number;
has_israeli_implementation?: boolean;
limit?: number;
}
export interface EUImplementationSearchResult {
eu_document_id: string;
type: string;
year: number;
number: number;
title: string | null;
short_name: string | null;
israeli_statute_count: number;
}
export async function searchEUImplementations(
db: InstanceType<typeof Database>,
input: SearchEUImplementationsInput,
): Promise<ToolResponse<EUImplementationSearchResult[]>> {
try {
db.prepare('SELECT 1 FROM eu_documents LIMIT 1').get();
} catch {
return {
results: [],
_metadata: {
...generateResponseMetadata(db),
...{ note: 'EU documents not available in this database tier' },
},
};
}
const limit = Math.min(Math.max(input.limit ?? 20, 1), 100);
let sql = `
SELECT
ed.id as eu_document_id,
ed.type,
ed.year,
ed.number,
ed.title,
ed.short_name,
COUNT(DISTINCT er.document_id) as israeli_statute_count
FROM eu_documents ed
LEFT JOIN eu_references er ON er.eu_document_id = ed.id
WHERE 1=1
`;
const params: (string | number)[] = [];
if (input.query) {
sql += ' AND (ed.title LIKE ? OR ed.short_name LIKE ? OR ed.description LIKE ?)';
params.push(`%${input.query}%`, `%${input.query}%`, `%${input.query}%`);
}
if (input.type) {
sql += ' AND ed.type = ?';
params.push(input.type);
}
if (input.year_from) {
sql += ' AND ed.year >= ?';
params.push(input.year_from);
}
if (input.year_to) {
sql += ' AND ed.year <= ?';
params.push(input.year_to);
}
sql += ' GROUP BY ed.id';
if (input.has_israeli_implementation) {
sql += ' HAVING israeli_statute_count > 0';
}
sql += ' ORDER BY ed.year DESC, ed.number DESC LIMIT ?';
params.push(limit);
const rows = db.prepare(sql).all(...params) as EUImplementationSearchResult[];
return { results: rows, _metadata: generateResponseMetadata(db) };
}

View File

@@ -0,0 +1,86 @@
/**
* search_legislation -- Full-text search across Israeli statute provisions.
*/
import type Database from '@ansvar/mcp-sqlite';
import { buildFtsQueryVariants, sanitizeFtsInput } from '../utils/fts-query.js';
import { normalizeAsOfDate } from '../utils/as-of-date.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface SearchLegislationInput {
query: string;
document_id?: string;
status?: string;
as_of_date?: string;
limit?: number;
}
export interface SearchLegislationResult {
document_id: string;
document_title: string;
provision_ref: string;
chapter: string | null;
section: string;
title: string | null;
snippet: string;
relevance: number;
}
const DEFAULT_LIMIT = 10;
const MAX_LIMIT = 50;
export async function searchLegislation(
db: InstanceType<typeof Database>,
input: SearchLegislationInput,
): Promise<ToolResponse<SearchLegislationResult[]>> {
if (!input.query || input.query.trim().length === 0) {
return { results: [], _metadata: generateResponseMetadata(db) };
}
const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
const queryVariants = buildFtsQueryVariants(sanitizeFtsInput(input.query));
for (const ftsQuery of queryVariants) {
let sql = `
SELECT
lp.document_id,
ld.title as document_title,
lp.provision_ref,
lp.chapter,
lp.section,
lp.title,
snippet(provisions_fts, 0, '>>>', '<<<', '...', 32) as snippet,
bm25(provisions_fts) as relevance
FROM provisions_fts
JOIN legal_provisions lp ON lp.id = provisions_fts.rowid
JOIN legal_documents ld ON ld.id = lp.document_id
WHERE provisions_fts MATCH ?
`;
const params: (string | number)[] = [ftsQuery];
if (input.document_id) {
sql += ' AND lp.document_id = ?';
params.push(input.document_id);
}
if (input.status) {
sql += ' AND ld.status = ?';
params.push(input.status);
}
sql += ' ORDER BY relevance LIMIT ?';
params.push(limit);
try {
const rows = db.prepare(sql).all(...params) as SearchLegislationResult[];
if (rows.length > 0) {
return { results: rows, _metadata: generateResponseMetadata(db) };
}
} catch {
// FTS query syntax error -- try next variant
continue;
}
}
return { results: [], _metadata: generateResponseMetadata(db) };
}

View File

@@ -0,0 +1,161 @@
/**
* validate_citation -- Validate an Israeli legal citation against the database.
*
* Supports citation formats:
* - "Section N, [Law Name Year]"
* - "Section N [Law Name]"
* - "\u00a7N [Law Name]"
* - "\u05e1\u05e2\u05d9\u05e3 N" (Hebrew: se'if N)
*/
import type Database from '@ansvar/mcp-sqlite';
import { resolveDocumentId } from '../utils/statute-id.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface ValidateCitationInput {
citation: string;
}
export interface ValidateCitationResult {
valid: boolean;
citation: string;
normalized?: string;
document_id?: string;
document_title?: string;
provision_ref?: string;
status?: string;
warnings: string[];
}
/**
* Parse an Israeli legal citation.
* Supports:
* - "Section N, Law Name Year" / "Section N Law Name"
* - "\u00a7N Law Name" / "\u00a7 N Law Name"
* - "\u05e1\u05e2\u05d9\u05e3 N, Law Name" (Hebrew)
* - "Law Name, Section N"
* - Just a law name
*/
function parseCitation(citation: string): { documentRef: string; sectionRef?: string } | null {
const trimmed = citation.trim();
// "Section N, <law>" or "Section N <law>"
const sectionFirst = trimmed.match(/^Section\s+(\d+[A-Za-z]*(?:\(\d+\))?)[,;]?\s+(.+)$/i);
if (sectionFirst) {
return { documentRef: sectionFirst[2].trim(), sectionRef: sectionFirst[1] };
}
// "\u00a7N <law>" or "\u00a7 N <law>"
const paraFirst = trimmed.match(/^\u00a7\s*(\d+[A-Za-z]*(?:\(\d+\))?)[,;]?\s+(.+)$/);
if (paraFirst) {
return { documentRef: paraFirst[2].trim(), sectionRef: paraFirst[1] };
}
// "\u05e1\u05e2\u05d9\u05e3 N, <law>" (Hebrew: se'if)
const hebrewSection = trimmed.match(/^\u05e1\u05e2\u05d9\u05e3\s+(\d+[A-Za-z]*(?:\(\d+\))?)[,;]?\s+(.+)$/);
if (hebrewSection) {
return { documentRef: hebrewSection[2].trim(), sectionRef: hebrewSection[1] };
}
// "<law>, Section N" or "<law> Section N"
const sectionLast = trimmed.match(/^(.+?)[,;]?\s*Section\s+(\d+[A-Za-z]*(?:\(\d+\))?)$/i);
if (sectionLast) {
return { documentRef: sectionLast[1].trim(), sectionRef: sectionLast[2] };
}
// "<law>, \u00a7N" or "<law> \u00a7N"
const paraLast = trimmed.match(/^(.+?)[,;]?\s*\u00a7\s*(\d+[A-Za-z]*(?:\(\d+\))?)$/);
if (paraLast) {
return { documentRef: paraLast[1].trim(), sectionRef: paraLast[2] };
}
// Just a document reference
return { documentRef: trimmed };
}
export async function validateCitationTool(
db: InstanceType<typeof Database>,
input: ValidateCitationInput,
): Promise<ToolResponse<ValidateCitationResult>> {
const warnings: string[] = [];
const parsed = parseCitation(input.citation);
if (!parsed) {
return {
results: {
valid: false,
citation: input.citation,
warnings: ['Could not parse citation format'],
},
_metadata: generateResponseMetadata(db),
};
}
const docId = resolveDocumentId(db, parsed.documentRef);
if (!docId) {
return {
results: {
valid: false,
citation: input.citation,
warnings: [`Document not found: "${parsed.documentRef}"`],
},
_metadata: generateResponseMetadata(db),
};
}
const doc = db.prepare(
'SELECT id, title, status FROM legal_documents WHERE id = ?'
).get(docId) as { id: string; title: string; status: string };
if (doc.status === 'repealed') {
warnings.push(`WARNING: This statute has been repealed.`);
} else if (doc.status === 'amended') {
warnings.push(`Note: This statute has been amended. Verify you are referencing the current version.`);
}
if (parsed.sectionRef) {
const provision = db.prepare(
"SELECT provision_ref FROM legal_provisions WHERE document_id = ? AND (provision_ref = ? OR provision_ref = ? OR section = ?)"
).get(docId, parsed.sectionRef, `sec${parsed.sectionRef}`, parsed.sectionRef) as { provision_ref: string } | undefined;
if (!provision) {
return {
results: {
valid: false,
citation: input.citation,
document_id: docId,
document_title: doc.title,
warnings: [...warnings, `Provision "${parsed.sectionRef}" not found in ${doc.title}`],
},
_metadata: generateResponseMetadata(db),
};
}
return {
results: {
valid: true,
citation: input.citation,
normalized: `Section ${parsed.sectionRef}, ${doc.title}`,
document_id: docId,
document_title: doc.title,
provision_ref: provision.provision_ref,
status: doc.status,
warnings,
},
_metadata: generateResponseMetadata(db),
};
}
return {
results: {
valid: true,
citation: input.citation,
normalized: doc.title,
document_id: docId,
document_title: doc.title,
status: doc.status,
warnings,
},
_metadata: generateResponseMetadata(db),
};
}

View File

@@ -0,0 +1,134 @@
/**
* validate_eu_compliance -- Check EU alignment status for an Israeli statute.
*
* Israel has an EU adequacy decision for data protection, meaning the European
* Commission recognizes Israel's data protection framework as providing adequate
* safeguards. This tool checks alignment status for Israeli statutes.
*/
import type Database from '@ansvar/mcp-sqlite';
import { resolveDocumentId } from '../utils/statute-id.js';
import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js';
export interface ValidateEUComplianceInput {
document_id: string;
provision_ref?: string;
eu_document_id?: string;
}
export interface EUComplianceResult {
document_id: string;
document_title: string;
compliance_status: 'compliant' | 'partial' | 'unclear' | 'not_applicable';
eu_references_found: number;
warnings: string[];
recommendations: string[];
}
export async function validateEUCompliance(
db: InstanceType<typeof Database>,
input: ValidateEUComplianceInput,
): Promise<ToolResponse<EUComplianceResult>> {
const resolvedId = resolveDocumentId(db, input.document_id);
if (!resolvedId) {
return {
results: {
document_id: input.document_id,
document_title: 'Unknown',
compliance_status: 'not_applicable',
eu_references_found: 0,
warnings: [`Document not found: "${input.document_id}"`],
recommendations: [],
},
_metadata: generateResponseMetadata(db),
};
}
const doc = db.prepare(
'SELECT id, title, status FROM legal_documents WHERE id = ?'
).get(resolvedId) as { id: string; title: string; status: string };
const warnings: string[] = [];
const recommendations: string[] = [];
// Check if EU reference tables exist
let euRefCount = 0;
try {
let sql = 'SELECT COUNT(*) as count FROM eu_references WHERE document_id = ?';
const params: string[] = [resolvedId];
if (input.eu_document_id) {
sql += ' AND eu_document_id = ?';
params.push(input.eu_document_id);
}
const row = db.prepare(sql).get(...params) as { count: number };
euRefCount = row.count;
} catch {
return {
results: {
document_id: resolvedId,
document_title: doc.title,
compliance_status: 'not_applicable',
eu_references_found: 0,
warnings: ['EU references not available in this database tier'],
recommendations: [],
},
_metadata: generateResponseMetadata(db),
};
}
if (euRefCount === 0) {
return {
results: {
document_id: resolvedId,
document_title: doc.title,
compliance_status: 'not_applicable',
eu_references_found: 0,
warnings: [],
recommendations: ['No EU cross-references found for this statute. This may be a purely domestic law.'],
},
_metadata: generateResponseMetadata(db),
};
}
if (doc.status === 'repealed') {
warnings.push('This statute has been repealed.');
recommendations.push('Check for replacement legislation.');
}
// Check implementation status
const statuses = db.prepare(
'SELECT implementation_status, COUNT(*) as count FROM eu_references WHERE document_id = ? GROUP BY implementation_status'
).all(resolvedId) as { implementation_status: string | null; count: number }[];
const statusMap = new Map(statuses.map(s => [s.implementation_status, s.count]));
const completeCount = statusMap.get('complete') ?? 0;
const partialCount = statusMap.get('partial') ?? 0;
const unknownCount = statusMap.get('unknown') ?? 0;
let compliance_status: 'compliant' | 'partial' | 'unclear' | 'not_applicable';
if (completeCount > 0 && partialCount === 0 && unknownCount === 0) {
compliance_status = 'compliant';
} else if (partialCount > 0) {
compliance_status = 'partial';
warnings.push(`${partialCount} EU reference(s) have partial implementation status.`);
} else {
compliance_status = 'unclear';
if (unknownCount > 0) {
recommendations.push(`${unknownCount} EU reference(s) have unknown implementation status. Manual review recommended.`);
}
}
return {
results: {
document_id: resolvedId,
document_title: doc.title,
compliance_status,
eu_references_found: euRefCount,
warnings,
recommendations,
},
_metadata: generateResponseMetadata(db),
};
}

27
src/utils/as-of-date.ts Normal file
View File

@@ -0,0 +1,27 @@
/**
* Date normalization for temporal queries.
*/
/**
* Normalize an as-of date string to ISO 8601 format.
* Returns null if the input is not a valid date.
*/
export function normalizeAsOfDate(input?: string): string | null {
if (!input || input.trim().length === 0) return null;
const trimmed = input.trim();
// Already ISO 8601
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
const date = new Date(trimmed);
if (!isNaN(date.getTime())) return trimmed;
}
// Try parsing as a general date
const date = new Date(trimmed);
if (!isNaN(date.getTime())) {
return date.toISOString().slice(0, 10);
}
return null;
}

52
src/utils/fts-query.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* FTS5 query helpers for Israel Law MCP.
*
* Handles query sanitization and variant generation for SQLite FTS5.
*/
/**
* Sanitize user input for safe FTS5 queries.
* Removes characters that have special meaning in FTS5 syntax.
*/
export function sanitizeFtsInput(input: string): string {
return input
.replace(/['"(){}[\]^~*:]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Build FTS5 query variants for a search term.
* Returns variants in order of specificity (most specific first):
* 1. Exact phrase match
* 2. All terms required (AND)
* 3. Prefix match on last term
*/
export function buildFtsQueryVariants(sanitized: string): string[] {
if (!sanitized || sanitized.trim().length === 0) {
return [];
}
const terms = sanitized.split(/\s+/).filter(t => t.length > 0);
if (terms.length === 0) return [];
const variants: string[] = [];
// Exact phrase
if (terms.length > 1) {
variants.push(`"${terms.join(' ')}"`);
}
// AND query
variants.push(terms.join(' AND '));
// Prefix match on last term (for autocomplete-like behavior)
if (terms.length === 1 && terms[0].length >= 3) {
variants.push(`${terms[0]}*`);
} else if (terms.length > 1) {
const prefix = [...terms.slice(0, -1), `${terms[terms.length - 1]}*`];
variants.push(prefix.join(' AND '));
}
return variants;
}

41
src/utils/metadata.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* Response metadata utilities for Israel Law MCP.
*/
import type Database from '@ansvar/mcp-sqlite';
export interface ResponseMetadata {
data_source: string;
jurisdiction: string;
disclaimer: string;
freshness?: string;
}
export interface ToolResponse<T> {
results: T;
_metadata: ResponseMetadata;
}
export function generateResponseMetadata(
db: InstanceType<typeof Database>,
): ResponseMetadata {
let freshness: string | undefined;
try {
const row = db.prepare(
"SELECT value FROM db_metadata WHERE key = 'built_at'"
).get() as { value: string } | undefined;
if (row) freshness = row.value;
} catch {
// Ignore
}
return {
data_source: 'Knesset Legislation Database (knesset.gov.il) + gov.il English translations',
jurisdiction: 'IL',
disclaimer:
'This data is sourced from the Knesset Legislation Database and Israeli government publications. ' +
'Hebrew is the legally authoritative language. English translations are unofficial. ' +
'Always verify with the official Knesset or Nevo portals.',
freshness,
};
}

49
src/utils/statute-id.ts Normal file
View File

@@ -0,0 +1,49 @@
/**
* Statute ID resolution for Israel Law MCP.
*
* Resolves fuzzy document references (titles, law names) to database document IDs.
*/
import type Database from '@ansvar/mcp-sqlite';
/**
* Resolve a document identifier to a database document ID.
* Supports:
* - Direct ID match (e.g., "privacy-protection-law-1981")
* - Law name + year match (e.g., "Privacy Protection Law 1981")
* - Title substring match (e.g., "Privacy Protection", "Computer Law")
* - Short name match (e.g., "PPL", "Computer Law")
*/
export function resolveDocumentId(
db: InstanceType<typeof Database>,
input: string,
): string | null {
const trimmed = input.trim();
if (!trimmed) return null;
// Direct ID match
const directMatch = db.prepare(
'SELECT id FROM legal_documents WHERE id = ?'
).get(trimmed) as { id: string } | undefined;
if (directMatch) return directMatch.id;
// Short name exact match (case-insensitive)
const shortNameMatch = db.prepare(
"SELECT id FROM legal_documents WHERE LOWER(short_name) = LOWER(?) LIMIT 1"
).get(trimmed) as { id: string } | undefined;
if (shortNameMatch) return shortNameMatch.id;
// Title/short_name fuzzy match
const titleResult = db.prepare(
"SELECT id FROM legal_documents WHERE title LIKE ? OR short_name LIKE ? OR title_en LIKE ? LIMIT 1"
).get(`%${trimmed}%`, `%${trimmed}%`, `%${trimmed}%`) as { id: string } | undefined;
if (titleResult) return titleResult.id;
// Case-insensitive fallback
const lowerResult = db.prepare(
"SELECT id FROM legal_documents WHERE LOWER(title) LIKE LOWER(?) OR LOWER(short_name) LIKE LOWER(?) OR LOWER(title_en) LIKE LOWER(?) LIMIT 1"
).get(`%${trimmed}%`, `%${trimmed}%`, `%${trimmed}%`) as { id: string } | undefined;
if (lowerResult) return lowerResult.id;
return null;
}