Skip to main content
Glama
smridhiwho

open-india-law-mcp

by smridhiwho
README.md
# open-india-law-mcp

An MCP server that gives any MCP-compatible AI agent (Claude, Claude Code,
Claude Desktop, or any other MCP client) structured, correctly-attributed
access to [Vaquill AI's Open India Law dataset](https://github.com/vaquill-AI/open-india-law):

- 12.8M+ court judgments (Supreme Court + all 25 High Courts)
- 813K+ tribunal/regulator matters
- 1.1M+ legislation provisions (Central + every State/UT), section by section

It does **not** download the dataset. Court judgment files alone run to
tens of GB per court, and the full corpus is ~53.6 GB of judgment text plus
463.6 GB of vector embeddings. Instead it uses DuckDB's `hf://` support to
run filtered SQL directly against the parquet files on the Hugging Face
Hub, pulling only the row groups and columns a query actually needs.

## Why this design ("accessing it rightly")

- **No hardcoded file list.** The dataset can add or rename files between
  snapshots, so `list_catalog` asks the HF Hub what actually exists right
  now and categorizes it, instead of shipping a guess that goes stale.
- **No guessed schema for judgments.** The README documents exact fields
  for legislation and tribunal records, but not for plain court judgments.
  `search_judgments` / `get_judgment` introspect the real schema at call
  time (`get_schema`) and pick the right columns, rather than assuming
  field names that might not exist.
- **CC BY 4.0 is honored automatically.** Every tool response carries an
  `attribution` field (credit to Vaquill AI / Open India Law, CC BY 4.0)
  and a `caveat` field (point-in-time snapshot, not legal advice, verify
  against `source_url`/`source_pdf_url`). An agent doesn't have to
  remember the license terms — they travel with the data.
- **Scale-aware by construction.** `search_judgments` requires a `court`
  (scanning all 26 courts in one call isn't something a single query
  should silently attempt), and every query tool caps results at 200 rows.
  The generic `run_sql` tool refuses anything that isn't a single read-only
  `SELECT`/`WITH`.

## Install

Requires Python 3.10+.

```bash
git clone <this-repo>  
cd open-india-law-mcp
pip install -e .
```

This pulls in `mcp`, `duckdb`, and `huggingface_hub`. The dataset is
public, so no Hugging Face token is required — but if you hit HF rate
limits, set `HF_TOKEN` in your environment and the server will use it.

## Run it — 3 ways

**Before any of these:** run `python smoke_test.py` once. It hits the real
Hugging Face API and DuckDB extension CDN and confirms the whole path
works — nothing in this repo could be verified against live data during
development (the build sandbox blocks both hosts), so this is the first
real test.

### 1. As an MCP server (what Claude Desktop/Code actually run)

```bash
open-india-law-mcp
# equivalent: python -m open_india_law_mcp.server
```

This starts the stdio MCP server and blocks, waiting for a client to
connect over stdin/stdout. You won't see output — that's normal; connect a
client (below) rather than running it bare in a terminal to "test" it.

### 2. Directly from Python — no MCP protocol at all

`@mcp.tool()` registers a function with the server but returns it
unchanged, so every tool is just a plain importable Python function. Skip
the protocol entirely for a script, notebook, or test:

```python
from open_india_law_mcp import server

catalog = server.list_catalog()
hits = server.search_legislation(query="arbitration", state="central", limit=5)
act = server.get_act(act_id="IND_central_1996_26")
```

Full runnable version: [`examples/direct_python.py`](examples/direct_python.py).

### 3. As a real MCP client, in code

To drive it exactly the way Claude Desktop/Code do internally (spawn as a
subprocess, speak MCP over stdio) — useful for testing, or as a template
for a non-Claude agent:

```python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

params = StdioServerParameters(command="open-india-law-mcp")

async def main():
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("list_catalog", {})
            print(result.content[0].text)

asyncio.run(main())
```

Full runnable version: [`examples/mcp_client.py`](examples/mcp_client.py).

## Connecting real MCP clients

### Claude Code (CLI)

```bash
claude mcp add open-india-law -- open-india-law-mcp
```

Or, if it's not on your PATH:

```bash
claude mcp add open-india-law -- python -m open_india_law_mcp.server
```

Check it registered: `claude mcp list`. Remove it: `claude mcp remove open-india-law`.

### Claude Desktop

Edit your config file directly — `~/Library/Application Support/Claude/claude_desktop_config.json`
on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows:

```json
{
  "mcpServers": {
    "open-india-law": {
      "command": "open-india-law-mcp"
    }
  }
}
```

If you installed into a virtualenv rather than globally, either use that
venv's full path for `command` (`/path/to/venv/bin/open-india-law-mcp`),
or:

```json
{
  "mcpServers": {
    "open-india-law": {
      "command": "python",
      "args": ["-m", "open_india_law_mcp.server"],
      "cwd": "/path/to/open-india-law-mcp"
    }
  }
}
```

Restart Claude Desktop after editing the config.

### MCP Inspector (interactive debugging, no client needed)

```bash
npx @modelcontextprotocol/inspector open-india-law-mcp
```

Opens a browser UI where you can call each tool by hand and see the raw
JSON — the fastest way to poke at this without writing any code.

### Any other MCP client

This is a standard stdio server; point any MCP-compatible client at the
`open-india-law-mcp` command the same way you would point it at any other.

## Tools

| Tool | What it does |
| --- | --- |
| `list_catalog(refresh=False)` | Lists every court/regulator/state file that currently exists, with its `hf://` URI. **Call this first.** |
| `get_schema(file_uri)` | Real column names + types for a file, from `list_catalog`. |
| `run_sql(sql, limit=20)` | Read-only DuckDB SQL against any file(s) from the catalog. Full flexibility, capped at 200 rows. |
| `search_legislation(query, state="central", act_id=None, in_force_only=True, limit=20)` | Section-level search of one jurisdiction's Acts. |
| `get_act(act_id, state=None)` | Every section of one Act, in order. `state` is inferred from `act_id` if omitted. |
| `search_regulations(body, query=None, in_force_only=True, limit=20)` | Section-level search of one regulator's instruments (SEBI, RBI, ...). |
| `search_judgments(query, court, year_from=None, year_to=None, limit=20)` | Full-text search over one court's judgment chunks. `court` is required. |
| `get_judgment(case_id, court)` | Reassembles a full judgment from its chunks. |

Plus a static resource, `open-india-law://about`, with the license and
caveats in one place for clients that want to show it up front.

## How DuckDB reads parquet straight off the Hugging Face Hub

Nothing here is server-specific — it's plain DuckDB, so you can reproduce
any query from the `duckdb` CLI with zero Python:

```bash
duckdb -c "
INSTALL httpfs; LOAD httpfs;
SELECT act_id, section_number, section_title
FROM read_parquet('hf://datasets/vaquill/open-india-law/in_central_legislation.parquet')
WHERE text ILIKE '%arbitration%'
LIMIT 5;
"
```

**The `hf://` URI.** DuckDB's `httpfs` extension recognizes
`hf://datasets/<repo>/<path>` and resolves it to the Hub's actual
CDN/resolve URL under the hood — you never see or construct that URL
yourself. `catalog.hf_uri()` in this repo just builds the `hf://` string;
DuckDB does the resolution.

**Why this doesn't download whole files.** Parquet stores its schema and
per-row-group statistics (min/max, null counts) in a *footer* at the end
of the file. `httpfs` does an HTTP range request for that footer first,
then — for each column/predicate in your query — issues further range
requests only for the row groups whose stats can't rule them out. A
`WHERE year = 2023` on a 2 GB file might read a few MB. This is exactly
what lets `search_legislation`/`search_judgments` filter multi-GB files in
place instead of streaming them wholesale.

**Free-text search doesn't get this for free.** `ILIKE '%arbitration%'`
has no column statistic that can prove a row group doesn't contain the
word — DuckDB has to actually read every row group it can't rule out by
some *other* predicate. That's why `search_judgments` requires a `court`
(one file, not 26) and accepts `year_from`/`year_to`: the year filter
prunes row groups first, and only the survivors get scanned for text.

**Caching within a connection.** `duck.py` enables `enable_object_cache`
(keeps parsed parquet footers in memory) and `enable_http_metadata_cache`
(keeps ETag/size lookups in memory) on every connection it creates. Since
each worker thread reuses one long-lived connection, calling `get_schema`
then `search_legislation` against the same file only fetches that footer
once. There's no cross-process or on-disk cache — a fresh server process
starts cold.

**Authentication.** The dataset is public, so no token is required. If you
hit Hub rate limits, set `HF_TOKEN` in the environment the server runs in
— `duck.py` picks it up and registers it as a DuckDB secret
(`CREATE SECRET hf_token (TYPE HUGGINGFACE, TOKEN ...)`) automatically.

**Discovery vs. querying are two different Hub calls.** `list_catalog`
uses `huggingface_hub.HfApi.list_repo_files` (a small metadata API call,
cached for 15 minutes in `catalog.py`) to find out what files exist at
all. Actually reading a file's contents is a separate, unrelated path —
DuckDB's own HTTP client via `httpfs`, not `huggingface_hub`. The Python
package `huggingface_hub` is only ever used for that one discovery call.

## Performance notes

- Narrow before you search: pick one court/state/regulator file, and add a
  year range when the tool supports it. That's the difference between a
  sub-second query and one that has to scan a large fraction of a
  multi-GB file.
- Tribunal decisions (CAT, ITAT, NCLT, ...) are ~2.1M scanned PDFs with no
  text layer per the source dataset — they aren't in the judgment parquet
  files and this server doesn't attempt OCR. Use `list_catalog` /
  `run_sql` against whatever tribunal metadata files do exist in the repo.

## License

This server's code: MIT (add your own `LICENSE` file if you want one).
The **data** it queries is CC BY 4.0 — see `attribution.py` and the
`attribution`/`caveat` fields returned by every tool. The underlying legal
text is Government of India material reproduced under s.52(1)(q) of the
Copyright Act 1957. None of this is legal advice.

TDQS

A4.2/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a clearly distinct function: catalog discovery, schema inspection, raw SQL, legislation search, act retrieval, regulation search, judgment search, and judgment reassembly. The search_* and get_* pairs are cleanly separated, and run_sql is explicitly framed as a lower-level alternative rather than a competing high-level search tool.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern: list_catalog, get_schema, run_sql, search_legislation, get_act, search_regulations, search_judgments, get_judgment. The naming convention is uniform and predictable.

Tool Count5/5

Eight tools is well-scoped for a legal research server: three low-level data-access tools and five domain-specific search/retrieval tools. Each tool earns its place and the count is neither thin nor bloated.

Completeness4/5

Legislation has both search and full-act retrieval, judgments have search and full-judgment reassembly, and regulations have search but no dedicated get_regulation tool analogous to get_act. That is a minor gap, though an agent can work around it using run_sql.

Maintenance

ActivityMaintained
ResponsivenessNo issues