From 8390d81bbe9184a6d2489f1dd7123b35b613b138 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Tue, 1 Jul 2025 15:14:42 -0700 Subject: [PATCH 1/2] Preseve paths when building PRM URL, per specification. --- src/server/auth/router.test.ts | 44 +++++++++++++++++++++++++++++++++- src/server/auth/router.ts | 9 ++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/server/auth/router.test.ts b/src/server/auth/router.test.ts index bcf0a51a..e2c434d8 100644 --- a/src/server/auth/router.test.ts +++ b/src/server/auth/router.test.ts @@ -1,4 +1,4 @@ -import { mcpAuthRouter, AuthRouterOptions, mcpAuthMetadataRouter, AuthMetadataOptions } from './router.js'; +import { mcpAuthRouter, AuthRouterOptions, mcpAuthMetadataRouter, AuthMetadataOptions, getOAuthProtectedResourceMetadataUrl } from './router.js'; import { OAuthServerProvider, AuthorizationParams } from './provider.js'; import { OAuthRegisteredClientsStore } from './clients.js'; import { OAuthClientInformationFull, OAuthMetadata, OAuthTokenRevocationRequest, OAuthTokens } from '../../shared/auth.js'; @@ -480,3 +480,45 @@ describe('MCP Auth Metadata Router', () => { }); }); }); + +describe('MCP Protected Resource Metadata URL', () => { + + it('should insert well-known URI after host', () => { + const serverUrl = new URL('https://mcp.example.com') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource'); + }); + + // NOTE: There's some ambiguity in the specifications as to expected output. + // See discussion on OAuth WG mailing list for further details and rationale: + // https://mailarchive.ietf.org/arch/msg/oauth/LLoteOrAn0sd172dsll254oHGX4/ + it('should insert well-known URI after host with path', () => { + const serverUrl = new URL('https://mcp.example.com/') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource'); + }); + + it('should insert well-known URI between host and path', () => { + const serverUrl = new URL('https://mcp.example.com/mcp') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource/mcp'); + }); + + it('should insert well-known URI between host and query', () => { + const serverUrl = new URL('https://mcp.example.com?k=v') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource?k=v'); + }); + + it('should insert well-known URI between host and path with query', () => { + const serverUrl = new URL('https://mcp.example.com/mcp?k=v') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource/mcp?k=v'); + }); + + it('should insert well-known URI between host and path while preserving trailing slash from path', () => { + const serverUrl = new URL('https://mcp.example.com/mcp/') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource/mcp/'); + }); + + it('should insert well-known URI between host and path with query while preserving trailing slash from path', () => { + const serverUrl = new URL('https://mcp.example.com/mcp/?k=v') + expect(getOAuthProtectedResourceMetadataUrl(serverUrl)).toBe('https://mcp.example.com/.well-known/oauth-protected-resource/mcp/?k=v'); + }); + +}); diff --git a/src/server/auth/router.ts b/src/server/auth/router.ts index 3e752e7a..75f33225 100644 --- a/src/server/auth/router.ts +++ b/src/server/auth/router.ts @@ -222,5 +222,12 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) { * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource' */ export function getOAuthProtectedResourceMetadataUrl(serverUrl: URL): string { - return new URL('/.well-known/oauth-protected-resource', serverUrl).href; + const wellKnownUrl = new URL(serverUrl); + let path = wellKnownUrl.pathname; + if (path === '/') { + path = path.slice(0, -1); + } + + wellKnownUrl.pathname = `/.well-known/oauth-protected-resource${path}` + return wellKnownUrl.toString(); } From 037a2298c3d9c8bf291cf3831b3e1381df2c14f5 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Tue, 1 Jul 2025 15:25:04 -0700 Subject: [PATCH 2/2] Add further example. --- src/server/auth/router.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/auth/router.ts b/src/server/auth/router.ts index 75f33225..ca8f6f71 100644 --- a/src/server/auth/router.ts +++ b/src/server/auth/router.ts @@ -218,8 +218,12 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions) { * @returns The URL for the OAuth protected resource metadata endpoint * * @example - * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp')) + * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com')) * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource' + * + * @example + * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp')) + * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource/mcp' */ export function getOAuthProtectedResourceMetadataUrl(serverUrl: URL): string { const wellKnownUrl = new URL(serverUrl);