"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MCP_LIST_SCHEMA_VERSION = exports.MCP_LIST_KIND = void 0;
exports.listAvailableMcpServers = listAvailableMcpServers;
// biome-ignore lint/nursery/noExcessiveLinesPerFile: MCP inventory keeps worker-specific collection paths and shared result materialization together.
const promises_1 = require("node:fs/promises");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const environment_1 = require("../auth-profiles/environment");
const store_1 = require("../auth-profiles/store");
const requirement_exclusions_1 = require("../catalog/requirement-exclusions");
const app_server_client_1 = require("../codex/app-server-client");
const command_1 = require("../core/command");
const fs_errors_1 = require("../core/fs-errors");
const paths_1 = require("../core/paths");
const ref_1 = require("./ref");
exports.MCP_LIST_KIND = "alpaca.mcpList";
exports.MCP_LIST_SCHEMA_VERSION = 2;
const ANSI_ESCAPE = String.fromCharCode(27);
async function listAvailableMcpServers(input) {
    const workers = [...new Set(input.workers)].sort();
    const configuredWorkerSet = new Set(input.configuredWorkers);
    const env = input.env ?? process.env;
    let authProfiles = input.authProfiles;
    if (authProfiles === undefined) {
        const authProfileFile = await (0, store_1.loadAuthProfiles)(input.homeDirectory);
        authProfiles = authProfileFile.profiles;
    }
    const run = input.run ?? command_1.runCommand;
    const candidates = [];
    const diagnostics = [];
    if (workers.includes("codex")) {
        const codexResult = await collectCodexMcpCandidates({
            repoRoot: input.repoRoot,
            authProfiles,
            env,
            workerConfigured: configuredWorkerSet.has("codex"),
        });
        candidates.push(...codexResult.candidates);
        diagnostics.push(...codexResult.diagnostics);
    }
    if (workers.includes("claude")) {
        const claudeResult = await collectClaudeMcpCandidates({
            repoRoot: input.repoRoot,
            authProfiles,
            env,
            run,
            workerConfigured: configuredWorkerSet.has("claude"),
        });
        candidates.push(...claudeResult.candidates);
        diagnostics.push(...claudeResult.diagnostics);
    }
    for (const worker of workers) {
        if (worker === "codex" || worker === "claude") {
            continue;
        }
        const customResult = await collectPiProjectMcpCandidates({
            repoRoot: input.repoRoot,
            worker,
            workerConfigured: configuredWorkerSet.has(worker),
            homeDirectory: input.homeDirectory,
        });
        candidates.push(...customResult.candidates);
        diagnostics.push(...customResult.diagnostics);
    }
    const exclusions = await (0, requirement_exclusions_1.loadEffectiveRequirementExclusions)({
        repoRoot: input.repoRoot,
        ...(input.homeDirectory === undefined
            ? {}
            : { homeDirectory: input.homeDirectory }),
    });
    const allItems = materializeMcpItems({
        candidates,
        exclusions,
    });
    const items = input.includeExcluded === true
        ? allItems
        : allItems.filter((item) => item.excludedFromRequires === undefined);
    const hiddenItems = allItems.filter((item) => item.excludedFromRequires !== undefined);
    const exclusionHint = input.includeExcluded === true
        ? undefined
        : (0, requirement_exclusions_1.buildRequirementExclusionHintForVisibleResult)({
            visibleCount: items.length,
            hiddenMatches: hiddenItems.flatMap((item) => item.excludedFromRequires === undefined
                ? []
                : [item.excludedFromRequires]),
        });
    return {
        kind: exports.MCP_LIST_KIND,
        schemaVersion: exports.MCP_LIST_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        workers,
        items,
        diagnostics,
        ...(exclusionHint === undefined ? {} : { exclusionHint }),
    };
}
async function collectCodexMcpCandidates(input) {
    const appServerResult = await collectCodexAppServerMcpCandidates(input);
    return {
        candidates: appServerResult.candidates,
        diagnostics: appServerResult.diagnostics,
    };
}
async function collectCodexAppServerMcpCandidates(input) {
    const candidates = [];
    const diagnostics = [];
    let inspected = false;
    for (const env of collectWorkerCommandEnvironments({
        worker: "codex",
        authProfiles: input.authProfiles,
        env: input.env,
    })) {
        try {
            const statuses = await (0, app_server_client_1.listCodexMcpServerStatuses)({
                cwd: input.repoRoot,
                env,
            });
            inspected = true;
            candidates.push(...statuses.flatMap((status) => {
                if (!isUsableCodexMcpAuthStatus(status.authStatus)) {
                    diagnostics.push({
                        worker: "codex",
                        message: `Codex MCP server "${status.name}" is not usable: authStatus=${status.authStatus}.`,
                    });
                    return [];
                }
                const tools = Object.keys(status.tools).sort();
                if (tools.length === 0) {
                    diagnostics.push({
                        worker: "codex",
                        message: `Codex MCP server "${status.name}" exposed no tools and is not usable by the worker.`,
                    });
                }
                return [
                    {
                        serverName: status.name,
                        tools,
                        toolsKnown: true,
                        usable: tools.length > 0,
                        worker: "codex",
                        source: "codex-cli",
                        workerConfigured: input.workerConfigured,
                    },
                ];
            }));
        }
        catch (error) {
            diagnostics.push({
                worker: "codex",
                message: error instanceof Error
                    ? error.message
                    : `Unable to inspect Codex MCP status: ${String(error)}`,
            });
        }
    }
    return {
        candidates,
        diagnostics,
        inspected,
    };
}
function isUsableCodexMcpAuthStatus(authStatus) {
    const normalized = authStatus.trim().toLowerCase();
    if (normalized.length === 0) {
        return true;
    }
    return ![
        "auth_required",
        "auth-required",
        "denied",
        "error",
        "expired",
        "fail",
        "invalid",
        "missing",
        "needs_auth",
        "needs-auth",
        "requires_auth",
        "requires-auth",
        "unauth",
    ].some((token) => normalized.includes(token));
}
async function collectClaudeProjectMcpCandidates(input) {
    let config;
    try {
        config = await readOptionalJson(node_path_1.default.join(input.repoRoot, ".mcp.json"));
    }
    catch (error) {
        return {
            candidates: [],
            diagnostics: [
                {
                    worker: "claude",
                    message: error instanceof Error
                        ? `Unable to inspect Claude MCP configuration: ${error.message}`
                        : `Unable to inspect Claude MCP configuration: ${String(error)}`,
                },
            ],
        };
    }
    const serverNames = extractMcpServerNamesFromConfig(config);
    return {
        candidates: serverNames.map(({ serverName, tools }) => ({
            serverName,
            tools,
            toolsKnown: true,
            usable: false,
            worker: "claude",
            source: "claude-project",
            workerConfigured: input.workerConfigured,
        })),
        diagnostics: [],
    };
}
async function collectPiProjectMcpCandidates(input) {
    const configPaths = [
        node_path_1.default.join(input.repoRoot, ".mcp.json"),
        node_path_1.default.join(input.repoRoot, ".pi", "mcp.json"),
    ];
    const candidates = [];
    const diagnostics = [];
    const cacheResult = await readPiMcpMetadataCacheTools({
        worker: input.worker,
        homeDirectory: input.homeDirectory,
    });
    if (cacheResult.diagnostic !== null) {
        diagnostics.push({
            worker: input.worker,
            message: cacheResult.diagnostic,
        });
    }
    for (const configPath of configPaths) {
        let config;
        try {
            config = await readOptionalJson(configPath);
        }
        catch (error) {
            diagnostics.push({
                worker: input.worker,
                message: error instanceof Error
                    ? `Unable to inspect Pi MCP configuration: ${error.message}`
                    : `Unable to inspect Pi MCP configuration: ${String(error)}`,
            });
            continue;
        }
        candidates.push(...extractMcpServerNamesFromConfig(config, {
            skipDisabled: true,
        }).map(({ serverName, tools }) => {
            const cachedTools = cacheResult.toolsByServer.get(serverName);
            return {
                serverName,
                tools: [...new Set([...tools, ...(cachedTools ?? [])])].sort(),
                toolsKnown: tools.length > 0 || cachedTools !== undefined,
                usable: true,
                worker: input.worker,
                source: "pi-project",
                workerConfigured: input.workerConfigured,
            };
        }));
    }
    return { candidates, diagnostics };
}
async function readPiMcpMetadataCacheTools(input) {
    const cachePath = node_path_1.default.join((0, paths_1.getControlPlaneHomePath)(input.homeDirectory ?? node_os_1.default.homedir()), "pi-workers", input.worker, "mcp-cache.json");
    let cache;
    try {
        cache = await readOptionalJson(cachePath);
    }
    catch (error) {
        return {
            toolsByServer: new Map(),
            diagnostic: error instanceof Error
                ? `Unable to inspect Pi MCP metadata cache: ${error.message}`
                : `Unable to inspect Pi MCP metadata cache: ${String(error)}`,
        };
    }
    if (cache === null || typeof cache !== "object") {
        return { toolsByServer: new Map(), diagnostic: null };
    }
    const servers = Reflect.get(cache, "servers");
    if (servers === null ||
        typeof servers !== "object" ||
        Array.isArray(servers)) {
        return { toolsByServer: new Map(), diagnostic: null };
    }
    const toolsByServer = new Map();
    for (const [serverName, entry] of Object.entries(servers)) {
        if (entry === null || typeof entry !== "object") {
            continue;
        }
        toolsByServer.set(serverName, extractToolNames(entry));
    }
    return { toolsByServer, diagnostic: null };
}
async function collectClaudeMcpCandidates(input) {
    const [cliResult, projectResult] = await Promise.all([
        collectClaudeCliMcpCandidates(input),
        collectClaudeProjectMcpCandidates({
            repoRoot: input.repoRoot,
            workerConfigured: input.workerConfigured,
        }),
    ]);
    return {
        candidates: [...cliResult.candidates, ...projectResult.candidates],
        diagnostics: [...cliResult.diagnostics, ...projectResult.diagnostics],
    };
}
async function collectClaudeCliMcpCandidates(input) {
    const candidates = [];
    const diagnostics = [];
    const environments = collectWorkerCommandEnvironments({
        worker: "claude",
        authProfiles: input.authProfiles,
        env: input.env,
    }).filter((env) => hasClaudeConfigDir(env));
    for (const env of environments) {
        try {
            const inspectionCwd = node_os_1.default.tmpdir();
            const result = await input.run({
                command: "claude",
                args: ["mcp", "list"],
                cwd: inspectionCwd,
                env,
                allowNonZeroExit: true,
                timeoutMs: 30_000,
            });
            const entries = await Promise.all(parseClaudeMcpList(`${result.stdout}\n${result.stderr}`).map(async (entry) => mergeClaudeMcpDetails({
                ...input,
                cwd: inspectionCwd,
                env,
                entry,
            })));
            candidates.push(...entries.map((entry) => ({
                serverName: entry.serverName,
                tools: entry.tools,
                toolsKnown: true,
                usable: entry.usable,
                worker: "claude",
                source: "claude-cli",
                workerConfigured: input.workerConfigured,
            })));
        }
        catch (error) {
            diagnostics.push({
                worker: "claude",
                message: error instanceof Error
                    ? error.message
                    : `Unable to inspect Claude MCP configuration: ${String(error)}`,
            });
        }
    }
    return { candidates, diagnostics };
}
async function mergeClaudeMcpDetails(input) {
    const result = await input.run({
        command: "claude",
        args: ["mcp", "get", input.entry.serverName],
        cwd: input.cwd,
        env: input.env,
        allowNonZeroExit: true,
        timeoutMs: 30_000,
    });
    const details = parseClaudeMcpGet({
        stdout: `${result.stdout}\n${result.stderr}`,
    });
    return {
        serverName: input.entry.serverName,
        tools: [...new Set([...input.entry.tools, ...details.tools])].sort(),
        toolsKnown: true,
        usable: details.projectScoped
            ? false
            : (details.usable ?? input.entry.usable),
    };
}
function collectWorkerCommandEnvironments(input) {
    const environments = input.authProfiles
        .filter((profile) => (0, store_1.getAuthProfileProvider)(profile) === input.worker)
        .map((profile) => (0, environment_1.buildAuthProfileEnvironment)(profile, input.env))
        .filter((env) => env !== null);
    return environments.length > 0 ? environments : [input.env];
}
function hasClaudeConfigDir(env) {
    const claudeConfigDir = Reflect.get(env, "CLAUDE_CONFIG_DIR");
    return typeof claudeConfigDir === "string" && claudeConfigDir.trim() !== "";
}
async function readOptionalJson(filePath) {
    try {
        return JSON.parse(await (0, promises_1.readFile)(filePath, "utf8"));
    }
    catch (error) {
        if ((0, fs_errors_1.isMissingFileError)(error)) {
            return null;
        }
        throw error;
    }
}
function parseClaudeMcpList(stdout) {
    return stdout
        .split(/\r?\n/u)
        .map(stripAnsi)
        .map((line) => line.trim())
        .flatMap((line) => {
        const match = /^([^:\s][^:]*):\s*(.+)$/u.exec(line);
        if (match?.[1] === undefined || match[2] === undefined) {
            return [];
        }
        const serverName = match[1].trim();
        if ((0, ref_1.parseRequiredMcpRef)(serverName) === null) {
            return [];
        }
        const detail = match[2].trim();
        return [
            {
                serverName,
                tools: extractClaudeToolNames(detail),
                usable: isUsableClaudeMcpStatus(detail),
            },
        ];
    });
}
function parseClaudeMcpGet(input) {
    const lines = input.stdout
        .split(/\r?\n/u)
        .map(stripAnsi)
        .map((line) => line.trim())
        .filter((line) => line.length > 0);
    const scopeLine = lines.find((line) => /^scope:/iu.test(line));
    const statusLine = lines.find((line) => /^status:/iu.test(line));
    const tools = lines.flatMap(extractClaudeToolNames).sort();
    const projectScoped = scopeLine !== undefined &&
        /\bproject\b/iu.test(scopeLine.slice("scope:".length));
    return {
        tools,
        usable: projectScoped
            ? false
            : statusLine === undefined
                ? null
                : isUsableClaudeMcpStatus(statusLine.slice("status:".length)),
        projectScoped,
    };
}
function stripAnsi(value) {
    return value.replace(new RegExp(`${ANSI_ESCAPE}\\[[0-9;]*m`, "gu"), "");
}
function extractClaudeToolNames(value) {
    const toolsMatch = /\btools?\s*[:=]\s*([A-Za-z0-9_.:/,\s-]+)/iu.exec(value);
    if (toolsMatch?.[1] === undefined) {
        return [];
    }
    return toolsMatch[1]
        .split(/[, ]+/u)
        .map((tool) => tool.trim())
        .filter((tool) => tool.length > 0)
        .sort();
}
function isUsableClaudeMcpStatus(value) {
    const normalized = value.toLowerCase();
    if (normalized.includes("✗") || normalized.includes("failed")) {
        return false;
    }
    return normalized.includes("✓") || normalized.includes("connected");
}
function extractMcpServerNamesFromConfig(value, options = { skipDisabled: false }) {
    if (value === null || typeof value !== "object") {
        return [];
    }
    if (Array.isArray(value)) {
        return value.flatMap((item) => extractMcpServerNameFromArrayItem(item, options));
    }
    const object = value;
    for (const nested of [
        object.servers,
        object.items,
        object.configuredServers,
    ]) {
        if (Array.isArray(nested)) {
            return nested.flatMap((item) => extractMcpServerNameFromArrayItem(item, options));
        }
    }
    const mcpServers = object.mcpServers;
    if (mcpServers !== null &&
        typeof mcpServers === "object" &&
        !Array.isArray(mcpServers)) {
        return Object.entries(mcpServers)
            .filter(([, serverConfig]) => !options.skipDisabled || !isDisabledMcpServerItem(serverConfig))
            .map(([serverName, serverConfig]) => ({
            serverName,
            tools: extractToolNames(serverConfig),
        }));
    }
    return Object.entries(object)
        .filter(([, entry]) => entry !== null &&
        typeof entry === "object" &&
        (!options.skipDisabled || !isDisabledMcpServerItem(entry)))
        .map(([serverName, entry]) => ({
        serverName,
        tools: extractToolNames(entry),
    }));
}
function extractMcpServerNameFromArrayItem(value, options) {
    if (typeof value === "string") {
        return [{ serverName: value, tools: [] }];
    }
    if (value === null || typeof value !== "object") {
        return [];
    }
    if (options.skipDisabled && isDisabledMcpServerItem(value)) {
        return [];
    }
    const object = value;
    const name = object.name ?? object.serverName ?? object.id;
    return typeof name === "string"
        ? [{ serverName: name, tools: extractToolNames(value) }]
        : [];
}
function isDisabledMcpServerItem(value) {
    if (value === null || typeof value !== "object") {
        return false;
    }
    const object = value;
    const disabledReason = Reflect.get(value, "disabled_reason");
    return (object.enabled === false ||
        object.disabled === true ||
        object.status === "disabled" ||
        (typeof disabledReason === "string" && disabledReason.trim().length > 0) ||
        (typeof object.disabledReason === "string" &&
            object.disabledReason.trim().length > 0));
}
function extractToolNames(value) {
    if (value === null || typeof value !== "object") {
        return [];
    }
    const object = value;
    const tools = object.tools;
    if (!Array.isArray(tools)) {
        return [];
    }
    return tools
        .map((tool) => {
        if (typeof tool === "string") {
            return tool;
        }
        if (tool !== null && typeof tool === "object") {
            const name = tool.name;
            return typeof name === "string" ? name : null;
        }
        return null;
    })
        .filter((toolName) => toolName !== null)
        .sort();
}
function materializeMcpItems(input) {
    const byKey = new Map();
    for (const candidate of input.candidates) {
        const ref = candidate.serverName;
        if ((0, ref_1.parseRequiredMcpRef)(ref) === null) {
            continue;
        }
        const key = `${candidate.worker}\0${candidate.serverName}`;
        const existing = byKey.get(key);
        if (existing !== undefined) {
            existing.tools = [
                ...new Set([...existing.tools, ...candidate.tools]),
            ].sort();
            existing.toolsKnown = existing.toolsKnown || candidate.toolsKnown;
            existing.usable = existing.usable || candidate.usable;
            continue;
        }
        byKey.set(key, {
            ref,
            serverName: candidate.serverName,
            worker: candidate.worker,
            source: candidate.source,
            workerConfigured: candidate.workerConfigured,
            tools: [...new Set(candidate.tools)].sort(),
            toolsKnown: candidate.toolsKnown,
            usable: candidate.usable,
        });
    }
    return [...byKey.values()]
        .flatMap((item) => {
        const exclusion = (0, requirement_exclusions_1.findRequirementExclusionMatch)(input.exclusions, "mcp", {
            ref: `${item.worker}:${item.ref}`,
            id: item.serverName,
            source: item.source,
        }) ??
            (0, requirement_exclusions_1.findRequirementExclusionMatch)(input.exclusions, "mcp", {
                ref: item.ref,
                id: item.serverName,
                source: item.source,
            });
        return [
            {
                ...item,
                usable: item.usable && exclusion === null,
                ...(exclusion === null ? {} : { excludedFromRequires: exclusion }),
            },
        ];
    })
        .sort((left, right) => left.worker.localeCompare(right.worker) ||
        left.serverName.localeCompare(right.serverName));
}
