UNPKG

@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
"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