/**
 * Functions for extracting tools from an OpenAPI specification
 */
import OpenapiTypes from 'openapi-types';
const { OpenAPIV3 } = OpenapiTypes;
import { generateOperationId } from '../utils/code-gen.js';
import { shouldIncludeOperationForMcp } from '../utils/helpers.js';
/**
 * Extracts tool definitions from an OpenAPI document
 *
 * @param api OpenAPI document
 * @returns Array of MCP tool definitions
 */
export function extractToolsFromApi(api, defaultInclude = true) {
    const tools = [];
    const usedNames = new Set();
    const globalSecurity = api.security || [];
    if (!api.paths)
        return tools;
    for (const [path, pathItem] of Object.entries(api.paths)) {
        if (!pathItem)
            continue;
        for (const method of Object.values((OpenAPIV3 && OpenAPIV3.HttpMethods) || { GET: 'get', PUT: 'put', POST: 'post', DELETE: 'delete', OPTIONS: 'options', HEAD: 'head', PATCH: 'patch', TRACE: 'trace' })) {
            const operation = pathItem[method];
            if (!operation)
                continue;
            // Apply x-mcp filtering, precedence: operation > path > root
            try {
                if (!shouldIncludeOperationForMcp(api, pathItem, operation, defaultInclude)) {
                    continue;
                }
            }
            catch (error) {
                const loc = operation.operationId || `${method} ${path}`;
                const extVal = operation['x-mcp'] ?? pathItem['x-mcp'] ?? api['x-mcp'];
                let extPreview;
                try {
                    extPreview = JSON.stringify(extVal);
                }
                catch {
                    extPreview = String(extVal);
                }
                console.warn(`Error evaluating x-mcp extension for operation ${loc} (x-mcp=${extPreview}):`, error);
                if (!defaultInclude) {
                    continue;
                }
            }
            // Generate a unique name for the tool
            let baseName = operation.operationId || generateOperationId(method, path);
            if (!baseName)
                continue;
            // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -)
            baseName = baseName.replace(/\./g, '_').replace(/[^a-z0-9_-]/gi, '_');
            let finalToolName = baseName;
            let counter = 1;
            while (usedNames.has(finalToolName)) {
                finalToolName = `${baseName}_${counter++}`;
            }
            usedNames.add(finalToolName);
            // Get or create a description
            const description = operation.description || operation.summary || `Executes ${method.toUpperCase()} ${path}`;
            // Generate input schema and extract parameters
            const { inputSchema, parameters, requestBodyContentType } = generateInputSchemaAndDetails(operation, pathItem.parameters);
            // Extract parameter details for execution
            const executionParameters = parameters.map((p) => ({ name: p.name, in: p.in }));
            // Determine security requirements
            const securityRequirements = operation.security === null ? globalSecurity : operation.security || globalSecurity;
            // Create the tool definition
            tools.push({
                name: finalToolName,
                description,
                inputSchema,
                method,
                pathTemplate: path,
                parameters,
                executionParameters,
                requestBodyContentType,
                securityRequirements,
                operationId: baseName,
            });
        }
    }
    return tools;
}
/**
 * Generates input schema and extracts parameter details from an operation
 *
 * @param operation OpenAPI operation object
 * @param pathParameters Optional path-level parameters that apply to all operations in the path
 * @returns Input schema, parameters, and request body content type
 */
export function generateInputSchemaAndDetails(operation, pathParameters) {
    const properties = {};
    const required = [];
    // Process parameters - merge path parameters with operation parameters
    const operationParameters = Array.isArray(operation.parameters)
        ? operation.parameters.map((p) => p)
        : [];
    const pathParametersResolved = Array.isArray(pathParameters)
        ? pathParameters.map((p) => p)
        : [];
    // Combine path parameters and operation parameters
    // Operation parameters override path parameters if they have the same name/location
    const allParameters = [];
    pathParametersResolved.concat(operationParameters).forEach((param) => {
        const existingIndex = allParameters.findIndex((pathParam) => pathParam.name === param.name && pathParam.in === param.in);
        if (existingIndex >= 0) {
            // Override path parameter with operation parameter
            allParameters[existingIndex] = param;
        }
        else {
            // Add new operation parameter
            allParameters.push(param);
        }
    });
    allParameters.forEach((param) => {
        if (!param.name || !param.schema)
            return;
        const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema);
        if (typeof paramSchema === 'object') {
            paramSchema.description = param.description || paramSchema.description;
        }
        properties[param.name] = paramSchema;
        if (param.required)
            required.push(param.name);
    });
    // Process request body (if present)
    let requestBodyContentType = undefined;
    if (operation.requestBody) {
        const opRequestBody = operation.requestBody;
        const jsonContent = opRequestBody.content?.['application/json'];
        const firstContent = opRequestBody.content
            ? Object.entries(opRequestBody.content)[0]
            : undefined;
        if (jsonContent?.schema) {
            requestBodyContentType = 'application/json';
            const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema);
            if (typeof bodySchema === 'object') {
                bodySchema.description =
                    opRequestBody.description || bodySchema.description || 'The JSON request body.';
            }
            properties['requestBody'] = bodySchema;
            if (opRequestBody.required)
                required.push('requestBody');
        }
        else if (firstContent) {
            const [contentType] = firstContent;
            requestBodyContentType = contentType;
            properties['requestBody'] = {
                type: 'string',
                description: opRequestBody.description || `Request body (content type: ${contentType})`,
            };
            if (opRequestBody.required)
                required.push('requestBody');
        }
    }
    // Combine everything into a JSON Schema
    const inputSchema = {
        type: 'object',
        properties,
        ...(required.length > 0 && { required }),
    };
    return { inputSchema, parameters: allParameters, requestBodyContentType };
}
/**
 * Maps an OpenAPI schema to a JSON Schema with cycle protection.
 *
 * @param schema OpenAPI schema object or reference
 * @param seen WeakSet tracking already visited schema objects
 * @returns JSON Schema representation
 */
export function mapOpenApiSchemaToJsonSchema(schema, seen = new WeakSet()) {
    // Handle reference objects
    if ('$ref' in schema) {
        console.warn(`Unresolved $ref '${schema.$ref}'.`);
        return { type: 'object' };
    }
    // Handle boolean schemas
    if (typeof schema === 'boolean')
        return schema;
    // Detect cycles
    if (seen.has(schema)) {
        console.warn(`Cycle detected in schema${schema.title ? ` "${schema.title}"` : ''}, returning generic object to break recursion.`);
        return { type: 'object' };
    }
    seen.add(schema);
    try {
        // Create a copy of the schema to modify
        const jsonSchema = { ...schema };
        // Convert integer type to number (JSON Schema compatible)
        if (schema.type === 'integer')
            jsonSchema.type = 'number';
        // Remove OpenAPI-specific properties that aren't in JSON Schema
        delete jsonSchema.nullable;
        delete jsonSchema.example;
        delete jsonSchema.xml;
        delete jsonSchema.externalDocs;
        delete jsonSchema.deprecated;
        delete jsonSchema.readOnly;
        delete jsonSchema.writeOnly;
        // Handle nullable properties by adding null to the type
        if (schema.nullable) {
            if (Array.isArray(jsonSchema.type)) {
                if (!jsonSchema.type.includes('null'))
                    jsonSchema.type.push('null');
            }
            else if (typeof jsonSchema.type === 'string') {
                jsonSchema.type = [jsonSchema.type, 'null'];
            }
            else if (!jsonSchema.type) {
                jsonSchema.type = 'null';
            }
        }
        // Recursively process object properties
        if (jsonSchema.type === 'object' && jsonSchema.properties) {
            const mappedProps = {};
            for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
                if (typeof propSchema === 'object' && propSchema !== null) {
                    mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema, seen);
                }
                else if (typeof propSchema === 'boolean') {
                    mappedProps[key] = propSchema;
                }
            }
            jsonSchema.properties = mappedProps;
        }
        // Recursively process array items
        if (jsonSchema.type === 'array' &&
            typeof jsonSchema.items === 'object' &&
            jsonSchema.items !== null) {
            jsonSchema.items = mapOpenApiSchemaToJsonSchema(jsonSchema.items, seen);
        }
        return jsonSchema;
    }
    finally {
        seen.delete(schema);
    }
}
//# sourceMappingURL=extract-tools.js.map