@ai-sdk/mcp 2.0.47 → 2.0.49
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 +20 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ai-sdk/provider": "4.0.
|
|
35
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
34
|
+
"@ai-sdk/provider": "4.0.14",
|
|
35
|
+
"@ai-sdk/provider-utils": "5.0.40",
|
|
36
36
|
"cross-spawn": "^7.0.6",
|
|
37
37
|
"pkce-challenge": "^5.0.1"
|
|
38
38
|
},
|
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,
|
|
@@ -140,16 +142,20 @@ function isOAuthLoopbackHost(hostname: string): boolean {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/**
|
|
143
|
-
* Guards metadata-derived
|
|
144
|
-
*
|
|
145
|
-
*
|
|
145
|
+
* Guards metadata-derived OAuth URLs before they are requested. Loopback is
|
|
146
|
+
* allowed only when the caller has established that it belongs to a locally
|
|
147
|
+
* configured OAuth server; every other target uses the shared URL guard.
|
|
146
148
|
*
|
|
147
|
-
* Credential POSTs use `
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
149
|
+
* Credential POSTs use `fetchWithValidatedEndpoint`, which enforces
|
|
150
|
+
* `redirect: 'error'` so the authorization code, PKCE verifier, and client
|
|
151
|
+
* secret are never replayed to a redirect target.
|
|
150
152
|
*/
|
|
151
|
-
function assertSafeOAuthEndpoint(
|
|
153
|
+
function assertSafeOAuthEndpoint(
|
|
154
|
+
endpointUrl: URL,
|
|
155
|
+
{ allowLoopback = false }: { allowLoopback?: boolean } = {},
|
|
156
|
+
): void {
|
|
152
157
|
if (
|
|
158
|
+
allowLoopback &&
|
|
153
159
|
(endpointUrl.protocol === 'http:' || endpointUrl.protocol === 'https:') &&
|
|
154
160
|
isOAuthLoopbackHost(endpointUrl.hostname)
|
|
155
161
|
) {
|
|
@@ -166,6 +172,13 @@ function assertSafeOAuthEndpoint(endpointUrl: URL): void {
|
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
function getTrustedLoopbackOrigin(
|
|
176
|
+
authorizationServerUrl: string | URL,
|
|
177
|
+
): string | undefined {
|
|
178
|
+
const url = new URL(authorizationServerUrl);
|
|
179
|
+
return isOAuthLoopbackHost(url.hostname) ? url.origin : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
function validateAuthorizationResponseIssuer({
|
|
170
183
|
callbackIssuer,
|
|
171
184
|
expectedIssuer,
|
|
@@ -423,13 +436,31 @@ async function fetchWithCorsRetry(
|
|
|
423
436
|
url: URL,
|
|
424
437
|
headers?: Record<string, string>,
|
|
425
438
|
fetchFn: FetchFunction = fetch,
|
|
439
|
+
trustedOrigin?: string,
|
|
426
440
|
): Promise<Response | undefined> {
|
|
427
441
|
try {
|
|
428
|
-
return await
|
|
442
|
+
return await fetchWithValidatedRedirects({
|
|
443
|
+
url: url.href,
|
|
444
|
+
fetch: async (input, init) =>
|
|
445
|
+
fetchWithValidatedEndpoint({
|
|
446
|
+
url: input as string | URL,
|
|
447
|
+
init: {
|
|
448
|
+
...init,
|
|
449
|
+
headers: new Headers(init?.headers).has('MCP-Protocol-Version')
|
|
450
|
+
? headers
|
|
451
|
+
: undefined,
|
|
452
|
+
},
|
|
453
|
+
fetch: fetchFn,
|
|
454
|
+
trustedOrigin,
|
|
455
|
+
redirect: 'manual',
|
|
456
|
+
}),
|
|
457
|
+
headers,
|
|
458
|
+
trustedOrigin,
|
|
459
|
+
});
|
|
429
460
|
} catch (error) {
|
|
430
461
|
if (error instanceof TypeError) {
|
|
431
462
|
if (headers) {
|
|
432
|
-
return fetchWithCorsRetry(url, undefined, fetchFn);
|
|
463
|
+
return fetchWithCorsRetry(url, undefined, fetchFn, trustedOrigin);
|
|
433
464
|
} else {
|
|
434
465
|
return undefined;
|
|
435
466
|
}
|
|
@@ -445,11 +476,12 @@ async function tryMetadataDiscovery(
|
|
|
445
476
|
url: URL,
|
|
446
477
|
protocolVersion: string,
|
|
447
478
|
fetchFn: FetchFunction = fetch,
|
|
479
|
+
trustedOrigin?: string,
|
|
448
480
|
): Promise<Response | undefined> {
|
|
449
481
|
const headers = {
|
|
450
482
|
'MCP-Protocol-Version': protocolVersion,
|
|
451
483
|
};
|
|
452
|
-
return await fetchWithCorsRetry(url, headers, fetchFn);
|
|
484
|
+
return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
|
|
453
485
|
}
|
|
454
486
|
|
|
455
487
|
/**
|
|
@@ -476,6 +508,7 @@ async function discoverMetadataWithFallback(
|
|
|
476
508
|
protocolVersion?: string;
|
|
477
509
|
metadataUrl?: string | URL;
|
|
478
510
|
metadataServerUrl?: string | URL;
|
|
511
|
+
trustedOrigin?: string;
|
|
479
512
|
},
|
|
480
513
|
): Promise<Response | undefined> {
|
|
481
514
|
const issuer = new URL(serverUrl);
|
|
@@ -490,11 +523,21 @@ async function discoverMetadataWithFallback(
|
|
|
490
523
|
url.search = issuer.search;
|
|
491
524
|
}
|
|
492
525
|
|
|
493
|
-
let response = await tryMetadataDiscovery(
|
|
526
|
+
let response = await tryMetadataDiscovery(
|
|
527
|
+
url,
|
|
528
|
+
protocolVersion,
|
|
529
|
+
fetchFn,
|
|
530
|
+
opts?.trustedOrigin,
|
|
531
|
+
);
|
|
494
532
|
|
|
495
533
|
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
|
|
496
534
|
const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
|
|
497
|
-
response = await tryMetadataDiscovery(
|
|
535
|
+
response = await tryMetadataDiscovery(
|
|
536
|
+
rootUrl,
|
|
537
|
+
protocolVersion,
|
|
538
|
+
fetchFn,
|
|
539
|
+
opts?.trustedOrigin,
|
|
540
|
+
);
|
|
498
541
|
}
|
|
499
542
|
|
|
500
543
|
return response;
|
|
@@ -512,6 +555,9 @@ export async function discoverOAuthProtectedResourceMetadata(
|
|
|
512
555
|
{
|
|
513
556
|
protocolVersion: opts?.protocolVersion,
|
|
514
557
|
metadataUrl: opts?.resourceMetadataUrl,
|
|
558
|
+
// The configured MCP server is trusted as a request target. Redirects
|
|
559
|
+
// crossing its origin are still validated before they are followed.
|
|
560
|
+
trustedOrigin: new URL(serverUrl).origin,
|
|
515
561
|
},
|
|
516
562
|
);
|
|
517
563
|
|
|
@@ -624,9 +670,11 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
624
670
|
{
|
|
625
671
|
fetchFn = fetch,
|
|
626
672
|
protocolVersion = LATEST_PROTOCOL_VERSION,
|
|
673
|
+
trustedOrigin,
|
|
627
674
|
}: {
|
|
628
675
|
fetchFn?: FetchFunction;
|
|
629
676
|
protocolVersion?: string;
|
|
677
|
+
trustedOrigin?: string;
|
|
630
678
|
} = {},
|
|
631
679
|
): Promise<AuthorizationServerMetadata | undefined> {
|
|
632
680
|
const headers = { 'MCP-Protocol-Version': protocolVersion };
|
|
@@ -634,7 +682,12 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
634
682
|
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
635
683
|
|
|
636
684
|
for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
|
|
637
|
-
const response = await fetchWithCorsRetry(
|
|
685
|
+
const response = await fetchWithCorsRetry(
|
|
686
|
+
endpointUrl,
|
|
687
|
+
headers,
|
|
688
|
+
fetchFn,
|
|
689
|
+
trustedOrigin,
|
|
690
|
+
);
|
|
638
691
|
|
|
639
692
|
if (!response) {
|
|
640
693
|
/**
|
|
@@ -944,7 +997,10 @@ export async function exchangeAuthorization(
|
|
|
944
997
|
const tokenUrl = metadata?.token_endpoint
|
|
945
998
|
? new URL(metadata.token_endpoint)
|
|
946
999
|
: new URL('/token', authorizationServerUrl);
|
|
947
|
-
|
|
1000
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1001
|
+
assertSafeOAuthEndpoint(tokenUrl, {
|
|
1002
|
+
allowLoopback: tokenUrl.origin === trustedOrigin,
|
|
1003
|
+
});
|
|
948
1004
|
|
|
949
1005
|
if (
|
|
950
1006
|
metadata?.grant_types_supported &&
|
|
@@ -988,11 +1044,15 @@ export async function exchangeAuthorization(
|
|
|
988
1044
|
params.set('resource', resourceUrlStripSlash(resource));
|
|
989
1045
|
}
|
|
990
1046
|
|
|
991
|
-
const response = await (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1047
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1048
|
+
url: tokenUrl,
|
|
1049
|
+
init: {
|
|
1050
|
+
method: 'POST',
|
|
1051
|
+
headers,
|
|
1052
|
+
body: params,
|
|
1053
|
+
},
|
|
1054
|
+
fetch: fetchFn,
|
|
1055
|
+
trustedOrigin,
|
|
996
1056
|
});
|
|
997
1057
|
|
|
998
1058
|
if (!response.ok) {
|
|
@@ -1049,7 +1109,10 @@ export async function refreshAuthorization(
|
|
|
1049
1109
|
} else {
|
|
1050
1110
|
tokenUrl = new URL('/token', authorizationServerUrl);
|
|
1051
1111
|
}
|
|
1052
|
-
|
|
1112
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1113
|
+
assertSafeOAuthEndpoint(tokenUrl, {
|
|
1114
|
+
allowLoopback: tokenUrl.origin === trustedOrigin,
|
|
1115
|
+
});
|
|
1053
1116
|
|
|
1054
1117
|
const headers = new Headers({
|
|
1055
1118
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1082,11 +1145,15 @@ export async function refreshAuthorization(
|
|
|
1082
1145
|
params.set('resource', resourceUrlStripSlash(resource));
|
|
1083
1146
|
}
|
|
1084
1147
|
|
|
1085
|
-
const response = await (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1148
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1149
|
+
url: tokenUrl,
|
|
1150
|
+
init: {
|
|
1151
|
+
method: 'POST',
|
|
1152
|
+
headers,
|
|
1153
|
+
body: params,
|
|
1154
|
+
},
|
|
1155
|
+
fetch: fetchFn,
|
|
1156
|
+
trustedOrigin,
|
|
1090
1157
|
});
|
|
1091
1158
|
if (!response.ok) {
|
|
1092
1159
|
throw await parseErrorResponse(response);
|
|
@@ -1126,21 +1193,28 @@ export async function registerClient(
|
|
|
1126
1193
|
} else {
|
|
1127
1194
|
registrationUrl = new URL('/register', authorizationServerUrl);
|
|
1128
1195
|
}
|
|
1129
|
-
|
|
1196
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1197
|
+
assertSafeOAuthEndpoint(registrationUrl, {
|
|
1198
|
+
allowLoopback: registrationUrl.origin === trustedOrigin,
|
|
1199
|
+
});
|
|
1130
1200
|
|
|
1131
1201
|
const applicationType =
|
|
1132
1202
|
clientMetadata.application_type ??
|
|
1133
1203
|
inferOAuthApplicationType(clientMetadata.redirect_uris);
|
|
1134
|
-
const response = await (
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1204
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1205
|
+
url: registrationUrl,
|
|
1206
|
+
init: {
|
|
1207
|
+
method: 'POST',
|
|
1208
|
+
headers: {
|
|
1209
|
+
'Content-Type': 'application/json',
|
|
1210
|
+
},
|
|
1211
|
+
body: JSON.stringify({
|
|
1212
|
+
...clientMetadata,
|
|
1213
|
+
application_type: applicationType,
|
|
1214
|
+
}),
|
|
1138
1215
|
},
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
application_type: applicationType,
|
|
1142
|
-
}),
|
|
1143
|
-
redirect: 'error',
|
|
1216
|
+
fetch: fetchFn,
|
|
1217
|
+
trustedOrigin,
|
|
1144
1218
|
});
|
|
1145
1219
|
|
|
1146
1220
|
if (!response.ok) {
|
|
@@ -1273,6 +1347,24 @@ async function authInternal(
|
|
|
1273
1347
|
authorizationServerUrl = serverUrl;
|
|
1274
1348
|
}
|
|
1275
1349
|
|
|
1350
|
+
const parsedServerUrl = new URL(serverUrl);
|
|
1351
|
+
const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
|
|
1352
|
+
const serverOrigin = parsedServerUrl.origin;
|
|
1353
|
+
const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
|
|
1354
|
+
const trustedAuthorizationServerOrigin =
|
|
1355
|
+
authorizationServerOrigin === serverOrigin ||
|
|
1356
|
+
(isOAuthLoopbackHost(parsedServerUrl.hostname) &&
|
|
1357
|
+
isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname))
|
|
1358
|
+
? authorizationServerOrigin
|
|
1359
|
+
: undefined;
|
|
1360
|
+
|
|
1361
|
+
// An authorization server selected by response metadata is untrusted until
|
|
1362
|
+
// its target has passed the SSRF guard. A same-origin server is already the
|
|
1363
|
+
// developer-configured MCP request target.
|
|
1364
|
+
if (!trustedAuthorizationServerOrigin) {
|
|
1365
|
+
assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1276
1368
|
/** Validate and select the resource value sent to the AS */
|
|
1277
1369
|
const resource: URL | undefined = await selectResourceURL(
|
|
1278
1370
|
serverUrl,
|
|
@@ -1291,6 +1383,7 @@ async function authInternal(
|
|
|
1291
1383
|
authorizationServerUrl,
|
|
1292
1384
|
{
|
|
1293
1385
|
fetchFn,
|
|
1386
|
+
trustedOrigin: trustedAuthorizationServerOrigin,
|
|
1294
1387
|
},
|
|
1295
1388
|
);
|
|
1296
1389
|
const currentAuthorizationServerInformation =
|