Health & Discovery
Seed provides public discovery endpoints for OAuth metadata and health monitoring. These endpoints require no authentication and follow RFC standards.
Overview
| Endpoint | Method | Authentication | Purpose |
|---|---|---|---|
/health | GET | Public | Health check |
/metrics | GET | Public | Prometheus metrics |
/.well-known/oauth-authorization-server | GET | Public | OAuth server metadata (RFC 8414) |
/.well-known/oauth-protected-resource | GET | Public | Protected resource metadata (RFC 9728) |
GET /health
Health Check (Liveness Probe) - Verify server is running and accepting connections.
Request
GET /healthNo headers or parameters required.
Response (Healthy)
Status: 200 OK
{
"status": "ok",
"version": "0.1.3"
}Response (Shutting Down)
Status: 503 Service Unavailable
{
"status": "shutting_down",
"version": "0.1.3"
}Response Fields
| Field | Type | Description |
|---|---|---|
status | string | Health status: ok or shutting_down |
version | string | Server 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
GET /health/readyNo headers or parameters required.
Response (Ready)
Status: 200 OK
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
status | string | Readiness status: ready or degraded |
version | string | Server version from package.json |
checks | object | Detailed health checks for dependencies |
checks.redis | object | Redis connection health |
checks.redis.status | string | healthy or unhealthy |
checks.redis.connected | boolean | Whether Redis is connected |
checks.redis.circuitState | string | Circuit breaker state: closed, open, or half-open |
checks.jwks | object | JWKS cache health |
checks.jwks.status | string | healthy or unhealthy |
checks.jwks.cached | boolean | Whether JWKS is cached |
checks.jwks.expiresIn | number | Seconds until cache expiration |
checks.sessions | object | Session capacity metrics |
checks.sessions.active | number | Number of active MCP sessions |
checks.sessions.maxCapacity | number | Maximum session capacity |
checks.sessions.utilizationPercent | number | Capacity 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:
curl https://seed.example.com/healthReadiness probe:
curl https://seed.example.com/health/readyExample: JavaScript
Liveness probe:
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:
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:
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: 3Monitoring
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:
METRICS_ENABLED=true # Default: trueAccess 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
GET /metricsNo headers or parameters required.
Response
Status: 200 OK Content-Type: text/plain; version=0.0.4
# 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"} 5Metrics Categories
Process Metrics (automatic):
process_cpu_user_seconds_total- CPU usageprocess_resident_memory_bytes- Memory usagenodejs_eventloop_lag_seconds- Event loop lagnodejs_gc_duration_seconds- Garbage collection
HTTP Metrics:
http_request_duration_seconds- Request latency histogramhttp_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 sessionsmcp_tool_calls_total- Tool invocations- Labels:
tool_name,status
OAuth Metrics:
oauth_authorization_requests_total- OAuth authorization requestsoauth_token_exchanges_total- Token exchanges (authorization_code, refresh_token)oauth_token_exchange_duration_seconds- Token exchange latency histogramdcr_registrations_total- Dynamic client registrations- Labels:
result,grant_type
Token Refresh Metrics:
token_refresh_attempts_total- Token refresh attemptstoken_refresh_duration_seconds- Token refresh operation latencypending_tokens_claimed_total- Pending tokens claimed by sessions- Labels:
type(proactive/reactive),result(success/failure/skipped)
Example: cURL
curl https://seed.example.com/metricsExample: Prometheus Configuration
# 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:
rate(http_request_total[5m])95th percentile latency:
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))Error rate:
rate(http_request_total{status_code=~"5.."}[5m])Rate limit rejections:
rate(http_request_rate_limit_requests_total{limited="true"}[5m])OAuth authorization success rate:
rate(oauth_authorization_requests_total{result="success"}[5m])
/ rate(oauth_authorization_requests_total[5m])Token exchange P99 latency:
histogram_quantile(0.99,
rate(oauth_token_exchange_duration_seconds_bucket[5m])
)Token refresh success rate:
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
GET /.well-known/oauth-authorization-serverNo headers or parameters required.
Response
Status: 200 OK
{
"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
| Field | Type | Description |
|---|---|---|
issuer | string | OIDC issuer URL (from OIDC_ISSUER) |
authorization_endpoint | string | Authorization endpoint URL |
token_endpoint | string | Token endpoint URL |
registration_endpoint | string | Dynamic client registration URL |
jwks_uri | string | JWKS endpoint URL (from upstream IdP) |
scopes_supported | array | Supported OAuth scopes |
response_types_supported | array | Supported response types |
grant_types_supported | array | Supported grant types |
token_endpoint_auth_methods_supported | array | Supported auth methods |
code_challenge_methods_supported | array | Supported 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
curl https://seed.example.com/.well-known/oauth-authorization-serverExample: 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:
// 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
GET /.well-known/oauth-protected-resourceNo headers or parameters required.
Response
Status: 200 OK
{
"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
| Field | Type | Description |
|---|---|---|
resource | string | Protected resource URL (server base URL) |
authorization_servers | array | Authorization servers that can issue tokens |
scopes_supported | array | Scopes accepted by this resource |
bearer_methods_supported | array | Token transmission methods: header, body, query |
resource_documentation | string | URL to API documentation |
Example: cURL
curl https://seed.example.com/.well-known/oauth-protected-resourceExample: 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:
// 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:
// 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
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/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/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "server_error",
"error_description": "Unable to generate metadata"
}Best Practices
Client Implementation
- Cache discovery responses: Store metadata for 5 minutes
- Handle errors gracefully: Fall back to manual configuration if discovery fails
- Validate metadata: Check required fields are present
- Use HTTPS: Never use HTTP in production (except localhost)
- Follow redirects: Discovery endpoints may redirect
Server Monitoring
- Monitor /health: Check every 30-60 seconds
- Alert on degraded status: React to Redis disconnections
- Track response times: Alert if > 1 second
- Log discovery requests: Monitor client integration patterns
- Cache metadata: Serve from cache when possible
Testing
Test Health Endpoint
# 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
# 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
#!/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 0Related Documentation
- Authentication Endpoints - Using discovered endpoints
- OAuth Endpoints - Endpoints referenced in metadata
- OAuth 2.1 Implementation - Implementation details
- Configuration System - Configuring metadata values