"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HotMCPServer = void 0;
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const HotRouteMethod_1 = require("./HotRouteMethod");
const HotLog_1 = require("./HotLog");
const HotStaq_1 = require("./HotStaq");
const HotHTTPServerProcessRequest_1 = require("./HotHTTPServerProcessRequest");
/**
 * Exposes a HotAPI as an MCP (Model Context Protocol) server using the
 * Streamable HTTP transport in stateless mode.
 *
 * This class walks all routes and methods on a HotAPI instance and registers
 * them as MCP tools. When a tool is called via MCP, it makes an internal
 * in-process call through the full HotStaq request pipeline and returns
 * the result.
 *
 * Each POST to the MCP route is self-contained: a fresh Server + transport
 * are created per request, authorization runs once, and cleanup fires when
 * the response closes. There is no long-lived session state — which means
 * no "Session not found" 404s when a reverse proxy reaps an idle SSE
 * stream.
 */
class HotMCPServer {
    constructor(api, route = "/mcp") {
        this.api = api;
        this.route = route;
        this.logger = new HotLog_1.HotLog(HotLog_1.HotLogLevel.All);
        this.tools = [];
        this.onServerAuthorize = null;
        this.onSuccessfulConnection = null;
        this.onConnection = null;
        this.onConnectionError = null;
        this.buildToolDefinitions();
    }
    /**
     * Build MCP tool definitions from the API's routes and methods.
     */
    buildToolDefinitions() {
        if (this.api.routes == null)
            return;
        for (let routeName in this.api.routes) {
            let route = this.api.routes[routeName];
            if (route.methods == null)
                continue;
            for (let method of route.methods) {
                let toolName = `${route.route}_${method.name}`;
                let properties = {};
                let required = [];
                if (method.parameters != null) {
                    for (let paramName in method.parameters) {
                        let param = method.parameters[paramName];
                        if (typeof (param) === "function")
                            continue;
                        properties[paramName] = HotStaq_1.HotStaq.convertParamToJSONSchemaProperty(param);
                        if (param.required === true)
                            required.push(paramName);
                    }
                }
                let httpMethod = HotStaq_1.HotStaq.getHTTPMethodFromEvent(method.type);
                let version = route.version || "v1";
                let prefix = route.prefix || "";
                let urlPath = `/${version}/${prefix}${route.route}/${method.name}`;
                // Clean up double slashes
                urlPath = urlPath.replace(/\/+/g, "/");
                let toolDef = {
                    name: toolName,
                    description: method.description || `Call ${route.route}/${method.name}`,
                    inputSchema: {
                        type: "object",
                        properties: properties
                    },
                    httpMethod: httpMethod,
                    urlPath: urlPath,
                    routeName: routeName,
                    methodName: method.name
                };
                if (required.length > 0)
                    toolDef.inputSchema.required = required;
                this.tools.push(toolDef);
            }
        }
    }
    /**
     * Create a fresh MCP Server instance with handlers registered for this
     * request. The authorizedValue (from onServerAuthorize or the raw bearer
     * token) and the raw connection-level bearer token are captured in closure
     * so tool calls can flow them through as connection-level auth.
     *
     * Both values are threaded because onServerAuthorize commonly returns a
     * non-string (e.g. a user object). When it does, the original bearer would
     * otherwise be unavailable to downstream tool-call route handlers and
     * write-tools would fail with "jwt must be provided".
     */
    createServerInstance(authorizedValue, connectionBearerToken = "") {
        let instance = new index_js_1.Server({
            name: "HotStaq MCP Server",
            version: HotStaq_1.HotStaq.version
        }, {
            capabilities: {
                tools: {}
            }
        });
        this.registerHandlers(instance, authorizedValue, connectionBearerToken);
        return (instance);
    }
    /**
     * Register the MCP request handlers for tools/list and tools/call on
     * the given Server instance. authorizedValue is the result of
     * onServerAuthorize (or the raw bearer if no callback is set). The
     * connectionBearerToken parameter is the raw Bearer string extracted from
     * the original HTTP request — used as a fallback bearer for tool calls
     * when a per-call _meta.authorization isn't supplied AND the
     * authorizedValue isn't a string.
     */
    registerHandlers(instance, authorizedValue, connectionBearerToken = "") {
        instance.setRequestHandler(types_js_1.ListToolsRequestSchema, () => __awaiter(this, void 0, void 0, function* () {
            return ({
                tools: this.tools.map((tool) => {
                    return ({
                        name: tool.name,
                        description: tool.description,
                        inputSchema: tool.inputSchema
                    });
                })
            });
        }));
        instance.setRequestHandler(types_js_1.CallToolRequestSchema, (request) => __awaiter(this, void 0, void 0, function* () {
            let toolName = request.params.name;
            let args = request.params.arguments || {};
            let tool = this.tools.find((t) => t.name === toolName);
            if (tool == null) {
                let result = {
                    content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
                    isError: true
                };
                return (result);
            }
            try {
                let result = yield this.executeToolCall(tool, args, request, authorizedValue, connectionBearerToken);
                return (result);
            }
            catch (ex) {
                let errorMessage = "Unknown error";
                if (ex instanceof Error)
                    errorMessage = ex.message;
                else if (typeof (ex) === "string")
                    errorMessage = ex;
                let result = {
                    content: [{ type: "text", text: `Error calling ${toolName}: ${errorMessage}` }],
                    isError: true
                };
                return (result);
            }
        }));
    }
    /**
     * Execute a tool call by calling processRequest directly, going through
     * the full HotStaq sanitization pipeline (validation, authorization,
     * pre/post execute hooks, etc).
     *
     * Bearer-token resolution order (first non-empty wins):
     *   1. Per-call: `request.params._meta.authorization` from the MCP client.
     *   2. Connection-level raw Bearer captured before `onServerAuthorize` ran.
     *   3. Legacy fallback: `authorizedValue` if it happens to be a string.
     *
     * Step 2 is the fix for HS-1: when `onServerAuthorize` returns a non-string
     * (e.g. a user object) the legacy fallback in step 3 doesn't fire, and
     * before this change the route handler saw no JWT — every admin-only write
     * tool failed with "jwt must be provided" even though the connection-level
     * Bearer authenticated fine.
     */
    executeToolCall(tool_1, args_1, mcpRequest_1, authorizedValue_1) {
        return __awaiter(this, arguments, void 0, function* (tool, args, mcpRequest, authorizedValue, connectionBearerToken = "") {
            let server = this.api.connection;
            let route = this.api.routes[tool.routeName];
            if (route == null)
                throw new Error(`Route "${tool.routeName}" not found`);
            let method = route.methods.find((m) => m.name === tool.methodName);
            if (method == null)
                throw new Error(`Method "${tool.methodName}" not found on route "${tool.routeName}"`);
            let methodName = method.getRouteUrl();
            // 1. Extract bearer token from MCP request metadata if provided.
            // MCP clients pass auth via request.params._meta.authorization.
            let bearerToken = "";
            if (mcpRequest != null && mcpRequest.params != null &&
                mcpRequest.params._meta != null && mcpRequest.params._meta.authorization != null) {
                bearerToken = mcpRequest.params._meta.authorization;
                // Strip "Bearer " prefix if present, matching how processRequest handles it.
                if (bearerToken.startsWith("Bearer ") || bearerToken.startsWith("bearer "))
                    bearerToken = bearerToken.substring(7);
            }
            // 2. Fall back to the raw Bearer that came in on the original HTTP
            // request. This is what authenticated the MCP connection itself and is
            // the right token for route-level onAuthorizeUser to verify.
            if (bearerToken === "" && connectionBearerToken !== "") {
                bearerToken = connectionBearerToken;
            }
            // 3. Legacy fallback: connection-level authorizedValue when it's a
            // string. Kept for backward compat with consumers that have an
            // onServerAuthorize callback returning a Bearer string directly.
            if (bearerToken === "" && authorizedValue != null &&
                typeof (authorizedValue) === "string") {
                bearerToken = authorizedValue;
            }
            // Build the authorization header so processRequest can read it the same way
            // it does for normal HTTP requests.
            let authHeader = bearerToken !== "" ? `Bearer ${bearerToken}` : "";
            // Build a synthetic Express req object that mirrors a real Express request
            // closely enough for route handlers, middleware, and processRequest to work.
            // In addition to body/query/headers, handlers commonly access req.ip,
            // req.connection.remoteAddress, req.get(), req.originalUrl, req.protocol, etc.
            let reqHeaders = authHeader !== "" ? { authorization: authHeader } : {};
            // MCP arguments are always sent as a JSON object. Route handlers read from
            // req.jsonObj (which is req.body), so always populate body — regardless of
            // the underlying route's HTTP method. Putting args in the query string for
            // GET routes breaks object-typed params (e.g. list filters) because
            // HotProcessInput.processInput rejects non-object values with "must be an
            // object". Treating MCP calls as body-bearing requests avoids that.
            let req = {
                method: tool.httpMethod,
                body: args,
                query: {},
                headers: reqHeaders,
                ip: "127.0.0.1",
                ips: [],
                protocol: "mcp",
                secure: false,
                hostname: "localhost",
                originalUrl: tool.urlPath,
                path: tool.urlPath,
                baseUrl: "",
                params: {},
                httpVersion: "1.1",
                connection: { remoteAddress: "127.0.0.1", encrypted: false },
                socket: { remoteAddress: "127.0.0.1", encrypted: false },
                cookies: {},
                get: (name) => {
                    return (reqHeaders[name.toLowerCase()]);
                },
                header: (name) => {
                    return (reqHeaders[name.toLowerCase()]);
                },
                on: (_event, _handler) => { }
            };
            // Build a synthetic Express res that captures the response instead of
            // writing to a real HTTP connection — same pattern as HotWebSocketServer
            // which creates a ServerRequest without req/res when there's no HTTP context.
            let capturedStatus = 200;
            let capturedBody = undefined;
            let resHeaders = {};
            let res = {
                status: (code) => {
                    capturedStatus = code;
                    return (res);
                },
                json: (value) => {
                    capturedBody = value;
                },
                send: (value) => {
                    capturedBody = value;
                },
                end: () => { },
                on: (_event, _handler) => { },
                set: (headers) => {
                    if (typeof (headers) === "object")
                        Object.assign(resHeaders, headers);
                    return (res);
                },
                setHeader: (name, value) => {
                    resHeaders[name] = value;
                    return (res);
                },
                getHeader: (name) => {
                    return (resHeaders[name]);
                },
                getHeaders: () => {
                    return (resHeaders);
                },
                flushHeaders: () => { },
                headersSent: false
            };
            // Delegate to processRequest — this runs the full HotStaq pipeline:
            // onServerAuthorize / onAuthorizeUser, input validation (validateQueryInput,
            // validateJSONInput, processInput), onServerPreExecute, onServerExecute,
            // onServerPostExecute. The ServerRequest built inside processRequest will
            // have bearerToken set from req.headers.authorization, matching normal HTTP flow.
            let response = yield (0, HotHTTPServerProcessRequest_1.processRequest)(server, server.logger, route, method, methodName, req, res);
            // processRequest returns the result directly or routes through res.json().
            let resultBody = (response !== undefined) ? response : capturedBody;
            let isError = false;
            if (resultBody != null && resultBody.error != null)
                isError = true;
            let resultText;
            if (typeof (resultBody) === "string")
                resultText = resultBody;
            else
                resultText = JSON.stringify(resultBody);
            let result = {
                content: [{ type: "text", text: resultText }],
                isError: isError
            };
            return (result);
        });
    }
    /**
     * Handle an incoming MCP POST request via the Streamable HTTP transport.
     *
     * Each request is fully self-contained: connection gate → bearer-token
     * extraction → onServerAuthorize → fresh Server + transport → tool call
     * → response → cleanup. No state persists between requests, so there's
     * no session map to invalidate and no long-lived connection to reap.
     */
    handleMCPRequest(req, res) {
        return __awaiter(this, void 0, void 0, function* () {
            this.logger.verbose(() => `New MCP request from ${req.ip}`);
            // Early connection gate — developer can reject before auth runs.
            if (this.onConnection != null) {
                let allowed = false;
                try {
                    allowed = yield this.onConnection(req);
                }
                catch (ex) {
                    this.logger.error(`MCP onConnection error: ${ex.message}`);
                    res.status(500).json({ error: "Internal Server Error" });
                    return;
                }
                if (allowed === false) {
                    this.logger.verbose(`MCP request rejected by onConnection from ${req.ip}`);
                    res.status(403).json({ error: "Forbidden" });
                    return;
                }
            }
            // Extract bearer token from Authorization header or query param.
            // This is done regardless of whether onServerAuthorize is set, so
            // the token can flow into tool calls even without a custom auth callback.
            let bearerToken = "";
            if (req.headers.authorization != null) {
                bearerToken = req.headers.authorization;
                if (bearerToken.startsWith("Bearer ") || bearerToken.startsWith("bearer "))
                    bearerToken = bearerToken.substring(7);
            }
            else if ((req.query.token != null) && (typeof (req.query.token) === "string")) {
                bearerToken = req.query.token;
            }
            // Request-level authorization — optional. When set, the callback
            // validates the token and returns an authorized value. When not set,
            // the raw bearer token is stored so it can flow into tool calls.
            let authorizedValue = bearerToken !== "" ? bearerToken : null;
            if (this.onServerAuthorize != null) {
                let request = new HotRouteMethod_1.ServerRequest({
                    req: req,
                    res: null,
                    bearerToken: bearerToken,
                    authorizedValue: null,
                    jsonObj: req.body || {},
                    queryObj: req.query,
                    files: null
                });
                try {
                    authorizedValue = yield this.onServerAuthorize(request);
                }
                catch (ex) {
                    this.logger.verbose(`MCP authorization error from ${req.ip}: ${ex.message}`);
                    if (this.onConnectionError != null)
                        yield this.onConnectionError(req, ex.message);
                    res.status(401).json({ error: ex.message });
                    return;
                }
                if (authorizedValue === undefined) {
                    this.logger.verbose(`MCP unauthorized request from ${req.ip}`);
                    if (this.onConnectionError != null)
                        yield this.onConnectionError(req, "Unauthorized");
                    res.status(401).json({ error: "Unauthorized" });
                    return;
                }
            }
            this.logger.verbose(() => `MCP request authorized from ${req.ip}`);
            // Stateless mode: sessionIdGenerator undefined → no session ID, no
            // validation, no map to poison. Each request is independent.
            let transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
                sessionIdGenerator: undefined
            });
            // Pass the raw connection-level bearer alongside authorizedValue so
            // tool calls can authenticate against route-level onAuthorizeUser
            // even when onServerAuthorize returned a non-string (e.g. a user
            // object). See HS-1.
            let sessionServer = this.createServerInstance(authorizedValue, bearerToken);
            try {
                yield sessionServer.connect(transport);
                if (this.onSuccessfulConnection != null) {
                    // Synthesize a per-request identifier for the callback's telemetry.
                    // This does not persist across requests — the stateless transport
                    // has no concept of a durable session.
                    let requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
                    yield this.onSuccessfulConnection(requestId, authorizedValue);
                }
                // The transport consumes the pre-parsed Express body so the JSON
                // middleware that's already drained the stream doesn't leave us
                // staring at an empty read.
                yield transport.handleRequest(req, res, req.body);
            }
            catch (ex) {
                this.logger.error(`MCP request error: ${ex.message}`);
                if (res.headersSent === false) {
                    res.status(500).json({
                        jsonrpc: "2.0",
                        error: { code: -32603, message: "Internal server error" },
                        id: null
                    });
                }
            }
            finally {
                // Clean up the per-request transport + server when the response
                // finishes. The SDK's close() is idempotent so calling it here is
                // safe even if the transport already closed naturally.
                let cleanup = () => {
                    try {
                        transport.close();
                    }
                    catch (_) { }
                    try {
                        sessionServer.close();
                    }
                    catch (_) { }
                };
                if (res.writableEnded)
                    cleanup();
                else
                    res.on("close", cleanup);
            }
        });
    }
    /**
     * Attach the MCP endpoints to an Express application.
     *
     * The server speaks the Streamable HTTP transport on a single POST
     * route. GET and DELETE on the same route return 405 Method Not
     * Allowed so misbehaving clients see a clear failure instead of a
     * quiet hang.
     */
    attach(app) {
        return __awaiter(this, void 0, void 0, function* () {
            app.post(this.route, (req, res) => __awaiter(this, void 0, void 0, function* () {
                yield this.handleMCPRequest(req, res);
            }));
            app.get(this.route, (_req, res) => {
                res.status(405).json({
                    jsonrpc: "2.0",
                    error: { code: -32000, message: "Method not allowed. Use POST for Streamable HTTP MCP." },
                    id: null
                });
            });
            app.delete(this.route, (_req, res) => {
                res.status(405).json({
                    jsonrpc: "2.0",
                    error: { code: -32000, message: "Method not allowed." },
                    id: null
                });
            });
        });
    }
}
exports.HotMCPServer = HotMCPServer;
//# sourceMappingURL=HotMCPServer.js.map