@openai/agents-core 0.7.2 → 0.8.1

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.
Files changed (56) hide show
  1. package/dist/extensions/handoffFilters.js +4 -1
  2. package/dist/extensions/handoffFilters.js.map +1 -1
  3. package/dist/extensions/handoffFilters.mjs +5 -2
  4. package/dist/extensions/handoffFilters.mjs.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/mcp.d.ts +107 -3
  9. package/dist/mcp.js +64 -9
  10. package/dist/mcp.js.map +1 -1
  11. package/dist/mcp.mjs +64 -9
  12. package/dist/mcp.mjs.map +1 -1
  13. package/dist/metadata.js +2 -2
  14. package/dist/metadata.mjs +2 -2
  15. package/dist/run.js +9 -1
  16. package/dist/run.js.map +1 -1
  17. package/dist/run.mjs +9 -1
  18. package/dist/run.mjs.map +1 -1
  19. package/dist/runner/conversation.d.ts +1 -1
  20. package/dist/runner/conversation.js +30 -1
  21. package/dist/runner/conversation.js.map +1 -1
  22. package/dist/runner/conversation.mjs +30 -1
  23. package/dist/runner/conversation.mjs.map +1 -1
  24. package/dist/runner/modelOutputs.js +16 -12
  25. package/dist/runner/modelOutputs.js.map +1 -1
  26. package/dist/runner/modelOutputs.mjs +16 -12
  27. package/dist/runner/modelOutputs.mjs.map +1 -1
  28. package/dist/runner/toolExecution.js +4 -5
  29. package/dist/runner/toolExecution.js.map +1 -1
  30. package/dist/runner/toolExecution.mjs +5 -6
  31. package/dist/runner/toolExecution.mjs.map +1 -1
  32. package/dist/runner/turnPreparation.d.ts +1 -0
  33. package/dist/runner/turnPreparation.js +34 -3
  34. package/dist/runner/turnPreparation.js.map +1 -1
  35. package/dist/runner/turnPreparation.mjs +31 -1
  36. package/dist/runner/turnPreparation.mjs.map +1 -1
  37. package/dist/runner/turnResolution.js +1 -1
  38. package/dist/runner/turnResolution.js.map +1 -1
  39. package/dist/runner/turnResolution.mjs +2 -2
  40. package/dist/runner/turnResolution.mjs.map +1 -1
  41. package/dist/shims/mcp-server/browser.d.ts +11 -1
  42. package/dist/shims/mcp-server/browser.js +30 -0
  43. package/dist/shims/mcp-server/browser.js.map +1 -1
  44. package/dist/shims/mcp-server/browser.mjs +30 -0
  45. package/dist/shims/mcp-server/browser.mjs.map +1 -1
  46. package/dist/shims/mcp-server/node.d.ts +30 -1
  47. package/dist/shims/mcp-server/node.js +574 -42
  48. package/dist/shims/mcp-server/node.js.map +1 -1
  49. package/dist/shims/mcp-server/node.mjs +574 -42
  50. package/dist/shims/mcp-server/node.mjs.map +1 -1
  51. package/dist/utils/messages.d.ts +6 -0
  52. package/dist/utils/messages.js +34 -6
  53. package/dist/utils/messages.js.map +1 -1
  54. package/dist/utils/messages.mjs +33 -6
  55. package/dist/utils/messages.mjs.map +1 -1
  56. 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 { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js'))).catch(failedToImport);
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.session = new Client({
324
- name: this._name,
325
- version: '1.0.0', // You may want to make this configurable
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 { CallToolResultSchema } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js'))).catch(failedToImport);
361
- if (!this.session) {
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 requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds, { timeout: this.timeout });
365
- const params = {
366
- name: toolName,
367
- arguments: args ?? {},
368
- ...(meta != null ? { _meta: meta } : {}),
369
- };
370
- const response = await this.session.callTool(params, undefined, requestOptions);
371
- const parsed = CallToolResultSchema.parse(response);
372
- const result = parsed.content;
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
- if (hasSessionTransport(transport)) {
382
- const sessionId = transport.sessionId;
383
- if (sessionId && typeof transport.terminateSession === 'function') {
384
- try {
385
- // Best-effort cleanup: we do not actively manage session lifecycles,
386
- // but if the server supports sessions we terminate to avoid leaks.
387
- await transport.terminateSession();
388
- }
389
- catch (error) {
390
- this.logger.warn('Failed to terminate MCP session:', error);
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
- if (transport) {
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
  }