@openai/agents-core 0.7.2 → 0.8.0
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/dist/extensions/handoffFilters.js +4 -1
- package/dist/extensions/handoffFilters.js.map +1 -1
- package/dist/extensions/handoffFilters.mjs +5 -2
- package/dist/extensions/handoffFilters.mjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/mcp.d.ts +107 -3
- package/dist/mcp.js +64 -9
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.mjs +64 -9
- package/dist/mcp.mjs.map +1 -1
- package/dist/metadata.js +2 -2
- package/dist/metadata.mjs +2 -2
- package/dist/runner/turnResolution.js +1 -1
- package/dist/runner/turnResolution.js.map +1 -1
- package/dist/runner/turnResolution.mjs +2 -2
- package/dist/runner/turnResolution.mjs.map +1 -1
- package/dist/shims/mcp-server/browser.d.ts +11 -1
- package/dist/shims/mcp-server/browser.js +30 -0
- package/dist/shims/mcp-server/browser.js.map +1 -1
- package/dist/shims/mcp-server/browser.mjs +30 -0
- package/dist/shims/mcp-server/browser.mjs.map +1 -1
- package/dist/shims/mcp-server/node.d.ts +30 -1
- package/dist/shims/mcp-server/node.js +574 -42
- package/dist/shims/mcp-server/node.js.map +1 -1
- package/dist/shims/mcp-server/node.mjs +574 -42
- package/dist/shims/mcp-server/node.mjs.map +1 -1
- package/dist/utils/messages.d.ts +6 -0
- package/dist/utils/messages.js +34 -6
- package/dist/utils/messages.js.map +1 -1
- package/dist/utils/messages.mjs +33 -6
- package/dist/utils/messages.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -61,6 +61,63 @@ function hasSessionTransport(transport) {
|
|
|
61
61
|
(typeof transport.terminateSession === 'function' ||
|
|
62
62
|
transport.sessionId !== undefined));
|
|
63
63
|
}
|
|
64
|
+
function getTransportProtocolVersion(transport) {
|
|
65
|
+
if (transport != null &&
|
|
66
|
+
typeof transport.protocolVersion ===
|
|
67
|
+
'string') {
|
|
68
|
+
return transport.protocolVersion;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function isNotConnectedError(error, client) {
|
|
73
|
+
return (error instanceof Error &&
|
|
74
|
+
error.message === 'Not connected' &&
|
|
75
|
+
client.transport == null);
|
|
76
|
+
}
|
|
77
|
+
function attachCause(error, cause) {
|
|
78
|
+
if (!(error instanceof Error)) {
|
|
79
|
+
return error;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
if (error.cause === undefined) {
|
|
83
|
+
Object.defineProperty(error, 'cause', {
|
|
84
|
+
value: cause,
|
|
85
|
+
configurable: true,
|
|
86
|
+
enumerable: false,
|
|
87
|
+
writable: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Best effort only.
|
|
93
|
+
}
|
|
94
|
+
return error;
|
|
95
|
+
}
|
|
96
|
+
function getSessionId(transport) {
|
|
97
|
+
return hasSessionTransport(transport) ? transport.sessionId : undefined;
|
|
98
|
+
}
|
|
99
|
+
function shouldTerminateTransportSession(transportToClose, activeTransport) {
|
|
100
|
+
const closingSessionId = getSessionId(transportToClose);
|
|
101
|
+
if (closingSessionId === undefined) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const activeSessionId = getSessionId(activeTransport);
|
|
105
|
+
return activeSessionId === undefined || activeSessionId !== closingSessionId;
|
|
106
|
+
}
|
|
107
|
+
function withTimeout(promise, timeoutMs, onTimeout) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timeoutId = setTimeout(() => {
|
|
110
|
+
reject(onTimeout());
|
|
111
|
+
}, timeoutMs);
|
|
112
|
+
void promise.then((value) => {
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
resolve(value);
|
|
115
|
+
}, (error) => {
|
|
116
|
+
clearTimeout(timeoutId);
|
|
117
|
+
reject(error);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
64
121
|
class NodeMCPServerStdio extends mcp_1.BaseMCPServerStdio {
|
|
65
122
|
session = null;
|
|
66
123
|
_cacheDirty = true;
|
|
@@ -157,6 +214,36 @@ class NodeMCPServerStdio extends mcp_1.BaseMCPServerStdio {
|
|
|
157
214
|
this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
|
|
158
215
|
return result;
|
|
159
216
|
}
|
|
217
|
+
async listResources(params) {
|
|
218
|
+
const { ListResourcesResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
219
|
+
if (!this.session) {
|
|
220
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
221
|
+
}
|
|
222
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
223
|
+
const response = await this.session.listResources(params, requestOptions);
|
|
224
|
+
this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
|
|
225
|
+
return ListResourcesResultSchema.parse(response);
|
|
226
|
+
}
|
|
227
|
+
async listResourceTemplates(params) {
|
|
228
|
+
const { ListResourceTemplatesResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
229
|
+
if (!this.session) {
|
|
230
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
231
|
+
}
|
|
232
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
233
|
+
const response = await this.session.listResourceTemplates(params, requestOptions);
|
|
234
|
+
this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
|
|
235
|
+
return ListResourceTemplatesResultSchema.parse(response);
|
|
236
|
+
}
|
|
237
|
+
async readResource(uri) {
|
|
238
|
+
const { ReadResourceResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
239
|
+
if (!this.session) {
|
|
240
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
241
|
+
}
|
|
242
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
243
|
+
const response = await this.session.readResource({ uri }, requestOptions);
|
|
244
|
+
this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
|
|
245
|
+
return ReadResourceResultSchema.parse(response);
|
|
246
|
+
}
|
|
160
247
|
get name() {
|
|
161
248
|
return this._name;
|
|
162
249
|
}
|
|
@@ -263,6 +350,36 @@ class NodeMCPServerSSE extends mcp_1.BaseMCPServerSSE {
|
|
|
263
350
|
this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
|
|
264
351
|
return result;
|
|
265
352
|
}
|
|
353
|
+
async listResources(params) {
|
|
354
|
+
const { ListResourcesResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
355
|
+
if (!this.session) {
|
|
356
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
357
|
+
}
|
|
358
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
359
|
+
const response = await this.session.listResources(params, requestOptions);
|
|
360
|
+
this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
|
|
361
|
+
return ListResourcesResultSchema.parse(response);
|
|
362
|
+
}
|
|
363
|
+
async listResourceTemplates(params) {
|
|
364
|
+
const { ListResourceTemplatesResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
365
|
+
if (!this.session) {
|
|
366
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
367
|
+
}
|
|
368
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
369
|
+
const response = await this.session.listResourceTemplates(params, requestOptions);
|
|
370
|
+
this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
|
|
371
|
+
return ListResourceTemplatesResultSchema.parse(response);
|
|
372
|
+
}
|
|
373
|
+
async readResource(uri) {
|
|
374
|
+
const { ReadResourceResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
375
|
+
if (!this.session) {
|
|
376
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
377
|
+
}
|
|
378
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
379
|
+
const response = await this.session.readResource({ uri }, requestOptions);
|
|
380
|
+
this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
|
|
381
|
+
return ReadResourceResultSchema.parse(response);
|
|
382
|
+
}
|
|
266
383
|
get name() {
|
|
267
384
|
return this._name;
|
|
268
385
|
}
|
|
@@ -302,6 +419,10 @@ class NodeMCPServerStreamableHttp extends mcp_1.BaseMCPServerStreamableHttp {
|
|
|
302
419
|
params;
|
|
303
420
|
_name;
|
|
304
421
|
transport = null;
|
|
422
|
+
reconnectingClientPromise = null;
|
|
423
|
+
reconnectingClientTarget = null;
|
|
424
|
+
isClosed = false;
|
|
425
|
+
connectionStateVersion = 0;
|
|
305
426
|
constructor(params) {
|
|
306
427
|
super(params);
|
|
307
428
|
this.clientSessionTimeoutSeconds = params.clientSessionTimeoutSeconds ?? 5;
|
|
@@ -309,30 +430,374 @@ class NodeMCPServerStreamableHttp extends mcp_1.BaseMCPServerStreamableHttp {
|
|
|
309
430
|
this._name = params.name || `streamable-http: ${this.params.url}`;
|
|
310
431
|
this.timeout = params.timeout ?? protocol_js_1.DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
311
432
|
}
|
|
433
|
+
async loadStreamableHttpRuntime() {
|
|
434
|
+
const [clientModule, transportModule, typesModule] = await Promise.all([
|
|
435
|
+
Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/index.js'))).catch(failedToImport),
|
|
436
|
+
Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js'))).catch(failedToImport),
|
|
437
|
+
Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport),
|
|
438
|
+
]);
|
|
439
|
+
return { clientModule, transportModule, typesModule };
|
|
440
|
+
}
|
|
441
|
+
createStreamableHttpTransport(StreamableHTTPClientTransport, options = {}) {
|
|
442
|
+
const transportOptions = {
|
|
443
|
+
authProvider: this.params.authProvider,
|
|
444
|
+
requestInit: this.params.requestInit,
|
|
445
|
+
fetch: this.params.fetch,
|
|
446
|
+
reconnectionOptions: this.params.reconnectionOptions,
|
|
447
|
+
sessionId: options.sessionId,
|
|
448
|
+
};
|
|
449
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.params.url), transportOptions);
|
|
450
|
+
if (options.protocolVersion !== undefined &&
|
|
451
|
+
typeof transport.setProtocolVersion === 'function') {
|
|
452
|
+
transport.setProtocolVersion(options.protocolVersion);
|
|
453
|
+
}
|
|
454
|
+
return transport;
|
|
455
|
+
}
|
|
456
|
+
async createConnectedStreamableHttpClient(options = {}) {
|
|
457
|
+
const { clientModule, transportModule } = await this.loadStreamableHttpRuntime();
|
|
458
|
+
const { Client } = clientModule;
|
|
459
|
+
const { StreamableHTTPClientTransport } = transportModule;
|
|
460
|
+
const transport = this.createStreamableHttpTransport(StreamableHTTPClientTransport, options);
|
|
461
|
+
const client = new Client({
|
|
462
|
+
name: this._name,
|
|
463
|
+
version: '1.0.0',
|
|
464
|
+
});
|
|
465
|
+
try {
|
|
466
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
467
|
+
await client.connect(transport, requestOptions);
|
|
468
|
+
return { client, transport };
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
await this.closeStreamableHttpClient({
|
|
472
|
+
client,
|
|
473
|
+
transport,
|
|
474
|
+
}, {
|
|
475
|
+
terminateSession: options.sessionId === undefined,
|
|
476
|
+
closeWarningMessage: 'Failed to close failed MCP connect client:',
|
|
477
|
+
terminateWarningMessage: 'Failed to terminate failed MCP connect session:',
|
|
478
|
+
});
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
getClientSessionTimeoutMs() {
|
|
483
|
+
return Math.max(1, (this.clientSessionTimeoutSeconds ?? 5) * 1000);
|
|
484
|
+
}
|
|
485
|
+
resetClientToolMetadataCache(client) {
|
|
486
|
+
client.cacheToolMetadata?.([]);
|
|
487
|
+
}
|
|
488
|
+
async publishConnectedStreamableHttpClient(args) {
|
|
489
|
+
this.transport = args.transport;
|
|
490
|
+
this.session = args.client;
|
|
491
|
+
this.connectionStateVersion += 1;
|
|
492
|
+
this._cacheDirty = true;
|
|
493
|
+
this._toolsList = [];
|
|
494
|
+
const previousSessionId = getSessionId(args.previousTransport);
|
|
495
|
+
const nextSessionId = getSessionId(args.transport);
|
|
496
|
+
if (previousSessionId === undefined ||
|
|
497
|
+
nextSessionId === undefined ||
|
|
498
|
+
previousSessionId !== nextSessionId) {
|
|
499
|
+
this.resetClientToolMetadataCache(args.client);
|
|
500
|
+
}
|
|
501
|
+
await (0, mcp_1.invalidateServerToolsCache)(this.name);
|
|
502
|
+
}
|
|
503
|
+
async clearPublishedStreamableHttpClientIfCurrent(args) {
|
|
504
|
+
if (this.connectionStateVersion !== args.stateVersion ||
|
|
505
|
+
this.session !== args.client ||
|
|
506
|
+
this.transport !== args.transport) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.session = null;
|
|
510
|
+
this.transport = null;
|
|
511
|
+
this._cacheDirty = true;
|
|
512
|
+
this._toolsList = [];
|
|
513
|
+
await (0, mcp_1.invalidateServerToolsCache)(this.name);
|
|
514
|
+
}
|
|
515
|
+
async reopenSharedStreamableHttpSession(client) {
|
|
516
|
+
await withTimeout(client.notification({ method: 'notifications/initialized' }), this.getClientSessionTimeoutMs(), () => new Error('Timed out reopening shared streamable HTTP MCP session.'));
|
|
517
|
+
}
|
|
518
|
+
async terminateDetachedStreamableHttpSession(transport, warningMessage) {
|
|
519
|
+
const sessionId = getSessionId(transport);
|
|
520
|
+
if (sessionId === undefined) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const { transportModule } = await this.loadStreamableHttpRuntime();
|
|
525
|
+
const detachedTransport = this.createStreamableHttpTransport(transportModule.StreamableHTTPClientTransport, {
|
|
526
|
+
protocolVersion: getTransportProtocolVersion(transport),
|
|
527
|
+
sessionId,
|
|
528
|
+
});
|
|
529
|
+
try {
|
|
530
|
+
if (typeof detachedTransport.terminateSession === 'function') {
|
|
531
|
+
await detachedTransport.terminateSession();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
await detachedTransport.close().catch(() => { });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
this.logger.warn(warningMessage, error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async closeCloseOwnedStreamableHttpState(args) {
|
|
543
|
+
const { client, transport, closeStateVersion, closeWarningMessage, terminateWarningMessage, } = args;
|
|
544
|
+
if (client && transport) {
|
|
545
|
+
await this.closeStreamableHttpClient({
|
|
546
|
+
client,
|
|
547
|
+
transport,
|
|
548
|
+
}, {
|
|
549
|
+
terminateSession: false,
|
|
550
|
+
closeWarningMessage,
|
|
551
|
+
terminateWarningMessage,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
else if (transport) {
|
|
555
|
+
await transport.close().catch((error) => {
|
|
556
|
+
this.logger.warn(closeWarningMessage, error);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
else if (client) {
|
|
560
|
+
await client.close().catch((error) => {
|
|
561
|
+
this.logger.warn(closeWarningMessage, error);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (!transport ||
|
|
565
|
+
this.connectionStateVersion !== closeStateVersion ||
|
|
566
|
+
!this.isClosed ||
|
|
567
|
+
!shouldTerminateTransportSession(transport, this.transport)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
await this.terminateDetachedStreamableHttpSession(transport, terminateWarningMessage);
|
|
571
|
+
}
|
|
572
|
+
async callToolWithClient(client, toolName, args, meta) {
|
|
573
|
+
const { CallToolResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
574
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds, {
|
|
575
|
+
timeout: this.timeout,
|
|
576
|
+
});
|
|
577
|
+
const params = {
|
|
578
|
+
name: toolName,
|
|
579
|
+
arguments: args ?? {},
|
|
580
|
+
...(meta != null ? { _meta: meta } : {}),
|
|
581
|
+
};
|
|
582
|
+
const response = await client.callTool(params, undefined, requestOptions);
|
|
583
|
+
const parsed = CallToolResultSchema.parse(response);
|
|
584
|
+
return parsed.content;
|
|
585
|
+
}
|
|
586
|
+
async closeStreamableHttpClient(args, options) {
|
|
587
|
+
const { client, transport } = args;
|
|
588
|
+
if (options.terminateSession && transport.sessionId) {
|
|
589
|
+
await this.terminateDetachedStreamableHttpSession(transport, options.terminateWarningMessage);
|
|
590
|
+
}
|
|
591
|
+
if (client.transport === transport) {
|
|
592
|
+
await client.close().catch((error) => {
|
|
593
|
+
this.logger.warn(options.closeWarningMessage, error);
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
await transport.close().catch((error) => {
|
|
598
|
+
this.logger.warn(options.closeWarningMessage, error);
|
|
599
|
+
});
|
|
600
|
+
await client.close().catch((error) => {
|
|
601
|
+
this.logger.warn(options.closeWarningMessage, error);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async reconnectExistingStreamableHttpClient(args) {
|
|
605
|
+
const { transportModule } = await this.loadStreamableHttpRuntime();
|
|
606
|
+
const { StreamableHTTPClientTransport } = transportModule;
|
|
607
|
+
const transport = this.createStreamableHttpTransport(StreamableHTTPClientTransport, {
|
|
608
|
+
protocolVersion: args.protocolVersion,
|
|
609
|
+
sessionId: args.sessionId,
|
|
610
|
+
});
|
|
611
|
+
try {
|
|
612
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
613
|
+
await args.client.connect(transport, requestOptions);
|
|
614
|
+
if (args.sessionId !== undefined) {
|
|
615
|
+
// Reconnecting with an existing session skips initialize(), so resend
|
|
616
|
+
// initialized to reopen the shared SSE stream for async responses.
|
|
617
|
+
await this.reopenSharedStreamableHttpSession(args.client);
|
|
618
|
+
}
|
|
619
|
+
return { client: args.client, transport };
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
await this.closeStreamableHttpClient({
|
|
623
|
+
client: args.client,
|
|
624
|
+
transport,
|
|
625
|
+
}, {
|
|
626
|
+
terminateSession: args.sessionId === undefined,
|
|
627
|
+
closeWarningMessage: 'Failed to close failed MCP reconnect client:',
|
|
628
|
+
terminateWarningMessage: 'Failed to terminate failed MCP reconnect session:',
|
|
629
|
+
});
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async reconnectClosedStreamableHttpClient(args) {
|
|
634
|
+
const { cause, failedClient, failedStateVersion } = args;
|
|
635
|
+
if (this.isClosed) {
|
|
636
|
+
throw attachCause(new Error('Cannot reconnect a closed streamable HTTP MCP server.'), cause);
|
|
637
|
+
}
|
|
638
|
+
if (this.connectionStateVersion !== failedStateVersion ||
|
|
639
|
+
this.session !== failedClient) {
|
|
640
|
+
if (this.session) {
|
|
641
|
+
return this.session;
|
|
642
|
+
}
|
|
643
|
+
throw attachCause(new Error('Streamable HTTP MCP server changed before reconnect.'), cause);
|
|
644
|
+
}
|
|
645
|
+
// Multiple tool calls can discover the same closed shared session in parallel.
|
|
646
|
+
// Share one reconnect so later callers do not replace and close the new client.
|
|
647
|
+
if (this.reconnectingClientPromise) {
|
|
648
|
+
const reconnectingClientTarget = this.reconnectingClientTarget;
|
|
649
|
+
if (reconnectingClientTarget?.client === failedClient &&
|
|
650
|
+
reconnectingClientTarget.stateVersion === failedStateVersion) {
|
|
651
|
+
try {
|
|
652
|
+
return await this.reconnectingClientPromise;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
throw attachCause(error, cause);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const reconnectStateVersion = this.connectionStateVersion;
|
|
660
|
+
const previousClient = this.session;
|
|
661
|
+
const previousTransport = this.transport;
|
|
662
|
+
const reconnectPromise = (async () => {
|
|
663
|
+
const sessionId = previousTransport && hasSessionTransport(previousTransport)
|
|
664
|
+
? previousTransport.sessionId
|
|
665
|
+
: undefined;
|
|
666
|
+
const protocolVersion = getTransportProtocolVersion(previousTransport);
|
|
667
|
+
if (!previousClient || !previousTransport) {
|
|
668
|
+
throw new Error('Cannot reconnect streamable HTTP MCP server without an active client.');
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
await this.closeStreamableHttpClient({
|
|
672
|
+
client: previousClient,
|
|
673
|
+
transport: previousTransport,
|
|
674
|
+
}, {
|
|
675
|
+
terminateSession: false,
|
|
676
|
+
closeWarningMessage: 'Failed to close stale MCP client:',
|
|
677
|
+
terminateWarningMessage: 'Failed to terminate stale MCP session:',
|
|
678
|
+
});
|
|
679
|
+
const recovered = await this.reconnectExistingStreamableHttpClient({
|
|
680
|
+
client: previousClient,
|
|
681
|
+
protocolVersion,
|
|
682
|
+
sessionId,
|
|
683
|
+
});
|
|
684
|
+
if (this.connectionStateVersion !== reconnectStateVersion ||
|
|
685
|
+
this.isClosed) {
|
|
686
|
+
await this.closeStreamableHttpClient({
|
|
687
|
+
client: recovered.client,
|
|
688
|
+
transport: recovered.transport,
|
|
689
|
+
}, {
|
|
690
|
+
terminateSession: false,
|
|
691
|
+
closeWarningMessage: 'Failed to close discarded MCP client:',
|
|
692
|
+
terminateWarningMessage: 'Failed to terminate discarded MCP session:',
|
|
693
|
+
});
|
|
694
|
+
if (this.isClosed) {
|
|
695
|
+
throw new Error('Streamable HTTP MCP server was closed during reconnect.');
|
|
696
|
+
}
|
|
697
|
+
if (this.session) {
|
|
698
|
+
return this.session;
|
|
699
|
+
}
|
|
700
|
+
throw new Error('Streamable HTTP MCP server changed during reconnect.');
|
|
701
|
+
}
|
|
702
|
+
await this.publishConnectedStreamableHttpClient({
|
|
703
|
+
client: recovered.client,
|
|
704
|
+
transport: recovered.transport,
|
|
705
|
+
previousTransport,
|
|
706
|
+
});
|
|
707
|
+
return recovered.client;
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
await this.clearPublishedStreamableHttpClientIfCurrent({
|
|
711
|
+
client: previousClient,
|
|
712
|
+
transport: previousTransport,
|
|
713
|
+
stateVersion: reconnectStateVersion,
|
|
714
|
+
});
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
})();
|
|
718
|
+
this.reconnectingClientPromise = reconnectPromise;
|
|
719
|
+
this.reconnectingClientTarget = {
|
|
720
|
+
client: failedClient,
|
|
721
|
+
stateVersion: failedStateVersion,
|
|
722
|
+
};
|
|
723
|
+
try {
|
|
724
|
+
return await reconnectPromise;
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
throw attachCause(error, cause);
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
if (this.reconnectingClientPromise === reconnectPromise) {
|
|
731
|
+
this.reconnectingClientPromise = null;
|
|
732
|
+
this.reconnectingClientTarget = null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async shouldReconnectClosedStreamableHttpClient(error, client) {
|
|
737
|
+
// Explicit session ids are a released contract, so keep callers pinned to the
|
|
738
|
+
// session they selected instead of silently switching them to a fresh one.
|
|
739
|
+
if (this.params.sessionId !== undefined) {
|
|
740
|
+
return 'none';
|
|
741
|
+
}
|
|
742
|
+
if (isNotConnectedError(error, client)) {
|
|
743
|
+
return 'reconnect-and-retry';
|
|
744
|
+
}
|
|
745
|
+
const { typesModule } = await this.loadStreamableHttpRuntime();
|
|
746
|
+
const { ErrorCode, McpError } = typesModule;
|
|
747
|
+
return error instanceof McpError &&
|
|
748
|
+
error.code === ErrorCode.ConnectionClosed
|
|
749
|
+
? 'reconnect-only'
|
|
750
|
+
: 'none';
|
|
751
|
+
}
|
|
312
752
|
async connect() {
|
|
753
|
+
const connectStateVersion = this.connectionStateVersion;
|
|
754
|
+
this.isClosed = false;
|
|
313
755
|
try {
|
|
314
|
-
const {
|
|
315
|
-
const { Client } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/index.js'))).catch(failedToImport);
|
|
316
|
-
this.transport = new StreamableHTTPClientTransport(new URL(this.params.url), {
|
|
317
|
-
authProvider: this.params.authProvider,
|
|
318
|
-
requestInit: this.params.requestInit,
|
|
319
|
-
fetch: this.params.fetch,
|
|
320
|
-
reconnectionOptions: this.params.reconnectionOptions,
|
|
756
|
+
const { client, transport } = await this.createConnectedStreamableHttpClient({
|
|
321
757
|
sessionId: this.params.sessionId,
|
|
322
758
|
});
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
|
|
759
|
+
if (this.isClosed ||
|
|
760
|
+
this.connectionStateVersion !== connectStateVersion) {
|
|
761
|
+
await this.closeStreamableHttpClient({
|
|
762
|
+
client,
|
|
763
|
+
transport,
|
|
764
|
+
}, {
|
|
765
|
+
// A stale overlapping connect can point at the same shared session as
|
|
766
|
+
// the winner, so only terminate truly discarded sessions.
|
|
767
|
+
terminateSession: shouldTerminateTransportSession(transport, this.transport),
|
|
768
|
+
closeWarningMessage: 'Failed to close discarded MCP client:',
|
|
769
|
+
terminateWarningMessage: 'Failed to terminate discarded MCP session:',
|
|
770
|
+
});
|
|
771
|
+
if (this.isClosed) {
|
|
772
|
+
throw new Error('Streamable HTTP MCP server was closed during connect.');
|
|
773
|
+
}
|
|
774
|
+
throw new Error('Streamable HTTP MCP server changed during connect.');
|
|
775
|
+
}
|
|
776
|
+
const previousClient = this.session;
|
|
777
|
+
const previousTransport = this.transport;
|
|
778
|
+
await this.publishConnectedStreamableHttpClient({
|
|
779
|
+
client,
|
|
780
|
+
transport,
|
|
781
|
+
previousTransport,
|
|
326
782
|
});
|
|
327
|
-
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
328
|
-
await this.session.connect(this.transport, requestOptions);
|
|
329
783
|
this.serverInitializeResult = {
|
|
330
784
|
serverInfo: { name: this._name, version: '1.0.0' },
|
|
331
785
|
};
|
|
786
|
+
if (previousClient && previousTransport) {
|
|
787
|
+
await this.closeStreamableHttpClient({
|
|
788
|
+
client: previousClient,
|
|
789
|
+
transport: previousTransport,
|
|
790
|
+
}, {
|
|
791
|
+
terminateSession: shouldTerminateTransportSession(previousTransport, transport),
|
|
792
|
+
closeWarningMessage: 'Failed to close replaced MCP client:',
|
|
793
|
+
terminateWarningMessage: 'Failed to terminate replaced MCP session:',
|
|
794
|
+
});
|
|
795
|
+
}
|
|
332
796
|
}
|
|
333
797
|
catch (e) {
|
|
798
|
+
// A losing concurrent connect can fail after another connect already
|
|
799
|
+
// published a healthy shared session, so avoid closing shared state here.
|
|
334
800
|
this.logger.error('Error initializing MCP server:', e);
|
|
335
|
-
await this.close();
|
|
336
801
|
throw e;
|
|
337
802
|
}
|
|
338
803
|
this.debugLog(() => `Connected to MCP server: ${this._name}`);
|
|
@@ -357,47 +822,114 @@ class NodeMCPServerStreamableHttp extends mcp_1.BaseMCPServerStreamableHttp {
|
|
|
357
822
|
return this._toolsList;
|
|
358
823
|
}
|
|
359
824
|
async callTool(toolName, args, meta) {
|
|
360
|
-
const
|
|
361
|
-
if (!
|
|
825
|
+
const client = this.session;
|
|
826
|
+
if (!client) {
|
|
362
827
|
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
363
828
|
}
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
829
|
+
const callToolStateVersion = this.connectionStateVersion;
|
|
830
|
+
let result;
|
|
831
|
+
try {
|
|
832
|
+
result = await this.callToolWithClient(client, toolName, args, meta);
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
const recoveryStrategy = await this.shouldReconnectClosedStreamableHttpClient(error, client);
|
|
836
|
+
if (recoveryStrategy === 'none') {
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
this.debugLog(() => `Reconnecting closed streamable HTTP MCP session for ${toolName}.`);
|
|
840
|
+
const recoveredClient = await this.reconnectClosedStreamableHttpClient({
|
|
841
|
+
cause: error,
|
|
842
|
+
failedClient: client,
|
|
843
|
+
failedStateVersion: callToolStateVersion,
|
|
844
|
+
});
|
|
845
|
+
if (recoveryStrategy === 'reconnect-only') {
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
result = await this.callToolWithClient(recoveredClient, toolName, args, meta);
|
|
850
|
+
}
|
|
851
|
+
catch (retryError) {
|
|
852
|
+
throw attachCause(retryError, error);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
373
855
|
this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
|
|
374
856
|
return result;
|
|
375
857
|
}
|
|
858
|
+
async listResources(params) {
|
|
859
|
+
const { ListResourcesResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
860
|
+
if (!this.session) {
|
|
861
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
862
|
+
}
|
|
863
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
864
|
+
const response = await this.session.listResources(params, requestOptions);
|
|
865
|
+
this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
|
|
866
|
+
return ListResourcesResultSchema.parse(response);
|
|
867
|
+
}
|
|
868
|
+
async listResourceTemplates(params) {
|
|
869
|
+
const { ListResourceTemplatesResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
870
|
+
if (!this.session) {
|
|
871
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
872
|
+
}
|
|
873
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
874
|
+
const response = await this.session.listResourceTemplates(params, requestOptions);
|
|
875
|
+
this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
|
|
876
|
+
return ListResourceTemplatesResultSchema.parse(response);
|
|
877
|
+
}
|
|
878
|
+
async readResource(uri) {
|
|
879
|
+
const { ReadResourceResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
|
|
880
|
+
if (!this.session) {
|
|
881
|
+
throw new Error('Server not initialized. Make sure you call connect() first.');
|
|
882
|
+
}
|
|
883
|
+
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
|
|
884
|
+
const response = await this.session.readResource({ uri }, requestOptions);
|
|
885
|
+
this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
|
|
886
|
+
return ReadResourceResultSchema.parse(response);
|
|
887
|
+
}
|
|
376
888
|
get name() {
|
|
377
889
|
return this._name;
|
|
378
890
|
}
|
|
891
|
+
get sessionId() {
|
|
892
|
+
const transport = this.transport;
|
|
893
|
+
return hasSessionTransport(transport) ? transport.sessionId : undefined;
|
|
894
|
+
}
|
|
379
895
|
async close() {
|
|
896
|
+
this.isClosed = true;
|
|
897
|
+
this.connectionStateVersion += 1;
|
|
898
|
+
const closeStateVersion = this.connectionStateVersion;
|
|
899
|
+
const reconnectPromise = this.reconnectingClientPromise;
|
|
900
|
+
const client = this.session;
|
|
380
901
|
const transport = this.transport;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
902
|
+
this.session = null;
|
|
903
|
+
this.transport = null;
|
|
904
|
+
await this.closeCloseOwnedStreamableHttpState({
|
|
905
|
+
client,
|
|
906
|
+
transport,
|
|
907
|
+
closeStateVersion,
|
|
908
|
+
closeWarningMessage: client && transport
|
|
909
|
+
? 'Failed to close MCP client:'
|
|
910
|
+
: transport
|
|
911
|
+
? 'Failed to close MCP transport:'
|
|
912
|
+
: 'Failed to close MCP client:',
|
|
913
|
+
terminateWarningMessage: 'Failed to terminate MCP session:',
|
|
914
|
+
});
|
|
915
|
+
if (reconnectPromise) {
|
|
916
|
+
await reconnectPromise.catch(() => { });
|
|
917
|
+
// A new connect() may have reopened the server while close() was waiting
|
|
918
|
+
// for the stale reconnect to settle, so only clean up close-owned state.
|
|
919
|
+
if (this.connectionStateVersion !== closeStateVersion || !this.isClosed) {
|
|
920
|
+
return;
|
|
392
921
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
await transport.close();
|
|
396
|
-
this.transport = null;
|
|
397
|
-
}
|
|
398
|
-
if (this.session) {
|
|
399
|
-
await this.session.close();
|
|
922
|
+
const recoveredClient = this.session;
|
|
923
|
+
const recoveredTransport = this.transport;
|
|
400
924
|
this.session = null;
|
|
925
|
+
this.transport = null;
|
|
926
|
+
await this.closeCloseOwnedStreamableHttpState({
|
|
927
|
+
client: recoveredClient,
|
|
928
|
+
transport: recoveredTransport,
|
|
929
|
+
closeStateVersion,
|
|
930
|
+
closeWarningMessage: 'Failed to close reconnected MCP client:',
|
|
931
|
+
terminateWarningMessage: 'Failed to terminate reconnected MCP session:',
|
|
932
|
+
});
|
|
401
933
|
}
|
|
402
934
|
}
|
|
403
935
|
}
|