// ─── MCP Client Session ─────────────────────────────────────────────────────
// Per-server MCP client that handles the JSON-RPC 2.0 protocol over
// a pluggable transport (stdio or streamable-http).
function isRecord(value) {
    return typeof value === "object" && value !== null;
}
function hasValidServerInfo(result) {
    if (!isRecord(result) || !isRecord(result.serverInfo))
        return false;
    return typeof result.serverInfo.name === "string" && result.serverInfo.name.trim().length > 0
        && typeof result.serverInfo.version === "string" && result.serverInfo.version.trim().length > 0;
}
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;
function normalizeTimeoutMs(value, fallback) {
    if (typeof value === "number" && Number.isFinite(value) && value > 0) {
        return Math.max(1, Math.floor(value));
    }
    return fallback;
}
function withTimeout(promise, ms, message) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => reject(new Error(message)), ms);
        promise.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
    });
}
// ─── MCP Client Session ─────────────────────────────────────────────────────
export class McpClientSession {
    config;
    transport = null;
    requestIdCounter = 0;
    pendingRequests = new Map();
    initialized = false;
    notificationHandlers = new Map();
    closed = false;
    startupTimeoutMs;
    requestTimeoutMs;
    malformedMessageCount = 0;
    cleanupTimer = null;
    static MAX_MALFORMED_MESSAGES = 3;
    static CLOSE_TIMEOUT_MS = 10_000;
    serverInfo;
    constructor(config) {
        this.config = config;
        this.startupTimeoutMs = normalizeTimeoutMs(config.startupTimeoutMs, DEFAULT_STARTUP_TIMEOUT_MS);
        this.requestTimeoutMs = normalizeTimeoutMs(config.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
    }
    async createTransport() {
        const config = this.config;
        if (config.url) {
            const { StreamableHttpTransport } = await import("./transports/streamable-http.js");
            return new StreamableHttpTransport(config.url, {
                ...(config.http_headers ?? {}),
                ...(config.headers ?? {}),
            });
        }
        if (config.command) {
            const { StdioTransport } = await import("./transports/stdio.js");
            return new StdioTransport(config.command, config.args ?? [], config.env ?? {});
        }
        throw new Error(`Cannot create transport for server ${config.name}: no url or command specified`);
    }
    // ── Connection lifecycle ──────────────────────────────────────────────
    async connect() {
        if (this.initialized)
            return;
        this.transport = await this.createTransport();
        try {
            await withTimeout(this.transport.connect(), this.startupTimeoutMs, `MCP transport connect timed out after ${this.startupTimeoutMs}ms`);
        }
        catch (err) {
            await this.transport.close?.().catch(() => { });
            this.transport = null;
            throw err;
        }
        // Start listening for incoming messages
        this.transport.onMessage((raw) => this.handleIncoming(raw));
        this.transport.onNotification((method, params) => this.handleNotification(method, params));
        this.transport.onError((err) => this.handleTransportError(err));
        this.startCleanupTimer();
    }
    get transportPid() {
        return this.transport?.pid;
    }
    async close() {
        this.closed = true;
        this.initialized = false;
        this.rejectAllPending(new Error("Session closed"));
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = null;
        }
        if (this.transport) {
            await withTimeout(Promise.resolve(this.transport.close?.()), McpClientSession.CLOSE_TIMEOUT_MS, `MCP transport close timed out after ${McpClientSession.CLOSE_TIMEOUT_MS}ms`).catch(() => { });
        }
        this.transport = null;
    }
    handleTransportError(err) {
        this.rejectAllPending(err);
    }
    rejectAllPending(err) {
        for (const [, pending] of this.pendingRequests) {
            clearTimeout(pending.timeout);
            pending.abort();
            pending.reject(err);
        }
        this.pendingRequests.clear();
    }
    rejectPendingRequest(id, err) {
        const pending = this.pendingRequests.get(id);
        if (!pending)
            return;
        clearTimeout(pending.timeout);
        pending.abort();
        this.pendingRequests.delete(id);
        pending.reject(err);
    }
    startCleanupTimer() {
        if (this.cleanupTimer)
            return;
        this.cleanupTimer = setInterval(() => {
            if (this.pendingRequests.size === 0)
                return;
            if (this.closed || !this.transport) {
                this.rejectAllPending(new Error("MCP session no longer active"));
                return;
            }
            const now = Date.now();
            for (const [id, pending] of this.pendingRequests) {
                if (now - pending.createdAt > this.requestTimeoutMs) {
                    this.rejectPendingRequest(id, new Error(`MCP request stale: ${id}`));
                }
            }
        }, 30_000);
        this.cleanupTimer.unref?.();
    }
    // ── Message handling ──────────────────────────────────────────────────
    handleIncoming(raw) {
        let msg;
        try {
            msg = JSON.parse(raw);
            this.malformedMessageCount = 0;
        }
        catch {
            this.malformedMessageCount++;
            if (this.malformedMessageCount > McpClientSession.MAX_MALFORMED_MESSAGES) {
                this.rejectAllPending(new Error("MCP server sent too many malformed JSON messages"));
                this.malformedMessageCount = 0;
            }
            return;
        }
        if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
            const pending = this.pendingRequests.get(msg.id);
            clearTimeout(pending.timeout);
            pending.abort();
            this.pendingRequests.delete(msg.id);
            if (msg.error) {
                pending.reject(new Error(msg.error.message ?? `JSON-RPC error ${msg.error.code}`));
            }
            else {
                pending.resolve(msg.result);
            }
        }
    }
    handleNotification(method, params) {
        const handlers = this.notificationHandlers.get(method);
        if (handlers) {
            for (const handler of handlers) {
                try {
                    handler(params);
                }
                catch {
                    // ignore handler errors
                }
            }
        }
    }
    // ── JSON-RPC request/response ─────────────────────────────────────────
    sendRequest(method, params, timeoutMs = this.requestTimeoutMs, signal) {
        if (this.closed)
            return Promise.reject(new Error("Session is closed"));
        if (!this.transport)
            return Promise.reject(new Error("Session is not connected"));
        if (signal?.aborted)
            return Promise.reject(signal.reason ?? new Error("Request aborted"));
        const transport = this.transport;
        const id = ++this.requestIdCounter;
        return new Promise((resolve, reject) => {
            const onAbort = () => {
                this.rejectPendingRequest(id, signal.reason ?? new Error("Request aborted"));
            };
            signal?.addEventListener("abort", onAbort, { once: true });
            const controller = new AbortController();
            const timeout = setTimeout(() => {
                controller.abort();
                this.rejectPendingRequest(id, new Error(`MCP request timed out: ${method}`));
            }, timeoutMs);
            this.pendingRequests.set(id, {
                resolve,
                reject: (e) => {
                    signal?.removeEventListener("abort", onAbort);
                    reject(e);
                },
                timeout,
                abort: () => {
                    controller.abort();
                    signal?.removeEventListener("abort", onAbort);
                },
                createdAt: Date.now(),
            });
            const payload = { jsonrpc: "2.0", id, method, params };
            const sendTimeoutMs = timeoutMs;
            withTimeout(transport.send(JSON.stringify(payload) + "\n", { signal: controller.signal, timeoutMs }), sendTimeoutMs, `MCP transport send timed out after ${sendTimeoutMs}ms: ${method}`).catch((err) => {
                controller.abort();
                this.rejectPendingRequest(id, err instanceof Error ? err : new Error(String(err)));
            });
        });
    }
    sendNotification(method, params) {
        if (this.closed || !this.transport)
            return;
        const payload = { jsonrpc: "2.0", method, params };
        const timeoutMs = Math.min(this.requestTimeoutMs, 30_000);
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), timeoutMs);
        this.transport.send(JSON.stringify(payload) + "\n", {
            signal: controller.signal,
            timeoutMs,
        }).catch(() => { }).finally(() => clearTimeout(timeout));
    }
    // ── MCP protocol methods ──────────────────────────────────────────────
    async initialize(params) {
        try {
            const result = (await this.sendRequest("initialize", params, this.startupTimeoutMs));
            if (!hasValidServerInfo(result)) {
                throw new Error("MCP initialize response missing serverInfo.name/version");
            }
            this.initialized = true;
            this.serverInfo = result.serverInfo;
            // Send initialized notification
            this.sendNotification("notifications/initialized", {});
            return result;
        }
        catch (err) {
            await this.close().catch(() => { });
            throw err;
        }
    }
    async listTools() {
        return (await this.sendRequest("tools/list", {}));
    }
    async callTool(name, args) {
        return this.sendRequest("tools/call", { name, arguments: args });
    }
    async listResources() {
        return (await this.sendRequest("resources/list", {}));
    }
    async readResource(uri) {
        return this.sendRequest("resources/read", { uri });
    }
    async listPrompts() {
        return (await this.sendRequest("prompts/list", {}));
    }
    async getPrompt(name, args) {
        return this.sendRequest("prompts/get", { name, arguments: args });
    }
    // ── Notification subscriptions ────────────────────────────────────────
    onNotification(method, handler) {
        if (!this.notificationHandlers.has(method)) {
            this.notificationHandlers.set(method, new Set());
        }
        const handlers = this.notificationHandlers.get(method);
        handlers.add(handler);
        return () => handlers.delete(handler);
    }
}
