@ai-sdk/mcp 1.0.46 → 1.0.47

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": "1.0.46",
3
+ "version": "1.0.47",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -21,7 +21,10 @@ export type {
21
21
  ClientCapabilities as MCPClientCapabilities,
22
22
  } from './tool/types';
23
23
  export { auth, UnauthorizedError } from './tool/oauth';
24
- export type { OAuthClientProvider } from './tool/oauth';
24
+ export type {
25
+ OAuthAuthorizationServerInformation,
26
+ OAuthClientProvider,
27
+ } from './tool/oauth';
25
28
  export type {
26
29
  OAuthClientInformation,
27
30
  OAuthClientMetadata,
@@ -1,18 +1,4 @@
1
1
  import { z } from 'zod/v4';
2
- /**
3
- * OAuth 2.1 token response
4
- */
5
- export const OAuthTokensSchema = z
6
- .object({
7
- access_token: z.string(),
8
- id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect
9
- token_type: z.string(),
10
- expires_in: z.number().optional(),
11
- scope: z.string().optional(),
12
- refresh_token: z.string().optional(),
13
- })
14
- .strip();
15
-
16
2
  /**
17
3
  * Reusable URL validation that disallows javascript: scheme
18
4
  */
@@ -42,6 +28,22 @@ export const SafeUrlSchema = z
42
28
  { message: 'URL cannot use javascript:, data:, or vbscript: scheme' },
43
29
  );
44
30
 
31
+ /**
32
+ * OAuth 2.1 token response
33
+ */
34
+ export const OAuthTokensSchema = z
35
+ .object({
36
+ access_token: z.string(),
37
+ id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect
38
+ token_type: z.string(),
39
+ expires_in: z.number().optional(),
40
+ scope: z.string().optional(),
41
+ refresh_token: z.string().optional(),
42
+ authorization_server: SafeUrlSchema.optional(),
43
+ token_endpoint: SafeUrlSchema.optional(),
44
+ })
45
+ .strip();
46
+
45
47
  export const OAuthProtectedResourceMetadataSchema = z
46
48
  .object({
47
49
  resource: z.string().url(),
@@ -118,6 +120,8 @@ export const OAuthClientInformationSchema = z
118
120
  client_secret: z.string().optional(),
119
121
  client_id_issued_at: z.number().optional(),
120
122
  client_secret_expires_at: z.number().optional(),
123
+ authorization_server: SafeUrlSchema.optional(),
124
+ token_endpoint: SafeUrlSchema.optional(),
121
125
  })
122
126
  .strip();
123
127
 
package/src/tool/oauth.ts CHANGED
@@ -31,6 +31,11 @@ import { parseJSON, type FetchFunction } from '@ai-sdk/provider-utils';
31
31
 
32
32
  export type AuthResult = 'AUTHORIZED' | 'REDIRECT';
33
33
 
34
+ export interface OAuthAuthorizationServerInformation {
35
+ authorizationServerUrl: string;
36
+ tokenEndpoint: string;
37
+ }
38
+
34
39
  export interface OAuthClientProvider {
35
40
  /**
36
41
  * Returns current access token if present; undefined otherwise.
@@ -83,6 +88,13 @@ export interface OAuthClientProvider {
83
88
  saveClientInformation?(
84
89
  clientInformation: OAuthClientInformation,
85
90
  ): void | Promise<void>;
91
+ authorizationServerInformation?():
92
+ | OAuthAuthorizationServerInformation
93
+ | undefined
94
+ | Promise<OAuthAuthorizationServerInformation | undefined>;
95
+ saveAuthorizationServerInformation?(
96
+ authorizationServerInformation: OAuthAuthorizationServerInformation,
97
+ ): void | Promise<void>;
86
98
  state?(): string | Promise<string>;
87
99
  saveState?(state: string): void | Promise<void>;
88
100
  storedState?(): string | undefined | Promise<string | undefined>;
@@ -99,6 +111,158 @@ export class UnauthorizedError extends Error {
99
111
  }
100
112
  }
101
113
 
114
+ function normalizeUrl(url: string | URL): string {
115
+ return new URL(url).href;
116
+ }
117
+
118
+ function createAuthorizationServerInformation(
119
+ authorizationServerUrl: string | URL,
120
+ metadata?: AuthorizationServerMetadata,
121
+ ): OAuthAuthorizationServerInformation {
122
+ return {
123
+ authorizationServerUrl: normalizeUrl(authorizationServerUrl),
124
+ tokenEndpoint: normalizeUrl(
125
+ metadata?.token_endpoint
126
+ ? new URL(metadata.token_endpoint)
127
+ : new URL('/token', authorizationServerUrl),
128
+ ),
129
+ };
130
+ }
131
+
132
+ function addAuthorizationServerInformationToTokens(
133
+ tokens: OAuthTokens,
134
+ authorizationServerInformation: OAuthAuthorizationServerInformation,
135
+ ): OAuthTokens {
136
+ return {
137
+ ...tokens,
138
+ authorization_server: authorizationServerInformation.authorizationServerUrl,
139
+ token_endpoint: authorizationServerInformation.tokenEndpoint,
140
+ };
141
+ }
142
+
143
+ function addAuthorizationServerInformationToClientInformation<
144
+ CLIENT_INFORMATION extends OAuthClientInformation,
145
+ >(
146
+ clientInformation: CLIENT_INFORMATION,
147
+ authorizationServerInformation: OAuthAuthorizationServerInformation,
148
+ ): CLIENT_INFORMATION {
149
+ return {
150
+ ...clientInformation,
151
+ authorization_server: authorizationServerInformation.authorizationServerUrl,
152
+ token_endpoint: authorizationServerInformation.tokenEndpoint,
153
+ };
154
+ }
155
+
156
+ function getAuthorizationServerInformationFromCredentials(credentials?: {
157
+ authorization_server?: string;
158
+ token_endpoint?: string;
159
+ }): OAuthAuthorizationServerInformation | undefined {
160
+ if (!credentials?.authorization_server || !credentials.token_endpoint) {
161
+ return undefined;
162
+ }
163
+
164
+ return {
165
+ authorizationServerUrl: normalizeUrl(credentials.authorization_server),
166
+ tokenEndpoint: normalizeUrl(credentials.token_endpoint),
167
+ };
168
+ }
169
+
170
+ async function getStoredAuthorizationServerInformation({
171
+ provider,
172
+ clientInformation,
173
+ tokens,
174
+ }: {
175
+ provider: OAuthClientProvider;
176
+ clientInformation: OAuthClientInformation;
177
+ tokens?: OAuthTokens;
178
+ }): Promise<OAuthAuthorizationServerInformation | undefined> {
179
+ const tokenAuthorizationServerInformation =
180
+ getAuthorizationServerInformationFromCredentials(tokens);
181
+ if (tokenAuthorizationServerInformation) {
182
+ return tokenAuthorizationServerInformation;
183
+ }
184
+
185
+ const providerAuthorizationServerInformation =
186
+ await provider.authorizationServerInformation?.();
187
+ if (providerAuthorizationServerInformation) {
188
+ return {
189
+ authorizationServerUrl: normalizeUrl(
190
+ providerAuthorizationServerInformation.authorizationServerUrl,
191
+ ),
192
+ tokenEndpoint: normalizeUrl(
193
+ providerAuthorizationServerInformation.tokenEndpoint,
194
+ ),
195
+ };
196
+ }
197
+
198
+ return getAuthorizationServerInformationFromCredentials(clientInformation);
199
+ }
200
+
201
+ async function saveAuthorizationServerInformation({
202
+ provider,
203
+ clientInformation,
204
+ authorizationServerInformation,
205
+ }: {
206
+ provider: OAuthClientProvider;
207
+ clientInformation: OAuthClientInformation;
208
+ authorizationServerInformation: OAuthAuthorizationServerInformation;
209
+ }): Promise<boolean> {
210
+ if (provider.saveAuthorizationServerInformation) {
211
+ await provider.saveAuthorizationServerInformation(
212
+ authorizationServerInformation,
213
+ );
214
+ return true;
215
+ }
216
+
217
+ if (provider.saveClientInformation) {
218
+ await provider.saveClientInformation(
219
+ addAuthorizationServerInformationToClientInformation(
220
+ clientInformation,
221
+ authorizationServerInformation,
222
+ ),
223
+ );
224
+ return true;
225
+ }
226
+
227
+ return false;
228
+ }
229
+
230
+ function assertResourceMetadataUrlSameOrigin(
231
+ serverUrl: string | URL,
232
+ resourceMetadataUrl?: URL,
233
+ ): void {
234
+ if (!resourceMetadataUrl) {
235
+ return;
236
+ }
237
+
238
+ const expectedOrigin = new URL(serverUrl).origin;
239
+ if (resourceMetadataUrl.origin !== expectedOrigin) {
240
+ throw new MCPClientOAuthError({
241
+ message: `OAuth protected resource metadata URL ${resourceMetadataUrl.href} must have the same origin as the MCP server URL ${expectedOrigin}`,
242
+ });
243
+ }
244
+ }
245
+
246
+ function assertAuthorizationServerInformationMatches({
247
+ storedAuthorizationServerInformation,
248
+ currentAuthorizationServerInformation,
249
+ }: {
250
+ storedAuthorizationServerInformation: OAuthAuthorizationServerInformation;
251
+ currentAuthorizationServerInformation: OAuthAuthorizationServerInformation;
252
+ }): void {
253
+ if (
254
+ storedAuthorizationServerInformation.authorizationServerUrl !==
255
+ currentAuthorizationServerInformation.authorizationServerUrl ||
256
+ storedAuthorizationServerInformation.tokenEndpoint !==
257
+ currentAuthorizationServerInformation.tokenEndpoint
258
+ ) {
259
+ throw new MCPClientOAuthError({
260
+ message:
261
+ 'OAuth authorization server metadata does not match the metadata that issued the stored credentials',
262
+ });
263
+ }
264
+ }
265
+
102
266
  /**
103
267
  * Extracts the OAuth 2.0 Protected Resource Metadata URL from a WWW-Authenticate header (RFC9728).
104
268
  * Looks for a resource="..." parameter.
@@ -920,6 +1084,11 @@ async function authInternal(
920
1084
  ): Promise<AuthResult> {
921
1085
  let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
922
1086
  let authorizationServerUrl: string | URL | undefined;
1087
+
1088
+ /** Reject Protected Resource Metadata URLs outside the configured MCP server origin. */
1089
+ assertResourceMetadataUrlSameOrigin(serverUrl, resourceMetadataUrl);
1090
+
1091
+ /** Discover PRM and select its advertised authorization server. */
923
1092
  try {
924
1093
  resourceMetadata = await discoverOAuthProtectedResourceMetadata(
925
1094
  serverUrl,
@@ -934,27 +1103,29 @@ async function authInternal(
934
1103
  }
935
1104
  } catch {}
936
1105
 
937
- /**
938
- * If we don't get a valid authorization server metadata from protected resource metadata,
939
- * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
940
- */
1106
+ /** Fall back to legacy MCP behavior where the MCP server is the Authorization Server */
941
1107
  if (!authorizationServerUrl) {
942
1108
  authorizationServerUrl = serverUrl;
943
1109
  }
944
1110
 
1111
+ /** Validate and select the resource value sent to the AS */
945
1112
  const resource: URL | undefined = await selectResourceURL(
946
1113
  serverUrl,
947
1114
  provider,
948
1115
  resourceMetadata,
949
1116
  );
950
1117
 
1118
+ /** Discover AS metadata and derive the credential pin for this flow */
951
1119
  const metadata = await discoverAuthorizationServerMetadata(
952
1120
  authorizationServerUrl,
953
1121
  {
954
1122
  fetchFn,
955
1123
  },
956
1124
  );
1125
+ const currentAuthorizationServerInformation =
1126
+ createAuthorizationServerInformation(authorizationServerUrl, metadata);
957
1127
 
1128
+ /** Load or register client credentials with the AS pin attached. */
958
1129
  let clientInformation = await Promise.resolve(provider.clientInformation());
959
1130
  if (!clientInformation) {
960
1131
  if (authorizationCode !== undefined) {
@@ -975,11 +1146,14 @@ async function authInternal(
975
1146
  fetchFn,
976
1147
  });
977
1148
 
978
- await provider.saveClientInformation(fullInformation);
979
- clientInformation = fullInformation;
1149
+ clientInformation = addAuthorizationServerInformationToClientInformation(
1150
+ fullInformation,
1151
+ currentAuthorizationServerInformation,
1152
+ );
1153
+ await provider.saveClientInformation(clientInformation);
980
1154
  }
981
1155
 
982
- // Exchange authorization code for tokens
1156
+ /** On callback, validate state and AS pin before code exchange */
983
1157
  if (authorizationCode !== undefined) {
984
1158
  if (provider.storedState) {
985
1159
  const expectedState = await provider.storedState();
@@ -990,6 +1164,22 @@ async function authInternal(
990
1164
  }
991
1165
  }
992
1166
 
1167
+ const storedAuthorizationServerInformation =
1168
+ await getStoredAuthorizationServerInformation({
1169
+ provider,
1170
+ clientInformation,
1171
+ });
1172
+ if (!storedAuthorizationServerInformation) {
1173
+ throw new MCPClientOAuthError({
1174
+ message:
1175
+ 'Stored OAuth authorization server metadata is required when exchanging an authorization code',
1176
+ });
1177
+ }
1178
+ assertAuthorizationServerInformationMatches({
1179
+ storedAuthorizationServerInformation,
1180
+ currentAuthorizationServerInformation,
1181
+ });
1182
+
993
1183
  const codeVerifier = await provider.codeVerifier();
994
1184
  const tokens = await exchangeAuthorization(authorizationServerUrl, {
995
1185
  metadata,
@@ -1002,27 +1192,55 @@ async function authInternal(
1002
1192
  fetchFn: fetchFn,
1003
1193
  });
1004
1194
 
1005
- await provider.saveTokens(tokens);
1195
+ await provider.saveTokens(
1196
+ addAuthorizationServerInformationToTokens(
1197
+ tokens,
1198
+ currentAuthorizationServerInformation,
1199
+ ),
1200
+ );
1006
1201
  return 'AUTHORIZED';
1007
1202
  }
1008
1203
 
1009
1204
  const tokens = await provider.tokens();
1010
1205
 
1011
- // Handle token refresh or new authorization
1206
+ /** Refresh only when stored credentials match the current AS pin */
1012
1207
  if (tokens?.refresh_token) {
1013
- try {
1014
- // Attempt to refresh the token
1015
- const newTokens = await refreshAuthorization(authorizationServerUrl, {
1016
- metadata,
1208
+ const storedAuthorizationServerInformation =
1209
+ await getStoredAuthorizationServerInformation({
1210
+ provider,
1017
1211
  clientInformation,
1018
- refreshToken: tokens.refresh_token,
1019
- resource,
1020
- addClientAuthentication: provider.addClientAuthentication,
1021
- fetchFn,
1212
+ tokens,
1022
1213
  });
1023
1214
 
1024
- await provider.saveTokens(newTokens);
1025
- return 'AUTHORIZED';
1215
+ if (storedAuthorizationServerInformation) {
1216
+ assertAuthorizationServerInformationMatches({
1217
+ storedAuthorizationServerInformation,
1218
+ currentAuthorizationServerInformation,
1219
+ });
1220
+ } else {
1221
+ await provider.invalidateCredentials?.('tokens');
1222
+ }
1223
+
1224
+ try {
1225
+ if (storedAuthorizationServerInformation) {
1226
+ // Attempt to refresh the token
1227
+ const newTokens = await refreshAuthorization(authorizationServerUrl, {
1228
+ metadata,
1229
+ clientInformation,
1230
+ refreshToken: tokens.refresh_token,
1231
+ resource,
1232
+ addClientAuthentication: provider.addClientAuthentication,
1233
+ fetchFn,
1234
+ });
1235
+
1236
+ await provider.saveTokens(
1237
+ addAuthorizationServerInformationToTokens(
1238
+ newTokens,
1239
+ currentAuthorizationServerInformation,
1240
+ ),
1241
+ );
1242
+ return 'AUTHORIZED';
1243
+ }
1026
1244
  } catch (error) {
1027
1245
  if (
1028
1246
  // If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
@@ -1037,6 +1255,7 @@ async function authInternal(
1037
1255
  }
1038
1256
  }
1039
1257
 
1258
+ /** Start authorization and persist the AS pin before redirecting */
1040
1259
  const state = provider.state ? await provider.state() : undefined;
1041
1260
  if (state && provider.saveState) {
1042
1261
  await provider.saveState(state);
@@ -1055,6 +1274,19 @@ async function authInternal(
1055
1274
  },
1056
1275
  );
1057
1276
 
1277
+ const savedAuthorizationServerInformation =
1278
+ await saveAuthorizationServerInformation({
1279
+ provider,
1280
+ clientInformation,
1281
+ authorizationServerInformation: currentAuthorizationServerInformation,
1282
+ });
1283
+ if (!savedAuthorizationServerInformation) {
1284
+ throw new MCPClientOAuthError({
1285
+ message:
1286
+ 'OAuth authorization server metadata must be saveable before starting authorization',
1287
+ });
1288
+ }
1289
+
1058
1290
  await provider.saveCodeVerifier(codeVerifier);
1059
1291
  await provider.redirectToAuthorization(authorizationUrl);
1060
1292
  return 'REDIRECT';