@@ -230,22 +230,8 @@ export async function discoverOAuthProtectedResourceMetadata(
230230 } else {
231231 url = new URL ( "/.well-known/oauth-protected-resource" , serverUrl ) ;
232232 }
233-
234- let response : Response ;
235- try {
236- response = await fetch ( url , {
237- headers : {
238- "MCP-Protocol-Version" : opts ?. protocolVersion ?? LATEST_PROTOCOL_VERSION
239- }
240- } ) ;
241- } catch ( error ) {
242- // CORS errors come back as TypeError
243- if ( error instanceof TypeError ) {
244- response = await fetch ( url ) ;
245- } else {
246- throw error ;
247- }
248- }
233+
234+ const response = await fetchWithCorsFallback ( url , opts ?. protocolVersion ?? LATEST_PROTOCOL_VERSION ) ;
249235
250236 if ( response . status === 404 ) {
251237 throw new Error ( `Resource server does not implement OAuth 2.0 Protected Resource Metadata.` ) ;
@@ -260,43 +246,59 @@ export async function discoverOAuthProtectedResourceMetadata(
260246}
261247
262248/**
263- * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata .
249+ * Looks up authorization server metadata from an MCP-compliant server .
264250 *
265- * If the server returns a 404 for the well-known endpoint, this function will
251+ * Per the MCP specification, clients **MUST** support both OAuth 2.0
252+ * Authorization Server Metadata ([RFC8414](https://datatracker.ietf.org/doc/html/rfc8414))
253+ * and [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0-final.html).
254+ * This function implements this requirement by checking the well-known
255+ * discovery endpoints for both standards.
256+ *
257+ * The function can parse responses from both types of endpoints because OIDC
258+ * discovery metadata is a superset of the metadata defined in RFC 8414.
259+ *
260+ * If the server returns a 404 for all known endpoints, this function will
266261 * return `undefined`. Any other errors will be thrown as exceptions.
267262 */
268263export async function discoverOAuthMetadata (
269264 authorizationServerUrl : string | URL ,
270265 opts ?: { protocolVersion ?: string } ,
271266) : Promise < OAuthMetadata | undefined > {
272- const url = new URL ( "/.well-known/oauth-authorization-server" , authorizationServerUrl ) ;
273- let response : Response ;
274- try {
275- response = await fetch ( url , {
276- headers : {
277- "MCP-Protocol-Version" : opts ?. protocolVersion ?? LATEST_PROTOCOL_VERSION
278- }
279- } ) ;
280- } catch ( error ) {
281- // CORS errors come back as TypeError
282- if ( error instanceof TypeError ) {
283- response = await fetch ( url ) ;
284- } else {
285- throw error ;
267+
268+ /**
269+ * To support both OIDC and plain OAuth2 servers, this checks for their
270+ * respective discovery endpoints.
271+ */
272+ const potentialAuthServerMetadataUrls = [
273+ new URL ( "/.well-known/oauth-authorization-server" , authorizationServerUrl ) ,
274+ new URL ( "/.well-known/openid-configuration" , authorizationServerUrl ) ,
275+ ] ;
276+
277+ for ( const url of potentialAuthServerMetadataUrls ) {
278+ const response = await fetchWithCorsFallback ( url , opts ?. protocolVersion ?? LATEST_PROTOCOL_VERSION ) ;
279+
280+ if ( response . status === 404 ) {
281+ // Try the next URL
282+ continue ;
286283 }
287- }
288284
289- if ( response . status === 404 ) {
290- return undefined ;
291- }
285+ if ( ! response . ok ) {
286+ throw new Error (
287+ `HTTP ${ response . status } trying to load well-known OAuth metadata` ,
288+ ) ;
289+ }
292290
293- if ( ! response . ok ) {
294- throw new Error (
295- `HTTP ${ response . status } trying to load well-known OAuth metadata` ,
296- ) ;
291+ /**
292+ * The `OAuthMetadataSchema` is compatible with both OIDC and OAuth2
293+ * discovery responses. Because OIDC's metadata is a superset, `zod` will
294+ * correctly parse the fields defined in our schema and simply ignore any
295+ * additional OIDC-specific fields.
296+ */
297+ return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
297298 }
298299
299- return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
300+ // If all URLs returned 404, discovery is not supported by the server.
301+ return undefined ;
300302}
301303
302304/**
@@ -530,3 +532,23 @@ export async function registerClient(
530532
531533 return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
532534}
535+
536+ /**
537+ * A fetch wrapper that attempts to set the MCP-Protocol-Version header, but
538+ * falls back to a header-less request if a cors error occurs.
539+ */
540+ const fetchWithCorsFallback = async ( url : URL , protocolVersion : string ) => {
541+ try {
542+ return await fetch ( url , {
543+ headers : {
544+ "MCP-Protocol-Version" : protocolVersion
545+ }
546+ } )
547+ } catch ( error ) {
548+ if ( error instanceof TypeError ) {
549+ // CORS errors come back as TypeError, try again without protocol version header
550+ return await fetch ( url ) ;
551+ }
552+ throw error ;
553+ }
554+ }
0 commit comments