@openai/agents-core 0.7.1 → 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/items.d.ts +4 -0
- package/dist/runner/items.js +74 -1
- package/dist/runner/items.js.map +1 -1
- package/dist/runner/items.mjs +72 -1
- package/dist/runner/items.mjs.map +1 -1
- package/dist/runner/sessionPersistence.js +14 -5
- package/dist/runner/sessionPersistence.js.map +1 -1
- package/dist/runner/sessionPersistence.mjs +15 -6
- package/dist/runner/sessionPersistence.mjs.map +1 -1
- package/dist/runner/turnPreparation.js +1 -1
- package/dist/runner/turnPreparation.js.map +1 -1
- package/dist/runner/turnPreparation.mjs +2 -2
- package/dist/runner/turnPreparation.mjs.map +1 -1
- 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
|
@@ -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 {
|
|
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.
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
320
|
-
if (!
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
}
|