Skip to main content
Glama
slapash

JurisCassation MCP Server

by slapash
README.md
# JurisCassation API

GraphQL + REST wrapper for [juriscassation.cspj.ma](https://juriscassation.cspj.ma) — the Moroccan Court of Cassation decision portal.

## Features

- **Live search** — real-time passthrough to the Arabic search endpoint
- **Local search** — SQLite FTS5 full-text search over scraped decisions  
- **GraphQL API** — type-safe queries and mutations
- **PDF proxy** — cache and serve decision PDFs by encrypted ID
- **Background scraper** — index all decisions into local SQLite

## Quick Start

```bash
# Docker (GraphQL API on port 5091)
docker compose up -d

# Or direct
pip install -r requirements.txt
python app.py serve --port 8000

# MCP Server (port 5098, SSE transport)
# Via systemd:
sudo systemctl start juriscassation-mcp

# Or direct:
JURIS_API_BASE=http://localhost:5091 python mcp_server.py
```

## API Endpoints

| Endpoint | Description |
|----------|-------------|
| `GET /graphql` | GraphQL playground (interactive) |
| `POST /graphql` | GraphQL API |
| `GET /api/pdf/{encrypted_id}` | Download decision PDF (proxied + cached) |
| `GET /api/stats` | Local index statistics |

## GraphQL Schema

### Queries

```graphql
# Live search against the Moroccan portal (real-time)
query {
  searchLive(chamberId: 1, sujet: "طلاق", lang: "ar", page: 1) {
    total
    page
    perPage
    items {
      encryptedId
      dossierNumber
      decisionNumber
      decisionDate
      keywordsAr
      pdfUrl
    }
  }
}

# Search the local SQLite index
query {
  searchLocal(query: "طلاق", chamberId: 1, page: 1, perPage: 20) {
    total
    page
    perPage
    items {
      id
      encryptedId
      dossierNumber
      decisionNumber
      decisionDate
      keywordsAr
      pdfUrl
    }
  }
}

# List chambers with decision counts
query {
  chambers {
    id
    nameAr
    nameFr
    nameEn
    decisionCount
  }
}

# Scraper/indexer status
query {
  scraperStatus {
    totalDecisions
    lastScrape
    chambers { id nameFr decisionCount }
  }
}
```

### Mutations

```graphql
# Scrape decisions from a chamber (Arabic endpoint with alef wildcard)
mutation {
  scrapeChamber(chamberId: 1, maxPages: 5)
}

# Scrape all pages
mutation {
  scrapeChamber(chamberId: 1, maxPages: 0)
}
```

## Chambers

| ID | Arabic | French | English |
|----|--------|--------|---------|
| 1 | الغرفة المدنية | Chambre Civile | Civil |
| 2 | غرفة الأحوال الشخصية و الميراث | Statut Personnel & Successions | Personal Status & Inheritance |
| 3 | الغرفة التجارية | Chambre Commerciale | Commercial |
| 4 | الغرفة الإدارية | Chambre Administrative | Administrative |
| 5 | الغرفة الاجتماعية | Chambre Sociale | Social |
| 6 | الغرفة الجنائية | Chambre Pénale | Criminal |
| 7 | الغرفة العقارية | Chambre Immobilière | Real Estate |

## CLI Scraper

```bash
# Scrape chamber 1 (all pages)
python app.py scrape --chamber 1

# Scrape all chambers, max 50 pages each
python app.py scrape --chamber 0 --max-pages 50

# Scrape from page 100 onwards
python app.py scrape --chamber 6 --since-page 100
```

## Architecture Notes

- The Arabic endpoint (`/Decisions/RechercheDecisionsRes`) returns encrypted `data-id` attributes needed for PDF download
- The French endpoint (`/Decisions/RechercheDecisionsResFr`) returns numeric `idArretCassation` IDs but no encrypted IDs
- PDFs are downloaded via `/Decisions/GetArret?encryptedId=...` — no auth required
- Empty `Sujet` in Arabic search returns 0 results; we use ا (alef) as a wildcard catch-all
- SSL cert on the upstream site is broken; `verify=False` is required

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `JURIS_DB` | `/data/decisions.db` | SQLite database path |
| `JURIS_PDF_CACHE` | `/data/pdfs` | PDF cache directory |
| `JURIS_CONCURRENCY` | `3` | Max concurrent scrape requests |
| `JURIS_DELAY` | `0.5` | Delay between scrape pages (seconds) |

## MCP Server

An MCP (Model Context Protocol) server exposes all API features as AI-agent tools via SSE on port 5098.

**MCP Endpoint:** `http://localhost:5098/sse`

### Tools

| Tool | Description |
|------|-------------|
| `search_decisions` | Live search against juriscassation.cspj.ma |
| `search_local` | FTS5 search over local SQLite index |
| `get_decision_pdf` | Check/download a decision PDF by encrypted ID |
| `list_chambers` | List all 7 chambers with index counts |
| `scrape_chamber` | Index decisions from a chamber |
| `get_stats` | Local index statistics |

### Resources

| URI | Description |
|-----|-------------|
| `juriscassation://chambers` | Chamber reference list (AR/FR/EN) |
| `juriscassation://stats` | Current index statistics |

### MCP Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `JURIS_API_BASE` | `http://localhost:5091` | GraphQL API base URL |
| `JURIS_DB` | `/mnt/data/juriscassation/decisions.db` | SQLite path (for direct search) |
| `MCP_PORT` | `5098` | MCP server port |
| `MCP_HOST` | `0.0.0.0` | MCP server bind host |
| `MCP_TRANSPORT` | `sse` | Transport type (`sse` or `streamable-http`) |

### Test with mcporter

```bash
# List tools
npx mcporter list --http-url http://localhost:5098/sse --allow-http --schema

# Search
npx mcporter call search_decisions sujet=طلاق chamber_id=1 --allow-http --http-url http://localhost:5098/sse
```

## Deployment

The app is containerized and runs on port 8000 internally, mapped to host port 5091.