/**
 * McpClient - Model Context Protocol Client for Ag-Bash
 */
import { sanitizeErrorMessage } from "../fs/sanitize-error.js";
/** Error thrown when an SSRF-blocked URL is detected. */
export class McpTransportSecurityError extends Error {
    constructor(message) {
        super(message);
        this.name = "McpTransportSecurityError";
    }
}
/**
 * Checks whether a URL targets a private or internal IP address.
 * Used to prevent SSRF attacks via the HttpTransport.
 */
function isPrivateUrl(url) {
    const parsed = new URL(url);
    const hostname = parsed.hostname.toLowerCase();
    // Block localhost variants
    if (hostname === "localhost" || hostname === "[::1]") {
        return true;
    }
    // Block IPv6 loopback and private ranges
    if (hostname === "::1") {
        return true;
    }
    // Strip brackets for IPv6
    const bare = hostname.startsWith("[") && hostname.endsWith("]")
        ? hostname.slice(1, -1)
        : hostname;
    // IPv6 private: fc00::/7 covers fc and fd prefixes
    if (bare.startsWith("fc") || bare.startsWith("fd")) {
        return true;
    }
    // IPv6 loopback
    if (bare === "::1") {
        return true;
    }
    // Parse IPv4 octets
    const ipv4Parts = bare.split(".");
    if (ipv4Parts.length === 4) {
        const octets = ipv4Parts.map(Number);
        if (octets.some((o) => Number.isNaN(o) || o < 0 || o > 255)) {
            return false;
        }
        const [a, b] = octets;
        // 0.0.0.0
        if (a === 0 && b === 0 && octets[2] === 0 && octets[3] === 0) {
            return true;
        }
        // 127.0.0.0/8
        if (a === 127) {
            return true;
        }
        // 10.0.0.0/8
        if (a === 10) {
            return true;
        }
        // 172.16.0.0/12
        if (a === 172 && b >= 16 && b <= 31) {
            return true;
        }
        // 192.168.0.0/16
        if (a === 192 && b === 168) {
            return true;
        }
        // 169.254.0.0/16 (link-local)
        if (a === 169 && b === 254) {
            return true;
        }
    }
    return false;
}
class HttpTransport {
    url;
    securityConfig;
    constructor(url, securityConfig) {
        this.url = url;
        this.securityConfig = securityConfig ?? Object.create(null);
    }
    async init() { }
    async send(message) {
        if (!this.securityConfig.allowPrivateNetworks && isPrivateUrl(this.url)) {
            throw new McpTransportSecurityError(`SSRF blocked: requests to private/internal network addresses are not allowed (${new URL(this.url).hostname})`);
        }
        const response = await fetch(this.url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(message),
        });
        return await response.json();
    }
    close() { }
}
class StdioTransport {
    command;
    args;
    process = null;
    pendingRequests = new Map();
    pendingTimeouts = new Map();
    nextId = 1;
    requestTimeoutMs;
    constructor(command, args, options) {
        this.command = command;
        this.args = args;
        this.requestTimeoutMs = options?.requestTimeoutMs ?? 30_000;
    }
    async init() {
        const { spawn } = await import("node:child_process");
        this.process = spawn(this.command, this.args, {
            stdio: ["pipe", "pipe", "inherit"],
        });
        let buffer = "";
        this.process.stdout?.on("data", (data) => {
            buffer += data.toString();
            let newlineIndex;
            while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
                const line = buffer.slice(0, newlineIndex);
                buffer = buffer.slice(newlineIndex + 1);
                try {
                    const response = JSON.parse(line);
                    if (response.id !== undefined) {
                        const pending = this.pendingRequests.get(response.id);
                        if (pending) {
                            const timeout = this.pendingTimeouts.get(response.id);
                            if (timeout !== undefined) {
                                clearTimeout(timeout);
                                this.pendingTimeouts.delete(response.id);
                            }
                            pending.resolve(response);
                            this.pendingRequests.delete(response.id);
                        }
                    }
                }
                catch (_e) {
                    // Silently swallow malformed JSON-RPC response
                }
            }
        });
    }
    async send(message) {
        if (!this.process)
            throw new Error("Transport not initialized. Call init() first.");
        const id = this.nextId++;
        const envelope = Object.assign(Object.create(null), message, {
            id,
            jsonrpc: "2.0",
        });
        return new Promise((resolve, reject) => {
            this.pendingRequests.set(id, { resolve, reject });
            const timeout = setTimeout(() => {
                this.pendingTimeouts.delete(id);
                const pending = this.pendingRequests.get(id);
                if (pending) {
                    this.pendingRequests.delete(id);
                    pending.reject(new Error(`MCP request timed out after ${this.requestTimeoutMs}ms (id: ${id})`));
                }
            }, this.requestTimeoutMs);
            this.pendingTimeouts.set(id, timeout);
            this.process?.stdin?.write(`${JSON.stringify(envelope)}\n`);
        });
    }
    close() {
        for (const timeout of this.pendingTimeouts.values()) {
            clearTimeout(timeout);
        }
        this.pendingTimeouts.clear();
        for (const pending of this.pendingRequests.values()) {
            pending.reject(new Error("Transport closed while request was pending"));
        }
        this.pendingRequests.clear();
        if (this.process) {
            this.process.kill();
        }
    }
}
export class McpClient {
    connections = new Map();
    async connectStdio(id, command, args, cmdCtx, options) {
        const bash = cmdCtx.bash;
        if (bash && this.connections.size >= bash.limits.maxMcpServers) {
            throw new Error(`Maximum MCP servers reached (${bash.limits.maxMcpServers})`);
        }
        const transport = new StdioTransport(command, args, options);
        await transport.init();
        const connection = {
            id,
            name: id,
            type: "stdio",
            status: "connected",
            tools: [],
            transport,
        };
        this.connections.set(id, connection);
        await this.discoverTools(id);
        return connection;
    }
    async connectHttp(id, url, bash, securityConfig) {
        if (bash && this.connections.size >= bash.limits.maxMcpServers) {
            throw new Error(`Maximum MCP servers reached (${bash.limits.maxMcpServers})`);
        }
        const transport = new HttpTransport(url, securityConfig);
        await transport.init();
        const connection = {
            id,
            name: id,
            type: "http",
            status: "connected",
            tools: [],
            transport,
        };
        this.connections.set(id, connection);
        await this.discoverTools(id);
        return connection;
    }
    async discoverTools(id) {
        const conn = this.connections.get(id);
        if (!conn)
            return;
        const response = (await conn.transport.send({
            method: "list_tools",
            params: Object.create(null),
        }));
        if (response.result?.tools) {
            conn.tools = response.result.tools;
        }
    }
    async callTool(connectionId, toolName, args, bash) {
        const conn = this.connections.get(connectionId);
        if (!conn)
            throw new Error(`Connection ${connectionId} not found`);
        if (bash) {
            // Bash.state is private; callers pass Bash via CommandContext.bash
            // (typed as any). Use a narrow structural cast to access the counter.
            const withState = bash;
            if (withState.state &&
                withState.state.mcpToolCallCount >= bash.limits.maxMcpToolCalls) {
                throw new Error(`Maximum MCP tool calls reached (${bash.limits.maxMcpToolCalls})`);
            }
            if (withState.state) {
                withState.state.mcpToolCallCount++;
            }
        }
        const response = (await conn.transport.send({
            method: "call_tool",
            params: { name: toolName, arguments: args },
        }));
        if (response.error) {
            throw new Error(sanitizeErrorMessage(response.error.message || "Unknown MCP error"));
        }
        return response.result;
    }
    listConnections() {
        return Array.from(this.connections.values());
    }
    disconnect(id) {
        const conn = this.connections.get(id);
        if (conn) {
            conn.transport.close();
            this.connections.delete(id);
        }
    }
    /** Release all resources and disconnect all servers. */
    async dispose() {
        for (const conn of this.connections.values()) {
            conn.transport.close();
        }
        this.connections.clear();
    }
}
