"use strict";
/**
 * Web Standards Streamable HTTP Server Transport
 *
 * This is the core transport implementation using Web Standard APIs (Request, Response, ReadableStream).
 * It can run on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
 *
 * For Node.js Express/HTTP compatibility, use `StreamableHTTPServerTransport` which wraps this transport.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebStandardStreamableHTTPServerTransport = void 0;
const mediaType_js_1 = require("../shared/mediaType.js");
const sseKeepAlive_js_1 = require("./sseKeepAlive.js");
const types_js_1 = require("../types.js");
/**
 * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
 * using Web Standard APIs (Request, Response, ReadableStream).
 *
 * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
 *
 * Usage example:
 *
 * ```typescript
 * // Stateful mode - server sets the session ID
 * const statefulTransport = new WebStandardStreamableHTTPServerTransport({
 *   sessionIdGenerator: () => crypto.randomUUID(),
 * });
 *
 * // Stateless mode - explicitly set session ID to undefined
 * const statelessTransport = new WebStandardStreamableHTTPServerTransport({
 *   sessionIdGenerator: undefined,
 * });
 *
 * // Hono.js usage
 * app.all('/mcp', async (c) => {
 *   return transport.handleRequest(c.req.raw);
 * });
 *
 * // Cloudflare Workers usage
 * export default {
 *   async fetch(request: Request): Promise<Response> {
 *     return transport.handleRequest(request);
 *   }
 * };
 * ```
 *
 * In stateful mode:
 * - Session ID is generated and included in response headers
 * - Session ID is always included in initialization responses
 * - Requests with invalid session IDs are rejected with 404 Not Found
 * - Non-initialization requests without a session ID are rejected with 400 Bad Request
 * - State is maintained in-memory (connections, message history)
 *
 * In stateless mode:
 * - No Session ID is included in any responses
 * - No session validation is performed
 */
class WebStandardStreamableHTTPServerTransport {
    constructor(options = {}) {
        this._started = false;
        this._hasHandledRequest = false;
        this._streamMapping = new Map();
        this._requestToStreamMapping = new Map();
        /**
         * Request streams whose client holds a Last-Event-ID cursor (at least one
         * id-bearing event was delivered on them), so a stored response can be
         * replayed on resume.
         */
        this._resumableStreams = new Set();
        this._requestResponseMap = new Map();
        this._initialized = false;
        this._enableJsonResponse = false;
        this._standaloneSseStreamId = '_GET_stream';
        this._closed = false;
        this.sessionIdGenerator = options.sessionIdGenerator;
        this._enableJsonResponse = options.enableJsonResponse ?? false;
        this._eventStore = options.eventStore;
        this._onsessioninitialized = options.onsessioninitialized;
        this._onsessionclosed = options.onsessionclosed;
        this._allowedHosts = options.allowedHosts;
        this._allowedOrigins = options.allowedOrigins;
        this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
        this._retryInterval = options.retryInterval;
        this._keepAliveMs = options.keepAliveMs ?? sseKeepAlive_js_1.DEFAULT_SSE_KEEP_ALIVE_MS;
    }
    /**
     * Arms a keep-alive interval for an SSE stream that periodically writes an SSE
     * comment frame so intermediaries and idle timeouts don't kill the connection.
     * The returned timer is owned by the stream it was armed for: the stream's
     * cancel/cleanup callbacks must clear it. The interval clears itself if a
     * write fails (stream already closed/cancelled).
     */
    startKeepAlive(controller, encoder) {
        // After close() nothing may arm: the stream cleanups have already run.
        if (this._closed)
            return undefined;
        const timer = (0, sseKeepAlive_js_1.armSseKeepAlive)(this._keepAliveMs, () => {
            try {
                controller.enqueue(encoder.encode(': keepalive\n\n'));
            }
            catch {
                if (timer !== undefined)
                    clearInterval(timer);
            }
        });
        return timer;
    }
    /**
     * Starts the transport. This is required by the Transport interface but is a no-op
     * for the Streamable HTTP transport as connections are managed per-request.
     */
    async start() {
        if (this._started) {
            throw new Error('Transport already started');
        }
        this._started = true;
    }
    /**
     * Helper to create a JSON error response
     */
    createJsonErrorResponse(status, code, message, options) {
        const error = { code, message };
        if (options?.data !== undefined) {
            error.data = options.data;
        }
        return new Response(JSON.stringify({
            jsonrpc: '2.0',
            error,
            id: null
        }), {
            status,
            headers: {
                'Content-Type': 'application/json',
                ...options?.headers
            }
        });
    }
    /**
     * Validates request headers for DNS rebinding protection.
     * @returns Error response if validation fails, undefined if validation passes.
     */
    validateRequestHeaders(req) {
        // Skip validation if protection is not enabled
        if (!this._enableDnsRebindingProtection) {
            return undefined;
        }
        // Validate Host header if allowedHosts is configured
        if (this._allowedHosts && this._allowedHosts.length > 0) {
            const hostHeader = req.headers.get('host');
            if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
                const error = `Invalid Host header: ${hostHeader}`;
                this.onerror?.(new Error(error));
                return this.createJsonErrorResponse(403, -32000, error);
            }
        }
        // Validate Origin header if allowedOrigins is configured
        if (this._allowedOrigins && this._allowedOrigins.length > 0) {
            const originHeader = req.headers.get('origin');
            if (originHeader && !this._allowedOrigins.includes(originHeader)) {
                const error = `Invalid Origin header: ${originHeader}`;
                this.onerror?.(new Error(error));
                return this.createJsonErrorResponse(403, -32000, error);
            }
        }
        return undefined;
    }
    /**
     * Handles an incoming HTTP request, whether GET, POST, or DELETE
     * Returns a Response object (Web Standard)
     */
    async handleRequest(req, options) {
        if (this._closed) {
            return this.createJsonErrorResponse(404, -32001, 'Session not found');
        }
        // In stateless mode (no sessionIdGenerator), each request must use a fresh transport.
        // Reusing a stateless transport causes message ID collisions between clients.
        if (!this.sessionIdGenerator && this._hasHandledRequest) {
            throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');
        }
        this._hasHandledRequest = true;
        // Validate request headers for DNS rebinding protection
        const validationError = this.validateRequestHeaders(req);
        if (validationError) {
            return validationError;
        }
        switch (req.method) {
            case 'POST':
                return this.handlePostRequest(req, options);
            case 'GET':
                return this.handleGetRequest(req);
            case 'DELETE':
                return this.handleDeleteRequest(req);
            default:
                return this.handleUnsupportedRequest();
        }
    }
    /**
     * Writes a priming event to establish resumption capability.
     * Only sends if eventStore is configured (opt-in for resumability) and
     * the client's protocol version supports empty SSE data (>= 2025-11-25).
     */
    async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
        if (!this._eventStore) {
            return;
        }
        // Priming events have empty data which older clients cannot handle.
        // Only send priming events to clients with protocol version >= 2025-11-25
        // which includes the fix for handling empty SSE data.
        if (protocolVersion < '2025-11-25') {
            return;
        }
        const primingEventId = await this._eventStore.storeEvent(streamId, {});
        let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
        if (this._retryInterval !== undefined) {
            primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
        }
        controller.enqueue(encoder.encode(primingEvent));
        this._resumableStreams.add(streamId);
    }
    /**
     * Handles GET requests for SSE stream
     */
    async handleGetRequest(req) {
        // The client MUST include an Accept header, listing text/event-stream as a supported content type.
        const acceptHeader = req.headers.get('accept');
        if (!acceptHeader?.includes('text/event-stream')) {
            this.onerror?.(new Error('Not Acceptable: Client must accept text/event-stream'));
            return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream');
        }
        // If an Mcp-Session-Id is returned by the server during initialization,
        // clients using the Streamable HTTP transport MUST include it
        // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
        const sessionError = this.validateSession(req);
        if (sessionError) {
            return sessionError;
        }
        const protocolError = this.validateProtocolVersion(req);
        if (protocolError) {
            return protocolError;
        }
        // Handle resumability: check for Last-Event-ID header
        if (this._eventStore) {
            const lastEventId = req.headers.get('last-event-id');
            if (lastEventId) {
                return this.replayEvents(lastEventId);
            }
        }
        // Check if there's already an active standalone SSE stream for this session
        if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) {
            // Only one GET SSE stream is allowed per session
            this.onerror?.(new Error('Conflict: Only one SSE stream is allowed per session'));
            return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session');
        }
        const encoder = new TextEncoder();
        let streamController;
        let keepAliveTimer = undefined;
        // Create a ReadableStream with a controller we can use to push SSE events
        const readable = new ReadableStream({
            start: controller => {
                streamController = controller;
            },
            cancel: () => {
                // Stream was cancelled by client. Only tear down the mapping if it
                // still points at this stream — a stale cancel arriving after a
                // reconnect re-registered the id must not tear down the successor.
                if (keepAliveTimer !== undefined) {
                    clearInterval(keepAliveTimer);
                }
                if (this._streamMapping.get(this._standaloneSseStreamId)?.controller === streamController) {
                    this._streamMapping.delete(this._standaloneSseStreamId);
                }
            }
        });
        const headers = {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache, no-transform',
            Connection: 'keep-alive',
            'X-Accel-Buffering': 'no'
        };
        // After initialization, always include the session ID if we have one
        if (this.sessionId !== undefined) {
            headers['mcp-session-id'] = this.sessionId;
        }
        // Store the stream mapping with the controller for pushing data
        this._streamMapping.set(this._standaloneSseStreamId, {
            controller: streamController,
            encoder,
            cleanup: () => {
                if (keepAliveTimer !== undefined) {
                    clearInterval(keepAliveTimer);
                }
                this._streamMapping.delete(this._standaloneSseStreamId);
                try {
                    streamController.close();
                }
                catch {
                    // Controller might already be closed
                }
            }
        });
        keepAliveTimer = this.startKeepAlive(streamController, encoder);
        return new Response(readable, { headers });
    }
    /**
     * Replays events that would have been sent after the specified event ID
     * Only used when resumability is enabled
     */
    async replayEvents(lastEventId) {
        if (!this._eventStore) {
            this.onerror?.(new Error('Event store not configured'));
            return this.createJsonErrorResponse(400, -32000, 'Event store not configured');
        }
        try {
            // If getStreamIdForEventId is available, use it for conflict checking
            let streamId;
            if (this._eventStore.getStreamIdForEventId) {
                streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
                if (!streamId) {
                    this.onerror?.(new Error('Invalid event ID format'));
                    return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format');
                }
                // Check conflict with the SAME streamId we'll use for mapping
                if (this._streamMapping.get(streamId) !== undefined) {
                    this.onerror?.(new Error('Conflict: Stream already has an active connection'));
                    return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection');
                }
            }
            const headers = {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache, no-transform',
                Connection: 'keep-alive',
                'X-Accel-Buffering': 'no'
            };
            if (this.sessionId !== undefined) {
                headers['mcp-session-id'] = this.sessionId;
            }
            // Create a ReadableStream with controller for SSE
            const encoder = new TextEncoder();
            let streamController;
            let keepAliveTimer = undefined;
            let replayedStreamId = undefined;
            let cancelled = false;
            const readable = new ReadableStream({
                start: controller => {
                    streamController = controller;
                },
                cancel: () => {
                    // Stream was cancelled by client. Only tear down the mapping if
                    // it still points at this stream — a stale cancel must not tear
                    // down a successor registered by a later resume.
                    cancelled = true;
                    if (keepAliveTimer !== undefined) {
                        clearInterval(keepAliveTimer);
                    }
                    if (replayedStreamId !== undefined && this._streamMapping.get(replayedStreamId)?.controller === streamController) {
                        this._streamMapping.delete(replayedStreamId);
                    }
                }
            });
            // Replay events - returns the streamId for backwards compatibility
            const replayedEventIds = new Set();
            replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, {
                send: async (eventId, message) => {
                    const success = this.writeSSEEvent(streamController, encoder, message, eventId);
                    if (!success) {
                        this.onerror?.(new Error('Failed replay events'));
                        try {
                            streamController.close();
                        }
                        catch {
                            // Controller might already be closed
                        }
                    }
                    else {
                        replayedEventIds.add(eventId);
                    }
                }
            });
            // close() may have run, or the stream may have been cancelled, while
            // the event store was replaying: don't hand out or register a stream
            // that nothing can ever clean up again.
            if (this._closed || cancelled) {
                try {
                    streamController.close();
                }
                catch {
                    // Controller might already be closed
                }
                return this.createJsonErrorResponse(404, -32001, 'Session not found');
            }
            // A reconnect re-registers the same stream id: close the predecessor
            // stream cleanly (clearing its keep-alive timer) instead of leaving it
            // registered nowhere with its timer still running.
            this._streamMapping.get(replayedStreamId)?.cleanup();
            this._streamMapping.set(replayedStreamId, {
                controller: streamController,
                encoder,
                replayedEventIds,
                cleanup: () => {
                    if (keepAliveTimer !== undefined) {
                        clearInterval(keepAliveTimer);
                    }
                    this._streamMapping.delete(replayedStreamId);
                    try {
                        streamController.close();
                    }
                    catch {
                        // Controller might already be closed
                    }
                }
            });
            // The resume itself proves the client holds a Last-Event-ID cursor
            this._resumableStreams.add(replayedStreamId);
            keepAliveTimer = this.startKeepAlive(streamController, encoder);
            return new Response(readable, { headers });
        }
        catch (error) {
            this.onerror?.(error);
            return this.createJsonErrorResponse(500, -32000, 'Error replaying events');
        }
    }
    /**
     * Writes an event to an SSE stream via controller with proper formatting
     */
    writeSSEEvent(controller, encoder, message, eventId) {
        try {
            let eventData = `event: message\n`;
            // Include event ID if provided - this is important for resumability
            if (eventId) {
                eventData += `id: ${eventId}\n`;
            }
            eventData += `data: ${JSON.stringify(message)}\n\n`;
            controller.enqueue(encoder.encode(eventData));
            return true;
        }
        catch (error) {
            this.onerror?.(error);
            return false;
        }
    }
    /**
     * Handles unsupported requests (PUT, PATCH, etc.)
     */
    handleUnsupportedRequest() {
        this.onerror?.(new Error('Method not allowed.'));
        return new Response(JSON.stringify({
            jsonrpc: '2.0',
            error: {
                code: -32000,
                message: 'Method not allowed.'
            },
            id: null
        }), {
            status: 405,
            headers: {
                Allow: 'GET, POST, DELETE',
                'Content-Type': 'application/json'
            }
        });
    }
    /**
     * Handles POST requests containing JSON-RPC messages
     */
    async handlePostRequest(req, options) {
        try {
            // Validate the Accept header
            const acceptHeader = req.headers.get('accept');
            // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.
            // Accept is a comma-separated list, so a substring check is the intended semantics here (unlike Content-Type below).
            // eslint-disable-next-line no-restricted-syntax
            if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) {
                this.onerror?.(new Error('Not Acceptable: Client must accept both application/json and text/event-stream'));
                return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept both application/json and text/event-stream');
            }
            // Parsed media type, never a substring match — see isJsonContentType.
            const ct = req.headers.get('content-type');
            if (!(0, mediaType_js_1.isJsonContentType)(ct)) {
                this.onerror?.(new Error('Unsupported Media Type: Content-Type must be application/json'));
                return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json');
            }
            // Build request info from headers and URL
            const requestInfo = {
                headers: Object.fromEntries(req.headers.entries()),
                url: new URL(req.url)
            };
            let rawMessage;
            if (options?.parsedBody !== undefined) {
                rawMessage = options.parsedBody;
            }
            else {
                try {
                    rawMessage = await req.json();
                }
                catch {
                    this.onerror?.(new Error('Parse error: Invalid JSON'));
                    return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON');
                }
            }
            let messages;
            // handle batch and single messages
            try {
                if (Array.isArray(rawMessage)) {
                    messages = rawMessage.map(msg => types_js_1.JSONRPCMessageSchema.parse(msg));
                }
                else {
                    messages = [types_js_1.JSONRPCMessageSchema.parse(rawMessage)];
                }
            }
            catch {
                this.onerror?.(new Error('Parse error: Invalid JSON-RPC message'));
                return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message');
            }
            // close() may have run while awaiting the request body above: don't
            // initialize a session (or fire onsessioninitialized) on a transport
            // whose cleanups have already run.
            if (this._closed) {
                return this.createJsonErrorResponse(404, -32001, 'Session not found');
            }
            // Check if this is an initialization request
            // https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/
            const isInitializationRequest = messages.some(types_js_1.isInitializeRequest);
            if (isInitializationRequest) {
                // If it's a server with session management and the session ID is already set we should reject the request
                // to avoid re-initialization.
                if (this._initialized && this.sessionId !== undefined) {
                    this.onerror?.(new Error('Invalid Request: Server already initialized'));
                    return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized');
                }
                if (messages.length > 1) {
                    this.onerror?.(new Error('Invalid Request: Only one initialization request is allowed'));
                    return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed');
                }
                this.sessionId = this.sessionIdGenerator?.();
                this._initialized = true;
                // If we have a session ID and an onsessioninitialized handler, call it immediately
                // This is needed in cases where the server needs to keep track of multiple sessions
                if (this.sessionId && this._onsessioninitialized) {
                    await Promise.resolve(this._onsessioninitialized(this.sessionId));
                }
            }
            if (!isInitializationRequest) {
                // If an Mcp-Session-Id is returned by the server during initialization,
                // clients using the Streamable HTTP transport MUST include it
                // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
                const sessionError = this.validateSession(req);
                if (sessionError) {
                    return sessionError;
                }
                // Mcp-Protocol-Version header is required for all requests after initialization.
                const protocolError = this.validateProtocolVersion(req);
                if (protocolError) {
                    return protocolError;
                }
            }
            // close() may have run while awaiting the request body or the session
            // initialization callback above: don't register streams into maps the
            // close sweep has already cleared, or hand out a stream that nothing
            // will ever write to.
            if (this._closed) {
                return this.createJsonErrorResponse(404, -32001, 'Session not found');
            }
            // check if it contains requests
            const hasRequests = messages.some(types_js_1.isJSONRPCRequest);
            if (!hasRequests) {
                // if it only contains notifications or responses, return 202
                for (const message of messages) {
                    this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
                }
                return new Response(null, { status: 202 });
            }
            // The default behavior is to use SSE streaming
            // but in some cases server will return JSON responses
            const streamId = crypto.randomUUID();
            // Extract protocol version for priming event decision.
            // For initialize requests, get from request params.
            // For other requests, get from header (already validated).
            const initRequest = messages.find(m => (0, types_js_1.isInitializeRequest)(m));
            const clientProtocolVersion = initRequest
                ? initRequest.params.protocolVersion
                : (req.headers.get('mcp-protocol-version') ?? types_js_1.DEFAULT_NEGOTIATED_PROTOCOL_VERSION);
            if (this._enableJsonResponse) {
                // For JSON response mode, return a Promise that resolves when all responses are ready
                return new Promise(resolve => {
                    this._streamMapping.set(streamId, {
                        resolveJson: resolve,
                        cleanup: () => {
                            this._streamMapping.delete(streamId);
                        }
                    });
                    for (const message of messages) {
                        if ((0, types_js_1.isJSONRPCRequest)(message)) {
                            this._requestToStreamMapping.set(message.id, streamId);
                        }
                    }
                    for (const message of messages) {
                        this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
                    }
                });
            }
            // SSE streaming mode - use ReadableStream with controller for more reliable data pushing
            const encoder = new TextEncoder();
            let streamController;
            let keepAliveTimer = undefined;
            const readable = new ReadableStream({
                start: controller => {
                    streamController = controller;
                },
                cancel: () => {
                    // Stream was cancelled by client. Only tear down the mapping if
                    // it still points at this stream — a stale cancel arriving after
                    // a resume re-registered the id must not tear down the successor.
                    if (keepAliveTimer !== undefined) {
                        clearInterval(keepAliveTimer);
                    }
                    if (this._streamMapping.get(streamId)?.controller === streamController) {
                        this._streamMapping.delete(streamId);
                    }
                }
            });
            const headers = {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache, no-transform',
                Connection: 'keep-alive',
                'X-Accel-Buffering': 'no'
            };
            // After initialization, always include the session ID if we have one
            if (this.sessionId !== undefined) {
                headers['mcp-session-id'] = this.sessionId;
            }
            // Store the response for this request to send messages back through this connection
            // We need to track by request ID to maintain the connection
            for (const message of messages) {
                if ((0, types_js_1.isJSONRPCRequest)(message)) {
                    this._streamMapping.set(streamId, {
                        controller: streamController,
                        encoder,
                        cleanup: () => {
                            if (keepAliveTimer !== undefined) {
                                clearInterval(keepAliveTimer);
                            }
                            this._streamMapping.delete(streamId);
                            try {
                                streamController.close();
                            }
                            catch {
                                // Controller might already be closed
                            }
                        }
                    });
                    this._requestToStreamMapping.set(message.id, streamId);
                }
            }
            try {
                // Write priming event if event store is configured (after mapping is set up)
                await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
                // handle each message
                for (const message of messages) {
                    // Build closeSSEStream callback for requests when eventStore is configured
                    // AND client supports resumability (protocol version >= 2025-11-25).
                    // Old clients can't resume if the stream is closed early because they
                    // didn't receive a priming event with an event ID.
                    let closeSSEStream;
                    let closeStandaloneSSEStream;
                    if ((0, types_js_1.isJSONRPCRequest)(message) && this._eventStore && clientProtocolVersion >= '2025-11-25') {
                        closeSSEStream = () => {
                            this.closeSSEStream(message.id);
                        };
                        closeStandaloneSSEStream = () => {
                            this.closeStandaloneSSEStream();
                        };
                    }
                    this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream });
                }
            }
            catch (error) {
                // Failing before the Response is handed out means nothing can ever
                // cancel this stream: release it and its request correlations here.
                this._streamMapping.get(streamId)?.cleanup();
                this._resumableStreams.delete(streamId);
                for (const message of messages) {
                    if ((0, types_js_1.isJSONRPCRequest)(message)) {
                        this._requestToStreamMapping.delete(message.id);
                    }
                }
                throw error;
            }
            // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses
            // This will be handled by the send() method when responses are ready
            // Arm keep-alive only after the fallible awaits above: an error path
            // returns 400 and discards the Response, so nothing could ever cancel
            // the stream and clear an already-armed timer. Skip if the responses
            // already completed and cleaned the stream up.
            if (this._streamMapping.get(streamId)?.controller === streamController) {
                keepAliveTimer = this.startKeepAlive(streamController, encoder);
            }
            return new Response(readable, { status: 200, headers });
        }
        catch (error) {
            // return JSON-RPC formatted error
            this.onerror?.(error);
            return this.createJsonErrorResponse(400, -32700, 'Parse error', { data: String(error) });
        }
    }
    /**
     * Handles DELETE requests to terminate sessions
     */
    async handleDeleteRequest(req) {
        const sessionError = this.validateSession(req);
        if (sessionError) {
            return sessionError;
        }
        const protocolError = this.validateProtocolVersion(req);
        if (protocolError) {
            return protocolError;
        }
        try {
            await Promise.resolve(this._onsessionclosed?.(this.sessionId));
            return new Response(null, { status: 200 });
        }
        finally {
            await this.close();
        }
    }
    /**
     * Validates session ID for non-initialization requests.
     * Returns Response error if invalid, undefined otherwise
     */
    validateSession(req) {
        if (this.sessionIdGenerator === undefined) {
            // If the sessionIdGenerator ID is not set, the session management is disabled
            // and we don't need to validate the session ID
            return undefined;
        }
        if (!this._initialized) {
            // If the server has not been initialized yet, reject all requests
            this.onerror?.(new Error('Bad Request: Server not initialized'));
            return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized');
        }
        const sessionId = req.headers.get('mcp-session-id');
        if (!sessionId) {
            // Non-initialization requests without a session ID should return 400 Bad Request
            this.onerror?.(new Error('Bad Request: Mcp-Session-Id header is required'));
            return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required');
        }
        if (sessionId !== this.sessionId) {
            // Reject requests with invalid session ID with 404 Not Found
            this.onerror?.(new Error('Session not found'));
            return this.createJsonErrorResponse(404, -32001, 'Session not found');
        }
        return undefined;
    }
    /**
     * Validates the MCP-Protocol-Version header on incoming requests.
     *
     * For initialization: Version negotiation handles unknown versions gracefully
     * (server responds with its supported version).
     *
     * For subsequent requests with MCP-Protocol-Version header:
     * - Accept if in supported list
     * - 400 if unsupported
     *
     * For HTTP requests without the MCP-Protocol-Version header:
     * - Accept and default to the version negotiated at initialization
     */
    validateProtocolVersion(req) {
        const protocolVersion = req.headers.get('mcp-protocol-version');
        if (protocolVersion !== null && !types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
            this.onerror?.(new Error(`Bad Request: Unsupported protocol version: ${protocolVersion}` +
                ` (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`));
            return this.createJsonErrorResponse(400, -32000, `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`);
        }
        return undefined;
    }
    async close() {
        if (this._closed) {
            return;
        }
        // Stop any deferred keep-alive arm (e.g. a replay await resolving after
        // close) from creating a timer nothing can clear.
        this._closed = true;
        // Close all SSE connections (each cleanup clears its own keep-alive timer)
        this._streamMapping.forEach(({ cleanup }) => {
            cleanup();
        });
        this._streamMapping.clear();
        // Clear any pending responses
        this._requestResponseMap.clear();
        this._resumableStreams.clear();
        this.onclose?.();
    }
    /**
     * Close an SSE stream for a specific request, triggering client reconnection.
     * Use this to implement polling behavior during long-running operations -
     * client will reconnect after the retry interval specified in the priming event.
     */
    closeSSEStream(requestId) {
        const streamId = this._requestToStreamMapping.get(requestId);
        if (!streamId)
            return;
        const stream = this._streamMapping.get(streamId);
        if (stream) {
            stream.cleanup();
        }
    }
    /**
     * Close the standalone GET SSE stream, triggering client reconnection.
     * Use this to implement polling behavior for server-initiated notifications.
     */
    closeStandaloneSSEStream() {
        const stream = this._streamMapping.get(this._standaloneSseStreamId);
        if (stream) {
            stream.cleanup();
        }
    }
    async send(message, options) {
        let requestId = options?.relatedRequestId;
        if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
            // If the message is a response, use the request ID from the message
            requestId = message.id;
        }
        // Check if this message should be sent on the standalone SSE stream (no request ID)
        // Ignore notifications from tools (which have relatedRequestId set)
        // Those will be sent via dedicated response SSE streams
        if (requestId === undefined) {
            // For standalone SSE streams, we can only send requests and notifications
            if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
                throw new Error('Cannot send a response on a standalone SSE stream unless resuming a previous client request');
            }
            // Generate and store event ID if event store is provided
            // Store even if stream is disconnected so events can be replayed on reconnect
            let eventId;
            if (this._eventStore) {
                // Stores the event and gets the generated event ID
                eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
            }
            const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
            if (standaloneSse === undefined) {
                // Stream is disconnected - event is stored for replay, nothing more to do
                return;
            }
            // Send the message to the standalone SSE stream, unless this stream
            // was registered by a replay that already delivered this event (the
            // store made it visible before this call resumed).
            if (standaloneSse.controller &&
                standaloneSse.encoder &&
                (eventId === undefined || !standaloneSse.replayedEventIds?.has(eventId))) {
                this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
            }
            return;
        }
        // Get the response for this request
        const streamId = this._requestToStreamMapping.get(requestId);
        if (!streamId) {
            throw new Error(`No connection established for request ID: ${String(requestId)}`);
        }
        let stream = this._streamMapping.get(streamId);
        if (!this._enableJsonResponse) {
            // Store the event even if the stream is disconnected (e.g. after
            // closeSSEStream switched the client to polling), so it can be
            // replayed on reconnect — mirroring the standalone stream path.
            let eventId;
            if (this._eventStore) {
                eventId = await this._eventStore.storeEvent(streamId, message);
                // Re-fetch after the await: a resume completing meanwhile replaces
                // this stream's registration with a successor, and the message
                // must go to the live stream, not the evicted one.
                stream = this._streamMapping.get(streamId);
            }
            // Skip the write if the stream was registered by a replay that
            // already delivered this event (the store made it visible before
            // this call resumed).
            if (stream?.controller && stream?.encoder && (eventId === undefined || !stream.replayedEventIds?.has(eventId))) {
                const written = this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
                // The client now holds a Last-Event-ID cursor for this stream
                if (written && eventId !== undefined) {
                    this._resumableStreams.add(streamId);
                }
            }
        }
        if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
            this._requestResponseMap.set(requestId, message);
            const relatedIds = Array.from(this._requestToStreamMapping.entries())
                .filter(([_, sid]) => sid === streamId)
                .map(([id]) => id);
            // Check if we have responses for all requests using this connection
            const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id));
            if (allResponsesReady) {
                if (!stream) {
                    // close() ran while the event store write above was awaiting:
                    // release the entries this call re-created after the sweep,
                    // then treat the late completion as a no-op.
                    if (this._closed) {
                        for (const id of relatedIds) {
                            this._requestResponseMap.delete(id);
                            this._requestToStreamMapping.delete(id);
                        }
                        return;
                    }
                    if (!this._enableJsonResponse && this._eventStore && this._resumableStreams.has(streamId)) {
                        // The stream went away (client disconnect, or
                        // closeSSEStream switched the client to polling), but the
                        // response was stored above and will be replayed on
                        // resume — release the correlations rather than throwing.
                        for (const id of relatedIds) {
                            this._requestResponseMap.delete(id);
                            this._requestToStreamMapping.delete(id);
                        }
                        this._resumableStreams.delete(streamId);
                        return;
                    }
                    // Without resumability (or in JSON response mode, where the
                    // pending HTTP response can never be settled by a replay),
                    // a missing stream means the response cannot be delivered.
                    // Release the correlations so the entries don't outlive the
                    // undeliverable response.
                    for (const id of relatedIds) {
                        this._requestResponseMap.delete(id);
                        this._requestToStreamMapping.delete(id);
                    }
                    throw new Error(`No connection established for request ID: ${String(requestId)}`);
                }
                if (this._enableJsonResponse && stream.resolveJson) {
                    // All responses ready, send as JSON
                    const headers = {
                        'Content-Type': 'application/json'
                    };
                    if (this.sessionId !== undefined) {
                        headers['mcp-session-id'] = this.sessionId;
                    }
                    const responses = relatedIds.map(id => this._requestResponseMap.get(id));
                    if (responses.length === 1) {
                        stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers }));
                    }
                    else {
                        stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers }));
                    }
                }
                else {
                    // End the SSE stream
                    stream.cleanup();
                }
                // Clean up
                for (const id of relatedIds) {
                    this._requestResponseMap.delete(id);
                    this._requestToStreamMapping.delete(id);
                }
                this._resumableStreams.delete(streamId);
            }
        }
    }
}
exports.WebStandardStreamableHTTPServerTransport = WebStandardStreamableHTTPServerTransport;
//# sourceMappingURL=webStandardStreamableHttp.js.map