Skip to content

Add command result support for resource commands#15622

Open
davidfowl wants to merge 4 commits intomainfrom
davidfowl/command-results
Open

Add command result support for resource commands#15622
davidfowl wants to merge 4 commits intomainfrom
davidfowl/command-results

Conversation

@davidfowl
Copy link
Contributor

@davidfowl davidfowl commented Mar 26, 2026

Description

Adds Result and ResultFormat properties to ExecuteCommandResult, allowing resource commands to return structured output data (text or JSON) back to the caller. The result flows through the entire pipeline: model → proto/gRPC → backchannel → Dashboard UI → CLI → MCP tools.

Model: New CommandResultFormat enum (None, Text, Json), Result/ResultFormat on ExecuteCommandResult, and CommandResults.Success(result, format) overload.

Proto/gRPC: ResourceCommandResponse gets optional string result and CommandResultFormat result_format fields.

Backchannel: ExecuteResourceCommandResponse gains Result and ResultFormat (string) properties.

Dashboard: On command success with result data, opens TextVisualizerDialog with syntax highlighting. JSON results lock the viewer to JSON mode.

CLI: Result written to stdout via DisplayRawText(result, ConsoleOutput.Standard). Status messages routed to stderr, enabling clean piping (e.g., aspire resource myResource generate-token | jq .token). MCP tool returns result as additional TextContentBlock.

Replica aggregation: Takes the first successful replica's result when multiple replicas return data.

Playground: Added "Generate Token" (JSON) and "Get Connection String" (text) demo commands on stress-telemetryservice.

Fixes #15610

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?

@github-actions
Copy link
Contributor

github-actions bot commented Mar 26, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15622

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15622"

@afscrome
Copy link
Contributor

Something I've been playing around with in my JWT scenario is writing the output directly to the user's clipboard - something worth thinking about. Works reasonably well for secrets

@davidfowl davidfowl force-pushed the davidfowl/command-results branch 2 times, most recently from 2f9c523 to fc149ed Compare March 26, 2026 23:08
@davidfowl davidfowl marked this pull request as ready for review March 27, 2026 01:19
Copilot AI review requested due to automatic review settings March 27, 2026 01:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for resource commands to return structured result data (text/JSON) back to callers (Dashboard, CLI, MCP), flowing from hosting model → backchannel/gRPC → UI/CLI.

Changes:

  • Introduces CommandResultFormat and extends ExecuteCommandResult/CommandResults.Success(...) to carry result payload + format.
  • Extends dashboard gRPC/backchannel contracts and mapping to transport result/result_format.
  • Updates Dashboard/CLI/MCP to surface command results, plus adds/updates tests and regenerated polyglot codegen snapshots.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs Adds hosting tests for result propagation and replica aggregation behavior.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts Updates TS generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs Updates Rust generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py Updates Python generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java Updates Java generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go Updates Go generated API snapshots for new fields/enum.
tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs Adds a hook to capture DisplayMessage calls in CLI tests.
tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs Verifies MCP tool returns additional content block when a command returns result data.
tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs Adds CLI tests around displaying command results.
src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto Adds result and result_format fields + CommandResultFormat enum to the dashboard proto.
src/Aspire.Hosting/Dashboard/DashboardServiceData.cs Extends command execution tuple to include result payload + format.
src/Aspire.Hosting/Dashboard/DashboardService.cs Maps hosting command results into gRPC response fields.
src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs Adds backchannel DTO properties for result payload + format.
src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs Maps hosting ExecuteCommandResult to backchannel result fields.
src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs Aggregates replica results and returns the first successful result payload/format.
src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs Adds CommandResultFormat, ExecuteCommandResult.Result/ResultFormat, and CommandResults.Success(...) overload.
src/Aspire.Dashboard/ServiceClient/Partials.cs Maps gRPC result fields into dashboard view model.
src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs Adds result payload + format to the dashboard model and defines CommandResultFormat.
src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs Opens a text visualizer dialog on successful command results (locks to JSON when applicable).
src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs Returns command result as an additional MCP TextContentBlock on success.
src/Aspire.Cli/Commands/ResourceCommandHelper.cs Displays command result in CLI after successful execution.
playground/Stress/Stress.AppHost/Program.cs Adds sample commands returning JSON/text results in the stress playground.

Comment on lines +41 to +47
Assert.Equal(0, exitCode);
Assert.NotNull(capturedMessage);
// Verify the brackets are escaped so Spectre doesn't interpret them as markup
// EscapeMarkup doubles [ and ] so [" becomes [["
Assert.Contains("[[", capturedMessage);
Assert.Contains("]]", capturedMessage);
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts that ResourceCommandHelper escapes Spectre markup (expects [[/]]), but in production the escaping is performed by ConsoleInteractionService.DisplayMessage when allowMarkup is false. With the current helper implementation, real output will be double-escaped; if the helper is fixed to stop pre-escaping, this test should be updated to reflect the actual contract (helper passes raw result; interaction service handles escaping), or TestInteractionService.DisplayMessage should mimic the production escaping behavior to catch double-escaping regressions.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — this test was rewritten to verify raw text output (no escaping). The helper now uses DisplayRawText instead of DisplayMessage, so no markup escaping is involved.

Copy link
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl force-pushed the davidfowl/command-results branch from 4619b34 to 0898d6d Compare March 27, 2026 05:04
@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings — 52 recordings uploaded (commit 0898d6d)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23632067527

Comment on lines +73 to +76
if (response.Result is not null)
{
interactionService.DisplayRawText(response.Result, ConsoleOutput.Standard);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only display result on success? I can imagine wanting to display output on error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — result is now displayed on both success and error paths.

Comment on lines +77 to +85
var content = new List<TextContentBlock>
{
new() { Text = $"Command '{commandName}' executed successfully on resource '{resourceName}'." }
};

if (response.Result is not null)
{
content.Add(new TextContentBlock { Text = response.Result });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in previous comment, result is only available for success result.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — MCP tool now returns result on error too via CallToolResult with IsError = true (instead of throwing).

public sealed class DashboardCommandExecutor(
IDashboardClient dashboardClient,
IDialogService dialogService,
IServiceProvider serviceProvider,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not pass in DashboardDialogService directly? It and the containing type have the same lifetime (scoped) so shouldn't it be fine?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a whole debate with the agent about blazor, and the fact that this is a singleton resolving a scoped service (which is badness)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — DashboardCommandExecutor is registered as scoped (TryAddScoped), not singleton. Fixed to inject DashboardDialogService directly.

Comment on lines +153 to +163
if (response.Result is not null)
{
var fixedFormat = response.ResultFormat == CommandResultFormat.Json ? DashboardUIHelpers.JsonFormat : null;
await TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions
{
DialogService = serviceProvider.GetRequiredService<DashboardDialogService>(),
ValueDescription = command.GetDisplayName(),
Value = response.Result,
FixedFormat = fixedFormat
}).ConfigureAwait(false);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The await TextVisualizerDialog.OpenDialogAsync(...) blocks until the user dismisses the dialog. This runs before the toast update logic below, so while the dialog is open the toast is either stuck in the "Executing command…" progress state or auto-closes via closeToastCts. The success toast update is deferred until after the dialog is dismissed.

Consider either moving this call to after the toast update block, or fire-and-forgetting the dialog so the toast updates immediately.

Copy link
Member

@JamesNK JamesNK Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested and I didn't see this. And this wouldn't be a concern after switching to accessing result via persistent notification.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — toast ordering isn't a visible issue in practice and will be moot with the persistent notification approach.

Comment on lines +74 to +76
{
interactionService.DisplayRawText(response.Result, ConsoleOutput.Standard);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This result handling only exists in ExecuteGenericCommandAsync. The sibling path via ExecuteResourceCommandAsyncHandleResponse doesn't check response.Result, so if a well-known command (start/stop/restart) ever returns result data, it will be silently discarded. Worth either forwarding results in HandleResponse too, or documenting that well-known commands intentionally ignore result data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — HandleResponse now also outputs response.Result when present, so well-known commands won't silently discard result data.

davidfowl and others added 3 commits March 27, 2026 09:55
- Display result data on both success and error paths in CLI and MCP
- Add result handling to HandleResponse for well-known commands
- MCP tool returns CallToolResult with IsError instead of throwing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Failure(errorMessage, result, resultFormat) overload to CommandResults
- Add test for error path with result data in ResourceCommandHelperTests
- Add 'Validate Config' (JSON error) and 'Check Health' (text error) demo
  commands to Stress playground

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both DashboardCommandExecutor and DashboardDialogService are scoped,
so direct injection is safe and simpler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve using commands from the CLI or MCP

4 participants