Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions client/src/components/OAuthFlowProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -150,13 +159,7 @@ export const OAuthFlowProgress = ({
<div className="mt-2">
<p className="font-medium">Resource Metadata:</p>
<p className="text-xs text-muted-foreground">
From{" "}
{
new URL(
"/.well-known/oauth-protected-resource",
serverUrl,
).href
}
From {protectedResourceMetadataDiscoveryUrl}
</p>
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(authState.resourceMetadata, null, 2)}
Expand All @@ -169,22 +172,12 @@ export const OAuthFlowProgress = ({
<p className="text-sm font-medium text-blue-700">
ℹ️ Problem with resource metadata from{" "}
<a
href={
new URL(
"/.well-known/oauth-protected-resource",
serverUrl,
).href
}
href={protectedResourceMetadataDiscoveryUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700"
>
{
new URL(
"/.well-known/oauth-protected-resource",
serverUrl,
).href
}
{protectedResourceMetadataDiscoveryUrl}
</a>
</p>
<p className="text-xs text-blue-600 mt-1">
Expand Down
21 changes: 21 additions & 0 deletions client/src/utils/__tests__/oauthUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
parseOAuthCallbackParams,
generateOAuthState,
getAuthorizationServerMetadataDiscoveryUrl,
getProtectedResourceMetadataDiscoveryUrl,
} from "@/utils/oauthUtils.ts";

describe("parseOAuthCallbackParams", () => {
Expand Down Expand Up @@ -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");
});
});
30 changes: 30 additions & 0 deletions client/src/utils/oauthUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};