/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import fs from 'node:fs/promises';
import path from 'node:path';
import { Storage, coreEvents } from '@google/gemini-cli-core';
/**
 * Normalize a server ID to canonical lowercase form.
 */
export function normalizeServerId(serverId) {
    return serverId.toLowerCase().trim();
}
/**
 * Check if a server ID is in a settings list (with backward compatibility).
 * Handles case-insensitive matching and plain name fallback for ext: servers.
 */
export function isInSettingsList(serverId, list) {
    const normalizedId = normalizeServerId(serverId);
    const normalizedList = list.map(normalizeServerId);
    // Exact canonical match
    if (normalizedList.includes(normalizedId)) {
        return { found: true };
    }
    // Backward compat: for ext: servers, check if plain name matches
    if (normalizedId.startsWith('ext:')) {
        const plainName = normalizedId.split(':').pop();
        if (plainName && normalizedList.includes(plainName)) {
            return {
                found: true,
                deprecationWarning: `Settings reference '${plainName}' matches extension server '${serverId}'. ` +
                    `Update your settings to use the full identifier '${serverId}' instead.`,
            };
        }
    }
    return { found: false };
}
/**
 * Single source of truth for whether a server can be loaded.
 * Used by: isAllowedMcpServer(), connectServer(), CLI handlers, slash handlers.
 *
 * Uses callbacks instead of direct enablementManager reference to keep
 * packages/core independent of packages/cli.
 */
export async function canLoadServer(serverId, config) {
    const normalizedId = normalizeServerId(serverId);
    // 1. Admin kill switch
    if (!config.adminMcpEnabled) {
        return {
            allowed: false,
            reason: 'MCP servers are disabled by administrator. Check admin settings or contact your admin.',
            blockType: 'admin',
        };
    }
    // 2. Allowlist check
    if (config.allowedList && config.allowedList.length > 0) {
        const { found, deprecationWarning } = isInSettingsList(normalizedId, config.allowedList);
        if (deprecationWarning) {
            coreEvents.emitFeedback('warning', deprecationWarning);
        }
        if (!found) {
            return {
                allowed: false,
                reason: `Server '${serverId}' is not in mcp.allowed list. Add it to settings.json mcp.allowed array to enable.`,
                blockType: 'allowlist',
            };
        }
    }
    // 3. Excludelist check
    if (config.excludedList) {
        const { found, deprecationWarning } = isInSettingsList(normalizedId, config.excludedList);
        if (deprecationWarning) {
            coreEvents.emitFeedback('warning', deprecationWarning);
        }
        if (found) {
            return {
                allowed: false,
                reason: `Server '${serverId}' is blocked by mcp.excluded. Remove it from settings.json mcp.excluded array to enable.`,
                blockType: 'excludelist',
            };
        }
    }
    // 4. Session disable check (before file-based enablement)
    if (config.enablement?.isSessionDisabled(normalizedId)) {
        return {
            allowed: false,
            reason: `Server '${serverId}' is disabled for this session. Run 'gemini mcp enable ${serverId} --session' to clear.`,
            blockType: 'session',
        };
    }
    // 5. File-based enablement check
    if (config.enablement &&
        !(await config.enablement.isFileEnabled(normalizedId))) {
        return {
            allowed: false,
            reason: `Server '${serverId}' is disabled. Run 'gemini mcp enable ${serverId}' to enable.`,
            blockType: 'enablement',
        };
    }
    return { allowed: true };
}
const MCP_ENABLEMENT_FILENAME = 'mcp-server-enablement.json';
/**
 * McpServerEnablementManager
 *
 * Manages the enabled/disabled state of MCP servers.
 * Uses a simplified format compared to ExtensionEnablementManager.
 * Supports both persistent (file) and session-only (in-memory) states.
 *
 * NOTE: Use getInstance() to get the singleton instance. This ensures
 * session state (sessionDisabled Set) is shared across all code paths.
 */
export class McpServerEnablementManager {
    static instance = null;
    configFilePath;
    configDir;
    sessionDisabled = new Set();
    /**
     * Get the singleton instance.
     */
    static getInstance() {
        if (!McpServerEnablementManager.instance) {
            McpServerEnablementManager.instance = new McpServerEnablementManager();
        }
        return McpServerEnablementManager.instance;
    }
    /**
     * Reset the singleton instance (for testing only).
     */
    static resetInstance() {
        McpServerEnablementManager.instance = null;
    }
    constructor() {
        this.configDir = Storage.getGlobalGeminiDir();
        this.configFilePath = path.join(this.configDir, MCP_ENABLEMENT_FILENAME);
    }
    /**
     * Check if server is enabled in FILE (persistent config only).
     * Does NOT include session state.
     */
    async isFileEnabled(serverName) {
        const config = await this.readConfig();
        const state = config[normalizeServerId(serverName)];
        return state?.enabled ?? true;
    }
    /**
     * Check if server is session-disabled.
     */
    isSessionDisabled(serverName) {
        return this.sessionDisabled.has(normalizeServerId(serverName));
    }
    /**
     * Check effective enabled state (combines file + session).
     * Convenience method; canLoadServer() uses separate callbacks for granular blockType.
     */
    async isEffectivelyEnabled(serverName) {
        if (this.isSessionDisabled(serverName)) {
            return false;
        }
        return this.isFileEnabled(serverName);
    }
    /**
     * Enable a server persistently.
     * Removes the server from config file (defaults to enabled).
     */
    async enable(serverName) {
        const normalizedName = normalizeServerId(serverName);
        const config = await this.readConfig();
        if (normalizedName in config) {
            delete config[normalizedName];
            await this.writeConfig(config);
        }
    }
    /**
     * Disable a server persistently.
     * Adds server to config file with enabled: false.
     */
    async disable(serverName) {
        const config = await this.readConfig();
        config[normalizeServerId(serverName)] = { enabled: false };
        await this.writeConfig(config);
    }
    /**
     * Disable a server for current session only (in-memory).
     */
    disableForSession(serverName) {
        this.sessionDisabled.add(normalizeServerId(serverName));
    }
    /**
     * Clear session disable for a server.
     */
    clearSessionDisable(serverName) {
        this.sessionDisabled.delete(normalizeServerId(serverName));
    }
    /**
     * Get display state for a specific server (for UI).
     */
    async getDisplayState(serverName) {
        const isSessionDisabled = this.isSessionDisabled(serverName);
        const isPersistentDisabled = !(await this.isFileEnabled(serverName));
        return {
            enabled: !isSessionDisabled && !isPersistentDisabled,
            isSessionDisabled,
            isPersistentDisabled,
        };
    }
    /**
     * Get all display states (for UI listing).
     */
    async getAllDisplayStates(serverIds) {
        const result = {};
        for (const serverId of serverIds) {
            result[normalizeServerId(serverId)] =
                await this.getDisplayState(serverId);
        }
        return result;
    }
    /**
     * Get enablement callbacks for passing to core.
     */
    getEnablementCallbacks() {
        return {
            isSessionDisabled: (id) => this.isSessionDisabled(id),
            isFileEnabled: (id) => this.isFileEnabled(id),
        };
    }
    /**
     * Auto-enable any disabled MCP servers by name.
     * Returns server names that were actually re-enabled.
     */
    async autoEnableServers(serverNames) {
        const enabledServers = [];
        for (const serverName of serverNames) {
            const normalizedName = normalizeServerId(serverName);
            const state = await this.getDisplayState(normalizedName);
            let wasDisabled = false;
            if (state.isPersistentDisabled) {
                await this.enable(normalizedName);
                wasDisabled = true;
            }
            if (state.isSessionDisabled) {
                this.clearSessionDisable(normalizedName);
                wasDisabled = true;
            }
            if (wasDisabled) {
                enabledServers.push(serverName);
            }
        }
        return enabledServers;
    }
    /**
     * Read config from file asynchronously.
     */
    async readConfig() {
        try {
            const content = await fs.readFile(this.configFilePath, 'utf-8');
            return JSON.parse(content);
        }
        catch (error) {
            if (error instanceof Error &&
                'code' in error &&
                error.code === 'ENOENT') {
                return {};
            }
            coreEvents.emitFeedback('error', 'Failed to read MCP server enablement config.', error);
            return {};
        }
    }
    /**
     * Write config to file asynchronously.
     */
    async writeConfig(config) {
        await fs.mkdir(this.configDir, { recursive: true });
        await fs.writeFile(this.configFilePath, JSON.stringify(config, null, 2));
    }
}
//# sourceMappingURL=mcpServerEnablement.js.map