@@ -230,22 +230,8 @@ export async function discoverOAuthProtectedResourceMetadata(
230
230
} else {
231
231
url = new URL ( "/.well-known/oauth-protected-resource" , serverUrl ) ;
232
232
}
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 ) ;
249
235
250
236
if ( response . status === 404 ) {
251
237
throw new Error ( `Resource server does not implement OAuth 2.0 Protected Resource Metadata.` ) ;
@@ -260,43 +246,59 @@ export async function discoverOAuthProtectedResourceMetadata(
260
246
}
261
247
262
248
/**
263
- * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata .
249
+ * Looks up authorization server metadata from an MCP-compliant server .
264
250
*
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
266
261
* return `undefined`. Any other errors will be thrown as exceptions.
267
262
*/
268
263
export async function discoverOAuthMetadata (
269
264
authorizationServerUrl : string | URL ,
270
265
opts ?: { protocolVersion ?: string } ,
271
266
) : 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 ;
286
283
}
287
- }
288
284
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
+ }
292
290
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 ( ) ) ;
297
298
}
298
299
299
- return OAuthMetadataSchema . parse ( await response . json ( ) ) ;
300
+ // If all URLs returned 404, discovery is not supported by the server.
301
+ return undefined ;
300
302
}
301
303
302
304
/**
@@ -530,3 +532,23 @@ export async function registerClient(
530
532
531
533
return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
532
534
}
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