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
Metadata
Release files for paragraf 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| paragraf-0.1.0.tar.gz | 70.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| paragraf-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 121.4 kB
Release files / paragraf-0.1.0.tar.gz
| Download URL | paragraf-0.1.0.tar.gz |
|---|---|
| Size | 70.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
abc19ed5f70bfb8460f0274416a3da1526d668d3d5faca913e669849b1e071b9
|
|
BLAKE2b-256 checksum How to use checksums |
91b880990f7a1ec44ad6fc8af8b3ee33f4d31c95f0562ef786f3b33055812dfe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / paragraf-0.1.0-py3-none-any.whl
| Download URL | paragraf-0.1.0-py3-none-any.whl |
|---|---|
| Size | 51.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9bfe183c970268ead80343f58d4a95dc4fd7338a54c28d70fb75c2636219de53
|
|
BLAKE2b-256 checksum How to use checksums |
f785c522e4a8dab69f8477ab155c4267a434a0a7061977b391db7f378dcfa4ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|