# MCP-server til AI-agenter

Grundfasts MCP-server giver en AI-agent adgang til registrene som værktøjer. Agenten forbinder med en almindelig API-nøgle og får de samme svar som REST-API’et.

## Endpoint og protokol

| Egenskab | Værdi |
| --- | --- |
| Endpoint | `https://api.grundfast.dk/mcp` |
| Transport | Streamable HTTP: hver JSON-RPC 2.0-besked sendes som `POST` og besvares med JSON. |
| Protokolversion | `2025-06-18` |
| Tilstand | Stateless — ingen session-id, hvert kald står alene. |
| Godkendelse | `Authorization: Bearer gf_live_...` (eller `gf_test_...`). Kan klienten ikke sætte en header, accepteres nøglen også som `?token=` på URL’en — men brug headeren, hvor du kan. |
| Metoder | `initialize`, `tools/list`, `tools/call`, `ping` og notifikationer. |

- **Forbrug:** hvert vellykket `tools/call` tæller som ét kald, præcis som et REST-kald. `initialize`, `tools/list`, `ping` og værktøjskald, der fejler, faktureres ikke.
- **Grænser:** samme nøgle, burst-grænse og månedskvote som REST. Rammer agenten en grænse, svarer serveren HTTP `429` med `Retry-After`. Se [Rate limits](https://grundfast.dk/docs/rate-limits).
- **Fejl:** et værktøj, der fejler (ugyldigt argument, ukendt BFE, en kilde der er nede), svarer med `isError: true` og API’ets fejltekst i resultatet — ikke med en protokolfejl. Et ukendt værktøjsnavn er JSON-RPC-fejl `-32601`.
- **Testnøgler** virker også, så du kan bygge og teste en agent uden at blive faktureret.

## Værktøjerne

Hvert værktøj svarer til et REST-endpoint og returnerer den samme JSON som tekst i `content`. `tools/list` returnerer den aktuelle liste med JSON Schema for argumenterne.

| Værktøj | Argumenter | Hvad det svarer på |
| --- | --- | --- |
| bbr_ejendom | { bfe } | Hele BBR-ejendommen: bygninger, enheder, grund — med fredning og SAVE-bevaringsværdi pr. bygning. |
| bbr_historik | { bfe } | Versionshistorikken for hver af ejendommens nuværende bygninger, nyeste først. |
| bbr_bygningssoegning | { kommunekode \| postnr, anvendelse?, opfoert_fra?, opfoert_til?, varme?, opvarmning?, tag?, ydervaeg?, asbest?, areal_min?, areal_max?, olietank?, side?, per_side? } | Alle stående bygninger i en kommune eller et postnummer, der matcher BBR-fakta: anvendelse, alder, varme, materialer, asbest, areal og olietank. Antal og første side på alle planer. |
| matrikel_ejendom | { bfe } | Jordstykker og ejerlav for ejendommen. |
| ejendomsvurdering | { bfe } | Ejendoms- og grundværdier pr. vurderingsår. |
| geodanmark_bygninger | { bfe } | Bygningernes omrids som WGS84-polygoner. |
| dhm_bygningshoejder | { bfe } | Målt højde på hver bygning, målt inde i omridset, i DVR90. |
| adresse_soeg | { q, per_side? } | Adressesøgning i fritekst. |
| adresse_opslag | { uuid } | En adresse på DAR-UUID, med BFE. |
| adresse_historik | { uuid } | Versionshistorikken for en adresse eller et husnummer. |
| adresse_datavask | { betegnelse } | Vask en uren adressestreng til rangerede kandidater med A/B/C-kategori. |
| adgangsadresse_reverse | { lon, lat, radius_m? } | De nærmeste adgangsadresser til et punkt. |
| adresse_aendringer | { kommunekode?, postnr?, adgangsadresse_id?, haendelse?, efter?, antal? } | Nye, ændrede og nedlagte adresser i et område fra den daglige DAR-opdatering. Kræver Pro. |
| cvr_virksomhed | { cvr } | En virksomhed på CVR-nummer. |
| dagi_reverse | { lon, lat } | Region, kommune, sogn, retskreds, politikreds og opstillingskreds for et punkt. |
| dagi_omraade | { tema, kode } | Ét administrativt område på tema og kode, fx kommune 101. |
| dhm_hoejde | { lon, lat } | Terræn- og overfladekote i et punkt. |
| stednavne_naer | { lon, lat, radius_m? } | Stednavne nær et punkt. |
| jord_omraadeklassificering | { lon, lat } | Om punktet er områdeklassificeret (§ 50 a), og om det er analysefrit. |
| natur_skovbyggelinje | { lon, lat } | Om punktet ligger inden for en gældende eller ophævet skovbyggelinje (§ 17). |
| natur_byggelinjer | { lon, lat } | Alle byggelinjer og § 3-beskyttelser på punktet med afstand til kanten. |
| plan | { lon, lat } | Zone, lokalplaner, delområder og kommuneplanrammer på punktet (Plandata). |
| plan_ejendom | { bfe } | Planerne for en ejendom plus vejledende restbyggeret fra Matriklen og BBR. |
| miljoe_screening | { bfe } | Miljø-forhåndsscreening pr. bygningsdel (asbest, PCB, bly, PAH, klorparaffiner, tungmetaller) plus jord, bindinger og affald. |
| miljoe_stoej | { lon, lat } | Vej-, jernbane-, lufthavns- og industristøj på punktet (Miljøstyrelsens støjkortlægning, 2017). |
| orto_luftfoto_aargange | { lon, lat } | De luftfoto-årgange, der er fløjet over punktet. |

Agenten får kun offentlige registerdata i samme form som REST. Koordinater er WGS84 `lon`/`lat` og skal ligge i Danmark.

## Opsætning

### Claude Code

```bash
claude mcp add --transport http grundfast https://api.grundfast.dk/mcp \
  --header "Authorization: Bearer gf_live_..."
```

### Claude Desktop

Claude Desktops konfigurationsfil tager lokale servere (en kommando), ikke en fjern-URL med headere. Brug broen `mcp-remote`, som kører lokalt via `npx` (kræver Node) og videresender til Grundfast med din nøgle:

```json
{
  "mcpServers": {
    "grundfast": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://api.grundfast.dk/mcp",
        "--header",
        "Authorization:${GRUNDFAST_AUTH}"
      ],
      "env": {
        "GRUNDFAST_AUTH": "Bearer gf_live_..."
      }
    }
  }
}
```

Bemærk, at der ikke er mellemrum efter kolonet i `Authorization:${GRUNDFAST_AUTH}`; mellemrummet ligger i værdien af `GRUNDFAST_AUTH`. Det undgår, at argumentet bliver delt i to. Genstart Claude Desktop efter ændringen.

### Cursor

```json
{
  "mcpServers": {
    "grundfast": {
      "url": "https://api.grundfast.dk/mcp",
      "headers": { "Authorization": "Bearer gf_live_..." }
    }
  }
}
```

### ChatGPT og OpenAI

ChatGPTs forbindelser til egne MCP-servere understøtter OAuth eller ingen godkendelse, ikke en fast Bearer-header, så Grundfast kan ikke tilføjes dér. Via OpenAI’s Responses API kan du derimod angive headeren på MCP-værktøjet:

**Python**

```py
from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-4.1",
    tools=[
        {
            "type": "mcp",
            "server_label": "grundfast",
            "server_url": "https://api.grundfast.dk/mcp",
            "headers": {"Authorization": "Bearer gf_live_..."},
            "require_approval": "never",
        }
    ],
    input="Hvornår er hovedbygningen på BFE 5651067 opført, og hvad er taget lavet af?",
)
print(response.output_text)
```

**JavaScript**

```js
import OpenAI from 'openai';

const client = new OpenAI();

const response = await client.responses.create({
  model: 'gpt-4.1',
  tools: [
    {
      type: 'mcp',
      server_label: 'grundfast',
      server_url: 'https://api.grundfast.dk/mcp',
      headers: { Authorization: 'Bearer gf_live_...' },
      require_approval: 'never',
    },
  ],
  input: 'Hvornår er hovedbygningen på BFE 5651067 opført, og hvad er taget lavet af?',
});
console.log(response.output_text);
```

Andre MCP-klienter, der understøtter Streamable HTTP med egne headere, forbindes på samme måde: URL’en ovenfor og `Authorization`-headeren.

## Rå JSON-RPC

Vil du teste serveren uden en klient, kan du sende beskederne med curl. Hver besked er ét JSON-objekt; batches af flere beskeder understøttes ikke.

```bash
KEY="gf_test_..."

# 1. Håndtryk
curl -s https://api.grundfast.dk/mcp \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'

# 2. List værktøjerne
curl -s https://api.grundfast.dk/mcp \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 3. Kald et værktøj
curl -s https://api.grundfast.dk/mcp \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"bbr_ejendom","arguments":{"bfe":5651067}}}'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "grundfast", "title": "Grundfast", "version": "1.0.0" }
  }
}
```

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{\"type\":\"ejendom\",\"bfe_nummer\":5651067, … }" }]
  }
}
```

Et værktøj, der fejler, svarer med `"isError": true` og fejlteksten i `content`. En notifikation (en besked uden `id`) besvares med `202` uden indhold, og `GET` på endpointet giver `405`. Uden gyldig nøgle svarer serveren `401` med en `WWW-Authenticate`-header, der beskriver det forventede format.
