@ai-sdk/mcp 1.0.89 → 1.0.91

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,24 @@
1
1
  # @ai-sdk/mcp
2
2
 
3
+ ## 1.0.91
4
+
5
+ ### Patch Changes
6
+
7
+ - 5267a7a: Preserve stored OAuth credentials when an authorization-code exchange returns `invalid_grant`, without retrying the rejected code.
8
+
9
+ Add optional token context to `OAuthClientProvider.invalidateCredentials` so providers sharing storage can atomically delete only the rejected token generation and preserve tokens saved by a concurrent refresh.
10
+
11
+ ## 1.0.90
12
+
13
+ ### Patch Changes
14
+
15
+ - 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.
16
+ - a9cea74: fix(mcp): prevent SSRF in OAuth metadata discovery
17
+ - Updated dependencies [a9cea74]
18
+ - Updated dependencies [a9cea74]
19
+ - Updated dependencies [a9cea74]
20
+ - @ai-sdk/provider-utils@4.0.57
21
+
3
22
  ## 1.0.89
4
23
 
5
24
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -184,8 +184,15 @@ interface OAuthClientProvider {
184
184
  * If implemented, provides a way for the client to invalidate (e.g. delete) the specified
185
185
  * credentials, in the case where the server has indicated that they are no longer valid.
186
186
  * This avoids requiring the user to intervene manually.
187
+ *
188
+ * For token invalidation, context identifies the token generation being invalidated
189
+ * when available. Providers sharing storage must atomically compare and delete
190
+ * that generation so a concurrent refresh's tokens are preserved. Omitting or
191
+ * ignoring context retains unconditional invalidation.
187
192
  */
188
- invalidateCredentials?(scope: 'all' | 'client' | 'tokens' | 'verifier'): void | Promise<void>;
193
+ invalidateCredentials?(scope: 'all' | 'client' | 'tokens' | 'verifier', context?: {
194
+ tokens: OAuthTokens;
195
+ }): void | Promise<void>;
189
196
  get redirectUrl(): string | URL;
190
197
  get clientMetadata(): OAuthClientMetadata;
191
198
  clientInformation(): OAuthClientInformation | undefined | Promise<OAuthClientInformation | undefined>;
package/dist/index.d.ts CHANGED
@@ -184,8 +184,15 @@ interface OAuthClientProvider {
184
184
  * If implemented, provides a way for the client to invalidate (e.g. delete) the specified
185
185
  * credentials, in the case where the server has indicated that they are no longer valid.
186
186
  * This avoids requiring the user to intervene manually.
187
+ *
188
+ * For token invalidation, context identifies the token generation being invalidated
189
+ * when available. Providers sharing storage must atomically compare and delete
190
+ * that generation so a concurrent refresh's tokens are preserved. Omitting or
191
+ * ignoring context retains unconditional invalidation.
187
192
  */
188
- invalidateCredentials?(scope: 'all' | 'client' | 'tokens' | 'verifier'): void | Promise<void>;
193
+ invalidateCredentials?(scope: 'all' | 'client' | 'tokens' | 'verifier', context?: {
194
+ tokens: OAuthTokens;
195
+ }): void | Promise<void>;
189
196
  get redirectUrl(): string | URL;
190
197
  get clientMetadata(): OAuthClientMetadata;
191
198
  clientInformation(): OAuthClientInformation | undefined | Promise<OAuthClientInformation | undefined>;
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);
@@ -1157,8 +1215,9 @@ async function registerClient(authorizationServerUrl, {
1157
1215
  }
1158
1216
  async function auth(provider, options) {
1159
1217
  var _a3, _b3, _c;
1218
+ const refreshAttempt = {};
1160
1219
  try {
1161
- return await authInternal(provider, options);
1220
+ return await authInternal(provider, options, refreshAttempt);
1162
1221
  } catch (error) {
1163
1222
  if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
1164
1223
  if (options.authorizationCode !== void 0 || !await ((_a3 = provider.isClientInformationDynamicallyRegistered) == null ? void 0 : _a3.call(provider))) {
@@ -1166,8 +1225,10 @@ async function auth(provider, options) {
1166
1225
  }
1167
1226
  await ((_b3 = provider.invalidateCredentials) == null ? void 0 : _b3.call(provider, "all"));
1168
1227
  return await authInternal(provider, options);
1169
- } else if (error instanceof InvalidGrantError) {
1170
- await ((_c = provider.invalidateCredentials) == null ? void 0 : _c.call(provider, "tokens"));
1228
+ } else if (error instanceof InvalidGrantError && refreshAttempt.tokens) {
1229
+ await ((_c = provider.invalidateCredentials) == null ? void 0 : _c.call(provider, "tokens", {
1230
+ tokens: refreshAttempt.tokens
1231
+ }));
1171
1232
  return await authInternal(provider, options);
1172
1233
  }
1173
1234
  throw error;
@@ -1201,7 +1262,7 @@ async function authInternal(provider, {
1201
1262
  scope,
1202
1263
  resourceMetadataUrl,
1203
1264
  fetchFn
1204
- }) {
1265
+ }, refreshAttempt) {
1205
1266
  var _a3, _b3, _c;
1206
1267
  let resourceMetadata;
1207
1268
  let authorizationServerUrl;
@@ -1231,6 +1292,14 @@ async function authInternal(provider, {
1231
1292
  if (!authorizationServerUrl) {
1232
1293
  authorizationServerUrl = (_a3 = callbackAuthorizationServerInformation == null ? void 0 : callbackAuthorizationServerInformation.authorizationServerUrl) != null ? _a3 : serverUrl;
1233
1294
  }
1295
+ const parsedServerUrl = new URL(serverUrl);
1296
+ const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
1297
+ const serverOrigin = parsedServerUrl.origin;
1298
+ const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
1299
+ const trustedAuthorizationServerOrigin = authorizationServerOrigin === serverOrigin || isOAuthLoopbackHost(parsedServerUrl.hostname) && isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname) ? authorizationServerOrigin : void 0;
1300
+ if (!trustedAuthorizationServerOrigin) {
1301
+ assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
1302
+ }
1234
1303
  const resource = await selectResourceURL(
1235
1304
  serverUrl,
1236
1305
  provider,
@@ -1244,7 +1313,8 @@ async function authInternal(provider, {
1244
1313
  const metadata = await discoverAuthorizationServerMetadata(
1245
1314
  authorizationServerUrl,
1246
1315
  {
1247
- fetchFn
1316
+ fetchFn,
1317
+ trustedOrigin: trustedAuthorizationServerOrigin
1248
1318
  }
1249
1319
  );
1250
1320
  const currentAuthorizationServerInformation = createAuthorizationServerInformation(authorizationServerUrl, metadata);
@@ -1336,10 +1406,15 @@ async function authInternal(provider, {
1336
1406
  currentAuthorizationServerInformation
1337
1407
  });
1338
1408
  } else {
1339
- await ((_c = provider.invalidateCredentials) == null ? void 0 : _c.call(provider, "tokens"));
1409
+ await ((_c = provider.invalidateCredentials) == null ? void 0 : _c.call(provider, "tokens", {
1410
+ tokens: { ...tokens }
1411
+ }));
1340
1412
  }
1341
1413
  try {
1342
1414
  if (storedAuthorizationServerInformation) {
1415
+ if (refreshAttempt) {
1416
+ refreshAttempt.tokens = { ...tokens };
1417
+ }
1343
1418
  const newTokens = await refreshAuthorization(authorizationServerUrl, {
1344
1419
  metadata,
1345
1420
  clientInformation,