MCP server for Norwegian law lookup via Lovdata
Project description
Paragraf
MCP-server som gir AI-assistenter tilgang til alle norske lover og forskrifter via Model Context Protocol.
92 000+ paragrafer fra 770 lover og 3 666 forskrifter — gratis under NLOD 2.0-lisensen.
Hvorfor
LLM-er hallusinerer lovtekst. Denne serveren gir dem presis, oppdatert norsk rett som verktøykall i stedet for gjetting.
Bruker: "Kan utleier si meg opp?"
AI: sok("oppsigelse leie") → 4 treff
lov("husleieloven", "9-7") → full tekst
Svar: "Etter husleieloven § 9-7 skal oppsigelse fra utleier
være skriftlig og begrunnet..."
Funksjoner
| Funksjon | Beskrivelse |
|---|---|
| Lovoppslag | Slå opp enhver lov/forskrift med kortnavn eller full ID |
| Fulltekstsøk (FTS) | PostgreSQL tsvector med norsk stemming, ~6ms |
| Semantisk søk | Hybrid vektor+FTS med Gemini embeddings for naturlig språk |
| Batch-henting | Hent flere paragrafer i ett kall (~80% raskere) |
| Innholdsfortegnelse | Hierarkisk oversikt (Del → Kapittel → §) med token-estimat |
| Alias-oppløsning | aml, avhl, pbl + fuzzy matching for stavefeil |
| OR-fallback | AND-søk som automatisk faller tilbake til OR ved 0 treff |
Arkitektur
┌──────────────────┐ HTTPS/JSON-RPC ┌──────────────────────────┐
│ Claude.ai │ ──────────────────────► │ Flask Backend │
│ Copilot Studio │ │ │
│ Gemini AI │ │ ┌────────────────────┐ │
│ (MCP-klient) │ ◄────────────────────── │ │ MCP Server │ │
└──────────────────┘ │ │ (JSON-RPC router) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ LovdataService │ │
│ │ (alias, validering│ │
│ │ formatering) │ │
│ └────────┬───────────┘ │
│ │ │
└───────────┼──────────────┘
│
┌─────────────────┴──────────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Supabase │ │ Lovdata API │
│ PostgreSQL │ │ api.lovdata.no │
│ │ │ │
│ • FTS (GIN) │ │ Bulk tar.bz2 │
│ • pgvector │ │ (kun ved sync) │
│ • pg_trgm │ │ │
└───────────────────┘ └───────────────────┘
Hurtigstart
Forutsetninger
- Python 3.11+
- PostgreSQL med
pgvectorogpg_trgm(Supabase anbefalt) - Valgfritt: Gemini API-nøkkel for semantisk søk
Installasjon
pip install paragraf # Minimal (SQLite backend)
pip install paragraf[supabase] # Med Supabase PostgreSQL
pip install paragraf[all] # Alt (Supabase + vektorsøk + HTTP)
Eller fra kildekode:
git clone https://github.com/khjohns/paragraf.git
cd paragraf
pip install -e ".[all]"
Konfigurasjon
# .env
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# Valgfritt: for semantisk søk
GEMINI_API_KEY=AIza...
Uten Supabase brukes SQLite som lokal fallback.
Kjør databasemigreringer
# Kjør mot Supabase (i rekkefølge)
supabase db push
Migreringene oppretter:
lovdata_documents— lover og forskrifterlovdata_sections— paragrafer med FTS og embeddinglovdata_structure— hierarkisk struktur (del/kapittel/avsnitt)lovdata_sync_meta— sync-metadata- SQL-funksjoner for søk og fuzzy matching
Synkroniser lovdata
paragraf sync # Første gang (tar 5-10 min)
Start serveren
paragraf serve # stdio MCP (for Claude Desktop, Cursor)
paragraf serve --http # HTTP MCP (for claude.ai connector)
paragraf serve --http --port 8000
Koble til Claude.ai
- Gå til Settings → Connectors → Add custom connector
- URL:
https://your-domain.com/mcp/ - Ferdig — ingen autentisering kreves
MCP-verktøy
| Verktøy | Beskrivelse | Eksempel |
|---|---|---|
lov |
Slå opp lov | lov("aml", "14-9") |
forskrift |
Slå opp forskrift | forskrift("foa", "25-2") |
sok |
Fulltekstsøk | sok("mangel bolig") |
semantisk_sok |
AI-drevet søk | semantisk_sok("skjulte feil i boligen") |
hent_flere |
Batch-henting | hent_flere("aml", ["14-9", "15-6"]) |
sjekk_storrelse |
Token-estimat | sjekk_storrelse("skatteloven", "5-1") |
liste |
Vis aliaser | liste() |
status |
Sync-status | status() |
sync |
Synkroniser | sync(force=True) |
Alias-oppløsning
Fire nivåer for å finne riktig lov:
| Nivå | Eksempel |
|---|---|
| 1. Hardkodet alias | aml → LOV-2005-06-17-62 |
| 2. Database (short_title) | husleieloven → lov/1999-03-26-17 |
| 3. Fuzzy (pg_trgm) | husleielova → husleieloven (similarity: 0.59) |
| 4. Direkte ID | lov/1999-03-26-17 → brukes som-er |
Søkesyntaks (FTS)
| Syntaks | Eksempel | Betydning |
|---|---|---|
| Standard | mangel bolig |
AND (begge ord) |
| OR | miljø OR klima |
Minst ett ord |
| Frase | "vesentlig mislighold" |
Eksakt frase |
| Ekskludering | mangel -bil |
mangel, ikke bil |
AND-søk som gir 0 treff faller automatisk tilbake til OR.
Mappestruktur
src/paragraf/
├── __init__.py # Pakke-eksport (MCPServer, LovdataService)
├── server.py # MCP JSON-RPC server (9 verktøy)
├── service.py # Forretningslogikk, aliaser, validering
├── supabase_backend.py # Supabase PostgreSQL backend
├── sqlite_backend.py # SQLite fallback + sync fra API
├── structure_parser.py # XML → hierarkisk struktur
├── vector_search.py # Hybrid vektor+FTS søk
├── cli.py # CLI (serve, sync, status)
└── web.py # Flask blueprint factory
web/
└── app.py # Full Flask blueprint (OAuth, SSE, HTTP)
scripts/
└── embed.py # Generer embeddings for vektorsøk
migrations/
├── 20260203_create_lovdata_tables.sql
└── 20260206_add_lovdata_structure.sql
API-endepunkter
| Metode | Sti | Beskrivelse |
|---|---|---|
POST |
/mcp/ |
MCP JSON-RPC (hovedendepunkt) |
HEAD |
/mcp/ |
Protokollversjon-sjekk |
GET |
/mcp/ |
SSE-stream (bakoverkompatibilitet) |
GET |
/mcp/health |
Helsesjekk |
GET |
/mcp/info |
Serverinfo og verktøyliste |
Ytelse
| Metrikk | Verdi |
|---|---|
| FTS-søk (warm cache) | ~6ms |
| FTS-søk (cold cache) | ~600ms |
| Lovoppslag | ~50-200ms |
| Batch 3 paragrafer | ~100ms (vs 491ms separat) |
| Database-størrelse | ~160MB tabell + 42MB TOAST + 37MB GIN |
| Vektorsøk (hybrid) | ~200-500ms (inkl. embedding) |
Datamodell
Tabeller
lovdata_documents (4 439 rader)
├── dok_id TEXT UNIQUE "lov/2005-05-20-28"
├── title TEXT "Lov om arbeidsmiljø..."
├── short_title TEXT "Arbeidsmiljøloven"
├── doc_type TEXT "lov" | "forskrift"
├── ministry TEXT "Arbeids- og inkluderingsdepartementet"
└── search_vector TSVECTOR
lovdata_sections (92 130 rader)
├── dok_id + section_id UNIQUE
├── content TEXT Paragraftekst
├── search_vector TSVECTOR Norsk stemming
├── embedding VECTOR(1536) Gemini embedding
├── char_count INTEGER GENERATED ALWAYS
└── structure_id UUID FK → lovdata_structure
lovdata_structure (13 909 rader)
├── structure_type TEXT "del" | "kapittel" | "avsnitt" | "vedlegg"
├── title TEXT "Kapittel 2. Arbeidsgivers plikter"
├── parent_id UUID FK Hierarkisk (self-ref)
└── sort_order INTEGER
Indekser
- GIN på
search_vector— fulltekstsøk - GIN på
short_titlemedpg_trgm— fuzzy matching - IVFFlat på
embedding(lists=100) — vektorsøk - B-tree på
dok_id,section_id,structure_id— oppslag
Sikkerhet
- Ingen brukerdata lagres — authless design
- Parameteriserte queries — ingen SQL injection
- Input-validering på alle MCP-verktøy
- Rate limiting anbefalt i produksjon (flask-limiter)
- NLOD 2.0-lisens — alle data er offentlige
Testet mot
| Angrep | Resultat |
|---|---|
SQL injection ('; DROP TABLE--) |
Blokkert |
Path traversal (../../../etc/passwd) |
Ingen filsystem-tilgang |
XSS (<script>alert('xss')</script>) |
Behandlet som tekst |
Begrensninger
Inkludert (gratis via Lovdata Public API)
- Gjeldende lover (770+)
- Sentrale forskrifter (3 666+)
- Lokale forskrifter, delegeringer, instrukser
IKKE inkludert
- Rettsavgjørelser (Høyesterett, lagmannsrett) — krever Lovdata Pro
- Forarbeider (NOU, Prop., Ot.prp.) — krever Lovdata Pro
- Juridiske artikler
Deploy
Render
# render.yaml
services:
- type: web
name: paragraf
runtime: python
buildCommand: pip install paragraf[all]
startCommand: paragraf serve --http
healthCheckPath: /mcp/health
Miljøvariabler
| Variabel | Påkrevd | Beskrivelse |
|---|---|---|
SUPABASE_URL |
Ja* | Supabase prosjekt-URL |
SUPABASE_SERVICE_ROLE_KEY |
Ja* | Service role nøkkel |
GEMINI_API_KEY |
Nei | For semantisk søk |
MCP_REQUIRE_AUTH |
Nei | true for OAuth 2.1 |
LOVDATA_CACHE_DIR |
Nei | SQLite cache-sti (default: /tmp/lovdata-cache) |
* SQLite brukes som fallback uten Supabase.
Utvikling
# Generer embeddings (krever GEMINI_API_KEY)
python scripts/embed.py
# Helsesjekk
curl http://localhost:8000/mcp/health
# Test MCP-kall
curl -X POST http://localhost:8000/mcp/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "lov",
"arguments": {"lov_id": "aml", "paragraf": "14-9"}
}
}'
Teknologi
| Komponent | Teknologi |
|---|---|
| Server | Flask + Python 3.11 |
| Database | Supabase PostgreSQL (SQLite fallback) |
| Fulltekstsøk | PostgreSQL tsvector + GIN |
| Vektorsøk | pgvector IVFFlat + Gemini embeddings |
| Fuzzy matching | pg_trgm |
| Protokoll | MCP 2025-06-18, Streamable HTTP |
| Datakilde | Lovdata Public API (NLOD 2.0) |
Lisens
Inneholder data under Norsk lisens for offentlige data (NLOD 2.0) tilgjengeliggjort av Lovdata.
Relatert dokumentasjon
- ADR-001: Arkitekturbeslutninger — alle arkitekturbeslutninger
- Dataflyt — sikkerhet og nettverksmodell
- Roadmap — veien videre
- MCP Specification
- Lovdata Public API
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paragraf-0.1.0.tar.gz.
File metadata
- Download URL: paragraf-0.1.0.tar.gz
- Upload date:
- Size: 70.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abc19ed5f70bfb8460f0274416a3da1526d668d3d5faca913e669849b1e071b9
|
|
| MD5 |
21bd87707137d6f2f3dcde75c599658c
|
|
| BLAKE2b-256 |
91b880990f7a1ec44ad6fc8af8b3ee33f4d31c95f0562ef786f3b33055812dfe
|
File details
Details for the file paragraf-0.1.0-py3-none-any.whl.
File metadata
- Download URL: paragraf-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bfe183c970268ead80343f58d4a95dc4fd7338a54c28d70fb75c2636219de53
|
|
| MD5 |
2d9c46526ccc1785ffd3fedd33964ee2
|
|
| BLAKE2b-256 |
f785c522e4a8dab69f8477ab155c4267a434a0a7061977b391db7f378dcfa4ec
|