@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
@@ -22,6 +22,63 @@ function hasSessionTransport(transport) {
22
22
  (typeof transport.terminateSession === 'function' ||
23
23
  transport.sessionId !== undefined));
24
24
  }
25
+ function getTransportProtocolVersion(transport) {
26
+ if (transport != null &&
27
+ typeof transport.protocolVersion ===
28
+ 'string') {
29
+ return transport.protocolVersion;
30
+ }
31
+ return undefined;
32
+ }
33
+ function isNotConnectedError(error, client) {
34
+ return (error instanceof Error &&
35
+ error.message === 'Not connected' &&
36
+ client.transport == null);
37
+ }
38
+ function attachCause(error, cause) {
39
+ if (!(error instanceof Error)) {
40
+ return error;
41
+ }
42
+ try {
43
+ if (error.cause === undefined) {
44
+ Object.defineProperty(error, 'cause', {
45
+ value: cause,
46
+ configurable: true,
47
+ enumerable: false,
48
+ writable: true,
49
+ });
50
+ }
51
+ }
52
+ catch {
53
+ // Best effort only.
54
+ }
55
+ return error;
56
+ }
57
+ function getSessionId(transport) {
58
+ return hasSessionTransport(transport) ? transport.sessionId : undefined;
59
+ }
60
+ function shouldTerminateTransportSession(transportToClose, activeTransport) {
61
+ const closingSessionId = getSessionId(transportToClose);
62
+ if (closingSessionId === undefined) {
63
+ return false;
64
+ }
65
+ const activeSessionId = getSessionId(activeTransport);
66
+ return activeSessionId === undefined || activeSessionId !== closingSessionId;
67
+ }
68
+ function withTimeout(promise, timeoutMs, onTimeout) {
69
+ return new Promise((resolve, reject) => {
70
+ const timeoutId = setTimeout(() => {
71
+ reject(onTimeout());
72
+ }, timeoutMs);
73
+ void promise.then((value) => {
74
+ clearTimeout(timeoutId);
75
+ resolve(value);
76
+ }, (error) => {
77
+ clearTimeout(timeoutId);
78
+ reject(error);
79
+ });
80
+ });
81
+ }
25
82
  export class NodeMCPServerStdio extends BaseMCPServerStdio {
26
83
  session = null;
27
84
  _cacheDirty = true;
@@ -118,6 +175,36 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio {
118
175
  this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
119
176
  return result;
120
177
  }
178
+ async listResources(params) {
179
+ const { ListResourcesResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
180
+ if (!this.session) {
181
+ throw new Error('Server not initialized. Make sure you call connect() first.');
182
+ }
183
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
184
+ const response = await this.session.listResources(params, requestOptions);
185
+ this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
186
+ return ListResourcesResultSchema.parse(response);
187
+ }
188
+ async listResourceTemplates(params) {
189
+ const { ListResourceTemplatesResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
190
+ if (!this.session) {
191
+ throw new Error('Server not initialized. Make sure you call connect() first.');
192
+ }
193
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
194
+ const response = await this.session.listResourceTemplates(params, requestOptions);
195
+ this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
196
+ return ListResourceTemplatesResultSchema.parse(response);
197
+ }
198
+ async readResource(uri) {
199
+ const { ReadResourceResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
200
+ if (!this.session) {
201
+ throw new Error('Server not initialized. Make sure you call connect() first.');
202
+ }
203
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
204
+ const response = await this.session.readResource({ uri }, requestOptions);
205
+ this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
206
+ return ReadResourceResultSchema.parse(response);
207
+ }
121
208
  get name() {
122
209
  return this._name;
123
210
  }
@@ -223,6 +310,36 @@ export class NodeMCPServerSSE extends BaseMCPServerSSE {
223
310
  this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
224
311
  return result;
225
312
  }
313
+ async listResources(params) {
314
+ const { ListResourcesResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
315
+ if (!this.session) {
316
+ throw new Error('Server not initialized. Make sure you call connect() first.');
317
+ }
318
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
319
+ const response = await this.session.listResources(params, requestOptions);
320
+ this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
321
+ return ListResourcesResultSchema.parse(response);
322
+ }
323
+ async listResourceTemplates(params) {
324
+ const { ListResourceTemplatesResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
325
+ if (!this.session) {
326
+ throw new Error('Server not initialized. Make sure you call connect() first.');
327
+ }
328
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
329
+ const response = await this.session.listResourceTemplates(params, requestOptions);
330
+ this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
331
+ return ListResourceTemplatesResultSchema.parse(response);
332
+ }
333
+ async readResource(uri) {
334
+ const { ReadResourceResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
335
+ if (!this.session) {
336
+ throw new Error('Server not initialized. Make sure you call connect() first.');
337
+ }
338
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
339
+ const response = await this.session.readResource({ uri }, requestOptions);
340
+ this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
341
+ return ReadResourceResultSchema.parse(response);
342
+ }
226
343
  get name() {
227
344
  return this._name;
228
345
  }
@@ -261,6 +378,10 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
261
378
  params;
262
379
  _name;
263
380
  transport = null;
381
+ reconnectingClientPromise = null;
382
+ reconnectingClientTarget = null;
383
+ isClosed = false;
384
+ connectionStateVersion = 0;
264
385
  constructor(params) {
265
386
  super(params);
266
387
  this.clientSessionTimeoutSeconds = params.clientSessionTimeoutSeconds ?? 5;
@@ -268,30 +389,374 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
268
389
  this._name = params.name || `streamable-http: ${this.params.url}`;
269
390
  this.timeout = params.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
270
391
  }
392
+ async loadStreamableHttpRuntime() {
393
+ const [clientModule, transportModule, typesModule] = await Promise.all([
394
+ import('@modelcontextprotocol/sdk/client/index.js').catch(failedToImport),
395
+ import('@modelcontextprotocol/sdk/client/streamableHttp.js').catch(failedToImport),
396
+ import('@modelcontextprotocol/sdk/types.js').catch(failedToImport),
397
+ ]);
398
+ return { clientModule, transportModule, typesModule };
399
+ }
400
+ createStreamableHttpTransport(StreamableHTTPClientTransport, options = {}) {
401
+ const transportOptions = {
402
+ authProvider: this.params.authProvider,
403
+ requestInit: this.params.requestInit,
404
+ fetch: this.params.fetch,
405
+ reconnectionOptions: this.params.reconnectionOptions,
406
+ sessionId: options.sessionId,
407
+ };
408
+ const transport = new StreamableHTTPClientTransport(new URL(this.params.url), transportOptions);
409
+ if (options.protocolVersion !== undefined &&
410
+ typeof transport.setProtocolVersion === 'function') {
411
+ transport.setProtocolVersion(options.protocolVersion);
412
+ }
413
+ return transport;
414
+ }
415
+ async createConnectedStreamableHttpClient(options = {}) {
416
+ const { clientModule, transportModule } = await this.loadStreamableHttpRuntime();
417
+ const { Client } = clientModule;
418
+ const { StreamableHTTPClientTransport } = transportModule;
419
+ const transport = this.createStreamableHttpTransport(StreamableHTTPClientTransport, options);
420
+ const client = new Client({
421
+ name: this._name,
422
+ version: '1.0.0',
423
+ });
424
+ try {
425
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
426
+ await client.connect(transport, requestOptions);
427
+ return { client, transport };
428
+ }
429
+ catch (error) {
430
+ await this.closeStreamableHttpClient({
431
+ client,
432
+ transport,
433
+ }, {
434
+ terminateSession: options.sessionId === undefined,
435
+ closeWarningMessage: 'Failed to close failed MCP connect client:',
436
+ terminateWarningMessage: 'Failed to terminate failed MCP connect session:',
437
+ });
438
+ throw error;
439
+ }
440
+ }
441
+ getClientSessionTimeoutMs() {
442
+ return Math.max(1, (this.clientSessionTimeoutSeconds ?? 5) * 1000);
443
+ }
444
+ resetClientToolMetadataCache(client) {
445
+ client.cacheToolMetadata?.([]);
446
+ }
447
+ async publishConnectedStreamableHttpClient(args) {
448
+ this.transport = args.transport;
449
+ this.session = args.client;
450
+ this.connectionStateVersion += 1;
451
+ this._cacheDirty = true;
452
+ this._toolsList = [];
453
+ const previousSessionId = getSessionId(args.previousTransport);
454
+ const nextSessionId = getSessionId(args.transport);
455
+ if (previousSessionId === undefined ||
456
+ nextSessionId === undefined ||
457
+ previousSessionId !== nextSessionId) {
458
+ this.resetClientToolMetadataCache(args.client);
459
+ }
460
+ await invalidateServerToolsCache(this.name);
461
+ }
462
+ async clearPublishedStreamableHttpClientIfCurrent(args) {
463
+ if (this.connectionStateVersion !== args.stateVersion ||
464
+ this.session !== args.client ||
465
+ this.transport !== args.transport) {
466
+ return;
467
+ }
468
+ this.session = null;
469
+ this.transport = null;
470
+ this._cacheDirty = true;
471
+ this._toolsList = [];
472
+ await invalidateServerToolsCache(this.name);
473
+ }
474
+ async reopenSharedStreamableHttpSession(client) {
475
+ await withTimeout(client.notification({ method: 'notifications/initialized' }), this.getClientSessionTimeoutMs(), () => new Error('Timed out reopening shared streamable HTTP MCP session.'));
476
+ }
477
+ async terminateDetachedStreamableHttpSession(transport, warningMessage) {
478
+ const sessionId = getSessionId(transport);
479
+ if (sessionId === undefined) {
480
+ return;
481
+ }
482
+ try {
483
+ const { transportModule } = await this.loadStreamableHttpRuntime();
484
+ const detachedTransport = this.createStreamableHttpTransport(transportModule.StreamableHTTPClientTransport, {
485
+ protocolVersion: getTransportProtocolVersion(transport),
486
+ sessionId,
487
+ });
488
+ try {
489
+ if (typeof detachedTransport.terminateSession === 'function') {
490
+ await detachedTransport.terminateSession();
491
+ }
492
+ }
493
+ finally {
494
+ await detachedTransport.close().catch(() => { });
495
+ }
496
+ }
497
+ catch (error) {
498
+ this.logger.warn(warningMessage, error);
499
+ }
500
+ }
501
+ async closeCloseOwnedStreamableHttpState(args) {
502
+ const { client, transport, closeStateVersion, closeWarningMessage, terminateWarningMessage, } = args;
503
+ if (client && transport) {
504
+ await this.closeStreamableHttpClient({
505
+ client,
506
+ transport,
507
+ }, {
508
+ terminateSession: false,
509
+ closeWarningMessage,
510
+ terminateWarningMessage,
511
+ });
512
+ }
513
+ else if (transport) {
514
+ await transport.close().catch((error) => {
515
+ this.logger.warn(closeWarningMessage, error);
516
+ });
517
+ }
518
+ else if (client) {
519
+ await client.close().catch((error) => {
520
+ this.logger.warn(closeWarningMessage, error);
521
+ });
522
+ }
523
+ if (!transport ||
524
+ this.connectionStateVersion !== closeStateVersion ||
525
+ !this.isClosed ||
526
+ !shouldTerminateTransportSession(transport, this.transport)) {
527
+ return;
528
+ }
529
+ await this.terminateDetachedStreamableHttpSession(transport, terminateWarningMessage);
530
+ }
531
+ async callToolWithClient(client, toolName, args, meta) {
532
+ const { CallToolResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
533
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds, {
534
+ timeout: this.timeout,
535
+ });
536
+ const params = {
537
+ name: toolName,
538
+ arguments: args ?? {},
539
+ ...(meta != null ? { _meta: meta } : {}),
540
+ };
541
+ const response = await client.callTool(params, undefined, requestOptions);
542
+ const parsed = CallToolResultSchema.parse(response);
543
+ return parsed.content;
544
+ }
545
+ async closeStreamableHttpClient(args, options) {
546
+ const { client, transport } = args;
547
+ if (options.terminateSession && transport.sessionId) {
548
+ await this.terminateDetachedStreamableHttpSession(transport, options.terminateWarningMessage);
549
+ }
550
+ if (client.transport === transport) {
551
+ await client.close().catch((error) => {
552
+ this.logger.warn(options.closeWarningMessage, error);
553
+ });
554
+ return;
555
+ }
556
+ await transport.close().catch((error) => {
557
+ this.logger.warn(options.closeWarningMessage, error);
558
+ });
559
+ await client.close().catch((error) => {
560
+ this.logger.warn(options.closeWarningMessage, error);
561
+ });
562
+ }
563
+ async reconnectExistingStreamableHttpClient(args) {
564
+ const { transportModule } = await this.loadStreamableHttpRuntime();
565
+ const { StreamableHTTPClientTransport } = transportModule;
566
+ const transport = this.createStreamableHttpTransport(StreamableHTTPClientTransport, {
567
+ protocolVersion: args.protocolVersion,
568
+ sessionId: args.sessionId,
569
+ });
570
+ try {
571
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
572
+ await args.client.connect(transport, requestOptions);
573
+ if (args.sessionId !== undefined) {
574
+ // Reconnecting with an existing session skips initialize(), so resend
575
+ // initialized to reopen the shared SSE stream for async responses.
576
+ await this.reopenSharedStreamableHttpSession(args.client);
577
+ }
578
+ return { client: args.client, transport };
579
+ }
580
+ catch (error) {
581
+ await this.closeStreamableHttpClient({
582
+ client: args.client,
583
+ transport,
584
+ }, {
585
+ terminateSession: args.sessionId === undefined,
586
+ closeWarningMessage: 'Failed to close failed MCP reconnect client:',
587
+ terminateWarningMessage: 'Failed to terminate failed MCP reconnect session:',
588
+ });
589
+ throw error;
590
+ }
591
+ }
592
+ async reconnectClosedStreamableHttpClient(args) {
593
+ const { cause, failedClient, failedStateVersion } = args;
594
+ if (this.isClosed) {
595
+ throw attachCause(new Error('Cannot reconnect a closed streamable HTTP MCP server.'), cause);
596
+ }
597
+ if (this.connectionStateVersion !== failedStateVersion ||
598
+ this.session !== failedClient) {
599
+ if (this.session) {
600
+ return this.session;
601
+ }
602
+ throw attachCause(new Error('Streamable HTTP MCP server changed before reconnect.'), cause);
603
+ }
604
+ // Multiple tool calls can discover the same closed shared session in parallel.
605
+ // Share one reconnect so later callers do not replace and close the new client.
606
+ if (this.reconnectingClientPromise) {
607
+ const reconnectingClientTarget = this.reconnectingClientTarget;
608
+ if (reconnectingClientTarget?.client === failedClient &&
609
+ reconnectingClientTarget.stateVersion === failedStateVersion) {
610
+ try {
611
+ return await this.reconnectingClientPromise;
612
+ }
613
+ catch (error) {
614
+ throw attachCause(error, cause);
615
+ }
616
+ }
617
+ }
618
+ const reconnectStateVersion = this.connectionStateVersion;
619
+ const previousClient = this.session;
620
+ const previousTransport = this.transport;
621
+ const reconnectPromise = (async () => {
622
+ const sessionId = previousTransport && hasSessionTransport(previousTransport)
623
+ ? previousTransport.sessionId
624
+ : undefined;
625
+ const protocolVersion = getTransportProtocolVersion(previousTransport);
626
+ if (!previousClient || !previousTransport) {
627
+ throw new Error('Cannot reconnect streamable HTTP MCP server without an active client.');
628
+ }
629
+ try {
630
+ await this.closeStreamableHttpClient({
631
+ client: previousClient,
632
+ transport: previousTransport,
633
+ }, {
634
+ terminateSession: false,
635
+ closeWarningMessage: 'Failed to close stale MCP client:',
636
+ terminateWarningMessage: 'Failed to terminate stale MCP session:',
637
+ });
638
+ const recovered = await this.reconnectExistingStreamableHttpClient({
639
+ client: previousClient,
640
+ protocolVersion,
641
+ sessionId,
642
+ });
643
+ if (this.connectionStateVersion !== reconnectStateVersion ||
644
+ this.isClosed) {
645
+ await this.closeStreamableHttpClient({
646
+ client: recovered.client,
647
+ transport: recovered.transport,
648
+ }, {
649
+ terminateSession: false,
650
+ closeWarningMessage: 'Failed to close discarded MCP client:',
651
+ terminateWarningMessage: 'Failed to terminate discarded MCP session:',
652
+ });
653
+ if (this.isClosed) {
654
+ throw new Error('Streamable HTTP MCP server was closed during reconnect.');
655
+ }
656
+ if (this.session) {
657
+ return this.session;
658
+ }
659
+ throw new Error('Streamable HTTP MCP server changed during reconnect.');
660
+ }
661
+ await this.publishConnectedStreamableHttpClient({
662
+ client: recovered.client,
663
+ transport: recovered.transport,
664
+ previousTransport,
665
+ });
666
+ return recovered.client;
667
+ }
668
+ catch (error) {
669
+ await this.clearPublishedStreamableHttpClientIfCurrent({
670
+ client: previousClient,
671
+ transport: previousTransport,
672
+ stateVersion: reconnectStateVersion,
673
+ });
674
+ throw error;
675
+ }
676
+ })();
677
+ this.reconnectingClientPromise = reconnectPromise;
678
+ this.reconnectingClientTarget = {
679
+ client: failedClient,
680
+ stateVersion: failedStateVersion,
681
+ };
682
+ try {
683
+ return await reconnectPromise;
684
+ }
685
+ catch (error) {
686
+ throw attachCause(error, cause);
687
+ }
688
+ finally {
689
+ if (this.reconnectingClientPromise === reconnectPromise) {
690
+ this.reconnectingClientPromise = null;
691
+ this.reconnectingClientTarget = null;
692
+ }
693
+ }
694
+ }
695
+ async shouldReconnectClosedStreamableHttpClient(error, client) {
696
+ // Explicit session ids are a released contract, so keep callers pinned to the
697
+ // session they selected instead of silently switching them to a fresh one.
698
+ if (this.params.sessionId !== undefined) {
699
+ return 'none';
700
+ }
701
+ if (isNotConnectedError(error, client)) {
702
+ return 'reconnect-and-retry';
703
+ }
704
+ const { typesModule } = await this.loadStreamableHttpRuntime();
705
+ const { ErrorCode, McpError } = typesModule;
706
+ return error instanceof McpError &&
707
+ error.code === ErrorCode.ConnectionClosed
708
+ ? 'reconnect-only'
709
+ : 'none';
710
+ }
271
711
  async connect() {
712
+ const connectStateVersion = this.connectionStateVersion;
713
+ this.isClosed = false;
272
714
  try {
273
- const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js').catch(failedToImport);
274
- const { Client } = await import('@modelcontextprotocol/sdk/client/index.js').catch(failedToImport);
275
- this.transport = new StreamableHTTPClientTransport(new URL(this.params.url), {
276
- authProvider: this.params.authProvider,
277
- requestInit: this.params.requestInit,
278
- fetch: this.params.fetch,
279
- reconnectionOptions: this.params.reconnectionOptions,
715
+ const { client, transport } = await this.createConnectedStreamableHttpClient({
280
716
  sessionId: this.params.sessionId,
281
717
  });
282
- this.session = new Client({
283
- name: this._name,
284
- version: '1.0.0', // You may want to make this configurable
718
+ if (this.isClosed ||
719
+ this.connectionStateVersion !== connectStateVersion) {
720
+ await this.closeStreamableHttpClient({
721
+ client,
722
+ transport,
723
+ }, {
724
+ // A stale overlapping connect can point at the same shared session as
725
+ // the winner, so only terminate truly discarded sessions.
726
+ terminateSession: shouldTerminateTransportSession(transport, this.transport),
727
+ closeWarningMessage: 'Failed to close discarded MCP client:',
728
+ terminateWarningMessage: 'Failed to terminate discarded MCP session:',
729
+ });
730
+ if (this.isClosed) {
731
+ throw new Error('Streamable HTTP MCP server was closed during connect.');
732
+ }
733
+ throw new Error('Streamable HTTP MCP server changed during connect.');
734
+ }
735
+ const previousClient = this.session;
736
+ const previousTransport = this.transport;
737
+ await this.publishConnectedStreamableHttpClient({
738
+ client,
739
+ transport,
740
+ previousTransport,
285
741
  });
286
- const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
287
- await this.session.connect(this.transport, requestOptions);
288
742
  this.serverInitializeResult = {
289
743
  serverInfo: { name: this._name, version: '1.0.0' },
290
744
  };
745
+ if (previousClient && previousTransport) {
746
+ await this.closeStreamableHttpClient({
747
+ client: previousClient,
748
+ transport: previousTransport,
749
+ }, {
750
+ terminateSession: shouldTerminateTransportSession(previousTransport, transport),
751
+ closeWarningMessage: 'Failed to close replaced MCP client:',
752
+ terminateWarningMessage: 'Failed to terminate replaced MCP session:',
753
+ });
754
+ }
291
755
  }
292
756
  catch (e) {
757
+ // A losing concurrent connect can fail after another connect already
758
+ // published a healthy shared session, so avoid closing shared state here.
293
759
  this.logger.error('Error initializing MCP server:', e);
294
- await this.close();
295
760
  throw e;
296
761
  }
297
762
  this.debugLog(() => `Connected to MCP server: ${this._name}`);
@@ -316,47 +781,114 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
316
781
  return this._toolsList;
317
782
  }
318
783
  async callTool(toolName, args, meta) {
319
- const { CallToolResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
320
- if (!this.session) {
784
+ const client = this.session;
785
+ if (!client) {
321
786
  throw new Error('Server not initialized. Make sure you call connect() first.');
322
787
  }
323
- const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds, { timeout: this.timeout });
324
- const params = {
325
- name: toolName,
326
- arguments: args ?? {},
327
- ...(meta != null ? { _meta: meta } : {}),
328
- };
329
- const response = await this.session.callTool(params, undefined, requestOptions);
330
- const parsed = CallToolResultSchema.parse(response);
331
- const result = parsed.content;
788
+ const callToolStateVersion = this.connectionStateVersion;
789
+ let result;
790
+ try {
791
+ result = await this.callToolWithClient(client, toolName, args, meta);
792
+ }
793
+ catch (error) {
794
+ const recoveryStrategy = await this.shouldReconnectClosedStreamableHttpClient(error, client);
795
+ if (recoveryStrategy === 'none') {
796
+ throw error;
797
+ }
798
+ this.debugLog(() => `Reconnecting closed streamable HTTP MCP session for ${toolName}.`);
799
+ const recoveredClient = await this.reconnectClosedStreamableHttpClient({
800
+ cause: error,
801
+ failedClient: client,
802
+ failedStateVersion: callToolStateVersion,
803
+ });
804
+ if (recoveryStrategy === 'reconnect-only') {
805
+ throw error;
806
+ }
807
+ try {
808
+ result = await this.callToolWithClient(recoveredClient, toolName, args, meta);
809
+ }
810
+ catch (retryError) {
811
+ throw attachCause(retryError, error);
812
+ }
813
+ }
332
814
  this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
333
815
  return result;
334
816
  }
817
+ async listResources(params) {
818
+ const { ListResourcesResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
819
+ if (!this.session) {
820
+ throw new Error('Server not initialized. Make sure you call connect() first.');
821
+ }
822
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
823
+ const response = await this.session.listResources(params, requestOptions);
824
+ this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
825
+ return ListResourcesResultSchema.parse(response);
826
+ }
827
+ async listResourceTemplates(params) {
828
+ const { ListResourceTemplatesResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
829
+ if (!this.session) {
830
+ throw new Error('Server not initialized. Make sure you call connect() first.');
831
+ }
832
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
833
+ const response = await this.session.listResourceTemplates(params, requestOptions);
834
+ this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
835
+ return ListResourceTemplatesResultSchema.parse(response);
836
+ }
837
+ async readResource(uri) {
838
+ const { ReadResourceResultSchema } = await import('@modelcontextprotocol/sdk/types.js').catch(failedToImport);
839
+ if (!this.session) {
840
+ throw new Error('Server not initialized. Make sure you call connect() first.');
841
+ }
842
+ const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
843
+ const response = await this.session.readResource({ uri }, requestOptions);
844
+ this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
845
+ return ReadResourceResultSchema.parse(response);
846
+ }
335
847
  get name() {
336
848
  return this._name;
337
849
  }
850
+ get sessionId() {
851
+ const transport = this.transport;
852
+ return hasSessionTransport(transport) ? transport.sessionId : undefined;
853
+ }
338
854
  async close() {
855
+ this.isClosed = true;
856
+ this.connectionStateVersion += 1;
857
+ const closeStateVersion = this.connectionStateVersion;
858
+ const reconnectPromise = this.reconnectingClientPromise;
859
+ const client = this.session;
339
860
  const transport = this.transport;
340
- if (hasSessionTransport(transport)) {
341
- const sessionId = transport.sessionId;
342
- if (sessionId && typeof transport.terminateSession === 'function') {
343
- try {
344
- // Best-effort cleanup: we do not actively manage session lifecycles,
345
- // but if the server supports sessions we terminate to avoid leaks.
346
- await transport.terminateSession();
347
- }
348
- catch (error) {
349
- this.logger.warn('Failed to terminate MCP session:', error);
350
- }
861
+ this.session = null;
862
+ this.transport = null;
863
+ await this.closeCloseOwnedStreamableHttpState({
864
+ client,
865
+ transport,
866
+ closeStateVersion,
867
+ closeWarningMessage: client && transport
868
+ ? 'Failed to close MCP client:'
869
+ : transport
870
+ ? 'Failed to close MCP transport:'
871
+ : 'Failed to close MCP client:',
872
+ terminateWarningMessage: 'Failed to terminate MCP session:',
873
+ });
874
+ if (reconnectPromise) {
875
+ await reconnectPromise.catch(() => { });
876
+ // A new connect() may have reopened the server while close() was waiting
877
+ // for the stale reconnect to settle, so only clean up close-owned state.
878
+ if (this.connectionStateVersion !== closeStateVersion || !this.isClosed) {
879
+ return;
351
880
  }
352
- }
353
- if (transport) {
354
- await transport.close();
355
- this.transport = null;
356
- }
357
- if (this.session) {
358
- await this.session.close();
881
+ const recoveredClient = this.session;
882
+ const recoveredTransport = this.transport;
359
883
  this.session = null;
884
+ this.transport = null;
885
+ await this.closeCloseOwnedStreamableHttpState({
886
+ client: recoveredClient,
887
+ transport: recoveredTransport,
888
+ closeStateVersion,
889
+ closeWarningMessage: 'Failed to close reconnected MCP client:',
890
+ terminateWarningMessage: 'Failed to terminate reconnected MCP session:',
891
+ });
360
892
  }
361
893
  }
362
894
  }