Skip to main content
Glama
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.