inspector/core/auth/ema/resourceContext.ts
cliffhall f9f79fff54 Address @claude review: format core/, typed output-validation error, document progress heuristic
- Run Prettier on the 60 core/ files this PR touched — the deterministic
  import rewrite had collapsed multi-line imports and left throws unwrapped;
  core/ is in no format gate so it slipped past CI. Now Prettier-clean.
- Default-path output-schema violation now throws
  ProtocolError(ProtocolErrorCode.InvalidParams, …) instead of a bare Error,
  matching the prior v1 contract (SDK threw a typed McpError with a code) so
  downstream `instanceof`/`.code` branching keeps working.
- Document the "assumes serial construction" invariant on pollTaskToolCall's
  progress-subscription revival (the `_progressHandlers` new-key heuristic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:08:54 -04:00

83 lines
2.5 KiB
TypeScript

import {
discoverAuthorizationServerMetadata,
discoverOAuthProtectedResourceMetadata,
selectResourceURL,
} from "@modelcontextprotocol/client";
import type { OAuthClientProvider } from "@modelcontextprotocol/client";
import type { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/client";
import { getAuthorizationServerUrl } from "../discovery.js";
export interface EmaResourceContext {
resourceMetadata: OAuthProtectedResourceMetadata;
resourceAsUrl: URL;
resourceUrl?: URL;
scope?: string;
}
export function resolveEmaScopes(
resourceMetadata: OAuthProtectedResourceMetadata | null | undefined,
configuredScope?: string,
): string | undefined {
const trimmed = configuredScope?.trim();
if (trimmed) return trimmed;
const supported = resourceMetadata?.scopes_supported;
if (supported && supported.length > 0) {
return supported.join(" ");
}
return undefined;
}
export async function discoverEmaResourceContext(
serverUrl: string,
configuredScope?: string,
fetchFn?: typeof fetch,
): Promise<EmaResourceContext> {
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(
serverUrl,
undefined,
fetchFn,
);
/* v8 ignore start -- defensive: the SDK's discoverOAuthProtectedResourceMetadata
validates the response against a Zod schema that requires `resource` to be a
valid URL, so a metadata object reaching here always has a truthy `resource`.
This guard is unreachable through the public discovery path. */
if (!resourceMetadata.resource) {
throw new Error(
"EMA requires protected resource metadata with a resource identifier",
);
}
/* v8 ignore stop */
const resourceAsUrl = getAuthorizationServerUrl(serverUrl, resourceMetadata);
const resourceUrl = await selectResourceURL(
serverUrl,
{
clientMetadata: {
scope: configuredScope ?? "",
redirect_uris: [],
},
} as unknown as OAuthClientProvider,
resourceMetadata,
);
const scope = resolveEmaScopes(resourceMetadata, configuredScope);
return {
resourceMetadata,
resourceAsUrl,
resourceUrl: resourceUrl ?? undefined,
scope,
};
}
export async function discoverResourceAsMetadata(
resourceAsUrl: URL,
fetchFn?: typeof fetch,
) {
const metadata = await discoverAuthorizationServerMetadata(resourceAsUrl, {
fetchFn,
});
if (!metadata) {
throw new Error(
"Failed to discover resource authorization server metadata",
);
}
return metadata;
}