Skip to content

Health & Discovery

Seed provides public discovery endpoints for OAuth metadata and health monitoring. These endpoints require no authentication and follow RFC standards.

Overview

EndpointMethodAuthenticationPurpose
/healthGETPublicHealth check
/metricsGETPublicPrometheus metrics
/.well-known/oauth-authorization-serverGETPublicOAuth server metadata (RFC 8414)
/.well-known/oauth-protected-resourceGETPublicProtected resource metadata (RFC 9728)

GET /health

Health Check (Liveness Probe) - Verify server is running and accepting connections.

Request

http
GET /health

No headers or parameters required.

Response (Healthy)

Status: 200 OK

json
{
  "status": "ok",
  "version": "0.1.3"
}

Response (Shutting Down)

Status: 503 Service Unavailable

json
{
  "status": "shutting_down",
  "version": "0.1.3"
}

Response Fields

FieldTypeDescription
statusstringHealth status: ok or shutting_down
versionstringServer version from package.json

Implementation Note: This endpoint serves as a liveness probe for Kubernetes/Docker health checks. It returns 503 during graceful shutdown to prevent new traffic from being routed to the server.

GET /health/ready

Readiness Probe - Verify server and dependencies are operational and ready to serve traffic.

Request

http
GET /health/ready

No headers or parameters required.

Response (Ready)

Status: 200 OK

json
{
  "status": "ready",
  "version": "0.1.3",
  "checks": {
    "redis": {
      "status": "healthy",
      "connected": true,
      "circuitState": "closed"
    },
    "jwks": {
      "status": "healthy",
      "cached": true,
      "expiresIn": 3245
    },
    "sessions": {
      "active": 15,
      "maxCapacity": 10000,
      "utilizationPercent": 0.15
    }
  }
}

Response (Degraded)

Status: 503 Service Unavailable

json
{
  "status": "degraded",
  "version": "0.1.3",
  "checks": {
    "redis": {
      "status": "unhealthy",
      "connected": false,
      "circuitState": "open",
      "error": "Connection refused"
    },
    "jwks": {
      "status": "healthy",
      "cached": true,
      "expiresIn": 3245
    },
    "sessions": {
      "active": 15,
      "maxCapacity": 10000,
      "utilizationPercent": 0.15
    }
  }
}

Response Fields

FieldTypeDescription
statusstringReadiness status: ready or degraded
versionstringServer version from package.json
checksobjectDetailed health checks for dependencies
checks.redisobjectRedis connection health
checks.redis.statusstringhealthy or unhealthy
checks.redis.connectedbooleanWhether Redis is connected
checks.redis.circuitStatestringCircuit breaker state: closed, open, or half-open
checks.jwksobjectJWKS cache health
checks.jwks.statusstringhealthy or unhealthy
checks.jwks.cachedbooleanWhether JWKS is cached
checks.jwks.expiresInnumberSeconds until cache expiration
checks.sessionsobjectSession capacity metrics
checks.sessions.activenumberNumber of active MCP sessions
checks.sessions.maxCapacitynumberMaximum session capacity
checks.sessions.utilizationPercentnumberCapacity utilization percentage

Implementation Note: This endpoint serves as a readiness probe for Kubernetes/Docker. It checks that all critical dependencies (Redis, JWKS) are operational before accepting traffic.

Example: cURL

Liveness probe:

bash
curl https://seed.example.com/health

Readiness probe:

bash
curl https://seed.example.com/health/ready

Example: JavaScript

Liveness probe:

javascript
const response = await fetch('https://seed.example.com/health');
const health = await response.json();

if (health.status !== 'ok') {
  console.warn('Server is shutting down');
}

Readiness probe:

javascript
const response = await fetch('https://seed.example.com/health/ready');
const health = await response.json();

if (health.status !== 'ready') {
  console.warn('Server not ready:', health.checks);
}

Kubernetes Probes

Deployment YAML:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: seed-mcp-server
spec:
  template:
    spec:
      containers:
      - name: seed
        image: seed-mcp-server:latest
        ports:
        - containerPort: 3000

        # Liveness probe - check if server is alive
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 10
          failureThreshold: 3

        # Readiness probe - check if server can serve traffic
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
          failureThreshold: 3

Monitoring

Liveness probe (/health):

  • Purpose: Verify server process is running
  • Expected status: 200 OK with status: "ok"
  • Alert on: Status code != 200 or status is "shutting_down" for > 30 seconds

Readiness probe (/health/ready):

  • Purpose: Verify server can handle requests
  • Expected status: 200 OK with status: "ready"
  • Alert conditions:
    • Status code == 503 (degraded)
    • Redis unhealthy for > 5 minutes
    • JWKS cache expired
    • Session utilization > 90%

GET /metrics

Prometheus Metrics - Expose server metrics in Prometheus format for monitoring and alerting.

Configuration

Enable/Disable:

bash
METRICS_ENABLED=true  # Default: true

Access Control:

  • Endpoint is public when enabled (no authentication required)
  • Recommended: Use network-level access control (firewall rules, IP allowlists)
  • Only enable in environments with protected network access

Request

http
GET /metrics

No headers or parameters required.

Response

Status: 200 OK Content-Type: text/plain; version=0.0.4

prometheus
# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter
process_cpu_user_seconds_total 12.34

# HELP http_request_duration_seconds HTTP request latency histogram
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="POST",route="/mcp",status_code="200",le="0.01"} 45
http_request_duration_seconds_bucket{method="POST",route="/mcp",status_code="200",le="0.05"} 89
http_request_duration_seconds_bucket{method="POST",route="/mcp",status_code="200",le="0.1"} 95
http_request_duration_seconds_sum{method="POST",route="/mcp",status_code="200"} 4.23
http_request_duration_seconds_count{method="POST",route="/mcp",status_code="200"} 100

# HELP http_request_total Total HTTP requests
# TYPE http_request_total counter
http_request_total{method="POST",route="/mcp",status_code="200"} 100
http_request_total{method="POST",route="/mcp",status_code="401"} 5

Metrics Categories

Process Metrics (automatic):

  • process_cpu_user_seconds_total - CPU usage
  • process_resident_memory_bytes - Memory usage
  • nodejs_eventloop_lag_seconds - Event loop lag
  • nodejs_gc_duration_seconds - Garbage collection

HTTP Metrics:

  • http_request_duration_seconds - Request latency histogram
  • http_request_total - Request counter
  • Labels: method, route, status_code

Rate Limiting Metrics:

  • http_request_rate_limit_requests_total - Rate limit evaluations
  • Labels: endpoint, limited (true/false)

MCP Metrics:

  • mcp_sessions_total - Active MCP sessions
  • mcp_tool_calls_total - Tool invocations
  • Labels: tool_name, status

OAuth Metrics:

  • oauth_authorization_requests_total - OAuth authorization requests
  • oauth_token_exchanges_total - Token exchanges (authorization_code, refresh_token)
  • oauth_token_exchange_duration_seconds - Token exchange latency histogram
  • dcr_registrations_total - Dynamic client registrations
  • Labels: result, grant_type

Token Refresh Metrics:

  • token_refresh_attempts_total - Token refresh attempts
  • token_refresh_duration_seconds - Token refresh operation latency
  • pending_tokens_claimed_total - Pending tokens claimed by sessions
  • Labels: type (proactive/reactive), result (success/failure/skipped)

Example: cURL

bash
curl https://seed.example.com/metrics

Example: Prometheus Configuration

yaml
# prometheus.yml
scrape_configs:
  - job_name: 'seed-mcp-server'
    scrape_interval: 15s
    static_configs:
      - targets: ['seed.example.com:3000']
    metrics_path: '/metrics'

Example Queries

Request rate:

promql
rate(http_request_total[5m])

95th percentile latency:

promql
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

Error rate:

promql
rate(http_request_total{status_code=~"5.."}[5m])

Rate limit rejections:

promql
rate(http_request_rate_limit_requests_total{limited="true"}[5m])

OAuth authorization success rate:

promql
rate(oauth_authorization_requests_total{result="success"}[5m])
/ rate(oauth_authorization_requests_total[5m])

Token exchange P99 latency:

promql
histogram_quantile(0.99,
  rate(oauth_token_exchange_duration_seconds_bucket[5m])
)

Token refresh success rate:

promql
rate(token_refresh_attempts_total{result="success"}[5m])
/ rate(token_refresh_attempts_total[5m])

For detailed metrics documentation, see Observability Architecture.

GET /.well-known/oauth-authorization-server

OAuth 2.0 Authorization Server Metadata - RFC 8414 compliant discovery endpoint.

Request

http
GET /.well-known/oauth-authorization-server

No headers or parameters required.

Response

Status: 200 OK

json
{
  "issuer": "https://auth.example.com/application/o/my-app/",
  "authorization_endpoint": "https://seed.example.com/oauth/authorize",
  "token_endpoint": "https://seed.example.com/oauth/token",
  "registration_endpoint": "https://seed.example.com/oauth/register",
  "jwks_uri": "https://auth.example.com/application/o/my-app/jwks/",
  "scopes_supported": [
    "openid",
    "profile",
    "email"
  ],
  "response_types_supported": [
    "code"
  ],
  "response_modes_supported": [
    "query"
  ],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ],
  "token_endpoint_auth_methods_supported": [
    "none"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ]
}

Response Fields

FieldTypeDescription
issuerstringOIDC issuer URL (from OIDC_ISSUER)
authorization_endpointstringAuthorization endpoint URL
token_endpointstringToken endpoint URL
registration_endpointstringDynamic client registration URL
jwks_uristringJWKS endpoint URL (from upstream IdP)
scopes_supportedarraySupported OAuth scopes
response_types_supportedarraySupported response types
grant_types_supportedarraySupported grant types
token_endpoint_auth_methods_supportedarraySupported auth methods
code_challenge_methods_supportedarraySupported PKCE methods

Caching

Cache-Control: public, max-age=300 (5 minutes)

The response is cacheable for 5 minutes to reduce load on the server.

Example: cURL

bash
curl https://seed.example.com/.well-known/oauth-authorization-server

Example: JavaScript

javascript
const response = await fetch('https://seed.example.com/.well-known/oauth-authorization-server');
const metadata = await response.json();

console.log('Authorization endpoint:', metadata.authorization_endpoint);
console.log('Token endpoint:', metadata.token_endpoint);
console.log('Supported scopes:', metadata.scopes_supported);

Use Cases

OAuth client configuration:

javascript
// Discover OAuth endpoints dynamically
const discovery = await fetch('https://seed.example.com/.well-known/oauth-authorization-server')
  .then(r => r.json());

const oauth = {
  authorizationEndpoint: discovery.authorization_endpoint,
  tokenEndpoint: discovery.token_endpoint,
  registrationEndpoint: discovery.registration_endpoint,
  supportedScopes: discovery.scopes_supported,
};

GET /.well-known/oauth-protected-resource

OAuth 2.0 Protected Resource Metadata - RFC 9728 compliant resource server metadata.

Request

http
GET /.well-known/oauth-protected-resource

No headers or parameters required.

Response

Status: 200 OK

json
{
  "resource": "https://seed.example.com",
  "authorization_servers": [
    "https://seed.example.com"
  ],
  "scopes_supported": [
    "openid",
    "profile",
    "email"
  ],
  "bearer_methods_supported": [
    "header"
  ],
  "resource_documentation": "https://gitlab.byterecursion.com/mcp-servers/seed"
}

Response Fields

FieldTypeDescription
resourcestringProtected resource URL (server base URL)
authorization_serversarrayAuthorization servers that can issue tokens
scopes_supportedarrayScopes accepted by this resource
bearer_methods_supportedarrayToken transmission methods: header, body, query
resource_documentationstringURL to API documentation

Example: cURL

bash
curl https://seed.example.com/.well-known/oauth-protected-resource

Example: JavaScript

javascript
const response = await fetch('https://seed.example.com/.well-known/oauth-protected-resource');
const metadata = await response.json();

console.log('Resource server:', metadata.resource);
console.log('Authorization servers:', metadata.authorization_servers);
console.log('Required scopes:', metadata.scopes_supported);

Use Cases

Discover authorization server:

javascript
// When accessing a protected resource, discover where to authenticate
const resourceMetadata = await fetch('https://seed.example.com/.well-known/oauth-protected-resource')
  .then(r => r.json());

const authServer = resourceMetadata.authorization_servers[0];

// Fetch authorization server metadata
const authMetadata = await fetch(`${authServer}/.well-known/oauth-authorization-server`)
  .then(r => r.json());

// Now you know where to start OAuth flow
window.location.href = authMetadata.authorization_endpoint + '?...';

Implement WWW-Authenticate handling:

javascript
// When receiving 401, check WWW-Authenticate header
const response = await fetch('https://seed.example.com/mcp', {
  headers: { 'Authorization': `Bearer ${token}` }
});

if (response.status === 401) {
  const wwwAuth = response.headers.get('WWW-Authenticate');
  // Example: Bearer resource_metadata="https://seed.example.com/.well-known/oauth-protected-resource"

  // Extract metadata URL and fetch it
  const metadataUrl = wwwAuth.match(/resource_metadata="([^"]+)"/)?.[1];
  const metadata = await fetch(metadataUrl).then(r => r.json());

  // Redirect to authorization server
  const authServer = metadata.authorization_servers[0];
  // Start OAuth flow...
}

Discovery Flow

Complete OAuth Discovery

Integration Example

javascript
class SeedClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.metadata = null;
  }

  async discover() {
    // Discover protected resource metadata
    const resourceMeta = await fetch(`${this.baseUrl}/.well-known/oauth-protected-resource`)
      .then(r => r.json());

    // Discover authorization server metadata
    const authServer = resourceMeta.authorization_servers[0];
    const authMeta = await fetch(`${authServer}/.well-known/oauth-authorization-server`)
      .then(r => r.json());

    this.metadata = { resource: resourceMeta, auth: authMeta };
    return this.metadata;
  }

  async register(redirectUris, clientName) {
    if (!this.metadata) await this.discover();

    const response = await fetch(this.metadata.auth.registration_endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ redirect_uris: redirectUris, client_name: clientName }),
    });

    return response.json();
  }

  getAuthorizationUrl(clientId, redirectUri, state, codeChallenge) {
    const url = new URL(this.metadata.auth.authorization_endpoint);
    url.searchParams.set('response_type', 'code');
    url.searchParams.set('client_id', clientId);
    url.searchParams.set('redirect_uri', redirectUri);
    url.searchParams.set('scope', 'openid profile email');
    url.searchParams.set('state', state);
    url.searchParams.set('code_challenge', codeChallenge);
    url.searchParams.set('code_challenge_method', 'S256');
    return url.toString();
  }

  async exchangeCode(code, clientId, redirectUri, codeVerifier) {
    const response = await fetch(this.metadata.auth.token_endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        client_id: clientId,
        redirect_uri: redirectUri,
        code_verifier: codeVerifier,
      }),
    });

    return response.json();
  }
}

// Usage
const client = new SeedClient('https://seed.example.com');
await client.discover();
const registration = await client.register(['https://myapp.com/callback'], 'My App');
const authUrl = client.getAuthorizationUrl(registration.client_id, 'https://myapp.com/callback', 'state', 'challenge');

Error Responses

404 Not Found

If the discovery endpoint is not found (misconfigured server):

http
HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": "not_found",
  "error_description": "Resource not found"
}

500 Internal Server Error

If the server cannot generate metadata:

http
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "error": "server_error",
  "error_description": "Unable to generate metadata"
}

Best Practices

Client Implementation

  1. Cache discovery responses: Store metadata for 5 minutes
  2. Handle errors gracefully: Fall back to manual configuration if discovery fails
  3. Validate metadata: Check required fields are present
  4. Use HTTPS: Never use HTTP in production (except localhost)
  5. Follow redirects: Discovery endpoints may redirect

Server Monitoring

  1. Monitor /health: Check every 30-60 seconds
  2. Alert on degraded status: React to Redis disconnections
  3. Track response times: Alert if > 1 second
  4. Log discovery requests: Monitor client integration patterns
  5. Cache metadata: Serve from cache when possible

Testing

Test Health Endpoint

bash
# Basic health check
curl https://seed.example.com/health

# With timing
curl -w "Time: %{time_total}s\n" https://seed.example.com/health

# Check status
curl -s https://seed.example.com/health | jq '.status'

Test Discovery Endpoints

bash
# OAuth authorization server metadata
curl https://seed.example.com/.well-known/oauth-authorization-server | jq

# Protected resource metadata
curl https://seed.example.com/.well-known/oauth-protected-resource | jq

# Verify endpoints are accessible
METADATA=$(curl -s https://seed.example.com/.well-known/oauth-authorization-server)
AUTH_ENDPOINT=$(echo $METADATA | jq -r '.authorization_endpoint')
echo "Authorization endpoint: $AUTH_ENDPOINT"

Automated Testing

bash
#!/bin/bash
# health-check.sh

BASE_URL="https://seed.example.com"

# Check health
HEALTH=$(curl -s "$BASE_URL/health")
STATUS=$(echo $HEALTH | jq -r '.status')

if [ "$STATUS" != "healthy" ]; then
  echo "ERROR: Server health is $STATUS"
  exit 1
fi

# Check discovery
if ! curl -sf "$BASE_URL/.well-known/oauth-authorization-server" > /dev/null; then
  echo "ERROR: OAuth discovery endpoint not accessible"
  exit 1
fi

echo "All checks passed"
exit 0

Released under the MIT License.