@ai-sdk/mcp 2.0.31 → 2.0.33

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.
@@ -26,14 +26,22 @@ import {
26
26
  isCustomMcpTransport,
27
27
  type MCPTransport,
28
28
  type MCPTransportConfig,
29
+ type MCPTransportSendOptions,
29
30
  } from './mcp-transport';
30
31
  import { getMCPAppToolMeta, MCP_APP_MIME_TYPE } from './mcp-apps';
32
+ import {
33
+ createMCPToolHeaders,
34
+ getMCPToolHeaderBindings,
35
+ type MCPToolHeaderBinding,
36
+ } from './mcp-http-headers';
31
37
  import {
32
38
  CallToolResultSchema,
33
39
  CompleteResultSchema,
40
+ DiscoverResultSchema,
34
41
  ElicitationRequestSchema,
35
42
  ElicitResultSchema,
36
43
  InitializeResultSchema,
44
+ LATEST_LEGACY_PROTOCOL_VERSION,
37
45
  LATEST_PROTOCOL_VERSION,
38
46
  ListResourceTemplatesResultSchema,
39
47
  ListResourcesResultSchema,
@@ -66,9 +74,12 @@ import {
66
74
  type ToolMeta,
67
75
  type McpProviderMetadata,
68
76
  type InitializeResult,
77
+ type DiscoverResult,
69
78
  } from './types';
70
79
  const CLIENT_VERSION = '1.0.0';
71
80
  const DEFAULT_MAX_TOOL_CALL_RETRIES = 0;
81
+ const DEFAULT_PROTOCOL_DISCOVERY_TIMEOUT = 1000;
82
+ const MODERN_PROTOCOL_ERROR_CODES = [-32020, -32021, -32022];
72
83
 
73
84
  const DEFAULT_RETRY_ERROR_CODES = [
74
85
  'ConnectionRefused',
@@ -230,6 +241,16 @@ function mcpToModelOutput({
230
241
  export interface MCPClientConfig {
231
242
  /** Transport configuration for connecting to the MCP server */
232
243
  transport: MCPTransportConfig | MCPTransport;
244
+ /**
245
+ * Whether transports that support stateless protocol discovery should probe
246
+ * with `server/discover` before falling back to legacy initialization.
247
+ *
248
+ * Disable this for legacy servers that require `initialize` to be the first
249
+ * request.
250
+ *
251
+ * @default true
252
+ */
253
+ protocolVersionDiscovery?: boolean;
233
254
  /**
234
255
  * Options that bound or cancel transport startup and the initialize request.
235
256
  */
@@ -388,6 +409,7 @@ export interface MCPClient {
388
409
  */
389
410
  class DefaultMCPClient implements MCPClient {
390
411
  private transport: MCPTransport;
412
+ private protocolVersionDiscovery: boolean;
391
413
  private onUncaughtError?: (error: unknown) => void;
392
414
  private maxRetries: number;
393
415
  private clientInfo: ClientConfiguration;
@@ -402,11 +424,14 @@ class DefaultMCPClient implements MCPClient {
402
424
  private serverCapabilities: ServerCapabilities = {};
403
425
  private _serverInfo: Configuration = { name: '', version: '' };
404
426
  private _initializeResult: InitializeResult = {
405
- protocolVersion: LATEST_PROTOCOL_VERSION,
427
+ protocolVersion: LATEST_LEGACY_PROTOCOL_VERSION,
406
428
  capabilities: {},
407
429
  serverInfo: this._serverInfo,
408
430
  };
409
431
  private _serverInstructions?: string;
432
+ private protocolEra: 'legacy' | 'modern' = 'legacy';
433
+ private protocolVersion = LATEST_LEGACY_PROTOCOL_VERSION;
434
+ private toolHeaderBindings = new Map<string, MCPToolHeaderBinding[]>();
410
435
  private isClosed = true;
411
436
  private elicitationRequestHandler?: (
412
437
  request: ElicitationRequest,
@@ -422,12 +447,14 @@ class DefaultMCPClient implements MCPClient {
422
447
  capabilities,
423
448
  initialInitializeResult,
424
449
  initializationOptions,
450
+ protocolVersionDiscovery = true,
425
451
  }: MCPClientConfig) {
426
452
  this.onUncaughtError = onUncaughtError;
427
453
  this.maxRetries = prepareMaxRetries(maxRetries);
428
454
  this.clientCapabilities = capabilities ?? {};
429
455
  this.initialInitializeResult = initialInitializeResult;
430
456
  this.initializationOptions = initializationOptions;
457
+ this.protocolVersionDiscovery = protocolVersionDiscovery;
431
458
 
432
459
  if (isCustomMcpTransport(transportConfig)) {
433
460
  this.transport = transportConfig;
@@ -510,11 +537,25 @@ class DefaultMCPClient implements MCPClient {
510
537
  return this;
511
538
  }
512
539
 
540
+ if (
541
+ this.protocolVersionDiscovery &&
542
+ this.transport.supportsProtocolVersionDiscovery
543
+ ) {
544
+ const discovered = await this.tryProtocolDiscovery(signal);
545
+ if (discovered) {
546
+ return this;
547
+ }
548
+ }
549
+
550
+ this.protocolEra = 'legacy';
551
+ this.protocolVersion = LATEST_LEGACY_PROTOCOL_VERSION;
552
+ this.setTransportProtocolVersion(this.protocolVersion);
553
+
513
554
  const result = await this.request({
514
555
  request: {
515
556
  method: 'initialize',
516
557
  params: {
517
- protocolVersion: LATEST_PROTOCOL_VERSION,
558
+ protocolVersion: LATEST_LEGACY_PROTOCOL_VERSION,
518
559
  capabilities: this.clientCapabilities,
519
560
  clientInfo: this.clientInfo,
520
561
  },
@@ -565,6 +606,75 @@ class DefaultMCPClient implements MCPClient {
565
606
  }
566
607
  }
567
608
 
609
+ private async tryProtocolDiscovery(
610
+ signal: AbortSignal | undefined,
611
+ ): Promise<boolean> {
612
+ this.protocolEra = 'modern';
613
+ this.protocolVersion = LATEST_PROTOCOL_VERSION;
614
+ this.setTransportProtocolVersion(this.protocolVersion);
615
+
616
+ try {
617
+ const result = await this.request({
618
+ request: { method: 'server/discover' },
619
+ resultSchema: DiscoverResultSchema,
620
+ options: {
621
+ signal,
622
+ timeout: DEFAULT_PROTOCOL_DISCOVERY_TIMEOUT,
623
+ },
624
+ });
625
+
626
+ this.applyDiscoverResult(result);
627
+ return true;
628
+ } catch (error) {
629
+ if (
630
+ MCPClientError.isInstance(error) &&
631
+ error.code != null &&
632
+ MODERN_PROTOCOL_ERROR_CODES.includes(error.code)
633
+ ) {
634
+ throw error;
635
+ }
636
+
637
+ return false;
638
+ }
639
+ }
640
+
641
+ private applyDiscoverResult(result: DiscoverResult): void {
642
+ if (!result.supportedVersions.includes(this.protocolVersion)) {
643
+ throw new MCPClientError({
644
+ message: `Server does not support the requested protocol version: ${this.protocolVersion}`,
645
+ });
646
+ }
647
+
648
+ const serverInfo = result._meta?.['io.modelcontextprotocol/serverInfo'];
649
+ if (
650
+ serverInfo != null &&
651
+ typeof serverInfo === 'object' &&
652
+ 'name' in serverInfo &&
653
+ typeof serverInfo.name === 'string' &&
654
+ 'version' in serverInfo &&
655
+ typeof serverInfo.version === 'string'
656
+ ) {
657
+ this._serverInfo = serverInfo as Configuration;
658
+ }
659
+
660
+ this.serverCapabilities = result.capabilities;
661
+ this._serverInstructions = result.instructions;
662
+ this._initializeResult = {
663
+ protocolVersion: this.protocolVersion,
664
+ capabilities: result.capabilities,
665
+ serverInfo: this._serverInfo,
666
+ instructions: result.instructions,
667
+ };
668
+ }
669
+
670
+ private setTransportProtocolVersion(version: string): void {
671
+ if (this.transport.setProtocolVersion) {
672
+ this.transport.setProtocolVersion(version);
673
+ } else {
674
+ this.transport.protocolVersion = version;
675
+ }
676
+ }
677
+
568
678
  private applyInitializeResult(result: InitializeResult): void {
569
679
  if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
570
680
  throw new MCPClientError({
@@ -573,13 +683,11 @@ class DefaultMCPClient implements MCPClient {
573
683
  }
574
684
 
575
685
  this.serverCapabilities = result.capabilities;
686
+ this.protocolEra = 'legacy';
687
+ this.protocolVersion = result.protocolVersion;
576
688
  this._serverInfo = result.serverInfo;
577
689
  this._initializeResult = result;
578
- if (this.transport.setProtocolVersion) {
579
- this.transport.setProtocolVersion(result.protocolVersion);
580
- } else {
581
- this.transport.protocolVersion = result.protocolVersion;
582
- }
690
+ this.setTransportProtocolVersion(result.protocolVersion);
583
691
  this._serverInstructions = result.instructions;
584
692
  }
585
693
 
@@ -591,17 +699,17 @@ class DefaultMCPClient implements MCPClient {
591
699
 
592
700
  private send(
593
701
  message: JSONRPCMessage,
594
- signal: AbortSignal | undefined,
702
+ options?: MCPTransportSendOptions,
595
703
  ): Promise<void> {
596
- return this.transport.send(
597
- message,
598
- signal == null ? undefined : { signal },
599
- );
704
+ return options == null
705
+ ? this.transport.send(message)
706
+ : this.transport.send(message, options);
600
707
  }
601
708
 
602
709
  private assertCapability(method: string): void {
603
710
  switch (method) {
604
711
  case 'initialize':
712
+ case 'server/discover':
605
713
  break;
606
714
  case 'completion/complete':
607
715
  if (!this.serverCapabilities.completions) {
@@ -677,11 +785,29 @@ class DefaultMCPClient implements MCPClient {
677
785
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
678
786
 
679
787
  const messageId = this.requestMessageId++;
788
+ const preparedRequest =
789
+ this.protocolEra === 'modern'
790
+ ? {
791
+ ...request,
792
+ params: {
793
+ ...request.params,
794
+ _meta: {
795
+ ...request.params?._meta,
796
+ 'io.modelcontextprotocol/protocolVersion':
797
+ this.protocolVersion,
798
+ 'io.modelcontextprotocol/clientCapabilities':
799
+ this.clientCapabilities,
800
+ 'io.modelcontextprotocol/clientInfo': this.clientInfo,
801
+ },
802
+ },
803
+ }
804
+ : request;
680
805
  const jsonrpcRequest: JSONRPCRequest = {
681
- ...request,
806
+ ...preparedRequest,
682
807
  jsonrpc: '2.0',
683
808
  id: messageId,
684
809
  };
810
+ const headers = this.getToolRequestHeaders(preparedRequest);
685
811
 
686
812
  const rejectWithAbortError = () => {
687
813
  reject(
@@ -729,14 +855,31 @@ class DefaultMCPClient implements MCPClient {
729
855
  }
730
856
 
731
857
  try {
858
+ if (
859
+ this.protocolEra === 'modern' &&
860
+ response.result.resultType == null
861
+ ) {
862
+ throw new MCPClientError({
863
+ message: 'Modern MCP result is missing resultType',
864
+ });
865
+ }
866
+ if (response.result.resultType === 'input_required') {
867
+ throw new MCPClientError({
868
+ message:
869
+ 'Server requested additional input, but multi round-trip requests are not supported yet',
870
+ });
871
+ }
872
+
732
873
  const result = resultSchema.parse(response.result);
733
874
  cleanup();
734
875
  resolve(result);
735
876
  } catch (error) {
736
- const parseError = new MCPClientError({
737
- message: 'Failed to parse server response',
738
- cause: error,
739
- });
877
+ const parseError = MCPClientError.isInstance(error)
878
+ ? error
879
+ : new MCPClientError({
880
+ message: 'Failed to parse server response',
881
+ cause: error,
882
+ });
740
883
  rejectAndCleanup(parseError);
741
884
  }
742
885
  });
@@ -747,10 +890,14 @@ class DefaultMCPClient implements MCPClient {
747
890
  timeoutId = setTimeout(onTimeout, timeout);
748
891
  }
749
892
 
893
+ const sendOptions: MCPTransportSendOptions = {
894
+ ...(transportSignal == null ? {} : { signal: transportSignal }),
895
+ ...(headers == null ? {} : { headers }),
896
+ };
750
897
  const sendPromise =
751
- transportSignal == null
752
- ? this.transport.send(jsonrpcRequest)
753
- : this.send(jsonrpcRequest, transportSignal);
898
+ Object.keys(sendOptions).length === 0
899
+ ? this.send(jsonrpcRequest)
900
+ : this.send(jsonrpcRequest, sendOptions);
754
901
 
755
902
  sendPromise.catch(error => {
756
903
  rejectAndCleanup(error);
@@ -765,11 +912,78 @@ class DefaultMCPClient implements MCPClient {
765
912
  params?: PaginatedRequest['params'];
766
913
  options?: RequestOptions;
767
914
  } = {}): Promise<ListToolsResult> {
768
- return this.request({
915
+ const result = await this.request({
769
916
  request: { method: 'tools/list', params },
770
917
  resultSchema: ListToolsResultSchema,
771
918
  options,
772
919
  });
920
+ return this.prepareToolDefinitions(result, params?.cursor == null);
921
+ }
922
+
923
+ private prepareToolDefinitions(
924
+ definitions: ListToolsResult,
925
+ resetHeaderBindings = false,
926
+ ): ListToolsResult {
927
+ if (
928
+ this.protocolEra !== 'modern' ||
929
+ !this.transport.supportsMcpToolParameterHeaders
930
+ ) {
931
+ return definitions;
932
+ }
933
+
934
+ if (resetHeaderBindings) {
935
+ this.toolHeaderBindings.clear();
936
+ }
937
+ const tools = definitions.tools.filter(toolDefinition => {
938
+ const result = getMCPToolHeaderBindings(toolDefinition.inputSchema);
939
+ if (!result.success) {
940
+ this.onError(
941
+ new MCPClientError({
942
+ message: `Ignoring MCP tool "${toolDefinition.name}": ${result.error}`,
943
+ }),
944
+ );
945
+ return false;
946
+ }
947
+
948
+ this.toolHeaderBindings.set(toolDefinition.name, result.bindings);
949
+ return true;
950
+ });
951
+
952
+ return { ...definitions, tools };
953
+ }
954
+
955
+ private getToolRequestHeaders(
956
+ request: Request,
957
+ ): Record<string, string> | undefined {
958
+ if (
959
+ this.protocolEra !== 'modern' ||
960
+ request.method !== 'tools/call' ||
961
+ typeof request.params?.name !== 'string'
962
+ ) {
963
+ return undefined;
964
+ }
965
+
966
+ const bindings = this.toolHeaderBindings.get(request.params.name);
967
+ if (bindings == null || bindings.length === 0) {
968
+ return undefined;
969
+ }
970
+
971
+ const args = request.params.arguments;
972
+ if (args == null || typeof args !== 'object' || Array.isArray(args)) {
973
+ return undefined;
974
+ }
975
+
976
+ try {
977
+ return createMCPToolHeaders({
978
+ bindings,
979
+ args: args as Record<string, unknown>,
980
+ });
981
+ } catch (error) {
982
+ throw new MCPClientError({
983
+ message: `Failed to create MCP headers for tool "${request.params.name}"`,
984
+ cause: error,
985
+ });
986
+ }
773
987
  }
774
988
 
775
989
  private async callToolWithRetry({
@@ -934,7 +1148,10 @@ class DefaultMCPClient implements MCPClient {
934
1148
  jsonrpc: '2.0',
935
1149
  };
936
1150
  await waitForAbort(
937
- this.send(jsonrpcNotification, options?.signal),
1151
+ this.send(
1152
+ jsonrpcNotification,
1153
+ options?.signal == null ? undefined : { signal: options.signal },
1154
+ ),
938
1155
  options?.signal,
939
1156
  );
940
1157
  }
@@ -964,6 +1181,7 @@ class DefaultMCPClient implements MCPClient {
964
1181
  schemas?: TOOL_SCHEMAS;
965
1182
  },
966
1183
  ): McpToolSet<TOOL_SCHEMAS> {
1184
+ definitions = this.prepareToolDefinitions(definitions);
967
1185
  const tools: Record<string, Tool & { _meta?: ToolMeta }> = {};
968
1186
 
969
1187
  for (const {
@@ -1278,6 +1496,17 @@ class DefaultMCPClient implements MCPClient {
1278
1496
  }
1279
1497
 
1280
1498
  private onResponse(response: JSONRPCResponse | JSONRPCError): void {
1499
+ if (response.id == null) {
1500
+ this.onError(
1501
+ new MCPClientError({
1502
+ message: `Protocol error: Received a response without a message ID: ${JSON.stringify(
1503
+ response,
1504
+ )}`,
1505
+ }),
1506
+ );
1507
+ return;
1508
+ }
1509
+
1281
1510
  const messageId = Number(response.id);
1282
1511
  const handler = this.responseHandlers.get(messageId);
1283
1512
 
@@ -0,0 +1,161 @@
1
+ import { convertUint8ArrayToBase64, isRecord } from '@ai-sdk/provider-utils';
2
+
3
+ type HeaderValueType = 'boolean' | 'integer' | 'string';
4
+
5
+ export type MCPToolHeaderBinding = {
6
+ headerName: string;
7
+ path: string[];
8
+ valueType: HeaderValueType;
9
+ };
10
+
11
+ export type MCPToolHeaderBindingsResult =
12
+ | { success: true; bindings: MCPToolHeaderBinding[] }
13
+ | { success: false; error: string };
14
+
15
+ const HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
16
+ const BASE64_SENTINEL_PATTERN = /^=\?base64\?.*\?=$/;
17
+
18
+ export function encodeMCPHeaderValue(value: string): string {
19
+ const isPlainAscii = [...value].every(character => {
20
+ const code = character.charCodeAt(0);
21
+ return code === 0x09 || (code >= 0x20 && code <= 0x7e);
22
+ });
23
+
24
+ if (
25
+ isPlainAscii &&
26
+ value.trim() === value &&
27
+ !BASE64_SENTINEL_PATTERN.test(value)
28
+ ) {
29
+ return value;
30
+ }
31
+
32
+ return `=?base64?${convertUint8ArrayToBase64(new TextEncoder().encode(value))}?=`;
33
+ }
34
+
35
+ export function getMCPToolHeaderBindings(
36
+ inputSchema: unknown,
37
+ ): MCPToolHeaderBindingsResult {
38
+ if (!isRecord(inputSchema)) {
39
+ return {
40
+ success: false,
41
+ error: 'inputSchema must be a JSON Schema object',
42
+ };
43
+ }
44
+
45
+ const bindings: MCPToolHeaderBinding[] = [];
46
+ const headerNames = new Set<string>();
47
+ let error: string | undefined;
48
+
49
+ const visit = (
50
+ value: unknown,
51
+ path: string[],
52
+ staticallyReachable: boolean,
53
+ ): void => {
54
+ if (error != null || !isRecord(value)) {
55
+ return;
56
+ }
57
+
58
+ if ('x-mcp-header' in value) {
59
+ if (!staticallyReachable || path.length === 0) {
60
+ error = 'x-mcp-header is not on a statically reachable property';
61
+ return;
62
+ }
63
+
64
+ const headerName = value['x-mcp-header'];
65
+ if (
66
+ typeof headerName !== 'string' ||
67
+ !HTTP_TOKEN_PATTERN.test(headerName)
68
+ ) {
69
+ error = 'x-mcp-header must be a non-empty HTTP token';
70
+ return;
71
+ }
72
+
73
+ const normalizedHeaderName = headerName.toLowerCase();
74
+ if (headerNames.has(normalizedHeaderName)) {
75
+ error = `x-mcp-header value "${headerName}" is not unique`;
76
+ return;
77
+ }
78
+
79
+ const valueType = value.type;
80
+ if (
81
+ valueType !== 'boolean' &&
82
+ valueType !== 'integer' &&
83
+ valueType !== 'string'
84
+ ) {
85
+ error =
86
+ 'x-mcp-header can only annotate boolean, integer, or string properties';
87
+ return;
88
+ }
89
+
90
+ headerNames.add(normalizedHeaderName);
91
+ bindings.push({ headerName, path, valueType });
92
+ }
93
+
94
+ for (const [key, child] of Object.entries(value)) {
95
+ if (key === 'x-mcp-header') {
96
+ continue;
97
+ }
98
+
99
+ if (key === 'properties' && isRecord(child)) {
100
+ for (const [propertyName, propertySchema] of Object.entries(child)) {
101
+ visit(propertySchema, [...path, propertyName], staticallyReachable);
102
+ }
103
+ } else {
104
+ visit(child, path, false);
105
+ }
106
+ }
107
+ };
108
+
109
+ visit(inputSchema, [], true);
110
+
111
+ return error == null
112
+ ? { success: true, bindings }
113
+ : { success: false, error };
114
+ }
115
+
116
+ function getValueAtPath(
117
+ value: Record<string, unknown>,
118
+ path: string[],
119
+ ): unknown {
120
+ let current: unknown = value;
121
+ for (const segment of path) {
122
+ if (!isRecord(current)) {
123
+ return undefined;
124
+ }
125
+ current = current[segment];
126
+ }
127
+ return current;
128
+ }
129
+
130
+ export function createMCPToolHeaders({
131
+ bindings,
132
+ args,
133
+ }: {
134
+ bindings: MCPToolHeaderBinding[];
135
+ args: Record<string, unknown>;
136
+ }): Record<string, string> {
137
+ const headers: Record<string, string> = {};
138
+
139
+ for (const binding of bindings) {
140
+ const value = getValueAtPath(args, binding.path);
141
+ if (value == null) {
142
+ continue;
143
+ }
144
+
145
+ if (
146
+ (binding.valueType === 'string' && typeof value !== 'string') ||
147
+ (binding.valueType === 'boolean' && typeof value !== 'boolean') ||
148
+ (binding.valueType === 'integer' && !Number.isSafeInteger(value))
149
+ ) {
150
+ throw new TypeError(
151
+ `Tool argument "${binding.path.join('.')}" does not match its x-mcp-header type`,
152
+ );
153
+ }
154
+
155
+ headers[`Mcp-Param-${binding.headerName}`] = encodeMCPHeaderValue(
156
+ String(value),
157
+ );
158
+ }
159
+
160
+ return headers;
161
+ }