Skip to content

Latest commit

 

History

History
452 lines (366 loc) · 11.5 KB

File metadata and controls

452 lines (366 loc) · 11.5 KB

Production Readiness Assessment

Verdict: PRODUCTION READY

This server is production ready for its intended use case: an MCP server for AI tooling that queries Nordic business registries.


What Makes It Production Ready

1. Resilience Patterns

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

2. Caching

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

3. Security (HTTP Mode)

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

4. Observability

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

5. Graceful Shutdown

  • Signal handling (SIGINT, SIGTERM)
  • 30-second shutdown timeout
  • Resource cleanup (caches, rate limiters)
  • Context cancellation propagation

6. Input Validation

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

7. Error Handling

  • Typed errors (NotFoundError, ValidationError)
  • Consistent error messages across all tools
  • Panic recovery in tool handlers
  • Circuit breaker errors include retry guidance

Known Limitations

1. No Built-in TLS

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.

2. Test Coverage

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).

3. External API Dependencies

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).

4. No Persistent Cache

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.


Deployment Checklist

Stdio Mode (Claude Desktop, Claude Code)

# No special configuration needed
./nordic-registry-mcp-server

Just add to your MCP client configuration.

HTTP Mode (Remote Access)

# 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"

Environment Variables

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

Reverse Proxy Example (Caddy)

mcp.example.com {
    reverse_proxy 127.0.0.1:8080
}

Docker (Linux Container)

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/health

Docker 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: 3

Kubernetes

Deployment 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: 80

Deploy:

# 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/health

Horizontal 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: 70

Monitoring

Prometheus Metrics

Key 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

Health Checks

# 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

Performance Characteristics

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

Upgrade Path

From 0.x to 1.x

No breaking changes. Tool names and parameters are stable.

Adding New Countries

  1. Create internal/{country}/ package
  2. Add tool definitions to tools/definitions.go
  3. Register handlers in tools/handlers.go
  4. Update this documentation