import { createMcpClient } from "./mcp-client.js";
import { parseMcpConfig, readMcpConfig } from "./mcp-config.js";
import { pauseServer as persistPause, removeServer as persistRemove, restartServer as persistRestart, } from "./mcp-config-writer.js";
import { wrapMcpServerAsToolDefinitions } from "./mcp-tool-bridge.js";
const DEFAULT_TIMEOUT_MS = 10_000;
function emptyCapabilities() {
    return { tools: [], resources: [], prompts: [] };
}
function mcpServerResourceId(config) {
    return config.resourceId ?? config.name;
}
function logCloseError(e, name) {
    const msg = e instanceof Error ? e.message : String(e);
    console.error(`McpHost: error closing client for ${JSON.stringify(name)}: ${msg}`);
}
/**
 * Synchronously removes all entries whose `name` starts with `mcp__` and returns them removed.
 * The shared `customTools` array is mutated in place; call sites must not `await` between
 * this step and re-appending new MCP tools.
 */
function removeMcpOwnedTools(customTools) {
    let w = 0;
    for (let r = 0; r < customTools.length; r++) {
        const t = customTools[r];
        if (t && !t.name.startsWith("mcp__")) {
            customTools[w] = t;
            w++;
        }
    }
    customTools.length = w;
}
export class McpHost {
    cwd;
    customTools;
    configPath;
    configRoot;
    timeoutMs;
    createClient;
    logs;
    _configError;
    /** From the last `readMcpConfig` that succeeded. Empty on parse failure or when file empty. */
    orderedServers = [];
    /** State aligned with `orderedServers` order for `getStatuses()`. */
    lastStates = new Map();
    clients = new Map();
    /** In-flight `restart()`: all concurrent callers share one stop+start cycle. */
    _restartInFlight;
    constructor(options) {
        this.cwd = options.cwd;
        this.customTools = options.customTools;
        this.configPath = options.configPath;
        this.configRoot = options.configRoot;
        this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
        this.createClient = options.createClient ?? createMcpClient;
        this.logs = options.logs;
    }
    log(level, message, details) {
        try {
            this.logs?.[level](message, details);
        }
        catch {
            // Logging must never break MCP startup.
        }
    }
    /**
     * The shared `ToolDefinition[]` from construction (mutated by this host; same
     * instance as the hub / agent `customTools` array when wired from `HubRuntime`).
     */
    getSharedCustomToolsArray() {
        return this.customTools;
    }
    /** Populated when `mcp.json` is missing or not valid JSON, or the parser fails. */
    getConfigError() {
        return this._configError;
    }
    getStatuses() {
        if (this._configError !== undefined && this.orderedServers.length === 0) {
            return [];
        }
        const out = [];
        for (const c of this.orderedServers) {
            const resourceId = mcpServerResourceId(c);
            const s = this.lastStates.get(resourceId);
            if (!s) {
                const row = {
                    resourceId,
                    name: c.name,
                    transport: c.transport,
                    status: "stopped",
                    capabilities: emptyCapabilities(),
                };
                if (c.disabled === true) {
                    row.disabled = true;
                }
                out.push(row);
                continue;
            }
            const row = {
                resourceId,
                name: c.name,
                transport: c.transport,
                status: s.status,
                capabilities: s.capabilities,
            };
            if (s.error !== undefined) {
                row.error = s.error;
            }
            if (c.disabled === true) {
                row.disabled = true;
            }
            out.push(row);
        }
        return out;
    }
    /**
     * `stop()` + `start()` in sequence. Concurrent callers await the same in-flight
     * operation so the host does not interleave `close` and `connect` for multiple cycles.
     */
    async restart() {
        if (this._restartInFlight) {
            await this._restartInFlight;
            return;
        }
        const run = (async () => {
            try {
                await this.stop();
                await this.start();
            }
            finally {
                this._restartInFlight = undefined;
            }
        })();
        this._restartInFlight = run;
        await run;
    }
    async stop() {
        for (const [resourceId, entry] of this.clients) {
            try {
                await entry.handle.close();
            }
            catch (e) {
                logCloseError(e, resourceId);
            }
        }
        this.clients.clear();
        removeMcpOwnedTools(this.customTools);
        for (const c of this.orderedServers) {
            const resourceId = mcpServerResourceId(c);
            const st = this.lastStates.get(resourceId);
            if (st) {
                this.lastStates.set(resourceId, {
                    ...st,
                    status: "stopped",
                    error: undefined,
                    capabilities: emptyCapabilities(),
                });
            }
            else {
                this.lastStates.set(resourceId, {
                    transport: c.transport,
                    status: "stopped",
                    capabilities: emptyCapabilities(),
                });
            }
        }
    }
    async start() {
        for (const [resourceId, entry] of this.clients) {
            try {
                await entry.handle.close();
            }
            catch (e) {
                logCloseError(e, resourceId);
            }
        }
        this.clients.clear();
        this.lastStates.clear();
        this._configError = undefined;
        this.orderedServers = [];
        const read = this.configRoot ? parseMcpConfig(this.configRoot()) : readMcpConfig(this.cwd, this.configPath);
        if (!read.ok) {
            this._configError = read.error;
            this.log("warning", "mcp config error", { error: read.error });
            removeMcpOwnedTools(this.customTools);
            return;
        }
        this.orderedServers = read.servers;
        if (this.orderedServers.length === 0) {
            removeMcpOwnedTools(this.customTools);
            return;
        }
        for (const c of this.orderedServers) {
            if (c.disabled === true) {
                this.lastStates.set(mcpServerResourceId(c), {
                    transport: c.transport,
                    status: "stopped",
                    capabilities: emptyCapabilities(),
                });
            }
        }
        const toConnect = [];
        for (const c of this.orderedServers) {
            if (c.disabled !== true) {
                toConnect.push(c);
            }
        }
        const results = await Promise.allSettled(toConnect.map((cfg) => this.createClient(cfg, { timeoutMs: cfg.timeoutMs ?? this.timeoutMs })));
        const newMcpTools = [];
        for (let i = 0; i < toConnect.length; i++) {
            const cfg = toConnect[i];
            const resourceId = mcpServerResourceId(cfg);
            const settled = results[i];
            if (settled.status === "rejected") {
                const err = settled.reason;
                const msg = err instanceof Error ? err.message : String(err);
                this.log("error", "mcp server error", {
                    mcpServer: cfg.name,
                    resourceId,
                    transport: cfg.transport,
                    timeoutMs: cfg.timeoutMs ?? this.timeoutMs,
                    error: msg,
                });
                this.lastStates.set(resourceId, {
                    transport: cfg.transport,
                    status: "error",
                    error: msg,
                    capabilities: emptyCapabilities(),
                });
                continue;
            }
            const handle = settled.value;
            this.clients.set(resourceId, { handle });
            this.lastStates.set(resourceId, {
                transport: cfg.transport,
                status: "running",
                capabilities: handle.capabilities,
            });
            const { tools: wrapped } = wrapMcpServerAsToolDefinitions(resourceId, handle);
            for (const t of wrapped) {
                newMcpTools.push(t);
            }
        }
        removeMcpOwnedTools(this.customTools);
        for (const t of newMcpTools) {
            this.customTools.push(t);
        }
    }
    requireKnownServerInConfigOrFail(resourceId) {
        const read = this.configRoot ? parseMcpConfig(this.configRoot()) : readMcpConfig(this.cwd, this.configPath);
        if (!read.ok) {
            return { ok: false, error: read.error };
        }
        if (!read.servers.some((s) => mcpServerResourceId(s) === resourceId)) {
            return { ok: false, error: `Unknown MCP server resourceId: ${JSON.stringify(resourceId)}` };
        }
        return null;
    }
    async pauseServer(resourceId) {
        const pre = this.requireKnownServerInConfigOrFail(resourceId);
        if (pre) {
            return pre;
        }
        const w = persistPause(this.cwd, resourceId, this.configPath);
        if (!w.ok) {
            return w;
        }
        await this.restart();
        return { ok: true, servers: this.getStatuses() };
    }
    async restartServer(resourceId) {
        const pre = this.requireKnownServerInConfigOrFail(resourceId);
        if (pre) {
            return pre;
        }
        const w = persistRestart(this.cwd, resourceId, this.configPath);
        if (!w.ok) {
            return w;
        }
        await this.restart();
        return { ok: true, servers: this.getStatuses() };
    }
    async removeServer(resourceId) {
        const pre = this.requireKnownServerInConfigOrFail(resourceId);
        if (pre) {
            return pre;
        }
        const w = persistRemove(this.cwd, resourceId, this.configPath);
        if (!w.ok) {
            return w;
        }
        await this.restart();
        return { ok: true, servers: this.getStatuses() };
    }
}
//# sourceMappingURL=mcp-host.js.map