@ai-sdk/mcp 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -437,7 +437,12 @@ function checkResourceAllowed({
437
437
  }
438
438
 
439
439
  // src/tool/oauth.ts
440
- import { parseJSON as parseJSON2 } from "@ai-sdk/provider-utils";
440
+ import {
441
+ fetchWithValidatedEndpoint,
442
+ fetchWithValidatedRedirects,
443
+ parseJSON as parseJSON2,
444
+ validateDownloadUrl
445
+ } from "@ai-sdk/provider-utils";
441
446
  var UnauthorizedError = class extends Error {
442
447
  constructor(message = "Unauthorized") {
443
448
  super(message);
@@ -471,13 +476,48 @@ function buildWellKnownPath(wellKnownPrefix, pathname = "", options = {}) {
471
476
  }
472
477
  return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
473
478
  }
474
- async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
479
+ function isOAuthLoopbackHost(hostname) {
480
+ const normalized = hostname.toLowerCase().replace(/\.+$/, "");
481
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "127.0.0.1" || normalized === "[::1]" || normalized === "::1";
482
+ }
483
+ function assertSafeOAuthEndpoint(endpointUrl, { allowLoopback = false } = {}) {
484
+ if (allowLoopback && (endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) {
485
+ return;
486
+ }
475
487
  try {
476
- return await fetchFn(url, { headers });
488
+ validateDownloadUrl(endpointUrl.href);
489
+ } catch (error) {
490
+ throw new MCPClientOAuthError({
491
+ message: `OAuth endpoint URL is not allowed: ${endpointUrl.href}`,
492
+ cause: error
493
+ });
494
+ }
495
+ }
496
+ function getTrustedLoopbackOrigin(authorizationServerUrl) {
497
+ const url = new URL(authorizationServerUrl);
498
+ return isOAuthLoopbackHost(url.hostname) ? url.origin : void 0;
499
+ }
500
+ async function fetchWithCorsRetry(url, headers, fetchFn = fetch, trustedOrigin) {
501
+ try {
502
+ return await fetchWithValidatedRedirects({
503
+ url: url.href,
504
+ fetch: async (input, init) => fetchWithValidatedEndpoint({
505
+ url: input,
506
+ init: {
507
+ ...init,
508
+ headers: new Headers(init == null ? void 0 : init.headers).has("MCP-Protocol-Version") ? headers : void 0
509
+ },
510
+ fetch: fetchFn,
511
+ trustedOrigin,
512
+ redirect: "manual"
513
+ }),
514
+ headers,
515
+ trustedOrigin
516
+ });
477
517
  } catch (error) {
478
518
  if (error instanceof TypeError) {
479
519
  if (headers) {
480
- return fetchWithCorsRetry(url, void 0, fetchFn);
520
+ return fetchWithCorsRetry(url, void 0, fetchFn, trustedOrigin);
481
521
  } else {
482
522
  return void 0;
483
523
  }
@@ -485,11 +525,11 @@ async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
485
525
  throw error;
486
526
  }
487
527
  }
488
- async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
528
+ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch, trustedOrigin) {
489
529
  const headers = {
490
530
  "MCP-Protocol-Version": protocolVersion
491
531
  };
492
- return await fetchWithCorsRetry(url, headers, fetchFn);
532
+ return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
493
533
  }
494
534
  function shouldAttemptFallback(response, pathname) {
495
535
  return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
@@ -506,10 +546,20 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
506
546
  url = new URL(wellKnownPath, (_b3 = opts == null ? void 0 : opts.metadataServerUrl) != null ? _b3 : issuer);
507
547
  url.search = issuer.search;
508
548
  }
509
- let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
549
+ let response = await tryMetadataDiscovery(
550
+ url,
551
+ protocolVersion,
552
+ fetchFn,
553
+ opts == null ? void 0 : opts.trustedOrigin
554
+ );
510
555
  if (!(opts == null ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
511
556
  const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
512
- response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
557
+ response = await tryMetadataDiscovery(
558
+ rootUrl,
559
+ protocolVersion,
560
+ fetchFn,
561
+ opts == null ? void 0 : opts.trustedOrigin
562
+ );
513
563
  }
514
564
  return response;
515
565
  }
@@ -520,7 +570,10 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn =
520
570
  fetchFn,
521
571
  {
522
572
  protocolVersion: opts == null ? void 0 : opts.protocolVersion,
523
- metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl
573
+ metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl,
574
+ // The configured MCP server is trusted as a request target. Redirects
575
+ // crossing its origin are still validated before they are followed.
576
+ trustedOrigin: new URL(serverUrl).origin
524
577
  }
525
578
  );
526
579
  if (!response || response.status === 404) {
@@ -577,13 +630,19 @@ function buildDiscoveryUrls(authorizationServerUrl) {
577
630
  }
578
631
  async function discoverAuthorizationServerMetadata(authorizationServerUrl, {
579
632
  fetchFn = fetch,
580
- protocolVersion = LATEST_PROTOCOL_VERSION
633
+ protocolVersion = LATEST_PROTOCOL_VERSION,
634
+ trustedOrigin
581
635
  } = {}) {
582
636
  var _a3;
583
637
  const headers = { "MCP-Protocol-Version": protocolVersion };
584
638
  const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
585
639
  for (const { url: endpointUrl, type } of urlsToTry) {
586
- const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
640
+ const response = await fetchWithCorsRetry(
641
+ endpointUrl,
642
+ headers,
643
+ fetchFn,
644
+ trustedOrigin
645
+ );
587
646
  if (!response) {
588
647
  continue;
589
648
  }
@@ -743,6 +802,10 @@ async function exchangeAuthorization(authorizationServerUrl, {
743
802
  var _a3;
744
803
  const grantType = "authorization_code";
745
804
  const tokenUrl = (metadata == null ? void 0 : metadata.token_endpoint) ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
805
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
806
+ assertSafeOAuthEndpoint(tokenUrl, {
807
+ allowLoopback: tokenUrl.origin === trustedOrigin
808
+ });
746
809
  if ((metadata == null ? void 0 : metadata.grant_types_supported) && !metadata.grant_types_supported.includes(grantType)) {
747
810
  throw new Error(
748
811
  `Incompatible auth server: does not support grant type ${grantType}`
@@ -771,10 +834,15 @@ async function exchangeAuthorization(authorizationServerUrl, {
771
834
  if (resource) {
772
835
  params.set("resource", resource.href);
773
836
  }
774
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
775
- method: "POST",
776
- headers,
777
- body: params
837
+ const response = await fetchWithValidatedEndpoint({
838
+ url: tokenUrl,
839
+ init: {
840
+ method: "POST",
841
+ headers,
842
+ body: params
843
+ },
844
+ fetch: fetchFn,
845
+ trustedOrigin
778
846
  });
779
847
  if (!response.ok) {
780
848
  throw await parseErrorResponse(response);
@@ -802,6 +870,10 @@ async function refreshAuthorization(authorizationServerUrl, {
802
870
  } else {
803
871
  tokenUrl = new URL("/token", authorizationServerUrl);
804
872
  }
873
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
874
+ assertSafeOAuthEndpoint(tokenUrl, {
875
+ allowLoopback: tokenUrl.origin === trustedOrigin
876
+ });
805
877
  const headers = new Headers({
806
878
  "Content-Type": "application/x-www-form-urlencoded"
807
879
  });
@@ -822,10 +894,15 @@ async function refreshAuthorization(authorizationServerUrl, {
822
894
  if (resource) {
823
895
  params.set("resource", resource.href);
824
896
  }
825
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
826
- method: "POST",
827
- headers,
828
- body: params
897
+ const response = await fetchWithValidatedEndpoint({
898
+ url: tokenUrl,
899
+ init: {
900
+ method: "POST",
901
+ headers,
902
+ body: params
903
+ },
904
+ fetch: fetchFn,
905
+ trustedOrigin
829
906
  });
830
907
  if (!response.ok) {
831
908
  throw await parseErrorResponse(response);
@@ -851,12 +928,21 @@ async function registerClient(authorizationServerUrl, {
851
928
  } else {
852
929
  registrationUrl = new URL("/register", authorizationServerUrl);
853
930
  }
854
- const response = await (fetchFn != null ? fetchFn : fetch)(registrationUrl, {
855
- method: "POST",
856
- headers: {
857
- "Content-Type": "application/json"
931
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
932
+ assertSafeOAuthEndpoint(registrationUrl, {
933
+ allowLoopback: registrationUrl.origin === trustedOrigin
934
+ });
935
+ const response = await fetchWithValidatedEndpoint({
936
+ url: registrationUrl,
937
+ init: {
938
+ method: "POST",
939
+ headers: {
940
+ "Content-Type": "application/json"
941
+ },
942
+ body: JSON.stringify(clientMetadata)
858
943
  },
859
- body: JSON.stringify(clientMetadata)
944
+ fetch: fetchFn,
945
+ trustedOrigin
860
946
  });
861
947
  if (!response.ok) {
862
948
  throw await parseErrorResponse(response);
@@ -922,6 +1008,14 @@ async function authInternal(provider, {
922
1008
  if (!authorizationServerUrl) {
923
1009
  authorizationServerUrl = serverUrl;
924
1010
  }
1011
+ const parsedServerUrl = new URL(serverUrl);
1012
+ const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
1013
+ const serverOrigin = parsedServerUrl.origin;
1014
+ const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
1015
+ const trustedAuthorizationServerOrigin = authorizationServerOrigin === serverOrigin || isOAuthLoopbackHost(parsedServerUrl.hostname) && isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname) ? authorizationServerOrigin : void 0;
1016
+ if (!trustedAuthorizationServerOrigin) {
1017
+ assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
1018
+ }
925
1019
  const resource = await selectResourceURL(
926
1020
  serverUrl,
927
1021
  provider,
@@ -930,7 +1024,8 @@ async function authInternal(provider, {
930
1024
  const metadata = await discoverAuthorizationServerMetadata(
931
1025
  authorizationServerUrl,
932
1026
  {
933
- fetchFn
1027
+ fetchFn,
1028
+ trustedOrigin: trustedAuthorizationServerOrigin
934
1029
  }
935
1030
  );
936
1031
  let clientInformation = await Promise.resolve(provider.clientInformation());