import { z } from "zod";
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { loadDynamicMcpServers, saveDynamicMcpServers } from "../config/index.js";
import { getAgentState } from "../agent/state.js";
import { logger } from "../logger.js";
export const MAX_DYNAMIC_MCP_SERVERS = 10;
const RESERVED_NAMES = new Set(["visionclaw", "playwright", "playwriter", "phonecall"]);
const stringMapEntrySchema = z.object({
    key: z
        .string()
        .min(1)
        .describe("Environment variable or header name"),
    value: z
        .string()
        .describe("Environment variable or header value"),
});
function buildSoftLimitWarning(currentCount) {
    if (currentCount >= MAX_DYNAMIC_MCP_SERVERS) {
        return `\n\nWarning: You have ${currentCount} dynamic MCP servers configured. Adding more may degrade performance due to increased context usage and resource consumption. Consider removing unused servers first.`;
    }
    return "";
}
export const manageMcpServersTool = tool("manage_mcp_servers", "Manage external MCP (Model Context Protocol) servers at runtime. You can add new servers (stdio, http, sse), remove existing ones, or list all configured servers with their status. Changes are persisted and take effect on the next wake cycle.", {
    action: z
        .enum(["add", "remove", "list"])
        .describe("Action to perform"),
    name: z
        .string()
        .regex(/^[a-z0-9][a-z0-9-]*$/)
        .optional()
        .describe("Server name (required for add/remove). Lowercase alphanumeric with hyphens."),
    type: z
        .enum(["stdio", "http", "sse"])
        .optional()
        .describe("Server transport type (required for add)"),
    command: z
        .string()
        .optional()
        .describe("Command to run (required for stdio type, e.g. 'npx')"),
    args: z
        .array(z.string())
        .optional()
        .describe("Command arguments (optional, for stdio type)"),
    env: z
        .array(stringMapEntrySchema)
        .optional()
        .describe("Environment variables as key/value pairs (optional, for stdio type)"),
    url: z
        .string()
        .optional()
        .describe("Server URL (required for http/sse type)"),
    headers: z
        .array(stringMapEntrySchema)
        .optional()
        .describe("HTTP headers as key/value pairs (optional, for http/sse type)"),
}, async (args) => {
    try {
        switch (args.action) {
            case "add":
                return handleAdd(args);
            case "remove":
                return await handleRemove(args);
            case "list":
                return await handleList();
        }
    }
    catch (err) {
        return {
            content: [{
                    type: "text",
                    text: `MCP server operation failed: ${err instanceof Error ? err.message : String(err)}`,
                }],
            isError: true,
        };
    }
});
function handleAdd(args) {
    if (!args.name) {
        return {
            content: [{ type: "text", text: "Error: name is required for add." }],
            isError: true,
        };
    }
    if (!args.type) {
        return {
            content: [{ type: "text", text: "Error: type is required for add (stdio, http, or sse)." }],
            isError: true,
        };
    }
    if (RESERVED_NAMES.has(args.name)) {
        return {
            content: [{ type: "text", text: `Error: "${args.name}" is a reserved server name and cannot be added.` }],
            isError: true,
        };
    }
    let serverConfig;
    const envRecord = args.env ? Object.fromEntries(args.env.map((entry) => [entry.key, entry.value])) : undefined;
    const headerRecord = args.headers
        ? Object.fromEntries(args.headers.map((entry) => [entry.key, entry.value]))
        : undefined;
    if (args.type === "stdio") {
        if (!args.command) {
            return {
                content: [{ type: "text", text: "Error: command is required for stdio type." }],
                isError: true,
            };
        }
        serverConfig = {
            command: args.command,
            ...(args.args ? { args: args.args } : {}),
            ...(envRecord ? { env: envRecord } : {}),
        };
    }
    else {
        if (!args.url) {
            return {
                content: [{ type: "text", text: `Error: url is required for ${args.type} type.` }],
                isError: true,
            };
        }
        serverConfig = {
            type: args.type,
            url: args.url,
            ...(headerRecord ? { headers: headerRecord } : {}),
        };
    }
    const servers = loadDynamicMcpServers();
    const existed = args.name in servers;
    servers[args.name] = serverConfig;
    saveDynamicMcpServers(servers);
    updateSessionDynamicServers(servers);
    const currentCount = Object.keys(servers).length;
    let text = `MCP server "${args.name}" ${existed ? "updated" : "added"} (${args.type}). It will be available on the next wake cycle.`;
    text += buildSoftLimitWarning(currentCount);
    return { content: [{ type: "text", text }] };
}
async function handleRemove(args) {
    if (!args.name) {
        return {
            content: [{ type: "text", text: "Error: name is required for remove." }],
            isError: true,
        };
    }
    if (RESERVED_NAMES.has(args.name)) {
        return {
            content: [{ type: "text", text: `Error: "${args.name}" is a reserved server name and cannot be removed.` }],
            isError: true,
        };
    }
    const servers = loadDynamicMcpServers();
    if (!(args.name in servers)) {
        return {
            content: [{ type: "text", text: `MCP server "${args.name}" not found in dynamic servers.` }],
            isError: true,
        };
    }
    const { [args.name]: _removed, ...remaining } = servers;
    saveDynamicMcpServers(remaining);
    updateSessionDynamicServers(remaining);
    const disabled = await disableServerInRunningQuery(args.name);
    const suffix = disabled
        ? " Disabled in current session."
        : " Will be fully disconnected on the next wake cycle.";
    return {
        content: [{ type: "text", text: `MCP server "${args.name}" removed.${suffix}` }],
    };
}
async function handleList() {
    const servers = loadDynamicMcpServers();
    const serverNames = Object.keys(servers);
    const lines = [];
    lines.push(`Dynamic MCP servers: ${serverNames.length} configured`);
    if (serverNames.length > 0) {
        lines.push("");
        for (const name of serverNames) {
            const cfg = servers[name];
            const cfgType = "type" in cfg ? cfg.type : "stdio";
            lines.push(`- ${name} (${cfgType})`);
        }
    }
    let liveStatuses;
    try {
        const state = getAgentState();
        const q = state.sessionManager?.activeSession.getCurrentQuery();
        if (q?.mcpServerStatus) {
            liveStatuses = await q.mcpServerStatus();
        }
    }
    catch {
        // Agent state not available or query not running
    }
    if (liveStatuses && liveStatuses.length > 0) {
        lines.push("");
        lines.push("Live server statuses:");
        for (const s of liveStatuses) {
            const toolCount = s.tools?.length ?? 0;
            lines.push(`- ${s.name}: ${s.status} (${toolCount} tools)`);
        }
    }
    return { content: [{ type: "text", text: lines.join("\n") }] };
}
/**
 * Update the session's in-memory dynamic server record so the next query()
 * call includes the updated servers. Does NOT call setMcpServers() on the
 * running query — that replaces ALL dynamically-added servers (including
 * visionclaw), which breaks the current session.
 */
function updateSessionDynamicServers(servers) {
    try {
        const state = getAgentState();
        const session = state.sessionManager?.activeSession;
        if (session) {
            session.setDynamicMcpServers(servers);
        }
    }
    catch {
        // Agent state not yet initialized
    }
}
/**
 * Disable a specific server in the running query via toggleMcpServer().
 * Unlike setMcpServers(), this targets a single server without replacing
 * the entire dynamic set, so it's safe to call mid-session.
 */
async function disableServerInRunningQuery(serverName) {
    try {
        const state = getAgentState();
        const q = state.sessionManager?.activeSession.getCurrentQuery();
        if (!q?.toggleMcpServer)
            return false;
        await q.toggleMcpServer(serverName, false);
        return true;
    }
    catch (err) {
        logger.warn(`Failed to disable MCP server "${serverName}" in running query: ${err instanceof Error ? err.message : String(err)}`);
        return false;
    }
}
//# sourceMappingURL=mcp-manager.js.map