
import { __decorate, __param } from '../../../../../../external/tslib/tslib.es6.js';
import { Sequencer, DeferredPromise, timeout, raceCancellationError } from '../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { CancellationError } from '../../../base/common/errors.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
import { scopesMatch, parseWWWAuthenticateHeader, AUTH_SCOPE_SEPARATOR, fetchResourceMetadata, fetchAuthorizationServerMetadata, getDefaultMetadataForUrl } from '../../../base/common/oauth.js';
import { SSEParser } from '../../../base/common/sseParser.js';
import { URI } from '../../../base/common/uri.js';
import { vObj, vOptionalProp, vArray, vString, vObjAny, vNumber } from '../../../base/common/validation.js';
import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js';
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { LogLevel, canLog } from '../../../platform/log/common/log.js';
import { ILogService } from '../../../platform/log/common/log.service.js';
import product from '../../../platform/product/common/product.js';
import { StorageScope } from '../../../platform/storage/common/storage.js';
import { McpServerLaunch, McpServerTransportType, extensionPrefixedIdentifier, McpServerStaticToolAvailability, McpConnectionState, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js';
import { MCP } from '../../../platform/mcp/common/modelContextProtocol.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import { MainContext, IAuthResourceMetadataSource, IAuthServerMetadataSource } from './extHost.protocol.js';
import { IExtHostInitDataService } from './extHostInitDataService.js';
import { IExtHostRpcService } from './extHostRpcService.js';
import { McpServerDefinition } from './extHostTypeConverters.js';
import { McpToolAvailability } from './extHostTypes.js';
import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js';
import { IExtHostWorkspace } from './extHostWorkspace.js';

const IExtHostMpcService = ( createDecorator("IExtHostMpcService"));
const serverDataValidation = vObj({
    label: vString(),
    version: vOptionalProp(vString()),
    metadata: vOptionalProp(vObj({
        capabilities: vOptionalProp(vObjAny()),
        serverInfo: vOptionalProp(vObjAny()),
        tools: vOptionalProp(vArray(vObj({
            availability: vNumber(),
            definition: vObjAny()
        })))
    })),
    authentication: vOptionalProp(vObj({
        providerId: vString(),
        scopes: vArray(vString())
    }))
});
let ExtHostMcpService = class ExtHostMcpService extends Disposable {
    constructor(
        extHostRpc,
        _logService,
        _extHostInitData,
        _workspaceService,
        _variableResolver
    ) {
        super();
        this._logService = _logService;
        this._extHostInitData = _extHostInitData;
        this._workspaceService = _workspaceService;
        this._variableResolver = _variableResolver;
        this._initialProviderPromises = ( new Set());
        this._sseEventSources = this._register(( new DisposableMap()));
        this._unresolvedMcpServers = ( new Map());
        this._onDidChangeMcpServerDefinitions = this._register(( new Emitter()));
        this.onDidChangeMcpServerDefinitions = this._onDidChangeMcpServerDefinitions.event;
        this._mcpServerDefinitions = [];
        this._activeGateways = ( new Map());
        this._proxy = ( extHostRpc.getProxy(MainContext.MainThreadMcp));
    }
    get mcpServerDefinitions() {
        return this._mcpServerDefinitions;
    }
    $onDidChangeMcpServerDefinitions(servers) {
        this._mcpServerDefinitions = ( servers.map(dto => McpServerDefinition.to(dto)));
        this._onDidChangeMcpServerDefinitions.fire();
    }
    $startMcp(id, opts) {
        this._startMcp(
            id,
            McpServerLaunch.fromSerialized(opts.launch),
            opts.defaultCwd && URI.revive(opts.defaultCwd),
            opts.errorOnUserInteraction
        );
    }
    _startMcp(id, launch, _defaultCwd, errorOnUserInteraction) {
        if (launch.type === McpServerTransportType.HTTP) {
            this._sseEventSources.set(id, ( new McpHTTPHandle(id, launch, this._proxy, this._logService, errorOnUserInteraction)));
            return;
        }
        throw ( new Error("not implemented"));
    }
    async $substituteVariables(_workspaceFolder, value) {
        const folderURI = URI.revive(_workspaceFolder);
        const folder = folderURI && (await this._workspaceService.resolveWorkspaceFolder(folderURI));
        const variableResolver = await this._variableResolver.getResolver();
        return variableResolver.resolveAsync(folder && {
            uri: folder.uri,
            name: folder.name,
            index: folder.index
        }, value);
    }
    $stopMcp(id) {
        this._sseEventSources.get(id)?.close().then(() => this._didClose(id));
    }
    _didClose(id) {
        this._sseEventSources.deleteAndDispose(id);
    }
    $sendMessage(id, message) {
        this._sseEventSources.get(id)?.send(message);
    }
    async $waitForInitialCollectionProviders() {
        await Promise.all(this._initialProviderPromises);
    }
    async $resolveMcpLaunch(collectionId, label) {
        const rec = this._unresolvedMcpServers.get(collectionId);
        if (!rec) {
            return;
        }
        const server = rec.servers.find(s => s.label === label);
        if (!server) {
            return;
        }
        if (!rec.provider.resolveMcpServerDefinition) {
            return McpServerDefinition.from(server);
        }
        const resolved = await rec.provider.resolveMcpServerDefinition(server, CancellationToken.None);
        return resolved ? McpServerDefinition.from(resolved) : undefined;
    }
    registerMcpConfigurationProvider(extension, id, provider) {
        const store = ( new DisposableStore());
        const metadata = extension.contributes?.mcpServerDefinitionProviders?.find(m => m.id === id);
        if (!metadata) {
            throw ( new Error(
                `MCP configuration providers must be registered in the contributes.mcpServerDefinitionProviders array within your package.json, but "${id}" was not`
            ));
        }
        const mcp = {
            id: extensionPrefixedIdentifier(extension.identifier, id),
            isTrustedByDefault: true,
            label: metadata?.label ?? extension.displayName ?? extension.name,
            scope: StorageScope.WORKSPACE,
            canResolveLaunch: typeof provider.resolveMcpServerDefinition === "function",
            extensionId: extension.identifier.value,
            configTarget: this._extHostInitData.remote.isRemote ? ConfigurationTarget.USER_REMOTE : ConfigurationTarget.USER
        };
        const update = async () => {
            const list = await provider.provideMcpServerDefinitions(CancellationToken.None);
            this._unresolvedMcpServers.set(mcp.id, {
                servers: list ?? [],
                provider
            });
            const servers = [];
            for (const item of list ?? []) {
                let id = ExtensionIdentifier.toKey(extension.identifier) + "/" + item.label;
                if (( servers.some(s => s.id === id))) {
                    let i = 2;
                    while (( servers.some(s => s.id === id + i))) {
                        i++;
                    }
                    id = id + i;
                }
                serverDataValidation.validateOrThrow(item);
                if (item.authentication) {
                    checkProposedApiEnabled(extension, "mcpToolDefinitions");
                }
                let staticMetadata;
                const castAs2 = item;
                if (isProposedApiEnabled(extension) && castAs2.metadata) {
                    staticMetadata = {
                        capabilities: castAs2.metadata.capabilities,
                        instructions: castAs2.metadata.instructions,
                        serverInfo: castAs2.metadata.serverInfo,
                        tools: castAs2.metadata.tools?.map(t => ({
                            availability: t.availability === McpToolAvailability.Dynamic ? McpServerStaticToolAvailability.Dynamic : McpServerStaticToolAvailability.Initial,
                            definition: t.definition
                        }))
                    };
                }
                servers.push({
                    id,
                    label: item.label,
                    cacheNonce: item.version || "$$NONE",
                    staticMetadata,
                    launch: McpServerDefinition.from(item)
                });
            }
            this._proxy.$upsertMcpCollection(mcp, servers);
        };
        store.add(toDisposable(() => {
            this._unresolvedMcpServers.delete(mcp.id);
            this._proxy.$deleteMcpCollection(mcp.id);
        }));
        if (provider.onDidChangeMcpServerDefinitions) {
            store.add(provider.onDidChangeMcpServerDefinitions(update));
        }
        if (provider.onDidChangeServerDefinitions) {
            store.add(provider.onDidChangeServerDefinitions(update));
        }
        if (provider.onDidChange) {
            store.add(provider.onDidChange(update));
        }
        const promise = ( new Promise(resolve => {
            setTimeout(() => update().finally(() => {
                this._initialProviderPromises.delete(promise);
                resolve();
            }), 0);
        }));
        this._initialProviderPromises.add(promise);
        return store;
    }
    async startMcpGateway() {
        const result = await this._proxy.$startMcpGateway();
        if (!result) {
            return undefined;
        }
        const gatewayId = result.gatewayId;
        const servers = ( result.servers.map(s => ({
            label: s.label,
            address: URI.revive(s.address)
        })));
        const onDidChangeServers = ( new Emitter());
        this._activeGateways.set(gatewayId, {
            servers,
            onDidChangeServers
        });
        return {
            get servers() {
                return servers;
            },
            onDidChangeServers: onDidChangeServers.event,
            dispose: () => {
                this._activeGateways.delete(gatewayId);
                onDidChangeServers.dispose();
                this._proxy.$disposeMcpGateway(gatewayId);
            }
        };
    }
    $onDidChangeGatewayServers(gatewayId, newServers) {
        const gateway = this._activeGateways.get(gatewayId);
        if (!gateway) {
            return;
        }
        const servers = ( newServers.map(s => ({
            label: s.label,
            address: URI.revive(s.address)
        })));
        gateway.servers.length = 0;
        gateway.servers.push(...servers);
        gateway.onDidChangeServers.fire(servers);
    }
};
ExtHostMcpService = ( __decorate([( __param(0, IExtHostRpcService)), ( __param(1, ILogService)), ( __param(2, IExtHostInitDataService)), ( __param(3, IExtHostWorkspace)), ( __param(4, IExtHostVariableResolverProvider))], ExtHostMcpService));
var HttpMode;
(function(HttpMode) {
    HttpMode[HttpMode["Unknown"] = 0] = "Unknown";
    HttpMode[HttpMode["Http"] = 1] = "Http";
    HttpMode[HttpMode["SSE"] = 2] = "SSE";
})(HttpMode || (HttpMode = {}));
const MAX_FOLLOW_REDIRECTS = 5;
const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
class McpHTTPHandle extends Disposable {
    constructor(_id, _launch, _proxy, _logService, _errorOnUserInteraction) {
        super();
        this._id = _id;
        this._launch = _launch;
        this._proxy = _proxy;
        this._logService = _logService;
        this._errorOnUserInteraction = _errorOnUserInteraction;
        this._requestSequencer = ( new Sequencer());
        this._postEndpoint = ( new DeferredPromise());
        this._mode = {
            value: HttpMode.Unknown
        };
        this._cts = ( new CancellationTokenSource());
        this._abortCtrl = ( new AbortController());
        this._didSendClose = false;
        this._register(toDisposable(() => {
            this._abortCtrl.abort();
            this._cts.dispose(true);
        }));
        this._proxy.$onDidChangeState(this._id, {
            state: McpConnectionState.Kind.Running
        });
    }
    async send(message) {
        try {
            if (this._mode.value === HttpMode.Unknown) {
                await this._requestSequencer.queue(() => this._send(message));
            } else {
                await this._send(message);
            }
        } catch (err) {
            const msg = `Error sending message to ${this._launch.uri}: ${String(err)}`;
            this._proxy.$onDidChangeState(this._id, {
                state: McpConnectionState.Kind.Error,
                message: msg
            });
        }
    }
    async close() {
        if (this._mode.value === HttpMode.Http && this._mode.sessionId && !this._didSendClose) {
            this._didSendClose = true;
            try {
                await this._closeSession(this._mode.sessionId);
            } catch {}
        }
        this._proxy.$onDidChangeState(this._id, {
            state: McpConnectionState.Kind.Stopped
        });
    }
    async _closeSession(sessionId) {
        const headers = {
            ...Object.fromEntries(this._launch.headers),
            "Mcp-Session-Id": sessionId
        };
        try {
            await this._addAuthHeader(headers, {
                errorOnUserInteraction: true
            });
        } catch (e) {
            this._log(
                LogLevel.Debug,
                `Skipping session close: authentication no longer available`
            );
            return;
        }
        await this._fetch(( this._launch.uri.toString(true)), {
            method: "DELETE",
            headers
        });
    }
    _send(message) {
        if (this._mode.value === HttpMode.SSE) {
            return this._sendLegacySSE(this._mode.endpoint, message);
        } else {
            return this._sendStreamableHttp(
                message,
                this._mode.value === HttpMode.Http ? this._mode.sessionId : undefined
            );
        }
    }
    async _sendStreamableHttp(message, sessionId) {
        const asBytes = ( new TextEncoder()).encode(message);
        const headers = {
            ...Object.fromEntries(this._launch.headers),
            "Content-Type": "application/json",
            "Content-Length": String(asBytes.length),
            Accept: "text/event-stream, application/json"
        };
        if (sessionId) {
            headers["Mcp-Session-Id"] = sessionId;
        }
        await this._addAuthHeader(headers);
        const res = await this._fetchWithAuthRetry(( this._launch.uri.toString(true)), {
            method: "POST",
            headers,
            body: asBytes
        }, headers);
        const wasUnknown = this._mode.value === HttpMode.Unknown;
        const nextSessionId = res.headers.get("Mcp-Session-Id");
        if (nextSessionId) {
            this._mode = {
                value: HttpMode.Http,
                sessionId: nextSessionId
            };
        }
        if (
        this._mode.value === HttpMode.Unknown &&
        res.status >= 400 && res.status < 500 && !isAuthStatusCode(res.status)) {
            this._log(
                LogLevel.Info,
                `${res.status} status sending message to ${this._launch.uri}, will attempt to fall back to legacy SSE`
            );
            this._sseFallbackWithMessage(message);
            return;
        }
        if (res.status >= 300) {
            const retryWithSessionId = this._mode.value === HttpMode.Http && !!this._mode.sessionId && (res.status === 400 || res.status === 404);
            this._proxy.$onDidChangeState(this._id, {
                state: McpConnectionState.Kind.Error,
                message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + (retryWithSessionId ? `; will retry with new session ID` : ""),
                shouldRetry: retryWithSessionId
            });
            return;
        }
        if (this._mode.value === HttpMode.Unknown) {
            this._mode = {
                value: HttpMode.Http,
                sessionId: undefined
            };
        }
        if (wasUnknown) {
            this._attachStreamableBackchannel();
        }
        await this._handleSuccessfulStreamableHttp(res, message);
    }
    async _sseFallbackWithMessage(message) {
        const endpoint = await this._attachSSE();
        if (endpoint) {
            this._mode = {
                value: HttpMode.SSE,
                endpoint
            };
            await this._sendLegacySSE(endpoint, message);
        }
    }
    async _handleSuccessfulStreamableHttp(res, message) {
        if (res.status === 202) {
            return;
        }
        const contentType = res.headers.get("Content-Type")?.toLowerCase() || "";
        if (contentType.startsWith("text/event-stream")) {
            const parser = ( new SSEParser(event => {
                if (event.type === "message") {
                    this._proxy.$onDidReceiveMessage(this._id, event.data);
                } else if (event.type === "endpoint") {
                    this._log(
                        LogLevel.Warning,
                        `Received SSE endpoint from a POST to ${this._launch.uri}, will fall back to legacy SSE`
                    );
                    this._sseFallbackWithMessage(message);
                    throw ( new CancellationError());
                }
            }));
            try {
                await this._doSSE(parser, res);
            } catch (err) {
                this._log(LogLevel.Warning, `Error reading SSE stream: ${String(err)}`);
            }
        } else if (contentType.startsWith("application/json")) {
            this._proxy.$onDidReceiveMessage(this._id, await res.text());
        } else {
            const responseBody = await res.text();
            if (isJSON(responseBody)) {
                this._proxy.$onDidReceiveMessage(this._id, responseBody);
            } else {
                this._log(
                    LogLevel.Warning,
                    `Unexpected ${res.status} response for request: ${responseBody}`
                );
            }
        }
    }
    async _attachStreamableBackchannel() {
        let lastEventId;
        let canReconnectAt;
        for (let retry = 0; !this._store.isDisposed; retry++) {
            if (canReconnectAt !== undefined) {
                await timeout(Math.max(0, canReconnectAt - Date.now()), this._cts.token);
                canReconnectAt = undefined;
            } else {
                await timeout(Math.min(retry * 1000, 30_000), this._cts.token);
            }
            let res;
            try {
                const headers = {
                    ...Object.fromEntries(this._launch.headers),
                    "Accept": "text/event-stream"
                };
                await this._addAuthHeader(headers);
                if (this._mode.value === HttpMode.Http && this._mode.sessionId !== undefined) {
                    headers["Mcp-Session-Id"] = this._mode.sessionId;
                }
                if (lastEventId) {
                    headers["Last-Event-ID"] = lastEventId;
                }
                res = await this._fetchWithAuthRetry(( this._launch.uri.toString(true)), {
                    method: "GET",
                    headers
                }, headers);
            } catch (e) {
                this._log(
                    LogLevel.Info,
                    `Error connecting to ${this._launch.uri} for async notifications, will retry`
                );
                continue;
            }
            if (res.status >= 400) {
                this._log(
                    LogLevel.Debug,
                    `${res.status} status connecting to ${this._launch.uri} for async notifications; they will be disabled: ${await this._getErrText(res)}`
                );
                return;
            }
            if (res.headers.get("content-type")?.toLowerCase().includes("text/event-stream")) {
                retry = 0;
            }
            const parser = ( new SSEParser(event => {
                if (event.retry) {
                    canReconnectAt = Date.now() + event.retry;
                }
                if (event.type === "message" && event.data) {
                    this._proxy.$onDidReceiveMessage(this._id, event.data);
                }
                if (event.id) {
                    lastEventId = event.id;
                }
            }));
            try {
                await this._doSSE(parser, res);
            } catch (e) {
                this._log(LogLevel.Info, `Error reading from async stream, we will reconnect: ${e}`);
            }
        }
    }
    async _attachSSE() {
        const postEndpoint = ( new DeferredPromise());
        const headers = {
            ...Object.fromEntries(this._launch.headers),
            "Accept": "text/event-stream"
        };
        await this._addAuthHeader(headers);
        let res;
        try {
            res = await this._fetchWithAuthRetry(( this._launch.uri.toString(true)), {
                method: "GET",
                headers
            }, headers);
            if (res.status >= 300) {
                this._proxy.$onDidChangeState(this._id, {
                    state: McpConnectionState.Kind.Error,
                    message: `${res.status} status connecting to ${this._launch.uri} as SSE: ${await this._getErrText(res)}`
                });
                return;
            }
        } catch (e) {
            this._proxy.$onDidChangeState(this._id, {
                state: McpConnectionState.Kind.Error,
                message: `Error connecting to ${this._launch.uri} as SSE: ${e}`
            });
            return;
        }
        const parser = ( new SSEParser(event => {
            if (event.type === "message") {
                this._proxy.$onDidReceiveMessage(this._id, event.data);
            } else if (event.type === "endpoint") {
                postEndpoint.complete(( ( new URL(event.data, ( this._launch.uri.toString(true)))).toString()));
            }
        }));
        this._register(toDisposable(() => postEndpoint.cancel()));
        this._doSSE(parser, res).catch(err => {
            this._proxy.$onDidChangeState(this._id, {
                state: McpConnectionState.Kind.Error,
                message: `Error reading SSE stream: ${String(err)}`
            });
        });
        return postEndpoint.p;
    }
    async _sendLegacySSE(url, message) {
        const asBytes = ( new TextEncoder()).encode(message);
        const headers = {
            ...Object.fromEntries(this._launch.headers),
            "Content-Type": "application/json",
            "Content-Length": String(asBytes.length)
        };
        await this._addAuthHeader(headers);
        const res = await this._fetch(url, {
            method: "POST",
            headers,
            body: asBytes
        });
        if (res.status >= 300) {
            this._log(
                LogLevel.Warning,
                `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`
            );
        }
    }
    async _doSSE(parser, res) {
        if (!res.body) {
            return;
        }
        const reader = res.body.getReader();
        let chunk;
        do {
            try {
                chunk = await raceCancellationError(reader.read(), this._cts.token);
            } catch (err) {
                reader.cancel();
                if (this._store.isDisposed) {
                    return;
                } else {
                    throw err;
                }
            }
            if (chunk.value) {
                parser.feed(chunk.value);
            }
        } while (!chunk.done);
    }
    async _addAuthHeader(headers, options) {
        const errorOnUserInteraction = options?.errorOnUserInteraction ?? this._errorOnUserInteraction;
        if (this._authMetadata) {
            try {
                const authDetails = {
                    authorizationServer: this._authMetadata.authorizationServer.toJSON(),
                    authorizationServerMetadata: this._authMetadata.serverMetadata,
                    resourceMetadata: this._authMetadata.resourceMetadata,
                    scopes: this._authMetadata.scopes
                };
                const token = await this._proxy.$getTokenFromServerMetadata(this._id, authDetails, {
                    errorOnUserInteraction,
                    forceNewRegistration: options?.forceNewRegistration
                });
                if (token) {
                    headers["Authorization"] = `Bearer ${token}`;
                }
            } catch (e) {
                if (UserInteractionRequiredError.is(e)) {
                    this._proxy.$onDidChangeState(this._id, {
                        state: McpConnectionState.Kind.Stopped,
                        reason: "needs-user-interaction"
                    });
                    throw ( new CancellationError());
                }
                this._log(LogLevel.Warning, `Error getting token from server metadata: ${String(e)}`);
            }
        }
        if (this._launch.authentication) {
            try {
                this._log(
                    LogLevel.Debug,
                    `Using provided authentication config: providerId=${this._launch.authentication.providerId}, scopes=${this._launch.authentication.scopes.join(", ")}`
                );
                const token = await this._proxy.$getTokenForProviderId(
                    this._id,
                    this._launch.authentication.providerId,
                    this._launch.authentication.scopes,
                    {
                        errorOnUserInteraction,
                        forceNewRegistration: options?.forceNewRegistration
                    }
                );
                if (token) {
                    headers["Authorization"] = `Bearer ${token}`;
                    this._log(
                        LogLevel.Info,
                        "Successfully obtained token from provided authentication config"
                    );
                }
            } catch (e) {
                if (UserInteractionRequiredError.is(e)) {
                    this._proxy.$onDidChangeState(this._id, {
                        state: McpConnectionState.Kind.Stopped,
                        reason: "needs-user-interaction"
                    });
                    throw ( new CancellationError());
                }
                this._log(
                    LogLevel.Warning,
                    `Error getting token from provided authentication config: ${String(e)}`
                );
            }
        }
        return headers;
    }
    _log(level, message) {
        if (!this._store.isDisposed) {
            this._proxy.$onDidPublishLog(this._id, level, message);
        }
    }
    async _getErrText(res) {
        try {
            return await res.text();
        } catch {
            return res.statusText;
        }
    }
    async _fetchWithAuthRetry(mcpUrl, init, headers) {
        const doFetch = () => this._fetch(mcpUrl, init);
        let res = await doFetch();
        if (isAuthStatusCode(res.status)) {
            if (!this._authMetadata) {
                this._authMetadata = await createAuthMetadata(mcpUrl, res.headers, {
                    sameOriginHeaders: {
                        ...Object.fromEntries(this._launch.headers),
                        "MCP-Protocol-Version": MCP.LATEST_PROTOCOL_VERSION
                    },
                    fetch: (url, init) => this._fetch(url, init),
                    log: (level, message) => this._log(level, message)
                });
                this._proxy.$logMcpAuthSetup(this._authMetadata.telemetry);
                await this._addAuthHeader(headers);
                if (headers["Authorization"]) {
                    init.headers = headers;
                    res = await doFetch();
                }
            } else {
                if (this._authMetadata.update(res.headers)) {
                    await this._addAuthHeader(headers);
                    if (headers["Authorization"]) {
                        init.headers = headers;
                        res = await doFetch();
                    }
                }
            }
        }
        if (headers["Authorization"] && isAuthStatusCode(res.status)) {
            const errorText = await this._getErrText(res);
            this._log(
                LogLevel.Info,
                `Received ${res.status} status with Authorization header, retrying with new auth registration. Error details: ${errorText || "no additional details"}`
            );
            await this._addAuthHeader(headers, {
                forceNewRegistration: true
            });
            res = await doFetch();
        }
        return res;
    }
    async _fetch(url, init) {
        init.headers["user-agent"] = `${product.nameLong}/${product.version}`;
        if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
            const traceObj = {
                ...init,
                headers: {
                    ...init.headers
                }
            };
            if (traceObj.body) {
                traceObj.body = ( new TextDecoder()).decode(traceObj.body);
            }
            if (traceObj.headers?.Authorization) {
                traceObj.headers.Authorization = "***";
            }
            this._log(
                LogLevel.Trace,
                `Fetching ${url} with options: ${JSON.stringify(traceObj)}`
            );
        }
        let currentUrl = url;
        let response;
        for (let redirectCount = 0; redirectCount < MAX_FOLLOW_REDIRECTS; redirectCount++) {
            response = await this._fetchInternal(currentUrl, {
                ...init,
                signal: this._abortCtrl.signal,
                redirect: "manual"
            });
            if (!REDIRECT_STATUS_CODES.includes(response.status)) {
                break;
            }
            const location = response.headers.get("location");
            if (!location) {
                break;
            }
            const nextUrl = ( ( new URL(location, currentUrl)).toString());
            this._log(
                LogLevel.Trace,
                `Redirect (${response.status}) from ${currentUrl} to ${nextUrl}`
            );
            currentUrl = nextUrl;
            if (response.status === 303 || ((response.status === 301 || response.status === 302) && init.method === "POST")) {
                init.method = "GET";
                delete init.body;
            }
        }
        if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
            const headers = {};
            response.headers.forEach((value, key) => {
                headers[key] = value;
            });
            this._log(LogLevel.Trace, `Fetched ${currentUrl}: ${JSON.stringify({
    status: response.status,
    headers: headers
})}`);
        }
        return response;
    }
    _fetchInternal(url, init) {
        return fetch(url, init);
    }
}
function isJSON(str) {
    try {
        JSON.parse(str);
        return true;
    } catch (e) {
        return false;
    }
}
function isAuthStatusCode(status) {
    return status === 401 || status === 403;
}
class AuthMetadata {
    constructor(
        authorizationServer,
        serverMetadata,
        resourceMetadata,
        scopes,
        telemetry,
        _log
    ) {
        this.authorizationServer = authorizationServer;
        this.serverMetadata = serverMetadata;
        this.resourceMetadata = resourceMetadata;
        this.telemetry = telemetry;
        this._log = _log;
        this._scopes = scopes;
    }
    get scopes() {
        return this._scopes;
    }
    update(responseHeaders) {
        const scopesChallenge = this._parseScopesFromResponse(responseHeaders);
        if (!scopesMatch(scopesChallenge, this._scopes)) {
            this._log(
                LogLevel.Info,
                `Scopes changed from ${JSON.stringify(this._scopes)} to ${JSON.stringify(scopesChallenge)}, updating`
            );
            this._scopes = scopesChallenge;
            return true;
        }
        return false;
    }
    _parseScopesFromResponse(responseHeaders) {
        const authHeader = responseHeaders.get("WWW-Authenticate");
        if (!authHeader) {
            return undefined;
        }
        const challenges = parseWWWAuthenticateHeader(authHeader);
        for (const challenge of challenges) {
            if (challenge.scheme === "Bearer" && challenge.params["scope"]) {
                const scopes = challenge.params["scope"].split(AUTH_SCOPE_SEPARATOR).filter(s => s.trim().length);
                if (scopes.length) {
                    this._log(
                        LogLevel.Info,
                        `Found scope challenge in WWW-Authenticate header: ${challenge.params["scope"]}`
                    );
                    return scopes;
                }
            }
        }
        return undefined;
    }
}
async function createAuthMetadata(resourceUrl, initialResponseHeaders, options) {
    const {
        sameOriginHeaders,
        fetch,
        log
    } = options;
    let resourceMetadataSource = IAuthResourceMetadataSource.None;
    let serverMetadataSource;
    const {
        resourceMetadataChallenge,
        scopesChallenge: scopesChallengeFromHeader
    } = parseWWWAuthenticateHeaderForChallenges(initialResponseHeaders.get("WWW-Authenticate") ?? undefined, log);
    let serverMetadataUrl;
    let resource;
    let scopesChallenge = scopesChallengeFromHeader;
    try {
        const {
            metadata,
            discoveryUrl,
            errors
        } = await fetchResourceMetadata(resourceUrl, resourceMetadataChallenge, {
            sameOriginHeaders,
            fetch: (url, init) => fetch(url, init)
        });
        for (const err of errors) {
            log(LogLevel.Warning, `Error fetching resource metadata: ${err}`);
        }
        log(LogLevel.Info, `Discovered resource metadata at ${discoveryUrl}`);
        resourceMetadataSource = resourceMetadataChallenge ? IAuthResourceMetadataSource.Header : IAuthResourceMetadataSource.WellKnown;
        serverMetadataUrl = metadata.authorization_servers?.[0];
        if (!serverMetadataUrl) {
            log(
                LogLevel.Warning,
                `No authorization_servers found in resource metadata ${discoveryUrl} - Is this resource metadata configured correctly?`
            );
        } else {
            log(LogLevel.Info, `Using auth server metadata url: ${serverMetadataUrl}`);
            serverMetadataSource = IAuthServerMetadataSource.ResourceMetadata;
        }
        scopesChallenge ??= metadata.scopes_supported;
        resource = metadata;
    } catch (e) {
        log(LogLevel.Warning, `Could not fetch resource metadata: ${String(e)}`);
    }
    const baseUrl = ( new URL(resourceUrl)).origin;
    let additionalHeaders = {};
    if (!serverMetadataUrl) {
        serverMetadataUrl = baseUrl;
        if (sameOriginHeaders) {
            additionalHeaders = sameOriginHeaders;
        }
    }
    try {
        log(
            LogLevel.Debug,
            `Fetching auth server metadata for: ${serverMetadataUrl} ...`
        );
        const {
            metadata,
            discoveryUrl,
            errors
        } = await fetchAuthorizationServerMetadata(serverMetadataUrl, {
            additionalHeaders,
            fetch: (url, init) => fetch(url, init)
        });
        for (const err of errors) {
            log(LogLevel.Warning, `Error fetching authorization server metadata: ${err}`);
        }
        log(
            LogLevel.Info,
            `Discovered authorization server metadata at ${discoveryUrl}`
        );
        serverMetadataSource ??= IAuthServerMetadataSource.WellKnown;
        return ( new AuthMetadata(( URI.parse(serverMetadataUrl)), metadata, resource, scopesChallenge, {
            resourceMetadataSource,
            serverMetadataSource
        }, log));
    } catch (e) {
        log(
            LogLevel.Warning,
            `Error populating auth server metadata for ${serverMetadataUrl}: ${String(e)}`
        );
    }
    const defaultMetadata = getDefaultMetadataForUrl(( new URL(baseUrl)));
    log(LogLevel.Info, "Using default auth metadata");
    return ( new AuthMetadata(( URI.parse(baseUrl)), defaultMetadata, resource, scopesChallenge, {
        resourceMetadataSource,
        serverMetadataSource: IAuthServerMetadataSource.Default
    }, log));
}
function parseWWWAuthenticateHeaderForChallenges(wwwAuthenticateValue, log) {
    if (!wwwAuthenticateValue) {
        return {};
    }
    let resourceMetadataChallenge;
    let scopesChallenge;
    const challenges = parseWWWAuthenticateHeader(wwwAuthenticateValue);
    for (const challenge of challenges) {
        if (challenge.scheme === "Bearer") {
            if (!resourceMetadataChallenge && challenge.params["resource_metadata"]) {
                resourceMetadataChallenge = challenge.params["resource_metadata"];
                log(
                    LogLevel.Debug,
                    `Found resource_metadata challenge in WWW-Authenticate header: ${resourceMetadataChallenge}`
                );
            }
            if (!scopesChallenge && challenge.params["scope"]) {
                const scopes = challenge.params["scope"].split(AUTH_SCOPE_SEPARATOR).filter(s => s.trim().length);
                if (scopes.length) {
                    log(
                        LogLevel.Debug,
                        `Found scope challenge in WWW-Authenticate header: ${challenge.params["scope"]}`
                    );
                    scopesChallenge = scopes;
                }
            }
            if (resourceMetadataChallenge && scopesChallenge) {
                break;
            }
        }
    }
    return {
        resourceMetadataChallenge,
        scopesChallenge
    };
}

export { ExtHostMcpService, IExtHostMpcService, McpHTTPHandle, createAuthMetadata };
