@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 +11 -0
- package/dist/index.js +99 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/tool/oauth.ts +125 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.90",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"cross-spawn": "^7.0.6",
|
|
36
36
|
"pkce-challenge": "^5.0.0",
|
|
37
37
|
"@ai-sdk/provider": "3.0.18",
|
|
38
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
38
|
+
"@ai-sdk/provider-utils": "4.0.57"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/cross-spawn": "^6.0.6",
|
package/src/tool/oauth.ts
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
} from '../util/oauth-util';
|
|
29
29
|
import { LATEST_PROTOCOL_VERSION } from './types';
|
|
30
30
|
import {
|
|
31
|
+
fetchWithValidatedEndpoint,
|
|
32
|
+
fetchWithValidatedRedirects,
|
|
31
33
|
parseJSON,
|
|
32
34
|
validateDownloadUrl,
|
|
33
35
|
type FetchFunction,
|
|
@@ -149,16 +151,20 @@ function isOAuthLoopbackHost(hostname: string): boolean {
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/**
|
|
152
|
-
* Guards metadata-derived
|
|
153
|
-
*
|
|
154
|
-
*
|
|
154
|
+
* Guards metadata-derived OAuth URLs before they are requested. Loopback is
|
|
155
|
+
* allowed only when the caller has established that it belongs to a locally
|
|
156
|
+
* configured OAuth server; every other target uses the shared URL guard.
|
|
155
157
|
*
|
|
156
|
-
* Credential POSTs use `
|
|
157
|
-
* `
|
|
158
|
-
*
|
|
158
|
+
* Credential POSTs use `fetchWithValidatedEndpoint`, which enforces
|
|
159
|
+
* `redirect: 'error'` so the authorization code, PKCE verifier, and client
|
|
160
|
+
* secret are never replayed to a redirect target.
|
|
159
161
|
*/
|
|
160
|
-
function assertSafeOAuthEndpoint(
|
|
162
|
+
function assertSafeOAuthEndpoint(
|
|
163
|
+
endpointUrl: URL,
|
|
164
|
+
{ allowLoopback = false }: { allowLoopback?: boolean } = {},
|
|
165
|
+
): void {
|
|
161
166
|
if (
|
|
167
|
+
allowLoopback &&
|
|
162
168
|
(endpointUrl.protocol === 'http:' || endpointUrl.protocol === 'https:') &&
|
|
163
169
|
isOAuthLoopbackHost(endpointUrl.hostname)
|
|
164
170
|
) {
|
|
@@ -175,6 +181,13 @@ function assertSafeOAuthEndpoint(endpointUrl: URL): void {
|
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
function getTrustedLoopbackOrigin(
|
|
185
|
+
authorizationServerUrl: string | URL,
|
|
186
|
+
): string | undefined {
|
|
187
|
+
const url = new URL(authorizationServerUrl);
|
|
188
|
+
return isOAuthLoopbackHost(url.hostname) ? url.origin : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function createAuthorizationServerInformation(
|
|
179
192
|
authorizationServerUrl: string | URL,
|
|
180
193
|
metadata?: AuthorizationServerMetadata,
|
|
@@ -408,13 +421,31 @@ async function fetchWithCorsRetry(
|
|
|
408
421
|
url: URL,
|
|
409
422
|
headers?: Record<string, string>,
|
|
410
423
|
fetchFn: FetchFunction = fetch,
|
|
424
|
+
trustedOrigin?: string,
|
|
411
425
|
): Promise<Response | undefined> {
|
|
412
426
|
try {
|
|
413
|
-
return await
|
|
427
|
+
return await fetchWithValidatedRedirects({
|
|
428
|
+
url: url.href,
|
|
429
|
+
fetch: async (input, init) =>
|
|
430
|
+
fetchWithValidatedEndpoint({
|
|
431
|
+
url: input as string | URL,
|
|
432
|
+
init: {
|
|
433
|
+
...init,
|
|
434
|
+
headers: new Headers(init?.headers).has('MCP-Protocol-Version')
|
|
435
|
+
? headers
|
|
436
|
+
: undefined,
|
|
437
|
+
},
|
|
438
|
+
fetch: fetchFn,
|
|
439
|
+
trustedOrigin,
|
|
440
|
+
redirect: 'manual',
|
|
441
|
+
}),
|
|
442
|
+
headers,
|
|
443
|
+
trustedOrigin,
|
|
444
|
+
});
|
|
414
445
|
} catch (error) {
|
|
415
446
|
if (error instanceof TypeError) {
|
|
416
447
|
if (headers) {
|
|
417
|
-
return fetchWithCorsRetry(url, undefined, fetchFn);
|
|
448
|
+
return fetchWithCorsRetry(url, undefined, fetchFn, trustedOrigin);
|
|
418
449
|
} else {
|
|
419
450
|
return undefined;
|
|
420
451
|
}
|
|
@@ -430,11 +461,12 @@ async function tryMetadataDiscovery(
|
|
|
430
461
|
url: URL,
|
|
431
462
|
protocolVersion: string,
|
|
432
463
|
fetchFn: FetchFunction = fetch,
|
|
464
|
+
trustedOrigin?: string,
|
|
433
465
|
): Promise<Response | undefined> {
|
|
434
466
|
const headers = {
|
|
435
467
|
'MCP-Protocol-Version': protocolVersion,
|
|
436
468
|
};
|
|
437
|
-
return await fetchWithCorsRetry(url, headers, fetchFn);
|
|
469
|
+
return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
|
|
438
470
|
}
|
|
439
471
|
|
|
440
472
|
/**
|
|
@@ -461,6 +493,7 @@ async function discoverMetadataWithFallback(
|
|
|
461
493
|
protocolVersion?: string;
|
|
462
494
|
metadataUrl?: string | URL;
|
|
463
495
|
metadataServerUrl?: string | URL;
|
|
496
|
+
trustedOrigin?: string;
|
|
464
497
|
},
|
|
465
498
|
): Promise<Response | undefined> {
|
|
466
499
|
const issuer = new URL(serverUrl);
|
|
@@ -475,11 +508,21 @@ async function discoverMetadataWithFallback(
|
|
|
475
508
|
url.search = issuer.search;
|
|
476
509
|
}
|
|
477
510
|
|
|
478
|
-
let response = await tryMetadataDiscovery(
|
|
511
|
+
let response = await tryMetadataDiscovery(
|
|
512
|
+
url,
|
|
513
|
+
protocolVersion,
|
|
514
|
+
fetchFn,
|
|
515
|
+
opts?.trustedOrigin,
|
|
516
|
+
);
|
|
479
517
|
|
|
480
518
|
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
|
|
481
519
|
const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
|
|
482
|
-
response = await tryMetadataDiscovery(
|
|
520
|
+
response = await tryMetadataDiscovery(
|
|
521
|
+
rootUrl,
|
|
522
|
+
protocolVersion,
|
|
523
|
+
fetchFn,
|
|
524
|
+
opts?.trustedOrigin,
|
|
525
|
+
);
|
|
483
526
|
}
|
|
484
527
|
|
|
485
528
|
return response;
|
|
@@ -497,6 +540,9 @@ export async function discoverOAuthProtectedResourceMetadata(
|
|
|
497
540
|
{
|
|
498
541
|
protocolVersion: opts?.protocolVersion,
|
|
499
542
|
metadataUrl: opts?.resourceMetadataUrl,
|
|
543
|
+
// The configured MCP server is trusted as a request target. Redirects
|
|
544
|
+
// crossing its origin are still validated before they are followed.
|
|
545
|
+
trustedOrigin: new URL(serverUrl).origin,
|
|
500
546
|
},
|
|
501
547
|
);
|
|
502
548
|
|
|
@@ -609,9 +655,11 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
609
655
|
{
|
|
610
656
|
fetchFn = fetch,
|
|
611
657
|
protocolVersion = LATEST_PROTOCOL_VERSION,
|
|
658
|
+
trustedOrigin,
|
|
612
659
|
}: {
|
|
613
660
|
fetchFn?: FetchFunction;
|
|
614
661
|
protocolVersion?: string;
|
|
662
|
+
trustedOrigin?: string;
|
|
615
663
|
} = {},
|
|
616
664
|
): Promise<AuthorizationServerMetadata | undefined> {
|
|
617
665
|
const headers = { 'MCP-Protocol-Version': protocolVersion };
|
|
@@ -619,7 +667,12 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
619
667
|
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
620
668
|
|
|
621
669
|
for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
|
|
622
|
-
const response = await fetchWithCorsRetry(
|
|
670
|
+
const response = await fetchWithCorsRetry(
|
|
671
|
+
endpointUrl,
|
|
672
|
+
headers,
|
|
673
|
+
fetchFn,
|
|
674
|
+
trustedOrigin,
|
|
675
|
+
);
|
|
623
676
|
|
|
624
677
|
if (!response) {
|
|
625
678
|
/**
|
|
@@ -929,7 +982,10 @@ export async function exchangeAuthorization(
|
|
|
929
982
|
const tokenUrl = metadata?.token_endpoint
|
|
930
983
|
? new URL(metadata.token_endpoint)
|
|
931
984
|
: new URL('/token', authorizationServerUrl);
|
|
932
|
-
|
|
985
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
986
|
+
assertSafeOAuthEndpoint(tokenUrl, {
|
|
987
|
+
allowLoopback: tokenUrl.origin === trustedOrigin,
|
|
988
|
+
});
|
|
933
989
|
|
|
934
990
|
if (
|
|
935
991
|
metadata?.grant_types_supported &&
|
|
@@ -973,11 +1029,15 @@ export async function exchangeAuthorization(
|
|
|
973
1029
|
params.set('resource', resourceUrlStripSlash(resource));
|
|
974
1030
|
}
|
|
975
1031
|
|
|
976
|
-
const response = await (
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1032
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1033
|
+
url: tokenUrl,
|
|
1034
|
+
init: {
|
|
1035
|
+
method: 'POST',
|
|
1036
|
+
headers,
|
|
1037
|
+
body: params,
|
|
1038
|
+
},
|
|
1039
|
+
fetch: fetchFn,
|
|
1040
|
+
trustedOrigin,
|
|
981
1041
|
});
|
|
982
1042
|
|
|
983
1043
|
if (!response.ok) {
|
|
@@ -1034,7 +1094,10 @@ export async function refreshAuthorization(
|
|
|
1034
1094
|
} else {
|
|
1035
1095
|
tokenUrl = new URL('/token', authorizationServerUrl);
|
|
1036
1096
|
}
|
|
1037
|
-
|
|
1097
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1098
|
+
assertSafeOAuthEndpoint(tokenUrl, {
|
|
1099
|
+
allowLoopback: tokenUrl.origin === trustedOrigin,
|
|
1100
|
+
});
|
|
1038
1101
|
|
|
1039
1102
|
const headers = new Headers({
|
|
1040
1103
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1067,11 +1130,15 @@ export async function refreshAuthorization(
|
|
|
1067
1130
|
params.set('resource', resourceUrlStripSlash(resource));
|
|
1068
1131
|
}
|
|
1069
1132
|
|
|
1070
|
-
const response = await (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1133
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1134
|
+
url: tokenUrl,
|
|
1135
|
+
init: {
|
|
1136
|
+
method: 'POST',
|
|
1137
|
+
headers,
|
|
1138
|
+
body: params,
|
|
1139
|
+
},
|
|
1140
|
+
fetch: fetchFn,
|
|
1141
|
+
trustedOrigin,
|
|
1075
1142
|
});
|
|
1076
1143
|
if (!response.ok) {
|
|
1077
1144
|
throw await parseErrorResponse(response);
|
|
@@ -1111,15 +1178,22 @@ export async function registerClient(
|
|
|
1111
1178
|
} else {
|
|
1112
1179
|
registrationUrl = new URL('/register', authorizationServerUrl);
|
|
1113
1180
|
}
|
|
1114
|
-
|
|
1181
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1182
|
+
assertSafeOAuthEndpoint(registrationUrl, {
|
|
1183
|
+
allowLoopback: registrationUrl.origin === trustedOrigin,
|
|
1184
|
+
});
|
|
1115
1185
|
|
|
1116
|
-
const response = await (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1186
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1187
|
+
url: registrationUrl,
|
|
1188
|
+
init: {
|
|
1189
|
+
method: 'POST',
|
|
1190
|
+
headers: {
|
|
1191
|
+
'Content-Type': 'application/json',
|
|
1192
|
+
},
|
|
1193
|
+
body: JSON.stringify(clientMetadata),
|
|
1120
1194
|
},
|
|
1121
|
-
|
|
1122
|
-
|
|
1195
|
+
fetch: fetchFn,
|
|
1196
|
+
trustedOrigin,
|
|
1123
1197
|
});
|
|
1124
1198
|
|
|
1125
1199
|
if (!response.ok) {
|
|
@@ -1262,6 +1336,24 @@ async function authInternal(
|
|
|
1262
1336
|
serverUrl;
|
|
1263
1337
|
}
|
|
1264
1338
|
|
|
1339
|
+
const parsedServerUrl = new URL(serverUrl);
|
|
1340
|
+
const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
|
|
1341
|
+
const serverOrigin = parsedServerUrl.origin;
|
|
1342
|
+
const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
|
|
1343
|
+
const trustedAuthorizationServerOrigin =
|
|
1344
|
+
authorizationServerOrigin === serverOrigin ||
|
|
1345
|
+
(isOAuthLoopbackHost(parsedServerUrl.hostname) &&
|
|
1346
|
+
isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname))
|
|
1347
|
+
? authorizationServerOrigin
|
|
1348
|
+
: undefined;
|
|
1349
|
+
|
|
1350
|
+
// An authorization server selected by response metadata is untrusted until
|
|
1351
|
+
// its target has passed the SSRF guard. A same-origin server is already the
|
|
1352
|
+
// developer-configured MCP request target.
|
|
1353
|
+
if (!trustedAuthorizationServerOrigin) {
|
|
1354
|
+
assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1265
1357
|
/** Validate and select the resource value sent to the AS */
|
|
1266
1358
|
const resource: URL | undefined = await selectResourceURL(
|
|
1267
1359
|
serverUrl,
|
|
@@ -1280,6 +1372,7 @@ async function authInternal(
|
|
|
1280
1372
|
authorizationServerUrl,
|
|
1281
1373
|
{
|
|
1282
1374
|
fetchFn,
|
|
1375
|
+
trustedOrigin: trustedAuthorizationServerOrigin,
|
|
1283
1376
|
},
|
|
1284
1377
|
);
|
|
1285
1378
|
const currentAuthorizationServerInformation =
|