| File: Mcp\Tools\ExecuteResourceCommandTool.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 System.Text.Json; using Aspire.Cli.Backchannel; using Microsoft.Extensions.Logging; using ModelContextProtocol; using ModelContextProtocol.Protocol; namespace Aspire.Cli.Mcp.Tools; /// <summary> /// MCP tool for executing commands on resources. /// Executes commands directly via the AppHost backchannel. /// </summary> internal sealed class ExecuteResourceCommandTool( IAuxiliaryBackchannelMonitor auxiliaryBackchannelMonitor, ILogger<ExecuteResourceCommandTool> logger) : CliMcpTool { public override string Name => KnownMcpTools.ExecuteResourceCommand; public override string Description => "Executes a command on a resource. If a resource needs to be restarted and is currently stopped, use the start command instead."; public override JsonElement GetInputSchema() { return JsonDocument.Parse(""" { "type": "object", "properties": { "resourceName": { "type": "string", "description": "The resource name" }, "commandName": { "type": "string", "description": "The command name" } }, "required": ["resourceName", "commandName"] } """).RootElement; } public override async ValueTask<CallToolResult> CallToolAsync(CallToolContext context, CancellationToken cancellationToken) { var arguments = context.Arguments; if (arguments is null || !arguments.TryGetValue("resourceName", out var resourceNameElement) || !arguments.TryGetValue("commandName", out var commandNameElement)) { throw new McpProtocolException("Missing required arguments 'resourceName' and 'commandName'.", McpErrorCode.InvalidParams); } var resourceName = resourceNameElement.GetString(); var commandName = commandNameElement.GetString(); if (string.IsNullOrEmpty(resourceName) || string.IsNullOrEmpty(commandName)) { throw new McpProtocolException("Arguments 'resourceName' and 'commandName' cannot be empty.", McpErrorCode.InvalidParams); } var connection = await AppHostConnectionHelper.GetSelectedConnectionAsync(auxiliaryBackchannelMonitor, logger, cancellationToken).ConfigureAwait(false); if (connection is null) { logger.LogWarning("No Aspire AppHost is currently running"); throw new McpProtocolException(McpErrorMessages.NoAppHostRunning, McpErrorCode.InternalError); } try { logger.LogDebug("Executing command '{CommandName}' on resource '{ResourceName}' via backchannel", commandName, resourceName); var response = await connection.ExecuteResourceCommandAsync(resourceName, commandName, cancellationToken).ConfigureAwait(false); if (response.Success) { return new CallToolResult { Content = [new TextContentBlock { Text = $"Command '{commandName}' executed successfully on resource '{resourceName}'." }] }; } else if (response.Canceled) { throw new McpProtocolException($"Command '{commandName}' was cancelled.", McpErrorCode.InternalError); } else { var message = response.ErrorMessage is { Length: > 0 } ? response.ErrorMessage : "Unknown error. See logs for details."; throw new McpProtocolException($"Command '{commandName}' failed for resource '{resourceName}': {message}", McpErrorCode.InternalError); } } catch (McpProtocolException) { throw; } catch (Exception ex) { logger.LogError(ex, "Error executing command '{CommandName}' on resource '{ResourceName}'", commandName, resourceName); throw new McpProtocolException($"Error executing command '{commandName}' for resource '{resourceName}': {ex.Message}", McpErrorCode.InternalError); } } }