File: Mcp\SelectAppHostTool.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.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 System.Text.Json;
using Aspire.Cli.Backchannel;
using ModelContextProtocol.Protocol;
 
namespace Aspire.Cli.Mcp;
 
/// <summary>
/// MCP tool for selecting which AppHost to use when multiple are running.
/// </summary>
internal sealed class SelectAppHostTool(IAuxiliaryBackchannelMonitor auxiliaryBackchannelMonitor, CliExecutionContext executionContext) : CliMcpTool
{
    public override string Name => "select_apphost";
 
    public override string Description => "Selects which AppHost to use when multiple AppHosts are running. The path can be a fully qualified path or a workspace root relative path.";
 
    public override JsonElement GetInputSchema()
    {
        return JsonDocument.Parse("""
            {
              "type": "object",
              "properties": {
                "appHostPath": {
                  "type": "string",
                  "description": "The fully qualified or workspace root relative path to the AppHost project."
                }
              },
              "required": ["appHostPath"]
            }
            """).RootElement;
    }
 
    public override ValueTask<CallToolResult> CallToolAsync(ModelContextProtocol.Client.McpClient mcpClient, IReadOnlyDictionary<string, JsonElement>? arguments, CancellationToken cancellationToken)
    {
        // This tool does not use the MCP client as it operates locally
        _ = mcpClient;
        _ = cancellationToken;
 
        if (arguments == null || !arguments.TryGetValue("appHostPath", out var appHostPathElement))
        {
            return ValueTask.FromResult(new CallToolResult
            {
                IsError = true,
                Content = [new TextContentBlock { Text = "The 'appHostPath' argument is required." }]
            });
        }
 
        var appHostPath = appHostPathElement.GetString();
        if (string.IsNullOrWhiteSpace(appHostPath))
        {
            return ValueTask.FromResult(new CallToolResult
            {
                IsError = true,
                Content = [new TextContentBlock { Text = "The 'appHostPath' argument cannot be empty." }]
            });
        }
 
        // Resolve the path to an absolute path
        string resolvedPath;
        if (Path.IsPathRooted(appHostPath))
        {
            resolvedPath = Path.GetFullPath(appHostPath);
        }
        else
        {
            resolvedPath = Path.GetFullPath(Path.Combine(executionContext.WorkingDirectory.FullName, appHostPath));
        }
 
        // Check if there's a running AppHost with this path
        var matchingConnection = auxiliaryBackchannelMonitor.Connections.Values
            .FirstOrDefault(c =>
            {
                if (c.AppHostInfo?.AppHostPath is null)
                {
                    return false;
                }
                var candidatePath = Path.GetFullPath(c.AppHostInfo.AppHostPath);
                return string.Equals(candidatePath, resolvedPath, StringComparison.OrdinalIgnoreCase);
            });
 
        if (matchingConnection == null)
        {
            // List available AppHosts
            var availableAppHosts = auxiliaryBackchannelMonitor.Connections.Values
                .Where(c => c.AppHostInfo?.AppHostPath != null)
                .Select(c => c.AppHostInfo!.AppHostPath)
                .ToList();
 
            var message = $"No running AppHost found at path '{resolvedPath}'.";
            if (availableAppHosts.Count > 0)
            {
                message += $" Available AppHosts:\n{string.Join("\n", availableAppHosts.Select(p => $"  - {p}"))}";
            }
            else
            {
                message += " No AppHosts are currently running.";
            }
 
            return ValueTask.FromResult(new CallToolResult
            {
                IsError = true,
                Content = [new TextContentBlock { Text = message }]
            });
        }
 
        // Set the selected AppHost path
        auxiliaryBackchannelMonitor.SelectedAppHostPath = resolvedPath;
 
        return ValueTask.FromResult(new CallToolResult
        {
            Content = [new TextContentBlock { Text = $"Selected AppHost: {resolvedPath}" }]
        });
    }
}