@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 +21 -0
- package/dist/index.js +104 -35
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/tool/oauth.ts +128 -35
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
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 (
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
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 (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
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);
|