Skip to content
25 changes: 20 additions & 5 deletions src/server/auth/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthorizationHandlerOptions, "provider">;
clientRegistrationOptions?: Omit<ClientRegistrationHandlerOptions, "clientsStore">;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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));

Expand All @@ -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;
}