@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.91",
|
|
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,
|
|
@@ -78,9 +80,15 @@ export interface OAuthClientProvider {
|
|
|
78
80
|
* If implemented, provides a way for the client to invalidate (e.g. delete) the specified
|
|
79
81
|
* credentials, in the case where the server has indicated that they are no longer valid.
|
|
80
82
|
* This avoids requiring the user to intervene manually.
|
|
83
|
+
*
|
|
84
|
+
* For token invalidation, context identifies the token generation being invalidated
|
|
85
|
+
* when available. Providers sharing storage must atomically compare and delete
|
|
86
|
+
* that generation so a concurrent refresh's tokens are preserved. Omitting or
|
|
87
|
+
* ignoring context retains unconditional invalidation.
|
|
81
88
|
*/
|
|
82
89
|
invalidateCredentials?(
|
|
83
90
|
scope: 'all' | 'client' | 'tokens' | 'verifier',
|
|
91
|
+
context?: { tokens: OAuthTokens },
|
|
84
92
|
): void | Promise<void>;
|
|
85
93
|
get redirectUrl(): string | URL;
|
|
86
94
|
get clientMetadata(): OAuthClientMetadata;
|
|
@@ -149,16 +157,20 @@ function isOAuthLoopbackHost(hostname: string): boolean {
|
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/**
|
|
152
|
-
* Guards metadata-derived
|
|
153
|
-
*
|
|
154
|
-
*
|
|
160
|
+
* Guards metadata-derived OAuth URLs before they are requested. Loopback is
|
|
161
|
+
* allowed only when the caller has established that it belongs to a locally
|
|
162
|
+
* configured OAuth server; every other target uses the shared URL guard.
|
|
155
163
|
*
|
|
156
|
-
* Credential POSTs use `
|
|
157
|
-
* `
|
|
158
|
-
*
|
|
164
|
+
* Credential POSTs use `fetchWithValidatedEndpoint`, which enforces
|
|
165
|
+
* `redirect: 'error'` so the authorization code, PKCE verifier, and client
|
|
166
|
+
* secret are never replayed to a redirect target.
|
|
159
167
|
*/
|
|
160
|
-
function assertSafeOAuthEndpoint(
|
|
168
|
+
function assertSafeOAuthEndpoint(
|
|
169
|
+
endpointUrl: URL,
|
|
170
|
+
{ allowLoopback = false }: { allowLoopback?: boolean } = {},
|
|
171
|
+
): void {
|
|
161
172
|
if (
|
|
173
|
+
allowLoopback &&
|
|
162
174
|
(endpointUrl.protocol === 'http:' || endpointUrl.protocol === 'https:') &&
|
|
163
175
|
isOAuthLoopbackHost(endpointUrl.hostname)
|
|
164
176
|
) {
|
|
@@ -175,6 +187,13 @@ function assertSafeOAuthEndpoint(endpointUrl: URL): void {
|
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
189
|
|
|
190
|
+
function getTrustedLoopbackOrigin(
|
|
191
|
+
authorizationServerUrl: string | URL,
|
|
192
|
+
): string | undefined {
|
|
193
|
+
const url = new URL(authorizationServerUrl);
|
|
194
|
+
return isOAuthLoopbackHost(url.hostname) ? url.origin : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
function createAuthorizationServerInformation(
|
|
179
198
|
authorizationServerUrl: string | URL,
|
|
180
199
|
metadata?: AuthorizationServerMetadata,
|
|
@@ -408,13 +427,31 @@ async function fetchWithCorsRetry(
|
|
|
408
427
|
url: URL,
|
|
409
428
|
headers?: Record<string, string>,
|
|
410
429
|
fetchFn: FetchFunction = fetch,
|
|
430
|
+
trustedOrigin?: string,
|
|
411
431
|
): Promise<Response | undefined> {
|
|
412
432
|
try {
|
|
413
|
-
return await
|
|
433
|
+
return await fetchWithValidatedRedirects({
|
|
434
|
+
url: url.href,
|
|
435
|
+
fetch: async (input, init) =>
|
|
436
|
+
fetchWithValidatedEndpoint({
|
|
437
|
+
url: input as string | URL,
|
|
438
|
+
init: {
|
|
439
|
+
...init,
|
|
440
|
+
headers: new Headers(init?.headers).has('MCP-Protocol-Version')
|
|
441
|
+
? headers
|
|
442
|
+
: undefined,
|
|
443
|
+
},
|
|
444
|
+
fetch: fetchFn,
|
|
445
|
+
trustedOrigin,
|
|
446
|
+
redirect: 'manual',
|
|
447
|
+
}),
|
|
448
|
+
headers,
|
|
449
|
+
trustedOrigin,
|
|
450
|
+
});
|
|
414
451
|
} catch (error) {
|
|
415
452
|
if (error instanceof TypeError) {
|
|
416
453
|
if (headers) {
|
|
417
|
-
return fetchWithCorsRetry(url, undefined, fetchFn);
|
|
454
|
+
return fetchWithCorsRetry(url, undefined, fetchFn, trustedOrigin);
|
|
418
455
|
} else {
|
|
419
456
|
return undefined;
|
|
420
457
|
}
|
|
@@ -430,11 +467,12 @@ async function tryMetadataDiscovery(
|
|
|
430
467
|
url: URL,
|
|
431
468
|
protocolVersion: string,
|
|
432
469
|
fetchFn: FetchFunction = fetch,
|
|
470
|
+
trustedOrigin?: string,
|
|
433
471
|
): Promise<Response | undefined> {
|
|
434
472
|
const headers = {
|
|
435
473
|
'MCP-Protocol-Version': protocolVersion,
|
|
436
474
|
};
|
|
437
|
-
return await fetchWithCorsRetry(url, headers, fetchFn);
|
|
475
|
+
return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
|
|
438
476
|
}
|
|
439
477
|
|
|
440
478
|
/**
|
|
@@ -461,6 +499,7 @@ async function discoverMetadataWithFallback(
|
|
|
461
499
|
protocolVersion?: string;
|
|
462
500
|
metadataUrl?: string | URL;
|
|
463
501
|
metadataServerUrl?: string | URL;
|
|
502
|
+
trustedOrigin?: string;
|
|
464
503
|
},
|
|
465
504
|
): Promise<Response | undefined> {
|
|
466
505
|
const issuer = new URL(serverUrl);
|
|
@@ -475,11 +514,21 @@ async function discoverMetadataWithFallback(
|
|
|
475
514
|
url.search = issuer.search;
|
|
476
515
|
}
|
|
477
516
|
|
|
478
|
-
let response = await tryMetadataDiscovery(
|
|
517
|
+
let response = await tryMetadataDiscovery(
|
|
518
|
+
url,
|
|
519
|
+
protocolVersion,
|
|
520
|
+
fetchFn,
|
|
521
|
+
opts?.trustedOrigin,
|
|
522
|
+
);
|
|
479
523
|
|
|
480
524
|
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
|
|
481
525
|
const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
|
|
482
|
-
response = await tryMetadataDiscovery(
|
|
526
|
+
response = await tryMetadataDiscovery(
|
|
527
|
+
rootUrl,
|
|
528
|
+
protocolVersion,
|
|
529
|
+
fetchFn,
|
|
530
|
+
opts?.trustedOrigin,
|
|
531
|
+
);
|
|
483
532
|
}
|
|
484
533
|
|
|
485
534
|
return response;
|
|
@@ -497,6 +546,9 @@ export async function discoverOAuthProtectedResourceMetadata(
|
|
|
497
546
|
{
|
|
498
547
|
protocolVersion: opts?.protocolVersion,
|
|
499
548
|
metadataUrl: opts?.resourceMetadataUrl,
|
|
549
|
+
// The configured MCP server is trusted as a request target. Redirects
|
|
550
|
+
// crossing its origin are still validated before they are followed.
|
|
551
|
+
trustedOrigin: new URL(serverUrl).origin,
|
|
500
552
|
},
|
|
501
553
|
);
|
|
502
554
|
|
|
@@ -609,9 +661,11 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
609
661
|
{
|
|
610
662
|
fetchFn = fetch,
|
|
611
663
|
protocolVersion = LATEST_PROTOCOL_VERSION,
|
|
664
|
+
trustedOrigin,
|
|
612
665
|
}: {
|
|
613
666
|
fetchFn?: FetchFunction;
|
|
614
667
|
protocolVersion?: string;
|
|
668
|
+
trustedOrigin?: string;
|
|
615
669
|
} = {},
|
|
616
670
|
): Promise<AuthorizationServerMetadata | undefined> {
|
|
617
671
|
const headers = { 'MCP-Protocol-Version': protocolVersion };
|
|
@@ -619,7 +673,12 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
619
673
|
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
620
674
|
|
|
621
675
|
for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
|
|
622
|
-
const response = await fetchWithCorsRetry(
|
|
676
|
+
const response = await fetchWithCorsRetry(
|
|
677
|
+
endpointUrl,
|
|
678
|
+
headers,
|
|
679
|
+
fetchFn,
|
|
680
|
+
trustedOrigin,
|
|
681
|
+
);
|
|
623
682
|
|
|
624
683
|
if (!response) {
|
|
625
684
|
/**
|
|
@@ -929,7 +988,10 @@ export async function exchangeAuthorization(
|
|
|
929
988
|
const tokenUrl = metadata?.token_endpoint
|
|
930
989
|
? new URL(metadata.token_endpoint)
|
|
931
990
|
: new URL('/token', authorizationServerUrl);
|
|
932
|
-
|
|
991
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
992
|
+
assertSafeOAuthEndpoint(tokenUrl, {
|
|
993
|
+
allowLoopback: tokenUrl.origin === trustedOrigin,
|
|
994
|
+
});
|
|
933
995
|
|
|
934
996
|
if (
|
|
935
997
|
metadata?.grant_types_supported &&
|
|
@@ -973,11 +1035,15 @@ export async function exchangeAuthorization(
|
|
|
973
1035
|
params.set('resource', resourceUrlStripSlash(resource));
|
|
974
1036
|
}
|
|
975
1037
|
|
|
976
|
-
const response = await (
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1038
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1039
|
+
url: tokenUrl,
|
|
1040
|
+
init: {
|
|
1041
|
+
method: 'POST',
|
|
1042
|
+
headers,
|
|
1043
|
+
body: params,
|
|
1044
|
+
},
|
|
1045
|
+
fetch: fetchFn,
|
|
1046
|
+
trustedOrigin,
|
|
981
1047
|
});
|
|
982
1048
|
|
|
983
1049
|
if (!response.ok) {
|
|
@@ -1034,7 +1100,10 @@ export async function refreshAuthorization(
|
|
|
1034
1100
|
} else {
|
|
1035
1101
|
tokenUrl = new URL('/token', authorizationServerUrl);
|
|
1036
1102
|
}
|
|
1037
|
-
|
|
1103
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1104
|
+
assertSafeOAuthEndpoint(tokenUrl, {
|
|
1105
|
+
allowLoopback: tokenUrl.origin === trustedOrigin,
|
|
1106
|
+
});
|
|
1038
1107
|
|
|
1039
1108
|
const headers = new Headers({
|
|
1040
1109
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1067,11 +1136,15 @@ export async function refreshAuthorization(
|
|
|
1067
1136
|
params.set('resource', resourceUrlStripSlash(resource));
|
|
1068
1137
|
}
|
|
1069
1138
|
|
|
1070
|
-
const response = await (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1139
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1140
|
+
url: tokenUrl,
|
|
1141
|
+
init: {
|
|
1142
|
+
method: 'POST',
|
|
1143
|
+
headers,
|
|
1144
|
+
body: params,
|
|
1145
|
+
},
|
|
1146
|
+
fetch: fetchFn,
|
|
1147
|
+
trustedOrigin,
|
|
1075
1148
|
});
|
|
1076
1149
|
if (!response.ok) {
|
|
1077
1150
|
throw await parseErrorResponse(response);
|
|
@@ -1111,15 +1184,22 @@ export async function registerClient(
|
|
|
1111
1184
|
} else {
|
|
1112
1185
|
registrationUrl = new URL('/register', authorizationServerUrl);
|
|
1113
1186
|
}
|
|
1114
|
-
|
|
1187
|
+
const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
|
|
1188
|
+
assertSafeOAuthEndpoint(registrationUrl, {
|
|
1189
|
+
allowLoopback: registrationUrl.origin === trustedOrigin,
|
|
1190
|
+
});
|
|
1115
1191
|
|
|
1116
|
-
const response = await (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1192
|
+
const response = await fetchWithValidatedEndpoint({
|
|
1193
|
+
url: registrationUrl,
|
|
1194
|
+
init: {
|
|
1195
|
+
method: 'POST',
|
|
1196
|
+
headers: {
|
|
1197
|
+
'Content-Type': 'application/json',
|
|
1198
|
+
},
|
|
1199
|
+
body: JSON.stringify(clientMetadata),
|
|
1120
1200
|
},
|
|
1121
|
-
|
|
1122
|
-
|
|
1201
|
+
fetch: fetchFn,
|
|
1202
|
+
trustedOrigin,
|
|
1123
1203
|
});
|
|
1124
1204
|
|
|
1125
1205
|
if (!response.ok) {
|
|
@@ -1140,8 +1220,9 @@ export async function auth(
|
|
|
1140
1220
|
fetchFn?: FetchFunction;
|
|
1141
1221
|
},
|
|
1142
1222
|
): Promise<AuthResult> {
|
|
1223
|
+
const refreshAttempt: { tokens?: OAuthTokens } = {};
|
|
1143
1224
|
try {
|
|
1144
|
-
return await authInternal(provider, options);
|
|
1225
|
+
return await authInternal(provider, options, refreshAttempt);
|
|
1145
1226
|
} catch (error) {
|
|
1146
1227
|
if (
|
|
1147
1228
|
error instanceof InvalidClientError ||
|
|
@@ -1156,8 +1237,11 @@ export async function auth(
|
|
|
1156
1237
|
|
|
1157
1238
|
await provider.invalidateCredentials?.('all');
|
|
1158
1239
|
return await authInternal(provider, options);
|
|
1159
|
-
} else if (error instanceof InvalidGrantError) {
|
|
1160
|
-
|
|
1240
|
+
} else if (error instanceof InvalidGrantError && refreshAttempt.tokens) {
|
|
1241
|
+
// Only invalidate the tokens used by a failed refresh.
|
|
1242
|
+
await provider.invalidateCredentials?.('tokens', {
|
|
1243
|
+
tokens: refreshAttempt.tokens,
|
|
1244
|
+
});
|
|
1161
1245
|
return await authInternal(provider, options);
|
|
1162
1246
|
}
|
|
1163
1247
|
|
|
@@ -1213,6 +1297,7 @@ async function authInternal(
|
|
|
1213
1297
|
resourceMetadataUrl?: URL;
|
|
1214
1298
|
fetchFn?: FetchFunction;
|
|
1215
1299
|
},
|
|
1300
|
+
refreshAttempt?: { tokens?: OAuthTokens },
|
|
1216
1301
|
): Promise<AuthResult> {
|
|
1217
1302
|
let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
|
|
1218
1303
|
let authorizationServerUrl: string | URL | undefined;
|
|
@@ -1262,6 +1347,24 @@ async function authInternal(
|
|
|
1262
1347
|
serverUrl;
|
|
1263
1348
|
}
|
|
1264
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
|
+
|
|
1265
1368
|
/** Validate and select the resource value sent to the AS */
|
|
1266
1369
|
const resource: URL | undefined = await selectResourceURL(
|
|
1267
1370
|
serverUrl,
|
|
@@ -1280,6 +1383,7 @@ async function authInternal(
|
|
|
1280
1383
|
authorizationServerUrl,
|
|
1281
1384
|
{
|
|
1282
1385
|
fetchFn,
|
|
1386
|
+
trustedOrigin: trustedAuthorizationServerOrigin,
|
|
1283
1387
|
},
|
|
1284
1388
|
);
|
|
1285
1389
|
const currentAuthorizationServerInformation =
|
|
@@ -1390,11 +1494,16 @@ async function authInternal(
|
|
|
1390
1494
|
currentAuthorizationServerInformation,
|
|
1391
1495
|
});
|
|
1392
1496
|
} else {
|
|
1393
|
-
await provider.invalidateCredentials?.('tokens'
|
|
1497
|
+
await provider.invalidateCredentials?.('tokens', {
|
|
1498
|
+
tokens: { ...tokens },
|
|
1499
|
+
});
|
|
1394
1500
|
}
|
|
1395
1501
|
|
|
1396
1502
|
try {
|
|
1397
1503
|
if (storedAuthorizationServerInformation) {
|
|
1504
|
+
if (refreshAttempt) {
|
|
1505
|
+
refreshAttempt.tokens = { ...tokens };
|
|
1506
|
+
}
|
|
1398
1507
|
// Attempt to refresh the token
|
|
1399
1508
|
const newTokens = await refreshAuthorization(authorizationServerUrl, {
|
|
1400
1509
|
metadata,
|