@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 +19 -0
- package/dist/index.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +112 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +114 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/tool/oauth.ts +145 -36
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'
|
|
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'
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
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 (
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
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,
|