#!/usr/bin/env node
import { L as state, c as logger, j as getEnvInt, k as getEnvBool } from "./logger-4ntIRWmf.js";
import { h as isMissingPackageImportError } from "./util-DAS7mIn_.js";
import { l as renderAuthVars, n as getAuthHeaders, o as getOAuthTokenWithExpiry, r as getAuthQueryParams, t as applyQueryParams } from "./util-DHD4E_Wx.js";
import path from "path";
//#region src/providers/mcp/client.ts
async function loadMcpClientSdk() {
	try {
		return await import("@modelcontextprotocol/sdk/client/index.js");
	} catch (error) {
		if (isMissingPackageImportError(error, "@modelcontextprotocol/sdk")) throw new Error("The @modelcontextprotocol/sdk package is required for MCP provider support. Install it with: npm install @modelcontextprotocol/sdk");
		throw error;
	}
}
/**
* Get the effective request options for MCP requests.
* Priority: config values > MCP_REQUEST_TIMEOUT_MS env var > undefined (SDK default of 60s)
*/
function getEffectiveRequestOptions(config) {
	const timeout = config.timeout ?? getEnvInt("MCP_REQUEST_TIMEOUT_MS");
	if (!timeout && !config.resetTimeoutOnProgress && !config.maxTotalTimeout) return;
	const options = {};
	if (timeout) options.timeout = timeout;
	if (config.resetTimeoutOnProgress) options.resetTimeoutOnProgress = config.resetTimeoutOnProgress;
	if (config.maxTotalTimeout) options.maxTotalTimeout = config.maxTotalTimeout;
	return options;
}
var MCPClient = class {
	clients = /* @__PURE__ */ new Map();
	tools = /* @__PURE__ */ new Map();
	config;
	transports = /* @__PURE__ */ new Map();
	oauthConfigs = /* @__PURE__ */ new Map();
	tokenExpiresAt = /* @__PURE__ */ new Map();
	tokenRefreshLocks = /* @__PURE__ */ new Map();
	get hasInitialized() {
		return this.clients.size > 0;
	}
	get connectedServers() {
		return Array.from(this.clients.keys());
	}
	/**
	* Check if debug mode is enabled (config takes priority over env var)
	*/
	get isDebugEnabled() {
		return this.config.debug ?? getEnvBool("MCP_DEBUG") ?? false;
	}
	/**
	* Check if verbose mode is enabled (config takes priority over env var)
	*/
	get isVerboseEnabled() {
		return this.config.verbose ?? getEnvBool("MCP_VERBOSE") ?? false;
	}
	constructor(config) {
		this.config = config;
	}
	async initialize() {
		if (!this.config.enabled) return;
		const servers = this.config.servers || (this.config.server ? [this.config.server] : []);
		for (const server of servers) {
			logger.info(`connecting to server ${server.name || server.url || server.path || "default"}`);
			await this.connectToServer(server);
		}
	}
	async connectToServer(server) {
		const serverKey = server.name || server.url || server.path || "default";
		const { Client } = await loadMcpClientSdk();
		const client = new Client({
			name: "promptfoo-MCP",
			version: "1.0.0",
			description: "Promptfoo MCP client for connecting to MCP servers during LLM evaluations"
		});
		let transport;
		try {
			const requestOptions = getEffectiveRequestOptions(this.config);
			if (server.command && server.args) {
				const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
				transport = new StdioClientTransport({
					command: server.command,
					args: server.args,
					env: process.env
				});
				await client.connect(transport, requestOptions);
			} else if (server.path) {
				const isJs = server.path.endsWith(".js");
				const isPy = server.path.endsWith(".py");
				if (!isJs && !isPy) throw new Error("Local server must be a .js or .py file");
				const command = isPy ? process.platform === "win32" ? "python" : "python3" : process.execPath;
				const serverPath = state.basePath ? path.resolve(state.basePath, server.path) : server.path;
				const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
				transport = new StdioClientTransport({
					command,
					args: [serverPath],
					env: process.env
				});
				await client.connect(transport, requestOptions);
			} else if (server.url) {
				const renderedServer = renderAuthVars(server);
				let authHeaders = {};
				if (renderedServer.auth?.type === "oauth") {
					const oauthAuth = renderedServer.auth;
					logger.debug("[MCP] Fetching OAuth token");
					const { accessToken, expiresAt } = await getOAuthTokenWithExpiry(oauthAuth, server.url);
					authHeaders = { Authorization: `Bearer ${accessToken}` };
					this.oauthConfigs.set(serverKey, {
						serverKey,
						serverConfig: server,
						auth: oauthAuth
					});
					this.tokenExpiresAt.set(serverKey, expiresAt);
				} else authHeaders = getAuthHeaders(renderedServer);
				const headers = {
					...server.headers || {},
					...authHeaders
				};
				const queryParams = getAuthQueryParams(renderedServer);
				const serverUrl = applyQueryParams(server.url, queryParams);
				const transportOptions = {};
				if (Object.keys(headers).length > 0) transportOptions.requestInit = { headers };
				const hasOptions = Object.keys(transportOptions).length > 0;
				try {
					const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
					transport = new StreamableHTTPClientTransport(new URL(serverUrl), hasOptions ? transportOptions : void 0);
					await client.connect(transport, requestOptions);
					logger.debug("Connected using Streamable HTTP transport");
				} catch (error) {
					logger.debug(`Failed to connect to MCP server with Streamable HTTP transport ${serverKey}: ${error}`);
					const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
					transport = new SSEClientTransport(new URL(serverUrl), hasOptions ? transportOptions : void 0);
					await client.connect(transport, requestOptions);
					logger.debug("Connected using SSE transport");
				}
			} else throw new Error("Either command+args or path or url must be specified for MCP server");
			if (this.config.pingOnConnect) try {
				await client.ping(requestOptions);
				logger.debug(`MCP server ${serverKey} ping successful`);
			} catch (pingError) {
				const pingErrorMessage = pingError instanceof Error ? pingError.message : String(pingError);
				throw new Error(`MCP server ${serverKey} ping failed: ${pingErrorMessage}`);
			}
			const serverTools = (await client.listTools(void 0, requestOptions))?.tools?.map((tool) => ({
				name: tool.name,
				description: tool.description || "",
				inputSchema: tool.inputSchema
			})) || [];
			let filteredTools = serverTools;
			if (this.config.tools) filteredTools = serverTools.filter((tool) => this.config.tools?.includes(tool.name));
			if (this.config.exclude_tools) filteredTools = filteredTools.filter((tool) => !this.config.exclude_tools?.includes(tool.name));
			this.transports.set(serverKey, transport);
			this.clients.set(serverKey, client);
			this.tools.set(serverKey, filteredTools);
			if (this.isVerboseEnabled) console.log(`Connected to MCP server ${serverKey} with tools:`, filteredTools.map((tool) => tool.name));
		} catch (error) {
			const errorMessage = error instanceof Error ? error.message : String(error);
			if (this.isDebugEnabled) logger.error(`Failed to connect to MCP server ${serverKey}: ${errorMessage}`);
			throw new Error(`Failed to connect to MCP server ${serverKey}: ${errorMessage}`);
		}
	}
	getAllTools() {
		return Array.from(this.tools.values()).flat();
	}
	/**
	* Proactively refresh OAuth token for a server if it's close to expiration.
	* Uses a locking mechanism to prevent concurrent refresh attempts.
	*/
	async refreshOAuthTokenIfNeeded(serverKey) {
		const oauthConfig = this.oauthConfigs.get(serverKey);
		if (!oauthConfig) return;
		await this.refreshOAuthToken(serverKey, oauthConfig, false);
	}
	hasValidToken(serverKey) {
		const expiresAt = this.tokenExpiresAt.get(serverKey);
		return expiresAt != null && this.clients.has(serverKey) && Date.now() + 6e4 < expiresAt;
	}
	async refreshOAuthToken(serverKey, oauthConfig, forceRefresh) {
		while (true) {
			const existingRefreshPromise = this.tokenRefreshLocks.get(serverKey)?.promise;
			if (!existingRefreshPromise) break;
			logger.debug(`[MCP] Token refresh already in progress for ${serverKey}, waiting...`);
			try {
				await existingRefreshPromise;
				if (this.hasValidToken(serverKey)) return;
				logger.debug(`[MCP] Token still needs refresh for ${serverKey}, refreshing again...`);
			} catch {
				logger.debug(`[MCP] Previous token refresh failed for ${serverKey}, retrying...`);
			}
		}
		if (!forceRefresh && this.hasValidToken(serverKey)) {
			logger.debug(`[MCP] Token for ${serverKey} still valid, no refresh needed`);
			return;
		}
		logger.debug(`[MCP] Refreshing OAuth token for server ${serverKey}`);
		const refreshLock = { promise: this.performTokenRefresh(serverKey, oauthConfig) };
		this.tokenRefreshLocks.set(serverKey, refreshLock);
		try {
			await refreshLock.promise;
		} finally {
			if (this.tokenRefreshLocks.get(serverKey) === refreshLock) this.tokenRefreshLocks.delete(serverKey);
		}
	}
	/**
	* Perform the actual token refresh and reconnection.
	*/
	async performTokenRefresh(serverKey, oauthConfig) {
		const existingTransport = this.transports.get(serverKey);
		const existingClient = this.clients.get(serverKey);
		if (existingTransport) await existingTransport.close().catch(() => {});
		if (existingClient) await existingClient.close().catch(() => {});
		this.clients.delete(serverKey);
		this.transports.delete(serverKey);
		await this.connectToServer(oauthConfig.serverConfig);
		logger.debug(`[MCP] Successfully refreshed OAuth token for server ${serverKey}`);
	}
	async callTool(name, args) {
		const requestOptions = getEffectiveRequestOptions(this.config);
		const disconnectedServers = [];
		for (const [serverKey, serverTools] of this.tools.entries()) if (serverTools.some((tool) => tool.name === name)) {
			try {
				await this.refreshOAuthTokenIfNeeded(serverKey);
			} catch (error) {
				const errorMessage = error instanceof Error ? error.message : String(error);
				logger.debug(`[MCP] Failed to refresh OAuth token for ${serverKey}, trying the next matching server: ${errorMessage}`);
			}
			const client = this.clients.get(serverKey);
			if (!client) {
				logger.debug(`[MCP] Server ${serverKey} is not connected, trying the next matching server`);
				disconnectedServers.push(serverKey);
				continue;
			}
			let currentClient = client;
			let retried = false;
			while (true) try {
				const result = await currentClient.callTool({
					name,
					arguments: args
				}, void 0, requestOptions);
				let content = "";
				if (result?.content) if (typeof result.content === "string") try {
					const parsed = JSON.parse(result.content);
					content = typeof parsed === "string" ? parsed : JSON.stringify(parsed);
				} catch {
					content = result.content;
				}
				else if (Buffer.isBuffer(result.content)) content = result.content.toString();
				else content = JSON.stringify(result.content);
				return {
					content,
					...result.isError ? { isError: true } : {},
					raw: result
				};
			} catch (error) {
				const errorMessage = error instanceof Error ? error.message : String(error);
				const isAuthError = errorMessage.includes("401") || errorMessage.includes("Unauthorized") || errorMessage.includes("authorization_endpoint") || errorMessage.includes("token");
				const oauthConfig = this.oauthConfigs.get(serverKey);
				if (!retried && isAuthError && oauthConfig) {
					logger.debug(`[MCP] Auth error for ${serverKey}, attempting reactive token refresh`);
					retried = true;
					try {
						await this.refreshOAuthToken(serverKey, oauthConfig, true);
						const newClient = this.clients.get(serverKey);
						if (newClient) {
							currentClient = newClient;
							continue;
						}
					} catch (refreshError) {
						const refreshErrorMsg = refreshError instanceof Error ? refreshError.message : String(refreshError);
						logger.error(`[MCP] Token refresh failed for ${serverKey}: ${refreshErrorMsg}`);
					}
				}
				if (this.isDebugEnabled) logger.error(`Error calling tool ${name}: ${errorMessage}`);
				return {
					content: "",
					error: errorMessage
				};
			}
		}
		if (disconnectedServers.length > 0) {
			const plural = disconnectedServers.length > 1 ? "s are" : " is";
			throw new Error(`Tool ${name} is known but MCP server${plural} disconnected: ${disconnectedServers.join(", ")}`);
		}
		throw new Error(`Tool ${name} not found in any connected MCP server`);
	}
	async cleanup() {
		for (const [serverKey, client] of this.clients.entries()) try {
			const transport = this.transports.get(serverKey);
			if (transport) await transport.close();
			await client.close();
		} catch (error) {
			if (this.isDebugEnabled) logger.error(`Error during cleanup: ${error instanceof Error ? error.message : String(error)}`);
		}
		this.clients.clear();
		this.transports.clear();
		this.tools.clear();
		this.oauthConfigs.clear();
		this.tokenExpiresAt.clear();
		this.tokenRefreshLocks.clear();
	}
};
//#endregion
export { MCPClient as t };

//# sourceMappingURL=client-CvPvnqvw.js.map