brreg-mcp
by hybriden
README.md
# brreg-mcp
MCP-server rundt Enhetsregisteret, bygget for ett spørsmål registeret ikke svarer
på selv: **hvilke enheter har oppgitt dette regnskapsforetaket som
regnskapsfører?**
Enhetsregisteret indekserer rollen `REGN` fra kunden og utover
(`/enheter/{orgnr}/roller` → hvem er *min* regnskapsfører), aldri motsatt vei.
Den omvendte indeksen finnes ikke som API, så den bygges lokalt av den åpne
rolledumpen og lastes inn i D1.
Stateless Worker, ingen Durable Object, ingen autentisering.
**Live:** `https://brreg-mcp.hans-christian-thjomoe.workers.dev/mcp`
## Verktøy
### `kunder_for_regnskapsforer(orgnr, limit=500, offset=0, berik=true)`
Det omvendte oppslaget, fra den lokale indeksen.
| Felt | |
|---|---|
| `orgnr` | organisasjonsnummeret til regnskapsforetaket, 9 siffer |
| `limit` / `offset` | paginering, maks 1000 per kall |
| `berik` | `true` slår opp navn, bransje, sted, ansatte og konkursstatus live fra Enhetsregisteret. `false` gir bare organisasjonsnumre |
Svaret inneholder foretakets navn og godkjenningsstatus, totalt antall kunder,
datoen indeksen ble bygget (`kilde_dato`), tidspunktet for siste innspilte
endring (`oppdatert_til`), og kundelisten.
### `regnskapsforer_for(orgnr)`
Motsatt vei, live fra `/enheter/{orgnr}/roller`. Hver regnskapsfører i svaret får
`kunder_i_indeksen` fra den lokale indeksen, så du ser hvor stor den er. Tomt
svar er vanlig: registrering er frivillig, og store selskaper fører selv.
### `sok_enhet(navn, antall=10, ...filtre)`
Enhetsregisterets eget navnesøk, hele registeret. Filtre: `organisasjonsform`,
`kommunenummer`, `naeringskode`, `fra_antall_ansatte` — ukjente parametre gir 400
hos Brreg, så bare disse sendes videre.
Søket er uskarpt og relevanssortert. `navn=equinr` gir fortsatt EQUINOR ASA
øverst, men flerordssøk treffer dårlig: `navn=norsk hydro` legger Norsk Hydro ASA
på fjerdeplass til du filtrerer på `organisasjonsform=ASA`. `totalElements` fra
Brreg er rekkevidden til det uskarpe søket, ikke et treffantall, og rapporteres
derfor bare som flagget `flere_finnes`.
### `regnskapsforere_i(sted, bare_med_kunder=true, antall=50)`
Regnskapsforetakene i en kommune, sortert på kundetall. `sted` er kommunenavn
eller kommunenummer; kommunelista (365 stk.) hentes i ett kall og caches per
isolat. HERØY og VÅLER finnes to ganger, så begge slås opp og slås sammen.
Utvalget er næringskode **69.202** (regnskapsføring og bokføring). Merk at
`69.201` er *revisjon* — det er lett å bytte om. Foretak som fører regnskap under
en annen næringskode, kommer ikke med.
```
regnskapsforere_i("Tønsberg")
→ 64 foretak, 38 med kunder, 4191 registrerte kunder
571 918097902 SKYA REGNSKAP AS (34 ansatte)
432 881498022 HEIMTUN REGNSKAP & RÅDGIVNING AS (13 ansatte)
```
Krysningen leser hele `regnskapsforer`-tabellen og slår sammen i JS, så store
kommuner (Oslo: 795 foretak) ikke går i taket på D1s grense for bundne parametre.
## Bygge og laste indeksen
```bash
npm install
python3 bygg_indeks.py # laster dumpen (304 hvis uendret), skriver sql/data_*.sql
npx wrangler d1 create brreg # legg database_id inn i wrangler.jsonc
npx wrangler d1 execute brreg --remote --file=sql/schema.sql
for f in sql/data_*.sql; do npx wrangler d1 execute brreg --remote --file=$f; done
npx wrangler deploy
```
Per 2026-09-04: 2 773 regnskapsforetak, 443 307 kunderelasjoner, ~13 MB SQL.
Dumpen legges ut på nytt hver natt. Nedlastingen er betinget (`If-None-Match`),
så en re-bygging koster ingenting når filen er uendret. Med synken under trengs
re-bygging bare hvis indeksen skal settes opp på nytt.
Lokal utvikling: bytt `--remote` mot `--local`, så `npx wrangler dev`.
## Deploy
Push til `main` deployer via `.github/workflows/deploy.yml` — typesjekk, så
`wrangler deploy`. Prod er dermed det som står i `main`; `npx wrangler deploy`
lokalt gjør det ikke, og de to kan gli fra hverandre uten at noen ser det.
Krever to repo-secrets: `CLOUDFLARE_API_TOKEN` (mal «Edit Cloudflare Workers»,
med D1-tilgang) og `CLOUDFLARE_ACCOUNT_ID`.
## Holde indeksen oppdatert
En cron-trigger kjører hvert minutt og spiller inn Enhetsregisterets
endringsfeed (`/oppdateringer/roller`). For hver endret enhet hentes
`/enheter/{orgnr}/roller`, og enhetens rader i `kunde` erstattes med dagens
REGN-roller. `antall_kunder` justeres med differansen.
- Pekeren ligger i `metadata.siste_hendelse_id`. Mangler den, starter synken ett
døgn før `kilde_dato`; hver hendelse leses som nåtilstand, så avspilling er
ufarlig.
- 40 enheter per kjøring holder seg under Workers Free-grensen på 50
subrequests. Normal dag er rundt 1 500 endringer, som tas unna fortløpende;
etter en ny import tar innhentingen noen timer.
- `bygg_indeks.py` skriver metadata sist, så en re-bygging nullstiller pekeren.
- Databaser bygget før indeksen `kunde_etter_kunde` fantes, trenger den én gang:
`npx wrangler d1 execute brreg --remote --command "CREATE INDEX IF NOT EXISTS kunde_etter_kunde ON kunde (kunde_orgnr)"`
Test lokalt: `npx wrangler dev --test-scheduled`, så
`curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"`.
## Forbehold
- Registrering av regnskapsfører i Enhetsregisteret er **frivillig** for de
fleste selskapsformer. Listen er et gulv, ikke en fasit — kundeforhold som
ikke er registrert, er ikke synlige.
- Indeksen er ikke begrenset til autoriserte regnskapsforetak. Konsern som
fører regnskap for egne datterselskaper dukker også opp; Equinor ASA står
f.eks. som regnskapsfører for 65 enheter. Sjekk `godkjenningsstatus` i svaret.
- Indeksen henger etter feeden med opptil noen minutter. `oppdatert_til` i
svaret sier hvor langt synken har kommet.
Data: Enhetsregisteret, Brønnøysundregistrene. Lisens: NLOD.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues