@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/mcp",
3
- "version": "2.0.37",
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.30",
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": "rm -rf dist *.tsbuildinfo",
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 { parseJSON, type FetchFunction } from '@ai-sdk/provider-utils';
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 === 'localhost' ||
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: provider.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: selectScope({
1414
- scope,
1415
- resourceMetadata,
1416
- clientMetadata: provider.clientMetadata,
1417
- }),
1468
+ scope: selectedScope,
1418
1469
  resource,
1419
1470
  },
1420
1471
  );