"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.listMcpServers = listMcpServers;
exports.upsertMcpServer = upsertMcpServer;
exports.removeMcpServer = removeMcpServer;
exports.startMcpServer = startMcpServer;
exports.discoverMcpServerTools = discoverMcpServerTools;
exports.invokeMcpServerTool = invokeMcpServerTool;
exports.stopMcpServer = stopMcpServer;
exports.stopAllMcpServers = stopAllMcpServers;
exports.readMcpServerDefinitions = readMcpServerDefinitions;
exports.readMcpServerDefinitionsSync = readMcpServerDefinitionsSync;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs/promises"));
const fsSync = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const atomicFile_1 = require("../persistence/atomicFile");
const mcpClient_1 = require("./mcpClient");
const MCP_SERVERS_PATH = path.join('.harness', 'mcp', 'servers.json');
const MAX_STDERR_LENGTH = 4_000;
const runningServers = new Map();
const lastExitCodes = new Map();
async function listMcpServers(projectDir) {
    const definitions = await readMcpServerDefinitions(projectDir);
    return definitions.map((definition) => toStatus(definition));
}
async function upsertMcpServer(projectDir, input) {
    const definition = sanitizeMcpServerDefinition(input);
    await (0, atomicFile_1.withFileLock)(path.join(projectDir, MCP_SERVERS_PATH), async () => {
        const definitions = await readMcpServerDefinitions(projectDir);
        const existingIndex = definitions.findIndex((item) => item.id === definition.id);
        if (existingIndex >= 0)
            definitions[existingIndex] = definition;
        else
            definitions.push(definition);
        await writeMcpServerDefinitionsUnlocked(projectDir, definitions);
    });
    return toStatus(definition);
}
async function removeMcpServer(projectDir, id) {
    const normalizedId = normalizeMcpId(id);
    if (!normalizedId)
        return false;
    await stopMcpServer(normalizedId);
    return (0, atomicFile_1.withFileLock)(path.join(projectDir, MCP_SERVERS_PATH), async () => {
        const definitions = await readMcpServerDefinitions(projectDir);
        const next = definitions.filter((item) => item.id !== normalizedId);
        if (next.length === definitions.length)
            return false;
        await writeMcpServerDefinitionsUnlocked(projectDir, next);
        return true;
    });
}
async function startMcpServer(projectDir, id) {
    const definition = await findMcpServerDefinition(projectDir, id);
    if (!definition)
        throw new Error('MCP server not found.');
    const active = runningServers.get(definition.id);
    if (active) {
        if (!active.process.killed) {
            try {
                active.process.kill('SIGTERM');
            }
            catch { /* already exiting */ }
        }
        runningServers.delete(definition.id);
    }
    const cwd = resolveMcpCwd(projectDir, definition.cwd);
    const launch = resolveMcpLaunch(definition.command, definition.args);
    const child = (0, child_process_1.spawn)(launch.command, launch.args, {
        cwd,
        env: buildMcpProcessEnv(definition.env),
        windowsHide: true,
    });
    const record = { process: child, client: new mcpClient_1.McpStdioClient(child), startedAt: new Date().toISOString() };
    runningServers.set(definition.id, record);
    lastExitCodes.delete(definition.id);
    child.stderr.on('data', (chunk) => {
        const text = String(chunk);
        record.lastError = (record.lastError ? record.lastError + text : text).slice(-MAX_STDERR_LENGTH);
    });
    child.on('error', (error) => {
        record.lastError = error.message;
    });
    child.on('exit', (code) => {
        lastExitCodes.set(definition.id, code);
        runningServers.delete(definition.id);
    });
    return toStatus(definition);
}
async function discoverMcpServerTools(projectDir, id) {
    const definition = await findMcpServerDefinition(projectDir, id);
    if (!definition)
        throw new Error('MCP server not found.');
    const active = runningServers.get(definition.id);
    if (!active || active.process.killed)
        throw new Error('MCP server is not running.');
    const tools = await active.client.listTools();
    await (0, atomicFile_1.withFileLock)(path.join(projectDir, MCP_SERVERS_PATH), async () => {
        const definitions = await readMcpServerDefinitions(projectDir);
        const index = definitions.findIndex((item) => item.id === definition.id);
        if (index >= 0) {
            definitions[index] = { ...definitions[index], tools };
            await writeMcpServerDefinitionsUnlocked(projectDir, definitions);
        }
    });
    return toStatus({ ...definition, tools });
}
async function invokeMcpServerTool(projectDir, serverId, toolName, input) {
    const definition = await findMcpServerDefinition(projectDir, serverId);
    if (!definition)
        throw new Error('MCP server not found.');
    if (!definition.enabled)
        throw new Error('MCP server is disabled.');
    const configured = definition.tools.find((tool) => tool.name === toolName);
    if (!configured)
        throw new Error('MCP tool is not configured. Discover tools before invocation.');
    const active = runningServers.get(definition.id);
    if (!active || active.process.killed)
        throw new Error('MCP server is not running.');
    return active.client.callTool(toolName, input);
}
async function stopMcpServer(id) {
    const normalizedId = normalizeMcpId(id);
    const active = normalizedId ? runningServers.get(normalizedId) : undefined;
    if (!active)
        return false;
    const exitPromise = new Promise((resolve) => {
        active.process.once('exit', () => resolve());
    });
    active.process.kill();
    runningServers.delete(normalizedId);
    await Promise.race([exitPromise, new Promise((resolve) => setTimeout(resolve, 250))]);
    return true;
}
async function stopAllMcpServers() {
    for (const id of Array.from(runningServers.keys()))
        await stopMcpServer(id);
}
async function readMcpServerDefinitions(projectDir) {
    try {
        const raw = await fs.readFile(path.join(projectDir, MCP_SERVERS_PATH), 'utf-8');
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed))
            return [];
        return parsed.map((item) => sanitizeMcpServerDefinition(item));
    }
    catch {
        return [];
    }
}
function readMcpServerDefinitionsSync(projectDir) {
    try {
        const raw = fsSync.readFileSync(path.join(projectDir, MCP_SERVERS_PATH), 'utf-8');
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed))
            return [];
        return parsed.map((item) => sanitizeMcpServerDefinition(item));
    }
    catch {
        return [];
    }
}
async function findMcpServerDefinition(projectDir, id) {
    const normalizedId = normalizeMcpId(id);
    if (!normalizedId)
        return undefined;
    return (await readMcpServerDefinitions(projectDir)).find((item) => item.id === normalizedId);
}
async function writeMcpServerDefinitions(projectDir, definitions) {
    const filePath = path.join(projectDir, MCP_SERVERS_PATH);
    await (0, atomicFile_1.withFileLock)(filePath, () => writeMcpServerDefinitionsUnlocked(projectDir, definitions));
}
// Internal: write without taking the lock. Callers must already hold the
// lock for path.join(projectDir, MCP_SERVERS_PATH).
async function writeMcpServerDefinitionsUnlocked(projectDir, definitions) {
    const filePath = path.join(projectDir, MCP_SERVERS_PATH);
    await (0, atomicFile_1.atomicWriteFile)(filePath, JSON.stringify(definitions, null, 2));
}
function sanitizeMcpServerDefinition(input) {
    const id = normalizeMcpId(input.id);
    if (!id)
        throw new Error('MCP server id is required and may contain only letters, numbers, dots, underscores, and dashes.');
    const command = String(input.command ?? '').trim();
    if (!command)
        throw new Error('MCP server command is required.');
    const args = Array.isArray(input.args) ? input.args.map((arg) => String(arg)).slice(0, 50) : [];
    const env = sanitizeStringRecord(input.env);
    return {
        id,
        catalogName: typeof input.catalogName === 'string' ? input.catalogName.trim().slice(0, 80) || undefined : undefined,
        command: command.slice(0, 260),
        args,
        cwd: typeof input.cwd === 'string' ? input.cwd.trim().slice(0, 260) || undefined : undefined,
        env,
        enabled: input.enabled !== false,
        tools: sanitizeConfiguredTools(input.tools),
    };
}
function sanitizeConfiguredTools(value) {
    if (!Array.isArray(value))
        return [];
    const tools = [];
    for (const item of value.slice(0, 100)) {
        const raw = item && typeof item === 'object' ? item : {};
        const name = normalizeMcpId(raw.name);
        if (!name)
            continue;
        tools.push({
            name,
            description: typeof raw.description === 'string' ? raw.description.trim().slice(0, 500) || undefined : undefined,
            inputSchema: raw.inputSchema && typeof raw.inputSchema === 'object' ? raw.inputSchema : undefined,
        });
    }
    return tools;
}
function sanitizeStringRecord(value) {
    if (!value || typeof value !== 'object')
        return undefined;
    const output = {};
    for (const [key, rawValue] of Object.entries(value).slice(0, 50)) {
        const normalizedKey = key.trim();
        if (!/^[A-Z_][A-Z0-9_]*$/i.test(normalizedKey))
            continue;
        output[normalizedKey] = String(rawValue).slice(0, 2_000);
    }
    return Object.keys(output).length ? output : undefined;
}
function normalizeMcpId(value) {
    const id = String(value ?? '').trim().toLowerCase();
    return /^[a-z0-9._-]{1,80}$/.test(id) ? id : '';
}
function resolveMcpCwd(projectDir, cwd) {
    if (!cwd)
        return projectDir;
    const resolved = path.resolve(projectDir, cwd);
    const relative = path.relative(projectDir, resolved);
    if (relative.startsWith('..') || path.isAbsolute(relative))
        throw new Error('MCP server cwd must stay inside the project directory.');
    return resolved;
}
function resolveCommandForPlatform(command) {
    if (os.platform() !== 'win32')
        return command;
    if (/^(npm|npx|pnpm|yarn|uvx)$/i.test(command))
        return `${command}.cmd`;
    return command;
}
function resolveMcpLaunch(command, args) {
    const resolvedCommand = resolveCommandForPlatform(command);
    if (os.platform() !== 'win32' || !/\.(?:cmd|bat)$/i.test(resolvedCommand)) {
        return { command: resolvedCommand, args };
    }
    return {
        command: process.env.ComSpec || 'cmd.exe',
        args: ['/d', '/s', '/c', [resolvedCommand, ...args].map(quoteWindowsCommandArg).join(' ')],
    };
}
function quoteWindowsCommandArg(value) {
    if (/^[A-Za-z0-9_/:.=+-]+$/.test(value))
        return value;
    return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, '$&$&')}"`;
}
function buildMcpProcessEnv(extraEnv) {
    const env = {};
    for (const [key, value] of Object.entries(process.env)) {
        if (!isValidEnvKey(key))
            continue;
        if (!isValidEnvValue(value))
            continue;
        env[key] = value;
    }
    for (const [key, value] of Object.entries(extraEnv || {})) {
        if (!isValidEnvKey(key))
            continue;
        if (!isValidEnvValue(value))
            continue;
        env[key] = value;
    }
    return env;
}
function isValidEnvKey(key) {
    return Boolean(key) && !key.includes('=') && !key.includes('\0');
}
function isValidEnvValue(value) {
    return typeof value === 'string' && !value.includes('\0');
}
function toStatus(definition) {
    const active = runningServers.get(definition.id);
    return {
        ...definition,
        running: Boolean(active && !active.process.killed),
        pid: active?.process.pid,
        startedAt: active?.startedAt,
        lastExitCode: lastExitCodes.get(definition.id),
        lastError: active?.lastError,
    };
}
//# sourceMappingURL=mcpRuntime.js.map