import { createRequire } from 'node:module';
import { loadServerDefinitions } from './config.js';
import { createPrefixedConsoleLogger, resolveLogLevelFromEnv } from './logging.js';
import { closeTransportAndWait } from './runtime-process-utils.js';
import './sdk-patches.js';
import { shouldResetConnection } from './runtime/errors.js';
import { resolveOAuthTimeoutFromEnv } from './runtime/oauth.js';
import { createClientContext } from './runtime/transport.js';
import { normalizeTimeout, raceWithTimeout } from './runtime/utils.js';
const PACKAGE_NAME = 'mcporter';
// Keep version in one place by reading package.json; fall back gracefully when bundled without it (e.g., bun bundle).
const CLIENT_VERSION = (() => {
    try {
        return createRequire(import.meta.url)('../package.json').version;
    }
    catch {
        return process.env.MCPORTER_VERSION ?? '0.0.0-dev';
    }
})();
export const MCPORTER_VERSION = CLIENT_VERSION;
const OAUTH_CODE_TIMEOUT_MS = resolveOAuthTimeoutFromEnv();
// createRuntime spins up a pooled MCP runtime from config JSON or provided definitions.
export async function createRuntime(options = {}) {
    // Build the runtime with either the provided server list or the config file contents.
    const servers = options.servers ??
        (await loadServerDefinitions({
            configPath: options.configPath,
            rootDir: options.rootDir,
        }));
    const runtime = new McpRuntime(servers, options);
    return runtime;
}
// callOnce connects to a server, invokes a single tool, and disposes the connection immediately.
export async function callOnce(params) {
    const runtime = await createRuntime({ configPath: params.configPath });
    try {
        return await runtime.callTool(params.server, params.toolName, {
            args: params.args,
        });
    }
    finally {
        await runtime.close(params.server);
    }
}
class McpRuntime {
    definitions;
    clients = new Map();
    logger;
    clientInfo;
    oauthTimeoutMs;
    constructor(servers, options = {}) {
        this.definitions = new Map(servers.map((entry) => [entry.name, entry]));
        this.logger = options.logger ?? createConsoleLogger();
        this.clientInfo = options.clientInfo ?? {
            name: PACKAGE_NAME,
            version: CLIENT_VERSION,
        };
        this.oauthTimeoutMs = options.oauthTimeoutMs;
    }
    // listServers returns configured names sorted alphabetically for stable CLI output.
    listServers() {
        return [...this.definitions.keys()].sort((a, b) => a.localeCompare(b));
    }
    // getDefinitions exposes raw server metadata to consumers such as the CLI.
    getDefinitions() {
        return [...this.definitions.values()];
    }
    // getDefinition throws when the caller requests an unknown server name.
    getDefinition(server) {
        const definition = this.definitions.get(server);
        if (!definition) {
            throw new Error(`Unknown MCP server '${server}'.`);
        }
        return definition;
    }
    registerDefinition(definition, options = {}) {
        if (!options.overwrite && this.definitions.has(definition.name)) {
            throw new Error(`MCP server '${definition.name}' already exists.`);
        }
        this.definitions.set(definition.name, definition);
        this.clients.delete(definition.name);
    }
    // listTools queries tool metadata and optionally includes schemas when requested.
    async listTools(server, options = {}) {
        // Toggle auto authorization so list can run without forcing OAuth flows.
        const autoAuthorize = options.autoAuthorize !== false;
        const context = await this.connect(server, {
            maxOAuthAttempts: autoAuthorize ? undefined : 0,
            skipCache: !autoAuthorize,
        });
        try {
            const tools = [];
            let cursor;
            do {
                const response = await context.client.listTools(cursor ? { cursor } : undefined);
                tools.push(...(response.tools ?? []).map((tool) => ({
                    name: tool.name,
                    description: tool.description ?? undefined,
                    inputSchema: options.includeSchema ? tool.inputSchema : undefined,
                    outputSchema: options.includeSchema ? tool.outputSchema : undefined,
                })));
                cursor = response.nextCursor ?? undefined;
            } while (cursor);
            return tools;
        }
        catch (error) {
            // Keep-alive STDIO transports often die when Chrome closes; drop the cached client
            // so the next call spins up a fresh process instead of reusing the broken handle.
            await this.resetConnectionOnError(server, error);
            throw error;
        }
        finally {
            if (!autoAuthorize) {
                await context.client.close().catch(() => { });
                await closeTransportAndWait(this.logger, context.transport).catch(() => { });
                await context.oauthSession?.close().catch(() => { });
            }
        }
    }
    // callTool executes a tool using the args provided by the caller.
    async callTool(server, toolName, options = {}) {
        try {
            const { client } = await this.connect(server);
            const params = {
                name: toolName,
                arguments: options.args ?? {},
            };
            // Forward the requested timeout to the MCP client so server-side requests don't hit the SDK's
            // default 60s cap. Keep our own outer race as a second guard.
            const timeoutMs = normalizeTimeout(options.timeoutMs);
            const resultPromise = client.callTool(params, undefined, {
                timeout: timeoutMs,
                // Long runs (e.g., GPT-5 Pro) emit progress/logging; allow that to refresh the timer.
                resetTimeoutOnProgress: true,
                maxTotalTimeout: timeoutMs,
            });
            if (!timeoutMs) {
                return await resultPromise;
            }
            return await raceWithTimeout(resultPromise, timeoutMs);
        }
        catch (error) {
            // Runtime timeouts and transport crashes should tear down the cached connection so
            // the daemon (or direct runtime) can relaunch the MCP server on the next attempt.
            await this.resetConnectionOnError(server, error);
            throw error;
        }
    }
    // listResources delegates to the MCP resources/list method with passthrough params.
    async listResources(server, options = {}) {
        try {
            const { client } = await this.connect(server);
            return await client.listResources(options);
        }
        catch (error) {
            // Fatal listResources errors usually mean the underlying transport has gone away.
            await this.resetConnectionOnError(server, error);
            throw error;
        }
    }
    // connect lazily instantiates a client context per server and memoizes it.
    async connect(server, options = {}) {
        // Reuse cached connections unless the caller explicitly opted out.
        const normalized = server.trim();
        const useCache = options.skipCache !== true && options.maxOAuthAttempts === undefined;
        if (useCache) {
            const existing = this.clients.get(normalized);
            if (existing) {
                return existing;
            }
        }
        const definition = this.definitions.get(normalized);
        if (!definition) {
            throw new Error(`Unknown MCP server '${normalized}'.`);
        }
        const connection = createClientContext(definition, this.logger, this.clientInfo, {
            maxOAuthAttempts: options.maxOAuthAttempts,
            oauthTimeoutMs: this.oauthTimeoutMs ?? OAUTH_CODE_TIMEOUT_MS,
            onDefinitionPromoted: (promoted) => this.definitions.set(promoted.name, promoted),
        });
        if (useCache) {
            this.clients.set(normalized, connection);
            try {
                return await connection;
            }
            catch (error) {
                this.clients.delete(normalized);
                throw error;
            }
        }
        return connection;
    }
    // close tears down transports (and OAuth sessions) for a single server or all servers.
    async close(server) {
        if (server) {
            const normalized = server.trim();
            const context = await this.clients.get(normalized);
            if (!context) {
                return;
            }
            await context.client.close().catch(() => { });
            await closeTransportAndWait(this.logger, context.transport).catch(() => { });
            await context.oauthSession?.close().catch(() => { });
            this.clients.delete(normalized);
            return;
        }
        for (const [name, promise] of this.clients.entries()) {
            try {
                const context = await promise;
                await context.client.close().catch(() => { });
                await closeTransportAndWait(this.logger, context.transport).catch(() => { });
                await context.oauthSession?.close().catch(() => { });
            }
            finally {
                this.clients.delete(name);
            }
        }
    }
    async resetConnectionOnError(server, error) {
        if (!shouldResetConnection(error)) {
            return;
        }
        const normalized = server.trim();
        if (!this.clients.has(normalized)) {
            return;
        }
        try {
            // Reuse the existing close() helper so transport shutdown stays consistent with
            // normal runtime disposal (wait for STDIO children, close OAuth sessions, etc.).
            await this.close(normalized);
        }
        catch (closeError) {
            const detail = closeError instanceof Error ? closeError.message : String(closeError);
            this.logger.warn(`Failed to reset '${normalized}' after error: ${detail}`);
        }
    }
}
// createConsoleLogger produces the default runtime logger honoring MCPORTER_LOG_LEVEL.
function createConsoleLogger(level = resolveLogLevelFromEnv()) {
    return createPrefixedConsoleLogger('mcporter', level);
}
export { readJsonFile, writeJsonFile } from './fs-json.js';
//# sourceMappingURL=runtime.js.map