@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.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -0
- package/dist/index.d.ts +54 -3
- package/dist/index.js +472 -67
- package/dist/index.js.map +1 -1
- package/dist/mcp-stdio/index.d.ts +19 -1
- package/dist/mcp-stdio/index.js +15 -5
- package/dist/mcp-stdio/index.js.map +1 -1
- package/package.json +1 -1
- package/src/tool/json-rpc-message.ts +1 -1
- package/src/tool/mcp-client.ts +251 -22
- package/src/tool/mcp-http-headers.ts +161 -0
- package/src/tool/mcp-http-transport.ts +119 -16
- package/src/tool/mcp-sse-transport.ts +12 -5
- package/src/tool/mcp-stdio/mcp-stdio-transport.ts +1 -0
- package/src/tool/mcp-transport.ts +25 -1
- package/src/tool/mock-mcp-transport.ts +2 -2
- package/src/tool/oauth-types.ts +9 -0
- package/src/tool/oauth.ts +126 -20
- package/src/tool/types.ts +19 -8
package/src/tool/mcp-client.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
702
|
+
options?: MCPTransportSendOptions,
|
|
595
703
|
): Promise<void> {
|
|
596
|
-
return
|
|
597
|
-
message
|
|
598
|
-
|
|
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
|
-
...
|
|
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 =
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
752
|
-
? this.
|
|
753
|
-
: this.send(jsonrpcRequest,
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|