@ai-sdk/mcp 2.0.48 → 2.0.50

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,26 @@
1
1
  # @ai-sdk/mcp
2
2
 
3
+ ## 2.0.50
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [5c0054d]
8
+ - Updated dependencies [39535af]
9
+ - @ai-sdk/provider@4.0.15
10
+ - @ai-sdk/provider-utils@5.0.41
11
+
12
+ ## 2.0.49
13
+
14
+ ### Patch Changes
15
+
16
+ - c43e4b7: fix(mcp): prevent SSRF in OAuth metadata discovery
17
+ - Updated dependencies [5ec21a6]
18
+ - Updated dependencies [7469a3b]
19
+ - Updated dependencies [813bb36]
20
+ - Updated dependencies [c43e4b7]
21
+ - @ai-sdk/provider@4.0.14
22
+ - @ai-sdk/provider-utils@5.0.40
23
+
3
24
  ## 2.0.48
4
25
 
5
26
  ### Patch Changes
package/dist/index.js CHANGED
@@ -570,6 +570,8 @@ function checkResourceAllowed({
570
570
 
571
571
  // src/tool/oauth.ts
572
572
  import {
573
+ fetchWithValidatedEndpoint,
574
+ fetchWithValidatedRedirects,
573
575
  parseJSON as parseJSON2,
574
576
  validateDownloadUrl
575
577
  } from "@ai-sdk/provider-utils";
@@ -586,8 +588,8 @@ function isOAuthLoopbackHost(hostname) {
586
588
  const normalized = hostname.toLowerCase().replace(/\.+$/, "");
587
589
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "127.0.0.1" || normalized === "[::1]" || normalized === "::1";
588
590
  }
589
- function assertSafeOAuthEndpoint(endpointUrl) {
590
- if ((endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) {
591
+ function assertSafeOAuthEndpoint(endpointUrl, { allowLoopback = false } = {}) {
592
+ if (allowLoopback && (endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) {
591
593
  return;
592
594
  }
593
595
  try {
@@ -599,6 +601,10 @@ function assertSafeOAuthEndpoint(endpointUrl) {
599
601
  });
600
602
  }
601
603
  }
604
+ function getTrustedLoopbackOrigin(authorizationServerUrl) {
605
+ const url = new URL(authorizationServerUrl);
606
+ return isOAuthLoopbackHost(url.hostname) ? url.origin : void 0;
607
+ }
602
608
  function validateAuthorizationResponseIssuer({
603
609
  callbackIssuer,
604
610
  expectedIssuer
@@ -754,13 +760,27 @@ function buildWellKnownPath(wellKnownPrefix, pathname = "", options = {}) {
754
760
  }
755
761
  return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
756
762
  }
757
- async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
763
+ async function fetchWithCorsRetry(url, headers, fetchFn = fetch, trustedOrigin) {
758
764
  try {
759
- return await fetchFn(url, { headers });
765
+ return await fetchWithValidatedRedirects({
766
+ url: url.href,
767
+ fetch: async (input, init) => fetchWithValidatedEndpoint({
768
+ url: input,
769
+ init: {
770
+ ...init,
771
+ headers: new Headers(init == null ? void 0 : init.headers).has("MCP-Protocol-Version") ? headers : void 0
772
+ },
773
+ fetch: fetchFn,
774
+ trustedOrigin,
775
+ redirect: "manual"
776
+ }),
777
+ headers,
778
+ trustedOrigin
779
+ });
760
780
  } catch (error) {
761
781
  if (error instanceof TypeError) {
762
782
  if (headers) {
763
- return fetchWithCorsRetry(url, void 0, fetchFn);
783
+ return fetchWithCorsRetry(url, void 0, fetchFn, trustedOrigin);
764
784
  } else {
765
785
  return void 0;
766
786
  }
@@ -768,11 +788,11 @@ async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
768
788
  throw error;
769
789
  }
770
790
  }
771
- async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
791
+ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch, trustedOrigin) {
772
792
  const headers = {
773
793
  "MCP-Protocol-Version": protocolVersion
774
794
  };
775
- return await fetchWithCorsRetry(url, headers, fetchFn);
795
+ return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
776
796
  }
777
797
  function shouldAttemptFallback(response, pathname) {
778
798
  return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
@@ -789,10 +809,20 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
789
809
  url = new URL(wellKnownPath, (_b3 = opts == null ? void 0 : opts.metadataServerUrl) != null ? _b3 : issuer);
790
810
  url.search = issuer.search;
791
811
  }
792
- let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
812
+ let response = await tryMetadataDiscovery(
813
+ url,
814
+ protocolVersion,
815
+ fetchFn,
816
+ opts == null ? void 0 : opts.trustedOrigin
817
+ );
793
818
  if (!(opts == null ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
794
819
  const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
795
- response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
820
+ response = await tryMetadataDiscovery(
821
+ rootUrl,
822
+ protocolVersion,
823
+ fetchFn,
824
+ opts == null ? void 0 : opts.trustedOrigin
825
+ );
796
826
  }
797
827
  return response;
798
828
  }
@@ -803,7 +833,10 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn =
803
833
  fetchFn,
804
834
  {
805
835
  protocolVersion: opts == null ? void 0 : opts.protocolVersion,
806
- metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl
836
+ metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl,
837
+ // The configured MCP server is trusted as a request target. Redirects
838
+ // crossing its origin are still validated before they are followed.
839
+ trustedOrigin: new URL(serverUrl).origin
807
840
  }
808
841
  );
809
842
  if (!response || response.status === 404) {
@@ -876,13 +909,19 @@ function assertMetadataIssuerMatches(metadata, expectedIssuer) {
876
909
  }
877
910
  async function discoverAuthorizationServerMetadata(authorizationServerUrl, {
878
911
  fetchFn = fetch,
879
- protocolVersion = LATEST_PROTOCOL_VERSION
912
+ protocolVersion = LATEST_PROTOCOL_VERSION,
913
+ trustedOrigin
880
914
  } = {}) {
881
915
  var _a3;
882
916
  const headers = { "MCP-Protocol-Version": protocolVersion };
883
917
  const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
884
918
  for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
885
- const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
919
+ const response = await fetchWithCorsRetry(
920
+ endpointUrl,
921
+ headers,
922
+ fetchFn,
923
+ trustedOrigin
924
+ );
886
925
  if (!response) {
887
926
  continue;
888
927
  }
@@ -1048,7 +1087,10 @@ async function exchangeAuthorization(authorizationServerUrl, {
1048
1087
  var _a3;
1049
1088
  const grantType = "authorization_code";
1050
1089
  const tokenUrl = (metadata == null ? void 0 : metadata.token_endpoint) ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
1051
- assertSafeOAuthEndpoint(tokenUrl);
1090
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
1091
+ assertSafeOAuthEndpoint(tokenUrl, {
1092
+ allowLoopback: tokenUrl.origin === trustedOrigin
1093
+ });
1052
1094
  if ((metadata == null ? void 0 : metadata.grant_types_supported) && !metadata.grant_types_supported.includes(grantType)) {
1053
1095
  throw new Error(
1054
1096
  `Incompatible auth server: does not support grant type ${grantType}`
@@ -1082,11 +1124,15 @@ async function exchangeAuthorization(authorizationServerUrl, {
1082
1124
  if (resource) {
1083
1125
  params.set("resource", resourceUrlStripSlash(resource));
1084
1126
  }
1085
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
1086
- method: "POST",
1087
- headers,
1088
- body: params,
1089
- redirect: "error"
1127
+ const response = await fetchWithValidatedEndpoint({
1128
+ url: tokenUrl,
1129
+ init: {
1130
+ method: "POST",
1131
+ headers,
1132
+ body: params
1133
+ },
1134
+ fetch: fetchFn,
1135
+ trustedOrigin
1090
1136
  });
1091
1137
  if (!response.ok) {
1092
1138
  throw await parseErrorResponse(response);
@@ -1114,7 +1160,10 @@ async function refreshAuthorization(authorizationServerUrl, {
1114
1160
  } else {
1115
1161
  tokenUrl = new URL("/token", authorizationServerUrl);
1116
1162
  }
1117
- assertSafeOAuthEndpoint(tokenUrl);
1163
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
1164
+ assertSafeOAuthEndpoint(tokenUrl, {
1165
+ allowLoopback: tokenUrl.origin === trustedOrigin
1166
+ });
1118
1167
  const headers = new Headers({
1119
1168
  "Content-Type": "application/x-www-form-urlencoded",
1120
1169
  Accept: "application/json"
@@ -1141,11 +1190,15 @@ async function refreshAuthorization(authorizationServerUrl, {
1141
1190
  if (resource) {
1142
1191
  params.set("resource", resourceUrlStripSlash(resource));
1143
1192
  }
1144
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
1145
- method: "POST",
1146
- headers,
1147
- body: params,
1148
- redirect: "error"
1193
+ const response = await fetchWithValidatedEndpoint({
1194
+ url: tokenUrl,
1195
+ init: {
1196
+ method: "POST",
1197
+ headers,
1198
+ body: params
1199
+ },
1200
+ fetch: fetchFn,
1201
+ trustedOrigin
1149
1202
  });
1150
1203
  if (!response.ok) {
1151
1204
  throw await parseErrorResponse(response);
@@ -1172,18 +1225,25 @@ async function registerClient(authorizationServerUrl, {
1172
1225
  } else {
1173
1226
  registrationUrl = new URL("/register", authorizationServerUrl);
1174
1227
  }
1175
- assertSafeOAuthEndpoint(registrationUrl);
1228
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
1229
+ assertSafeOAuthEndpoint(registrationUrl, {
1230
+ allowLoopback: registrationUrl.origin === trustedOrigin
1231
+ });
1176
1232
  const applicationType = (_a3 = clientMetadata.application_type) != null ? _a3 : inferOAuthApplicationType(clientMetadata.redirect_uris);
1177
- const response = await (fetchFn != null ? fetchFn : fetch)(registrationUrl, {
1178
- method: "POST",
1179
- headers: {
1180
- "Content-Type": "application/json"
1233
+ const response = await fetchWithValidatedEndpoint({
1234
+ url: registrationUrl,
1235
+ init: {
1236
+ method: "POST",
1237
+ headers: {
1238
+ "Content-Type": "application/json"
1239
+ },
1240
+ body: JSON.stringify({
1241
+ ...clientMetadata,
1242
+ application_type: applicationType
1243
+ })
1181
1244
  },
1182
- body: JSON.stringify({
1183
- ...clientMetadata,
1184
- application_type: applicationType
1185
- }),
1186
- redirect: "error"
1245
+ fetch: fetchFn,
1246
+ trustedOrigin
1187
1247
  });
1188
1248
  if (!response.ok) {
1189
1249
  throw await parseErrorResponse(response);
@@ -1260,6 +1320,14 @@ async function authInternal(provider, {
1260
1320
  if (!authorizationServerUrl) {
1261
1321
  authorizationServerUrl = serverUrl;
1262
1322
  }
1323
+ const parsedServerUrl = new URL(serverUrl);
1324
+ const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
1325
+ const serverOrigin = parsedServerUrl.origin;
1326
+ const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
1327
+ const trustedAuthorizationServerOrigin = authorizationServerOrigin === serverOrigin || isOAuthLoopbackHost(parsedServerUrl.hostname) && isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname) ? authorizationServerOrigin : void 0;
1328
+ if (!trustedAuthorizationServerOrigin) {
1329
+ assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
1330
+ }
1263
1331
  const resource = await selectResourceURL(
1264
1332
  serverUrl,
1265
1333
  provider,
@@ -1273,7 +1341,8 @@ async function authInternal(provider, {
1273
1341
  const metadata = await discoverAuthorizationServerMetadata(
1274
1342
  authorizationServerUrl,
1275
1343
  {
1276
- fetchFn
1344
+ fetchFn,
1345
+ trustedOrigin: trustedAuthorizationServerOrigin
1277
1346
  }
1278
1347
  );
1279
1348
  const currentAuthorizationServerInformation = createAuthorizationServerInformation(authorizationServerUrl, metadata);