@@ -41,6 +41,12 @@ export type AuthRouterOptions = {
41
41
*/
42
42
resourceName ?: string ;
43
43
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
+
44
50
// Individual options per route
45
51
authorizationOptions ?: Omit < AuthorizationHandlerOptions , "provider" > ;
46
52
clientRegistrationOptions ?: Omit < ClientRegistrationHandlerOptions , "clientsStore" > ;
@@ -130,8 +136,8 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
130
136
131
137
router . use ( mcpAuthMetadataRouter ( {
132
138
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 ) ,
135
141
serviceDocumentationUrl : options . serviceDocumentationUrl ,
136
142
scopesSupported : options . scopesSupported ,
137
143
resourceName : options . resourceName
@@ -185,13 +191,18 @@ export type AuthMetadataOptions = {
185
191
resourceName ?: string ;
186
192
}
187
193
188
- export function mcpAuthMetadataRouter ( options : AuthMetadataOptions ) {
194
+ export function mcpAuthMetadataRouter ( options : AuthMetadataOptions ) : express . Router {
189
195
checkIssuerUrl ( new URL ( options . oauthMetadata . issuer ) ) ;
190
196
191
197
const router = express . Router ( ) ;
192
198
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
+
193
204
const protectedResourceMetadata : OAuthProtectedResourceMetadata = {
194
- resource : options . resourceServerUrl . href ,
205
+ resource : resourceHref ,
195
206
196
207
authorization_servers : [
197
208
options . oauthMetadata . issuer
@@ -202,8 +213,15 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) {
202
213
resource_documentation : options . serviceDocumentationUrl ?. href ,
203
214
} ;
204
215
216
+ // Serve PRM at the base well-known URL…
205
217
router . use ( "/.well-known/oauth-protected-resource" , metadataHandler ( protectedResourceMetadata ) ) ;
206
218
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
+
207
225
// Always add this for backwards compatibility
208
226
router . use ( "/.well-known/oauth-authorization-server" , metadataHandler ( options . oauthMetadata ) ) ;
209
227
@@ -219,8 +237,10 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) {
219
237
*
220
238
* @example
221
239
* 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 '
223
241
*/
224
242
export 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 ;
226
246
}
0 commit comments