"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.isMcpToolsetLeasedToDialog = isMcpToolsetLeasedToDialog;
exports.releaseMcpToolsetLeaseForDialog = releaseMcpToolsetLeaseForDialog;
exports.getMcpRuntimeLeasesForDialog = getMcpRuntimeLeasesForDialog;
exports.withMcpCatalogClient = withMcpCatalogClient;
exports.getMcpDeclaredServerRuntimeStatuses = getMcpDeclaredServerRuntimeStatuses;
exports.startMcpSupervisor = startMcpSupervisor;
exports.stopMcpSupervisor = stopMcpSupervisor;
exports.requestMcpServerRestart = requestMcpServerRestart;
exports.requestMcpServerDisable = requestMcpServerDisable;
exports.requestMcpConfigReload = requestMcpConfigReload;
exports.buildHttpHeaders = buildHttpHeaders;
const time_1 = require("@longrun-ai/kernel/utils/time");
const fs = __importStar(require("fs"));
const node_crypto_1 = require("node:crypto");
const path = __importStar(require("path"));
const yaml_1 = __importStar(require("yaml"));
const log_1 = require("../log");
const persistence_1 = require("../persistence");
const problems_1 = require("../problems");
const rtws_1 = require("../rtws");
const work_language_1 = require("../runtime/work-language");
const tool_1 = require("../tool");
const tool_availability_updates_1 = require("../tool-availability-updates");
const spec_1 = require("../tools/manual/spec");
const registry_1 = require("../tools/registry");
const team_mgmt_mcp_manual_1 = require("../tools/team_mgmt-mcp-manual");
const config_1 = require("./config");
const manual_problems_1 = require("./manual-problems");
const resources_1 = require("./resources");
const sdk_client_1 = require("./sdk-client");
const server_runtime_1 = require("./server-runtime");
const tool_names_1 = require("./tool-names");
const log = (0, log_1.createLogger)('mcp/supervisor');
const MCP_YAML_PATH = path.join('.minds', 'mcp.yaml');
const MCP_TOOL_CALL_PROBLEM_PREFIX = 'mcp/tool_call_error/';
const serverStateById = new Map();
const toolOwnerByName = new Map();
const toolsetOwnerByName = new Map();
let declaredServerIdsInYamlOrder = [];
const declaredServerRuntimeCatalogById = new Map();
function makeLeaseReminderMeta(serverId) {
    return { kind: 'mcp_lease', serverId };
}
function isLeaseReminderMeta(value) {
    if (!isRecord(value) || Array.isArray(value))
        return false;
    if (value.kind !== 'mcp_lease')
        return false;
    return typeof value.serverId === 'string' && value.serverId.trim().length > 0;
}
function ensureLeaseReminder(dlg, serverId) {
    for (const r of dlg.reminders) {
        if (!r.owner)
            continue;
        if (r.owner.name !== 'mcpLease')
            continue;
        const meta = r.meta;
        if (!isLeaseReminderMeta(meta))
            continue;
        if (meta.serverId !== serverId)
            continue;
        return;
    }
    const owner = (0, registry_1.getReminderOwner)('mcpLease');
    const workLanguage = (0, work_language_1.getWorkLanguage)();
    const content = workLanguage === 'zh'
        ? [
            `已租用 MCP 工具集：${serverId}`,
            '',
            '该 MCP server 被视为非“真正无状态”；当前对话持有一个需要显式生命周期管理的底层进程/连接。',
        ].join('\n')
        : [
            `MCP toolset leased: ${serverId}`,
            '',
            `This MCP server is treated as non-stateless; the current dialog holds an underlying process/connection with explicit lifecycle management.`,
        ].join('\n');
    dlg.addReminder(content, owner, makeLeaseReminderMeta(serverId));
}
class McpServerDispatch {
    serverId;
    toolsetName;
    cfg;
    sharedRuntime;
    leasesByDialogKey = new Map();
    leaseInitByDialogKey = new Map();
    canceledLeaseDialogs = new Set();
    stopRequested = false;
    constructor(params) {
        this.serverId = params.serverId;
        this.toolsetName = params.toolsetName;
        this.cfg = params.cfg;
        this.sharedRuntime = params.sharedRuntime;
    }
    hasLeaseForDialog(dialogKey) {
        return this.leasesByDialogKey.has(dialogKey) || this.leaseInitByDialogKey.has(dialogKey);
    }
    releaseLeaseForDialog(dialogKey) {
        const init = this.leaseInitByDialogKey.get(dialogKey);
        if (init) {
            this.canceledLeaseDialogs.add(dialogKey);
            // Best-effort: allow the in-flight connect to complete, then immediately stop it.
            void init
                .then((rt) => rt.requestStop({ forceKillAfterMs: 3_000 }))
                .catch(() => {
                // ignore
            });
            this.leaseInitByDialogKey.delete(dialogKey);
        }
        const existing = this.leasesByDialogKey.get(dialogKey);
        if (!existing)
            return false;
        this.leasesByDialogKey.delete(dialogKey);
        existing.requestStop({ forceKillAfterMs: 3_000 });
        return true;
    }
    requestStop() {
        if (this.stopRequested)
            return;
        this.stopRequested = true;
        if (this.sharedRuntime) {
            this.sharedRuntime.requestStop({ forceKillAfterMs: 3_000 });
        }
        for (const rt of this.leasesByDialogKey.values()) {
            rt.requestStop({ forceKillAfterMs: 3_000 });
        }
        this.leasesByDialogKey.clear();
        for (const [dialogKey, init] of this.leaseInitByDialogKey.entries()) {
            this.canceledLeaseDialogs.add(dialogKey);
            void init
                .then((rt) => rt.requestStop({ forceKillAfterMs: 3_000 }))
                .catch(() => {
                // ignore
            });
        }
        this.leaseInitByDialogKey.clear();
    }
    async callToolForDialog(dlg, mcpToolName, args) {
        const serverId = this.serverId;
        const withActionableError = async (fn) => {
            try {
                const raw = await fn();
                const out = await materializeMcpToolCallOutput({
                    dlg,
                    serverId,
                    toolName: mcpToolName,
                    raw,
                });
                clearMcpToolCallProblem(serverId);
                return out;
            }
            catch (err) {
                const workLanguage = (0, work_language_1.getWorkLanguage)();
                const errorText = err instanceof Error ? err.message : String(err);
                upsertMcpToolCallProblem({
                    serverId,
                    toolName: mcpToolName,
                    errorText,
                });
                const msg = workLanguage === 'zh'
                    ? `MCP 工具调用失败（详见 Problems 面板）：${serverId}.${mcpToolName}: ${errorText}`
                    : `MCP tool call failed (see Problems panel): ${serverId}.${mcpToolName}: ${errorText}`;
                const wrapped = new Error(msg);
                // Attach the original error for debugging without relying on ErrorOptions typing.
                wrapped.cause = err;
                throw wrapped;
            }
        };
        if (this.cfg.truelyStateless) {
            const sharedRuntime = this.sharedRuntime;
            if (!sharedRuntime) {
                throw new Error(`MCP server '${this.serverId}' missing shared runtime`);
            }
            return await withActionableError(async () => await sharedRuntime.callToolRaw(mcpToolName, args));
        }
        const dialogKey = dlg.id.key();
        const existing = this.leasesByDialogKey.get(dialogKey);
        if (existing) {
            this.attachLeaseReminder(dlg);
            return await withActionableError(async () => await existing.callToolRaw(mcpToolName, args));
        }
        if (this.stopRequested) {
            const oneShot = await this.connectNewLeaseRuntime();
            this.attachLeaseReminder(dlg);
            try {
                return await withActionableError(async () => await oneShot.callToolRaw(mcpToolName, args));
            }
            finally {
                oneShot.requestStop({ forceKillAfterMs: 3_000 });
            }
        }
        let init = this.leaseInitByDialogKey.get(dialogKey);
        if (!init) {
            this.canceledLeaseDialogs.delete(dialogKey);
            init = (async () => {
                try {
                    const rt = await this.connectNewLeaseRuntime();
                    this.finalizeLeaseForDialog(dialogKey, rt);
                    return rt;
                }
                catch (err) {
                    this.leaseInitByDialogKey.delete(dialogKey);
                    this.canceledLeaseDialogs.delete(dialogKey);
                    throw err;
                }
            })();
            this.leaseInitByDialogKey.set(dialogKey, init);
        }
        const runtime = await init;
        this.attachLeaseReminder(dlg);
        return await withActionableError(async () => await runtime.callToolRaw(mcpToolName, args));
    }
    async connectNewLeaseRuntime() {
        const serverId = this.serverId;
        const client = this.cfg.transport === 'stdio'
            ? await sdk_client_1.McpSdkClient.connectStdio({
                serverId,
                command: this.cfg.command,
                args: this.cfg.args,
                env: buildChildEnv(this.cfg, serverId),
                cwd: await resolveStdioServerCwd(this.cfg, serverId),
            })
            : await sdk_client_1.McpSdkClient.connectStreamableHttp({
                serverId,
                url: this.cfg.url,
                headers: buildHttpHeaders(this.cfg, serverId),
                sessionId: this.cfg.sessionId,
            });
        const runtime = new server_runtime_1.McpServerRuntime({
            serverId: this.serverId,
            toolsetName: this.toolsetName,
            client,
        });
        return runtime;
    }
    finalizeLeaseForDialog(dialogKey, runtime) {
        this.leaseInitByDialogKey.delete(dialogKey);
        if (this.canceledLeaseDialogs.has(dialogKey)) {
            this.canceledLeaseDialogs.delete(dialogKey);
            runtime.requestStop({ forceKillAfterMs: 3_000 });
            return;
        }
        if (this.stopRequested) {
            runtime.requestStop({ forceKillAfterMs: 3_000 });
            return;
        }
        this.leasesByDialogKey.set(dialogKey, runtime);
        (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp_lease_acquired:${this.serverId}:${dialogKey}`);
    }
    attachLeaseReminder(dlg) {
        if (this.cfg.truelyStateless)
            return;
        ensureLeaseReminder(dlg, this.serverId);
    }
}
function isMcpToolsetLeasedToDialog(serverId, dialogKey) {
    const state = serverStateById.get(serverId);
    if (!state)
        return false;
    return state.dispatch.hasLeaseForDialog(dialogKey);
}
function releaseMcpToolsetLeaseForDialog(serverId, dialogKey) {
    const state = serverStateById.get(serverId);
    if (!state) {
        return { ok: false, errorText: `MCP server '${serverId}' is not loaded` };
    }
    if (state.cfg.truelyStateless) {
        return { ok: true, released: false };
    }
    const released = state.dispatch.releaseLeaseForDialog(dialogKey);
    return { ok: true, released };
}
function getMcpRuntimeLeasesForDialog(dialogKey) {
    const leases = [];
    for (const [serverId, state] of serverStateById.entries()) {
        if (state.cfg.truelyStateless) {
            continue;
        }
        if (!state.dispatch.hasLeaseForDialog(dialogKey)) {
            continue;
        }
        leases.push({
            serverId,
            transport: state.cfg.transport,
        });
    }
    leases.sort((a, b) => a.serverId.localeCompare(b.serverId));
    return leases;
}
async function withMcpCatalogClient(serverId, fn) {
    const state = serverStateById.get(serverId);
    if (!state) {
        throw new Error(`MCP server '${serverId}' is not loaded`);
    }
    const client = state.cfg.transport === 'stdio'
        ? await sdk_client_1.McpSdkClient.connectStdio({
            serverId,
            command: state.cfg.command,
            args: state.cfg.args,
            env: buildChildEnv(state.cfg, serverId),
            cwd: await resolveStdioServerCwd(state.cfg, serverId),
        })
        : await sdk_client_1.McpSdkClient.connectStreamableHttp({
            serverId,
            url: state.cfg.url,
            headers: buildHttpHeaders(state.cfg, serverId),
            sessionId: state.cfg.sessionId,
        });
    try {
        return await fn(client);
    }
    finally {
        await client.close();
    }
}
function getMcpDeclaredServerRuntimeStatuses() {
    return declaredServerIdsInYamlOrder.map((serverId) => {
        const catalogEntry = declaredServerRuntimeCatalogById.get(serverId);
        if (catalogEntry?.configErrorText) {
            return {
                serverId,
                transport: catalogEntry.transport,
                status: 'config_invalid',
                errorText: catalogEntry.configErrorText,
            };
        }
        if (catalogEntry?.disabled) {
            return {
                serverId,
                transport: catalogEntry.transport,
                status: 'disabled',
            };
        }
        if (serverStateById.has(serverId)) {
            return {
                serverId,
                transport: catalogEntry?.transport ?? 'unknown',
                status: 'loaded',
            };
        }
        return {
            serverId,
            transport: catalogEntry?.transport ?? 'unknown',
            status: 'temporarily_unavailable',
            errorText: catalogEntry?.runtimeErrorText,
        };
    });
}
let mindsDirWatcher;
let workspaceWatcher;
let pollTimer;
let debounceTimer;
let lastSeenMcpYamlSig;
let reloadChain = Promise.resolve();
let supervisorStarted = false;
function startMcpSupervisor() {
    if (supervisorStarted)
        return;
    supervisorStarted = true;
    reloadChain = reloadChain
        .then(async () => await reloadNow('startup'))
        .catch((err) => {
        log.warn('MCP initial load failed', err);
    });
    // Initialize signature baseline (best-effort). This reduces false negatives in polling on filesystems
    // with coarse mtime resolution.
    reloadChain = reloadChain
        .then(async () => {
        const sig = await readMcpYamlSig();
        lastSeenMcpYamlSig = sig;
    })
        .catch((err) => {
        log.warn('MCP initial signature read failed', err);
    });
    // Best-effort file watch (fast feedback). `.minds/` may be wiped/recreated during a dev session,
    // so watcher setup must be resilient and re-attempted at runtime.
    void ensureMindsDirWatcher('startup');
    // Watch rtws root for `.minds/` create/delete. This improves responsiveness when `.minds/`
    // appears/disappears without requiring a full polling interval.
    try {
        workspaceWatcher = fs.watch('.', { persistent: false }, (_event, filename) => {
            const name = filename ? filename.toString() : '';
            if (name !== '' && name !== path.dirname(MCP_YAML_PATH))
                return;
            void ensureMindsDirWatcher('rtws.watch');
            scheduleReload('rtws.watch');
        });
        workspaceWatcher.on('error', () => {
            if (workspaceWatcher) {
                workspaceWatcher.close();
                workspaceWatcher = undefined;
            }
        });
    }
    catch {
        // ignore; polling still works
    }
    // Polling fallback for reliability (editors that write via rename, platforms where watch misses).
    pollTimer = setInterval(() => {
        void maybePollReload();
        void ensureMindsDirWatcher('poll');
    }, 1500);
}
function stopMcpSupervisor() {
    if (mindsDirWatcher) {
        mindsDirWatcher.close();
        mindsDirWatcher = undefined;
    }
    if (workspaceWatcher) {
        workspaceWatcher.close();
        workspaceWatcher = undefined;
    }
    if (pollTimer) {
        clearInterval(pollTimer);
        pollTimer = undefined;
    }
    if (debounceTimer) {
        clearTimeout(debounceTimer);
        debounceTimer = undefined;
    }
    lastSeenMcpYamlSig = undefined;
    clearDeclaredServerRuntimeCatalog();
    (0, resources_1.clearMcpPromptResourceCatalog)();
    supervisorStarted = false;
}
function requestMcpServerRestart(serverId) {
    return new Promise((resolve) => {
        reloadChain = reloadChain
            .then(async () => {
            try {
                const res = await restartServerNow(serverId);
                resolve(res);
            }
            catch (err) {
                const errorText = err instanceof Error ? err.message : String(err);
                log.warn(`MCP server restart failed`, err, { serverId });
                resolve({ ok: false, errorText });
            }
        })
            .catch((err) => {
            const errorText = err instanceof Error ? err.message : String(err);
            log.warn(`MCP server restart enqueue failed`, err, { serverId });
            resolve({ ok: false, errorText });
        });
    });
}
function requestMcpServerDisable(serverId) {
    return new Promise((resolve) => {
        reloadChain = reloadChain
            .then(async () => {
            try {
                const res = await disableServerNow(serverId);
                resolve(res);
            }
            catch (err) {
                const errorText = err instanceof Error ? err.message : String(err);
                log.warn(`MCP server disable failed`, err, { serverId });
                resolve({ ok: false, errorText });
            }
        })
            .catch((err) => {
            const errorText = err instanceof Error ? err.message : String(err);
            log.warn(`MCP server disable enqueue failed`, err, { serverId });
            resolve({ ok: false, errorText });
        });
    });
}
function requestMcpConfigReload(reason = 'manual') {
    const normalizedReason = reason.trim() === '' ? 'manual' : reason.trim();
    return new Promise((resolve) => {
        reloadChain = reloadChain
            .then(async () => {
            try {
                await reloadNow(`manual request (${normalizedReason})`);
                resolve({ ok: true });
            }
            catch (err) {
                const errorText = err instanceof Error ? err.message : String(err);
                log.warn(`MCP config reload failed`, err, { reason: normalizedReason });
                resolve({ ok: false, errorText });
            }
        })
            .catch((err) => {
            const errorText = err instanceof Error ? err.message : String(err);
            log.warn(`MCP config reload enqueue failed`, err, { reason: normalizedReason });
            resolve({ ok: false, errorText });
        });
    });
}
async function ensureMindsDirWatcher(reason) {
    let st = null;
    try {
        st = await fs.promises.stat(path.dirname(MCP_YAML_PATH));
    }
    catch (err) {
        const code = isRecord(err) && 'code' in err ? err.code : undefined;
        if (code === 'ENOENT') {
            // `.minds/` may not exist; treat as normal and ensure watcher is detached.
            if (mindsDirWatcher) {
                mindsDirWatcher.close();
                mindsDirWatcher = undefined;
            }
            return;
        }
        return;
    }
    if (!st.isDirectory()) {
        if (mindsDirWatcher) {
            mindsDirWatcher.close();
            mindsDirWatcher = undefined;
        }
        return;
    }
    if (mindsDirWatcher)
        return;
    try {
        mindsDirWatcher = fs.watch(path.dirname(MCP_YAML_PATH), { persistent: false }, (_event, filename) => {
            if (filename && filename.toString() !== path.basename(MCP_YAML_PATH))
                return;
            scheduleReload(`minds.watch (${reason})`);
        });
        mindsDirWatcher.on('error', () => {
            // Directory may be wiped/recreated; drop watcher and let polling recreate it.
            if (mindsDirWatcher) {
                mindsDirWatcher.close();
                mindsDirWatcher = undefined;
            }
        });
    }
    catch (err) {
        const code = isRecord(err) && 'code' in err ? err.code : undefined;
        if (code === 'ENOENT') {
            // `.minds/` disappeared between stat and watch; normal in dev.
            return;
        }
        // Non-fatal; polling still works.
        log.warn('Failed to start fs.watch for MCP config; polling only', err);
    }
}
function scheduleReload(reason) {
    if (debounceTimer)
        clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
        reloadChain = reloadChain
            .then(async () => await reloadNow(reason))
            .catch((err) => {
            log.warn('MCP reload failed', err);
        });
    }, 200);
}
async function readMcpYamlSig() {
    try {
        const st = await fs.promises.stat(MCP_YAML_PATH);
        if (!st.isFile()) {
            return `not_file/${st.size}/${st.mtimeMs}/${st.ctimeMs}`;
        }
        return `${st.size}/${st.mtimeMs}/${st.ctimeMs}`;
    }
    catch (err) {
        const code = isRecord(err) && 'code' in err ? err.code : undefined;
        if (code === 'ENOENT') {
            return 'missing';
        }
        return 'error';
    }
}
async function maybePollReload() {
    const sig = await readMcpYamlSig();
    if (lastSeenMcpYamlSig === undefined) {
        lastSeenMcpYamlSig = sig;
        return;
    }
    if (sig === lastSeenMcpYamlSig)
        return;
    lastSeenMcpYamlSig = sig;
    scheduleReload('poll');
}
async function reloadNow(reason) {
    let rawText;
    try {
        rawText = await fs.promises.readFile(MCP_YAML_PATH, 'utf8');
    }
    catch (err) {
        const code = isRecord(err) && 'code' in err ? err.code : undefined;
        if (code === 'ENOENT') {
            // Deletion is treated as empty config.
            await applyWorkspaceConfig({ version: 1, servers: {} }, [], [], [], [], null, `missing file (${reason})`);
            clearWorkspaceConfigProblem();
            await reconcileMcpManualProblemsForRuntime([], null);
            return;
        }
        clearDeclaredServerRuntimeCatalog();
        await reconcileMcpManualProblemsForRuntime([], null);
        upsertWorkspaceConfigProblem(`Failed to read ${MCP_YAML_PATH}: ${String(err)}`);
        return;
    }
    const parsed = (0, config_1.parseMcpYaml)(rawText);
    if (!parsed.ok) {
        clearDeclaredServerRuntimeCatalog();
        await reconcileMcpManualProblemsForRuntime([], null);
        upsertWorkspaceConfigProblem(parsed.errorText);
        return;
    }
    clearWorkspaceConfigProblem();
    await applyWorkspaceConfig(parsed.config, parsed.invalidServers, parsed.serverIdsInYamlOrder, parsed.validServerIdsInYamlOrder, parsed.disabledServerIdsInYamlOrder, rawText, reason);
    await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), rawText);
}
async function restartServerNow(serverId) {
    let removedPlaceholder = false;
    const notifyPlaceholderRemovalOnFailure = (trigger) => {
        if (!removedPlaceholder)
            return;
        (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
            reason: 'registry_changed',
            trigger,
        });
    };
    let rawText;
    try {
        rawText = await fs.promises.readFile(MCP_YAML_PATH, 'utf8');
    }
    catch (err) {
        const code = isRecord(err) && 'code' in err ? err.code : undefined;
        if (code === 'ENOENT') {
            // Deletion is treated as empty config, so restart cannot proceed.
            clearWorkspaceConfigProblem();
            await reconcileMcpManualProblemsForRuntime([], null);
            return { ok: false, errorText: `Cannot restart '${serverId}': ${MCP_YAML_PATH} is missing` };
        }
        await reconcileMcpManualProblemsForRuntime([], null);
        upsertWorkspaceConfigProblem(`Failed to read ${MCP_YAML_PATH}: ${String(err)}`);
        return { ok: false, errorText: `Failed to read ${MCP_YAML_PATH}: ${String(err)}` };
    }
    let activeRawText = rawText;
    let parsed = (0, config_1.parseMcpYaml)(activeRawText);
    if (!parsed.ok) {
        await reconcileMcpManualProblemsForRuntime([], null);
        upsertWorkspaceConfigProblem(parsed.errorText);
        return { ok: false, errorText: parsed.errorText };
    }
    clearWorkspaceConfigProblem();
    const disabledServer = parsed.serverIdsInYamlOrder.includes(serverId) &&
        parsed.invalidServers.every((s) => s.serverId !== serverId) &&
        parsed.config.servers[serverId] === undefined;
    if (disabledServer) {
        const enableRes = setServerEnabledInMcpYaml(rawText, serverId, true);
        if (!enableRes.ok) {
            await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), rawText);
            return { ok: false, errorText: enableRes.errorText };
        }
        if (enableRes.changed) {
            await fs.promises.writeFile(MCP_YAML_PATH, enableRes.rawText, 'utf8');
            lastSeenMcpYamlSig = await readMcpYamlSig();
            removedPlaceholder = unregisterMcpToolsetPlaceholder(serverId) || removedPlaceholder;
        }
        activeRawText = enableRes.rawText;
        parsed = (0, config_1.parseMcpYaml)(activeRawText);
        if (!parsed.ok) {
            clearDeclaredServerRuntimeCatalog();
            await reconcileMcpManualProblemsForRuntime([], null);
            upsertWorkspaceConfigProblem(parsed.errorText);
            notifyPlaceholderRemovalOnFailure(`mcp:restart:${serverId}:parse-failed`);
            return { ok: false, errorText: parsed.errorText };
        }
        clearWorkspaceConfigProblem();
    }
    const invalid = parsed.invalidServers.find((s) => s.serverId === serverId);
    if (invalid) {
        replaceDeclaredServerRuntimeCatalog(parsed.config, parsed.invalidServers, parsed.serverIdsInYamlOrder, parsed.disabledServerIdsInYamlOrder);
        await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), activeRawText);
        upsertMcpServerConfigInvalidProblem(serverId, invalid.errorText);
        notifyPlaceholderRemovalOnFailure(`mcp:restart:${serverId}:config-invalid`);
        return { ok: false, errorText: invalid.errorText };
    }
    const serverCfg = parsed.config.servers[serverId];
    if (!serverCfg) {
        await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), activeRawText);
        notifyPlaceholderRemovalOnFailure(`mcp:restart:${serverId}:not-configured`);
        return {
            ok: false,
            errorText: `MCP server '${serverId}' is not configured in ${MCP_YAML_PATH}`,
        };
    }
    const desiredToolsetName = serverId;
    const fingerprint = fingerprintServerConfig(serverCfg);
    const existing = serverStateById.get(serverId);
    const runtimeLeaseChanged = existing !== undefined;
    replaceDeclaredServerRuntimeCatalog(parsed.config, parsed.invalidServers, parsed.serverIdsInYamlOrder, parsed.disabledServerIdsInYamlOrder);
    if (!existing) {
        removedPlaceholder = unregisterMcpToolsetPlaceholder(serverId) || removedPlaceholder;
    }
    const res = await tryBuildServerState(serverCfg, desiredToolsetName, fingerprint);
    if (!res.ok) {
        await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), activeRawText);
        upsertDeclaredServerRuntimeError(serverId, res.errorText);
        upsertMcpServerRuntimeUnavailableProblem(serverId, res.errorText, res.detailTextI18n);
        notifyPlaceholderRemovalOnFailure(`mcp:restart:${serverId}:runtime-unavailable`);
        return { ok: false, errorText: res.errorText };
    }
    clearDeclaredServerRuntimeError(serverId);
    (0, problems_1.removeProblemsByPrefix)(`${problemPrefixForServer(serverId)}server_error`);
    if (existing) {
        stopLoadedServer(serverId);
    }
    registerServer(res.state);
    serverStateById.set(serverId, res.state);
    (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), res.state.problems);
    reorderMcpToolsetsInRegistry(parsed.serverIdsInYamlOrder);
    await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), activeRawText);
    (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
        reason: 'registry_changed',
        trigger: `mcp:restart:${serverId}`,
    });
    if (runtimeLeaseChanged) {
        (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp:restart:${serverId}`);
    }
    return { ok: true };
}
async function disableServerNow(serverId) {
    const stoppedExisting = stopLoadedServer(serverId);
    let rawText;
    try {
        rawText = await fs.promises.readFile(MCP_YAML_PATH, 'utf8');
    }
    catch (err) {
        const code = isRecord(err) && 'code' in err ? err.code : undefined;
        if (stoppedExisting) {
            (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
                reason: 'registry_changed',
                trigger: `mcp:disable:${serverId}:read-failed`,
            });
            (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp:disable:${serverId}:read-failed`);
        }
        if (code === 'ENOENT') {
            return { ok: false, errorText: `Cannot disable '${serverId}': ${MCP_YAML_PATH} is missing` };
        }
        return { ok: false, errorText: `Failed to read ${MCP_YAML_PATH}: ${String(err)}` };
    }
    const disableRes = setServerEnabledInMcpYaml(rawText, serverId, false);
    if (!disableRes.ok) {
        if (stoppedExisting) {
            (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
                reason: 'registry_changed',
                trigger: `mcp:disable:${serverId}:disable-failed`,
            });
            (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp:disable:${serverId}:disable-failed`);
        }
        return { ok: false, errorText: disableRes.errorText };
    }
    if (disableRes.changed) {
        await fs.promises.writeFile(MCP_YAML_PATH, disableRes.rawText, 'utf8');
        lastSeenMcpYamlSig = await readMcpYamlSig();
    }
    const parsed = (0, config_1.parseMcpYaml)(disableRes.rawText);
    if (!parsed.ok) {
        clearDeclaredServerRuntimeCatalog();
        await reconcileMcpManualProblemsForRuntime([], null);
        upsertWorkspaceConfigProblem(parsed.errorText);
        if (stoppedExisting) {
            (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
                reason: 'registry_changed',
                trigger: `mcp:disable:${serverId}:parse-failed`,
            });
            (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp:disable:${serverId}:parse-failed`);
        }
        return { ok: false, errorText: parsed.errorText };
    }
    clearWorkspaceConfigProblem();
    await applyWorkspaceConfig(parsed.config, parsed.invalidServers, parsed.serverIdsInYamlOrder, parsed.validServerIdsInYamlOrder, parsed.disabledServerIdsInYamlOrder, disableRes.rawText, `mcp_disable:${serverId}`);
    await reconcileMcpManualProblemsForRuntime(mcpManualProblemServerIds(parsed), disableRes.rawText);
    if (stoppedExisting) {
        (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp:disable:${serverId}`);
    }
    return { ok: true };
}
function setServerEnabledInMcpYaml(rawText, serverId, enabled) {
    const doc = yaml_1.default.parseDocument(rawText, { prettyErrors: true });
    if (doc.errors.length > 0) {
        return { ok: false, errorText: doc.errors.map((e) => String(e)).join('\n') };
    }
    const parsed = doc.toJS();
    if (!isRecord(parsed)) {
        return { ok: false, errorText: `Invalid mcp.yaml: expected object at mcp.yaml root` };
    }
    if (parsed.version !== 1) {
        return { ok: false, errorText: `Invalid mcp.yaml: expected version: 1` };
    }
    const servers = parsed.servers;
    const server = isRecord(servers) ? servers[serverId] : undefined;
    if (!isRecord(server)) {
        return {
            ok: false,
            errorText: `MCP server '${serverId}' is not configured in ${MCP_YAML_PATH}`,
        };
    }
    const currentEnabled = server.enabled;
    if (currentEnabled !== undefined && typeof currentEnabled !== 'boolean') {
        return {
            ok: false,
            errorText: `Invalid mcp.yaml: servers.${serverId}.enabled must be a boolean`,
        };
    }
    if (currentEnabled === enabled) {
        return { ok: true, rawText, changed: false };
    }
    const serverNode = doc.getIn(['servers', serverId], true);
    if (!(0, yaml_1.isMap)(serverNode)) {
        return {
            ok: false,
            errorText: `Invalid mcp.yaml: servers.${serverId} must be an object`,
        };
    }
    const existingEnabledPair = serverNode.items.find((pair) => (0, yaml_1.isScalar)(pair.key) && pair.key.value === 'enabled');
    if (existingEnabledPair) {
        existingEnabledPair.value = doc.createNode(enabled);
    }
    else {
        serverNode.items.unshift(doc.createPair('enabled', enabled));
    }
    return { ok: true, rawText: String(doc), changed: true };
}
function stopLoadedServer(serverId) {
    const existing = serverStateById.get(serverId);
    if (!existing)
        return false;
    (0, resources_1.unregisterMcpPromptResourceCatalog)(serverId);
    unregisterServer(existing);
    existing.dispatch.requestStop();
    serverStateById.delete(serverId);
    (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), []);
    return true;
}
async function reconcileMcpManualProblemsForRuntime(serverIdsInYamlOrder, rawText) {
    const manualInfo = rawText === null ? (0, manual_problems_1.emptyMcpToolsetManualByServer)() : (0, manual_problems_1.parseMcpManualByServer)(rawText);
    await (0, manual_problems_1.reconcileMcpToolsetManualProblems)({
        serverIds: serverIdsInYamlOrder,
        manualInfo,
        measureRenderedWorkspaceMcpTopicRawChars: team_mgmt_mcp_manual_1.measureRenderedTeamMgmtMcpTopicRawChars,
        workspaceManualPath: MCP_YAML_PATH,
    });
}
function mcpManualProblemServerIds(parsed) {
    return [...parsed.validServerIdsInYamlOrder, ...parsed.disabledServerIdsInYamlOrder];
}
function upsertWorkspaceConfigProblem(errorText) {
    (0, problems_1.upsertProblem)({
        kind: 'mcp_workspace_config_error',
        source: 'mcp',
        id: 'mcp/workspace_config_error',
        severity: 'error',
        timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
        message: 'MCP rtws config error',
        messageI18n: {
            en: 'MCP rtws config error',
            zh: 'MCP rtws 配置错误',
        },
        detail: { filePath: MCP_YAML_PATH, errorText },
    });
}
function clearWorkspaceConfigProblem() {
    (0, problems_1.removeProblemsByPrefix)('mcp/workspace_config_error');
}
function upsertMcpServerConfigInvalidProblem(serverId, errorText, detailTextI18n) {
    (0, problems_1.upsertProblem)({
        kind: 'mcp_server_error',
        source: 'mcp',
        id: `${problemPrefixForServer(serverId)}server_error`,
        severity: 'error',
        timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
        message: `MCP server '${serverId}' failed to parse config`,
        messageI18n: {
            en: `MCP server '${serverId}' failed to parse config`,
            zh: `MCP server '${serverId}' 解析配置失败`,
        },
        detailTextI18n,
        detail: { serverId, errorText },
    });
}
function upsertMcpServerRuntimeUnavailableProblem(serverId, errorText, detailTextI18n) {
    (0, problems_1.upsertProblem)({
        kind: 'mcp_server_error',
        source: 'mcp',
        id: `${problemPrefixForServer(serverId)}server_error`,
        severity: 'info',
        timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
        message: `MCP server '${serverId}' is currently unavailable`,
        messageI18n: {
            en: `MCP server '${serverId}' is currently unavailable`,
            zh: `MCP server '${serverId}' 当前不可用`,
        },
        detailTextI18n,
        detail: { serverId, errorText },
    });
}
function upsertMcpToolCallProblem(args) {
    const workLanguage = (0, work_language_1.getWorkLanguage)();
    const normalizedErrorText = args.errorText.length > 10_000 ? `${args.errorText.slice(0, 10_000)}…` : args.errorText;
    const hintLines = workLanguage === 'zh'
        ? [
            '建议排查：',
            `- 如果需要全局重建该 MCP server，直接调用 mcp_restart({"serverId":"${args.serverId}"})；它成功后会清理旧连接/进程的全部 lease`,
            `- 如果只想丢弃当前对话的连接，再调用 mcp_release({"serverId":"${args.serverId}"})`,
            `- 重新打开/关闭浏览器窗口，避免 Playwright persistent context 残留`,
            `- 查看 ${MCP_YAML_PATH} 是否已加载且配置正确（Problems 面板 / 后端日志）`,
        ]
        : [
            'Suggested checks:',
            `- To rebuild this MCP server globally, call mcp_restart({"serverId":"${args.serverId}"}) directly; a successful restart clears all leases on the old connection/process`,
            `- To discard only this dialog's connection, call mcp_release({"serverId":"${args.serverId}"})`,
            `- Close/reopen browser windows to avoid leftover Playwright persistent contexts`,
            `- Verify ${MCP_YAML_PATH} is loaded and valid (Problems panel / backend logs)`,
        ];
    (0, problems_1.upsertProblem)({
        kind: 'generic_problem',
        source: 'system',
        id: `${MCP_TOOL_CALL_PROBLEM_PREFIX}${sanitizePathSegment(args.serverId)}`,
        severity: 'error',
        timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
        message: workLanguage === 'zh'
            ? `MCP 工具调用失败：${args.serverId}.${args.toolName}`
            : `MCP tool call failed: ${args.serverId}.${args.toolName}`,
        detail: {
            text: [
                `serverId=${args.serverId}`,
                `toolName=${args.toolName}`,
                '',
                'error:',
                normalizedErrorText,
                '',
                ...hintLines,
            ].join('\n'),
        },
    });
}
function clearMcpToolCallProblem(serverId) {
    (0, problems_1.removeProblemsByPrefix)(`${MCP_TOOL_CALL_PROBLEM_PREFIX}${sanitizePathSegment(serverId)}`);
}
async function applyWorkspaceConfig(config, invalidServers, serverIdsInYamlOrder, validServerIdsInYamlOrder, disabledServerIdsInYamlOrder, rawText, reason) {
    log.info(`Applying MCP rtws config (${reason})`);
    replaceDeclaredServerRuntimeCatalog(config, invalidServers, serverIdsInYamlOrder, disabledServerIdsInYamlOrder);
    let runtimeLeaseChanged = false;
    const invalidIds = new Set(invalidServers.map((s) => s.serverId));
    const desiredIds = new Set([...Object.keys(config.servers), ...invalidIds]);
    const declaredIds = new Set(serverIdsInYamlOrder);
    // Remove deleted servers first.
    for (const [serverId, state] of serverStateById.entries()) {
        if (desiredIds.has(serverId))
            continue;
        (0, resources_1.unregisterMcpPromptResourceCatalog)(serverId);
        unregisterServer(state);
        state.dispatch.requestStop();
        runtimeLeaseChanged = true;
        serverStateById.delete(serverId);
        (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), []);
    }
    // Remove disabled/deleted MCP placeholders that are no longer declared.
    for (const [toolsetName, owner] of Array.from(toolsetOwnerByName.entries())) {
        if (owner.kind !== 'mcp')
            continue;
        if (declaredIds.has(owner.serverId))
            continue;
        (0, registry_1.unregisterToolset)(toolsetName);
        toolsetOwnerByName.delete(toolsetName);
    }
    // Surface invalid server config errors (while keeping last-known-good runtimes registered).
    for (const s of invalidServers) {
        unregisterMcpToolsetPlaceholder(s.serverId);
        upsertMcpServerConfigInvalidProblem(s.serverId, s.errorText);
    }
    // Apply desired servers independently (deterministic order).
    for (const serverId of validServerIdsInYamlOrder) {
        const serverCfg = config.servers[serverId];
        if (!serverCfg)
            continue;
        const desiredToolsetName = serverId;
        const fingerprint = fingerprintServerConfig(serverCfg);
        const existing = serverStateById.get(serverId);
        if (!existing || existing.configFingerprint !== fingerprint) {
            if (!existing) {
                unregisterMcpToolsetPlaceholder(serverId);
            }
            const res = await tryBuildServerState(serverCfg, desiredToolsetName, fingerprint);
            if (!res.ok) {
                upsertDeclaredServerRuntimeError(serverId, res.errorText);
                // Keep last-known-good registration, but surface per-server error.
                upsertMcpServerRuntimeUnavailableProblem(serverId, res.errorText, res.detailTextI18n);
                continue;
            }
            // Successful start: replace old runtime/tools.
            clearDeclaredServerRuntimeError(serverId);
            (0, problems_1.removeProblemsByPrefix)(`${problemPrefixForServer(serverId)}server_error`);
            if (existing) {
                (0, resources_1.unregisterMcpPromptResourceCatalog)(serverId);
                unregisterServer(existing);
                existing.dispatch.requestStop();
                runtimeLeaseChanged = true;
            }
            registerServer(res.state);
            serverStateById.set(serverId, res.state);
            (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), res.state.problems);
            continue;
        }
        // Config unchanged, but recompute desired tool registration to:
        // - auto-clear disappeared problems
        // - re-attempt registering tools that were previously skipped due to collisions
        clearDeclaredServerRuntimeError(serverId);
        (0, problems_1.removeProblemsByPrefix)(`${problemPrefixForServer(serverId)}server_error`);
        const rebuilt = buildToolsForServer(serverCfg, existing.dispatch, existing.listedTools);
        const next = {
            serverId,
            toolsetName: desiredToolsetName,
            configFingerprint: fingerprint,
            cfg: serverCfg,
            listedTools: existing.listedTools,
            dispatch: existing.dispatch,
            tools: rebuilt.tools,
            ownedToolNames: rebuilt.ownedToolNames,
            problems: rebuilt.problems,
        };
        const sameTools = setsEqual(existing.ownedToolNames, next.ownedToolNames);
        if (!sameTools) {
            unregisterServer(existing);
            registerServer(next);
            serverStateById.set(serverId, next);
        }
        else if (!manualConfigEqual(existing.cfg.manual, serverCfg.manual)) {
            setLoadedMcpToolsetMeta(next.toolsetName, serverCfg);
            serverStateById.set(serverId, next);
        }
        (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), next.problems);
    }
    // Second pass: after all adds/updates, re-run collision resolution so earlier servers can pick up
    // tools that become available due to later server changes in the same reload cycle.
    for (let i = 0; i < 2; i++) {
        const changed = reconcileCollisionDependentRegistrations(config, validServerIdsInYamlOrder);
        if (!changed)
            break;
    }
    const manualInfo = rawText === null ? (0, manual_problems_1.emptyMcpToolsetManualByServer)() : (0, manual_problems_1.parseMcpManualByServer)(rawText);
    for (const serverId of disabledServerIdsInYamlOrder) {
        (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), []);
        registerDisabledServerToolset(serverId, manualInfo);
    }
    reorderMcpToolsetsInRegistry(serverIdsInYamlOrder);
    (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
        reason: 'registry_changed',
        trigger: `mcp:apply-workspace-config:${reason}`,
    });
    if (runtimeLeaseChanged) {
        (0, tool_availability_updates_1.notifyToolAvailabilityRuntimeLeaseChanged)(`mcp:apply-workspace-config:${reason}`);
    }
}
function setsEqual(a, b) {
    if (a.size !== b.size)
        return false;
    for (const v of a) {
        if (!b.has(v))
            return false;
    }
    return true;
}
function manualConfigEqual(a, b) {
    return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
}
function clearDeclaredServerRuntimeCatalog() {
    declaredServerIdsInYamlOrder = [];
    declaredServerRuntimeCatalogById.clear();
}
function replaceDeclaredServerRuntimeCatalog(config, invalidServers, serverIdsInYamlOrder, disabledServerIdsInYamlOrder) {
    const previousRuntimeErrors = new Map();
    for (const [serverId, entry] of declaredServerRuntimeCatalogById.entries()) {
        if (typeof entry.runtimeErrorText === 'string' && entry.runtimeErrorText.trim() !== '') {
            previousRuntimeErrors.set(serverId, entry.runtimeErrorText);
        }
    }
    declaredServerIdsInYamlOrder = [...serverIdsInYamlOrder];
    declaredServerRuntimeCatalogById.clear();
    const disabledIds = new Set(disabledServerIdsInYamlOrder);
    for (const serverId of serverIdsInYamlOrder) {
        if (disabledIds.has(serverId)) {
            declaredServerRuntimeCatalogById.set(serverId, {
                transport: 'unknown',
                disabled: true,
            });
            continue;
        }
        const cfg = config.servers[serverId];
        if (!cfg)
            continue;
        declaredServerRuntimeCatalogById.set(serverId, {
            transport: cfg.transport,
            runtimeErrorText: previousRuntimeErrors.get(serverId),
        });
    }
    for (const invalid of invalidServers) {
        declaredServerRuntimeCatalogById.set(invalid.serverId, {
            transport: 'invalid',
            configErrorText: invalid.errorText,
        });
    }
}
function upsertDeclaredServerRuntimeError(serverId, errorText) {
    const current = declaredServerRuntimeCatalogById.get(serverId);
    declaredServerRuntimeCatalogById.set(serverId, {
        transport: current?.transport ?? 'unknown',
        configErrorText: current?.configErrorText,
        runtimeErrorText: errorText,
    });
}
function clearDeclaredServerRuntimeError(serverId) {
    const current = declaredServerRuntimeCatalogById.get(serverId);
    if (!current || current.runtimeErrorText === undefined)
        return;
    declaredServerRuntimeCatalogById.set(serverId, {
        transport: current.transport,
        configErrorText: current.configErrorText,
    });
}
function reconcileCollisionDependentRegistrations(config, serverIds) {
    let changedAny = false;
    for (const serverId of serverIds) {
        const serverCfg = config.servers[serverId];
        if (!serverCfg)
            continue;
        const fingerprint = fingerprintServerConfig(serverCfg);
        const existing = serverStateById.get(serverId);
        if (!existing)
            continue;
        if (existing.configFingerprint !== fingerprint)
            continue; // handled by spawn/update path
        const rebuilt = buildToolsForServer(serverCfg, existing.dispatch, existing.listedTools);
        const next = {
            serverId,
            toolsetName: existing.toolsetName,
            configFingerprint: fingerprint,
            cfg: serverCfg,
            listedTools: existing.listedTools,
            dispatch: existing.dispatch,
            tools: rebuilt.tools,
            ownedToolNames: rebuilt.ownedToolNames,
            problems: rebuilt.problems,
        };
        const sameTools = setsEqual(existing.ownedToolNames, next.ownedToolNames);
        if (!sameTools) {
            unregisterServer(existing);
            registerServer(next);
            serverStateById.set(serverId, next);
            changedAny = true;
        }
        (0, problems_1.reconcileProblemsByPrefix)(problemPrefixForServer(serverId), next.problems);
    }
    return changedAny;
}
function registerServer(state) {
    for (const t of state.tools) {
        (0, registry_1.registerTool)(t);
        toolOwnerByName.set(t.name, { kind: 'mcp', serverId: state.serverId });
    }
    (0, registry_1.registerToolset)(state.toolsetName, state.tools);
    setLoadedMcpToolsetMeta(state.toolsetName, state.cfg);
    toolsetOwnerByName.set(state.toolsetName, { kind: 'mcp', serverId: state.serverId });
}
function setLoadedMcpToolsetMeta(toolsetName, cfg) {
    const manualSpec = cfg.manual?.contentFile
        ? (0, spec_1.buildMcpManualSpec)(cfg.manual.contentFile)
        : (0, spec_1.buildRawMcpManualSpec)();
    (0, registry_1.setToolsetMeta)(toolsetName, {
        source: 'mcp',
        descriptionI18n: {
            en: `MCP server: ${cfg.serverId}`,
            zh: `MCP 服务器：${cfg.serverId}`,
        },
        manualNoticeI18n: buildMcpToolsetManualNotice(cfg),
        manualSpec,
    });
}
function registerDisabledServerToolset(serverId, manualInfo) {
    const existingToolset = registry_1.toolsetsRegistry.get(serverId);
    const existingOwner = toolsetOwnerByName.get(serverId);
    if (existingToolset && (!existingOwner || existingOwner.serverId !== serverId)) {
        upsertMcpServerConfigInvalidProblem(serverId, `Toolset name collision: ${serverId}`);
        return;
    }
    (0, registry_1.registerToolset)(serverId, []);
    const manual = manualInfo.manualByServerId.get(serverId);
    const manualSpec = manual?.contentFile
        ? (0, spec_1.buildMcpManualSpec)(manual.contentFile)
        : (0, spec_1.buildRawMcpManualSpec)();
    const inlineManual = manual ? renderInlineMcpToolsetManual(manual) : '';
    const disabledNoticeEn = [
        `This MCP server is configured but disabled in \`.minds/mcp.yaml\` (\`enabled: false\`).`,
        `It intentionally exposes zero tools until an MCP administrator enables it with \`mcp_restart({"serverId":"${serverId}"})\`.`,
        inlineManual,
    ]
        .filter((part) => part.trim() !== '')
        .join('\n\n');
    const disabledNoticeZh = [
        `该 MCP server 已在 \`.minds/mcp.yaml\` 中配置，但当前为禁用状态（\`enabled: false\`）。`,
        `它会刻意暴露为 0 工具的 toolset；需要 MCP 排障/管理员用 \`mcp_restart({"serverId":"${serverId}"})\` 启用后才会加载工具。`,
        inlineManual,
    ]
        .filter((part) => part.trim() !== '')
        .join('\n\n');
    (0, registry_1.setToolsetMeta)(serverId, {
        source: 'mcp',
        descriptionI18n: {
            en: `MCP server: ${serverId} (disabled)`,
            zh: `MCP 服务器：${serverId}（已禁用）`,
        },
        manualNoticeI18n: {
            en: disabledNoticeEn,
            zh: disabledNoticeZh,
        },
        ...(manualSpec !== undefined ? { manualSpec } : {}),
    });
    toolsetOwnerByName.set(serverId, { kind: 'mcp', serverId });
}
function buildMcpToolsetManualNotice(cfg) {
    const endpoint = formatMcpEndpointForManual(cfg);
    const inlineManual = cfg.manual ? renderInlineMcpToolsetManual(cfg.manual) : '';
    return {
        zh: [
            `该工具集来自标准 MCP 协议接入：serverId=\`${cfg.serverId}\`，transport=\`${cfg.transport}\`，${endpoint}。`,
            'MCP Server 通过 `tools/list` 提供的工具名称、说明与参数 schema 已自动注入给智能体；工具契约详见各工具函数说明，不在手册正文中重复罗列。',
            '`.minds/mcp.yaml` 中的 `manual` 是可选增强说明，适合补充使用场景、安全边界、故障处置和团队约定，不需要重复罗列工具清单或参数定义。',
            inlineManual,
        ]
            .filter((part) => part.trim() !== '')
            .join('\n\n'),
        en: [
            `This toolset is a standard Raw MCP protocol integration: serverId=\`${cfg.serverId}\`, transport=\`${cfg.transport}\`, ${endpoint}.`,
            'Tool names, descriptions, and parameter schemas returned by the MCP Server through `tools/list` are already provided to the agent; see the individual function-tool descriptions for the tool contract instead of duplicating it in this manual.',
            'The `.minds/mcp.yaml` `manual` field is optional enhancement guidance for use cases, guardrails, failure handling, and team norms; do not duplicate tool lists or parameter definitions there.',
            inlineManual,
        ]
            .filter((part) => part.trim() !== '')
            .join('\n\n'),
    };
}
function formatMcpEndpointForManual(cfg) {
    if (cfg.transport === 'stdio') {
        return [
            `command=${JSON.stringify(cfg.command)}`,
            `args_count=${cfg.args.length}`,
            cfg.cwd ? `cwd=${JSON.stringify(cfg.cwd)}` : '',
        ]
            .filter((part) => part !== '')
            .join(', ');
    }
    const url = new URL(cfg.url);
    url.username = '';
    url.password = '';
    url.search = '';
    url.hash = '';
    return `url=${url.toString()}`;
}
function renderInlineMcpToolsetManual(manual) {
    const parts = [];
    if (manual.content) {
        parts.push(manual.content);
    }
    for (const section of manual.sections) {
        parts.push(`#### ${section.title}\n\n${section.content}`);
    }
    return parts.join('\n\n');
}
function unregisterMcpToolsetPlaceholder(serverId) {
    if (serverStateById.has(serverId))
        return false;
    const owner = toolsetOwnerByName.get(serverId);
    if (owner?.kind !== 'mcp' || owner.serverId !== serverId)
        return false;
    const tools = registry_1.toolsetsRegistry.get(serverId);
    if (tools && tools.length > 0)
        return false;
    (0, registry_1.unregisterToolset)(serverId);
    toolsetOwnerByName.delete(serverId);
    return true;
}
function unregisterServer(state) {
    // Unregister toolset first so Team.Member.listTools doesn't resolve stale tools.
    if (toolsetOwnerByName.get(state.toolsetName)?.serverId === state.serverId) {
        (0, registry_1.unregisterToolset)(state.toolsetName);
        toolsetOwnerByName.delete(state.toolsetName);
    }
    for (const toolName of state.ownedToolNames) {
        if (toolOwnerByName.get(toolName)?.serverId !== state.serverId)
            continue;
        (0, registry_1.unregisterTool)(toolName);
        toolOwnerByName.delete(toolName);
    }
}
function reorderMcpToolsetsInRegistry(serverIdsInYamlOrder) {
    const desiredToolsetNames = serverIdsInYamlOrder;
    const currentEntries = Array.from(registry_1.toolsetsRegistry.entries());
    const nonMcpEntries = [];
    const mcpToolsetsByName = new Map();
    const mcpToolsetNamesInCurrentOrder = [];
    for (const [toolsetName, tools] of currentEntries) {
        const owner = toolsetOwnerByName.get(toolsetName);
        if (owner?.kind === 'mcp') {
            mcpToolsetsByName.set(toolsetName, tools);
            mcpToolsetNamesInCurrentOrder.push(toolsetName);
            continue;
        }
        nonMcpEntries.push([toolsetName, tools]);
    }
    const reordered = [...nonMcpEntries];
    const placed = new Set();
    for (const toolsetName of desiredToolsetNames) {
        const tools = mcpToolsetsByName.get(toolsetName);
        if (!tools)
            continue;
        reordered.push([toolsetName, tools]);
        placed.add(toolsetName);
    }
    for (const toolsetName of mcpToolsetNamesInCurrentOrder) {
        if (placed.has(toolsetName))
            continue;
        const tools = mcpToolsetsByName.get(toolsetName);
        if (!tools)
            continue;
        reordered.push([toolsetName, tools]);
    }
    registry_1.toolsetsRegistry.clear();
    for (const [toolsetName, tools] of reordered) {
        registry_1.toolsetsRegistry.set(toolsetName, tools);
    }
}
async function tryBuildServerState(cfg, toolsetName, fingerprint) {
    const serverId = cfg.serverId;
    // Toolset-name collisions should prevent committing this server.
    const existingToolset = registry_1.toolsetsRegistry.get(toolsetName);
    const existingOwner = toolsetOwnerByName.get(toolsetName);
    if (existingToolset && (!existingOwner || existingOwner.serverId !== serverId)) {
        return { ok: false, errorText: `Toolset name collision: ${toolsetName}` };
    }
    let client;
    let sharedRuntime;
    try {
        client =
            cfg.transport === 'stdio'
                ? await sdk_client_1.McpSdkClient.connectStdio({
                    serverId,
                    command: cfg.command,
                    args: cfg.args,
                    env: buildChildEnv(cfg, serverId),
                    cwd: await resolveStdioServerCwd(cfg, serverId),
                })
                : await sdk_client_1.McpSdkClient.connectStreamableHttp({
                    serverId,
                    url: cfg.url,
                    headers: buildHttpHeaders(cfg, serverId),
                    sessionId: cfg.sessionId,
                });
        const listedTools = await client.listTools();
        await (0, resources_1.refreshMcpPromptResourceCatalog)({
            serverId,
            client,
            transform: cfg.transform,
            prompts: cfg.prompts,
            resources: cfg.resources,
        });
        if (cfg.truelyStateless) {
            sharedRuntime = new server_runtime_1.McpServerRuntime({ serverId, toolsetName, client });
            client = undefined;
        }
        else {
            await client.close();
            client = undefined;
        }
        const dispatch = new McpServerDispatch({ serverId, toolsetName, cfg, sharedRuntime });
        const build = buildToolsForServer(cfg, dispatch, listedTools);
        const state = {
            serverId,
            toolsetName,
            configFingerprint: fingerprint,
            cfg,
            listedTools,
            dispatch,
            tools: build.tools,
            ownedToolNames: build.ownedToolNames,
            problems: build.problems,
        };
        return { ok: true, state };
    }
    catch (err) {
        if (sharedRuntime) {
            sharedRuntime.requestStop({ forceKillAfterMs: 3_000 });
        }
        if (client) {
            try {
                await client.close();
            }
            catch {
                // best-effort
            }
        }
        return {
            ok: false,
            errorText: err instanceof Error ? err.message : String(err),
            detailTextI18n: (0, sdk_client_1.extractMcpDiagnosticTextI18n)(err),
        };
    }
}
function buildToolsForServer(cfg, dispatch, listedTools) {
    const serverId = cfg.serverId;
    const problems = [];
    const tools = [];
    const ownedToolNames = new Set();
    const seenDomindsNames = new Set();
    for (const tool of listedTools) {
        const originalName = tool.name;
        if (!(0, tool_names_1.isValidProviderToolName)(originalName)) {
            log.warn(`Rejecting MCP tool with invalid name`, undefined, {
                serverId,
                toolName: originalName,
            });
            problems.push({
                kind: 'mcp_tool_invalid_name',
                source: 'mcp',
                id: `${problemPrefixForServer(serverId)}tool_invalid_name/${originalName}`,
                severity: 'warning',
                timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                message: `MCP tool name is invalid and was rejected`,
                detail: { serverId, toolName: originalName, rule: tool_names_1.TOOL_NAME_VALIDITY_RULE },
            });
            continue;
        }
        const exposure = (0, tool_names_1.decideToolExposure)(originalName, cfg.tools);
        if (exposure.kind === 'blacklisted') {
            log.warn(`MCP tool not registered (blacklisted)`, undefined, {
                serverId,
                toolName: originalName,
            });
            problems.push({
                kind: 'mcp_tool_blacklisted',
                source: 'mcp',
                id: `${problemPrefixForServer(serverId)}tool_blacklisted/${originalName}`,
                severity: 'warning',
                timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                message: `MCP tool was excluded by blacklist`,
                detail: { serverId, toolName: originalName, pattern: exposure.pattern },
            });
            continue;
        }
        if (exposure.kind === 'not_whitelisted') {
            log.warn(`MCP tool not registered (not whitelisted)`, undefined, {
                serverId,
                toolName: originalName,
            });
            problems.push({
                kind: 'mcp_tool_not_whitelisted',
                source: 'mcp',
                id: `${problemPrefixForServer(serverId)}tool_not_whitelisted/${originalName}`,
                severity: 'info',
                timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                message: `MCP tool was excluded by whitelist-only mode`,
                detail: { serverId, toolName: originalName, pattern: exposure.pattern },
            });
            continue;
        }
        const domindsName = (0, tool_names_1.applyMcpIdTransforms)(originalName, (0, config_1.resolveMcpScopedTransforms)(cfg.transform, cfg.tools.transform));
        if (!(0, tool_names_1.isValidProviderToolName)(domindsName)) {
            log.warn(`Rejecting MCP tool after transforms due to invalid Dominds tool name`, undefined, {
                serverId,
                toolName: originalName,
                domindsToolName: domindsName,
            });
            problems.push({
                kind: 'mcp_tool_invalid_name',
                source: 'mcp',
                id: `${problemPrefixForServer(serverId)}tool_invalid_name/${originalName}/transformed`,
                severity: 'warning',
                timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                message: `Dominds tool name produced by transforms is invalid and was rejected`,
                detail: { serverId, toolName: domindsName, rule: tool_names_1.TOOL_NAME_VALIDITY_RULE },
            });
            continue;
        }
        if (seenDomindsNames.has(domindsName)) {
            log.warn(`Skipping MCP tool due to within-server name collision`, undefined, {
                serverId,
                toolName: originalName,
                domindsToolName: domindsName,
            });
            problems.push({
                kind: 'mcp_tool_collision',
                source: 'mcp',
                id: `${problemPrefixForServer(serverId)}tool_collision/${domindsName}`,
                severity: 'warning',
                timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                message: `MCP tool name collision (after transforms)`,
                detail: { serverId, toolName: originalName, domindsToolName: domindsName },
            });
            continue;
        }
        const existingTool = registry_1.toolsRegistry.get(domindsName);
        const existingOwner = toolOwnerByName.get(domindsName);
        const canReplace = existingOwner?.serverId === serverId;
        if (existingTool && !canReplace) {
            log.warn(`Skipping MCP tool due to global name collision`, undefined, {
                serverId,
                toolName: originalName,
                domindsToolName: domindsName,
            });
            problems.push({
                kind: 'mcp_tool_collision',
                source: 'mcp',
                id: `${problemPrefixForServer(serverId)}tool_collision/${domindsName}`,
                severity: 'warning',
                timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                message: `MCP tool name collision with existing tool`,
                detail: { serverId, toolName: originalName, domindsToolName: domindsName },
            });
            continue;
        }
        const func = {
            type: 'func',
            name: domindsName,
            description: tool.description,
            parameters: tool.inputSchema,
            argsValidation: 'passthrough',
            call: async (dlg, _caller, args) => {
                return await dispatch.callToolForDialog(dlg, originalName, args);
            },
        };
        tools.push(func);
        ownedToolNames.add(domindsName);
        seenDomindsNames.add(domindsName);
    }
    return { tools, ownedToolNames, problems };
}
function buildChildEnv(cfg, serverId) {
    if (cfg.transport !== 'stdio') {
        return {};
    }
    const env = {};
    for (const [k, v] of Object.entries(process.env)) {
        if (typeof v === 'string')
            env[k] = v;
    }
    for (const [k, v] of Object.entries(cfg.env)) {
        switch (v.kind) {
            case 'literal':
                env[k] = v.value;
                break;
            case 'from_env': {
                const val = process.env[v.env];
                if (val === undefined) {
                    throw new Error(`MCP server '${serverId}' missing required host env var '${v.env}' (for env.${k})`);
                }
                env[k] = val;
                break;
            }
            default: {
                const _exhaustive = v;
                throw new Error(`Invalid env mapping kind: ${String(_exhaustive)}`);
            }
        }
    }
    return env;
}
async function resolveStdioServerCwd(cfg, serverId) {
    if (cfg.transport !== 'stdio') {
        throw new Error(`MCP server '${serverId}' resolveStdioServerCwd requires stdio transport`);
    }
    const baseCwd = (0, rtws_1.domindsRtwsRootAbs)();
    const resolved = cfg.cwd === undefined ? baseCwd : path.resolve(baseCwd, cfg.cwd);
    try {
        const st = await fs.promises.stat(resolved);
        if (!st.isDirectory()) {
            throw new Error(`MCP server '${serverId}' cwd is not a directory: ${resolved}`);
        }
        return resolved;
    }
    catch (err) {
        if (isRecord(err) && 'code' in err && err.code === 'ENOENT') {
            const source = cfg.cwd === undefined ? '<process.cwd()>' : cfg.cwd;
            throw new Error(`MCP server '${serverId}' cwd does not exist: ${source} -> ${resolved}`);
        }
        throw err;
    }
}
function fingerprintServerConfig(cfg) {
    const obj = cfg.transport === 'stdio'
        ? {
            truelyStateless: cfg.truelyStateless,
            transport: cfg.transport,
            command: cfg.command,
            args: cfg.args,
            cwd: cfg.cwd ?? null,
            env: sortedEntries(cfg.env),
            tools: cfg.tools,
            transform: cfg.transform,
            prompts: cfg.prompts,
            resources: cfg.resources,
        }
        : {
            truelyStateless: cfg.truelyStateless,
            transport: cfg.transport,
            url: cfg.url,
            headers: sortedEntries(cfg.headers),
            sessionId: cfg.sessionId ?? null,
            tools: cfg.tools,
            transform: cfg.transform,
            prompts: cfg.prompts,
            resources: cfg.resources,
        };
    return JSON.stringify(obj);
}
function problemPrefixForServer(serverId) {
    return `mcp/server/${serverId}/`;
}
function isRecord(value) {
    return typeof value === 'object' && value !== null;
}
function sanitizePathSegment(value) {
    const trimmed = value.trim();
    if (trimmed === '')
        return 'unnamed';
    return trimmed.replace(/[^0-9A-Za-z._-]/g, '_');
}
function stripDataUrlPrefix(value) {
    const trimmed = value.trim();
    if (!trimmed.startsWith('data:'))
        return trimmed;
    const idx = trimmed.indexOf('base64,');
    if (idx < 0)
        return trimmed;
    return trimmed.slice(idx + 'base64,'.length);
}
function mimeTypeToExt(mimeType) {
    switch (mimeType) {
        case 'image/png':
            return 'png';
        case 'image/jpeg':
            return 'jpg';
        case 'image/jpg':
            return 'jpg';
        case 'image/gif':
            return 'gif';
        case 'image/webp':
            return 'webp';
        case 'image/svg+xml':
            return 'svg';
        default:
            return 'bin';
    }
}
function stringifyMcpToolCallResultSafe(value) {
    if (!isRecord(value) || Array.isArray(value)) {
        return JSON.stringify(value);
    }
    if ('content' in value) {
        const content = value.content;
        if (!Array.isArray(content)) {
            return JSON.stringify(value);
        }
        const parts = [];
        for (const item of content) {
            if (isRecord(item) && !Array.isArray(item)) {
                const t = item.type;
                if (t === 'text' && typeof item.text === 'string') {
                    parts.push(item.text);
                    continue;
                }
                if (t === 'image') {
                    const mimeType = typeof item.mimeType === 'string' ? item.mimeType : 'unknown';
                    const data = item.data;
                    const dataSize = typeof data === 'string' ? data.length : 0;
                    parts.push(JSON.stringify({
                        type: 'image',
                        mimeType,
                        data: `[omitted base64; length=${dataSize}]`,
                    }));
                    continue;
                }
            }
            parts.push(JSON.stringify(item));
        }
        const joined = parts.join('\n').trim();
        if (joined)
            return joined;
        return JSON.stringify(value);
    }
    if ('toolResult' in value) {
        return JSON.stringify(value.toolResult);
    }
    return JSON.stringify(value);
}
async function materializeMcpToolCallOutput(params) {
    const rawValue = params.raw;
    if (!isRecord(rawValue) || Array.isArray(rawValue)) {
        return (0, tool_1.toolSuccess)(String(rawValue));
    }
    const maybeContent = rawValue.content;
    if (!Array.isArray(maybeContent)) {
        return (0, tool_1.toolSuccess)(stringifyMcpToolCallResultSafe(rawValue));
    }
    const eventsBase = persistence_1.DialogPersistence.getDialogEventsPath(params.dlg.id, params.dlg.status);
    const contentItems = [];
    const displayLines = [];
    for (const item of maybeContent) {
        if (!isRecord(item) || Array.isArray(item)) {
            displayLines.push(JSON.stringify(item));
            continue;
        }
        const t = item.type;
        if (t === 'text' && typeof item.text === 'string') {
            const text = item.text;
            contentItems.push({ type: 'input_text', text });
            if (text.trim() !== '') {
                displayLines.push(text);
            }
            continue;
        }
        if (t === 'image') {
            const mimeType = typeof item.mimeType === 'string' ? item.mimeType : 'application/octet-stream';
            const rawData = typeof item.data === 'string' ? item.data : '';
            const base64 = stripDataUrlPrefix(rawData);
            const bytes = Buffer.from(base64, 'base64');
            if (base64.trim() !== '' && bytes.length === 0) {
                displayLines.push(`[image decode failed: ${mimeType}]`);
                continue;
            }
            const ts = new Date().toISOString().replace(/[:.]/g, '-');
            const ext = mimeTypeToExt(mimeType);
            const relPath = path.posix.join('artifacts', 'mcp', sanitizePathSegment(params.serverId), sanitizePathSegment(params.toolName), `${ts}-${(0, node_crypto_1.randomUUID)()}.${ext}`);
            const absPath = path.join(eventsBase, ...relPath.split('/'));
            await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
            await fs.promises.writeFile(absPath, bytes);
            contentItems.push({
                type: 'input_image',
                mimeType,
                byteLength: bytes.length,
                artifact: {
                    rootId: params.dlg.id.rootId,
                    selfId: params.dlg.id.selfId,
                    status: params.dlg.status,
                    relPath,
                },
            });
            displayLines.push(`[image saved: ${mimeType}, ${bytes.length} bytes]`);
            continue;
        }
        displayLines.push(JSON.stringify(item));
    }
    const content = displayLines.join('\n').trim();
    return (0, tool_1.toolSuccess)(content !== '' ? content : stringifyMcpToolCallResultSafe(rawValue), contentItems.length > 0 ? contentItems : undefined);
}
function buildHttpHeaders(cfg, serverId) {
    const headers = {};
    for (const [k, v] of Object.entries(cfg.headers)) {
        switch (v.kind) {
            case 'literal':
                headers[k] = v.value;
                break;
            case 'from_env': {
                const val = process.env[v.env];
                if (val === undefined) {
                    throw new Error(`MCP server '${serverId}' missing required host env var '${v.env}' (for headers.${k})`);
                }
                headers[k] = `${v.prefix}${val}`;
                break;
            }
            default: {
                const _exhaustive = v;
                throw new Error(`Invalid header mapping kind: ${String(_exhaustive)}`);
            }
        }
    }
    return headers;
}
function sortedEntries(obj) {
    return Object.entries(obj)
        .map(([k, v]) => [k, v])
        .sort(([a], [b]) => a.localeCompare(b));
}
