// ─── Streamable HTTP Transport ──────────────────────────────────────────────
// Connects to a remote MCP server over HTTP with SSE (Server-Sent Events)
// for server→client streaming. Uses fetch for HTTP requests.
const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024;
const FETCH_TIMEOUT_MS = 300_000;
function normalizeTimeoutMs(value, fallback) {
    return Number.isFinite(value) && value !== undefined && value > 0 ? value : fallback;
}
export class StreamableHttpTransport {
    url;
    headers;
    messageHandlers = new Set();
    notificationHandlers = new Set();
    errorHandlers = new Set();
    sessionId = null;
    protocolVersion = null;
    connected = false;
    activeControllers = new Set();
    activeReaders = new Set();
    closing = false;
    constructor(url, headers = {}) {
        this.url = url.endsWith("/") ? url : url + "/";
        this.headers = {
            "Content-Type": "application/json",
            Accept: "application/json, text/event-stream",
            ...headers,
        };
    }
    async connect() {
        this.connected = true;
    }
    async send(message, options = {}) {
        if (!this.connected || this.closing)
            throw new Error("Not connected");
        const timeoutMs = normalizeTimeoutMs(options.timeoutMs, FETCH_TIMEOUT_MS);
        const expectedResponseId = this.requestIdFromMessage(message);
        const requestController = new AbortController();
        this.activeControllers.add(requestController);
        const signal = options.signal
            ? AbortSignal.any([options.signal, requestController.signal, AbortSignal.timeout(timeoutMs)])
            : AbortSignal.any([requestController.signal, AbortSignal.timeout(timeoutMs)]);
        try {
            const response = await fetch(this.url, {
                method: "POST",
                headers: this.buildRequestHeaders(),
                body: message,
                signal,
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            const sessionId = response.headers.get("Mcp-Session-Id");
            if (sessionId)
                this.sessionId = sessionId;
            const contentType = response.headers.get("Content-Type") ?? "";
            if (contentType.toLowerCase().includes("text/event-stream")) {
                await this.readSseEventStream(response, timeoutMs, expectedResponseId);
                return;
            }
            const data = JSON.parse(await this.readResponseText(response, timeoutMs));
            if (Array.isArray(data)) {
                for (const msg of data) {
                    this.dispatchJsonRpcMessage(msg);
                }
            }
            else {
                this.dispatchJsonRpcMessage(data);
            }
        }
        finally {
            requestController.abort();
            this.activeControllers.delete(requestController);
        }
    }
    async readResponseText(response, timeoutMs) {
        const contentLength = response.headers.get("Content-Length");
        if (contentLength !== null) {
            const parsedLength = Number(contentLength);
            if (Number.isFinite(parsedLength) && parsedLength > MAX_RESPONSE_BODY_BYTES) {
                throw new Error(`MCP HTTP response body exceeded ${MAX_RESPONSE_BODY_BYTES} bytes`);
            }
        }
        if (!response.body) {
            const text = await response.text();
            if (Buffer.byteLength(text, "utf8") > MAX_RESPONSE_BODY_BYTES) {
                throw new Error(`MCP HTTP response body exceeded ${MAX_RESPONSE_BODY_BYTES} bytes`);
            }
            return text;
        }
        const reader = response.body.getReader();
        this.activeReaders.add(reader);
        const chunks = [];
        let totalBytes = 0;
        try {
            while (true) {
                const { done, value } = await this.readChunkWithTimeout(reader, timeoutMs);
                if (done)
                    break;
                if (!value)
                    continue;
                totalBytes += value.byteLength;
                if (totalBytes > MAX_RESPONSE_BODY_BYTES) {
                    await reader.cancel();
                    throw new Error(`MCP HTTP response body exceeded ${MAX_RESPONSE_BODY_BYTES} bytes`);
                }
                chunks.push(value);
            }
        }
        finally {
            this.activeReaders.delete(reader);
            reader.releaseLock();
        }
        const body = new Uint8Array(totalBytes);
        let offset = 0;
        for (const chunk of chunks) {
            body.set(chunk, offset);
            offset += chunk.byteLength;
        }
        return new TextDecoder().decode(body);
    }
    async readSseEventStream(response, timeoutMs, expectedResponseId) {
        if (!response.body) {
            this.parseSseEventStream(await response.text(), expectedResponseId);
            return;
        }
        const reader = response.body.getReader();
        this.activeReaders.add(reader);
        const decoder = new TextDecoder();
        let buffer = "";
        let totalBytes = 0;
        try {
            while (true) {
                const { done, value } = await this.readChunkWithTimeout(reader, timeoutMs);
                if (done)
                    break;
                if (!value)
                    continue;
                totalBytes += value.byteLength;
                if (totalBytes > MAX_RESPONSE_BODY_BYTES) {
                    await reader.cancel();
                    throw new Error(`MCP HTTP response body exceeded ${MAX_RESPONSE_BODY_BYTES} bytes`);
                }
                buffer += decoder.decode(value, { stream: true });
                const dispatched = this.dispatchCompleteSseFrames(buffer, expectedResponseId);
                buffer = dispatched.remainder;
                if (dispatched.matchedExpectedResponse) {
                    await reader.cancel();
                    return;
                }
            }
            buffer += decoder.decode();
            this.parseSseEventStream(buffer, expectedResponseId);
        }
        finally {
            this.activeReaders.delete(reader);
            reader.releaseLock();
        }
    }
    async readChunkWithTimeout(reader, timeoutMs) {
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reader.cancel().catch(() => { });
                reject(new Error(`MCP HTTP chunk read timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            reader.read().then((result) => {
                clearTimeout(timer);
                resolve(result);
            }, (err) => {
                clearTimeout(timer);
                reject(err);
            });
        });
    }
    buildRequestHeaders() {
        const headers = { ...this.headers };
        if (this.sessionId)
            headers["Mcp-Session-Id"] = this.sessionId;
        if (this.protocolVersion)
            headers["MCP-Protocol-Version"] = this.protocolVersion;
        return headers;
    }
    requestIdFromMessage(message) {
        try {
            const parsed = JSON.parse(message);
            if (!parsed || typeof parsed !== "object")
                return undefined;
            const id = parsed.id;
            return typeof id === "string" || typeof id === "number" ? id : undefined;
        }
        catch {
            return undefined;
        }
    }
    dispatchJsonRpcMessage(data, expectedResponseId) {
        if (!data || typeof data !== "object")
            return false;
        const msg = data;
        if (msg.jsonrpc !== "2.0")
            return false;
        if (typeof msg.result?.protocolVersion === "string") {
            this.protocolVersion = msg.result.protocolVersion;
        }
        if (msg.id !== undefined) {
            for (const h of this.messageHandlers)
                h(JSON.stringify(data));
            return expectedResponseId !== undefined && msg.id === expectedResponseId;
        }
        if (typeof msg.method === "string") {
            for (const h of this.notificationHandlers)
                h(msg.method, msg.params);
        }
        return false;
    }
    parseSseEventStream(stream, expectedResponseId) {
        const { remainder, matchedExpectedResponse } = this.dispatchCompleteSseFrames(stream, expectedResponseId);
        if (remainder.trim()) {
            return this.dispatchSseFrame(remainder, expectedResponseId) || matchedExpectedResponse;
        }
        return matchedExpectedResponse;
    }
    dispatchCompleteSseFrames(stream, expectedResponseId) {
        let remaining = stream;
        let matchedExpectedResponse = false;
        while (true) {
            const match = /\r?\n\r?\n/.exec(remaining);
            if (!match)
                return { remainder: remaining, matchedExpectedResponse };
            const frame = remaining.slice(0, match.index);
            if (this.dispatchSseFrame(frame, expectedResponseId)) {
                matchedExpectedResponse = true;
            }
            remaining = remaining.slice(match.index + match[0].length);
        }
    }
    dispatchSseFrame(frame, expectedResponseId) {
        const dataLines = [];
        for (const line of frame.split(/\r?\n/)) {
            if (line === "" || line.startsWith(":"))
                continue;
            if (line.startsWith("data:")) {
                const value = line.slice(5);
                dataLines.push(value.startsWith(" ") ? value.slice(1) : value);
            }
        }
        if (dataLines.length === 0)
            return false;
        return this.dispatchSseData(dataLines.join("\n"), expectedResponseId);
    }
    dispatchSseData(data, expectedResponseId) {
        try {
            const parsed = JSON.parse(data);
            if (Array.isArray(parsed)) {
                let matchedExpectedResponse = false;
                for (const msg of parsed) {
                    if (this.dispatchJsonRpcMessage(msg, expectedResponseId)) {
                        matchedExpectedResponse = true;
                    }
                }
                return matchedExpectedResponse;
            }
            return this.dispatchJsonRpcMessage(parsed, expectedResponseId);
        }
        catch {
            // Ignore malformed SSE data frames.
            return false;
        }
    }
    onMessage(handler) {
        this.messageHandlers.add(handler);
    }
    onNotification(handler) {
        this.notificationHandlers.add(handler);
    }
    onError(handler) {
        this.errorHandlers.add(handler);
    }
    async close() {
        this.closing = true;
        this.connected = false;
        for (const controller of this.activeControllers) {
            controller.abort();
        }
        this.activeControllers.clear();
        await Promise.all([...this.activeReaders].map((reader) => reader.cancel().catch(() => { })));
        this.activeReaders.clear();
        this.sessionId = null;
        this.protocolVersion = null;
    }
}
