@@ -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,7 +191,7 @@ 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 ( ) ;
@@ -202,8 +208,15 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) {
202
208
resource_documentation : options . serviceDocumentationUrl ?. href ,
203
209
} ;
204
210
211
+ // Serve PRM at the base well-known URL…
205
212
router . use ( "/.well-known/oauth-protected-resource" , metadataHandler ( protectedResourceMetadata ) ) ;
206
213
214
+ // …and also at the path-specific URL per RFC 9728 when the resource has a path (e.g., /mcp)
215
+ const rsPath = new URL ( options . resourceServerUrl . href ) . pathname ;
216
+ if ( rsPath && rsPath !== "/" ) {
217
+ router . use ( `/.well-known/oauth-protected-resource${ rsPath } ` , metadataHandler ( protectedResourceMetadata ) ) ;
218
+ }
219
+
207
220
// Always add this for backwards compatibility
208
221
router . use ( "/.well-known/oauth-authorization-server" , metadataHandler ( options . oauthMetadata ) ) ;
209
222
@@ -219,8 +232,10 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) {
219
232
*
220
233
* @example
221
234
* getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp'))
222
- * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource'
235
+ * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource/mcp '
223
236
*/
224
237
export function getOAuthProtectedResourceMetadataUrl ( serverUrl : URL ) : string {
225
- return new URL ( '/.well-known/oauth-protected-resource' , serverUrl ) . href ;
238
+ const u = new URL ( serverUrl . href ) ;
239
+ const rsPath = u . pathname && u . pathname !== '/' ? u . pathname : '' ;
240
+ return new URL ( `/.well-known/oauth-protected-resource${ rsPath } ` , u ) . href ;
226
241
}
0 commit comments