inspector/core/auth/discovery.ts
Bob Dickinson 5de4e911e3
feat(web,core): enterprise-managed authorization (EMA) (#1509) (#1512)
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
2026-06-24 22:15:12 -04:00

62 lines
2.2 KiB
TypeScript

import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
import type { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { parseHttpUrl } from "./utils.js";
/**
* Returns the URL to use for OAuth authorization server metadata discovery.
* Uses resource metadata's authorization_servers[0] when present, otherwise the MCP server URL.
*/
export function getAuthorizationServerUrl(
serverUrl: string,
resourceMetadata?: OAuthProtectedResourceMetadata | null,
): URL {
const first = resourceMetadata?.authorization_servers?.[0];
// Use truthy check to match original state-machine: empty string falls back to serverUrl
if (first) {
return parseHttpUrl(first, "protected resource authorization_servers[0]");
}
try {
return new URL("/", serverUrl);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
throw new Error(`Invalid MCP server URL: "${serverUrl}" (${detail})`);
}
}
/**
* Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
* @param serverUrl - The MCP server URL
* @param resourceMetadata - Optional resource metadata containing preferred scopes
* @param fetchFn - Optional fetch function for HTTP requests (e.g. proxy fetch in browser)
* @returns Promise resolving to space-separated scope string or undefined
*/
export const discoverScopes = async (
serverUrl: string,
resourceMetadata?: OAuthProtectedResourceMetadata,
fetchFn?: typeof fetch,
): Promise<string | undefined> => {
try {
const authServerUrl = getAuthorizationServerUrl(
serverUrl,
resourceMetadata,
);
const metadata = await discoverAuthorizationServerMetadata(authServerUrl, {
fetchFn,
});
// Prefer resource metadata scopes, but fall back to OAuth metadata if empty
const resourceScopes = resourceMetadata?.scopes_supported;
const oauthScopes = metadata?.scopes_supported;
const scopesSupported =
resourceScopes && resourceScopes.length > 0
? resourceScopes
: oauthScopes;
return scopesSupported && scopesSupported.length > 0
? scopesSupported.join(" ")
: undefined;
} catch {
return undefined;
}
};