import { toErrorMessage } from '../../utils/error.js';
import { generateMCPClientId } from '../../utils/id.js';
import { getRootLogger } from '../../utils/logger.js';
import { HttpSseTransport } from './http-sse.js';
import { StdioTransport } from './stdio.js';
import { MCP_PROTOCOL_VERSION } from '../../constants/mcp/index.js';
import { VERSION } from '../../version.js';
const NAMZU_CLIENT_INFO = { name: 'namzu-sdk', version: VERSION };
export class MCPClient {
    id;
    transport;
    status = 'disconnected';
    serverInfo;
    serverCapabilities;
    connectedAt;
    error;
    pendingRequests = new Map();
    nextRequestId = 1;
    notificationHandlers = [];
    log;
    config;
    constructor(config) {
        this.config = config;
        this.id = config.id ?? generateMCPClientId();
        this.transport = this.createTransport(config.transport);
        this.log = getRootLogger().child({ component: 'MCPClient', serverId: config.serverName });
    }
    async connect() {
        if (this.status === 'connected') {
            throw new Error(`MCPClient already connected to "${this.config.serverName}"`);
        }
        this.status = 'connecting';
        try {
            this.transport.onMessage((msg) => this.handleMessage(msg));
            this.transport.onClose(() => {
                this.status = 'disconnected';
                this.log.info('MCP transport closed');
            });
            this.transport.onError((err) => {
                this.status = 'error';
                this.error = err.message;
                this.log.error('MCP transport error', { error: err.message });
            });
            await this.transport.connect();
            const result = (await this.request('initialize', {
                protocolVersion: MCP_PROTOCOL_VERSION,
                capabilities: this.config.capabilities ?? {},
                clientInfo: this.config.clientInfo ?? NAMZU_CLIENT_INFO,
            }));
            this.serverInfo = result.serverInfo;
            this.serverCapabilities = result.capabilities;
            await this.notify('notifications/initialized', {});
            this.status = 'connected';
            this.connectedAt = Date.now();
            this.log.info(`Connected to MCP server: ${result.serverInfo.name}`);
            return result;
        }
        catch (err) {
            this.status = 'error';
            this.error = toErrorMessage(err);
            this.log.error('MCP connection failed', { error: this.error });
            throw err;
        }
    }
    async disconnect() {
        if (this.status === 'disconnected')
            return;
        for (const [, pending] of this.pendingRequests) {
            pending.reject(new Error('MCPClient disconnecting'));
        }
        this.pendingRequests.clear();
        await this.transport.close();
        this.status = 'disconnected';
        this.connectedAt = undefined;
        this.log.info('MCP client disconnected');
    }
    isConnected() {
        return this.status === 'connected';
    }
    getState() {
        return {
            id: this.id,
            serverName: this.config.serverName,
            status: this.status,
            serverInfo: this.serverInfo,
            serverCapabilities: this.serverCapabilities,
            connectedAt: this.connectedAt,
            error: this.error,
        };
    }
    async listTools() {
        this.requireConnected();
        const result = (await this.request('tools/list', {}));
        return result.tools;
    }
    async callTool(name, args) {
        this.requireConnected();
        const result = (await this.request('tools/call', {
            name,
            arguments: args ?? {},
        }));
        return result;
    }
    async listResources() {
        this.requireConnected();
        const result = (await this.request('resources/list', {}));
        return result.resources;
    }
    async readResource(uri) {
        this.requireConnected();
        const result = (await this.request('resources/read', { uri }));
        return result.contents;
    }
    async listResourceTemplates() {
        this.requireConnected();
        const result = (await this.request('resources/templates/list', {}));
        return result.resourceTemplates;
    }
    onNotification(handler) {
        this.notificationHandlers.push(handler);
    }
    createTransport(config) {
        switch (config.type) {
            case 'stdio':
                return new StdioTransport(config);
            case 'http-sse':
                return new HttpSseTransport(config);
            default:
                throw new Error(`Unsupported MCP transport type: ${config.type}`);
        }
    }
    async request(method, params) {
        const id = this.nextRequestId++;
        const message = {
            jsonrpc: '2.0',
            id,
            method,
            params,
        };
        return new Promise((resolve, reject) => {
            this.pendingRequests.set(id, { resolve, reject });
            this.transport.send(message).catch((err) => {
                this.pendingRequests.delete(id);
                reject(err);
            });
        });
    }
    async notify(method, params) {
        const message = {
            jsonrpc: '2.0',
            method,
            params,
        };
        await this.transport.send(message);
    }
    handleMessage(message) {
        if (message.id !== undefined) {
            const pending = this.pendingRequests.get(message.id);
            if (pending) {
                this.pendingRequests.delete(message.id);
                if (message.error) {
                    pending.reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
                }
                else {
                    pending.resolve(message.result);
                }
                return;
            }
        }
        if (message.method && message.id === undefined) {
            for (const handler of this.notificationHandlers) {
                handler(message.method, message.params);
            }
        }
    }
    requireConnected() {
        if (this.status !== 'connected') {
            throw new Error(`MCPClient "${this.config.serverName}" is not connected (status: ${this.status})`);
        }
    }
}
//# sourceMappingURL=client.js.map