Skip to main content
WorldMonitor runs a minimal OAuth 2.1 authorization server whose only client-facing purpose today is granting access to the MCP server at /api/mcp. It implements:
  • RFC 7591 — Dynamic Client Registration
  • RFC 7636 — PKCE (required, S256 only)
  • RFC 8414 — Authorization Server Metadata
  • RFC 9728 — Protected Resource Metadata

Discovery

/.well-known/oauth-protected-resource currently advertises the public resource scope mcp. Pro authorization-code grants return the internal scope value mcp_pro; legacy API-key grants and client_credentials return mcp.

Endpoints

POST /api/oauth/register

Dynamic Client Registration. Returns a client_id (public clients, no secret). Request:
Response:
Redirect URI allowlist: only these prefixes are accepted:
  • https://claude.ai/api/mcp/auth_callback
  • https://claude.com/api/mcp/auth_callback
  • http://localhost:<port> / http://127.0.0.1:<port> — any port
At most 3 redirect_uris per registration; more returns 400 invalid_request. Rate limit: 5 registrations / 60 s / IP. Client TTL: 90 days sliding (every successful token exchange refreshes).

GET /api/oauth/authorize

Starts the OAuth flow. Renders a consent page that redirects to Clerk for sign-in, then issues an authorization code bound to the caller’s entitlement. The Pro sign-in leg of the consent flow is served by the sibling GET /oauth/authorize-pro (HTML; not called directly by clients). It admits Pro subscribers and confirmed free accounts; a provider-confirmed lapse is reclassified onto the free-account path, so authorization continues with a restricted, allowance-metered token. Retryable verification failures return 503, while genuinely insufficient states such as an expired or disabled paid row without a confirmed lapse render the Pro-required page. Required query params:
  • response_type=code
  • client_id — from DCR
  • redirect_uri — must match the one registered
  • code_challenge — PKCE S256
  • code_challenge_method=S256
  • state — opaque
  • scope (optional)
Code TTL: 10 minutes. Single-use (atomic GETDEL on exchange).

POST /api/oauth/token

Exchanges an authorization code for an access token, or refreshes an existing token. Grant type: authorization_code:
Response:
Grant type: refresh_token:
Grant type: client_credentials (operator-issued enterprise keys only):
Validates the client_secret against the deployment’s operator key allowlist and returns a bearer token with scope: "mcp" and the standard 3600 s TTL. Not available for dashboard wm_… keys — those are sent directly as X-WorldMonitor-Key instead. Rate limit: 10 token requests / minute. The limiter is keyed by client_secret hash for client_credentials, by client_id when present (authorization_code and refresh_token), and falls back to caller IP only when neither identifier is available. All three grant types fail open when the limiter is unconfigured or throws; the response then carries X-RateLimit-Mode: degraded (listed in Access-Control-Expose-Headers) so operators and cross-origin clients can tell that traffic apart from healthy limiter grants. A Redis storage outage still fails token persistence closed. Token TTLs:
  • Access token: 1 hour
  • Refresh token: 7 days
Access and refresh tokens are opaque UUIDs. All token-endpoint responses include Cache-Control: no-store, Pragma: no-cache.

Using tokens

Pass the access token on every MCP request:
Tokens are bound to the user’s account and re-check entitlement on every call. A provider-confirmed lapse removes paid capability but keeps the OAuth identity on the restricted, allowance-metered free-account path; an expired or disabled paid entitlement without that confirmed lapse, or another genuinely insufficient non-free state, is denied on the next request.

Error responses

Per RFC 6749 §5.2:
Common errors: invalid_request, invalid_client, invalid_grant, unsupported_grant_type, invalid_scope.