"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.McpStdioClient = void 0;
const child_process_1 = require("child_process");
const readline = __importStar(require("readline"));
const log_1 = require("../log");
const log = (0, log_1.createLogger)('mcp/stdio-client');
class McpStdioClient {
    proc;
    nextId = 1;
    pending = new Map();
    closed = false;
    constructor(proc) {
        this.proc = proc;
    }
    static async spawn(params) {
        const proc = (0, child_process_1.spawn)(params.command, params.args, {
            stdio: 'pipe',
            env: params.env,
            cwd: params.cwd,
            windowsHide: true,
        });
        proc.stdout.setEncoding('utf8');
        proc.stderr.setEncoding('utf8');
        const client = new McpStdioClient(proc);
        client.attachIo();
        return client;
    }
    attachIo() {
        const rl = readline.createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
        rl.on('line', (line) => {
            const trimmed = line.trim();
            if (!trimmed)
                return;
            this.handleLine(trimmed);
        });
        rl.on('close', () => {
            // stdout closed. Process exit handler will finish cleanup.
        });
        this.proc.stderr.on('data', (chunk) => {
            const text = chunk.trimEnd();
            if (!text)
                return;
            log.debug(`[stderr] ${text}`);
        });
        this.proc.on('exit', (code, signal) => {
            const msg = `MCP process exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`;
            if (!this.closed) {
                log.warn(msg);
            }
            else {
                log.debug(msg);
            }
            this.closed = true;
            for (const [id, p] of this.pending.entries()) {
                clearTimeout(p.timeout);
                p.reject(new Error(`MCP request ${id} (${p.method}) failed: process exited`));
            }
            this.pending.clear();
        });
        this.proc.on('error', (err) => {
            log.warn('MCP process error', err);
        });
    }
    handleLine(line) {
        let parsed;
        try {
            parsed = JSON.parse(line);
        }
        catch (err) {
            log.warn('Failed to parse MCP JSON-RPC line as JSON', undefined, { line, err });
            return;
        }
        if (!isRecord(parsed)) {
            log.warn('Ignoring MCP line: not an object', undefined, { line });
            return;
        }
        const jsonrpc = parsed.jsonrpc;
        if (jsonrpc !== '2.0') {
            log.warn('Ignoring MCP line: jsonrpc is not 2.0', undefined, { line });
            return;
        }
        if ('id' in parsed) {
            const id = parsed.id;
            if (typeof id !== 'number') {
                log.warn('Ignoring MCP response: id is not a number', undefined, { line });
                return;
            }
            const pending = this.pending.get(id);
            if (!pending) {
                log.warn('Ignoring MCP response: no pending request', undefined, { id });
                return;
            }
            this.pending.delete(id);
            clearTimeout(pending.timeout);
            const errVal = 'error' in parsed ? parsed.error : undefined;
            const resultVal = 'result' in parsed ? parsed.result : undefined;
            if (errVal !== undefined) {
                pending.reject(new Error(formatJsonRpcError(errVal)));
                return;
            }
            pending.resolve(resultVal);
            return;
        }
        if ('method' in parsed && typeof parsed.method === 'string') {
            // Notification: ignored for now.
            return;
        }
        log.warn('Ignoring MCP line: not a response or notification', undefined, { line });
    }
    async initialize(timeoutMs = 15_000) {
        // Best-effort MCP initialize handshake.
        // If servers require a different protocolVersion, they will reject and we'll surface the error.
        await this.request('initialize', {
            protocolVersion: '2024-11-05',
            capabilities: {},
            clientInfo: { name: 'dominds', version: 'dev' },
        }, timeoutMs);
        await this.notify('notifications/initialized', undefined);
    }
    async listTools(timeoutMs = 15_000) {
        const all = [];
        let cursor;
        for (let i = 0; i < 50; i++) {
            const res = await this.request('tools/list', cursor ? { cursor } : {}, timeoutMs);
            const parsed = parseToolsListResult(res);
            all.push(...parsed.tools);
            cursor = parsed.nextCursor;
            if (!cursor)
                break;
        }
        return all;
    }
    async callTool(name, args, timeoutMs = 60_000) {
        const res = await this.request('tools/call', { name, arguments: args }, timeoutMs);
        return stringifyToolCallResult(res);
    }
    async close() {
        this.closed = true;
        try {
            this.proc.kill();
        }
        catch (err) {
            log.debug('Failed to kill MCP process', err);
        }
    }
    async notify(method, params) {
        const msg = { jsonrpc: '2.0', method, params };
        this.writeMessage(msg);
    }
    async request(method, params, timeoutMs) {
        if (this.closed) {
            throw new Error('MCP client is closed');
        }
        const id = this.nextId++;
        const msg = { jsonrpc: '2.0', id, method, params };
        return await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.pending.delete(id);
                reject(new Error(`MCP request timeout (${method})`));
            }, timeoutMs);
            this.pending.set(id, {
                resolve,
                reject: (e) => reject(e),
                timeout,
                method,
            });
            this.writeMessage(msg);
        });
    }
    writeMessage(msg) {
        const line = JSON.stringify(msg);
        try {
            this.proc.stdin.write(`${line}\n`, 'utf8');
        }
        catch (err) {
            log.warn('Failed to write MCP message to stdin', err);
        }
    }
}
exports.McpStdioClient = McpStdioClient;
function parseToolsListResult(value) {
    const obj = asRecord(value, 'tools/list result');
    const toolsVal = obj.tools;
    if (!Array.isArray(toolsVal)) {
        throw new Error(`Invalid MCP tools/list result: expected tools[]`);
    }
    const tools = [];
    for (const t of toolsVal) {
        const toolObj = asRecord(t, 'tools/list tool');
        const name = toolObj.name;
        if (typeof name !== 'string' || !name.trim()) {
            throw new Error(`Invalid MCP tool entry: missing name`);
        }
        const description = typeof toolObj.description === 'string' ? toolObj.description : undefined;
        const inputSchemaVal = toolObj.inputSchema;
        if (!isRecord(inputSchemaVal) || Array.isArray(inputSchemaVal)) {
            throw new Error(`Invalid MCP tool entry '${name}': inputSchema must be an object`);
        }
        tools.push({ name, description, inputSchema: inputSchemaVal });
    }
    const nextCursor = typeof obj.nextCursor === 'string' && obj.nextCursor.trim()
        ? obj.nextCursor
        : undefined;
    return { tools, nextCursor };
}
function stringifyToolCallResult(value) {
    if (!isRecord(value) || Array.isArray(value)) {
        return JSON.stringify(value);
    }
    const contentVal = 'content' in value ? value.content : undefined;
    if (!Array.isArray(contentVal)) {
        return JSON.stringify(value);
    }
    const parts = [];
    for (const item of contentVal) {
        if (isRecord(item) && !Array.isArray(item)) {
            const t = item.type;
            if (t === 'text' && typeof item.text === 'string') {
                parts.push(item.text);
                continue;
            }
        }
        parts.push(JSON.stringify(item));
    }
    return parts.join('\n').trim();
}
function formatJsonRpcError(errVal) {
    if (!isRecord(errVal) || Array.isArray(errVal)) {
        return `MCP error: ${JSON.stringify(errVal)}`;
    }
    const code = 'code' in errVal ? errVal.code : undefined;
    const message = 'message' in errVal ? errVal.message : undefined;
    const data = 'data' in errVal ? errVal.data : undefined;
    const codeStr = typeof code === 'number' ? `code=${code}` : undefined;
    const msgStr = typeof message === 'string' ? message : JSON.stringify(errVal);
    const dataStr = data !== undefined ? ` data=${JSON.stringify(data)}` : '';
    return `MCP error${codeStr ? ` (${codeStr})` : ''}: ${msgStr}${dataStr}`;
}
function asRecord(value, label) {
    if (!isRecord(value) || Array.isArray(value)) {
        throw new Error(`Invalid MCP payload: expected object at ${label}`);
    }
    return value;
}
function isRecord(value) {
    return typeof value === 'object' && value !== null;
}
