@ai-sdk/mcp 2.0.37 → 2.0.40
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 +25 -0
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/tool/oauth.ts +62 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@ai-sdk/provider": "4.0.8",
|
|
35
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
35
|
+
"@ai-sdk/provider-utils": "5.0.33",
|
|
36
36
|
"cross-spawn": "^7.0.6",
|
|
37
37
|
"pkce-challenge": "^5.0.1"
|
|
38
38
|
},
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
|
|
74
74
|
"build:watch": "pnpm clean && tsup --watch",
|
|
75
|
-
"clean": "
|
|
75
|
+
"clean": "del-cli dist *.tsbuildinfo",
|
|
76
76
|
"type-check": "tsc --build",
|
|
77
77
|
"test": "pnpm test:node && pnpm test:edge",
|
|
78
78
|
"test:update": "pnpm test:node -u",
|
package/src/tool/oauth.ts
CHANGED
|
@@ -27,7 +27,11 @@ import {
|
|
|
27
27
|
resourceUrlStripSlash,
|
|
28
28
|
} from '../util/oauth-util';
|
|
29
29
|
import { LATEST_PROTOCOL_VERSION } from './types';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
parseJSON,
|
|
32
|
+
validateDownloadUrl,
|
|
33
|
+
type FetchFunction,
|
|
34
|
+
} from '@ai-sdk/provider-utils';
|
|
31
35
|
export type AuthResult = 'AUTHORIZED' | 'REDIRECT';
|
|
32
36
|
|
|
33
37
|
export interface OAuthAuthorizationServerInformation {
|
|
@@ -123,6 +127,45 @@ function normalizeUrl(url: string | URL): string {
|
|
|
123
127
|
return new URL(url).href;
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
/** Allow loopback HTTP(S) for local MCP OAuth (RFC 8252 §7.3, RFC 6761 §6.3). */
|
|
131
|
+
function isOAuthLoopbackHost(hostname: string): boolean {
|
|
132
|
+
const normalized = hostname.toLowerCase().replace(/\.+$/, '');
|
|
133
|
+
return (
|
|
134
|
+
normalized === 'localhost' ||
|
|
135
|
+
normalized.endsWith('.localhost') ||
|
|
136
|
+
normalized === '127.0.0.1' ||
|
|
137
|
+
normalized === '[::1]' ||
|
|
138
|
+
normalized === '::1'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Guards metadata-derived token/registration URLs before credentials are sent.
|
|
144
|
+
* Loopback is allowed for local OAuth; every other target uses the shared
|
|
145
|
+
* download URL guard (http(s) only, no private/link-local IPs).
|
|
146
|
+
*
|
|
147
|
+
* Credential POSTs use `redirect: 'error'` instead of
|
|
148
|
+
* `fetchWithValidatedRedirects`, which is GET-only and would follow hops with
|
|
149
|
+
* the authorization code, PKCE verifier, and client secret still attached.
|
|
150
|
+
*/
|
|
151
|
+
function assertSafeOAuthEndpoint(endpointUrl: URL): void {
|
|
152
|
+
if (
|
|
153
|
+
(endpointUrl.protocol === 'http:' || endpointUrl.protocol === 'https:') &&
|
|
154
|
+
isOAuthLoopbackHost(endpointUrl.hostname)
|
|
155
|
+
) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
validateDownloadUrl(endpointUrl.href);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new MCPClientOAuthError({
|
|
163
|
+
message: `OAuth endpoint URL is not allowed: ${endpointUrl.href}`,
|
|
164
|
+
cause: error,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
126
169
|
function validateAuthorizationResponseIssuer({
|
|
127
170
|
callbackIssuer,
|
|
128
171
|
expectedIssuer,
|
|
@@ -896,6 +939,7 @@ export async function exchangeAuthorization(
|
|
|
896
939
|
const tokenUrl = metadata?.token_endpoint
|
|
897
940
|
? new URL(metadata.token_endpoint)
|
|
898
941
|
: new URL('/token', authorizationServerUrl);
|
|
942
|
+
assertSafeOAuthEndpoint(tokenUrl);
|
|
899
943
|
|
|
900
944
|
if (
|
|
901
945
|
metadata?.grant_types_supported &&
|
|
@@ -943,6 +987,7 @@ export async function exchangeAuthorization(
|
|
|
943
987
|
method: 'POST',
|
|
944
988
|
headers,
|
|
945
989
|
body: params,
|
|
990
|
+
redirect: 'error',
|
|
946
991
|
});
|
|
947
992
|
|
|
948
993
|
if (!response.ok) {
|
|
@@ -999,6 +1044,7 @@ export async function refreshAuthorization(
|
|
|
999
1044
|
} else {
|
|
1000
1045
|
tokenUrl = new URL('/token', authorizationServerUrl);
|
|
1001
1046
|
}
|
|
1047
|
+
assertSafeOAuthEndpoint(tokenUrl);
|
|
1002
1048
|
|
|
1003
1049
|
const headers = new Headers({
|
|
1004
1050
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1035,6 +1081,7 @@ export async function refreshAuthorization(
|
|
|
1035
1081
|
method: 'POST',
|
|
1036
1082
|
headers,
|
|
1037
1083
|
body: params,
|
|
1084
|
+
redirect: 'error',
|
|
1038
1085
|
});
|
|
1039
1086
|
if (!response.ok) {
|
|
1040
1087
|
throw await parseErrorResponse(response);
|
|
@@ -1074,6 +1121,7 @@ export async function registerClient(
|
|
|
1074
1121
|
} else {
|
|
1075
1122
|
registrationUrl = new URL('/register', authorizationServerUrl);
|
|
1076
1123
|
}
|
|
1124
|
+
assertSafeOAuthEndpoint(registrationUrl);
|
|
1077
1125
|
|
|
1078
1126
|
const applicationType =
|
|
1079
1127
|
clientMetadata.application_type ??
|
|
@@ -1087,6 +1135,7 @@ export async function registerClient(
|
|
|
1087
1135
|
...clientMetadata,
|
|
1088
1136
|
application_type: applicationType,
|
|
1089
1137
|
}),
|
|
1138
|
+
redirect: 'error',
|
|
1090
1139
|
});
|
|
1091
1140
|
|
|
1092
1141
|
if (!response.ok) {
|
|
@@ -1101,10 +1150,7 @@ function inferOAuthApplicationType(redirectUris: string[]): 'native' | 'web' {
|
|
|
1101
1150
|
const url = new URL(redirectUri);
|
|
1102
1151
|
return (
|
|
1103
1152
|
((url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
1104
|
-
(url.hostname
|
|
1105
|
-
url.hostname.endsWith('.localhost') ||
|
|
1106
|
-
url.hostname === '127.0.0.1' ||
|
|
1107
|
-
url.hostname === '[::1]')) ||
|
|
1153
|
+
isOAuthLoopbackHost(url.hostname)) ||
|
|
1108
1154
|
(url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
1109
1155
|
);
|
|
1110
1156
|
};
|
|
@@ -1244,6 +1290,12 @@ async function authInternal(
|
|
|
1244
1290
|
);
|
|
1245
1291
|
const currentAuthorizationServerInformation =
|
|
1246
1292
|
createAuthorizationServerInformation(authorizationServerUrl, metadata);
|
|
1293
|
+
const clientMetadata = provider.clientMetadata;
|
|
1294
|
+
const selectedScope = selectScope({
|
|
1295
|
+
scope,
|
|
1296
|
+
resourceMetadata,
|
|
1297
|
+
clientMetadata,
|
|
1298
|
+
});
|
|
1247
1299
|
|
|
1248
1300
|
/** Load or register client credentials with the AS pin attached. */
|
|
1249
1301
|
let clientInformation = await Promise.resolve(provider.clientInformation());
|
|
@@ -1276,7 +1328,10 @@ async function authInternal(
|
|
|
1276
1328
|
|
|
1277
1329
|
const fullInformation = await registerClient(authorizationServerUrl, {
|
|
1278
1330
|
metadata,
|
|
1279
|
-
clientMetadata:
|
|
1331
|
+
clientMetadata: {
|
|
1332
|
+
...clientMetadata,
|
|
1333
|
+
scope: selectedScope,
|
|
1334
|
+
},
|
|
1280
1335
|
fetchFn,
|
|
1281
1336
|
});
|
|
1282
1337
|
|
|
@@ -1410,11 +1465,7 @@ async function authInternal(
|
|
|
1410
1465
|
clientInformation,
|
|
1411
1466
|
state,
|
|
1412
1467
|
redirectUrl: provider.redirectUrl,
|
|
1413
|
-
scope:
|
|
1414
|
-
scope,
|
|
1415
|
-
resourceMetadata,
|
|
1416
|
-
clientMetadata: provider.clientMetadata,
|
|
1417
|
-
}),
|
|
1468
|
+
scope: selectedScope,
|
|
1418
1469
|
resource,
|
|
1419
1470
|
},
|
|
1420
1471
|
);
|