@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
1,176 lines • 51.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeMCPServerStreamableHttp = exports.NodeMCPServerSSE = exports.NodeMCPServerStdio = void 0;
const client_1 = require("@modelcontextprotocol/client");
const mcpShared_1 = require("../../mcpShared.js");
const errors_1 = require("../../errors.js");
const mcpToolCache_1 = require("../../mcpToolCache.js");
const logger_1 = __importStar(require("../../logger.js"));
const mcpLogging_1 = require("../../mcpLogging.js");
const abortSignals_1 = require("../../utils/abortSignals.js");
function failedToImport(error) {
logger_1.default.error(`
Failed to load the MCP SDK. Please install the @modelcontextprotocol/client package.
npm install @modelcontextprotocol/client
`.trim());
throw error;
}
function attachParsedCallToolResultMetadata(result) {
result.content = (0, mcpShared_1.attachCallToolResultMetadata)(result.content, {
_meta: result._meta,
structuredContent: result.structuredContent,
isError: result.isError,
});
return result;
}
function buildRequestOptions(clientSessionTimeoutSeconds, overrides) {
const baseOptions = clientSessionTimeoutSeconds === undefined
? undefined
: { timeout: clientSessionTimeoutSeconds * 1000 };
const mergedOptions = { ...(baseOptions ?? {}), ...(overrides ?? {}) };
return Object.keys(mergedOptions).length === 0 ? undefined : mergedOptions;
}
function buildCacheableRequestOptions(clientSessionTimeoutSeconds, cacheMode = 'refresh') {
return {
...buildRequestOptions(clientSessionTimeoutSeconds),
cacheMode,
};
}
function validateMaxListPages(value) {
if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
throw new errors_1.UserError('maxListPages must be a positive integer.');
}
return value ?? 64;
}
class ToolListPageLimitError extends errors_1.UserError {
constructor() {
super('MCP tool listing exceeded maxListPages. Increase maxListPages or check the server pagination.');
}
}
async function listAllMcpTools(client, requestOptions, maxListPages) {
const tools = [];
const seenCursors = new Set();
let cursor;
let pages = 0;
while (true) {
let response;
try {
response = (await client.request({
method: 'tools/list',
...(cursor === undefined ? {} : { params: { cursor } }),
}, requestOptions));
}
catch (error) {
if (cursor === undefined) {
throw error;
}
throw new Error('MCP tool listing failed while fetching a continuation page.');
}
pages += 1;
tools.push(...response.tools);
const nextCursor = response.nextCursor;
if (nextCursor === undefined) {
return { tools };
}
if (seenCursors.has(nextCursor)) {
throw new Error('MCP server returned a repeated cursor while listing tools.');
}
if (maxListPages !== undefined && pages >= maxListPages) {
throw new ToolListPageLimitError();
}
seenCursors.add(nextCursor);
cursor = nextCursor;
}
}
async function listMcpTools(client, requestOptions, maxListPages) {
if (getNegotiatedModernCapabilities(client) !== undefined) {
try {
const response = await client.listTools(undefined, {
...requestOptions,
cacheMode: 'bypass',
});
return { tools: response.tools };
}
catch (error) {
if (error instanceof client_1.SdkError &&
error.code === client_1.SdkErrorCode.ListPaginationExceeded) {
throw new ToolListPageLimitError();
}
throw error;
}
}
return listAllMcpTools(client, requestOptions, maxListPages);
}
function getNegotiatedModernCapabilities(client) {
const capabilities = client.getServerCapabilities();
return client.getProtocolEra() === 'modern' && capabilities !== undefined
? capabilities
: undefined;
}
function getListedToolForCall(tools, toolName) {
const tool = tools.find((tool) => tool.name === toolName);
if (tool?.execution?.taskSupport === 'required') {
throw new client_1.ProtocolError(client_1.ProtocolErrorCode.InvalidRequest, `Tool "${toolName}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`);
}
return tool;
}
function listResourcesPage(client, params, requestOptions) {
const capabilities = getNegotiatedModernCapabilities(client);
if (capabilities !== undefined && capabilities.resources === undefined) {
return Promise.resolve({ resources: [] });
}
return client.request({
method: 'resources/list',
...(params === undefined ? {} : { params }),
}, requestOptions);
}
function listResourceTemplatesPage(client, params, requestOptions) {
const capabilities = getNegotiatedModernCapabilities(client);
if (capabilities !== undefined && capabilities.resources === undefined) {
return Promise.resolve({ resourceTemplates: [] });
}
return client.request({
method: 'resources/templates/list',
...(params === undefined ? {} : { params }),
}, requestOptions);
}
async function callWithMCPRequestSignal(sourceSignal, call) {
const { signal: requestSignal, cleanup } = (0, abortSignals_1.combineAbortSignals)(sourceSignal);
try {
return await call(requestSignal);
}
catch (error) {
sourceSignal?.throwIfAborted();
throw error;
}
finally {
cleanup();
}
}
function assertMcpToolListingIsCurrent(args) {
if (args.currentClient !== args.listedClient ||
args.currentGeneration !== args.listedGeneration) {
throw new Error('MCP tool listing became stale before it completed.');
}
}
function hasSessionTransport(transport) {
return (transport != null &&
typeof transport.close === 'function' &&
(typeof transport.terminateSession === 'function' ||
transport.sessionId !== undefined));
}
function getTransportProtocolVersion(transport) {
if (transport != null &&
typeof transport.protocolVersion ===
'string') {
return transport.protocolVersion;
}
return undefined;
}
function attachCause(error, cause) {
if (!(error instanceof Error)) {
return error;
}
try {
if (error.cause === undefined) {
Object.defineProperty(error, 'cause', {
value: cause,
configurable: true,
enumerable: false,
writable: true,
});
}
}
catch {
// Best effort only.
}
return error;
}
function getSessionId(transport) {
return hasSessionTransport(transport) ? transport.sessionId : undefined;
}
function shouldTerminateTransportSession(transportToClose, activeTransport) {
const closingSessionId = getSessionId(transportToClose);
if (closingSessionId === undefined) {
return false;
}
const activeSessionId = getSessionId(activeTransport);
return activeSessionId === undefined || activeSessionId !== closingSessionId;
}
function withTimeout(promise, timeoutMs, onTimeout) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(onTimeout());
}, timeoutMs);
void promise.then((value) => {
clearTimeout(timeoutId);
resolve(value);
}, (error) => {
clearTimeout(timeoutId);
reject(error);
});
});
}
async function runMcpTransportOperation(endpoint, operation, action, sourceSignal) {
try {
return await action();
}
catch (error) {
if (error instanceof ToolListPageLimitError ||
isCallerAbortReason(error, sourceSignal)) {
throw error;
}
throw (0, mcpLogging_1.sanitizeMcpTransportError)(error, endpoint, operation);
}
}
function isCallerAbortReason(error, sourceSignal) {
return sourceSignal?.aborted === true && error === sourceSignal.reason;
}
function logMcpTransportWarning(targetLogger, endpoint, operation, message, error) {
const redact = targetLogger.dontLogToolData;
const logError = redact
? (0, logger_1.getSafeErrorType)(error)
: (0, mcpLogging_1.sanitizeMcpTransportError)(error, endpoint, operation);
targetLogger.warn(message, logError);
}
class NodeMCPServerStdio extends mcpShared_1.BaseMCPServerStdio {
session = null;
_cacheDirty = true;
_toolsList = [];
_toolsCacheGeneration = 0;
serverInitializeResult = null;
clientSessionTimeoutSeconds;
timeout;
maxListPages;
params;
_name;
transport = null;
constructor(params) {
super(params);
this.maxListPages = validateMaxListPages(params.maxListPages);
this.clientSessionTimeoutSeconds = params.clientSessionTimeoutSeconds ?? 5;
this.timeout = params.timeout ?? client_1.DEFAULT_REQUEST_TIMEOUT_MSEC;
if ('fullCommand' in params) {
const elements = params.fullCommand.split(' ');
const command = elements.shift();
if (!command) {
throw new Error('Invalid fullCommand: ' + params.fullCommand);
}
this.params = {
...params,
command: command,
args: elements,
encoding: params.encoding || 'utf-8',
encodingErrorHandler: params.encodingErrorHandler || 'strict',
};
}
else {
this.params = params;
}
this._name = params.name || `stdio: ${this.params.command}`;
}
async connect() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
try {
const { StdioClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/client/stdio'))).catch(failedToImport);
const { Client } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/client'))).catch(failedToImport);
this.transport = new StdioClientTransport({
command: this.params.command,
args: this.params.args,
env: this.params.env,
cwd: this.params.cwd,
});
this.session = new Client({
name: this._name,
version: '1.0.0', // You may want to make this configurable
}, {
versionNegotiation: { mode: 'auto' },
listMaxPages: this.maxListPages ?? 0,
});
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
await this.session.connect(this.transport, requestOptions);
this.serverInitializeResult = {
serverInfo: { name: this._name, version: '1.0.0' },
};
}
catch (e) {
(0, logger_1.logToolActionError)(this.logger, 'Error initializing MCP server:', e);
await this.close();
throw e;
}
this.debugLog(() => `Connected to MCP server: ${this._name}`);
}
async invalidateToolsCache() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
await (0, mcpToolCache_1.invalidateServerToolsCache)(this.name);
}
async listTools() {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
if (this.cacheToolsList && !this._cacheDirty && this._toolsList) {
return this._toolsList;
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const session = this.session;
const cacheGeneration = this._toolsCacheGeneration;
const response = await listMcpTools(session, requestOptions, this.maxListPages);
this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`);
assertMcpToolListingIsCurrent({
listedClient: session,
currentClient: this.session,
listedGeneration: cacheGeneration,
currentGeneration: this._toolsCacheGeneration,
});
const tools = response.tools;
this._toolsList = tools;
this._cacheDirty = false;
return tools;
}
async callTool(toolName, args, meta, options) {
return (await this.callToolResult(toolName, args, meta, options)).content;
}
async callToolResult(toolName, args, meta, options) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const session = this.session;
const params = {
name: toolName,
arguments: args ?? {},
...(meta != null ? { _meta: meta } : {}),
};
const response = await callWithMCPRequestSignal(options?.signal, (requestSignal) => session.callTool(params, {
...buildRequestOptions(this.clientSessionTimeoutSeconds, {
timeout: this.timeout,
signal: requestSignal,
}),
toolDefinition: getListedToolForCall(this._toolsList, toolName),
}));
const result = attachParsedCallToolResultMetadata(response);
this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
return result;
}
async listResources(params) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const response = await listResourcesPage(this.session, params, requestOptions);
this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
return response;
}
async listResourceTemplates(params) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const response = await listResourceTemplatesPage(this.session, params, requestOptions);
this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
return response;
}
async readResource(uri) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildCacheableRequestOptions(this.clientSessionTimeoutSeconds);
const response = await this.session.readResource({ uri }, requestOptions);
this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
return response;
}
get name() {
return this._name;
}
async close() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
const transport = this.transport;
if (transport && typeof transport.terminateSession === 'function') {
try {
// Best-effort cleanup: we do not actively manage session lifecycles,
// but if the server supports sessions we terminate to avoid leaks.
await transport.terminateSession();
}
catch (error) {
(0, logger_1.logToolActionWarning)(this.logger, 'Failed to terminate MCP session:', error);
}
}
if (transport) {
await transport.close();
this.transport = null;
}
if (this.session) {
await this.session.close();
this.session = null;
}
}
}
exports.NodeMCPServerStdio = NodeMCPServerStdio;
class NodeMCPServerSSE extends mcpShared_1.BaseMCPServerSSE {
session = null;
_cacheDirty = true;
_toolsList = [];
_toolsCacheGeneration = 0;
serverInitializeResult = null;
clientSessionTimeoutSeconds;
timeout;
maxListPages;
params;
_name;
transport = null;
constructor(params) {
super(params);
this.maxListPages = validateMaxListPages(params.maxListPages);
this.clientSessionTimeoutSeconds = params.clientSessionTimeoutSeconds ?? 5;
this.params = params;
this._name = params.name || `sse: ${this.params.url}`;
this.timeout = params.timeout ?? client_1.DEFAULT_REQUEST_TIMEOUT_MSEC;
}
async connect() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
try {
const { SSEClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/client'))).catch(failedToImport);
const { Client } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/client'))).catch(failedToImport);
this.transport = new SSEClientTransport(new URL(this.params.url), {
authProvider: this.params.authProvider,
requestInit: this.params.requestInit,
eventSourceInit: this.params.eventSourceInit,
fetch: this.params.fetch,
});
this.session = new Client({
name: this._name,
version: '1.0.0', // You may want to make this configurable
}, { listMaxPages: this.maxListPages ?? 0 });
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
await this.session.connect(this.transport, requestOptions);
this.serverInitializeResult = {
serverInfo: { name: this._name, version: '1.0.0' },
};
}
catch (e) {
const error = (0, mcpLogging_1.sanitizeMcpTransportError)(e, this.params.url, 'SSE connect');
(0, logger_1.logToolActionError)(this.logger, 'Error initializing MCP server:', error);
await this.close();
throw error;
}
this.debugLog(() => `Connected to MCP server: ${this._name}`);
}
async invalidateToolsCache() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
await (0, mcpToolCache_1.invalidateServerToolsCache)(this.name);
}
async listTools() {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
if (this.cacheToolsList && !this._cacheDirty && this._toolsList) {
return this._toolsList;
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const session = this.session;
const cacheGeneration = this._toolsCacheGeneration;
const response = await runMcpTransportOperation(this.params.url, 'SSE list tools', () => listMcpTools(session, requestOptions, this.maxListPages));
this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`);
assertMcpToolListingIsCurrent({
listedClient: session,
currentClient: this.session,
listedGeneration: cacheGeneration,
currentGeneration: this._toolsCacheGeneration,
});
const tools = response.tools;
this._toolsList = tools;
this._cacheDirty = false;
return tools;
}
async callTool(toolName, args, meta, options) {
return (await this.callToolResult(toolName, args, meta, options)).content;
}
async callToolResult(toolName, args, meta, options) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const session = this.session;
const params = {
name: toolName,
arguments: args ?? {},
...(meta != null ? { _meta: meta } : {}),
};
const response = await runMcpTransportOperation(this.params.url, 'SSE tool call', () => callWithMCPRequestSignal(options?.signal, (requestSignal) => session.callTool(params, {
...buildRequestOptions(this.clientSessionTimeoutSeconds, {
timeout: this.timeout,
signal: requestSignal,
}),
toolDefinition: getListedToolForCall(this._toolsList, toolName),
})), options?.signal);
const result = attachParsedCallToolResultMetadata(response);
this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
return result;
}
async listResources(params) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const response = await runMcpTransportOperation(this.params.url, 'SSE list resources', () => listResourcesPage(this.session, params, requestOptions));
this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
return response;
}
async listResourceTemplates(params) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const response = await runMcpTransportOperation(this.params.url, 'SSE list resource templates', () => listResourceTemplatesPage(this.session, params, requestOptions));
this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
return response;
}
async readResource(uri) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildCacheableRequestOptions(this.clientSessionTimeoutSeconds);
const response = await runMcpTransportOperation(this.params.url, 'SSE read resource', () => this.session.readResource({ uri }, requestOptions));
this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
return response;
}
get name() {
return this._name;
}
async close() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
await runMcpTransportOperation(this.params.url, 'SSE cleanup', async () => {
const transport = this.transport;
if (hasSessionTransport(transport)) {
const sessionId = transport.sessionId;
if (sessionId && typeof transport.terminateSession === 'function') {
try {
// Best-effort cleanup: we do not actively manage session lifecycles,
// but if the server supports sessions we terminate to avoid leaks.
await transport.terminateSession();
}
catch (error) {
logMcpTransportWarning(this.logger, this.params.url, 'SSE session termination', 'Failed to terminate MCP session:', error);
}
}
}
if (transport) {
await transport.close();
this.transport = null;
}
if (this.session) {
await this.session.close();
this.session = null;
}
});
}
}
exports.NodeMCPServerSSE = NodeMCPServerSSE;
class NodeMCPServerStreamableHttp extends mcpShared_1.BaseMCPServerStreamableHttp {
session = null;
_cacheDirty = true;
_toolsList = [];
_toolsCacheGeneration = 0;
serverInitializeResult = null;
clientSessionTimeoutSeconds;
timeout;
maxListPages;
params;
_name;
transport = null;
reconnectingClientPromise = null;
reconnectingClientTarget = null;
isClosed = false;
connectionStateVersion = 0;
constructor(params) {
super(params);
this.maxListPages = validateMaxListPages(params.maxListPages);
this.clientSessionTimeoutSeconds = params.clientSessionTimeoutSeconds ?? 5;
this.params = params;
this._name = params.name || `streamable-http: ${this.params.url}`;
this.timeout = params.timeout ?? client_1.DEFAULT_REQUEST_TIMEOUT_MSEC;
}
async loadStreamableHttpRuntime() {
const clientModule = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/client'))).catch(failedToImport);
return { clientModule, transportModule: clientModule };
}
createStreamableHttpTransport(StreamableHTTPClientTransport, options = {}) {
const transportOptions = {
authProvider: this.params.authProvider,
requestInit: this.params.requestInit,
fetch: this.params.fetch,
reconnectionOptions: this.params.reconnectionOptions,
sessionId: options.sessionId,
};
const transport = new StreamableHTTPClientTransport(new URL(this.params.url), transportOptions);
if (options.protocolVersion !== undefined &&
typeof transport.setProtocolVersion === 'function') {
transport.setProtocolVersion(options.protocolVersion);
}
return transport;
}
async createConnectedStreamableHttpClient(options = {}) {
const { clientModule, transportModule } = await this.loadStreamableHttpRuntime();
const { Client } = clientModule;
const { StreamableHTTPClientTransport } = transportModule;
const transport = this.createStreamableHttpTransport(StreamableHTTPClientTransport, options);
const client = new Client({
name: this._name,
version: '1.0.0',
}, {
versionNegotiation: { mode: 'auto' },
listMaxPages: this.maxListPages ?? 0,
});
try {
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
await client.connect(transport, requestOptions);
return { client, transport };
}
catch (error) {
await this.closeStreamableHttpClient({
client,
transport,
}, {
terminateSession: options.sessionId === undefined,
closeWarningMessage: 'Failed to close failed MCP connect client:',
terminateWarningMessage: 'Failed to terminate failed MCP connect session:',
});
throw error;
}
}
getClientSessionTimeoutMs() {
return Math.max(1, (this.clientSessionTimeoutSeconds ?? 5) * 1000);
}
async publishConnectedStreamableHttpClient(args) {
this.transport = args.transport;
this.session = args.client;
this.connectionStateVersion += 1;
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
if (!args.preserveTools) {
this._toolsList = [];
}
await (0, mcpToolCache_1.invalidateServerToolsCache)(this.name);
}
async clearPublishedStreamableHttpClientIfCurrent(args) {
if (this.connectionStateVersion !== args.stateVersion ||
this.session !== args.client ||
this.transport !== args.transport) {
return;
}
this.session = null;
this.transport = null;
this._cacheDirty = true;
this._toolsList = [];
await (0, mcpToolCache_1.invalidateServerToolsCache)(this.name);
}
async reopenSharedStreamableHttpSession(client) {
await withTimeout(client.notification({ method: 'notifications/initialized' }), this.getClientSessionTimeoutMs(), () => new Error('Timed out reopening shared streamable HTTP MCP session.'));
}
async terminateDetachedStreamableHttpSession(transport, warningMessage) {
const sessionId = getSessionId(transport);
if (sessionId === undefined) {
return;
}
try {
const { transportModule } = await this.loadStreamableHttpRuntime();
const detachedTransport = this.createStreamableHttpTransport(transportModule.StreamableHTTPClientTransport, {
protocolVersion: getTransportProtocolVersion(transport),
sessionId,
});
try {
if (typeof detachedTransport.terminateSession === 'function') {
// start() installs the abort controller used by terminateSession().
// Closing this detached transport then cancels a stalled DELETE too.
await detachedTransport.start();
await withTimeout(detachedTransport.terminateSession(), this.getClientSessionTimeoutMs(), () => new Error('Timed out terminating streamable HTTP MCP session.'));
}
}
finally {
await detachedTransport.close().catch(() => { });
}
}
catch (error) {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP session termination', warningMessage, error);
}
}
async closeCloseOwnedStreamableHttpState(args) {
const { client, transport, closeStateVersion, closeWarningMessage, terminateWarningMessage, } = args;
if (client && transport) {
await this.closeStreamableHttpClient({
client,
transport,
}, {
terminateSession: false,
closeWarningMessage,
terminateWarningMessage,
});
}
else if (transport) {
await transport.close().catch((error) => {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP cleanup', closeWarningMessage, error);
});
}
else if (client) {
await client.close().catch((error) => {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP cleanup', closeWarningMessage, error);
});
}
if (!transport ||
this.connectionStateVersion !== closeStateVersion ||
!this.isClosed ||
!shouldTerminateTransportSession(transport, this.transport)) {
return;
}
await this.terminateDetachedStreamableHttpSession(transport, terminateWarningMessage);
}
async callToolWithClient(client, toolName, args, meta, options) {
const params = {
name: toolName,
arguments: args ?? {},
...(meta != null ? { _meta: meta } : {}),
};
const response = await callWithMCPRequestSignal(options?.signal, (requestSignal) => client.callTool(params, {
...buildRequestOptions(this.clientSessionTimeoutSeconds, {
timeout: this.timeout,
signal: requestSignal,
}),
toolDefinition: getListedToolForCall(this._toolsList, toolName),
}));
return attachParsedCallToolResultMetadata(response);
}
async closeStreamableHttpClient(args, options) {
const { client, transport } = args;
if (options.terminateSession && transport.sessionId) {
await this.terminateDetachedStreamableHttpSession(transport, options.terminateWarningMessage);
}
if (client.transport === transport) {
await client.close().catch((error) => {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP cleanup', options.closeWarningMessage, error);
});
return;
}
await transport.close().catch((error) => {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP cleanup', options.closeWarningMessage, error);
});
await client.close().catch((error) => {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP cleanup', options.closeWarningMessage, error);
});
}
async closeStreamableHttpTransport(transport, warningMessage) {
await transport.close().catch((error) => {
logMcpTransportWarning(this.logger, this.params.url, 'streamable HTTP cleanup', warningMessage, error);
});
}
async reconnectExistingStreamableHttpClient(args) {
const { transportModule } = await this.loadStreamableHttpRuntime();
const { StreamableHTTPClientTransport } = transportModule;
const transport = this.createStreamableHttpTransport(StreamableHTTPClientTransport, {
protocolVersion: args.protocolVersion,
sessionId: args.sessionId,
});
try {
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
await args.client.connect(transport, requestOptions);
if (args.sessionId !== undefined &&
args.client.getProtocolEra() === 'legacy') {
// Reconnecting with an existing session skips initialize(), so resend
// initialized to reopen the shared SSE stream for async responses.
await this.reopenSharedStreamableHttpSession(args.client);
}
return { client: args.client, transport };
}
catch (error) {
await this.closeStreamableHttpClient({
client: args.client,
transport,
}, {
terminateSession: args.sessionId === undefined,
closeWarningMessage: 'Failed to close failed MCP reconnect client:',
terminateWarningMessage: 'Failed to terminate failed MCP reconnect session:',
});
throw error;
}
}
async reconnectClosedStreamableHttpClient(args) {
const { cause, failedClient, failedStateVersion } = args;
if (this.isClosed) {
throw attachCause(new Error('Cannot reconnect a closed streamable HTTP MCP server.'), cause);
}
if (this.connectionStateVersion !== failedStateVersion ||
this.session !== failedClient) {
if (this.session) {
return this.session;
}
throw attachCause(new Error('Streamable HTTP MCP server changed before reconnect.'), cause);
}
// Multiple tool calls can discover the same closed shared session in parallel.
// Share one reconnect so later callers do not replace and close the new client.
if (this.reconnectingClientPromise) {
const reconnectingClientTarget = this.reconnectingClientTarget;
if (reconnectingClientTarget?.client === failedClient &&
reconnectingClientTarget.stateVersion === failedStateVersion) {
try {
return await this.reconnectingClientPromise;
}
catch (error) {
throw attachCause(error, cause);
}
}
}
const reconnectStateVersion = this.connectionStateVersion;
const previousClient = this.session;
const previousTransport = this.transport;
const reconnectPromise = (async () => {
const sessionId = previousTransport && hasSessionTransport(previousTransport)
? previousTransport.sessionId
: undefined;
const protocolVersion = getTransportProtocolVersion(previousTransport);
if (!previousClient || !previousTransport) {
throw new Error('Cannot reconnect streamable HTTP MCP server without an active client.');
}
try {
if (sessionId === undefined) {
await this.closeStreamableHttpClient({
client: previousClient,
transport: previousTransport,
}, {
terminateSession: false,
closeWarningMessage: 'Failed to close stale MCP client:',
terminateWarningMessage: 'Failed to terminate stale MCP session:',
});
}
else {
// Keep the v2 Client's negotiated capabilities and tool metadata when
// resuming the same server session on a replacement transport.
await this.closeStreamableHttpTransport(previousTransport, 'Failed to close stale MCP transport:');
}
const recovered = await this.reconnectExistingStreamableHttpClient({
client: previousClient,
protocolVersion,
sessionId,
});
if (this.connectionStateVersion !== reconnectStateVersion ||
this.isClosed) {
await this.closeStreamableHttpClient({
client: recovered.client,
transport: recovered.transport,
}, {
terminateSession: false,
closeWarningMessage: 'Failed to close discarded MCP client:',
terminateWarningMessage: 'Failed to terminate discarded MCP session:',
});
if (this.isClosed) {
throw new Error('Streamable HTTP MCP server was closed during reconnect.');
}
if (this.session) {
return this.session;
}
throw new Error('Streamable HTTP MCP server changed during reconnect.');
}
await this.publishConnectedStreamableHttpClient({
client: recovered.client,
transport: recovered.transport,
preserveTools: sessionId !== undefined,
});
return recovered.client;
}
catch (error) {
await this.clearPublishedStreamableHttpClientIfCurrent({
client: previousClient,
transport: previousTransport,
stateVersion: reconnectStateVersion,
});
throw error;
}
})();
this.reconnectingClientPromise = reconnectPromise;
this.reconnectingClientTarget = {
client: failedClient,
stateVersion: failedStateVersion,
};
try {
return await reconnectPromise;
}
catch (error) {
throw attachCause(error, cause);
}
finally {
if (this.reconnectingClientPromise === reconnectPromise) {
this.reconnectingClientPromise = null;
this.reconnectingClientTarget = null;
}
}
}
async shouldReconnectClosedStreamableHttpClient(error, client) {
// Explicit session ids are a released contract, so keep callers pinned to the
// session they selected instead of silently switching them to a fresh one.
if (this.params.sessionId !== undefined) {
return 'none';
}
const { clientModule } = await this.loadStreamableHttpRuntime();
const { SdkError, SdkErrorCode } = clientModule;
if (error instanceof SdkError && error.code === SdkErrorCode.NotConnected) {
return client.transport == null ? 'reconnect-and-retry' : 'none';
}
// MCP SDK v2 currently throws this plain Error after a transport detaches.
// Guard the message match with the actual disconnected client state.
if (client.transport == null &&
error instanceof Error &&
error.message === 'Not connected') {
return 'reconnect-and-retry';
}
return error instanceof SdkError &&
error.code === SdkErrorCode.ConnectionClosed
? 'reconnect-only'
: 'none';
}
async connect() {
const connectStateVersion = this.connectionStateVersion;
this.isClosed = false;
try {
const { client, transport } = await this.createConnectedStreamableHttpClient({
sessionId: this.params.sessionId,
});
if (this.isClosed ||
this.connectionStateVersion !== connectStateVersion) {
await this.closeStreamableHttpClient({
client,
transport,
}, {
// A stale overlapping connect can point at the same shared session as
// the winner, so only terminate truly discarded sessions.
terminateSession: shouldTerminateTransportSession(transport, this.transport),
closeWarningMessage: 'Failed to close discarded MCP client:',
terminateWarningMessage: 'Failed to terminate discarded MCP session:',
});
if (this.isClosed) {
throw new Error('Streamable HTTP MCP server was closed during connect.');
}
throw new Error('Streamable HTTP MCP server changed during connect.');
}
const previousClient = this.session;
const previousTransport = this.transport;
await this.publishConnectedStreamableHttpClient({
client,
transport,
});
this.serverInitializeResult = {
serverInfo: { name: this._name, version: '1.0.0' },
};
if (previousClient && previousTransport) {
await this.closeStreamableHttpClient({
client: previousClient,
transport: previousTransport,
}, {
terminateSession: shouldTerminateTransportSession(previousTransport, transport),
closeWarningMessage: 'Failed to close replaced MCP client:',
terminateWarningMessage: 'Failed to terminate replaced MCP session:',
});
}
}
catch (e) {
// A losing concurrent connect can fail after another connect already
// published a healthy shared session, so avoid closing shared state here.
const error = (0, mcpLogging_1.sanitizeMcpTransportError)(e, this.params.url, 'streamable HTTP connect');
(0, logger_1.logToolActionError)(this.logger, 'Error initializing MCP server:', error);
throw error;
}
this.debugLog(() => `Connected to MCP server: ${this._name}`);
}
async invalidateToolsCache() {
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
await (0, mcpToolCache_1.invalidateServerToolsCache)(this.name);
}
async listTools() {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
if (this.cacheToolsList && !this._cacheDirty && this._toolsList) {
return this._toolsList;
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const session = this.session;
const cacheGeneration = this._toolsCacheGeneration;
const response = await runMcpTransportOperation(this.params.url, 'streamable HTTP list tools', () => listMcpTools(session, requestOptions, this.maxListPages));
this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`);
assertMcpToolListingIsCurrent({
listedClient: session,
currentClient: this.session,
listedGeneration: cacheGeneration,
currentGeneration: this._toolsCacheGeneration,
});
const tools = response.tools;
this._toolsList = tools;
this._cacheDirty = false;
return tools;
}
async callTool(toolName, args, meta, options) {
return (await this.callToolResult(toolName, args, meta, options)).content;
}
async callToolResult(toolName, args, meta, options) {
const client = this.session;
if (!client) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const callToolStateVersion = this.connectionStateVersion;
let result;
try {
result = await this.callToolWithClient(client, toolName, args, meta, options);
}
catch (error) {
if (isCallerAbortReason(error, options?.signal)) {
throw error;
}
const recoveryStrategy = await runMcpTransportOperation(this.params.url, 'streamable HTTP tool call recovery classification', () => this.shouldReconnectClosedStreamableHttpClient(error, client));
if (recoveryStrategy === 'none') {
throw (0, mcpLogging_1.sanitizeMcpTransportError)(error, this.params.url, 'streamable HTTP tool call');
}
this.debugLog(() => `Reconnecting closed streamable HTTP MCP session for ${toolName}.`);
const recoveredClient = await runMcpTransportOperation(this.params.url, 'streamable HTTP reconnect', () => this.reconnectClosedStreamableHttpClient({
cause: error,
failedClient: client,
failedStateVersion: callToolStateVersion,
}));
if (recoveryStrategy === 'reconnect-only') {
throw (0, mcpLogging_1.sanitizeMcpTransportError)(error, this.params.url, 'streamable HTTP tool call');
}
try {
result = await this.callToolWithClient(recoveredClient, toolName, args, meta, options);
}
catch (retryError) {
if (isCallerAbortReason(retryError, options?.signal)) {
throw retryError;
}
throw (0, mcpLogging_1.sanitizeMcpTransportError)(attachCause(retryError, error), this.params.url, 'streamable HTTP tool call retry');
}
}
this.debugLog(() => `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`);
return result;
}
async listResources(params) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const response = await runMcpTransportOperation(this.params.url, 'streamable HTTP list resources', () => listResourcesPage(this.session, params, requestOptions));
this.debugLog(() => `Listed resources: ${JSON.stringify(response)}`);
return response;
}
async listResourceTemplates(params) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildRequestOptions(this.clientSessionTimeoutSeconds);
const response = await runMcpTransportOperation(this.params.url, 'streamable HTTP list resource templates', () => listResourceTemplatesPage(this.session, params, requestOptions));
this.debugLog(() => `Listed resource templates: ${JSON.stringify(response)}`);
return response;
}
async readResource(uri) {
if (!this.session) {
throw new Error('Server not initialized. Make sure you call connect() first.');
}
const requestOptions = buildCacheableRequestOptions(this.clientSessionTimeoutSeconds);
const response = await runMcpTransportOperation(this.params.url, 'streamable HTTP read resource', () => this.session.readResource({ uri }, requestOptions));
this.debugLog(() => `Read resource ${uri}: ${JSON.stringify(response)}`);
return response;
}
get name() {
return this._name;
}
get sessionId() {
const transport = this.transport;
return hasSessionTransport(transport) ? transport.sessionId : undefined;
}
async close() {
this.isClosed = true;
this.connectionStateVersion += 1;
this._toolsCacheGeneration += 1;
this._cacheDirty = true;
this._toolsList = [];
const closeStateVersion = this.connectionStateVersion;
const reconnectPromise = this.reconnectingClientPromise;
const client = this.session;
const transport = this.transport;
this.session = null;
this.transport = null;
await this.closeCloseOwnedStreamableHttpState({
client,
transport,
closeStateVersion,
closeWarningMessage: client && transport
? 'Failed to close MCP client:'
: transport
? 'Failed to close MCP transport:'
: 'Failed to close MCP client:',
terminateWarningMessage: 'Failed to terminate MCP session:',
});
if (reconnectPromise) {
await reconnectPromise.catch(() => { });
// A new connect() may have reopened the server while close() was waiting
// for the stale reconnect to settle, so only clean up close-owned state.
if (this.connectionStateVersion !== closeStateVersion || !this.isClosed) {
return;
}
const recoveredClient = this.session;
const recoveredTransport = this.transport;
this.session = null;
this.transport = null;
await this.closeCloseOwnedStreamableHttpState({
client: recoveredClient,
transport: recoveredTransport,
closeStateVersion,
closeWarningMessage: 'Failed to close reconnected MCP client:',
terminateWarningMessage: 'Failed to terminate reconnected MCP session:',
});
}
}
}
exports.NodeMCPServerStreamableHttp = NodeMCPServerStreamableHttp;
//# sourceMappingURL=node.js.map