From 89289962e60d65940236f8ddf5dbe97a7577df58 Mon Sep 17 00:00:00 2001 From: Blake Wang Date: Wed, 8 Jul 2026 16:38:42 -0700 Subject: [PATCH] Preserve OAuth resource path during scope discovery --- .../hooks/__tests__/useConnection.test.tsx | 50 ++++++++++++++++++- client/src/lib/hooks/useConnection.ts | 2 +- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 875c9e387..fe74b0978 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -24,7 +24,10 @@ import { ElicitResult, ElicitRequest, } from "@modelcontextprotocol/sdk/types.js"; -import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; +import { + auth, + discoverOAuthProtectedResourceMetadata, +} from "@modelcontextprotocol/sdk/client/auth.js"; import { discoverScopes } from "../../auth"; import { CustomHeaders } from "../../types/customHeaders"; @@ -129,6 +132,9 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => { return { UnauthorizedError, auth: jest.fn().mockResolvedValue("AUTHORIZED"), + discoverOAuthProtectedResourceMetadata: jest + .fn() + .mockResolvedValue(undefined), }; }); @@ -154,6 +160,10 @@ jest.mock("../../auth", () => ({ })); const mockAuth = auth as jest.MockedFunction; +const mockDiscoverOAuthProtectedResourceMetadata = + discoverOAuthProtectedResourceMetadata as jest.MockedFunction< + typeof discoverOAuthProtectedResourceMetadata + >; const mockDiscoverScopes = discoverScopes as jest.MockedFunction< typeof discoverScopes >; @@ -1603,6 +1613,44 @@ describe("useConnection", () => { expect.not.objectContaining({ fetchFn: expect.anything() }), ); }); + + it("preserves path-based MCP endpoints when discovering protected resource metadata", async () => { + const previewUrl = + "https://mcp.stg.services.atlassian.com/v1/mcp/preview"; + const resourceMetadata = { + resource: previewUrl, + authorization_servers: ["https://auth.stg.atlassian.com/example"], + bearer_methods_supported: ["header"], + scopes_supported: ["search:rovo:agent-interface"], + }; + + mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValueOnce( + resourceMetadata, + ); + mockDiscoverScopes.mockResolvedValue("search:rovo:agent-interface"); + setup401Error(); + + await attemptConnection({ + ...defaultProps, + sseUrl: previewUrl, + }); + + expect( + mockDiscoverOAuthProtectedResourceMetadata.mock.calls[0][0].href, + ).toBe(previewUrl); + expect(mockDiscoverScopes).toHaveBeenCalledWith( + previewUrl, + resourceMetadata, + expect.any(Function), + ); + expect(mockAuth).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + serverUrl: previewUrl, + scope: "search:rovo:agent-interface", + }), + ); + }); }); describe("Inspector proxy McpError auth recovery", () => { diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9694b891f..920ad60ae 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -398,7 +398,7 @@ export function useConnection({ let resourceMetadata; try { resourceMetadata = await discoverOAuthProtectedResourceMetadata( - new URL("/", sseUrl), + new URL(sseUrl), {}, fetchFn, );