@@ -28,13 +28,23 @@ namespace Aspire.Cli.Commands;
2828/// </summary>
2929internal sealed class AgentMcpCommand : BaseCommand
3030{
31- private readonly Dictionary < string , CliMcpTool > _knownTools ;
31+ private readonly Dictionary < string , CliMcpTool > _knownTools = [ ] ;
3232 private readonly IMcpResourceToolRefreshService _resourceToolRefreshService ;
3333 private McpServer ? _server ;
3434 private readonly IAuxiliaryBackchannelMonitor _auxiliaryBackchannelMonitor ;
3535 private readonly IMcpTransportFactory _transportFactory ;
3636 private readonly ILoggerFactory _loggerFactory ;
3737 private readonly ILogger < AgentMcpCommand > _logger ;
38+ private readonly IHttpClientFactory _httpClientFactory ;
39+ private readonly IPackagingService _packagingService ;
40+ private readonly IEnvironmentChecker _environmentChecker ;
41+ private readonly IDocsSearchService _docsSearchService ;
42+ private readonly IDocsIndexService _docsIndexService ;
43+ private readonly CliExecutionContext _executionContext ;
44+ private bool _dashboardOnlyMode ;
45+
46+ private static readonly Option < string ? > s_dashboardUrlOption = TelemetryCommandHelpers . CreateDashboardUrlOption ( ) ;
47+ private static readonly Option < string ? > s_apiKeyOption = TelemetryCommandHelpers . CreateApiKeyOption ( ) ;
3848
3949 /// <summary>
4050 /// Gets the dictionary of known MCP tools. Exposed for testing purposes.
@@ -62,24 +72,16 @@ public AgentMcpCommand(
6272 _transportFactory = transportFactory ;
6373 _loggerFactory = loggerFactory ;
6474 _logger = logger ;
75+ _httpClientFactory = httpClientFactory ;
76+ _packagingService = packagingService ;
77+ _environmentChecker = environmentChecker ;
78+ _docsSearchService = docsSearchService ;
79+ _docsIndexService = docsIndexService ;
80+ _executionContext = executionContext ;
6581 _resourceToolRefreshService = new McpResourceToolRefreshService ( auxiliaryBackchannelMonitor , loggerFactory . CreateLogger < McpResourceToolRefreshService > ( ) ) ;
66- _knownTools = new Dictionary < string , CliMcpTool >
67- {
68- [ KnownMcpTools . ListResources ] = new ListResourcesTool ( auxiliaryBackchannelMonitor , loggerFactory . CreateLogger < ListResourcesTool > ( ) ) ,
69- [ KnownMcpTools . ListConsoleLogs ] = new ListConsoleLogsTool ( auxiliaryBackchannelMonitor , loggerFactory . CreateLogger < ListConsoleLogsTool > ( ) ) ,
70- [ KnownMcpTools . ExecuteResourceCommand ] = new ExecuteResourceCommandTool ( auxiliaryBackchannelMonitor , loggerFactory . CreateLogger < ExecuteResourceCommandTool > ( ) ) ,
71- [ KnownMcpTools . ListStructuredLogs ] = new ListStructuredLogsTool ( auxiliaryBackchannelMonitor , httpClientFactory , loggerFactory . CreateLogger < ListStructuredLogsTool > ( ) ) ,
72- [ KnownMcpTools . ListTraces ] = new ListTracesTool ( auxiliaryBackchannelMonitor , httpClientFactory , loggerFactory . CreateLogger < ListTracesTool > ( ) ) ,
73- [ KnownMcpTools . ListTraceStructuredLogs ] = new ListTraceStructuredLogsTool ( auxiliaryBackchannelMonitor , httpClientFactory , loggerFactory . CreateLogger < ListTraceStructuredLogsTool > ( ) ) ,
74- [ KnownMcpTools . SelectAppHost ] = new SelectAppHostTool ( auxiliaryBackchannelMonitor , executionContext ) ,
75- [ KnownMcpTools . ListAppHosts ] = new ListAppHostsTool ( auxiliaryBackchannelMonitor , executionContext ) ,
76- [ KnownMcpTools . ListIntegrations ] = new ListIntegrationsTool ( packagingService , executionContext , auxiliaryBackchannelMonitor ) ,
77- [ KnownMcpTools . Doctor ] = new DoctorTool ( environmentChecker ) ,
78- [ KnownMcpTools . RefreshTools ] = new RefreshToolsTool ( _resourceToolRefreshService ) ,
79- [ KnownMcpTools . ListDocs ] = new ListDocsTool ( docsIndexService ) ,
80- [ KnownMcpTools . SearchDocs ] = new SearchDocsTool ( docsSearchService , docsIndexService ) ,
81- [ KnownMcpTools . GetDoc ] = new GetDocTool ( docsIndexService )
82- } ;
82+
83+ Options . Add ( s_dashboardUrlOption ) ;
84+ Options . Add ( s_apiKeyOption ) ;
8385 }
8486
8587 protected override bool UpdateNotificationsEnabled => false ;
@@ -95,6 +97,44 @@ internal Task<int> ExecuteCommandAsync(ParseResult parseResult, CancellationToke
9597
9698 protected override async Task < int > ExecuteAsync ( ParseResult parseResult , CancellationToken cancellationToken )
9799 {
100+ var dashboardUrl = parseResult . GetValue ( s_dashboardUrlOption ) ;
101+ var apiKey = parseResult . GetValue ( s_apiKeyOption ) ;
102+
103+ if ( dashboardUrl is not null )
104+ {
105+ if ( ! UrlHelper . IsHttpUrl ( dashboardUrl ) )
106+ {
107+ _logger . LogError ( "Invalid --dashboard-url: {DashboardUrl}" , dashboardUrl ) ;
108+ return ExitCodeConstants . InvalidCommand ;
109+ }
110+
111+ _dashboardOnlyMode = true ;
112+ var staticProvider = new StaticDashboardInfoProvider ( dashboardUrl , apiKey ) ;
113+
114+ _knownTools [ KnownMcpTools . ListStructuredLogs ] = new ListStructuredLogsTool ( staticProvider , _httpClientFactory , _loggerFactory . CreateLogger < ListStructuredLogsTool > ( ) ) ;
115+ _knownTools [ KnownMcpTools . ListTraces ] = new ListTracesTool ( staticProvider , _httpClientFactory , _loggerFactory . CreateLogger < ListTracesTool > ( ) ) ;
116+ _knownTools [ KnownMcpTools . ListTraceStructuredLogs ] = new ListTraceStructuredLogsTool ( staticProvider , _httpClientFactory , _loggerFactory . CreateLogger < ListTraceStructuredLogsTool > ( ) ) ;
117+ }
118+ else
119+ {
120+ var dashboardInfoProvider = new BackchannelDashboardInfoProvider ( _auxiliaryBackchannelMonitor , _logger ) ;
121+
122+ _knownTools [ KnownMcpTools . ListResources ] = new ListResourcesTool ( _auxiliaryBackchannelMonitor , _loggerFactory . CreateLogger < ListResourcesTool > ( ) ) ;
123+ _knownTools [ KnownMcpTools . ListConsoleLogs ] = new ListConsoleLogsTool ( _auxiliaryBackchannelMonitor , _loggerFactory . CreateLogger < ListConsoleLogsTool > ( ) ) ;
124+ _knownTools [ KnownMcpTools . ExecuteResourceCommand ] = new ExecuteResourceCommandTool ( _auxiliaryBackchannelMonitor , _loggerFactory . CreateLogger < ExecuteResourceCommandTool > ( ) ) ;
125+ _knownTools [ KnownMcpTools . ListStructuredLogs ] = new ListStructuredLogsTool ( dashboardInfoProvider , _httpClientFactory , _loggerFactory . CreateLogger < ListStructuredLogsTool > ( ) ) ;
126+ _knownTools [ KnownMcpTools . ListTraces ] = new ListTracesTool ( dashboardInfoProvider , _httpClientFactory , _loggerFactory . CreateLogger < ListTracesTool > ( ) ) ;
127+ _knownTools [ KnownMcpTools . ListTraceStructuredLogs ] = new ListTraceStructuredLogsTool ( dashboardInfoProvider , _httpClientFactory , _loggerFactory . CreateLogger < ListTraceStructuredLogsTool > ( ) ) ;
128+ _knownTools [ KnownMcpTools . SelectAppHost ] = new SelectAppHostTool ( _auxiliaryBackchannelMonitor , _executionContext ) ;
129+ _knownTools [ KnownMcpTools . ListAppHosts ] = new ListAppHostsTool ( _auxiliaryBackchannelMonitor , _executionContext ) ;
130+ _knownTools [ KnownMcpTools . ListIntegrations ] = new ListIntegrationsTool ( _packagingService , _executionContext , _auxiliaryBackchannelMonitor ) ;
131+ _knownTools [ KnownMcpTools . Doctor ] = new DoctorTool ( _environmentChecker ) ;
132+ _knownTools [ KnownMcpTools . RefreshTools ] = new RefreshToolsTool ( _resourceToolRefreshService ) ;
133+ _knownTools [ KnownMcpTools . ListDocs ] = new ListDocsTool ( _docsIndexService ) ;
134+ _knownTools [ KnownMcpTools . SearchDocs ] = new SearchDocsTool ( _docsSearchService , _docsIndexService ) ;
135+ _knownTools [ KnownMcpTools . GetDoc ] = new GetDocTool ( _docsIndexService ) ;
136+ }
137+
98138 var icons = McpIconHelper . GetAspireIcons ( typeof ( AgentMcpCommand ) . Assembly , "Aspire.Cli.Mcp.Resources" ) ;
99139
100140 var options = new McpServerOptions
@@ -135,32 +175,40 @@ private async ValueTask<ListToolsResult> HandleListToolsAsync(RequestContext<Lis
135175
136176 var tools = new List < Tool > ( ) ;
137177
138- tools . AddRange ( KnownTools . Values . Select ( tool => new Tool
178+ tools . AddRange ( KnownTools . Select ( tool => new Tool
139179 {
140- Name = tool . Name ,
141- Description = tool . Description ,
142- InputSchema = tool . GetInputSchema ( )
180+ Name = tool . Value . Name ,
181+ Description = tool . Value . Description ,
182+ InputSchema = tool . Value . GetInputSchema ( )
143183 } ) ) ;
144184
145185 try
146186 {
147- // Refresh resource tools if needed (e.g., AppHost selection changed or invalidated)
148- if ( ! _resourceToolRefreshService . TryGetResourceToolMap ( out var resourceToolMap ) )
187+ // In dashboard-only mode, skip resource tool discovery
188+ if ( _dashboardOnlyMode )
149189 {
150- // Don't send tools/list_changed here — the client already called tools/list
151- // and will receive the up-to-date result. Sending a notification during the
152- // list handler would cause the client to call tools/list again, creating an
153- // infinite loop when tool availability is unstable (e.g., container MCP tools
154- // oscillating between available/unavailable).
155- ( resourceToolMap , _ ) = await _resourceToolRefreshService . RefreshResourceToolMapAsync ( cancellationToken ) ;
190+ _logger . LogDebug ( "Dashboard-only mode: skipping resource tool discovery" ) ;
156191 }
157-
158- tools . AddRange ( resourceToolMap . Select ( x => new Tool
192+ else
159193 {
160- Name = x . Key ,
161- Description = x . Value . Tool . Description ,
162- InputSchema = x . Value . Tool . InputSchema
163- } ) ) ;
194+ // Refresh resource tools if needed (e.g., AppHost selection changed or invalidated)
195+ if ( ! _resourceToolRefreshService . TryGetResourceToolMap ( out var resourceToolMap ) )
196+ {
197+ // Don't send tools/list_changed here — the client already called tools/list
198+ // and will receive the up-to-date result. Sending a notification during the
199+ // list handler would cause the client to call tools/list again, creating an
200+ // infinite loop when tool availability is unstable (e.g., container MCP tools
201+ // oscillating between available/unavailable).
202+ ( resourceToolMap , _ ) = await _resourceToolRefreshService . RefreshResourceToolMapAsync ( cancellationToken ) ;
203+ }
204+
205+ tools . AddRange ( resourceToolMap . Select ( x => new Tool
206+ {
207+ Name = x . Key ,
208+ Description = x . Value . Tool . Description ,
209+ InputSchema = x . Value . Tool . InputSchema
210+ } ) ) ;
211+ }
164212 }
165213 catch ( Exception ex )
166214 {
@@ -179,6 +227,14 @@ private async ValueTask<CallToolResult> HandleCallToolAsync(RequestContext<CallT
179227
180228 _logger . LogDebug ( "MCP CallTool request received for tool: {ToolName}" , toolName ) ;
181229
230+ // In dashboard-only mode, only allow tools that were registered
231+ if ( _dashboardOnlyMode && ! _knownTools . ContainsKey ( toolName ) )
232+ {
233+ throw new McpProtocolException (
234+ $ "Tool '{ toolName } ' is not available in dashboard-only mode. Only telemetry tools (list_structured_logs, list_traces, list_trace_structured_logs) are available when using --dashboard-url.",
235+ McpErrorCode . MethodNotFound ) ;
236+ }
237+
182238 if ( KnownTools . TryGetValue ( toolName , out var tool ) )
183239 {
184240 var args = request . Params ? . Arguments is { } a
0 commit comments