From f0bd5acf806ffb9a79409502b92c0a1921e1775c Mon Sep 17 00:00:00 2001 From: Mortalus Date: Sun, 1 Mar 2026 05:53:12 +0100 Subject: [PATCH 1/9] fix: Dockerfile CMD path and chown for Docker proxy support --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 25617f2..fdf5633 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,9 @@ COPY --from=builder /app/dist ./dist COPY data/database.db ./data/database.db # Security: non-root user -RUN addgroup -S nodejs && adduser -S nodejs -G nodejs +RUN addgroup -S nodejs && adduser -S nodejs -G nodejs \ + && chown -R nodejs:nodejs /app/data USER nodejs ENV NODE_ENV=production -CMD ["node", "dist/src/http-server.js"] +CMD ["node", "dist/http-server.js"] From 614e9ef7b7d86d2d96220cc7d40a6ee3acf2c23e Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Mon, 2 Mar 2026 11:17:44 +0000 Subject: [PATCH 2/9] chore: remove legacy codeql.yml (ADR-011 GHAS migration) --- .github/workflows/codeql.yml | 42 ------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 9956dec..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: "CodeQL Security Analysis" - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - # Run weekly on Monday at 6 AM UTC - - cron: '0 6 * * 1' - -jobs: - analyze: - name: Analyze Code - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['javascript'] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-extended # More thorough than default - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" From 320b7e492f0e71489a3d058a40e841e314aa4246 Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Mon, 2 Mar 2026 11:17:45 +0000 Subject: [PATCH 3/9] chore: remove legacy gitleaks.yml (ADR-011 GHAS migration) --- .github/workflows/gitleaks.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/gitleaks.yml diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml deleted file mode 100644 index a4380f0..0000000 --- a/.github/workflows/gitleaks.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Secret Scanning (Gitleaks) - -on: - push: - branches: ['**'] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -jobs: - scan: - name: Scan for secrets - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Full history for accurate scanning - - - name: Install Gitleaks - run: | - curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.30.0/gitleaks_8.30.0_linux_x64.tar.gz | tar -xz - sudo mv gitleaks /usr/local/bin/ - - - name: Run Gitleaks - run: gitleaks detect --source . --verbose From ed706096bfc86123144492692eb18ed8d985ccd8 Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Mon, 2 Mar 2026 11:58:47 +0000 Subject: [PATCH 4/9] docs: add TOOLS.md with tool documentation --- TOOLS.md | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 TOOLS.md diff --git a/TOOLS.md b/TOOLS.md new file mode 100644 index 0000000..bf71a51 --- /dev/null +++ b/TOOLS.md @@ -0,0 +1,109 @@ +# Tools — Israel Law MCP + +8 tools for searching and retrieving Israel legislation. + +--- + +## 1. search_legislation + +Full-text search across all Israel statutes and regulations. + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `query` | string | Yes | Search query | +| `limit` | number | No | Max results (default 10, max 50) | +| `status` | string | No | Filter: `in_force`, `amended`, `repealed` | + +**Returns:** Matching provisions with document context, snippets, and relevance scores. + +--- + +## 2. get_provision + +Retrieve the full text of a specific provision from a statute. + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `document_id` | string | Yes | Statute identifier or title | +| `section` | string | No | Section/article number | + +**Returns:** Full provision text with document metadata. + +--- + +## 3. list_sources + +List all data sources with provenance metadata and database statistics. + +**Returns:** Source authority, coverage scope, document/provision counts, and build date. + +--- + +## 4. validate_citation + +Validate a legal citation against the database (zero-hallucination check). + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `citation` | string | Yes | Citation string to validate | + +**Returns:** Whether the cited document and provision exist, with warnings. + +--- + +## 5. build_legal_stance + +Build a comprehensive set of citations for a legal question. + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `query` | string | Yes | Legal question or topic | +| `limit` | number | No | Max results per category (default 5) | + +**Returns:** Aggregated relevant provisions from multiple statutes. + +--- + +## 6. format_citation + +Format a legal citation per standard conventions. + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `citation` | string | Yes | Citation to format | +| `format` | string | No | `full`, `short`, or `pinpoint` | + +**Returns:** Formatted citation string. + +--- + +## 7. check_currency + +Check whether a statute or provision is currently in force. + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `document_id` | string | Yes | Statute identifier or title | +| `provision_ref` | string | No | Optional provision reference | + +**Returns:** Status (in_force/amended/repealed), dates, and warnings. + +--- + +## 8. about + +Server metadata, dataset statistics, and data freshness. + +**Returns:** Document/provision counts, build date, source authority, and database version. From de2982ea413b66c86fca324ca1b9a0db35e332b0 Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Mon, 2 Mar 2026 21:04:26 +0100 Subject: [PATCH 5/9] =?UTF-8?q?fix(security):=20update=20lock=20file=20?= =?UTF-8?q?=E2=80=94=20hono=204.12.3=20+=20SDK=201.27.1=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated transitive deps to patched versions: - @modelcontextprotocol/sdk: 1.26.0 -> 1.27.1 (cross-client data leak via shared transport, affects 1.10.0-1.25.3, patched in 1.26.0) - hono: 4.12.0 -> 4.12.3 (authentication bypass via IP spoofing, patched in 4.12.3) No package.json change needed — existing semver ranges already allow the patched versions. --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c610f5d..7bb7e67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -621,9 +621,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -2615,9 +2615,9 @@ } }, "node_modules/hono": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", - "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", "license": "MIT", "engines": { "node": ">=16.9.0" From 68a627d1d09093bce589c06f640158d314c47854 Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Mon, 2 Mar 2026 21:04:30 +0100 Subject: [PATCH 6/9] docs: golden-standard README (#5) Brings README to production golden standard following the Ansvar Law MCP template. --- README.md | 264 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 161 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 4e08288..1f93d1b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Israeli Law MCP Server -**The Knesset alternative for the AI age.** +**The Nevo.co.il alternative for the AI age.** -[![npm version](https://badge.fury.io/js/%40ansvar/israel-law-mcp.svg)](https://www.npmjs.com/package/@ansvar/israel-law-mcp) +[![npm version](https://badge.fury.io/js/@ansvar%2Fisrael-law-mcp.svg)](https://www.npmjs.com/package/@ansvar/israel-law-mcp) [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![GitHub stars](https://img.shields.io/github/stars/Ansvar-Systems/Israel-law-mcp?style=social)](https://github.com/Ansvar-Systems/Israel-law-mcp) -[![CI](https://github.com/Ansvar-Systems/Israel-law-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Ansvar-Systems/Israel-law-mcp/actions/workflows/ci.yml) +[![GitHub stars](https://img.shields.io/github/stars/Ansvar-Systems/israel-law-mcp?style=social)](https://github.com/Ansvar-Systems/israel-law-mcp) +[![CI](https://github.com/Ansvar-Systems/israel-law-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Ansvar-Systems/israel-law-mcp/actions/workflows/ci.yml) +[![Database](https://img.shields.io/badge/database-pre--built-green)](https://github.com/Ansvar-Systems/israel-law-mcp) +[![Provisions](https://img.shields.io/badge/provisions-537-blue)](https://github.com/Ansvar-Systems/israel-law-mcp) -Query **Israeli legislation** -- covering data protection, cybersecurity, corporate law, and more -- directly from Claude, Cursor, or any MCP-compatible client. +Query **66 Israeli statutes** -- from the Privacy Protection Law and Data Security Regulations to the Penal Law, Companies Law, and Electronic Signature Law -- directly from Claude, Cursor, or any MCP-compatible client. If you're building legal tech, compliance tools, or doing Israeli legal research, this is your verified reference database. @@ -18,13 +20,14 @@ Built by [Ansvar Systems](https://ansvar.eu) -- Stockholm, Sweden ## Why This Exists -Israeli legal research is scattered across official government databases, commercial legal platforms, and institutional archives. Whether you're: -- A **lawyer** validating citations in a brief or contract -- A **compliance officer** checking if a statute is still in force -- A **legal tech developer** building tools on Israeli law -- A **researcher** tracing legislative history +Israeli legal research spans Knesset publications, Nevo.co.il, laws.gov.il (מאגר החקיקה הלאומי), and official Reshumot gazette entries. Whether you're: -...you shouldn't need dozens of browser tabs and manual PDF cross-referencing. Ask Claude. Get the exact provision. With context. +- A **lawyer** validating citations in a brief or contract +- A **compliance officer** checking Privacy Protection Law obligations or Data Security Regulation requirements +- A **legal tech developer** building tools on Israeli law +- A **researcher** tracing provisions across Knesset statutes + +...you shouldn't need dozens of browser tabs and manual cross-referencing. Ask Claude. Get the exact provision. With context. This MCP server makes Israeli law **searchable, cross-referenceable, and AI-readable**. @@ -41,7 +44,7 @@ This MCP server makes Israeli law **searchable, cross-referenceable, and AI-read | Client | How to Connect | |--------|---------------| | **Claude.ai** | Settings > Connectors > Add Integration > paste URL | -| **Claude Code** | `claude mcp add israel-law --transport http https://israel-law-mcp.vercel.app/mcp` | +| **Claude Code** | `claude mcp add israeli-law --transport http https://israel-law-mcp.vercel.app/mcp` | | **Claude Desktop** | Add to config (see below) | | **GitHub Copilot** | Add to VS Code settings (see below) | @@ -50,7 +53,7 @@ This MCP server makes Israeli law **searchable, cross-referenceable, and AI-read ```json { "mcpServers": { - "israel-law": { + "israeli-law": { "type": "url", "url": "https://israel-law-mcp.vercel.app/mcp" } @@ -63,7 +66,7 @@ This MCP server makes Israeli law **searchable, cross-referenceable, and AI-read ```json { "github.copilot.chat.mcp.servers": { - "israel-law": { + "israeli-law": { "type": "http", "url": "https://israel-law-mcp.vercel.app/mcp" } @@ -85,7 +88,7 @@ npx @ansvar/israel-law-mcp ```json { "mcpServers": { - "israel-law": { + "israeli-law": { "command": "npx", "args": ["-y", "@ansvar/israel-law-mcp"] } @@ -98,7 +101,7 @@ npx @ansvar/israel-law-mcp ```json { "mcp.servers": { - "israel-law": { + "israeli-law": { "command": "npx", "args": ["-y", "@ansvar/israel-law-mcp"] } @@ -112,36 +115,65 @@ npx @ansvar/israel-law-mcp Once connected, just ask naturally: -- *"What does the Israeli data protection law say about consent?"* -- *"Search for cybersecurity requirements in Israeli legislation"* -- *"Is this statute still in force?"* -- *"Find provisions about personal data in Israeli law"* -- *"What EU directives does this Israeli law implement?"* -- *"Which Israeli laws implement the GDPR?"* -- *"Validate this legal citation"* -- *"Build a legal stance on data breach notification requirements"* +- *"חיפוש הוראות בנושא 'הגנת הפרטיות' בחוק הגנת הפרטיות"* (Search provisions on "privacy protection") +- *"מה אומר חוק העונשין לגבי עבירות מחשב?"* (What does the Penal Law say about computer offenses?) +- *"מצא סעיפים בחוק החוזים העוסקים בתנאים מקפחים"* (Find provisions in the Contracts Law on unfair terms) +- *"מה דורש חוק הגנת הצרכן לגבי גילוי מידע?"* (What does the Consumer Protection Law require on disclosure?) +- *"Is the Privacy Protection Law still in force?"* +- *"Find provisions about data breach notification in Israeli law"* +- *"What EU laws align with the Israeli Privacy Protection Law?"* +- *"Validate the citation Privacy Protection Law, 5741-1981, Section 7"* +- *"Build a legal stance on data security requirements under Israeli regulations"* --- -## Key Legislation Covered +## What's Included -| Law | Year | Significance | -|-----|------|-------------| -| **Privacy Protection Law** | 1981 (amended) | Comprehensive privacy law; predates GDPR; Israel has EU adequacy decision; database registration regime | -| **Protection of Privacy Regulations (Data Security)** | 2017 | Specific technical and organisational security requirements for database owners; four security levels | -| **Computer Law** | 1995 | Criminalises unauthorised computer access, interference, and computer viruses | -| **Companies Law** | 1999 | Corporate governance, registration, directors' duties, and corporate obligations | -| **Electronic Signature Law** | 2001 | Legal recognition of electronic signatures and certification authorities | -| **Credit Data Law** | 2002 | Regulation of credit data collection, processing, and sharing | -| **Basic Law: Human Dignity and Liberty** | 1992 | Quasi-constitutional protection of human dignity and liberty including privacy | +| Category | Count | Details | +|----------|-------|---------| +| **Statutes** | 66 laws | Privacy, data security, computer law, companies, electronic signatures, credit data | +| **Provisions** | 537 sections | Full-text searchable with FTS5 | +| **EU Cross-References** | Linked | GDPR adequacy relationships and comparative alignment | +| **Database Size** | Optimized SQLite | Portable, pre-built | +| **Data Source** | main.knesset.gov.il | Official Knesset legislation database | + +**Verified data only** -- every citation is validated against official sources (Knesset, laws.gov.il). Zero LLM-generated content. --- -## Deployment Tier +## Why This Works -**SMALL** -- Single tier, bundled SQLite database shipped with the npm package. +**Verbatim Source Text (No LLM Processing):** +- All statute text is ingested from main.knesset.gov.il (מאגר החקיקה הלאומי) and official Knesset publications +- Provisions are returned **unchanged** from SQLite FTS5 database rows +- Zero LLM summarization or paraphrasing -- the database contains statute text, not AI interpretations -**Estimated database size:** ~60-120 MB (full corpus of Israeli federal legislation with English translations) +**Smart Context Management:** +- Search returns ranked provisions with BM25 scoring (safe for context) +- Provision retrieval gives exact text by statute name + section number +- Cross-references help navigate without loading everything at once + +**Technical Architecture:** +``` +Knesset API / laws.gov.il --> Parse --> SQLite --> FTS5 snippet() --> MCP response + ^ ^ + Provision parser Verbatim database query +``` + +### Traditional Research vs. This MCP + +| Traditional Approach | This MCP Server | +|---------------------|-----------------| +| Search Nevo.co.il or laws.gov.il by statute name | Search in Hebrew or English: *"הגנת הפרטיות נתונים"* | +| Navigate multi-section statutes manually | Get the exact provision with context | +| Manual cross-referencing between laws | `build_legal_stance` aggregates across sources | +| "Is this statute still in force?" -- check manually | `check_currency` tool -- answer in seconds | +| Find EU basis -- dig through EUR-Lex | `get_eu_basis` -- linked EU alignment instantly | +| No API, no integration | MCP protocol -- AI-native | + +**Traditional:** Search Nevo.co.il --> Navigate Hebrew/English statute --> Ctrl+F --> Cross-reference between laws --> Check EUR-Lex for GDPR adequacy --> Repeat + +**This MCP:** *"What are the data breach notification requirements under Israeli law and how do they compare to GDPR Article 33?"* --> Done. --- @@ -151,56 +183,41 @@ Once connected, just ask naturally: | Tool | Description | |------|-------------| -| `search_legislation` | FTS5 full-text search across all provisions with BM25 ranking | -| `get_provision` | Retrieve specific provision by statute + chapter/section | -| `check_currency` | Check if statute is in force, amended, or repealed | -| `validate_citation` | Validate citation against database (zero-hallucination check) | -| `build_legal_stance` | Aggregate citations from statutes for a legal topic | -| `format_citation` | Format citations per Israeli conventions (full/short/pinpoint) | -| `list_sources` | List all available statutes with metadata | -| `about` | Server info, capabilities, and coverage summary | +| `search_legislation` | FTS5 full-text search across 537 provisions with BM25 ranking. Supports Hebrew and English queries, quoted phrases, boolean operators | +| `get_provision` | Retrieve specific provision by statute name + section number | +| `check_currency` | Check if a statute is in force, amended, or repealed | +| `validate_citation` | Validate citation against database -- zero-hallucination check | +| `build_legal_stance` | Aggregate citations from multiple statutes for a legal topic | +| `format_citation` | Format citations per Israeli legal conventions | +| `list_sources` | List all available statutes with metadata and coverage scope | +| `about` | Server info, capabilities, dataset statistics, and coverage summary | -### EU/International Law Integration Tools (5) +### EU Law Integration Tools (5) | Tool | Description | |------|-------------| -| `get_eu_basis` | Get EU directives/regulations for Israeli statute | -| `get_israeli_implementations` | Find Israeli laws implementing EU act | -| `search_eu_implementations` | Search EU documents with Israeli implementation counts | -| `get_provision_eu_basis` | Get EU law references for specific provision | -| `validate_eu_compliance` | Check implementation status of EU directives | +| `get_eu_basis` | Get EU directives/regulations that an Israeli statute aligns with | +| `get_israeli_implementations` | Find Israeli laws corresponding to a specific EU act | +| `search_eu_implementations` | Search EU documents with Israeli alignment counts | +| `get_provision_eu_basis` | Get EU law references for a specific provision | +| `validate_eu_compliance` | Check alignment status of Israeli statutes against EU directives | --- -## Why This Works +## EU Law Integration -**Verbatim Source Text (No LLM Processing):** -- All statute text is ingested from official Israeli government sources -- Provisions are returned **unchanged** from SQLite FTS5 database rows -- Zero LLM summarization or paraphrasing -- the database contains regulation text, not AI interpretations +**Israel holds EU adequacy status under GDPR Article 45** -- the European Commission has determined that Israeli law provides an essentially equivalent level of protection to that guaranteed in the EU. This makes Israel one of a small group of non-EU countries recognized as adequate for personal data transfers. -**Smart Context Management:** -- Search returns ranked provisions with BM25 scoring (safe for context) -- Provision retrieval gives exact text by statute identifier + chapter/section -- Cross-references help navigate without loading everything at once +| Metric | Value | +|--------|-------| +| **GDPR Adequacy** | Yes -- Commission Decision (2011), upheld under GDPR Article 45 | +| **Key Israeli Privacy Law** | Privacy Protection Law, 5741-1981 | +| **Key Data Security Law** | Privacy Protection Regulations (Data Security), 5777-2017 | +| **Alignment Framework** | GDPR principles, ePrivacy concepts, NIS-equivalent provisions | -**Technical Architecture:** -``` -Official Sources --> Parse --> SQLite --> FTS5 snippet() --> MCP response - ^ ^ - Provision parser Verbatim database query -``` +The EU bridge tools allow you to explore these adequacy and alignment relationships -- checking which Israeli provisions correspond to EU requirements, and tracing the legislative basis for Israel's adequacy determination. -### Traditional Research vs. This MCP - -| Traditional Approach | This MCP Server | -|---------------------|-----------------| -| Search official databases by statute number | Search by plain language | -| Navigate multi-chapter statutes manually | Get the exact provision with context | -| Manual cross-referencing between laws | `build_legal_stance` aggregates across sources | -| "Is this statute still in force?" --> check manually | `check_currency` tool --> answer in seconds | -| Find EU basis --> dig through EUR-Lex | `get_eu_basis` --> linked EU directives instantly | -| No API, no integration | MCP protocol --> AI-native | +> **Note:** EU cross-references reflect adequacy and alignment relationships. Israel operates its own independent legal system. The EU tools identify where Israeli and EU law address the same domains under comparable principles. --- @@ -208,7 +225,29 @@ Official Sources --> Parse --> SQLite --> FTS5 snippet() --> MCP response All content is sourced from authoritative Israeli legal databases: -- **[Knesset](https://www.knesset.gov.il)** -- Official Israeli government legal database +- **[Knesset Legislation Database](https://main.knesset.gov.il)** (מאגר החקיקה הלאומי) -- Official source for all primary legislation +- **[laws.gov.il](https://laws.gov.il)** -- National legislation portal +- **[Nevo.co.il](https://nevo.co.il)** -- Cross-reference and citation validation + +### Data Provenance + +| Field | Value | +|-------|-------| +| **Authority** | Knesset of Israel | +| **Retrieval method** | Knesset official API and legislation portal | +| **Languages** | Hebrew (primary), English (where official translations exist) | +| **License** | Israeli Government public domain | +| **Coverage** | 66 statutes across privacy, data security, computer law, companies, and electronic transactions | + +### Automated Freshness Checks + +A [GitHub Actions workflow](.github/workflows/check-updates.yml) monitors data sources for changes: + +| Check | Method | +|-------|--------| +| **Statute amendments** | Drift detection against known provision anchors | +| **New statutes** | Comparison against official Knesset publications | +| **Repealed statutes** | Status change detection | **Verified data only** -- every citation is validated against official sources. Zero LLM-generated content. @@ -237,17 +276,18 @@ See [SECURITY.md](SECURITY.md) for the full policy and vulnerability reporting. > **THIS TOOL IS NOT LEGAL ADVICE** > -> Statute text is sourced from official Israeli government publications. However: +> Statute text is sourced from the Knesset official legislation database. However: > - This is a **research tool**, not a substitute for professional legal counsel -> - **Court case coverage is limited** -- do not rely solely on this for case law research -> - **Verify critical citations** against primary sources for court filings -> - **EU cross-references** are extracted from statute text, not EUR-Lex full text +> - **Court case coverage is not included** -- do not rely solely on this for case law research +> - **Verify critical citations** against primary sources (Nevo.co.il, laws.gov.il) for court filings +> - **EU cross-references** reflect adequacy and alignment relationships, not transposition +> - **Hebrew-language statutes** -- English translations are not always official; verify against the Hebrew source for legal proceedings **Before using professionally, read:** [DISCLAIMER.md](DISCLAIMER.md) | [SECURITY.md](SECURITY.md) ### Client Confidentiality -Queries go through the Claude API. For privileged or confidential matters, use on-premise deployment. +Queries go through the Claude API. For privileged or confidential matters, use on-premise deployment. For guidance on professional use, consult the **לשכת עורכי הדין בישראל** (Israel Bar Association) professional conduct rules. --- @@ -256,8 +296,8 @@ Queries go through the Claude API. For privileged or confidential matters, use o ### Setup ```bash -git clone https://github.com/Ansvar-Systems/Israel-law-mcp -cd Israel-law-mcp +git clone https://github.com/Ansvar-Systems/israel-law-mcp +cd israel-law-mcp npm install npm run build npm test @@ -270,6 +310,21 @@ npm run dev # Start MCP server npx @anthropic/mcp-inspector node dist/index.js # Test with MCP Inspector ``` +### Data Management + +```bash +npm run ingest # Ingest statutes from Knesset API +npm run build:db # Rebuild SQLite database +npm run drift:detect # Run drift detection against anchors +npm run check-updates # Check for amendments and new statutes +``` + +### Performance + +- **Search Speed:** <100ms for most FTS5 queries +- **Reliability:** 100% ingestion success rate +- **Languages:** Hebrew and English queries supported + --- ## Related Projects: Complete Compliance Suite @@ -279,16 +334,16 @@ This server is part of **Ansvar's Compliance Suite** -- MCP servers that work to ### [@ansvar/eu-regulations-mcp](https://github.com/Ansvar-Systems/EU_compliance_MCP) **Query 49 EU regulations directly from Claude** -- GDPR, AI Act, DORA, NIS2, MiFID II, eIDAS, and more. Full regulatory text with article-level search. `npx @ansvar/eu-regulations-mcp` +### [@ansvar/israel-law-mcp](https://github.com/Ansvar-Systems/israel-law-mcp) (This Project) +**Query 66 Israeli statutes directly from Claude** -- Privacy Protection Law, Data Security Regulations, Penal Law, Companies Law, and more. `npx @ansvar/israel-law-mcp` + ### [@ansvar/us-regulations-mcp](https://github.com/Ansvar-Systems/US_Compliance_MCP) **Query US federal and state compliance laws** -- HIPAA, CCPA, SOX, GLBA, FERPA, and more. `npx @ansvar/us-regulations-mcp` ### [@ansvar/security-controls-mcp](https://github.com/Ansvar-Systems/security-controls-mcp) **Query 261 security frameworks** -- ISO 27001, NIST CSF, SOC 2, CIS Controls, SCF, and more. `npx @ansvar/security-controls-mcp` -### [@ansvar/automotive-cybersecurity-mcp](https://github.com/Ansvar-Systems/Automotive-MCP) -**Query UNECE R155/R156 and ISO 21434** -- Automotive cybersecurity compliance. `npx @ansvar/automotive-cybersecurity-mcp` - -**30+ national law MCPs** covering Australia, Brazil, Canada, China, Denmark, Finland, France, Germany, Ghana, Iceland, India, Ireland, Israel, Italy, Japan, Kenya, Netherlands, Nigeria, Norway, Singapore, Slovenia, South Korea, Sweden, Switzerland, Thailand, UAE, UK, and more. +**70+ national law MCPs** covering Australia, Brazil, Canada, Cameroon, Denmark, Finland, France, Germany, Ghana, India, Ireland, Netherlands, Nigeria, Norway, Singapore, Sweden, Switzerland, UAE, UK, and more. --- @@ -297,23 +352,26 @@ This server is part of **Ansvar's Compliance Suite** -- MCP servers that work to Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Priority areas: -- Court case law expansion -- EU cross-reference improvements -- Historical statute versions and amendment tracking -- Additional statutory instruments and regulations +- Statute corpus expansion (additional Knesset legislation) +- Court case law integration (Supreme Court, District Courts) +- English translation coverage for key statutes +- Amendment tracking and historical versions +- Rabbinical court decisions (where applicable to civil matters) --- ## Roadmap - [x] Core statute database with FTS5 search -- [x] EU/international law cross-references +- [x] Privacy Protection Law and Data Security Regulations +- [x] EU/international law alignment tools (GDPR adequacy) - [x] Vercel Streamable HTTP deployment - [x] npm package publication -- [ ] Court case law expansion +- [ ] Court case law expansion (Bagatz, Supreme Court) +- [ ] Full statute corpus expansion (200+ Knesset laws) - [ ] Historical statute versions (amendment tracking) -- [ ] Preparatory works / explanatory memoranda -- [ ] Lower court and tribunal decisions +- [ ] Hebrew-only FTS5 optimization with morphological analysis +- [ ] English translation index for major statutes --- @@ -322,12 +380,12 @@ Priority areas: If you use this MCP server in academic research: ```bibtex -@software{israel_law_mcp_2025, +@software{israeli_law_mcp_2026, author = {Ansvar Systems AB}, title = {Israeli Law MCP Server: AI-Powered Legal Research Tool}, - year = {2025}, - url = {https://github.com/Ansvar-Systems/Israel-law-mcp}, - note = {Israeli legal database with full-text search and EU cross-references} + year = {2026}, + url = {https://github.com/Ansvar-Systems/israel-law-mcp}, + note = {66 Israeli statutes with 537 provisions, EU adequacy cross-references} } ``` @@ -339,16 +397,16 @@ Apache License 2.0. See [LICENSE](./LICENSE) for details. ### Data Licenses -- **Statutes & Legislation:** Israeli Government (public domain) +- **Statutes & Legislation:** Knesset of Israel (public domain) - **EU Metadata:** EUR-Lex (EU public domain) --- ## About Ansvar Systems -We build AI-accelerated compliance and legal research tools for the global market. This MCP server started as our internal reference tool -- turns out everyone building compliance tools has the same research frustrations. +We build AI-accelerated compliance and legal research tools for the global market. This MCP server started as our internal reference tool for Israeli law -- turns out everyone building for the Israeli market or navigating GDPR adequacy has the same research frustrations. -So we're open-sourcing it. +So we're open-sourcing it. Navigating 66 statutes across Hebrew and English shouldn't require a law degree. **[ansvar.eu](https://ansvar.eu)** -- Stockholm, Sweden From fac8992038e2e278c426f5c88e95484fe7d4a25d Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Wed, 4 Mar 2026 09:49:24 +0000 Subject: [PATCH 7/9] fix: align about.ts to golden standard Section 4.9 --- src/tools/about.ts | 56 +++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/tools/about.ts b/src/tools/about.ts index 6f5da76..f2adc02 100644 --- a/src/tools/about.ts +++ b/src/tools/about.ts @@ -1,5 +1,5 @@ /** - * about -- Server metadata, dataset statistics, and provenance. + * about — Server metadata, dataset statistics, and provenance. */ import type Database from '@ansvar/mcp-sqlite'; @@ -25,31 +25,41 @@ export function getAbout(db: InstanceType, context: AboutContex const caps = detectCapabilities(db); const meta = readDbMetadata(db); + const euRefs = safeCount(db, 'SELECT COUNT(*) as count FROM eu_references'); + + const stats: Record = { + 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'), + }; + + if (euRefs > 0) { + stats.eu_documents = safeCount(db, 'SELECT COUNT(*) as count FROM eu_documents'); + stats.eu_references = euRefs; + } + return { - server: SERVER_NAME, + name: 'Israel Law MCP', version: context.version, - repository: REPOSITORY_URL, - database: { - fingerprint: context.fingerprint, - built_at: context.dbBuilt, - tier: meta.tier, - schema_version: meta.schema_version, - capabilities: [...caps], + jurisdiction: 'IL', + description: 'Israel Law MCP — legislation via Model Context Protocol', + stats, + data_sources: [ + { + name: 'Knesset Legislation Database', + url: 'https://main.knesset.gov.il', + authority: 'Knesset (Israeli Parliament)', + }, + ], + freshness: { + database_built: context.dbBuilt, }, - 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'], + disclaimer: + 'This is a research tool, not legal advice. Verify critical citations against official sources.', + network: { + name: 'Ansvar MCP Network', + open_law: 'https://ansvar.eu/open-law', + directory: 'https://ansvar.ai/mcp', }, }; } From e5b8a78f923ccd0c0eaf1bd31bbf0dc558bf0019 Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Wed, 4 Mar 2026 10:27:54 +0000 Subject: [PATCH 8/9] fix: remove unused imports from about.ts (CI fix) --- src/tools/about.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/tools/about.ts b/src/tools/about.ts index f2adc02..b2dc5a5 100644 --- a/src/tools/about.ts +++ b/src/tools/about.ts @@ -3,8 +3,6 @@ */ 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; @@ -22,8 +20,6 @@ function safeCount(db: InstanceType, sql: string): number { } export function getAbout(db: InstanceType, context: AboutContext) { - const caps = detectCapabilities(db); - const meta = readDbMetadata(db); const euRefs = safeCount(db, 'SELECT COUNT(*) as count FROM eu_references'); From d730245538cc6132a068d631b211632f9f7642c2 Mon Sep 17 00:00:00 2001 From: Jeffrey von Rotz Date: Fri, 6 Mar 2026 09:14:17 +0100 Subject: [PATCH 9/9] fix: apply 5 fleet-wide bug fixes (dedup, wildcard, doc-id, fallback, metadata) (#10) - Add deduplicateResults() to search-legislation and build-legal-stance - Upgrade fts-query with stemming, boolean passthrough, LIKE fallback, OR tier - Use resolveDocumentId() for document_id parameter in search tools - Disclose query_strategy and note in metadata on broadened/failed queries - Add note and query_strategy optional fields to ResponseMetadata interface Co-authored-by: Claude Opus 4.6 --- src/tools/build-legal-stance.ts | 101 +++++++++++++++++++++++++++-- src/tools/search-legislation.ts | 111 ++++++++++++++++++++++++++++++-- src/utils/fts-query.ts | 90 ++++++++++++++++++++++---- src/utils/metadata.ts | 2 + 4 files changed, 279 insertions(+), 25 deletions(-) diff --git a/src/tools/build-legal-stance.ts b/src/tools/build-legal-stance.ts index 91bcc25..442794c 100644 --- a/src/tools/build-legal-stance.ts +++ b/src/tools/build-legal-stance.ts @@ -1,9 +1,10 @@ /** - * build_legal_stance -- Build a comprehensive set of citations for a legal question. + * 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 { buildFtsQueryVariants, buildLikePattern, sanitizeFtsInput } from '../utils/fts-query.js'; +import { resolveDocumentId } from '../utils/statute-id.js'; import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js'; export interface BuildLegalStanceInput { @@ -31,8 +32,26 @@ export async function buildLegalStance( } const limit = Math.min(Math.max(input.limit ?? 5, 1), 20); + const fetchLimit = limit * 2; const queryVariants = buildFtsQueryVariants(sanitizeFtsInput(input.query)); + // Resolve document_id from title if provided + let resolvedDocId: string | undefined; + if (input.document_id) { + const resolved = resolveDocumentId(db, input.document_id); + resolvedDocId = resolved ?? undefined; + if (!resolved) { + return { + results: [], + _metadata: { + ...generateResponseMetadata(db), + note: `No document found matching "${input.document_id}"`, + }, + }; + } + } + + let queryStrategy = 'none'; for (const ftsQuery of queryVariants) { let sql = ` SELECT @@ -50,23 +69,93 @@ export async function buildLegalStance( `; const params: (string | number)[] = [ftsQuery]; - if (input.document_id) { + if (resolvedDocId) { sql += ' AND lp.document_id = ?'; - params.push(input.document_id); + params.push(resolvedDocId); } sql += ' ORDER BY relevance LIMIT ?'; - params.push(limit); + params.push(fetchLimit); try { const rows = db.prepare(sql).all(...params) as LegalStanceResult[]; if (rows.length > 0) { - return { results: rows, _metadata: generateResponseMetadata(db) }; + queryStrategy = ftsQuery === queryVariants[0] ? 'exact' : 'fallback'; + const deduped = deduplicateResults(rows, limit); + return { + results: deduped, + _metadata: { + ...generateResponseMetadata(db), + ...(queryStrategy === 'fallback' ? { query_strategy: 'broadened' } : {}), + }, + }; } } catch { continue; } } + // LIKE fallback — final tier when FTS5 returns no results + { + const likePattern = buildLikePattern(sanitizeFtsInput(input.query)); + let likeSql = ` + SELECT + lp.document_id, + ld.title as document_title, + lp.provision_ref, + lp.section, + lp.title, + substr(lp.content, 1, 300) as snippet, + 0 as relevance + FROM legal_provisions lp + JOIN legal_documents ld ON ld.id = lp.document_id + WHERE lp.content LIKE ? + `; + const likeParams: (string | number)[] = [likePattern]; + + if (resolvedDocId) { + likeSql += ' AND lp.document_id = ?'; + likeParams.push(resolvedDocId); + } + + likeSql += ' LIMIT ?'; + likeParams.push(fetchLimit); + + try { + const rows = db.prepare(likeSql).all(...likeParams) as LegalStanceResult[]; + if (rows.length > 0) { + return { + results: deduplicateResults(rows, limit), + _metadata: { + ...generateResponseMetadata(db), + query_strategy: 'like_fallback', + }, + }; + } + } catch { + // LIKE query failed + } + } + return { results: [], _metadata: generateResponseMetadata(db) }; } + +/** + * Deduplicate results by document_title + provision_ref. + * Duplicate document IDs (numeric vs slug) cause the same provision to appear twice. + */ +function deduplicateResults( + rows: LegalStanceResult[], + limit: number, +): LegalStanceResult[] { + const seen = new Set(); + const deduped: LegalStanceResult[] = []; + for (const row of rows) { + const key = `${row.document_title}::${row.provision_ref}`; + if (seen.has(key)) continue; + seen.add(key); + deduped.push(row); + if (deduped.length >= limit) break; + } + return deduped; +} diff --git a/src/tools/search-legislation.ts b/src/tools/search-legislation.ts index 4d21bdd..ff2259d 100644 --- a/src/tools/search-legislation.ts +++ b/src/tools/search-legislation.ts @@ -1,10 +1,11 @@ /** - * search_legislation -- Full-text search across Israeli statute provisions. + * 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 { buildFtsQueryVariants, buildLikePattern, sanitizeFtsInput } from '../utils/fts-query.js'; import { normalizeAsOfDate } from '../utils/as-of-date.js'; +import { resolveDocumentId } from '../utils/statute-id.js'; import { generateResponseMetadata, type ToolResponse } from '../utils/metadata.js'; export interface SearchLegislationInput { @@ -38,8 +39,27 @@ export async function searchLegislation( } const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT); + // Fetch extra rows to account for deduplication + const fetchLimit = limit * 2; const queryVariants = buildFtsQueryVariants(sanitizeFtsInput(input.query)); + // Resolve document_id from title if provided (same resolution as get_provision) + let resolvedDocId: string | undefined; + if (input.document_id) { + const resolved = resolveDocumentId(db, input.document_id); + resolvedDocId = resolved ?? undefined; + if (!resolved) { + return { + results: [], + _metadata: { + ...generateResponseMetadata(db), + note: `No document found matching "${input.document_id}"`, + }, + }; + } + } + + let queryStrategy = 'none'; for (const ftsQuery of queryVariants) { let sql = ` SELECT @@ -58,9 +78,9 @@ export async function searchLegislation( `; const params: (string | number)[] = [ftsQuery]; - if (input.document_id) { + if (resolvedDocId) { sql += ' AND lp.document_id = ?'; - params.push(input.document_id); + params.push(resolvedDocId); } if (input.status) { @@ -69,18 +89,95 @@ export async function searchLegislation( } sql += ' ORDER BY relevance LIMIT ?'; - params.push(limit); + params.push(fetchLimit); try { const rows = db.prepare(sql).all(...params) as SearchLegislationResult[]; if (rows.length > 0) { - return { results: rows, _metadata: generateResponseMetadata(db) }; + queryStrategy = ftsQuery === queryVariants[0] ? 'exact' : 'fallback'; + const deduped = deduplicateResults(rows, limit); + return { + results: deduped, + _metadata: { + ...generateResponseMetadata(db), + ...(queryStrategy === 'fallback' ? { query_strategy: 'broadened' } : {}), + }, + }; } } catch { - // FTS query syntax error -- try next variant + // FTS query syntax error — try next variant continue; } } + // LIKE fallback — final tier when FTS5 returns no results + { + const likePattern = buildLikePattern(sanitizeFtsInput(input.query)); + let likeSql = ` + SELECT + lp.document_id, + ld.title as document_title, + lp.provision_ref, + lp.chapter, + lp.section, + lp.title, + substr(lp.content, 1, 200) as snippet, + 0 as relevance + FROM legal_provisions lp + JOIN legal_documents ld ON ld.id = lp.document_id + WHERE lp.content LIKE ? + `; + const likeParams: (string | number)[] = [likePattern]; + + if (resolvedDocId) { + likeSql += ' AND lp.document_id = ?'; + likeParams.push(resolvedDocId); + } + + if (input.status) { + likeSql += ' AND ld.status = ?'; + likeParams.push(input.status); + } + + likeSql += ' LIMIT ?'; + likeParams.push(fetchLimit); + + try { + const rows = db.prepare(likeSql).all(...likeParams) as SearchLegislationResult[]; + if (rows.length > 0) { + return { + results: deduplicateResults(rows, limit), + _metadata: { + ...generateResponseMetadata(db), + query_strategy: 'like_fallback', + }, + }; + } + } catch { + // LIKE query failed + } + } + return { results: [], _metadata: generateResponseMetadata(db) }; } + +/** + * Deduplicate search results by document_title + provision_ref. + * Duplicate document IDs (numeric vs slug) cause the same provision to appear twice. + * Keeps the first (highest-ranked) occurrence. + */ +function deduplicateResults( + rows: SearchLegislationResult[], + limit: number, +): SearchLegislationResult[] { + const seen = new Set(); + const deduped: SearchLegislationResult[] = []; + for (const row of rows) { + const key = `${row.document_title}::${row.provision_ref}`; + if (seen.has(key)) continue; + seen.add(key); + deduped.push(row); + if (deduped.length >= limit) break; + } + return deduped; +} diff --git a/src/utils/fts-query.ts b/src/utils/fts-query.ts index 36f198f..086ffc9 100644 --- a/src/utils/fts-query.ts +++ b/src/utils/fts-query.ts @@ -4,49 +4,115 @@ * Handles query sanitization and variant generation for SQLite FTS5. */ +const FTS5_BOOLEAN_OPS = /\b(AND|OR|NOT)\b/; + +/** + * Detect whether input contains FTS5 boolean operators. + */ +export function hasBooleanOperators(input: string): boolean { + return FTS5_BOOLEAN_OPS.test(input); +} + /** * Sanitize user input for safe FTS5 queries. - * Removes characters that have special meaning in FTS5 syntax. + * Preserves boolean operators (AND, OR, NOT) when detected. */ export function sanitizeFtsInput(input: string): string { + if (hasBooleanOperators(input)) { + // Preserve boolean structure: only strip dangerous chars, keep quotes and parens + return input.replace(/[{}[\]^~*:]/g, ' ').replace(/\s+/g, ' ').trim(); + } + // Preserve trailing * on words (FTS5 prefix search) but strip other special chars return input - .replace(/['"(){}[\]^~*:]/g, ' ') + .replace(/['"(){}[\]^~:]/g, ' ') + .replace(/\*(?!\s|$)/g, ' ') // strip * unless at end of word .replace(/\s+/g, ' ') .trim(); } +/** + * Truncate common English suffixes for stemming fallback. + * Returns stem + "*" ready string, or null if no stemming possible. + */ +function stemWord(word: string): string | null { + if (word.length < 5) return null; + const lower = word.toLowerCase(); + for (const suffix of [ + 'ies', 'ing', 'ers', 'tion', 'ment', 'ness', + 'able', 'ible', 'ous', 'ive', 'ed', 'es', 'er', 'ly', 's', + ]) { + if (lower.endsWith(suffix) && lower.length - suffix.length >= 3) { + return lower.slice(0, -suffix.length); + } + } + return null; +} + /** * 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 + * 3. Prefix AND (last term gets prefix wildcard) + * 4. Stemmed prefix (suffix-truncated + wildcard) + * 5. Any term matches (OR) — broad fallback + * + * When boolean operators are detected, passes query through as-is. */ export function buildFtsQueryVariants(sanitized: string): string[] { if (!sanitized || sanitized.trim().length === 0) { return []; } + // Boolean passthrough — user knows what they want + if (hasBooleanOperators(sanitized)) { + return [sanitized]; + } + const terms = sanitized.split(/\s+/).filter(t => t.length > 0); if (terms.length === 0) return []; const variants: string[] = []; - // Exact phrase if (terms.length > 1) { + // Exact phrase variants.push(`"${terms.join(' ')}"`); + // AND query + variants.push(terms.join(' AND ')); + // Prefix AND on last term + variants.push([...terms.slice(0, -1), `${terms[terms.length - 1]}*`].join(' AND ')); + } else { + // Single term + variants.push(terms[0]); + if (terms[0].length >= 3) { + variants.push(`${terms[0]}*`); + } } - // AND query - variants.push(terms.join(' AND ')); + // Stemmed variant — truncate suffixes + wildcard + const stemmedTerms = terms.map(t => { + const stem = stemWord(t); + return stem ? `${stem}*` : t; + }); + if (stemmedTerms.some((s, i) => s !== terms[i])) { + variants.push(stemmedTerms.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 ')); + // OR fallback — any term matches (broadest) + if (terms.length > 1) { + variants.push(terms.join(' OR ')); } return variants; } + +/** + * Build a SQL LIKE pattern from search terms. + * Used as a final fallback when FTS5 returns no results. + * Example: "penalty offence" -> "%penalty%offence%" + */ +export function buildLikePattern(query: string): string { + const terms = query.trim().split(/\s+/).filter(t => t.length > 0); + if (terms.length === 0) return '%'; + return `%${terms.join('%')}%`; +} diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index 097d23c..929e279 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -9,6 +9,8 @@ export interface ResponseMetadata { jurisdiction: string; disclaimer: string; freshness?: string; + note?: string; + query_strategy?: string; } export interface ToolResponse {