@@ -41,6 +41,12 @@ export type AuthRouterOptions = {
4141 */
4242 resourceName ?: string ;
4343
44+ /**
45+ * The URL of the protected resource (RS) whose metadata we advertise.
46+ * If not provided, falls back to `baseUrl` and then to `issuerUrl` (AS=RS).
47+ */
48+ resourceServerUrl ?: URL ;
49+
4450 // Individual options per route
4551 authorizationOptions ?: Omit < AuthorizationHandlerOptions , "provider" > ;
4652 clientRegistrationOptions ?: Omit < ClientRegistrationHandlerOptions , "clientsStore" > ;
@@ -130,8 +136,8 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
130136
131137 router . use ( mcpAuthMetadataRouter ( {
132138 oauthMetadata,
133- // This router is used for AS+RS combo's, so the issuer is also the resource server
134- resourceServerUrl : new URL ( oauthMetadata . issuer ) ,
139+ // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat)
140+ resourceServerUrl : options . resourceServerUrl ?? options . baseUrl ?? new URL ( oauthMetadata . issuer ) ,
135141 serviceDocumentationUrl : options . serviceDocumentationUrl ,
136142 scopesSupported : options . scopesSupported ,
137143 resourceName : options . resourceName
@@ -185,13 +191,18 @@ export type AuthMetadataOptions = {
185191 resourceName ?: string ;
186192}
187193
188- export function mcpAuthMetadataRouter ( options : AuthMetadataOptions ) {
194+ export function mcpAuthMetadataRouter ( options : AuthMetadataOptions ) : express . Router {
189195 checkIssuerUrl ( new URL ( options . oauthMetadata . issuer ) ) ;
190196
191197 const router = express . Router ( ) ;
192198
199+ // Normalize resource identifier: no trailing slash in the value per RFC 9728 examples
200+ const resourceHref = options . resourceServerUrl . href . endsWith ( '/' )
201+ ? options . resourceServerUrl . href . slice ( 0 , - 1 )
202+ : options . resourceServerUrl . href ;
203+
193204 const protectedResourceMetadata : OAuthProtectedResourceMetadata = {
194- resource : options . resourceServerUrl . href ,
205+ resource : resourceHref ,
195206
196207 authorization_servers : [
197208 options . oauthMetadata . issuer
@@ -202,8 +213,15 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) {
202213 resource_documentation : options . serviceDocumentationUrl ?. href ,
203214 } ;
204215
216+ // Serve PRM at the base well-known URL…
205217 router . use ( "/.well-known/oauth-protected-resource" , metadataHandler ( protectedResourceMetadata ) ) ;
206218
219+ // …and also at the path-specific URL per RFC 9728 when the resource has a path (e.g., /mcp)
220+ const rsPath = new URL ( resourceHref ) . pathname ;
221+ if ( rsPath && rsPath !== "/" ) {
222+ router . use ( `/.well-known/oauth-protected-resource${ rsPath } ` , metadataHandler ( protectedResourceMetadata ) ) ;
223+ }
224+
207225 // Always add this for backwards compatibility
208226 router . use ( "/.well-known/oauth-authorization-server" , metadataHandler ( options . oauthMetadata ) ) ;
209227
@@ -219,8 +237,10 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) {
219237 *
220238 * @example
221239 * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp'))
222- * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource'
240+ * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource/mcp '
223241 */
224242export function getOAuthProtectedResourceMetadataUrl ( serverUrl : URL ) : string {
225- return new URL ( '/.well-known/oauth-protected-resource' , serverUrl ) . href ;
243+ const u = new URL ( serverUrl . href ) ;
244+ const rsPath = u . pathname && u . pathname !== '/' ? u . pathname : '' ;
245+ return new URL ( `/.well-known/oauth-protected-resource${ rsPath } ` , u ) . href ;
226246}
0 commit comments