/**
 * MCP (Model Context Protocol) client for Playwright server integration.
 *
 * Handles communication with the Playwright MCP server to enable
 * browser automation capabilities for the judge system.
 */
/**
 * MCP client for communicating with Playwright MCP server.
 *
 * Implements the Model Context Protocol to interact with
 * a Playwright automation server for browser testing.
 */
export class MCPClient {
    serverUrl;
    logger;
    nextId = 1;
    connected = false;
    tools = new Map();
    constructor(serverUrl, logger) {
        this.serverUrl = serverUrl;
        this.logger = logger;
    }
    /**
     * Connects to the MCP server and discovers available tools.
     *
     * @throws Error if connection fails
     */
    async connect() {
        if (this.connected) {
            return;
        }
        try {
            // Discover available tools (skip initialize for simple JSON-RPC servers)
            const tools = await this.listTools();
            for (const tool of tools) {
                this.tools.set(tool.name, tool);
            }
            this.connected = true;
            this.logger.info(`Connected to MCP server at ${this.serverUrl} with ${this.tools.size} tools`);
        }
        catch (error) {
            this.logger.error("Failed to connect to MCP server", {
                error: {
                    kind: "MCPConnectionError",
                    message: String(error),
                    stack: error instanceof Error ? error.stack : undefined,
                },
            });
            throw new Error(`MCP connection failed: ${error}`);
        }
    }
    /**
     * Lists available tools from the MCP server.
     *
     * @returns Array of tool definitions
     */
    async listTools() {
        const response = await this.sendRequest("tools/list", {});
        return response.tools || [];
    }
    /**
     * Calls a tool on the MCP server.
     *
     * @param toolName - Name of the tool to call
     * @param args - Arguments for the tool
     * @returns Tool execution result
     */
    async callTool(toolName, args) {
        if (!this.connected) {
            await this.connect();
        }
        const tool = this.tools.get(toolName);
        if (!tool) {
            throw new Error(`Unknown tool: ${toolName}`);
        }
        try {
            const response = await this.sendRequest("tools/call", {
                name: toolName,
                arguments: args,
            });
            if (response.error) {
                throw new Error(`Tool execution failed: ${response.error.message}`);
            }
            return response.content;
        }
        catch (error) {
            // If the error looks like an import statement error, provide a helpful message
            const errorMsg = String(error);
            if (errorMsg.includes("import") && errorMsg.includes("module")) {
                throw new Error("Browser evaluation failed: Cannot use import/export/require statements in browser context. " +
                    "Please use plain JavaScript that can run in a browser console.");
            }
            throw error;
        }
    }
    /**
     * Executes a Playwright action via MCP.
     *
     * @param params - Playwright action parameters
     * @returns Action execution result
     */
    async executePlaywrightAction(params) {
        this.logger.info(`Executing Playwright action ${params.action}, params: ${params.params ? Object.keys(params.params).join(", ") : "none"}`);
        try {
            const toolArgs = {
                action: params.action,
                ...params.params,
            };
            this.logger.info(`Calling playwright_action tool for action: ${params.action}`);
            const result = await this.callTool("playwright_action", toolArgs);
            this.logger.info(`Tool call succeeded for ${params.action}, hasResult: ${!!result}`);
            return {
                success: true,
                data: result,
            };
        }
        catch (error) {
            this.logger.error("Playwright action failed", {
                error: {
                    kind: "PlaywrightActionError",
                    message: `Action ${params.action} failed: ${String(error)}`,
                    stack: error instanceof Error ? error.stack : undefined,
                },
            });
            return {
                success: false,
                error: String(error),
            };
        }
    }
    /**
     * Parses SSE (Server-Sent Events) response text.
     *
     * @param text - Raw SSE response text
     * @param requestId - Expected request ID
     * @returns Parsed MCP response
     */
    parseSSEResponse(text, requestId) {
        // SSE format: lines starting with "data: " contain JSON
        const lines = text.split("\n");
        for (const line of lines) {
            if (line.startsWith("data: ")) {
                const jsonStr = line.substring(6); // Remove "data: " prefix
                try {
                    const data = JSON.parse(jsonStr);
                    // Verify this response matches our request
                    if (data.id === requestId || data.id === null) {
                        return data;
                    }
                }
                catch {
                    // Continue to next line if parsing fails
                }
            }
        }
        // If we didn't find a matching response, throw error
        throw new Error(`No valid JSON-RPC response found in SSE stream for request ${requestId}`);
    }
    /**
     * Sends a request to the MCP server.
     *
     * @param method - RPC method name
     * @param params - Method parameters
     * @returns Server response
     */
    async sendRequest(method, params) {
        const request = {
            jsonrpc: "2.0",
            method,
            params,
            id: this.nextId++,
        };
        try {
            const response = await fetch(this.serverUrl, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Accept: "application/json, text/event-stream",
                },
                body: JSON.stringify(request),
                // Add timeout to prevent hanging if server crashes
                signal: AbortSignal.timeout(30000), // 30 second timeout
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            const contentType = response.headers.get("content-type") || "";
            // Handle SSE response
            if (contentType.includes("text/event-stream")) {
                const text = await response.text();
                const data = this.parseSSEResponse(text, request.id);
                if (data.error) {
                    throw new Error(data.error.message);
                }
                return data.result;
            }
            // Handle regular JSON response (fallback)
            const data = await response.json();
            if (data.error) {
                throw new Error(data.error.message);
            }
            return data.result;
        }
        catch (error) {
            const errorMsg = String(error);
            // Check for connection issues that might indicate server crash
            if (errorMsg.includes("ECONNREFUSED") ||
                errorMsg.includes("ECONNRESET")) {
                this.logger.error("MCP server connection failed - server may have crashed", {
                    error: {
                        kind: "MCPServerCrash",
                        message: `MCP server appears to be down: ${errorMsg}`,
                        stack: error instanceof Error ? error.stack : undefined,
                    },
                });
                this.connected = false;
                throw new Error("MCP server connection lost. The server may have crashed due to invalid JavaScript code.");
            }
            // Check for timeout
            if (errorMsg.includes("AbortError") || errorMsg.includes("timeout")) {
                this.logger.error("MCP request timed out", {
                    error: {
                        kind: "MCPTimeout",
                        message: `Method ${method} timed out after 30 seconds`,
                    },
                });
                throw new Error("MCP server request timed out. The server may be unresponsive.");
            }
            this.logger.error("MCP request failed", {
                error: {
                    kind: "MCPRequestError",
                    message: `Method ${method} failed: ${errorMsg}`,
                    stack: error instanceof Error ? error.stack : undefined,
                },
            });
            throw error;
        }
    }
    /**
     * Disconnects from the MCP server.
     */
    async disconnect() {
        if (!this.connected) {
            return;
        }
        try {
            // Send disconnect notification if supported
            await this.sendRequest("disconnect", {});
        }
        catch {
            // Ignore disconnect errors
        }
        finally {
            this.connected = false;
            this.tools.clear();
            this.logger.info("Disconnected from MCP server");
        }
    }
    /**
     * Checks if the client is connected.
     *
     * @returns True if connected
     */
    isConnected() {
        return this.connected;
    }
    /**
     * Gets available tool names.
     *
     * @returns Array of tool names
     */
    getAvailableTools() {
        return Array.from(this.tools.keys());
    }
    /**
     * Gets full tool definitions including schemas.
     *
     * Returns tool definitions with name, description, and inputSchema
     * for use with the MCP adapter.
     *
     * @returns Array of tool definitions with metadata
     */
    getToolDefinitions() {
        return Array.from(this.tools.values()).map((tool) => ({
            name: tool.name,
            description: tool.description,
            inputSchema: tool.inputSchema,
        }));
    }
}
//# sourceMappingURL=mcp-client.js.map