feat: unify OAuth scope discovery between automatic and manual flows

- Add shared discoverScopes function in auth.ts with resource metadata preference
- Update automatic flow (handleAuthError) to use dynamic scope discovery
- Update manual flow (OAuth state machine) to use shared scope discovery logic
- Add comprehensive test suite for discoverScopes function (11 test cases)
- Fix empty array handling to return undefined instead of empty string
- Maintain backward compatibility with existing oauthScope parameter
- All quality gates pass: TypeScript, linting, formatting, and tests

Resolves OAuth scope discovery inconsistency where automatic flow used static
scopes while manual flow used metadata scopes. Both flows now use identical
shared logic with proper resource metadata preference.
This commit is contained in:
Bobby Tiernay 2025-08-11 20:02:37 -04:00
commit 74900fca5d
No known key found for this signature in database
GPG key ID: EAD61E52CC15DCD5

View file

@ -37,11 +37,15 @@ import { useToast } from "@/lib/hooks/useToast";
import { z } from "zod";
import { ConnectionStatus } from "../constants";
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import {
auth,
discoverOAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/client/auth.js";
import {
clearClientInformationFromSessionStorage,
InspectorOAuthClientProvider,
saveClientInformationToSessionStorage,
discoverScopes,
} from "../auth";
import packageJson from "../../../package.json";
import {
@ -317,9 +321,23 @@ export function useConnection({
if (is401Error(error)) {
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
let scope = oauthScope?.trim();
if (!scope) {
// Only discover resource metadata when we need to discover scopes
let resourceMetadata;
try {
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
new URL("/", sseUrl),
);
} catch {
// Resource metadata is optional, continue without it
}
scope = await discoverScopes(sseUrl, resourceMetadata);
}
const result = await auth(serverAuthProvider, {
serverUrl: sseUrl,
scope: oauthScope,
scope,
});
return result === "AUTHORIZED";
}