@ai-sdk/mcp 1.0.89 → 1.0.90

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @ai-sdk/mcp
2
2
 
3
+ ## 1.0.90
4
+
5
+ ### Patch Changes
6
+
7
+ - a9cea74: Keep default Node.js downloads protected by DNS validation and connection pinning when frameworks or instrumentation wrap global fetch before or after the SDK loads.
8
+ - a9cea74: fix(mcp): prevent SSRF in OAuth metadata discovery
9
+ - Updated dependencies [a9cea74]
10
+ - Updated dependencies [a9cea74]
11
+ - Updated dependencies [a9cea74]
12
+ - @ai-sdk/provider-utils@4.0.57
13
+
3
14
  ## 1.0.89
4
15
 
5
16
  ### Patch Changes
package/dist/index.js CHANGED
@@ -572,8 +572,8 @@ function isOAuthLoopbackHost(hostname) {
572
572
  const normalized = hostname.toLowerCase().replace(/\.+$/, "");
573
573
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "127.0.0.1" || normalized === "[::1]" || normalized === "::1";
574
574
  }
575
- function assertSafeOAuthEndpoint(endpointUrl) {
576
- if ((endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) {
575
+ function assertSafeOAuthEndpoint(endpointUrl, { allowLoopback = false } = {}) {
576
+ if (allowLoopback && (endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) {
577
577
  return;
578
578
  }
579
579
  try {
@@ -585,6 +585,10 @@ function assertSafeOAuthEndpoint(endpointUrl) {
585
585
  });
586
586
  }
587
587
  }
588
+ function getTrustedLoopbackOrigin(authorizationServerUrl) {
589
+ const url = new URL(authorizationServerUrl);
590
+ return isOAuthLoopbackHost(url.hostname) ? url.origin : void 0;
591
+ }
588
592
  function createAuthorizationServerInformation(authorizationServerUrl, metadata) {
589
593
  return {
590
594
  authorizationServerUrl: normalizeUrl(authorizationServerUrl),
@@ -724,13 +728,27 @@ function buildWellKnownPath(wellKnownPrefix, pathname = "", options = {}) {
724
728
  }
725
729
  return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
726
730
  }
727
- async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
731
+ async function fetchWithCorsRetry(url, headers, fetchFn = fetch, trustedOrigin) {
728
732
  try {
729
- return await fetchFn(url, { headers });
733
+ return await (0, import_provider_utils2.fetchWithValidatedRedirects)({
734
+ url: url.href,
735
+ fetch: async (input, init) => (0, import_provider_utils2.fetchWithValidatedEndpoint)({
736
+ url: input,
737
+ init: {
738
+ ...init,
739
+ headers: new Headers(init == null ? void 0 : init.headers).has("MCP-Protocol-Version") ? headers : void 0
740
+ },
741
+ fetch: fetchFn,
742
+ trustedOrigin,
743
+ redirect: "manual"
744
+ }),
745
+ headers,
746
+ trustedOrigin
747
+ });
730
748
  } catch (error) {
731
749
  if (error instanceof TypeError) {
732
750
  if (headers) {
733
- return fetchWithCorsRetry(url, void 0, fetchFn);
751
+ return fetchWithCorsRetry(url, void 0, fetchFn, trustedOrigin);
734
752
  } else {
735
753
  return void 0;
736
754
  }
@@ -738,11 +756,11 @@ async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
738
756
  throw error;
739
757
  }
740
758
  }
741
- async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
759
+ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch, trustedOrigin) {
742
760
  const headers = {
743
761
  "MCP-Protocol-Version": protocolVersion
744
762
  };
745
- return await fetchWithCorsRetry(url, headers, fetchFn);
763
+ return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
746
764
  }
747
765
  function shouldAttemptFallback(response, pathname) {
748
766
  return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
@@ -759,10 +777,20 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
759
777
  url = new URL(wellKnownPath, (_b3 = opts == null ? void 0 : opts.metadataServerUrl) != null ? _b3 : issuer);
760
778
  url.search = issuer.search;
761
779
  }
762
- let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
780
+ let response = await tryMetadataDiscovery(
781
+ url,
782
+ protocolVersion,
783
+ fetchFn,
784
+ opts == null ? void 0 : opts.trustedOrigin
785
+ );
763
786
  if (!(opts == null ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
764
787
  const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
765
- response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
788
+ response = await tryMetadataDiscovery(
789
+ rootUrl,
790
+ protocolVersion,
791
+ fetchFn,
792
+ opts == null ? void 0 : opts.trustedOrigin
793
+ );
766
794
  }
767
795
  return response;
768
796
  }
@@ -773,7 +801,10 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn =
773
801
  fetchFn,
774
802
  {
775
803
  protocolVersion: opts == null ? void 0 : opts.protocolVersion,
776
- metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl
804
+ metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl,
805
+ // The configured MCP server is trusted as a request target. Redirects
806
+ // crossing its origin are still validated before they are followed.
807
+ trustedOrigin: new URL(serverUrl).origin
777
808
  }
778
809
  );
779
810
  if (!response || response.status === 404) {
@@ -846,13 +877,19 @@ function assertMetadataIssuerMatches(metadata, expectedIssuer) {
846
877
  }
847
878
  async function discoverAuthorizationServerMetadata(authorizationServerUrl, {
848
879
  fetchFn = fetch,
849
- protocolVersion = LATEST_PROTOCOL_VERSION
880
+ protocolVersion = LATEST_PROTOCOL_VERSION,
881
+ trustedOrigin
850
882
  } = {}) {
851
883
  var _a3;
852
884
  const headers = { "MCP-Protocol-Version": protocolVersion };
853
885
  const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
854
886
  for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
855
- const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
887
+ const response = await fetchWithCorsRetry(
888
+ endpointUrl,
889
+ headers,
890
+ fetchFn,
891
+ trustedOrigin
892
+ );
856
893
  if (!response) {
857
894
  continue;
858
895
  }
@@ -1018,7 +1055,10 @@ async function exchangeAuthorization(authorizationServerUrl, {
1018
1055
  var _a3;
1019
1056
  const grantType = "authorization_code";
1020
1057
  const tokenUrl = (metadata == null ? void 0 : metadata.token_endpoint) ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
1021
- assertSafeOAuthEndpoint(tokenUrl);
1058
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
1059
+ assertSafeOAuthEndpoint(tokenUrl, {
1060
+ allowLoopback: tokenUrl.origin === trustedOrigin
1061
+ });
1022
1062
  if ((metadata == null ? void 0 : metadata.grant_types_supported) && !metadata.grant_types_supported.includes(grantType)) {
1023
1063
  throw new Error(
1024
1064
  `Incompatible auth server: does not support grant type ${grantType}`
@@ -1052,11 +1092,15 @@ async function exchangeAuthorization(authorizationServerUrl, {
1052
1092
  if (resource) {
1053
1093
  params.set("resource", resourceUrlStripSlash(resource));
1054
1094
  }
1055
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
1056
- method: "POST",
1057
- headers,
1058
- body: params,
1059
- redirect: "error"
1095
+ const response = await (0, import_provider_utils2.fetchWithValidatedEndpoint)({
1096
+ url: tokenUrl,
1097
+ init: {
1098
+ method: "POST",
1099
+ headers,
1100
+ body: params
1101
+ },
1102
+ fetch: fetchFn,
1103
+ trustedOrigin
1060
1104
  });
1061
1105
  if (!response.ok) {
1062
1106
  throw await parseErrorResponse(response);
@@ -1084,7 +1128,10 @@ async function refreshAuthorization(authorizationServerUrl, {
1084
1128
  } else {
1085
1129
  tokenUrl = new URL("/token", authorizationServerUrl);
1086
1130
  }
1087
- assertSafeOAuthEndpoint(tokenUrl);
1131
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
1132
+ assertSafeOAuthEndpoint(tokenUrl, {
1133
+ allowLoopback: tokenUrl.origin === trustedOrigin
1134
+ });
1088
1135
  const headers = new Headers({
1089
1136
  "Content-Type": "application/x-www-form-urlencoded",
1090
1137
  Accept: "application/json"
@@ -1111,11 +1158,15 @@ async function refreshAuthorization(authorizationServerUrl, {
1111
1158
  if (resource) {
1112
1159
  params.set("resource", resourceUrlStripSlash(resource));
1113
1160
  }
1114
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
1115
- method: "POST",
1116
- headers,
1117
- body: params,
1118
- redirect: "error"
1161
+ const response = await (0, import_provider_utils2.fetchWithValidatedEndpoint)({
1162
+ url: tokenUrl,
1163
+ init: {
1164
+ method: "POST",
1165
+ headers,
1166
+ body: params
1167
+ },
1168
+ fetch: fetchFn,
1169
+ trustedOrigin
1119
1170
  });
1120
1171
  if (!response.ok) {
1121
1172
  throw await parseErrorResponse(response);
@@ -1141,14 +1192,21 @@ async function registerClient(authorizationServerUrl, {
1141
1192
  } else {
1142
1193
  registrationUrl = new URL("/register", authorizationServerUrl);
1143
1194
  }
1144
- assertSafeOAuthEndpoint(registrationUrl);
1145
- const response = await (fetchFn != null ? fetchFn : fetch)(registrationUrl, {
1146
- method: "POST",
1147
- headers: {
1148
- "Content-Type": "application/json"
1195
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
1196
+ assertSafeOAuthEndpoint(registrationUrl, {
1197
+ allowLoopback: registrationUrl.origin === trustedOrigin
1198
+ });
1199
+ const response = await (0, import_provider_utils2.fetchWithValidatedEndpoint)({
1200
+ url: registrationUrl,
1201
+ init: {
1202
+ method: "POST",
1203
+ headers: {
1204
+ "Content-Type": "application/json"
1205
+ },
1206
+ body: JSON.stringify(clientMetadata)
1149
1207
  },
1150
- body: JSON.stringify(clientMetadata),
1151
- redirect: "error"
1208
+ fetch: fetchFn,
1209
+ trustedOrigin
1152
1210
  });
1153
1211
  if (!response.ok) {
1154
1212
  throw await parseErrorResponse(response);
@@ -1231,6 +1289,14 @@ async function authInternal(provider, {
1231
1289
  if (!authorizationServerUrl) {
1232
1290
  authorizationServerUrl = (_a3 = callbackAuthorizationServerInformation == null ? void 0 : callbackAuthorizationServerInformation.authorizationServerUrl) != null ? _a3 : serverUrl;
1233
1291
  }
1292
+ const parsedServerUrl = new URL(serverUrl);
1293
+ const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
1294
+ const serverOrigin = parsedServerUrl.origin;
1295
+ const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
1296
+ const trustedAuthorizationServerOrigin = authorizationServerOrigin === serverOrigin || isOAuthLoopbackHost(parsedServerUrl.hostname) && isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname) ? authorizationServerOrigin : void 0;
1297
+ if (!trustedAuthorizationServerOrigin) {
1298
+ assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
1299
+ }
1234
1300
  const resource = await selectResourceURL(
1235
1301
  serverUrl,
1236
1302
  provider,
@@ -1244,7 +1310,8 @@ async function authInternal(provider, {
1244
1310
  const metadata = await discoverAuthorizationServerMetadata(
1245
1311
  authorizationServerUrl,
1246
1312
  {
1247
- fetchFn
1313
+ fetchFn,
1314
+ trustedOrigin: trustedAuthorizationServerOrigin
1248
1315
  }
1249
1316
  );
1250
1317
  const currentAuthorizationServerInformation = createAuthorizationServerInformation(authorizationServerUrl, metadata);