"use strict";
/**
 * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
 * SPDX-License-Identifier: MIT
 *
 * MCP 工具的共享代码（所有业务域共享）
 * 包含：MCP 客户端、类型定义、通用辅助函数
 */
import { createToolContext, formatToolResult, registerTool } from '../helpers';
import { handleInvokeErrorWithAutoAuth } from '../oapi/helpers';
import { getUserAgent } from '../../core/version';
import { mcpDomain } from '../../core/domains';
import fs from 'node:fs';
import path from 'node:path';
// ---------------------------------------------------------------------------
// 辅助函数
// ---------------------------------------------------------------------------
export function isRecord(v) {
    return typeof v === 'object' && v !== null;
}
/**
 * 从配置对象中提取 MCP endpoint URL
 */
export function extractMcpUrlFromConfig(cfg) {
    if (!isRecord(cfg))
        return undefined;
    const channels = cfg.channels;
    if (!isRecord(channels))
        return undefined;
    const feishu = channels.feishu;
    if (!isRecord(feishu))
        return undefined;
    const url = feishu.mcpEndpoint;
    const legacyUrl = feishu.mcp_url;
    const chosen = typeof url === 'string' ? url : typeof legacyUrl === 'string' ? legacyUrl : undefined;
    if (typeof chosen !== 'string')
        return undefined;
    const trimmed = chosen.trim();
    return trimmed ? trimmed : undefined;
}
/**
 * 部分 MCP 网关/代理会在 result 内再次包一层 JSON-RPC envelope。
 * 这里做一次递归解包，确保工具最终返回的是纯 result JSON（不包含 jsonrpc/id）。
 */
export function unwrapJsonRpcResult(v) {
    if (!isRecord(v))
        return v;
    const hasJsonRpc = typeof v.jsonrpc === 'string';
    const hasId = 'id' in v;
    const hasResult = 'result' in v;
    const hasError = 'error' in v;
    if (hasJsonRpc && (hasResult || hasError)) {
        if (hasError) {
            const err = v.error;
            if (isRecord(err) && typeof err.message === 'string') {
                throw new Error(err.message);
            }
            throw new Error('MCP 返回 error，但无法解析 message');
        }
        return unwrapJsonRpcResult(v.result);
    }
    // 某些实现可能只包了 { result: ... } 而没有 jsonrpc 字段
    if (!hasJsonRpc && !hasId && hasResult && !hasError) {
        return unwrapJsonRpcResult(v.result);
    }
    return v;
}
// ---------------------------------------------------------------------------
// MCP 配置管理
// ---------------------------------------------------------------------------
let mcpEndpointOverride;
export function setMcpEndpointOverride(endpoint) {
    mcpEndpointOverride = endpoint;
}
function readMcpUrlFromOpenclawJson() {
    // 优先读取工作目录下的 `.openclaw/openclaw.json`
    // 约定：channels.feishu.mcpEndpoint（兼容旧字段 mcp_url）
    try {
        const p = path.join(process.cwd(), '.openclaw', 'openclaw.json');
        if (!fs.existsSync(p))
            return undefined;
        const raw = fs.readFileSync(p, 'utf8');
        const cfg = JSON.parse(raw);
        return extractMcpUrlFromConfig(cfg);
    }
    catch {
        // 配置缺失/JSON 解析失败等情况，忽略并回退到默认逻辑
        return undefined;
    }
}
function getMcpEndpoint(brand) {
    // 优先级：运行时覆盖 > 配置文件 > 环境变量 > 基于 brand 的默认值
    return (mcpEndpointOverride ||
        readMcpUrlFromOpenclawJson() ||
        process.env.FEISHU_MCP_ENDPOINT?.trim() ||
        `${mcpDomain(brand)}/mcp`);
}
function buildAuthHeader() {
    // 允许通过环境变量注入鉴权（若服务端要求）
    const token = process.env.FEISHU_MCP_BEARER_TOKEN?.trim() || process.env.FEISHU_MCP_TOKEN?.trim();
    if (!token)
        return undefined;
    return token.toLowerCase().startsWith('bearer ') ? token : `Bearer ${token}`;
}
// ---------------------------------------------------------------------------
// MCP JSON-RPC 客户端
// ---------------------------------------------------------------------------
/**
 * 调用 MCP 工具
 * @param name MCP 工具名称
 * @param args 工具参数
 * @param toolCallId 工具调用 ID
 * @param uat 用户访问令牌(由 invoke 权限检查后传入)
 * @param brand 当前账号品牌，用于选择 MCP 端点域名
 */
export async function callMcpTool(name, args, toolCallId, uat, brand) {
    const endpoint = getMcpEndpoint(brand);
    const auth = buildAuthHeader();
    const body = {
        jsonrpc: '2.0',
        id: toolCallId,
        method: 'tools/call',
        params: {
            name,
            arguments: args,
        },
    };
    const headers = {
        'Content-Type': 'application/json',
        'X-Lark-MCP-UAT': uat,
        'X-Lark-MCP-Allowed-Tools': name,
        'User-Agent': getUserAgent(),
    };
    if (auth)
        headers.authorization = auth;
    const res = await fetch(endpoint, {
        method: 'POST',
        headers,
        body: JSON.stringify(body),
    });
    const text = await res.text();
    if (!res.ok) {
        throw new Error(`MCP HTTP ${res.status} ${res.statusText}: ${text.slice(0, 4000)}`);
    }
    let data;
    try {
        data = JSON.parse(text);
    }
    catch {
        throw new Error(`MCP 返回非 JSON：${text.slice(0, 4000)}`);
    }
    if ('error' in data) {
        throw new Error(`MCP error ${data.error.code}: ${data.error.message}`);
    }
    return unwrapJsonRpcResult(data.result);
}
// ---------------------------------------------------------------------------
// Scope 管理
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// 通用工具注册函数
// ---------------------------------------------------------------------------
/**
 * 注册 MCP 工具的通用函数 (使用 invoke 机制进行权限检查)
 */
export function registerMcpTool(api, config) {
    const { toolClient, log } = createToolContext(api, config.name);
    registerTool(api, {
        name: config.name,
        label: config.label,
        description: config.description,
        parameters: config.schema,
        async execute(toolCallId, params) {
            const p = params;
            try {
                log.debug?.(`Calling ${config.mcpToolName} (toolCallId: ${toolCallId})`);
                const startTime = Date.now();
                // 执行参数验证
                config.validate?.(p);
                const client = toolClient();
                const brand = client.account.brand;
                // 通过 invoke 进行权限检查并调用 MCP
                // 严格模式：必须拥有 toolActionKey 所需的所有 scope
                const result = await client.invoke(config.toolActionKey, async (_sdk, _opts, uat) => {
                    // 权限检查已通过，直接使用 invoke 传入的 UAT
                    if (!uat) {
                        throw new Error('UAT not available');
                    }
                    return callMcpTool(config.mcpToolName, p, toolCallId, uat, brand);
                }, {
                    as: 'user',
                });
                const duration = Date.now() - startTime;
                log.debug?.(`${config.mcpToolName} succeeded in ${duration}ms`);
                // MCP tools/call 返回值已经是 { content: [{ type, text }] } 格式，
                // 直接透传 content，避免被 formatToolResult 再包一层
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                if (isRecord(result) && Array.isArray(result.content)) {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    const mcpContent = result.content;
                    let details = result;
                    if (mcpContent.length === 1 && mcpContent[0]?.type === 'text') {
                        try {
                            details = JSON.parse(mcpContent[0].text);
                        }
                        catch {
                            // text 不是 JSON，保留原始 result
                        }
                    }
                    return {
                        content: mcpContent.map((c) => ({
                            type: 'text',
                            text: c.text,
                        })),
                        details,
                    };
                }
                return formatToolResult(result);
            }
            catch (err) {
                const errMsg = err instanceof Error ? err.message : String(err);
                log.error(`${config.mcpToolName} failed: ${errMsg}`);
                return handleInvokeErrorWithAutoAuth(err, api.config);
            }
        },
    }, { name: config.name });
}
