@ai-sdk/mcp 2.0.0-beta.6 → 2.0.0-beta.66

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/src/tool/oauth.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import pkceChallenge from 'pkce-challenge';
2
2
  import {
3
- OAuthTokens,
4
- OAuthProtectedResourceMetadata,
5
3
  OAuthProtectedResourceMetadataSchema,
6
4
  OAuthMetadataSchema,
7
5
  OpenIdProviderDiscoveryMetadataSchema,
8
- AuthorizationServerMetadata,
9
- OAuthClientInformation,
10
6
  OAuthTokensSchema,
11
7
  OAuthErrorResponseSchema,
12
- OAuthClientMetadata,
13
- OAuthClientInformationFull,
14
8
  OAuthClientInformationFullSchema,
9
+ type OAuthTokens,
10
+ type OAuthProtectedResourceMetadata,
11
+ type AuthorizationServerMetadata,
12
+ type OAuthClientInformation,
13
+ type OAuthClientMetadata,
14
+ type OAuthClientInformationFull,
15
15
  } from './oauth-types';
16
16
  import {
17
17
  MCPClientOAuthError,
@@ -24,12 +24,17 @@ import {
24
24
  import {
25
25
  resourceUrlFromServerUrl,
26
26
  checkResourceAllowed,
27
+ resourceUrlStripSlash,
27
28
  } from '../util/oauth-util';
28
29
  import { LATEST_PROTOCOL_VERSION } from './types';
29
- import { FetchFunction } from '@ai-sdk/provider-utils';
30
-
30
+ import { parseJSON, type FetchFunction } from '@ai-sdk/provider-utils';
31
31
  export type AuthResult = 'AUTHORIZED' | 'REDIRECT';
32
32
 
33
+ export interface OAuthAuthorizationServerInformation {
34
+ authorizationServerUrl: string;
35
+ tokenEndpoint: string;
36
+ }
37
+
33
38
  export interface OAuthClientProvider {
34
39
  /**
35
40
  * Returns current access token if present; undefined otherwise.
@@ -82,6 +87,21 @@ export interface OAuthClientProvider {
82
87
  saveClientInformation?(
83
88
  clientInformation: OAuthClientInformation,
84
89
  ): void | Promise<void>;
90
+ authorizationServerInformation?():
91
+ | OAuthAuthorizationServerInformation
92
+ | undefined
93
+ | Promise<OAuthAuthorizationServerInformation | undefined>;
94
+ saveAuthorizationServerInformation?(
95
+ authorizationServerInformation: OAuthAuthorizationServerInformation,
96
+ ): void | Promise<void>;
97
+ /**
98
+ * Validates an authorization server URL discovered from MCP protected resource
99
+ * metadata before the client fetches its OAuth metadata.
100
+ */
101
+ validateAuthorizationServerURL?(
102
+ serverUrl: string | URL,
103
+ authorizationServerUrl: string | URL,
104
+ ): void | Promise<void>;
85
105
  state?(): string | Promise<string>;
86
106
  saveState?(state: string): void | Promise<void>;
87
107
  storedState?(): string | undefined | Promise<string | undefined>;
@@ -98,6 +118,158 @@ export class UnauthorizedError extends Error {
98
118
  }
99
119
  }
100
120
 
121
+ function normalizeUrl(url: string | URL): string {
122
+ return new URL(url).href;
123
+ }
124
+
125
+ function createAuthorizationServerInformation(
126
+ authorizationServerUrl: string | URL,
127
+ metadata?: AuthorizationServerMetadata,
128
+ ): OAuthAuthorizationServerInformation {
129
+ return {
130
+ authorizationServerUrl: normalizeUrl(authorizationServerUrl),
131
+ tokenEndpoint: normalizeUrl(
132
+ metadata?.token_endpoint
133
+ ? new URL(metadata.token_endpoint)
134
+ : new URL('/token', authorizationServerUrl),
135
+ ),
136
+ };
137
+ }
138
+
139
+ function addAuthorizationServerInformationToTokens(
140
+ tokens: OAuthTokens,
141
+ authorizationServerInformation: OAuthAuthorizationServerInformation,
142
+ ): OAuthTokens {
143
+ return {
144
+ ...tokens,
145
+ authorization_server: authorizationServerInformation.authorizationServerUrl,
146
+ token_endpoint: authorizationServerInformation.tokenEndpoint,
147
+ };
148
+ }
149
+
150
+ function addAuthorizationServerInformationToClientInformation<
151
+ CLIENT_INFORMATION extends OAuthClientInformation,
152
+ >(
153
+ clientInformation: CLIENT_INFORMATION,
154
+ authorizationServerInformation: OAuthAuthorizationServerInformation,
155
+ ): CLIENT_INFORMATION {
156
+ return {
157
+ ...clientInformation,
158
+ authorization_server: authorizationServerInformation.authorizationServerUrl,
159
+ token_endpoint: authorizationServerInformation.tokenEndpoint,
160
+ };
161
+ }
162
+
163
+ function getAuthorizationServerInformationFromCredentials(credentials?: {
164
+ authorization_server?: string;
165
+ token_endpoint?: string;
166
+ }): OAuthAuthorizationServerInformation | undefined {
167
+ if (!credentials?.authorization_server || !credentials.token_endpoint) {
168
+ return undefined;
169
+ }
170
+
171
+ return {
172
+ authorizationServerUrl: normalizeUrl(credentials.authorization_server),
173
+ tokenEndpoint: normalizeUrl(credentials.token_endpoint),
174
+ };
175
+ }
176
+
177
+ async function getStoredAuthorizationServerInformation({
178
+ provider,
179
+ clientInformation,
180
+ tokens,
181
+ }: {
182
+ provider: OAuthClientProvider;
183
+ clientInformation: OAuthClientInformation;
184
+ tokens?: OAuthTokens;
185
+ }): Promise<OAuthAuthorizationServerInformation | undefined> {
186
+ const tokenAuthorizationServerInformation =
187
+ getAuthorizationServerInformationFromCredentials(tokens);
188
+ if (tokenAuthorizationServerInformation) {
189
+ return tokenAuthorizationServerInformation;
190
+ }
191
+
192
+ const providerAuthorizationServerInformation =
193
+ await provider.authorizationServerInformation?.();
194
+ if (providerAuthorizationServerInformation) {
195
+ return {
196
+ authorizationServerUrl: normalizeUrl(
197
+ providerAuthorizationServerInformation.authorizationServerUrl,
198
+ ),
199
+ tokenEndpoint: normalizeUrl(
200
+ providerAuthorizationServerInformation.tokenEndpoint,
201
+ ),
202
+ };
203
+ }
204
+
205
+ return getAuthorizationServerInformationFromCredentials(clientInformation);
206
+ }
207
+
208
+ async function saveAuthorizationServerInformation({
209
+ provider,
210
+ clientInformation,
211
+ authorizationServerInformation,
212
+ }: {
213
+ provider: OAuthClientProvider;
214
+ clientInformation: OAuthClientInformation;
215
+ authorizationServerInformation: OAuthAuthorizationServerInformation;
216
+ }): Promise<boolean> {
217
+ if (provider.saveAuthorizationServerInformation) {
218
+ await provider.saveAuthorizationServerInformation(
219
+ authorizationServerInformation,
220
+ );
221
+ return true;
222
+ }
223
+
224
+ if (provider.saveClientInformation) {
225
+ await provider.saveClientInformation(
226
+ addAuthorizationServerInformationToClientInformation(
227
+ clientInformation,
228
+ authorizationServerInformation,
229
+ ),
230
+ );
231
+ return true;
232
+ }
233
+
234
+ return false;
235
+ }
236
+
237
+ function assertResourceMetadataUrlSameOrigin(
238
+ serverUrl: string | URL,
239
+ resourceMetadataUrl?: URL,
240
+ ): void {
241
+ if (!resourceMetadataUrl) {
242
+ return;
243
+ }
244
+
245
+ const expectedOrigin = new URL(serverUrl).origin;
246
+ if (resourceMetadataUrl.origin !== expectedOrigin) {
247
+ throw new MCPClientOAuthError({
248
+ message: `OAuth protected resource metadata URL ${resourceMetadataUrl.href} must have the same origin as the MCP server URL ${expectedOrigin}`,
249
+ });
250
+ }
251
+ }
252
+
253
+ function assertAuthorizationServerInformationMatches({
254
+ storedAuthorizationServerInformation,
255
+ currentAuthorizationServerInformation,
256
+ }: {
257
+ storedAuthorizationServerInformation: OAuthAuthorizationServerInformation;
258
+ currentAuthorizationServerInformation: OAuthAuthorizationServerInformation;
259
+ }): void {
260
+ if (
261
+ storedAuthorizationServerInformation.authorizationServerUrl !==
262
+ currentAuthorizationServerInformation.authorizationServerUrl ||
263
+ storedAuthorizationServerInformation.tokenEndpoint !==
264
+ currentAuthorizationServerInformation.tokenEndpoint
265
+ ) {
266
+ throw new MCPClientOAuthError({
267
+ message:
268
+ 'OAuth authorization server metadata does not match the metadata that issued the stored credentials',
269
+ });
270
+ }
271
+ }
272
+
101
273
  /**
102
274
  * Extracts the OAuth 2.0 Protected Resource Metadata URL from a WWW-Authenticate header (RFC9728).
103
275
  * Looks for a resource="..." parameter.
@@ -270,23 +442,30 @@ export async function discoverOAuthProtectedResourceMetadata(
270
442
  */
271
443
  export function buildDiscoveryUrls(
272
444
  authorizationServerUrl: string | URL,
273
- ): { url: URL; type: 'oauth' | 'oidc' }[] {
445
+ ): { url: URL; type: 'oauth' | 'oidc'; expectedIssuer: string }[] {
274
446
  const url =
275
447
  typeof authorizationServerUrl === 'string'
276
448
  ? new URL(authorizationServerUrl)
277
449
  : authorizationServerUrl;
278
450
  const hasPath = url.pathname !== '/';
279
- const urlsToTry: { url: URL; type: 'oauth' | 'oidc' }[] = [];
451
+ const rootIssuer = url.origin;
452
+ const urlsToTry: {
453
+ url: URL;
454
+ type: 'oauth' | 'oidc';
455
+ expectedIssuer: string;
456
+ }[] = [];
280
457
 
281
458
  if (!hasPath) {
282
459
  urlsToTry.push({
283
460
  url: new URL('/.well-known/oauth-authorization-server', url.origin),
284
461
  type: 'oauth',
462
+ expectedIssuer: rootIssuer,
285
463
  });
286
464
 
287
465
  urlsToTry.push({
288
466
  url: new URL('/.well-known/openid-configuration', url.origin),
289
467
  type: 'oidc',
468
+ expectedIssuer: rootIssuer,
290
469
  });
291
470
 
292
471
  return urlsToTry;
@@ -296,6 +475,7 @@ export function buildDiscoveryUrls(
296
475
  if (pathname.endsWith('/')) {
297
476
  pathname = pathname.slice(0, -1);
298
477
  }
478
+ const pathIssuer = `${url.origin}${pathname}`;
299
479
 
300
480
  urlsToTry.push({
301
481
  url: new URL(
@@ -303,26 +483,41 @@ export function buildDiscoveryUrls(
303
483
  url.origin,
304
484
  ),
305
485
  type: 'oauth',
486
+ expectedIssuer: pathIssuer,
306
487
  });
307
488
 
308
489
  urlsToTry.push({
309
490
  url: new URL('/.well-known/oauth-authorization-server', url.origin),
310
491
  type: 'oauth',
492
+ expectedIssuer: rootIssuer,
311
493
  });
312
494
 
313
495
  urlsToTry.push({
314
496
  url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
315
497
  type: 'oidc',
498
+ expectedIssuer: pathIssuer,
316
499
  });
317
500
 
318
501
  urlsToTry.push({
319
502
  url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
320
503
  type: 'oidc',
504
+ expectedIssuer: pathIssuer,
321
505
  });
322
506
 
323
507
  return urlsToTry;
324
508
  }
325
509
 
510
+ function assertMetadataIssuerMatches(
511
+ metadata: AuthorizationServerMetadata,
512
+ expectedIssuer: string,
513
+ ): void {
514
+ if (metadata.issuer !== expectedIssuer) {
515
+ throw new MCPClientOAuthError({
516
+ message: `OAuth authorization server metadata issuer ${metadata.issuer} does not match expected issuer ${expectedIssuer}`,
517
+ });
518
+ }
519
+ }
520
+
326
521
  export async function discoverAuthorizationServerMetadata(
327
522
  authorizationServerUrl: string | URL,
328
523
  {
@@ -337,7 +532,7 @@ export async function discoverAuthorizationServerMetadata(
337
532
 
338
533
  const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
339
534
 
340
- for (const { url: endpointUrl, type } of urlsToTry) {
535
+ for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
341
536
  const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
342
537
 
343
538
  if (!response) {
@@ -359,11 +554,14 @@ export async function discoverAuthorizationServerMetadata(
359
554
  }
360
555
 
361
556
  if (type === 'oauth') {
362
- return OAuthMetadataSchema.parse(await response.json());
557
+ const metadata = OAuthMetadataSchema.parse(await response.json());
558
+ assertMetadataIssuerMatches(metadata, expectedIssuer);
559
+ return metadata;
363
560
  } else {
364
561
  const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(
365
562
  await response.json(),
366
563
  );
564
+ assertMetadataIssuerMatches(metadata, expectedIssuer);
367
565
 
368
566
  // MCP spec requires OIDC providers to support S256 PKCE
369
567
  if (!metadata.code_challenge_methods_supported?.includes('S256')) {
@@ -451,7 +649,10 @@ export async function startAuthorization(
451
649
  }
452
650
 
453
651
  if (resource) {
454
- authorizationUrl.searchParams.set('resource', resource.href);
652
+ authorizationUrl.searchParams.set(
653
+ 'resource',
654
+ resourceUrlStripSlash(resource),
655
+ );
455
656
  }
456
657
 
457
658
  return { authorizationUrl, codeVerifier };
@@ -587,7 +788,9 @@ export async function parseErrorResponse(
587
788
  const body = input instanceof Response ? await input.text() : input;
588
789
 
589
790
  try {
590
- const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
791
+ const result = OAuthErrorResponseSchema.parse(
792
+ await parseJSON({ text: body }),
793
+ );
591
794
  const { error, error_description, error_uri } = result;
592
795
  const errorClass = OAUTH_ERRORS[error] || ServerError;
593
796
  return new errorClass({
@@ -662,7 +865,12 @@ export async function exchangeAuthorization(
662
865
  });
663
866
 
664
867
  if (addClientAuthentication) {
665
- addClientAuthentication(headers, params, authorizationServerUrl, metadata);
868
+ await addClientAuthentication(
869
+ headers,
870
+ params,
871
+ authorizationServerUrl,
872
+ metadata,
873
+ );
666
874
  } else {
667
875
  const supportedMethods =
668
876
  metadata?.token_endpoint_auth_methods_supported ?? [];
@@ -675,7 +883,7 @@ export async function exchangeAuthorization(
675
883
  }
676
884
 
677
885
  if (resource) {
678
- params.set('resource', resource.href);
886
+ params.set('resource', resourceUrlStripSlash(resource));
679
887
  }
680
888
 
681
889
  const response = await (fetchFn ?? fetch)(tokenUrl, {
@@ -749,7 +957,12 @@ export async function refreshAuthorization(
749
957
  });
750
958
 
751
959
  if (addClientAuthentication) {
752
- addClientAuthentication(headers, params, authorizationServerUrl, metadata);
960
+ await addClientAuthentication(
961
+ headers,
962
+ params,
963
+ authorizationServerUrl,
964
+ metadata,
965
+ );
753
966
  } else {
754
967
  const supportedMethods =
755
968
  metadata?.token_endpoint_auth_methods_supported ?? [];
@@ -762,7 +975,7 @@ export async function refreshAuthorization(
762
975
  }
763
976
 
764
977
  if (resource) {
765
- params.set('resource', resource.href);
978
+ params.set('resource', resourceUrlStripSlash(resource));
766
979
  }
767
980
 
768
981
  const response = await (fetchFn ?? fetch)(tokenUrl, {
@@ -904,6 +1117,11 @@ async function authInternal(
904
1117
  ): Promise<AuthResult> {
905
1118
  let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
906
1119
  let authorizationServerUrl: string | URL | undefined;
1120
+
1121
+ /** Reject Protected Resource Metadata URLs outside the configured MCP server origin. */
1122
+ assertResourceMetadataUrlSameOrigin(serverUrl, resourceMetadataUrl);
1123
+
1124
+ /** Discover PRM and select its advertised authorization server. */
907
1125
  try {
908
1126
  resourceMetadata = await discoverOAuthProtectedResourceMetadata(
909
1127
  serverUrl,
@@ -918,27 +1136,35 @@ async function authInternal(
918
1136
  }
919
1137
  } catch {}
920
1138
 
921
- /**
922
- * If we don't get a valid authorization server metadata from protected resource metadata,
923
- * fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
924
- */
1139
+ /** Fall back to legacy MCP behavior where the MCP server is the Authorization Server */
925
1140
  if (!authorizationServerUrl) {
926
1141
  authorizationServerUrl = serverUrl;
927
1142
  }
928
1143
 
1144
+ /** Validate and select the resource value sent to the AS */
929
1145
  const resource: URL | undefined = await selectResourceURL(
930
1146
  serverUrl,
931
1147
  provider,
932
1148
  resourceMetadata,
933
1149
  );
934
1150
 
1151
+ /** Let applications constrain discovered AS URLs before metadata fetches. */
1152
+ await provider.validateAuthorizationServerURL?.(
1153
+ serverUrl,
1154
+ authorizationServerUrl,
1155
+ );
1156
+
1157
+ /** Discover AS metadata and derive the credential pin for this flow */
935
1158
  const metadata = await discoverAuthorizationServerMetadata(
936
1159
  authorizationServerUrl,
937
1160
  {
938
1161
  fetchFn,
939
1162
  },
940
1163
  );
1164
+ const currentAuthorizationServerInformation =
1165
+ createAuthorizationServerInformation(authorizationServerUrl, metadata);
941
1166
 
1167
+ /** Load or register client credentials with the AS pin attached. */
942
1168
  let clientInformation = await Promise.resolve(provider.clientInformation());
943
1169
  if (!clientInformation) {
944
1170
  if (authorizationCode !== undefined) {
@@ -959,11 +1185,14 @@ async function authInternal(
959
1185
  fetchFn,
960
1186
  });
961
1187
 
962
- await provider.saveClientInformation(fullInformation);
963
- clientInformation = fullInformation;
1188
+ clientInformation = addAuthorizationServerInformationToClientInformation(
1189
+ fullInformation,
1190
+ currentAuthorizationServerInformation,
1191
+ );
1192
+ await provider.saveClientInformation(clientInformation);
964
1193
  }
965
1194
 
966
- // Exchange authorization code for tokens
1195
+ /** On callback, validate state and AS pin before code exchange */
967
1196
  if (authorizationCode !== undefined) {
968
1197
  if (provider.storedState) {
969
1198
  const expectedState = await provider.storedState();
@@ -974,6 +1203,22 @@ async function authInternal(
974
1203
  }
975
1204
  }
976
1205
 
1206
+ const storedAuthorizationServerInformation =
1207
+ await getStoredAuthorizationServerInformation({
1208
+ provider,
1209
+ clientInformation,
1210
+ });
1211
+ if (!storedAuthorizationServerInformation) {
1212
+ throw new MCPClientOAuthError({
1213
+ message:
1214
+ 'Stored OAuth authorization server metadata is required when exchanging an authorization code',
1215
+ });
1216
+ }
1217
+ assertAuthorizationServerInformationMatches({
1218
+ storedAuthorizationServerInformation,
1219
+ currentAuthorizationServerInformation,
1220
+ });
1221
+
977
1222
  const codeVerifier = await provider.codeVerifier();
978
1223
  const tokens = await exchangeAuthorization(authorizationServerUrl, {
979
1224
  metadata,
@@ -986,27 +1231,55 @@ async function authInternal(
986
1231
  fetchFn: fetchFn,
987
1232
  });
988
1233
 
989
- await provider.saveTokens(tokens);
1234
+ await provider.saveTokens(
1235
+ addAuthorizationServerInformationToTokens(
1236
+ tokens,
1237
+ currentAuthorizationServerInformation,
1238
+ ),
1239
+ );
990
1240
  return 'AUTHORIZED';
991
1241
  }
992
1242
 
993
1243
  const tokens = await provider.tokens();
994
1244
 
995
- // Handle token refresh or new authorization
1245
+ /** Refresh only when stored credentials match the current AS pin */
996
1246
  if (tokens?.refresh_token) {
997
- try {
998
- // Attempt to refresh the token
999
- const newTokens = await refreshAuthorization(authorizationServerUrl, {
1000
- metadata,
1247
+ const storedAuthorizationServerInformation =
1248
+ await getStoredAuthorizationServerInformation({
1249
+ provider,
1001
1250
  clientInformation,
1002
- refreshToken: tokens.refresh_token,
1003
- resource,
1004
- addClientAuthentication: provider.addClientAuthentication,
1005
- fetchFn,
1251
+ tokens,
1252
+ });
1253
+
1254
+ if (storedAuthorizationServerInformation) {
1255
+ assertAuthorizationServerInformationMatches({
1256
+ storedAuthorizationServerInformation,
1257
+ currentAuthorizationServerInformation,
1006
1258
  });
1259
+ } else {
1260
+ await provider.invalidateCredentials?.('tokens');
1261
+ }
1007
1262
 
1008
- await provider.saveTokens(newTokens);
1009
- return 'AUTHORIZED';
1263
+ try {
1264
+ if (storedAuthorizationServerInformation) {
1265
+ // Attempt to refresh the token
1266
+ const newTokens = await refreshAuthorization(authorizationServerUrl, {
1267
+ metadata,
1268
+ clientInformation,
1269
+ refreshToken: tokens.refresh_token,
1270
+ resource,
1271
+ addClientAuthentication: provider.addClientAuthentication,
1272
+ fetchFn,
1273
+ });
1274
+
1275
+ await provider.saveTokens(
1276
+ addAuthorizationServerInformationToTokens(
1277
+ newTokens,
1278
+ currentAuthorizationServerInformation,
1279
+ ),
1280
+ );
1281
+ return 'AUTHORIZED';
1282
+ }
1010
1283
  } catch (error) {
1011
1284
  if (
1012
1285
  // 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.
@@ -1021,6 +1294,7 @@ async function authInternal(
1021
1294
  }
1022
1295
  }
1023
1296
 
1297
+ /** Start authorization and persist the AS pin before redirecting */
1024
1298
  const state = provider.state ? await provider.state() : undefined;
1025
1299
  if (state && provider.saveState) {
1026
1300
  await provider.saveState(state);
@@ -1039,6 +1313,19 @@ async function authInternal(
1039
1313
  },
1040
1314
  );
1041
1315
 
1316
+ const savedAuthorizationServerInformation =
1317
+ await saveAuthorizationServerInformation({
1318
+ provider,
1319
+ clientInformation,
1320
+ authorizationServerInformation: currentAuthorizationServerInformation,
1321
+ });
1322
+ if (!savedAuthorizationServerInformation) {
1323
+ throw new MCPClientOAuthError({
1324
+ message:
1325
+ 'OAuth authorization server metadata must be saveable before starting authorization',
1326
+ });
1327
+ }
1328
+
1042
1329
  await provider.saveCodeVerifier(codeVerifier);
1043
1330
  await provider.redirectToAuthorization(authorizationUrl);
1044
1331
  return 'REDIRECT';
package/src/tool/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod/v4';
2
- import { JSONObject } from '@ai-sdk/provider';
3
- import { FlexibleSchema, Tool } from '@ai-sdk/provider-utils';
2
+ import type { JSONObject } from '@ai-sdk/provider';
3
+ import type { FlexibleSchema, Tool } from '@ai-sdk/provider-utils';
4
4
 
5
5
  export const LATEST_PROTOCOL_VERSION = '2025-11-25';
6
6
  export const SUPPORTED_PROTOCOL_VERSIONS = [
@@ -10,6 +10,13 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [
10
10
  '2024-11-05',
11
11
  ];
12
12
 
13
+ export type McpProviderMetadata = {
14
+ clientName?: string;
15
+ title?: string;
16
+ toolName?: string;
17
+ app?: JSONObject;
18
+ };
19
+
13
20
  /** MCP tool metadata - keys should follow MCP _meta key format specification */
14
21
  const ToolMetaSchema = z.optional(z.record(z.string(), z.unknown()));
15
22
  export type ToolMeta = z.infer<typeof ToolMetaSchema>;
@@ -56,8 +63,10 @@ export type McpToolSet<TOOL_SCHEMAS extends ToolSchemas = 'automatic'> =
56
63
  const ClientOrServerImplementationSchema = z.looseObject({
57
64
  name: z.string(),
58
65
  version: z.string(),
66
+ title: z.optional(z.string()),
59
67
  });
60
68
 
69
+ // Maps to `Implementation` in the MCP specification
61
70
  export type Configuration = z.infer<typeof ClientOrServerImplementationSchema>;
62
71
 
63
72
  export const BaseParamsSchema = z.looseObject({
@@ -232,10 +241,24 @@ const EmbeddedResourceSchema = z
232
241
  resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]),
233
242
  })
234
243
  .loose();
244
+ const ResourceLinkContentSchema = z
245
+ .object({
246
+ type: z.literal('resource_link'),
247
+ uri: z.string(),
248
+ name: z.string(),
249
+ description: z.optional(z.string()),
250
+ mimeType: z.optional(z.string()),
251
+ })
252
+ .loose();
235
253
 
236
254
  export const CallToolResultSchema = ResultSchema.extend({
237
255
  content: z.array(
238
- z.union([TextContentSchema, ImageContentSchema, EmbeddedResourceSchema]),
256
+ z.union([
257
+ TextContentSchema,
258
+ ImageContentSchema,
259
+ EmbeddedResourceSchema,
260
+ ResourceLinkContentSchema,
261
+ ]),
239
262
  ),
240
263
  /**
241
264
  * @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
@@ -304,6 +327,7 @@ const PromptMessageSchema = z
304
327
  TextContentSchema,
305
328
  ImageContentSchema,
306
329
  EmbeddedResourceSchema,
330
+ ResourceLinkContentSchema,
307
331
  ]),
308
332
  })
309
333
  .loose();
@@ -14,6 +14,19 @@ export function resourceUrlFromServerUrl(url: URL | string): URL {
14
14
  return resourceURL;
15
15
  }
16
16
 
17
+ /**
18
+ * Serializes a resource URL to a string, removing the trailing slash that
19
+ * URL.href adds to pathless URLs. Per the MCP spec, implementations SHOULD
20
+ * use the form without the trailing slash for better interoperability.
21
+ */
22
+ export function resourceUrlStripSlash(resource: URL): string {
23
+ const href = resource.href;
24
+ if (resource.pathname === '/' && href.endsWith('/')) {
25
+ return href.slice(0, -1);
26
+ }
27
+ return href;
28
+ }
29
+
17
30
  /**
18
31
  * Checks if a requested resource URL matches a configured resource URL.
19
32
  * A requested resource matches if it has the same scheme, domain, port,