@ai-sdk/mcp 0.0.38 → 0.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @ai-sdk/mcp
2
2
 
3
+ ## 0.0.40
4
+
5
+ ### Patch Changes
6
+
7
+ - c87913e: fix(mcp): allow MCP tools to execute without options
8
+
9
+ ## 0.0.39
10
+
11
+ ### Patch Changes
12
+
13
+ - 2b1c2df: fix(mcp): reject private OAuth endpoints before sending credentials
14
+ - 2b1c2df: Keep default Node.js downloads protected by DNS validation and connection pinning when frameworks or instrumentation wrap global fetch before or after the SDK loads.
15
+ - 2b1c2df: fix(mcp): prevent SSRF in OAuth metadata discovery
16
+ - Updated dependencies [2b1c2df]
17
+ - Updated dependencies [2b1c2df]
18
+ - Updated dependencies [2b1c2df]
19
+ - @ai-sdk/provider-utils@3.0.41
20
+
3
21
  ## 0.0.38
4
22
 
5
23
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod/v4';
2
- import { FetchFunction, FlexibleSchema, Tool } from '@ai-sdk/provider-utils';
2
+ import { FetchFunction, FlexibleSchema, Tool, ToolCallOptions } from '@ai-sdk/provider-utils';
3
3
  import { JSONObject } from '@ai-sdk/provider';
4
4
 
5
5
  declare const JSONRPCRequestSchema: z.ZodObject<{
@@ -245,12 +245,16 @@ type MCPTransportConfig = {
245
245
  type ToolSchemas = Record<string, {
246
246
  inputSchema: FlexibleSchema<JSONObject | unknown>;
247
247
  }> | 'automatic' | undefined;
248
+ /** Base MCP tool type with execute */
249
+ type McpToolBase<INPUT = unknown, OUTPUT = CallToolResult> = Tool<INPUT, OUTPUT> & {
250
+ execute: (input: INPUT, options?: ToolCallOptions) => ReturnType<NonNullable<Tool<INPUT, OUTPUT>['execute']>>;
251
+ };
248
252
  type McpToolSet<TOOL_SCHEMAS extends ToolSchemas = 'automatic'> = TOOL_SCHEMAS extends Record<string, {
249
253
  inputSchema: FlexibleSchema<any>;
250
254
  }> ? {
251
255
  [K in keyof TOOL_SCHEMAS]: TOOL_SCHEMAS[K] extends {
252
256
  inputSchema: FlexibleSchema<infer INPUT>;
253
- } ? Tool<INPUT, CallToolResult> & Required<Pick<Tool<INPUT, CallToolResult>, 'execute'>> : never;
257
+ } ? McpToolBase<INPUT, CallToolResult> : never;
254
258
  } : McpToolSet<Record<string, {
255
259
  inputSchema: FlexibleSchema<unknown>;
256
260
  }>>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod/v4';
2
- import { FetchFunction, FlexibleSchema, Tool } from '@ai-sdk/provider-utils';
2
+ import { FetchFunction, FlexibleSchema, Tool, ToolCallOptions } from '@ai-sdk/provider-utils';
3
3
  import { JSONObject } from '@ai-sdk/provider';
4
4
 
5
5
  declare const JSONRPCRequestSchema: z.ZodObject<{
@@ -245,12 +245,16 @@ type MCPTransportConfig = {
245
245
  type ToolSchemas = Record<string, {
246
246
  inputSchema: FlexibleSchema<JSONObject | unknown>;
247
247
  }> | 'automatic' | undefined;
248
+ /** Base MCP tool type with execute */
249
+ type McpToolBase<INPUT = unknown, OUTPUT = CallToolResult> = Tool<INPUT, OUTPUT> & {
250
+ execute: (input: INPUT, options?: ToolCallOptions) => ReturnType<NonNullable<Tool<INPUT, OUTPUT>['execute']>>;
251
+ };
248
252
  type McpToolSet<TOOL_SCHEMAS extends ToolSchemas = 'automatic'> = TOOL_SCHEMAS extends Record<string, {
249
253
  inputSchema: FlexibleSchema<any>;
250
254
  }> ? {
251
255
  [K in keyof TOOL_SCHEMAS]: TOOL_SCHEMAS[K] extends {
252
256
  inputSchema: FlexibleSchema<infer INPUT>;
253
- } ? Tool<INPUT, CallToolResult> & Required<Pick<Tool<INPUT, CallToolResult>, 'execute'>> : never;
257
+ } ? McpToolBase<INPUT, CallToolResult> : never;
254
258
  } : McpToolSet<Record<string, {
255
259
  inputSchema: FlexibleSchema<unknown>;
256
260
  }>>;
package/dist/index.js CHANGED
@@ -503,13 +503,48 @@ function buildWellKnownPath(wellKnownPrefix, pathname = "", options = {}) {
503
503
  }
504
504
  return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
505
505
  }
506
- async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
506
+ function isOAuthLoopbackHost(hostname) {
507
+ const normalized = hostname.toLowerCase().replace(/\.+$/, "");
508
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "127.0.0.1" || normalized === "[::1]" || normalized === "::1";
509
+ }
510
+ function assertSafeOAuthEndpoint(endpointUrl, { allowLoopback = false } = {}) {
511
+ if (allowLoopback && (endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) {
512
+ return;
513
+ }
514
+ try {
515
+ (0, import_provider_utils2.validateDownloadUrl)(endpointUrl.href);
516
+ } catch (error) {
517
+ throw new MCPClientOAuthError({
518
+ message: `OAuth endpoint URL is not allowed: ${endpointUrl.href}`,
519
+ cause: error
520
+ });
521
+ }
522
+ }
523
+ function getTrustedLoopbackOrigin(authorizationServerUrl) {
524
+ const url = new URL(authorizationServerUrl);
525
+ return isOAuthLoopbackHost(url.hostname) ? url.origin : void 0;
526
+ }
527
+ async function fetchWithCorsRetry(url, headers, fetchFn = fetch, trustedOrigin) {
507
528
  try {
508
- return await fetchFn(url, { headers });
529
+ return await (0, import_provider_utils2.fetchWithValidatedRedirects)({
530
+ url: url.href,
531
+ fetch: async (input, init) => (0, import_provider_utils2.fetchWithValidatedEndpoint)({
532
+ url: input,
533
+ init: {
534
+ ...init,
535
+ headers: new Headers(init == null ? void 0 : init.headers).has("MCP-Protocol-Version") ? headers : void 0
536
+ },
537
+ fetch: fetchFn,
538
+ trustedOrigin,
539
+ redirect: "manual"
540
+ }),
541
+ headers,
542
+ trustedOrigin
543
+ });
509
544
  } catch (error) {
510
545
  if (error instanceof TypeError) {
511
546
  if (headers) {
512
- return fetchWithCorsRetry(url, void 0, fetchFn);
547
+ return fetchWithCorsRetry(url, void 0, fetchFn, trustedOrigin);
513
548
  } else {
514
549
  return void 0;
515
550
  }
@@ -517,11 +552,11 @@ async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
517
552
  throw error;
518
553
  }
519
554
  }
520
- async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
555
+ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch, trustedOrigin) {
521
556
  const headers = {
522
557
  "MCP-Protocol-Version": protocolVersion
523
558
  };
524
- return await fetchWithCorsRetry(url, headers, fetchFn);
559
+ return await fetchWithCorsRetry(url, headers, fetchFn, trustedOrigin);
525
560
  }
526
561
  function shouldAttemptFallback(response, pathname) {
527
562
  return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
@@ -538,10 +573,20 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
538
573
  url = new URL(wellKnownPath, (_b3 = opts == null ? void 0 : opts.metadataServerUrl) != null ? _b3 : issuer);
539
574
  url.search = issuer.search;
540
575
  }
541
- let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
576
+ let response = await tryMetadataDiscovery(
577
+ url,
578
+ protocolVersion,
579
+ fetchFn,
580
+ opts == null ? void 0 : opts.trustedOrigin
581
+ );
542
582
  if (!(opts == null ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
543
583
  const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
544
- response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
584
+ response = await tryMetadataDiscovery(
585
+ rootUrl,
586
+ protocolVersion,
587
+ fetchFn,
588
+ opts == null ? void 0 : opts.trustedOrigin
589
+ );
545
590
  }
546
591
  return response;
547
592
  }
@@ -552,7 +597,10 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn =
552
597
  fetchFn,
553
598
  {
554
599
  protocolVersion: opts == null ? void 0 : opts.protocolVersion,
555
- metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl
600
+ metadataUrl: opts == null ? void 0 : opts.resourceMetadataUrl,
601
+ // The configured MCP server is trusted as a request target. Redirects
602
+ // crossing its origin are still validated before they are followed.
603
+ trustedOrigin: new URL(serverUrl).origin
556
604
  }
557
605
  );
558
606
  if (!response || response.status === 404) {
@@ -609,13 +657,19 @@ function buildDiscoveryUrls(authorizationServerUrl) {
609
657
  }
610
658
  async function discoverAuthorizationServerMetadata(authorizationServerUrl, {
611
659
  fetchFn = fetch,
612
- protocolVersion = LATEST_PROTOCOL_VERSION
660
+ protocolVersion = LATEST_PROTOCOL_VERSION,
661
+ trustedOrigin
613
662
  } = {}) {
614
663
  var _a3;
615
664
  const headers = { "MCP-Protocol-Version": protocolVersion };
616
665
  const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
617
666
  for (const { url: endpointUrl, type } of urlsToTry) {
618
- const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
667
+ const response = await fetchWithCorsRetry(
668
+ endpointUrl,
669
+ headers,
670
+ fetchFn,
671
+ trustedOrigin
672
+ );
619
673
  if (!response) {
620
674
  continue;
621
675
  }
@@ -775,6 +829,10 @@ async function exchangeAuthorization(authorizationServerUrl, {
775
829
  var _a3;
776
830
  const grantType = "authorization_code";
777
831
  const tokenUrl = (metadata == null ? void 0 : metadata.token_endpoint) ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
832
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
833
+ assertSafeOAuthEndpoint(tokenUrl, {
834
+ allowLoopback: tokenUrl.origin === trustedOrigin
835
+ });
778
836
  if ((metadata == null ? void 0 : metadata.grant_types_supported) && !metadata.grant_types_supported.includes(grantType)) {
779
837
  throw new Error(
780
838
  `Incompatible auth server: does not support grant type ${grantType}`
@@ -803,10 +861,15 @@ async function exchangeAuthorization(authorizationServerUrl, {
803
861
  if (resource) {
804
862
  params.set("resource", resource.href);
805
863
  }
806
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
807
- method: "POST",
808
- headers,
809
- body: params
864
+ const response = await (0, import_provider_utils2.fetchWithValidatedEndpoint)({
865
+ url: tokenUrl,
866
+ init: {
867
+ method: "POST",
868
+ headers,
869
+ body: params
870
+ },
871
+ fetch: fetchFn,
872
+ trustedOrigin
810
873
  });
811
874
  if (!response.ok) {
812
875
  throw await parseErrorResponse(response);
@@ -834,6 +897,10 @@ async function refreshAuthorization(authorizationServerUrl, {
834
897
  } else {
835
898
  tokenUrl = new URL("/token", authorizationServerUrl);
836
899
  }
900
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
901
+ assertSafeOAuthEndpoint(tokenUrl, {
902
+ allowLoopback: tokenUrl.origin === trustedOrigin
903
+ });
837
904
  const headers = new Headers({
838
905
  "Content-Type": "application/x-www-form-urlencoded"
839
906
  });
@@ -854,10 +921,15 @@ async function refreshAuthorization(authorizationServerUrl, {
854
921
  if (resource) {
855
922
  params.set("resource", resource.href);
856
923
  }
857
- const response = await (fetchFn != null ? fetchFn : fetch)(tokenUrl, {
858
- method: "POST",
859
- headers,
860
- body: params
924
+ const response = await (0, import_provider_utils2.fetchWithValidatedEndpoint)({
925
+ url: tokenUrl,
926
+ init: {
927
+ method: "POST",
928
+ headers,
929
+ body: params
930
+ },
931
+ fetch: fetchFn,
932
+ trustedOrigin
861
933
  });
862
934
  if (!response.ok) {
863
935
  throw await parseErrorResponse(response);
@@ -883,12 +955,21 @@ async function registerClient(authorizationServerUrl, {
883
955
  } else {
884
956
  registrationUrl = new URL("/register", authorizationServerUrl);
885
957
  }
886
- const response = await (fetchFn != null ? fetchFn : fetch)(registrationUrl, {
887
- method: "POST",
888
- headers: {
889
- "Content-Type": "application/json"
958
+ const trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl);
959
+ assertSafeOAuthEndpoint(registrationUrl, {
960
+ allowLoopback: registrationUrl.origin === trustedOrigin
961
+ });
962
+ const response = await (0, import_provider_utils2.fetchWithValidatedEndpoint)({
963
+ url: registrationUrl,
964
+ init: {
965
+ method: "POST",
966
+ headers: {
967
+ "Content-Type": "application/json"
968
+ },
969
+ body: JSON.stringify(clientMetadata)
890
970
  },
891
- body: JSON.stringify(clientMetadata)
971
+ fetch: fetchFn,
972
+ trustedOrigin
892
973
  });
893
974
  if (!response.ok) {
894
975
  throw await parseErrorResponse(response);
@@ -954,6 +1035,14 @@ async function authInternal(provider, {
954
1035
  if (!authorizationServerUrl) {
955
1036
  authorizationServerUrl = serverUrl;
956
1037
  }
1038
+ const parsedServerUrl = new URL(serverUrl);
1039
+ const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
1040
+ const serverOrigin = parsedServerUrl.origin;
1041
+ const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
1042
+ const trustedAuthorizationServerOrigin = authorizationServerOrigin === serverOrigin || isOAuthLoopbackHost(parsedServerUrl.hostname) && isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname) ? authorizationServerOrigin : void 0;
1043
+ if (!trustedAuthorizationServerOrigin) {
1044
+ assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
1045
+ }
957
1046
  const resource = await selectResourceURL(
958
1047
  serverUrl,
959
1048
  provider,
@@ -962,7 +1051,8 @@ async function authInternal(provider, {
962
1051
  const metadata = await discoverAuthorizationServerMetadata(
963
1052
  authorizationServerUrl,
964
1053
  {
965
- fetchFn
1054
+ fetchFn,
1055
+ trustedOrigin: trustedAuthorizationServerOrigin
966
1056
  }
967
1057
  );
968
1058
  let clientInformation = await Promise.resolve(provider.clientInformation());