import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { ClientCredentialsProvider } from '@modelcontextprotocol/sdk/client/auth-extensions.js';
import { takeResult } from '@modelcontextprotocol/sdk/shared/responseMessage.js';
import { ElicitRequestSchema, LoggingMessageNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
import { context, propagation, trace } from '@opentelemetry/api';
import { McpTool } from './tools/mcp-tool.js';
import { logger } from './logging/index.js';
/** MCP Client for interacting with Model Context Protocol servers. */
export class McpClient {
    /** Default TTL for task polling in milliseconds (60 seconds). */
    static DEFAULT_TTL = 60000;
    /** Default poll timeout for task completion in milliseconds (5 minutes). */
    static DEFAULT_POLL_TIMEOUT = 300000;
    _clientName;
    _clientVersion;
    _transport;
    _state;
    _client;
    _continueOnError;
    _logHandler;
    _disableMcpInstrumentation;
    _tasksConfig;
    _elicitationCallback;
    _registeredToolNames = new Set();
    _onToolsChanged;
    _refreshingTools = false;
    _pendingRefresh = false;
    constructor(args) {
        this._clientName = args.applicationName || 'strands-agents-ts-sdk';
        this._clientVersion = args.applicationVersion || '0.0.1';
        this._transport = McpClient._resolveTransport(args);
        this._state = 'disconnected';
        this._continueOnError = args.continueOnError ?? false;
        this._logHandler = args.logHandler ?? defaultLogHandler;
        this._tasksConfig = args.tasksConfig;
        this._elicitationCallback = args.elicitationCallback;
        this._client = new Client({
            name: this._clientName,
            version: this._clientVersion,
        }, {
            ...(this._elicitationCallback ? { capabilities: { elicitation: { form: {}, url: {} } } } : undefined),
            listChanged: {
                tools: {
                    autoRefresh: false,
                    debounceMs: 300,
                    onChanged: () => {
                        this._handleToolsChanged();
                    },
                },
            },
        });
        this._client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
            this._logHandler(notification.params);
        });
        this._disableMcpInstrumentation = args.disableMcpInstrumentation ?? false;
    }
    static _resolveTransport(args) {
        if (args.transport && args.url) {
            throw new Error('McpClientConfig: provide either "transport" or "url", not both');
        }
        if (!args.transport && !args.url) {
            throw new Error('McpClientConfig: either "transport" or "url" must be provided');
        }
        if (args.transport) {
            if (args.auth || args.authProvider || args.headers) {
                throw new Error('McpClientConfig: "auth", "authProvider", and "headers" require "url" (not compatible with "transport")');
            }
            return args.transport;
        }
        if (args.auth && args.authProvider) {
            throw new Error('McpClientConfig: provide either "auth" or "authProvider", not both');
        }
        const authProvider = args.auth
            ? new ClientCredentialsProvider({
                clientId: args.auth.clientId,
                clientSecret: args.auth.clientSecret,
                ...(args.auth.scopes && { scope: args.auth.scopes.join(' ') }),
            })
            : args.authProvider;
        const url = args.url instanceof URL ? args.url : new URL(args.url);
        return new StreamableHTTPClientTransport(url, {
            ...(authProvider && { authProvider }),
            ...(args.headers && { requestInit: { headers: args.headers } }),
        });
    }
    get client() {
        return this._client;
    }
    get serverCapabilities() {
        return this._client.getServerCapabilities();
    }
    get serverVersion() {
        return this._client.getServerVersion();
    }
    get serverInstructions() {
        return this._client.getInstructions();
    }
    get connectionState() {
        return this._state;
    }
    get clientName() {
        return this._clientName;
    }
    get continueOnError() {
        return this._continueOnError;
    }
    /**
     * Connects the MCP client to the server.
     *
     * Called lazily before any operation that requires a connection. When `continueOnError` is true,
     * connection failures are swallowed and the client enters a `'failed'` state — subsequent
     * calls are no-ops until `connect(true)` is called explicitly to retry.
     *
     * @param reconnect - When true, forces a reconnect even if already connected or failed.
     * @returns A promise that resolves when the connection is established.
     */
    async connect(reconnect = false) {
        if (this._state !== 'disconnected' && !reconnect)
            return;
        if (this._state === 'connected' && reconnect) {
            await this._client.close();
            this._state = 'disconnected';
        }
        if (this._elicitationCallback) {
            const callback = this._elicitationCallback;
            this._client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
                return await callback(extra, request.params);
            });
        }
        try {
            await this._client.connect(this._transport);
            this._state = 'connected';
        }
        catch (error) {
            if (!this._continueOnError)
                throw error;
            this._state = 'failed';
            logger.warn(`client=<${this._clientName}>, error=<${error}> | MCP server failed to connect, continuing (continueOnError)`);
        }
    }
    /**
     * Disconnects the MCP client from the server and cleans up resources.
     *
     * @returns A promise that resolves when the disconnection is complete.
     */
    async disconnect() {
        // Must be done sequentially
        await this._client.close();
        await this._transport.close();
        this._state = 'disconnected';
    }
    /**
     * Enables the `await using` pattern for automatic resource cleanup.
     * Delegates to {@link McpClient.disconnect}.
     */
    async [Symbol.asyncDispose]() {
        await this.disconnect();
    }
    /**
     * Lists the tools available on the server and returns them as executable McpTool instances.
     *
     * @returns A promise that resolves with an array of McpTool instances.
     */
    async listTools() {
        await this.connect();
        if (this._state === 'failed')
            return [];
        const tools = [];
        let cursor;
        do {
            const result = await this._client.listTools(cursor ? { cursor } : undefined);
            tools.push(...result.tools.map((toolSpec) => new McpTool({
                name: toolSpec.name,
                description: toolSpec.description || `Tool which performs ${toolSpec.name}`,
                inputSchema: toolSpec.inputSchema,
                client: this,
            })));
            cursor = result.nextCursor;
        } while (cursor);
        this._registeredToolNames = new Set(tools.map((t) => t.name));
        return tools;
    }
    /**
     * Sets a callback invoked when the MCP server's tool list changes at runtime.
     *
     * @param callback - Handler receiving the previous tool names and the refreshed tool instances,
     *                   or undefined to remove the callback.
     */
    set onToolsChanged(callback) {
        this._onToolsChanged = callback;
    }
    async _handleToolsChanged() {
        if (this._refreshingTools) {
            this._pendingRefresh = true;
            return;
        }
        this._refreshingTools = true;
        try {
            do {
                this._pendingRefresh = false;
                const oldTools = [...this._registeredToolNames];
                const newTools = await this.listTools();
                this._onToolsChanged?.(oldTools, newTools);
            } while (this._pendingRefresh);
        }
        catch (err) {
            logger.warn(`client=<${this._clientName}>, error=<${err}> | failed to refresh tools after toolsChanged notification`);
        }
        finally {
            this._refreshingTools = false;
        }
    }
    /**
     * Invoke a tool on the connected MCP server using an McpTool instance.
     *
     * When `tasksConfig` was provided to the client constructor, uses experimental
     * task-based invocation which supports long-running tools with progress tracking.
     * Otherwise, calls tools directly without task management.
     *
     * @param tool - The McpTool instance to invoke.
     * @param args - The arguments to pass to the tool.
     * @param options - Optional settings for the request.
     * @returns A promise that resolves with the result of the tool invocation.
     */
    async callTool(tool, args, options) {
        await this.connect();
        if (this._state === 'failed')
            throw new Error('MCP server failed to connect. Call connect(true) to retry.');
        if (args === null || args === undefined) {
            return await this.callTool(tool, {}, options);
        }
        if (typeof args !== 'object' || Array.isArray(args)) {
            throw new Error(`MCP Protocol Error: Tool arguments must be a JSON Object (named parameters). Received: ${Array.isArray(args) ? 'Array' : typeof args}`);
        }
        // Inject OpenTelemetry trace context into tool arguments for distributed tracing
        const enhancedArgs = this._disableMcpInstrumentation ? args : injectTraceContext(args);
        const toolArgs = enhancedArgs;
        // When tasksConfig is undefined, call tools directly without task management
        if (this._tasksConfig === undefined) {
            return (await this._client.callTool({ name: tool.name, arguments: toolArgs }, undefined, options));
        }
        // When tasksConfig is defined (even as empty object), use task-based invocation
        // which supports long-running tools with progress tracking
        const stream = this._client.experimental.tasks.callToolStream({ name: tool.name, arguments: toolArgs }, undefined, {
            timeout: this._tasksConfig.ttl ?? McpClient.DEFAULT_TTL,
            maxTotalTimeout: this._tasksConfig.pollTimeout ?? McpClient.DEFAULT_POLL_TIMEOUT,
            resetTimeoutOnProgress: true,
            ...options,
        });
        const result = await takeResult(stream);
        return result;
    }
}
function defaultLogHandler(params) {
    const { level, logger: serverLogger, data } = params;
    const message = `logger=<${serverLogger ?? 'mcp'}>, data=<${JSON.stringify(data)}> | MCP server log`;
    if (level === 'debug') {
        logger.debug(message);
    }
    else if (level === 'info' || level === 'notice') {
        logger.info(message);
    }
    else if (level === 'warning') {
        logger.warn(message);
    }
    else {
        logger.error(message);
    }
}
/**
 * Injects OpenTelemetry trace context into MCP tool call arguments.
 * Returns the args with a `_meta` field containing W3C traceparent headers.
 * If no active span exists or injection fails, returns the original args unchanged.
 *
 * @param args - The tool call arguments (must be a non-null object)
 * @returns The args with trace context injected, or the original args on failure
 */
function injectTraceContext(args) {
    try {
        const currentContext = context.active();
        const currentSpan = trace.getSpan(currentContext);
        if (!currentSpan || !currentSpan.spanContext().traceId) {
            return args;
        }
        const carrier = {};
        propagation.inject(currentContext, carrier);
        const existingMeta = args._meta;
        const mergedMeta = existingMeta && typeof existingMeta === 'object' && !Array.isArray(existingMeta)
            ? { ...existingMeta, ...carrier }
            : carrier;
        return {
            ...args,
            _meta: mergedMeta,
        };
    }
    catch (error) {
        logger.warn(`error=<${error}> | failed to inject trace context into mcp tool call args`);
        return args;
    }
}
//# sourceMappingURL=mcp.js.map