Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.env
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
.coverage
htmlcov/
.git/
.gitignore
CLAUDE.md
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RETSINFORMATION_BASE_URL=https://retsinformation-api.dk
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Tests

on:
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v4

- run: uv sync --extra dev

- run: uv run pytest
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.egg-info/
dist/
build/
.eggs/

# Virtual environments
.venv/
venv/
env/

# uv
.uv/

# Environment
.env

# IDE
.idea/
.vscode/

# pytest
.pytest_cache/
.coverage
htmlcov/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
51 changes: 51 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

`mcp-retsinformation` is a [FastMCP](https://github.com/jlowin/fastmcp) server that exposes tools for querying Danish legislation via the [retsinformation-api.dk](https://retsinformation-api.dk) REST API (`https://retsinformation-api.dk/v1/lovgivning/`). The API is free, requires no authentication, but is rate-limited to 20 req/hour and 50 req/day per IP.

## Commands

Use [uv](https://github.com/astral-sh/uv) for dependency management.

```bash
# Install dependencies (creates .venv)
uv sync --extra dev

# Run the MCP server (stdio transport, for local development)
python -m mcp_retsinformation

# Run tests
uv run pytest

# Run a single test file
uv run pytest tests/test_search.py

# Build and run with Docker (dev profile mounts source for live reload)
docker compose --profile dev up --build

# Build and run in production mode
docker compose --profile prod up --build
```

## Architecture

```
src/mcp_retsinformation/
├── server.py # Creates the single FastMCP instance (`mcp`)
├── main.py # Entry point: imports tools (triggering registration), calls mcp.run()
├── __main__.py # Allows `python -m mcp_retsinformation`
├── config.py # Pydantic Settings — loads from .env
└── tools/
└── search.py # @mcp.tool()-decorated functions registered on the mcp instance
```

**Tool registration pattern:** `server.py` creates the `mcp = FastMCP(...)` instance. Each file in `tools/` imports `mcp` from `server.py` and decorates functions with `@mcp.tool()`. `main.py` imports the tools modules as side-effect imports to trigger registration before calling `mcp.run()`.

**Transport:** The server runs with `streamable-http` transport on port 8000 when deployed via Docker. For local Claude Desktop integration, run with the default `stdio` transport by omitting the `transport` argument.

## Configuration

Copy `.env.example` to `.env`. Currently the only setting is `RETSINFORMATION_BASE_URL` (defaults to `https://api.retsinformation.dk`).
27 changes: 27 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENVIRONMENT=/app/.venv \
PATH="/app/.venv/bin:$PATH"

ARG GID=1042
ARG UID=1042

RUN pip install --upgrade pip && pip install uv

WORKDIR /app

COPY . .
RUN uv sync --frozen --no-dev

RUN addgroup --gid ${GID} deploy \
&& useradd --gid ${GID} --uid ${UID} --home-dir /home/deploy --create-home --shell /bin/bash deploy

RUN chown -R deploy:deploy /app

USER deploy

EXPOSE 8000

CMD ["python", "-m", "mcp_retsinformation"]
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,89 @@
# mcp-retsinformation

An [MCP](https://modelcontextprotocol.io) server that gives AI assistants (Claude, etc.) direct access to Danish legislation via [retsinformation-api.dk](https://retsinformation-api.dk).

Once connected, an AI can search for laws, read their full text, inspect amendment history, and retrieve how a law looked on any given date in the past.

## Prerequisites

- [Docker](https://docs.docker.com/get-docker/)
- [Task](https://taskfile.dev/installation/)

## Getting started

```bash
cp .env.example .env
task dev
```

This builds the image and starts the dev container with the source directory mounted.

## Available tools

| Tool | Description | Test coverage |
|---|---|---|
| `search_lovgivning` | Search across all document types by free text | with query, without query, default limit |
| `get_lovgivning` | Fetch the full structured text of a law by year and number | correct URL and return value |
| `get_lovgivning_markdown` | Fetch a law as Markdown, with optional section filtering | no filters, with `exclude`, with `paragraphs` |
| `get_lovgivning_at_date` | Retrieve a law exactly as it appeared on a specific date | correct URL with date |
| `get_lovgivning_amendments` | List all amendments made to a law | correct URL and return value |
| `get_rate_limit_status` | Check API rate limit usage (20/hour, 50/day) | — |

## Testing

Run the full test suite:

```bash
task test
```

Run a single file:

```bash
task test:file FILE=tests/test_search.py
```

## All tasks

```
task install Install dependencies incl. dev extras
task run Run the MCP server (stdio transport)
task test Run all tests
task test:file Run a single test file (FILE=...)
task dev Build and start the dev container
task prod Build and start the production container
task down Stop and remove containers
```

## Rate limiting

The API allows 20 requests/hour and 50 requests/day. The server includes a `get_rate_limit_status` tool that tracks usage in-memory (resets on container restart).

To ensure the LLM checks rate limits before each request, add the following to your client's system prompt:

> Before using this retsinformation MCP tool always inform the user that you are using it to find retsinformation. Always call get_rate_limit_status first and display the result. If remaining_hour or remaining_day is 0, inform the user that the rate limit has been reached instead of making the request.

Server-level MCP `instructions` can also be set in `server.py`, but not all clients surface them to the LLM.

## Configuration

| Variable | Default | Description |
|---|---|---|
| `RETSINFORMATION_BASE_URL` | `https://retsinformation-api.dk` | Base URL for the API |

The API is free, requires no authentication, and is rate-limited to 20 requests/hour and 50 requests/day per IP.

## Connecting to Claude Desktop

Add the following to your `claude_desktop_config.json`:

```json
{
"mcpServers": {
"retsinformation": {
"command": "docker",
"args": ["compose", "run", "--rm", "app-dev", "python", "-m", "mcp_retsinformation"]
}
}
}
```
47 changes: 47 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
version: '3'

dotenv:
- .env

vars:
COMPOSE: docker compose
IMAGE_NAME: mcp-retsinformation

tasks:
install:
desc: Install dependencies incl. dev extras
cmds:
- "{{.COMPOSE}} run --rm app-dev uv sync --extra dev"

run:
desc: Run the MCP server (stdio transport)
cmds:
- "{{.COMPOSE}} run --rm app-dev python -m mcp_retsinformation"

test:
desc: Run all tests
cmds:
- "{{.COMPOSE}} run --rm app-dev uv run pytest"

test:file:
desc: "Run a single test file — usage: task test:file FILE=tests/test_search.py"
cmds:
- "{{.COMPOSE}} run --rm app-dev uv run pytest {{.FILE}}"
requires:
vars:
- FILE

dev:
desc: Build and start the dev container (source mounted for live reload)
cmds:
- "{{.COMPOSE}} up app-dev --build"

prod:
desc: Build and start the production container
cmds:
- "{{.COMPOSE}} up app-prod --build"

down:
desc: Stop and remove containers
cmds:
- "{{.COMPOSE}} down"
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
services:
app:
build: .
ports:
- "8000"
environment:
- TZ=Europe/Copenhagen
- RETSINFORMATION_BASE_URL=${RETSINFORMATION_BASE_URL:-https://retsinformation-api.dk}
profiles:
- base-only

app-dev:
extends:
service: app
volumes:
- .:/app
profiles:
- dev

app-prod:
extends:
service: app
profiles:
- prod
33 changes: 33 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[build-system]
requires = ["setuptools>=65.0"]
build-backend = "setuptools.build_meta"

[project]
name = "mcp-retsinformation"
version = "0.1.0"
description = "MCP server for retsinformation.dk — Danish legal information"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"fastmcp>=2.0",
"httpx>=0.28",
"loguru>=0.7",
"pydantic>=2.0",
"pydantic-settings>=2.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=1.0",
"pytest-mock>=3.0",
]

[project.scripts]
mcp-retsinformation = "mcp_retsinformation.__main__:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
Empty file.
4 changes: 4 additions & 0 deletions src/mcp_retsinformation/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .main import main

if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions src/mcp_retsinformation/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
retsinformation_base_url: str = "https://retsinformation-api.dk"

model_config = {"env_file": ".env"}


settings = Settings()
9 changes: 9 additions & 0 deletions src/mcp_retsinformation/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .server import mcp
from .tools import search # noqa: F401 — side-effect import registers tools on mcp


def main() -> None:
try:
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
except (KeyboardInterrupt, SystemExit):
pass
Loading
Loading