diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 58d4e99fb..31ef4400c 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -6,7 +6,10 @@ import { useEffect, useMemo, useState } from "react"; import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js"; import { validateRedirectUrl } from "@/utils/urlValidation"; import { useToast } from "@/lib/hooks/useToast"; -import { getAuthorizationServerMetadataDiscoveryUrl } from "@/utils/oauthUtils"; +import { + getAuthorizationServerMetadataDiscoveryUrl, + getProtectedResourceMetadataDiscoveryUrl, +} from "@/utils/oauthUtils"; interface OAuthStepProps { label: string; @@ -89,6 +92,12 @@ export const OAuthFlowProgress = ({ return getAuthorizationServerMetadataDiscoveryUrl(authState.authServerUrl); }, [authState.authServerUrl]); + // Mirrors the path-aware URL the MCP SDK actually fetches for the OAuth + // Protected Resource metadata, so the displayed URL matches the request. + const protectedResourceMetadataDiscoveryUrl = useMemo( + () => getProtectedResourceMetadataDiscoveryUrl(serverUrl), + [serverUrl], + ); const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep); @@ -150,13 +159,7 @@ export const OAuthFlowProgress = ({

Resource Metadata:

- From{" "} - { - new URL( - "/.well-known/oauth-protected-resource", - serverUrl, - ).href - } + From {protectedResourceMetadataDiscoveryUrl}

                     {JSON.stringify(authState.resourceMetadata, null, 2)}
@@ -169,22 +172,12 @@ export const OAuthFlowProgress = ({
                   

ℹ️ Problem with resource metadata from{" "} - { - new URL( - "/.well-known/oauth-protected-resource", - serverUrl, - ).href - } + {protectedResourceMetadataDiscoveryUrl}

diff --git a/client/src/utils/__tests__/oauthUtils.test.ts b/client/src/utils/__tests__/oauthUtils.test.ts index b885b5ecb..758db863f 100644 --- a/client/src/utils/__tests__/oauthUtils.test.ts +++ b/client/src/utils/__tests__/oauthUtils.test.ts @@ -3,6 +3,7 @@ import { parseOAuthCallbackParams, generateOAuthState, getAuthorizationServerMetadataDiscoveryUrl, + getProtectedResourceMetadataDiscoveryUrl, } from "@/utils/oauthUtils.ts"; describe("parseOAuthCallbackParams", () => { @@ -111,3 +112,23 @@ describe("getAuthorizationServerMetadataDiscoveryUrl", () => { ); }); }); + +describe("getProtectedResourceMetadataDiscoveryUrl", () => { + it("uses root discovery URL for root server URL", () => { + expect(getProtectedResourceMetadataDiscoveryUrl("https://example.com")).toBe( + "https://example.com/.well-known/oauth-protected-resource", + ); + }); + + it("appends path for non-root server URL (matches the URL the SDK fetches)", () => { + expect( + getProtectedResourceMetadataDiscoveryUrl("https://example.com/mcp"), + ).toBe("https://example.com/.well-known/oauth-protected-resource/mcp"); + }); + + it("strips trailing slash before appending path", () => { + expect( + getProtectedResourceMetadataDiscoveryUrl("https://example.com/mcp/"), + ).toBe("https://example.com/.well-known/oauth-protected-resource/mcp"); + }); +}); diff --git a/client/src/utils/oauthUtils.ts b/client/src/utils/oauthUtils.ts index cb6faf277..0f9ba7503 100644 --- a/client/src/utils/oauthUtils.ts +++ b/client/src/utils/oauthUtils.ts @@ -115,3 +115,33 @@ export const getAuthorizationServerMetadataDiscoveryUrl = ( url.origin, ).href; }; + +/** + * Returns the primary OAuth Protected Resource metadata discovery URL for a + * given resource (MCP server) URL, including path handling. + * + * Mirrors the path-aware discovery the MCP TypeScript SDK uses in + * `discoverOAuthProtectedResourceMetadata` so the URL the UI displays matches + * the URL that is actually fetched. See: + * https://github.com/modelcontextprotocol/inspector/issues/1166 + */ +export const getProtectedResourceMetadataDiscoveryUrl = ( + serverUrl: string | URL, +): string => { + const url = typeof serverUrl === "string" ? new URL(serverUrl) : serverUrl; + const hasPath = url.pathname !== "/"; + + if (!hasPath) { + return new URL("/.well-known/oauth-protected-resource", url.origin).href; + } + + // Strip trailing slash to avoid double slashes in path-aware discovery URLs. + const pathname = url.pathname.endsWith("/") + ? url.pathname.slice(0, -1) + : url.pathname; + + return new URL( + `/.well-known/oauth-protected-resource${pathname}`, + url.origin, + ).href; +};