#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as fsSync from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { FileSearchService } from "./services/search/FileSearchService.js";
import { OramaSearchService } from "./services/search/OramaSearchService.js";
import { getAllFiles } from "./utils/fs-helper.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Directory where endpoint definitions are stored.
// Priority:
// 1. Environment variable ENDPOINTS_DIR
// 2. Packaged: dist/../endpoints (relative to bundled index.js)
// 3. Local Dev: dist/../../endpoints (relative to mcp-server/src)
const PACKAGED_ENDPOINTS_DIR = path.resolve(__dirname, "../endpoints");
const LOCAL_ENDPOINTS_DIR = path.resolve(__dirname, "../../endpoints");
const ENDPOINTS_DIR = process.env.ENDPOINTS_DIR ||
    (fsSync.existsSync(PACKAGED_ENDPOINTS_DIR) ? PACKAGED_ENDPOINTS_DIR : LOCAL_ENDPOINTS_DIR);
const DATA_DIR = path.resolve(__dirname, "../data");
const INDEX_PATH = path.join(DATA_DIR, "search-index.oxy");
class ApiDocsServer {
    server;
    searchService;
    constructor() {
        this.server = new Server({
            name: "api-docs-mcp",
            version: "1.1.60",
        }, {
            capabilities: {
                resources: {},
                tools: {},
            },
        });
        // Initialize Search Service
        const forcedMode = process.env.SEARCH_MODE;
        const hasIndex = fsSync.existsSync(INDEX_PATH);
        const debugMode = process.env.DEBUG_MODE === 'true';
        this.debugMode = debugMode;
        if (debugMode) {
            console.error(`[Config] DEBUG MODE enabled - scores will be included in results`);
        }
        if (forcedMode === 'file') {
            console.error(`[Config] Forced Search Mode: FILE`);
            this.searchService = new FileSearchService(ENDPOINTS_DIR, debugMode);
        }
        else if (forcedMode === 'orama') {
            console.error(`[Config] Forced Search Mode: ORAMA (Index: ${INDEX_PATH})`);
            if (!hasIndex) {
                console.warn(`[Warning] Orama index missing in forced mode.`);
            }
            this.searchService = new OramaSearchService(INDEX_PATH, debugMode);
        }
        else {
            // Auto-detect
            if (hasIndex) {
                console.error(`[Config] Auto-detected Orama Search Index: ${INDEX_PATH}`);
                this.searchService = new OramaSearchService(INDEX_PATH, debugMode);
            }
            else {
                console.error(`[Config] Search index not found. Fallback to FileSearchService.`);
                this.searchService = new FileSearchService(ENDPOINTS_DIR, debugMode);
            }
        }
        this.setupResourceHandlers();
        this.setupToolHandlers();
        // Error handling
        this.server.onerror = (error) => console.error("[MCP Error]", error);
        process.on("SIGINT", async () => {
            await this.server.close();
            process.exit(0);
        });
    }
    debugMode;
    async setupResourceHandlers() {
        // List available resources (files in endpoints directory)
        this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
            try {
                const files = await getAllFiles(ENDPOINTS_DIR);
                const resources = files.map((filePath) => {
                    const relativePath = path.relative(ENDPOINTS_DIR, filePath);
                    // URI scheme: endpoints://v1/system.json
                    const uri = `endpoints://${relativePath}`;
                    return {
                        uri,
                        name: relativePath,
                        mimeType: "application/json",
                        description: `API Documentation for ${relativePath}`
                    };
                });
                return {
                    resources,
                };
            }
            catch (error) {
                console.error("Error listing resources:", error);
                return { resources: [] };
            }
        });
        // Read specific resource
        this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
            const uri = request.params.uri;
            if (!uri.startsWith("endpoints://")) {
                throw new McpError(ErrorCode.InvalidRequest, "Unknown URI scheme");
            }
            const relativePath = uri.replace("endpoints://", "");
            const finalPath = path.join(ENDPOINTS_DIR, relativePath);
            // Security check: prevent directory traversal
            if (!finalPath.startsWith(ENDPOINTS_DIR)) {
                throw new McpError(ErrorCode.InvalidRequest, "Access denied");
            }
            try {
                const content = await fs.readFile(finalPath, "utf-8");
                return {
                    contents: [
                        {
                            uri,
                            mimeType: "application/json",
                            text: content,
                        },
                    ],
                };
            }
            catch (_error) {
                throw new McpError(ErrorCode.InvalidRequest, `File not found: ${relativePath}`);
            }
        });
    }
    async setupToolHandlers() {
        this.server.setRequestHandler(ListToolsRequestSchema, async () => {
            return {
                tools: [
                    {
                        name: "search_apis",
                        description: "Search for API endpoints by keyword across all documentation files. Returns summarized results.",
                        inputSchema: {
                            type: "object",
                            properties: {
                                query: {
                                    type: "string",
                                    description: "Search query (e.g. 'user login', 'create project').",
                                },
                                method: {
                                    type: "string",
                                    description: "Filter by HTTP method (e.g. 'GET', 'POST'). Optional.",
                                },
                                version: {
                                    type: "string",
                                    description: "Filter by API version (e.g. 'v1', 'v2'). Optional.",
                                },
                                page: {
                                    type: "integer",
                                    description: "Page number for pagination (default: 1).",
                                },
                                path: {
                                    type: "string",
                                    description: "Optional explicit path to search for (e.g. '/api/v1/projects'). If provided, performs exact/contains match instead of semantic search.",
                                },
                            },
                            required: [],
                        },
                    },
                    {
                        name: "list_all_apis",
                        description: "List all available API endpoints (method + path). No parameters required. Use this to get a complete overview of the API surface.",
                        inputSchema: {
                            type: "object",
                            properties: {},
                            required: [],
                        },
                    },
                    {
                        name: "get_api_details",
                        description: "Get full details including schema and parameters for a specific API endpoint.",
                        inputSchema: {
                            type: "object",
                            properties: {
                                path: {
                                    type: "string",
                                    description: "API Path (e.g. '/api/v1/projects')",
                                },
                                method: {
                                    type: "string",
                                    description: "HTTP Method (e.g. 'POST', 'GET')",
                                },
                            },
                            required: ["path"],
                        },
                    },
                    {
                        name: "get_changes",
                        description: "Get API endpoint changelog. Shows what endpoints/parameters were added, removed, or modified across versions. Includes enum changes when available.",
                        inputSchema: {
                            type: "object",
                            properties: {
                                since_version: {
                                    type: "string",
                                    description: "Return all changes from this version onwards (e.g. '1.1.40'). Omit to get the latest version only.",
                                },
                                version: {
                                    type: "string",
                                    description: "Return changes for this exact version only (e.g. '1.1.49').",
                                },
                            },
                            required: [],
                        },
                    },
                    {
                        name: "get_enums",
                        description: "Get all Java enum definitions from the API codebase. Returns enum names and their constant values.",
                        inputSchema: {
                            type: "object",
                            properties: {
                                name: {
                                    type: "string",
                                    description: "Filter by enum name (partial match, case-insensitive). Omit to get all enums.",
                                },
                            },
                            required: [],
                        },
                    },
                ],
            };
        });
        this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
            if (request.params.name === "search_apis") {
                const query = request.params.arguments?.query ? String(request.params.arguments?.query).toLowerCase() : "";
                const method = request.params.arguments?.method ? String(request.params.arguments?.method).toUpperCase() : undefined;
                const version = request.params.arguments?.version ? String(request.params.arguments?.version).toLowerCase() : undefined;
                const page = request.params.arguments?.page ? Number(request.params.arguments?.page) : 1;
                const pathArg = request.params.arguments?.path ? String(request.params.arguments?.path) : undefined;
                if (!query) {
                    const categories = await this.searchService.listCategories();
                    return {
                        content: [
                            {
                                type: "text",
                                text: JSON.stringify(categories, null, 2),
                            },
                        ],
                    };
                }
                const results = await this.searchService.search(query, method, version, page, pathArg);
                // [Final Plan] Minimal output by default
                let finalOutput = results;
                if (!this.debugMode) {
                    finalOutput = {
                        results: results.results.map((r) => ({
                            method: r.method,
                            path: r.path,
                            summary: r.summary
                        })),
                        meta: {
                            total: results.meta.total,
                            // page/totalPages omitted in minimal if deemed noise, but keep for now
                            page: results.meta.page,
                            totalPages: results.meta.totalPages
                        },
                        warning: results.warning
                    };
                }
                else {
                    // Debug mode: Add common debug fields to the results
                    // OramaService already returns them in 'results', we just ensure they pass through.
                    // The 'r' object from search() already contains score, resourceType, etc. if debugMode was passed to Service.
                    // Wait, we passed debugMode to Service constructor. So results SHOULD contain them.
                    // Just need to make sure we don't trim them.
                }
                return {
                    content: [
                        {
                            type: "text",
                            text: JSON.stringify(finalOutput, null, 2),
                        },
                    ],
                };
            }
            if (request.params.name === "list_all_apis") {
                const result = await this.searchService.listAll();
                return {
                    content: [
                        {
                            type: "text",
                            text: JSON.stringify(result, null, 2),
                        },
                    ],
                };
            }
            if (request.params.name === "get_api_details") {
                const apiPath = String(request.params.arguments?.path);
                const method = request.params.arguments?.method ? String(request.params.arguments?.method).toUpperCase() : undefined;
                const result = await this.searchService.getDetails(apiPath, method);
                if (!result) {
                    return {
                        isError: true,
                        content: [
                            {
                                type: "text",
                                text: `API endpoint not found: ${method ? method + " " : ""}${apiPath}`,
                            },
                        ],
                    };
                }
                // [Final Plan] Details with Metadata in Debug Mode
                let finalContent = result;
                // Result is { content: any, metadata: any } due to interface update
                if (!this.debugMode) {
                    // Standard: Return only content (OpenAPI JSON)
                    finalContent = result.content;
                }
                else {
                    // Debug: Merge metadata into content (or wrap it)
                    // Merging is cleaner for "Everything in one view"
                    finalContent = {
                        ...result.content,
                        ...result.metadata // score, resourceType, etc.
                    };
                }
                return {
                    content: [
                        {
                            type: "text",
                            text: JSON.stringify(finalContent, null, 2),
                        },
                    ],
                };
            }
            if (request.params.name === "get_changes") {
                const changelogDir = path.join(DATA_DIR, "changelog");
                try {
                    const parseVer = (v) => v.split(".").map(Number);
                    const cmpVer = (a, b) => {
                        for (let i = 0; i < 3; i++) {
                            if ((a[i] || 0) !== (b[i] || 0))
                                return (a[i] || 0) - (b[i] || 0);
                        }
                        return 0;
                    };
                    // Read all version files from changelog/
                    const files = await fs.readdir(changelogDir).catch(() => []);
                    const changelog = {};
                    for (const file of files) {
                        if (!file.endsWith(".json"))
                            continue;
                        const ver = file.replace(".json", "");
                        const raw = await fs.readFile(path.join(changelogDir, file), "utf-8");
                        changelog[ver] = JSON.parse(raw);
                    }
                    const sinceVersion = request.params.arguments?.since_version
                        ? String(request.params.arguments.since_version) : undefined;
                    const exactVersion = request.params.arguments?.version
                        ? String(request.params.arguments.version) : undefined;
                    let result;
                    if (exactVersion) {
                        result = changelog[exactVersion] ? { [exactVersion]: changelog[exactVersion] } : {};
                    }
                    else if (sinceVersion) {
                        const sinceArr = parseVer(sinceVersion);
                        result = Object.fromEntries(Object.entries(changelog)
                            .filter(([v]) => v !== "next" && cmpVer(parseVer(v), sinceArr) >= 0)
                            .sort(([a], [b]) => cmpVer(parseVer(a), parseVer(b))));
                        // include "next" if present
                        if (changelog["next"])
                            result["next"] = changelog["next"];
                    }
                    else {
                        // Latest numbered version only (+ "next" if present)
                        const versions = Object.keys(changelog)
                            .filter(v => v !== "next")
                            .sort((a, b) => cmpVer(parseVer(b), parseVer(a)));
                        result = versions.length > 0 ? { [versions[0]]: changelog[versions[0]] } : {};
                        if (changelog["next"])
                            result["next"] = changelog["next"];
                    }
                    return {
                        content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
                    };
                }
                catch (_error) {
                    return {
                        content: [{ type: "text", text: JSON.stringify({ message: "No changelog available yet." }) }],
                    };
                }
            }
            if (request.params.name === "get_enums") {
                const enumsPath = path.join(DATA_DIR, "enums.json");
                try {
                    const raw = await fs.readFile(enumsPath, "utf-8");
                    const data = JSON.parse(raw);
                    const nameFilter = request.params.arguments?.name
                        ? String(request.params.arguments.name).toLowerCase() : undefined;
                    let enums = data.enums ?? {};
                    if (nameFilter) {
                        enums = Object.fromEntries(Object.entries(enums).filter(([k]) => k.toLowerCase().includes(nameFilter)));
                    }
                    return {
                        content: [{
                                type: "text",
                                text: JSON.stringify({
                                    generated_at: data.generated_at,
                                    count: Object.keys(enums).length,
                                    enums,
                                }, null, 2),
                            }],
                    };
                }
                catch (_error) {
                    return {
                        content: [{ type: "text", text: JSON.stringify({ message: "No enum data available. Run collect_enums.py first." }) }],
                    };
                }
            }
            throw new McpError(ErrorCode.MethodNotFound, "Tool not found");
        });
    }
    async run() {
        const transport = new StdioServerTransport();
        await this.server.connect(transport);
        console.error("MCP Server running on stdio");
    }
}
const server = new ApiDocsServer();
server.run().catch(console.error);
