/**
 * server-http.ts — ClawQL MCP Server over Streamable HTTP
 *
 * Remote MCP entrypoint for agents that connect via URL.
 * Exposes MCP on /mcp and health on /healthz.
 */
import "./load-env.js";
import { randomUUID } from "node:crypto";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { attachGraphqlHttpToMcpApp } from "./graphql-http-attach.js";
import { createRegisteredMcpServer } from "./mcp-server-factory.js";
import { loadSpec } from "./spec-loader.js";
import { preloadSchemaFieldCacheFromDisk } from "./tools.js";
import { maybeStartGrpcMcpServer } from "mcp-grpc-transport";
import { getObsidianVaultPath, validateObsidianVaultAtStartup } from "./vault-config.js";
import { registerOuroborosPoolShutdownHooks } from "./ouroboros/postgres-pool.js";
import { registerPostgresPoolShutdownHooks } from "./vector-store/pgvector.js";
const PORT = Number.parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "8080", 10);
const DEFAULT_MCP_PATH = "/mcp";
/**
 * When `CLAWQL_CORS_ALLOW_ORIGIN` is set (e.g. `*` for Gallery / mobile webviews),
 * enable CORS + OPTIONS preflight so browser `fetch` to `/mcp` works.
 */
function applyCorsIfConfigured(req, res, next) {
    const allow = process.env.CLAWQL_CORS_ALLOW_ORIGIN?.trim();
    if (!allow) {
        next();
        return;
    }
    res.setHeader("Access-Control-Allow-Origin", allow);
    if (allow !== "*") {
        res.setHeader("Vary", "Origin");
    }
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, mcp-session-id, mcp-protocol-version");
    res.setHeader("Access-Control-Expose-Headers", "mcp-session-id, Mcp-Session-Id, mcp-protocol-version");
    res.setHeader("Access-Control-Max-Age", "86400");
    if (req.method === "OPTIONS") {
        res.sendStatus(204);
        return;
    }
    next();
}
function jsonRpcError(res, message, code = -32000) {
    res.status(400).json({
        jsonrpc: "2.0",
        error: { code, message },
        id: null,
    });
}
/**
 * Build Express app with `/healthz` and Streamable HTTP MCP on `mcpPath`.
 * Each call uses a fresh session transport map (safe for parallel tests).
 */
export async function createMcpHttpApp(options = {}) {
    if (!options.skipSpecPreload) {
        await loadSpec();
        await preloadSchemaFieldCacheFromDisk();
        await validateObsidianVaultAtStartup();
    }
    const mcpPath = options.mcpPath?.trim() || process.env.MCP_PATH?.trim() || DEFAULT_MCP_PATH;
    const transports = new Map();
    const app = createMcpExpressApp({
        host: options.host || process.env.MCP_HOST || "0.0.0.0",
    });
    app.use(applyCorsIfConfigured);
    await attachGraphqlHttpToMcpApp(app);
    app.get("/healthz", async (_req, res) => {
        const base = {
            status: "ok",
            transport: "streamable-http",
            endpoint: mcpPath,
        };
        if (process.env.CLAWQL_HEALTHZ_MEMORY_ARTIFACTS?.trim() === "1") {
            try {
                const vault = getObsidianVaultPath();
                if (vault) {
                    const { loadCuckooArtifactUpdatedAt, loadVaultMerkleSnapshotFromDb, memoryDbSyncEnabled, } = await import("./memory-db.js");
                    if (memoryDbSyncEnabled()) {
                        if (process.env.CLAWQL_MERKLE_ENABLED === "1") {
                            base.merkleSnapshot = await loadVaultMerkleSnapshotFromDb(vault);
                        }
                        if (process.env.CLAWQL_CUCKOO_ENABLED === "1") {
                            base.cuckooMembershipArtifactsEnabled = true;
                            const { getCuckooMetricsSnapshot } = await import("./memory-cuckoo-metrics.js");
                            base.cuckooMetrics = getCuckooMetricsSnapshot();
                            base.cuckooFilterPersistedAt = await loadCuckooArtifactUpdatedAt(vault);
                        }
                    }
                }
            }
            catch {
                /* optional enrichment — ignore */
            }
        }
        res.json(base);
    });
    app.post(mcpPath, async (req, res) => {
        const sessionId = req.header("mcp-session-id");
        try {
            let transport;
            if (sessionId && transports.has(sessionId)) {
                transport = transports.get(sessionId);
            }
            else if (!sessionId && isInitializeRequest(req.body)) {
                transport = new StreamableHTTPServerTransport({
                    sessionIdGenerator: () => randomUUID(),
                    onsessioninitialized: (sid) => {
                        transports.set(sid, transport);
                    },
                });
                transport.onclose = () => {
                    const sid = transport.sessionId;
                    if (sid)
                        transports.delete(sid);
                };
                const server = createRegisteredMcpServer();
                await server.connect(transport);
            }
            else {
                jsonRpcError(res, "Bad Request: missing/invalid mcp-session-id, or initialize request required.");
                return;
            }
            if (!transport) {
                jsonRpcError(res, "Bad Request: transport could not be resolved.");
                return;
            }
            await transport.handleRequest(req, res, req.body);
        }
        catch (err) {
            console.error("[clawql-mcp-http] POST /mcp error:", err);
            if (!res.headersSent) {
                res.status(500).json({
                    jsonrpc: "2.0",
                    error: { code: -32603, message: "Internal server error" },
                    id: null,
                });
            }
        }
    });
    app.get(mcpPath, async (req, res) => {
        const sessionId = req.header("mcp-session-id");
        if (!sessionId) {
            jsonRpcError(res, "Bad Request: missing mcp-session-id.");
            return;
        }
        const transport = transports.get(sessionId);
        if (!transport) {
            jsonRpcError(res, "Bad Request: invalid mcp-session-id.");
            return;
        }
        await transport.handleRequest(req, res);
    });
    app.delete(mcpPath, async (req, res) => {
        const sessionId = req.header("mcp-session-id");
        if (!sessionId) {
            jsonRpcError(res, "Bad Request: missing mcp-session-id.");
            return;
        }
        const transport = transports.get(sessionId);
        if (!transport) {
            jsonRpcError(res, "Bad Request: invalid mcp-session-id.");
            return;
        }
        await transport.handleRequest(req, res);
    });
    return app;
}
async function main() {
    registerPostgresPoolShutdownHooks();
    registerOuroborosPoolShutdownHooks();
    const app = await createMcpHttpApp();
    const grpcPromise = maybeStartGrpcMcpServer({
        createMcpServer: () => createRegisteredMcpServer(),
    });
    app.listen(PORT, () => {
        const path = process.env.MCP_PATH?.trim() || DEFAULT_MCP_PATH;
        console.error(`[clawql-mcp-http] Streamable HTTP MCP listening on http://0.0.0.0:${PORT}${path}`);
    });
    const grpc = await grpcPromise;
    if (grpc) {
        const refl = grpc.reflectionEnabled ? " reflection=on" : "";
        console.error(`[clawql-mcp-http] gRPC listening on ${grpc.address} (mcp-grpc-transport ${grpc.version}; grpc.health.v1.Health, model_context_protocol.Mcp, mcp.transport.v1.Mcp.Session${refl})`);
    }
}
main().catch((err) => {
    console.error("[clawql-mcp-http] Fatal startup error:", err);
    process.exit(1);
});
