File: Backchannel\AppHostConnectionHelper.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.Logging;
using ModelContextProtocol;
 
namespace Aspire.Cli.Backchannel;
 
/// <summary>
/// Provides helper methods for working with AppHost connections.
/// </summary>
internal static class AppHostConnectionHelper
{
    /// <summary>
    /// Finds the connection whose AppHost project has the same filesystem identity as <paramref name="appHostPath"/>.
    /// </summary>
    public static IAppHostAuxiliaryBackchannel? FindConnectionByAppHostPath(
        IEnumerable<IAppHostAuxiliaryBackchannel> connections,
        string appHostPath)
    {
        return connections.SingleOrDefault(connection =>
            connection.AppHostInfo?.AppHostPath is { } candidatePath &&
            AppHostPathComparer.PathsEqual(candidatePath, appHostPath));
    }
 
    /// <summary>
    /// Gets the appropriate AppHost connection for MCP operations.
    /// </summary>
    /// <param name="auxiliaryBackchannelMonitor">The backchannel monitor to get connections from.</param>
    /// <param name="logger">Logger for debug output.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The selected connection, or null if none available.</returns>
    public static async Task<IAppHostAuxiliaryBackchannel?> GetSelectedConnectionAsync(
        IAuxiliaryBackchannelMonitor auxiliaryBackchannelMonitor,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var connections = auxiliaryBackchannelMonitor.Connections.ToList();
        var scannedConnections = false;
 
        if (connections.Count == 0)
        {
            await auxiliaryBackchannelMonitor.ScanAsync(cancellationToken).ConfigureAwait(false);
            scannedConnections = true;
            connections = auxiliaryBackchannelMonitor.Connections.ToList();
        }
 
        // Check if a specific AppHost was selected
        var selectedPath = auxiliaryBackchannelMonitor.SelectedAppHostPath;
        if (!string.IsNullOrEmpty(selectedPath))
        {
            var selectedConnection = FindSelectedConnection(connections, selectedPath, logger);
 
            if (selectedConnection is null && !scannedConnections)
            {
                // Explicit selections need one fresh scan before we report the pinned AppHost
                // unavailable. Cached unrelated connections can otherwise hide a newly started
                // AppHost until the next background scan.
                await auxiliaryBackchannelMonitor.ScanAsync(cancellationToken).ConfigureAwait(false);
                connections = auxiliaryBackchannelMonitor.Connections.ToList();
                selectedConnection = FindSelectedConnection(connections, selectedPath, logger);
            }
 
            if (selectedConnection is not null)
            {
                logger.LogDebug("Using explicitly selected AppHost: {AppHostPath}", selectedPath);
                return selectedConnection;
            }
 
            logger.LogWarning("The explicitly selected AppHost is unavailable: {AppHostPath}", selectedPath);
            throw new McpProtocolException(
                "The selected AppHost is not available. Start that AppHost and retry.",
                McpErrorCode.InternalError);
        }
 
        if (connections.Count == 0)
        {
            return null;
        }
 
        // Get in-scope connections
        var inScopeConnections = connections.Where(c => c.IsInScope).ToList();
 
        if (inScopeConnections.Count == 1)
        {
            logger.LogDebug("Using single in-scope AppHost: {AppHostPath}", inScopeConnections[0].AppHostInfo?.AppHostPath ?? "N/A");
            return inScopeConnections[0];
        }
 
        if (inScopeConnections.Count > 1)
        {
            throw new McpProtocolException(
                "Multiple Aspire AppHosts are running in the MCP server's working directory scope. " +
                "Use 'select_apphost' to choose the AppHost for this request.",
                McpErrorCode.InternalError);
        }
 
        throw new McpProtocolException(
            "Running Aspire AppHosts were found outside the MCP server's working directory scope. " +
            "Use 'list_apphosts' to discover available AppHosts, then 'select_apphost' to choose one.",
            McpErrorCode.InternalError);
    }
 
    private static IAppHostAuxiliaryBackchannel? FindSelectedConnection(
        IEnumerable<IAppHostAuxiliaryBackchannel> connections,
        string selectedPath,
        ILogger logger)
    {
        try
        {
            return FindConnectionByAppHostPath(connections, selectedPath);
        }
        catch (InvalidOperationException)
        {
            logger.LogWarning("Multiple running AppHost instances match the selected path: {AppHostPath}", selectedPath);
            throw new McpProtocolException(
                "Multiple running AppHost instances match the selected path. Stop the extra instance and retry.",
                McpErrorCode.InternalError);
        }
    }
}