From 90074a4f8b97674b05c74b0b8f530b4fc976ea83 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 13 Nov 2025 18:35:11 +0000 Subject: [PATCH 1/3] use base url for AS rather than full URL when doing march spec things --- src/client/auth.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/auth.ts b/src/client/auth.ts index 6d4ede84b..ea00d03a2 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -348,6 +348,7 @@ async function authInternal( ): Promise { let resourceMetadata: OAuthProtectedResourceMetadata | undefined; let authorizationServerUrl: string | URL | undefined; + try { resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn); if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) { @@ -359,10 +360,10 @@ async function authInternal( /** * If we don't get a valid authorization server metadata from protected resource metadata, - * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server. + * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server base URL acts as the Authorization server. */ if (!authorizationServerUrl) { - authorizationServerUrl = serverUrl; + authorizationServerUrl = new URL("/", serverUrl); } const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata); From 93ad25f38b9986d3bd18638979e5f942f885a0b9 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 13 Nov 2025 18:50:46 +0000 Subject: [PATCH 2/3] check march spec fall back w/ test --- src/client/auth.test.ts | 73 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index fc71b03d9..b589f25d3 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -1585,10 +1585,81 @@ describe('OAuth Authorization', () => { // First call should be to protected resource metadata expect(mockFetch.mock.calls[0][0].toString()).toBe('https://resource.example.com/.well-known/oauth-protected-resource'); - // Second call should be to oauth metadata + // Second call should be to oauth metadata at the root path expect(mockFetch.mock.calls[1][0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server'); }); + it('uses base URL (with root path) as authorization server when protected-resource-metadata discovery fails', async () => { + // Setup: First call to protected resource metadata fails (404) + // When no authorization_servers are found in protected resource metadata, + // the auth server URL should be set to the base URL with "/" path + let callCount = 0; + mockFetch.mockImplementation(url => { + callCount++; + + const urlString = url.toString(); + + if (urlString.includes('/.well-known/oauth-protected-resource')) { + // Protected resource metadata discovery attempts (both path-aware and root) fail with 404 + return Promise.resolve({ + ok: false, + status: 404 + }); + } else if (urlString === 'https://resource.example.com/.well-known/oauth-authorization-server') { + // Should fetch from base URL with root path, not the full serverUrl path + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({ + issuer: 'https://resource.example.com/', + authorization_endpoint: 'https://resource.example.com/authorize', + token_endpoint: 'https://resource.example.com/token', + registration_endpoint: 'https://resource.example.com/register', + response_types_supported: ['code'], + code_challenge_methods_supported: ['S256'] + }) + }); + } else if (urlString.includes('/register')) { + // Client registration succeeds + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_id_issued_at: 1612137600, + client_secret_expires_at: 1612224000, + redirect_uris: ['http://localhost:3000/callback'], + client_name: 'Test Client' + }) + }); + } + + return Promise.reject(new Error(`Unexpected fetch call #${callCount}: ${urlString}`)); + }); + + // Mock provider methods + (mockProvider.clientInformation as jest.Mock).mockResolvedValue(undefined); + (mockProvider.tokens as jest.Mock).mockResolvedValue(undefined); + mockProvider.saveClientInformation = jest.fn(); + + // Call the auth function with a server URL that has a path + const result = await auth(mockProvider, { + serverUrl: 'https://resource.example.com/path/to/server' + }); + + // Verify the result + expect(result).toBe('REDIRECT'); + + // Verify that the oauth-authorization-server call uses the base URL + // This proves the fix: using new URL("/", serverUrl) instead of serverUrl + const authServerCall = mockFetch.mock.calls.find(call => + call[0].toString().includes('/.well-known/oauth-authorization-server') + ); + expect(authServerCall).toBeDefined(); + expect(authServerCall[0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server'); + }); + it('passes resource parameter through authorization flow', async () => { // Mock successful metadata discovery - need to include protected resource metadata mockFetch.mockImplementation(url => { From 4b2ef9da637160731acf7fe3026a1dd37ed4e934 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 13 Nov 2025 19:19:49 +0000 Subject: [PATCH 3/3] Fix Prettier formatting --- src/client/auth.test.ts | 2 +- src/client/auth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index b589f25d3..8124fe768 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -1654,7 +1654,7 @@ describe('OAuth Authorization', () => { // Verify that the oauth-authorization-server call uses the base URL // This proves the fix: using new URL("/", serverUrl) instead of serverUrl const authServerCall = mockFetch.mock.calls.find(call => - call[0].toString().includes('/.well-known/oauth-authorization-server') + call[0].toString().includes('/.well-known/oauth-authorization-server') ); expect(authServerCall).toBeDefined(); expect(authServerCall[0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server'); diff --git a/src/client/auth.ts b/src/client/auth.ts index ea00d03a2..fba0e7bf7 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -363,7 +363,7 @@ async function authInternal( * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server base URL acts as the Authorization server. */ if (!authorizationServerUrl) { - authorizationServerUrl = new URL("/", serverUrl); + authorizationServerUrl = new URL('/', serverUrl); } const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);