/**
 * Minimal hand-rolled JSON-RPC 2.0 client for the Aexol MCP backend.
 *
 * Why hand-rolled, not @modelcontextprotocol/sdk?
 *  - The Aexol backend speaks a non-standard MCP dialect (custom content types,
 *    protocol version 0.1.0 negotiation, no SSE). The official SDK adds a lot
 *    of negotiation overhead we don't need and would actively break against
 *    this server.
 *  - All we want is: POST a JSON-RPC envelope with a Bearer token, get a
 *    JSON-RPC reply, surface errors. fetch() is built into Node 20+.
 */
/**
 * Errors thrown by AexolMcpClient carry an optional HTTP `status` so callers
 * can produce friendlier messages (e.g. 401 → "invalid token").
 */
export class AexolMcpError extends Error {
    status;
    code;
    constructor(message, opts = {}) {
        super(message);
        this.name = "AexolMcpError";
        this.status = opts.status;
        this.code = opts.code;
    }
}
export class AexolMcpClient {
    apiUrl;
    apiKey;
    timeoutMs;
    constructor(apiUrl, apiKey, timeoutMs = 30_000) {
        this.apiUrl = apiUrl;
        this.apiKey = apiKey;
        this.timeoutMs = timeoutMs;
    }
    /** Low-level: send a JSON-RPC call and return its `result` field. */
    async call(method, params) {
        const body = JSON.stringify({
            jsonrpc: "2.0",
            // crypto.randomUUID is overkill; a counter would do, but this keeps the
            // module stateless. JSON-RPC ids only need to be unique per outstanding call.
            id: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
            method,
            params,
        });
        let res;
        try {
            res = await fetch(this.apiUrl, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Accept: "application/json",
                    Authorization: `Bearer ${this.apiKey}`,
                },
                body,
                signal: AbortSignal.timeout(this.timeoutMs),
            });
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            throw new AexolMcpError(`Network error talking to ${this.apiUrl}: ${msg}`);
        }
        if (!res.ok) {
            // Try to read a body for context, but don't fail if we can't.
            let detail = "";
            try {
                detail = (await res.text()).slice(0, 500);
            }
            catch {
                /* ignore */
            }
            throw new AexolMcpError(`HTTP ${res.status} ${res.statusText} from ${this.apiUrl}${detail ? `: ${detail}` : ""}`, { status: res.status });
        }
        let payload;
        try {
            payload = (await res.json());
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            throw new AexolMcpError(`Invalid JSON response from ${this.apiUrl}: ${msg}`);
        }
        if ("error" in payload && payload.error) {
            throw new AexolMcpError(`JSON-RPC error ${payload.error.code}: ${payload.error.message}`, { code: payload.error.code });
        }
        if (!("result" in payload)) {
            throw new AexolMcpError(`JSON-RPC response missing 'result' field`);
        }
        return payload.result;
    }
    /**
     * Fetch the tool catalog. The Aexol backend returns `{ tools: [...] }`
     * per JSON-RPC convention, but we accept the raw array shape too in case
     * the server format drifts.
     */
    async listTools() {
        const result = await this.call("tools/list");
        if (Array.isArray(result))
            return result;
        if (result && Array.isArray(result.tools))
            return result.tools;
        throw new AexolMcpError("tools/list returned an unexpected payload shape");
    }
    /**
     * Invoke a single tool. Returns the raw `{ content, isError? }` envelope —
     * the caller (the spectral extension) decides how to surface it back to the model.
     */
    async callTool(name, args) {
        const result = await this.call("tools/call", {
            name,
            arguments: args ?? {},
        });
        if (!result || typeof result !== "object" ||
            !Array.isArray(result.content)) {
            throw new AexolMcpError(`tools/call returned an unexpected payload for "${name}"`);
        }
        return result;
    }
}
