diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 6e0fd6956..58d4e99fb 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -6,6 +6,7 @@ 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"; interface OAuthStepProps { label: string; @@ -81,6 +82,13 @@ export const OAuthFlowProgress = ({ const [clientInfo, setClientInfo] = useState( null, ); + const authorizationServerMetadataDiscoveryUrl = useMemo(() => { + if (!authState.authServerUrl) { + return null; + } + + return getAuthorizationServerMetadataDiscoveryUrl(authState.authServerUrl); + }, [authState.authServerUrl]); const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep); @@ -197,13 +205,7 @@ export const OAuthFlowProgress = ({

Authorization Server Metadata:

{authState.authServerUrl && (

- From{" "} - { - new URL( - "/.well-known/oauth-authorization-server", - authState.authServerUrl, - ).href - } + From {authorizationServerMetadataDiscoveryUrl}

)}
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index b05a48981..fec876778 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -681,7 +681,7 @@ describe("AuthDebugger", () => {
       const updateAuthState = jest.fn();
       const mockResourceMetadata = {
         resource: "https://example.com/mcp",
-        authorization_servers: ["https://custom-auth.example.com"],
+        authorization_servers: ["https://custom-auth.example.com/mcp/tenant"],
         bearer_methods_supported: ["header", "body"],
         resource_documentation: "https://example.com/mcp/docs",
         resource_policy_uri: "https://example.com/mcp/policy",
@@ -733,11 +733,17 @@ describe("AuthDebugger", () => {
         expect(updateAuthState).toHaveBeenCalledWith(
           expect.objectContaining({
             resourceMetadata: mockResourceMetadata,
-            authServerUrl: new URL("https://custom-auth.example.com"),
+            authServerUrl: new URL(
+              "https://custom-auth.example.com/mcp/tenant",
+            ),
             oauthStep: "client_registration",
           }),
         );
       });
+
+      expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
+        new URL("https://custom-auth.example.com/mcp/tenant"),
+      );
     });
 
     it("should handle protected resource metadata fetch failure gracefully", async () => {
diff --git a/client/src/utils/__tests__/oauthUtils.test.ts b/client/src/utils/__tests__/oauthUtils.test.ts
index c42f014eb..b885b5ecb 100644
--- a/client/src/utils/__tests__/oauthUtils.test.ts
+++ b/client/src/utils/__tests__/oauthUtils.test.ts
@@ -2,6 +2,7 @@ import {
   generateOAuthErrorDescription,
   parseOAuthCallbackParams,
   generateOAuthState,
+  getAuthorizationServerMetadataDiscoveryUrl,
 } from "@/utils/oauthUtils.ts";
 
 describe("parseOAuthCallbackParams", () => {
@@ -84,3 +85,29 @@ describe("generateOAuthErrorDescription", () => {
     });
   });
 });
+
+describe("getAuthorizationServerMetadataDiscoveryUrl", () => {
+  it("uses root discovery URL for root authorization server URL", () => {
+    expect(
+      getAuthorizationServerMetadataDiscoveryUrl("https://example.com"),
+    ).toBe("https://example.com/.well-known/oauth-authorization-server");
+  });
+
+  it("inserts tenant path for non-root authorization server URL", () => {
+    expect(
+      getAuthorizationServerMetadataDiscoveryUrl("https://example.com/tenant1"),
+    ).toBe(
+      "https://example.com/.well-known/oauth-authorization-server/tenant1",
+    );
+  });
+
+  it("strips trailing slash before appending tenant path", () => {
+    expect(
+      getAuthorizationServerMetadataDiscoveryUrl(
+        "https://example.com/tenant1/",
+      ),
+    ).toBe(
+      "https://example.com/.well-known/oauth-authorization-server/tenant1",
+    );
+  });
+});
diff --git a/client/src/utils/oauthUtils.ts b/client/src/utils/oauthUtils.ts
index 9b51179c8..cb6faf277 100644
--- a/client/src/utils/oauthUtils.ts
+++ b/client/src/utils/oauthUtils.ts
@@ -87,3 +87,31 @@ export const generateOAuthErrorDescription = (
     .filter(Boolean)
     .join("\n");
 };
+
+/**
+ * Returns the primary OAuth authorization server metadata discovery URL
+ * for a given authorization server URL, including tenant path handling.
+ */
+export const getAuthorizationServerMetadataDiscoveryUrl = (
+  authorizationServerUrl: string | URL,
+): string => {
+  const url =
+    typeof authorizationServerUrl === "string"
+      ? new URL(authorizationServerUrl)
+      : authorizationServerUrl;
+  const hasPath = url.pathname !== "/";
+
+  if (!hasPath) {
+    return new URL("/.well-known/oauth-authorization-server", url.origin).href;
+  }
+
+  // Strip trailing slash to avoid double slashes in tenant-aware discovery URLs.
+  const pathname = url.pathname.endsWith("/")
+    ? url.pathname.slice(0, -1)
+    : url.pathname;
+
+  return new URL(
+    `/.well-known/oauth-authorization-server${pathname}`,
+    url.origin,
+  ).href;
+};