Serves the RFC 9728 protected-resource metadata document MCP clients use to discover your authorization server:
# Phoenix router (path is fixed by RFC 9728)
forward "/.well-known/oauth-protected-resource",
Noizu.MCP.Auth.ProtectedResourceMetadataPlug,
resource: "https://api.example.com/mcp",
authorization_servers: ["https://auth.example.com"]Options: :resource (required), :authorization_servers (required),
:scopes_supported, :bearer_methods_supported, :resource_name,
:extra (map merged in).
Several mounts behind one forward
A host with more than one MCP mount needs a distinct resource value per
mount, and RFC 9728 path-insertion puts the mount path in the well-known
URL: /.well-known/oauth-protected-resource/mcp/learning describes
https://host/mcp/learning. Pass :resources — a map of path suffix to
per-resource options — and one forward answers them all:
forward "/.well-known/oauth-protected-resource",
Noizu.MCP.Auth.ProtectedResourceMetadataPlug,
authorization_servers: ["https://app.example.com"],
scopes_supported: ["mcp"],
default_resource: "/mcp",
resources: %{
"/mcp" => [resource: "https://app.example.com/mcp"],
"/mcp/learning" => [resource: "https://app.example.com/mcp/learning"],
"/mcp/workspace" => [resource: "https://app.example.com/mcp/workspace"]
}Top-level options are defaults every entry inherits; entry options win.
:default_resource names the suffix answered at the bare forward path
(what a client that skipped path-insertion asks for). An unknown suffix is
a 404 — never a document describing some other resource, which would
hand a client the wrong audience.
CORS
claude.ai performs discovery from a browser context, so the document is
served with Access-Control-Allow-Origin (default * — it is public,
unauthenticated metadata) and OPTIONS answers 204. Pass
allow_origin: nil to omit the header.