File: Commands\McpCallCommand.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.CommandLine;
using System.Globalization;
using System.Text.Json;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Configuration;
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
 
namespace Aspire.Cli.Commands;
 
/// <summary>
/// Calls an MCP tool on a running Aspire resource.
/// </summary>
internal sealed class McpCallCommand : BaseCommand
{
    internal override HelpGroup HelpGroup => HelpGroup.ToolsAndConfiguration;
 
    private readonly IInteractionService _interactionService;
    private readonly AppHostConnectionResolver _connectionResolver;
 
    private static readonly Argument<string> s_resourceArgument = new("resource")
    {
        Description = McpCommandStrings.CallCommand_ResourceArgumentDescription
    };
 
    private static readonly Argument<string> s_toolArgument = new("tool")
    {
        Description = McpCommandStrings.CallCommand_ToolArgumentDescription
    };
 
    private static readonly Option<string?> s_inputOption = new("--input", "-i")
    {
        Description = McpCommandStrings.CallCommand_InputOptionDescription
    };
 
    private static readonly OptionWithLegacy<FileInfo?> s_appHostOption = new("--apphost", "--project", SharedCommandStrings.AppHostOptionDescription);
 
    public McpCallCommand(
        IInteractionService interactionService,
        IAuxiliaryBackchannelMonitor backchannelMonitor,
        IFeatures features,
        ICliUpdateNotifier updateNotifier,
        CliExecutionContext executionContext,
        AspireCliTelemetry telemetry,
        ILogger<McpCallCommand> logger)
        : base("call", McpCommandStrings.CallCommand_Description, features, updateNotifier, executionContext, interactionService, telemetry)
    {
        _interactionService = interactionService;
        _connectionResolver = new AppHostConnectionResolver(backchannelMonitor, interactionService, executionContext, logger);
 
        Arguments.Add(s_resourceArgument);
        Arguments.Add(s_toolArgument);
        Options.Add(s_inputOption);
        Options.Add(s_appHostOption);
    }
 
    protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
    {
        var resourceName = parseResult.GetValue(s_resourceArgument)!;
        var toolName = parseResult.GetValue(s_toolArgument)!;
        var inputJson = parseResult.GetValue(s_inputOption);
        var passedAppHostProjectFile = parseResult.GetValue(s_appHostOption);
 
        var result = await _connectionResolver.ResolveConnectionAsync(
            passedAppHostProjectFile,
            SharedCommandStrings.ScanningForRunningAppHosts,
            string.Format(CultureInfo.CurrentCulture, SharedCommandStrings.SelectAppHost, "call MCP tool on"),
            SharedCommandStrings.AppHostNotRunning,
            cancellationToken);
 
        if (!result.Success)
        {
            _interactionService.DisplayError(result.ErrorMessage);
            return ExitCodeConstants.FailedToDotnetRunAppHost;
        }
 
        var connection = result.Connection!;
 
        // Parse input JSON into arguments dictionary
        IReadOnlyDictionary<string, JsonElement>? arguments = null;
        if (!string.IsNullOrEmpty(inputJson))
        {
            try
            {
                using var doc = JsonDocument.Parse(inputJson);
                if (doc.RootElement.ValueKind != JsonValueKind.Object)
                {
                    _interactionService.DisplayError("Invalid JSON input: expected a JSON object.");
                    return ExitCodeConstants.InvalidCommand;
                }
                var dict = new Dictionary<string, JsonElement>();
                foreach (var prop in doc.RootElement.EnumerateObject())
                {
                    dict[prop.Name] = prop.Value.Clone();
                }
                arguments = dict;
            }
            catch (JsonException ex)
            {
                _interactionService.DisplayError($"Invalid JSON input: {ex.Message}");
                return ExitCodeConstants.InvalidCommand;
            }
        }
 
        try
        {
            var callResult = await connection.CallResourceMcpToolAsync(
                resourceName,
                toolName,
                arguments,
                cancellationToken);
 
            // Output the result content
            if (callResult.Content is { Count: > 0 })
            {
                foreach (var content in callResult.Content)
                {
                    if (content is TextContentBlock textContent)
                    {
                        _interactionService.DisplayRawText(textContent.Text);
                    }
                    else
                    {
                        using var stream = new MemoryStream();
                        using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
                        {
                            writer.WriteStartObject();
                            writer.WriteString("type", content.Type);
                            writer.WriteEndObject();
                        }
                        _interactionService.DisplayRawText(System.Text.Encoding.UTF8.GetString(stream.ToArray()));
                    }
                }
            }
 
            if (callResult.IsError == true)
            {
                return ExitCodeConstants.InvalidCommand;
            }
 
            return ExitCodeConstants.Success;
        }
        catch (Exception ex)
        {
            _interactionService.DisplayError($"Failed to call tool '{toolName}' on resource '{resourceName}': {ex.Message}");
            return ExitCodeConstants.InvalidCommand;
        }
    }
}