/**
 * MCPToolProvider — Custom Promptfoo provider for MCP tool-use evaluation.
 *
 * Orchestrates the MCP evaluation flow:
 * 1. Connects to the MCP server and discovers available tools
 * 2. Selects the appropriate LLM backend based on model ID prefix
 * 3. Delegates the multi-turn tool loop to the backend
 * 4. Formats the result for Promptfoo (including tool call summary)
 *
 * Promptfoo config usage:
 *
 *   providers:
 *     - id: file://dist/pipeline/compiler/mode-handlers/mcp-tool-provider/index.js
 *       label: "Claude Opus 4.6 + MCP"
 *       config:
 *         model: anthropic:messages:claude-opus-4-6
 *         maxToolRounds: 5
 *         temperature: 0.2
 *         max_tokens: 4096
 *         mcpServer:
 *           url: https://mcp.sanity.io
 *           auth: { type: bearer, token: "{{env.SANITY_API_TOKEN}}" }
 *           name: mcp-live-query-documents
 *         mcpTools: [query_documents, get_schema]
 */
import { config as loadDotenv } from "dotenv";
import { connectMCP } from "./mcp-connection.js";
import { runAnthropicToolLoop } from "./tool-loop-anthropic.js";
import { runOpenAIToolLoop } from "./tool-loop-openai.js";
loadDotenv({
    override: true,
    path: new URL("../../../../../.env", import.meta.url).pathname,
});
// ---------------------------------------------------------------------------
// Backend registry — maps model ID prefixes to tool loop implementations
// ---------------------------------------------------------------------------
const BACKENDS = {
    anthropic: runAnthropicToolLoop,
    openai: runOpenAIToolLoop,
};
/**
 * Model used when a provider config omits `model`. Shared by `id()` and
 * `callApi` so the reported provider id always matches the model actually
 * evaluated.
 */
const DEFAULT_MCP_MODEL = "anthropic:messages:claude-opus-4-6";
/**
 * Resolve the LLM backend from a model ID.
 *
 * Model IDs follow the pattern `provider:type:model-name` (e.g.,
 * `anthropic:messages:claude-opus-4-6`). The first segment determines
 * which backend handles the tool loop. For OpenAI, the second segment
 * determines the API variant (`chat` → Chat Completions, `responses` →
 * Responses API).
 */
function resolveBackend(modelId) {
    const parts = modelId.split(":");
    const prefix = parts[0];
    const backend = BACKENDS[prefix];
    if (!backend) {
        const supported = Object.keys(BACKENDS).join(", ");
        throw new Error(`No backend for model "${modelId}". Supported prefixes: ${supported}`);
    }
    // Extract the model name for the API (e.g., "claude-opus-4-6" from "anthropic:messages:claude-opus-4-6")
    const modelName = parts.length > 2 ? parts.slice(2).join(":") : parts[parts.length - 1];
    // For OpenAI, extract the API variant from the second segment
    let apiVariant;
    if (prefix === "openai" && parts.length > 2) {
        const variant = parts[1];
        if (variant === "responses" || variant === "chat") {
            apiVariant = variant;
        }
    }
    return { backend, modelName, apiVariant };
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Append a machine-readable tool call summary for assertion detection */
function appendToolSummary(text, log) {
    if (log.length === 0)
        return text;
    const names = JSON.stringify(log.map((tc) => tc.name));
    return `${text}\n\n<!-- MCP_TOOLS_CALLED: ${names} -->`;
}
/** Resolve the API key for a given model prefix */
function resolveApiKey(prefix, config) {
    if (config.apiKey)
        return String(config.apiKey);
    const envMap = {
        anthropic: "ANTHROPIC_API_KEY",
        openai: "OPENAI_API_KEY",
    };
    const envVar = envMap[prefix];
    return envVar ? process.env[envVar] : undefined;
}
// ---------------------------------------------------------------------------
// Provider class
// ---------------------------------------------------------------------------
export default class MCPToolProvider {
    config;
    constructor(options = {}) {
        this.config = options.config || {};
    }
    /**
     * Reported provider id. Promptfoo serializes this into `result.provider.id`,
     * which the report pipeline (`emit-eval-results.ts`) uses verbatim as the
     * trace `modelId` and the per-entry artifact key. Returning the loader path
     * (`file://…index.js`, the constructor's `options.id`) made every model in a
     * run share one id, collapsing multi-model trace keys onto a single entry
     * (W-mcp-trace-model-identity). Return the evaluated model id instead —
     * mirroring the literacy `agentic-provider`, whose `id()` is likewise a
     * model-derived identity rather than the `file://` module path.
     */
    id() {
        return this.config.model ?? DEFAULT_MCP_MODEL;
    }
    async callApi(prompt, _context) {
        const mcpServerConfig = this.config.mcpServer;
        if (!mcpServerConfig) {
            return { error: "mcpServer config is required", output: undefined };
        }
        // Resolve model and backend
        const modelId = this.config.model || DEFAULT_MCP_MODEL;
        let backend;
        let modelName;
        let apiVariant;
        try {
            const resolved = resolveBackend(modelId);
            backend = resolved.backend;
            modelName = resolved.modelName;
            apiVariant = resolved.apiVariant;
        }
        catch (err) {
            return {
                error: err instanceof Error ? err.message : String(err),
                output: undefined,
            };
        }
        // Resolve API key
        const prefix = modelId.split(":")[0];
        const apiKey = resolveApiKey(prefix, this.config);
        if (!apiKey) {
            return {
                error: `API key not found for ${prefix}. Set ${prefix.toUpperCase()}_API_KEY in env or config.apiKey.`,
                output: undefined,
            };
        }
        // Connect to MCP server
        let mcpClient;
        try {
            mcpClient = await connectMCP(mcpServerConfig);
        }
        catch (err) {
            return {
                error: `Failed to connect to MCP server: ${err instanceof Error ? err.message : String(err)}`,
                output: undefined,
            };
        }
        try {
            // Filter tools by capabilities
            const allTools = mcpClient.getAllTools();
            const toolFilter = this.config.mcpTools;
            const tools = toolFilter
                ? allTools.filter((t) => toolFilter.includes(t.name))
                : allTools;
            // The server's real, discovered capabilities — every tool advertised by
            // `tools/list`, before the `mcpTools` allowlist narrows what the model
            // may call. `capability-available` assertions resolve against this.
            const serverCapabilities = allTools.map((t) => t.name);
            if (tools.length === 0) {
                return {
                    error: "No MCP tools available after filtering. Check mcpTools config and server capabilities.",
                    output: undefined,
                };
            }
            // Run the tool loop
            const result = await backend({
                prompt,
                tools,
                callTool: mcpClient.callTool,
                maxToolRounds: this.config.maxToolRounds || 5,
                model: modelName,
                temperature: this.config.temperature ?? 0.2,
                maxTokens: this.config.max_output_tokens ||
                    this.config.max_completion_tokens ||
                    this.config.max_tokens ||
                    4096,
                maxToolOutputChars: this.config.maxToolOutputChars,
                apiKey,
                apiVariant,
                providerConfig: this.config,
            });
            return {
                cost: 0,
                metadata: {
                    toolRounds: result.toolRounds,
                    // `toolCalls` is the cross-mode metadata key the telemetry collector
                    // and assertion bridge both read (agent modes use it too). The MCP
                    // provider previously emitted `toolCallLog`, which nothing consumed.
                    toolCalls: result.toolCallLog,
                    serverCapabilities,
                    exhaustedRounds: result.exhaustedRounds,
                    latencyMs: result.latencyMs,
                    // D0088 — per-turn LLM latency for the trace timeline. Undefined on
                    // backends that don't record it yet (e.g. the OpenAI loop).
                    ...(result.llmTimings ? { llmTimings: result.llmTimings } : {}),
                },
                output: appendToolSummary(result.output, result.toolCallLog),
                tokenUsage: result.tokenUsage,
            };
        }
        finally {
            await mcpClient.cleanup().catch(() => { });
        }
    }
}
