"use strict";
// =============================================================================
// Pure Agent - MCP Client
// Ported from Hermes tools/mcp_tool.py
// JSON-RPC over stdio/HTTP for Model Context Protocol
// =============================================================================
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.connectMCPServers = connectMCPServers;
exports.getMCPTools = getMCPTools;
exports.callMCPTool = callMCPTool;
exports.getMCPStatus = getMCPStatus;
exports.disconnectMCPServers = disconnectMCPServers;
exports.isMCPInitialized = isMCPInitialized;
exports.loadMCPConfig = loadMCPConfig;
const child_process_1 = require("child_process");
const events_1 = require("events");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const logger_1 = require("./logger");
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DEFAULT_TOOL_TIMEOUT = 120_000;
const DEFAULT_CONNECT_TIMEOUT = 60_000;
const MAX_RECONNECT_RETRIES = 3;
const MAX_BACKOFF_MS = 60_000;
const CIRCUIT_BREAKER_THRESHOLD = 3;
const CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
const LATEST_PROTOCOL_VERSION = '2025-03-26';
// ---------------------------------------------------------------------------
// JSON-RPC helpers
// ---------------------------------------------------------------------------
let _rpcId = 0;
function nextRpcId() {
    return ++_rpcId;
}
// ---------------------------------------------------------------------------
// MCPServer - manages a single MCP server connection
// ---------------------------------------------------------------------------
class MCPServer extends events_1.EventEmitter {
    name;
    _config;
    _process = null;
    _connected = false;
    _tools = [];
    _error = null;
    _errorCount = 0;
    _breakerOpenedAt = 0;
    _pendingRequests = new Map();
    _buffer = '';
    constructor(name, config) {
        super();
        this.name = name;
        this._config = config;
    }
    get connected() { return this._connected; }
    get tools() { return [...this._tools]; }
    get error() { return this._error; }
    get transport() { return this._config.transport || (this._config.url ? 'http' : 'stdio'); }
    // -------------------------------------------------------------------------
    // Connect
    // -------------------------------------------------------------------------
    async connect() {
        if (this._isBreakerOpen()) {
            throw new Error(`Circuit breaker open for ${this.name}`);
        }
        const transport = this.transport;
        (0, logger_1.getLogger)().info(`MCP: connecting to ${this.name} (${transport})`);
        // Retry with exponential backoff
        let lastError = null;
        for (let attempt = 0; attempt < MAX_RECONNECT_RETRIES; attempt++) {
            try {
                if (transport === 'stdio') {
                    await this._connectStdio();
                }
                else if (transport === 'http' || transport === 'sse') {
                    await this._connectHttp();
                }
                else {
                    throw new Error(`Unknown transport: ${transport}`);
                }
                lastError = null;
                break;
            }
            catch (err) {
                lastError = err;
                if (attempt < MAX_RECONNECT_RETRIES - 1) {
                    const delay = Math.min(1000 * Math.pow(2, attempt), MAX_BACKOFF_MS);
                    (0, logger_1.getLogger)().warn(`MCP ${this.name}: connect attempt ${attempt + 1} failed, retrying in ${delay}ms`);
                    await new Promise(r => setTimeout(r, delay));
                }
            }
        }
        if (lastError)
            throw lastError;
        // Initialize
        try {
            const initResult = await this._sendRequest('initialize', {
                protocolVersion: LATEST_PROTOCOL_VERSION,
                capabilities: {},
                clientInfo: { name: 'PureAgent', version: '1.0.0' },
            });
            // Send initialized notification
            this._sendNotification('notifications/initialized', {});
            // Discover tools
            await this._discoverTools();
            this._connected = true;
            this._error = null;
            this._errorCount = 0;
            (0, logger_1.getLogger)().info(`MCP: ${this.name} connected, ${this._tools.length} tools`);
        }
        catch (err) {
            this._error = err.message;
            this._bumpError();
            throw err;
        }
    }
    // -------------------------------------------------------------------------
    // stdio transport
    // -------------------------------------------------------------------------
    async _connectStdio() {
        const command = this._config.command;
        if (!command)
            throw new Error('No command specified for stdio transport');
        const args = this._config.args || [];
        const env = { ...process.env, ...this._config.env };
        // Resolve command (handle npx, node, etc.)
        let resolvedCommand = command;
        let resolvedArgs = args;
        if (command === 'npx') {
            resolvedCommand = isWindows() ? 'npx.cmd' : 'npx';
        }
        else if (command === 'node') {
            resolvedCommand = process.execPath;
        }
        this._process = (0, child_process_1.spawn)(resolvedCommand, resolvedArgs, {
            env,
            stdio: ['pipe', 'pipe', 'pipe'],
            windowsHide: true,
        });
        this._process.stdout?.on('data', (data) => {
            this._buffer += data.toString('utf-8');
            this._processBuffer();
        });
        this._process.stderr?.on('data', (data) => {
            (0, logger_1.getLogger)().debug(`MCP ${this.name} stderr: ${data.toString('utf-8').slice(0, 200)}`);
        });
        this._process.on('close', (code) => {
            (0, logger_1.getLogger)().info(`MCP ${this.name} process exited with code ${code}`);
            this._connected = false;
            this._rejectAllPending(new Error(`Process exited with code ${code}`));
        });
        this._process.on('error', (err) => {
            (0, logger_1.getLogger)().error(`MCP ${this.name} process error: ${err.message}`);
            this._connected = false;
            this._error = err.message;
            this._rejectAllPending(err);
        });
    }
    _processBuffer() {
        // MCP stdio uses newline-delimited JSON
        const lines = this._buffer.split('\n');
        this._buffer = lines.pop() || '';
        for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed)
                continue;
            try {
                const msg = JSON.parse(trimmed);
                if ('id' in msg && msg.id !== null) {
                    // Response
                    const pending = this._pendingRequests.get(msg.id);
                    if (pending) {
                        clearTimeout(pending.timer);
                        this._pendingRequests.delete(msg.id);
                        if (msg.error) {
                            pending.reject(new Error(msg.error.message || 'RPC error'));
                        }
                        else {
                            pending.resolve(msg.result);
                        }
                    }
                }
                else if ('method' in msg) {
                    // Notification from server
                    this._handleNotification(msg);
                }
            }
            catch (err) {
                (0, logger_1.getLogger)().warn(`MCP ${this.name}: failed to parse JSON: ${trimmed.slice(0, 100)}`);
            }
        }
    }
    // -------------------------------------------------------------------------
    // HTTP/SSE transport
    // -------------------------------------------------------------------------
    async _connectHttp() {
        const url = this._config.url;
        if (!url)
            throw new Error('No URL specified for HTTP transport');
        // Test connection with initialize
        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    jsonrpc: '2.0',
                    id: nextRpcId(),
                    method: 'initialize',
                    params: {
                        protocolVersion: LATEST_PROTOCOL_VERSION,
                        capabilities: {},
                        clientInfo: { name: 'PureAgent', version: '1.0.0' },
                    },
                }),
                signal: AbortSignal.timeout(DEFAULT_CONNECT_TIMEOUT),
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            this._connected = true;
            (0, logger_1.getLogger)().info(`MCP ${this.name}: HTTP transport connected`);
            // Start SSE listener if the server supports it
            this._startSSEListener(url).catch(() => {
                // SSE not supported - that's OK, we'll use request-response
            });
        }
        catch (err) {
            throw new Error(`HTTP connect failed: ${err.message}`);
        }
    }
    async _startSSEListener(baseUrl) {
        // Some MCP servers support SSE for server-initiated notifications
        const sseUrl = baseUrl.replace(/\/$/, '') + '/sse';
        try {
            const response = await fetch(sseUrl, {
                headers: { 'Accept': 'text/event-stream' },
                signal: AbortSignal.timeout(5000),
            });
            if (!response.ok || !response.body)
                return;
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let eventBuffer = '';
            const readLoop = async () => {
                try {
                    while (this._connected) {
                        const { done, value } = await reader.read();
                        if (done)
                            break;
                        eventBuffer += decoder.decode(value, { stream: true });
                        const lines = eventBuffer.split('\n');
                        eventBuffer = lines.pop() || '';
                        let eventType = '';
                        for (const line of lines) {
                            if (line.startsWith('event: ')) {
                                eventType = line.slice(7).trim();
                            }
                            else if (line.startsWith('data: ')) {
                                const data = line.slice(6);
                                try {
                                    const msg = JSON.parse(data);
                                    if (eventType === 'message' || eventType === 'notification') {
                                        this._handleNotification(msg);
                                    }
                                }
                                catch { /* ignore parse errors */ }
                            }
                        }
                    }
                }
                catch {
                    // Stream ended or errored - that's OK
                }
                finally {
                    reader.releaseLock();
                }
            };
            readLoop();
            (0, logger_1.getLogger)().debug(`MCP ${this.name}: SSE listener started`);
        }
        catch {
            // SSE endpoint not available - that's fine
        }
    }
    // -------------------------------------------------------------------------
    // Tool discovery
    // -------------------------------------------------------------------------
    async _discoverTools() {
        const result = await this._sendRequest('tools/list', {});
        const rawTools = result?.tools || [];
        this._tools = rawTools.map((t) => ({
            name: `mcp_${sanitizeName(this.name)}_${sanitizeName(t.name)}`,
            description: t.description || '',
            inputSchema: normalizeSchema(t.inputSchema),
            serverName: this.name,
        }));
    }
    // -------------------------------------------------------------------------
    // Tool call
    // -------------------------------------------------------------------------
    async callTool(toolName, args, timeout) {
        // Strip our prefix to get the MCP tool name
        const prefix = `mcp_${sanitizeName(this.name)}_`;
        const mcpToolName = toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
        const result = await this._sendRequest('tools/call', {
            name: mcpToolName,
            arguments: args,
        }, timeout || this._config.tool_timeout || DEFAULT_TOOL_TIMEOUT);
        // Extract text content from MCP result
        if (result?.content) {
            if (Array.isArray(result.content)) {
                return result.content
                    .filter((c) => c.type === 'text')
                    .map((c) => c.text)
                    .join('\n');
            }
            return JSON.stringify(result.content);
        }
        return JSON.stringify(result);
    }
    // -------------------------------------------------------------------------
    // JSON-RPC communication
    // -------------------------------------------------------------------------
    async _sendRequest(method, params, timeout = DEFAULT_TOOL_TIMEOUT) {
        const id = nextRpcId();
        const request = { jsonrpc: '2.0', id, method, params };
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                this._pendingRequests.delete(id);
                reject(new Error(`RPC timeout: ${method} (${timeout}ms)`));
            }, timeout);
            this._pendingRequests.set(id, { resolve, reject, timer });
            this._writeMessage(request);
        });
    }
    _sendNotification(method, params) {
        const notification = { jsonrpc: '2.0', method, params };
        this._writeMessage(notification);
    }
    _writeMessage(msg) {
        const json = JSON.stringify(msg) + '\n';
        if (this._process?.stdin && !this._process.stdin.destroyed) {
            this._process.stdin.write(json);
        }
        else if (this.transport === 'http') {
            // HTTP: send via fetch
            this._sendHttpRequest(msg).catch(err => {
                (0, logger_1.getLogger)().error(`MCP ${this.name} HTTP error: ${err.message}`);
            });
        }
    }
    async _sendHttpRequest(msg) {
        const url = this._config.url;
        if (!url)
            throw new Error('No URL for HTTP transport');
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' },
            body: JSON.stringify(msg),
            signal: AbortSignal.timeout(DEFAULT_TOOL_TIMEOUT),
        });
        if (!response.ok) {
            const errorText = await response.text().catch(() => 'Unknown error');
            throw new Error(`HTTP ${response.status}: ${errorText}`);
        }
        const contentType = response.headers.get('content-type') || '';
        // Handle SSE response (Streamable HTTP MCP)
        if (contentType.includes('text/event-stream')) {
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let finalResult = null;
            try {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done)
                        break;
                    buffer += decoder.decode(value, { stream: true });
                    const lines = buffer.split('\n');
                    buffer = lines.pop() || '';
                    for (const line of lines) {
                        const trimmed = line.trim();
                        if (!trimmed.startsWith('data: '))
                            continue;
                        try {
                            const data = JSON.parse(trimmed.slice(6));
                            if (data.id === msg.id) {
                                finalResult = data;
                            }
                            else if ('method' in data) {
                                this._handleNotification(data);
                            }
                        }
                        catch { /* ignore */ }
                    }
                }
            }
            finally {
                reader.releaseLock();
            }
            if (finalResult?.error) {
                throw new Error(finalResult.error.message || 'RPC error');
            }
            return finalResult?.result;
        }
        // Handle regular JSON response
        const result = await response.json();
        if (result?.error) {
            throw new Error(result.error.message || 'RPC error');
        }
        return result?.result;
    }
    // -------------------------------------------------------------------------
    // Notification handling
    // -------------------------------------------------------------------------
    _handleNotification(msg) {
        if (msg.method === 'notifications/tools/list_changed') {
            (0, logger_1.getLogger)().info(`MCP ${this.name}: tools changed, rediscovering`);
            this._discoverTools().catch(err => {
                (0, logger_1.getLogger)().error(`MCP ${this.name}: tool rediscovery failed: ${err.message}`);
            });
        }
        this.emit('notification', msg);
    }
    // -------------------------------------------------------------------------
    // Circuit breaker
    // -------------------------------------------------------------------------
    _isBreakerOpen() {
        if (this._errorCount < CIRCUIT_BREAKER_THRESHOLD)
            return false;
        const elapsed = Date.now() - this._breakerOpenedAt;
        if (elapsed >= CIRCUIT_BREAKER_COOLDOWN_MS) {
            this._errorCount = 0; // Half-open: allow retry
            return false;
        }
        return true;
    }
    _bumpError() {
        this._errorCount++;
        if (this._errorCount >= CIRCUIT_BREAKER_THRESHOLD) {
            this._breakerOpenedAt = Date.now();
            (0, logger_1.getLogger)().warn(`MCP ${this.name}: circuit breaker opened after ${this._errorCount} failures`);
        }
    }
    _rejectAllPending(err) {
        for (const [id, pending] of this._pendingRequests) {
            clearTimeout(pending.timer);
            pending.reject(err);
        }
        this._pendingRequests.clear();
    }
    // -------------------------------------------------------------------------
    // Disconnect
    // -------------------------------------------------------------------------
    async disconnect() {
        this._connected = false;
        this._rejectAllPending(new Error('Disconnected'));
        if (this._process) {
            this._process.kill('SIGTERM');
            setTimeout(() => {
                if (this._process && !this._process.killed) {
                    this._process.kill('SIGKILL');
                }
            }, 5000);
            this._process = null;
        }
        (0, logger_1.getLogger)().info(`MCP ${this.name}: disconnected`);
    }
}
// ---------------------------------------------------------------------------
// Module-level state
// ---------------------------------------------------------------------------
const _servers = new Map();
let _initialized = false;
// ---------------------------------------------------------------------------
// Helper functions
// ---------------------------------------------------------------------------
function sanitizeName(value) {
    return value.replace(/[^a-zA-Z0-9_]/g, '_');
}
function normalizeSchema(schema) {
    if (!schema)
        return { type: 'object', properties: {} };
    // Ensure type: "object"
    if (!schema.type)
        return { ...schema, type: 'object' };
    return schema;
}
function isWindows() {
    return process.platform === 'win32';
}
function interpolateEnvVars(value) {
    if (typeof value === 'string') {
        return value.replace(/\$\{(\w+)\}/g, (_, varName) => process.env[varName] || '');
    }
    if (Array.isArray(value))
        return value.map(interpolateEnvVars);
    if (typeof value === 'object' && value !== null) {
        const result = {};
        for (const [k, v] of Object.entries(value)) {
            result[k] = interpolateEnvVars(v);
        }
        return result;
    }
    return value;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
async function connectMCPServers(servers) {
    const allToolNames = [];
    for (const [name, rawConfig] of Object.entries(servers)) {
        const config = interpolateEnvVars(rawConfig);
        const server = new MCPServer(name, config);
        _servers.set(name, server);
        try {
            await server.connect();
            allToolNames.push(...server.tools.map(t => t.name));
        }
        catch (err) {
            (0, logger_1.getLogger)().error(`MCP ${name}: connection failed: ${err.message}`);
        }
    }
    _initialized = true;
    return allToolNames;
}
function getMCPTools() {
    const tools = [];
    for (const server of _servers.values()) {
        if (server.connected) {
            tools.push(...server.tools);
        }
    }
    return tools;
}
async function callMCPTool(toolName, args, signal) {
    // Find which server owns this tool
    for (const server of _servers.values()) {
        if (server.connected && server.tools.some(t => t.name === toolName)) {
            if (signal?.aborted)
                return JSON.stringify({ error: 'Interrupted' });
            return server.callTool(toolName, args);
        }
    }
    return JSON.stringify({ error: `MCP tool not found: ${toolName}` });
}
function getMCPStatus() {
    return Array.from(_servers.entries()).map(([name, server]) => ({
        name,
        transport: server.transport,
        connected: server.connected,
        tools: server.tools.length,
        error: server.error || undefined,
    }));
}
async function disconnectMCPServers() {
    for (const server of _servers.values()) {
        await server.disconnect();
    }
    _servers.clear();
    _initialized = false;
}
function isMCPInitialized() {
    return _initialized;
}
// ---------------------------------------------------------------------------
// Load MCP config from file
// ---------------------------------------------------------------------------
function loadMCPConfig() {
    const configPath = path.join(process.env.PURE_AGENT_HOME || path.join(os.homedir(), '.pure-agent'), 'mcp-config.json');
    try {
        if (fs.existsSync(configPath)) {
            const raw = fs.readFileSync(configPath, 'utf-8');
            return interpolateEnvVars(JSON.parse(raw));
        }
    }
    catch (err) {
        (0, logger_1.getLogger)().warn(`Failed to load MCP config: ${err.message}`);
    }
    return {};
}
//# sourceMappingURL=mcp-client.js.map