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..ca8f6f71 100644 --- a/src/server/auth/router.ts +++ b/src/server/auth/router.ts @@ -218,9 +218,20 @@ 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 { - 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(); }