This server is production ready for its intended use case: an MCP server for AI tooling that queries Nordic business registries.
| Pattern | Implementation | Location |
|---|---|---|
| Circuit Breaker | Opens after 5 consecutive failures, half-open after 30s | internal/infra/resilience.go |
| Request Deduplication | Coalesces identical concurrent requests | internal/infra/resilience.go |
| Retry with Backoff | Exponential backoff + jitter, max 3 attempts | internal/base/client.go:170-181 |
| Rate Limiting | Semaphore (5 concurrent) + IP-based (60/min in HTTP mode) | internal/base/client.go, main.go |
| Request Timeout | 30s tool timeout, 30s HTTP timeout | tools/handlers.go:123, internal/base/client.go:19 |
| Feature | Value | Notes |
|---|---|---|
| Type | LRU with TTL | Prevents unbounded memory growth |
| Max entries | 1000 per client | 3 clients = ~3000 entries max |
| TTL (search) | 2 minutes | Fresher results for searches |
| TTL (details) | 5 minutes | Company details cache longer |
| TTL (reference) | 24 hours | Municipalities, org forms |
| Cleanup | Every 5 minutes | Background goroutine |
| Feature | Status | Notes |
|---|---|---|
| Bearer token auth | Supported | -token flag or MCP_AUTH_TOKEN env |
| Constant-time comparison | Yes | crypto/subtle.ConstantTimeCompare |
| Request body limit | 2 MB default, 10 MB max | Prevents memory exhaustion |
| Response body limit | 10 MB | MaxResponseSize in base client |
| CORS | Configurable | -origins flag |
| Rate limiting | 60 req/min/IP default | -rate-limit flag |
| Security headers | Yes | X-Content-Type-Options, X-Frame-Options, Cache-Control |
| Trusted proxies | Supported | -trusted-proxies flag for X-Forwarded-For |
| Feature | Implementation |
|---|---|
| Structured logging | log/slog with JSON output |
| Metrics | Prometheus at /metrics |
| Tracing | OpenTelemetry (optional, via env vars) |
| Health check | /health - always returns 200 |
| Readiness check | /ready - checks circuit breaker states |
| Status endpoint | /status - circuit breaker + dedup stats |
- Signal handling (SIGINT, SIGTERM)
- 30-second shutdown timeout
- Resource cleanup (caches, rate limiters)
- Context cancellation propagation
| Country | Validation |
|---|---|
| Norway | 9 digits, auto-strips spaces/dashes |
| Denmark | 8 digits, auto-strips spaces/dashes/DK prefix |
| Finland | 7+1 format (1234567-8), auto-strips FI prefix |
- Typed errors (
NotFoundError,ValidationError) - Consistent error messages across all tools
- Panic recovery in tool handlers
- Circuit breaker errors include retry guidance
The server does NOT terminate TLS. In HTTP mode, you MUST:
- Run behind a reverse proxy (nginx, Caddy, Traefik) for HTTPS
- Or bind to localhost only (
-http 127.0.0.1:8080)
The server logs a warning if binding to an external interface without this setup.
| Package | Coverage |
|---|---|
internal/errors |
100% |
metrics |
100% |
tracing |
90.6% |
internal/infra |
89.6% |
internal/sweden |
83.5% |
internal/finland |
61.4% |
internal/denmark |
50.9% |
tools |
44.0% |
internal/norway |
40.8% |
internal/base |
24.1% |
main |
23.4% |
Infrastructure, error handling, and Sweden client are well-tested. Country clients have moderate coverage. Tool handlers have lower coverage (they're thin wrappers over validated client calls).
This server depends on four external APIs:
| Country | API | SLA | Rate Limits |
|---|---|---|---|
| Norway | data.brreg.no | Public, no SLA | Unspecified |
| Denmark | cvrapi.dk | Public, no SLA | Unspecified |
| Finland | avoindata.prh.fi | Public, no SLA | Unspecified |
| Sweden | api.bolagsverket.se | OAuth2, no SLA | Unspecified |
If any upstream API is down, that country's tools will fail (circuit breaker will open).
Cache is in-memory only. On restart:
- All cached data is lost
- Cold start = slower initial requests
- Circuit breakers reset to closed state
For most MCP use cases, this is acceptable.
# No special configuration needed
./nordic-registry-mcp-serverJust add to your MCP client configuration.
# Minimum secure setup
./nordic-registry-mcp-server \
-http :8080 \
-token "$(openssl rand -hex 32)"
# Production setup behind reverse proxy
./nordic-registry-mcp-server \
-http 127.0.0.1:8080 \
-token "${MCP_AUTH_TOKEN}" \
-origins "https://your-app.com" \
-rate-limit 100 \
-trusted-proxies "10.0.0.0/8,172.16.0.0/12"| Variable | Purpose |
|---|---|
MCP_AUTH_TOKEN |
Bearer token (alternative to -token flag) |
OTEL_EXPORTER_OTLP_ENDPOINT |
OpenTelemetry collector endpoint |
OTEL_SERVICE_NAME |
Override service name for tracing |
mcp.example.com {
reverse_proxy 127.0.0.1:8080
}The server compiles to a static Linux binary with no external dependencies.
Dockerfile:
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY nordic-registry-mcp-server /usr/local/bin/
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/nordic-registry-mcp-server"]
CMD ["-http", ":8080"]Run with Docker:
# Build (if building locally)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o nordic-registry-mcp-server .
docker build -t nordic-registry-mcp-server .
# Run
docker run -d \
--name nordic-registry \
-p 8080:8080 \
-e MCP_AUTH_TOKEN="$(openssl rand -hex 32)" \
-e BOLAGSVERKET_CLIENT_ID="${BOLAGSVERKET_CLIENT_ID}" \
-e BOLAGSVERKET_CLIENT_SECRET="${BOLAGSVERKET_CLIENT_SECRET}" \
--restart unless-stopped \
nordic-registry-mcp-server
# Verify
curl http://localhost:8080/healthDocker Compose:
version: '3.8'
services:
nordic-registry:
image: ghcr.io/olgasafonova/nordic-registry-mcp-server:latest
container_name: nordic-registry
ports:
- "8080:8080"
environment:
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
- BOLAGSVERKET_CLIENT_ID=${BOLAGSVERKET_CLIENT_ID:-}
- BOLAGSVERKET_CLIENT_SECRET=${BOLAGSVERKET_CLIENT_SECRET:-}
command: ["-http", ":8080", "-rate-limit", "100"]
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3Deployment with ConfigMap and Secret:
---
apiVersion: v1
kind: Secret
metadata:
name: nordic-registry-secrets
type: Opaque
stringData:
MCP_AUTH_TOKEN: "your-secret-token-here"
# Optional: Sweden API credentials
# BOLAGSVERKET_CLIENT_ID: "your-client-id"
# BOLAGSVERKET_CLIENT_SECRET: "your-client-secret"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nordic-registry
labels:
app: nordic-registry
spec:
replicas: 2
selector:
matchLabels:
app: nordic-registry
template:
metadata:
labels:
app: nordic-registry
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: nordic-registry
image: ghcr.io/olgasafonova/nordic-registry-mcp-server:latest
args:
- "-http"
- ":8080"
- "-rate-limit"
- "100"
- "-trusted-proxies"
- "10.0.0.0/8,172.16.0.0/12"
ports:
- containerPort: 8080
name: http
envFrom:
- secretRef:
name: nordic-registry-secrets
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
---
apiVersion: v1
kind: Service
metadata:
name: nordic-registry
spec:
selector:
app: nordic-registry
ports:
- port: 80
targetPort: 8080
name: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nordic-registry
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- nordic-registry.example.com
secretName: nordic-registry-tls
rules:
- host: nordic-registry.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nordic-registry
port:
number: 80Deploy:
# Create namespace (optional)
kubectl create namespace mcp-services
# Apply manifests
kubectl apply -f nordic-registry-k8s.yaml -n mcp-services
# Verify
kubectl get pods -n mcp-services -l app=nordic-registry
kubectl logs -n mcp-services -l app=nordic-registry
# Test endpoint
kubectl port-forward -n mcp-services svc/nordic-registry 8080:80
curl http://localhost:8080/healthHorizontal Pod Autoscaler (optional):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nordic-registry
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nordic-registry
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70Key metrics to alert on:
# High error rate (>10% errors in 5 min)
sum(rate(nordic_registry_mcp_requests_total{success="false"}[5m]))
/ sum(rate(nordic_registry_mcp_requests_total[5m])) > 0.1
# Circuit breaker open
nordic_registry_mcp_circuit_breaker_state{state="open"} > 0
# High latency (p95 > 5s)
histogram_quantile(0.95, rate(nordic_registry_mcp_request_duration_seconds_bucket[5m])) > 5
# Panic recovery (any panic is concerning)
increase(nordic_registry_mcp_panics_recovered_total[1h]) > 0
# Liveness (always 200 if process is running)
curl http://localhost:8080/health
# Readiness (503 if any circuit breaker is open)
curl http://localhost:8080/ready
# Detailed status
curl http://localhost:8080/status| Metric | Typical Value | Notes |
|---|---|---|
| Memory | 20-50 MB | Depends on cache utilization |
| Cold start | <100ms | Binary startup, no JVM warmup |
| Cached response | <1ms | LRU cache hit |
| API latency | 200-500ms | Depends on upstream API |
| Max concurrent | 5 per country | Semaphore-limited |
No breaking changes. Tool names and parameters are stable.
- Create
internal/{country}/package - Add tool definitions to
tools/definitions.go - Register handlers in
tools/handlers.go - Update this documentation