diff --git a/src/server/auth/router.ts b/src/server/auth/router.ts index a06bf73a1..bcbd4369c 100644 --- a/src/server/auth/router.ts +++ b/src/server/auth/router.ts @@ -41,6 +41,12 @@ export type AuthRouterOptions = { */ resourceName?: string; + /** + * The URL of the protected resource (RS) whose metadata we advertise. + * If not provided, falls back to `baseUrl` and then to `issuerUrl` (AS=RS). + */ + resourceServerUrl?: URL; + // Individual options per route authorizationOptions?: Omit; clientRegistrationOptions?: Omit; @@ -130,8 +136,8 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler { router.use(mcpAuthMetadataRouter({ oauthMetadata, - // This router is used for AS+RS combo's, so the issuer is also the resource server - resourceServerUrl: new URL(oauthMetadata.issuer), + // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat) + resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer), serviceDocumentationUrl: options.serviceDocumentationUrl, scopesSupported: options.scopesSupported, resourceName: options.resourceName @@ -185,7 +191,7 @@ export type AuthMetadataOptions = { resourceName?: string; } -export function mcpAuthMetadataRouter(options: AuthMetadataOptions) { +export function mcpAuthMetadataRouter(options: AuthMetadataOptions): express.Router { checkIssuerUrl(new URL(options.oauthMetadata.issuer)); const router = express.Router(); @@ -202,8 +208,15 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) { resource_documentation: options.serviceDocumentationUrl?.href, }; + // Serve PRM at the base well-known URL… router.use("/.well-known/oauth-protected-resource", metadataHandler(protectedResourceMetadata)); + // …and also at the path-specific URL per RFC 9728 when the resource has a path (e.g., /mcp) + const rsPath = new URL(options.resourceServerUrl.href).pathname; + if (rsPath && rsPath !== "/") { + router.use(`/.well-known/oauth-protected-resource${rsPath}`, metadataHandler(protectedResourceMetadata)); + } + // Always add this for backwards compatibility router.use("/.well-known/oauth-authorization-server", metadataHandler(options.oauthMetadata)); @@ -219,8 +232,10 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) { * * @example * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp')) - * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource' + * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource/mcp' */ export function getOAuthProtectedResourceMetadataUrl(serverUrl: URL): string { - return new URL('/.well-known/oauth-protected-resource', serverUrl).href; + const u = new URL(serverUrl.href); + const rsPath = u.pathname && u.pathname !== '/' ? u.pathname : ''; + return new URL(`/.well-known/oauth-protected-resource${rsPath}`, u).href; }