Protected Resource Metadata
Model Context Protocol (MCP) is the protocol AI applications (Claude, ChatGPT, Cursor and similar hosts) use to discover and call external tools. When an MCP client hits an MCP server without a token, it needs a way to find the authorization server. Protected resource metadata is a JSON discovery document that names the authorization server(s) and supported scopes for a given resource. LumoAuth serves one such document per registered MCP server.
Discovery mechanisms
Per the MCP spec, MCP servers MUST implement at least one of the following discovery mechanisms. LumoAuth supports both.
1. Well-known URI (recommended)
LumoAuth serves Protected Resource Metadata at well-known URIs for each registered MCP server:
GET /orgs/{orgId}/api/v1/.well-known/oauth-protected-resource/mcp/{serverId} — path-specific metadata for a specific MCP server.
GET /orgs/{orgId}/api/v1/.well-known/oauth-protected-resource — root-level metadata endpoint. For an organization with one protected server it returns that server's document; with several it returns {"resources": [...]}, one entry per server with resource, resource_metadata_url and authorization_servers. It answers 404 when the organization has no protected (OAuth-mode) MCP server.
GET /.well-known/oauth-protected-resource/orgs/{orgId}/api/v1/mcp/{serverId} — the standardized well-known suffix form (SEP-2351, the path-insertion rule of the protected resource metadata standard). Spec-compliant clients that derive the metadata URL from a LumoAuth-hosted resource URI insert the well-known segment at the path root; both forms return the same document.
The suffix form only applies to resource URIs hosted on the LumoAuth domain. If your MCP server's Resource URI lives on your own host (e.g. https://mcp.example.com), the well-known protected resource metadata document for that URI must be served from your host — typically by redirecting or proxying to the LumoAuth metadata URL, or by pointing clients at it via the WWW-Authenticate resource_metadata parameter.
2. WWW-Authenticate header
When your MCP server returns a 401 Unauthorized response, it should include a WWW-Authenticate header with the resource_metadata URL:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://app.lumoauth.dev/orgs/acme-corp/api/v1/.well-known/oauth-protected-resource/mcp/mcp_abc123",
scope="mcp:read mcp:write"
MCP clients MUST parse this header and use the resource_metadata URL to discover the authorization server. If the scope parameter is present, clients SHOULD use those scopes in the initial authorization request.
Metadata document structure
The Protected Resource Metadata document follows the standard protected-resource-metadata schema:
{
"resource": "https://mcp.example.com",
"authorization_servers": [
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/.well-known/oauth-authorization-server"
],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read", "mcp:write", "mcp:admin"]
}
Fields
| Field | Required | Description |
|---|---|---|
resource | Yes | The canonical URI of the MCP server (its Resource URI, the value clients send as the resource indicator) |
authorization_servers | Yes | Array containing the URL(s) of the authorization server(s) that can issue tokens for this resource. Points to LumoAuth's OAuth Authorization Server Metadata endpoint. |
bearer_methods_supported | No | Methods supported for Bearer token transmission. Per MCP spec, only header (Authorization request header) is MUST-supported. |
scopes_supported | No | OAuth scopes supported by this resource. Represents the minimal set for basic functionality. Present only when the server declares scopes. |
dpop_bound_access_tokens_required, dpop_signing_alg_values_supported | No | Present only when the server was registered with Require DPoP; see Sender-constrained (DPoP) tokens. |
Client discovery flow
MCP clients MUST support both discovery mechanisms and follow this priority order:
- Check WWW-Authenticate header: if the 401 response includes
resource_metadata, use that URL directly. - Try path-specific well-known URI:
/.well-known/oauth-protected-resource/{path}. - Try root well-known URI:
/.well-known/oauth-protected-resource.
- MCP Client to MCP Server: MCP request (no token)
- MCP Server to MCP Client: 401 Unauthorized + WWW-Authenticate header
- MCP Client to LumoAuth: GET resource_metadata URL
- LumoAuth to MCP Client: Protected Resource Metadata JSON
- MCP Client to LumoAuth: GET /.well-known/oauth-protected-resource/mcp/{serverId}
- LumoAuth to MCP Client: Protected Resource Metadata JSON
- Note: Extract authorization_servers URL
- MCP Client to LumoAuth: GET Authorization Server Metadata
- LumoAuth to MCP Client: OAuth 2.0 AS Metadata (endpoints, capabilities)
- Note: Proceed with OAuth 2.1 authorization flow
Testing discovery
You can test the discovery flow using the challenge endpoint:
# 1. Send unauthenticated request to trigger 401 challenge
curl -v https://app.lumoauth.dev/orgs/acme-corp/api/v1/mcp/mcp_abc123/challenge
# Look for: WWW-Authenticate: Bearer resource_metadata="...", scope="..."
# 1b. A bearer that is NOT a valid access token for this server gets the same
# challenge with error="invalid_token"; only a token issued by this
# organization whose `aud` (the resource indicator) names this MCP server
# returns 200 {"status": "authenticated", ...}.
curl -v -H 'Authorization: Bearer not-a-real-token' \
https://app.lumoauth.dev/orgs/acme-corp/api/v1/mcp/mcp_abc123/challenge
# 2. Fetch the Protected Resource Metadata
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/.well-known/oauth-protected-resource/mcp/mcp_abc123
# 3. Fetch the Authorization Server Metadata
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/.well-known/oauth-authorization-server
Scope handling
Scope selection strategy
Per the MCP spec, clients SHOULD follow this priority order for scope selection:
- Use
scopefrom WWW-Authenticate header if provided in the 401 response. - Use
scopes_supportedfrom Protected Resource Metadata if the header scope is absent. - Omit scope parameter if
scopes_supportedis also undefined.
The scopes_supported field should represent the minimal set of scopes necessary for basic MCP server functionality. Additional scopes can be requested incrementally through step-up authorization when the server returns a 403 Forbidden with error="insufficient_scope".
Step-up authorization (insufficient scope)
When a client's token lacks required scopes for a specific operation, the MCP server SHOULD respond with:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="mcp:admin",
resource_metadata="https://app.lumoauth.dev/orgs/acme-corp/api/v1/.well-known/oauth-protected-resource/mcp/mcp_abc123",
error_description="Additional scope required"
MCP clients SHOULD respond by initiating a step-up authorization flow with the expanded scope set.
LumoAuth's challenge endpoint (/orgs/{orgId}/api/v1/mcp/{serverId}/challenge) implements the standard bearer-token challenge: a token that is valid for the server but lacks scopes gets 403 with error="insufficient_scope" and scope listing only the missing scopes (SEP-2350 incremental consent). The required set is the request's scope parameter when given (the operation being attempted, e.g. ?scope=mcp:read mcp:admin), otherwise the server's scopes_supported — the minimal set for basic functionality. Servers without scopes_supported never step up. Each step-up challenge is audited as mcp.challenge_issued with reason: insufficient_scope.
Sender-constrained (DPoP) tokens
A DPoP-bound access token (cnf.jkt; DPoP ties a token to the client's key so a stolen copy cannot be replayed) is only accepted by the challenge endpoint together with a DPoP proof header for the request (htm/htu, ath of the token) signed by the bound key; it may be presented with either the DPoP or Bearer scheme. A bound token without a valid, matching proof is treated as invalid_token — it is never downgraded to a plain bearer. This mirrors what LumoAuth's own APIs enforce and what your MCP server must do with such tokens.
Scope vocabulary
A server's scopes_supported is also enforced at issuance: tokens for the server's Resource URI can only carry those scopes (plus the OIDC identity scopes). See Scope vocabulary. Protected Resource Metadata and challenge endpoints answer 404 for organizations that are not active.
Related
- MCP Authorization Flow — end-to-end OAuth 2.1 flow
- MCP Server Registration — register a server and configure scopes
Standards used on this page
| Plain name | Standard |
|---|---|
| Protected resource metadata | RFC 9728 |
| Model Context Protocol (MCP) authorization | MCP Authorization specification |
| Resource indicators | RFC 8707 |
| Bearer tokens | RFC 6750 |
| Sender-constrained tokens (DPoP) | RFC 9449 |
See the standards reference for what each one covers and how LumoAuth uses it.