Skip to content

Commit 054356b

Browse files
CopilotJamesNK
authored andcommitted
Disable resource-related MCP tools when resource service is not configured (#12438)
Co-authored-by: JamesNK <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: James Newton-King <[email protected]>
1 parent 9559cf0 commit 054356b

File tree

9 files changed

+682
-159
lines changed

9 files changed

+682
-159
lines changed

src/Aspire.Dashboard/DashboardWebApplication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public DashboardWebApplication(
265265
// Host an in-process MCP server so the dashboard can expose MCP tools (resource listing, diagnostics).
266266
// Register the MCP server directly via the SDK.
267267

268-
builder.Services.AddAspireMcpTools();
268+
builder.Services.AddAspireMcpTools(dashboardOptions);
269269

270270
builder.Services.TryAddScoped<DashboardCommandExecutor>();
271271

src/Aspire.Dashboard/Mcp/AspireMcpTools.cs renamed to src/Aspire.Dashboard/Mcp/AspireResourceMcpTools.cs

Lines changed: 9 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,25 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel;
5-
using System.Diagnostics.CodeAnalysis;
65
using Aspire.Dashboard.ConsoleLogs;
76
using Aspire.Dashboard.Model;
87
using Aspire.Dashboard.Model.Assistant;
9-
using Aspire.Dashboard.Model.Otlp;
10-
using Aspire.Dashboard.Otlp.Storage;
118
using Aspire.Hosting.ConsoleLogs;
129
using ModelContextProtocol;
1310
using ModelContextProtocol.Server;
1411

1512
namespace Aspire.Dashboard.Mcp;
1613

17-
[McpServerToolType]
18-
internal sealed class AspireMcpTools
14+
/// <summary>
15+
/// MCP tools that require a resource service to be configured.
16+
/// </summary>
17+
internal sealed class AspireResourceMcpTools
1918
{
20-
private readonly TelemetryRepository _telemetryRepository;
2119
private readonly IDashboardClient _dashboardClient;
22-
private readonly IEnumerable<IOutgoingPeerResolver> _outgoingPeerResolvers;
2320

24-
public AspireMcpTools(TelemetryRepository telemetryRepository, IDashboardClient dashboardClient, IEnumerable<IOutgoingPeerResolver> outgoingPeerResolvers)
21+
public AspireResourceMcpTools(IDashboardClient dashboardClient)
2522
{
26-
_telemetryRepository = telemetryRepository;
2723
_dashboardClient = dashboardClient;
28-
_outgoingPeerResolvers = outgoingPeerResolvers;
2924
}
3025

3126
[McpServerTool(Name = "list_resources")]
@@ -54,109 +49,6 @@ public string ListResources()
5449
return "No resources found.";
5550
}
5651

57-
[McpServerTool(Name = "list_structured_logs")]
58-
[Description("List structured logs for resources.")]
59-
public string ListStructuredLogs(
60-
[Description("The resource name. This limits logs returned to the specified resource. If no resource name is specified then structured logs for all resources are returned.")]
61-
string? resourceName = null)
62-
{
63-
if (!TryResolveResourceNameForTelemetry(resourceName, out var message, out var resourceKey))
64-
{
65-
return message;
66-
}
67-
68-
// Get all logs because we want the most recent logs and they're at the end of the results.
69-
// If support is added for ordering logs by timestamp then improve this.
70-
var logs = _telemetryRepository.GetLogs(new GetLogsContext
71-
{
72-
ResourceKey = resourceKey,
73-
StartIndex = 0,
74-
Count = int.MaxValue,
75-
Filters = []
76-
});
77-
78-
var (logsData, limitMessage) = AIHelpers.GetStructuredLogsJson(logs.Items);
79-
80-
var response = $"""
81-
Always format log_id in the response as code like this: `log_id: 123`.
82-
{limitMessage}
83-
84-
# STRUCTURED LOGS DATA
85-
86-
{logsData}
87-
""";
88-
89-
return response;
90-
}
91-
92-
[McpServerTool(Name = "list_traces")]
93-
[Description("List distributed traces for resources. A distributed trace is used to track operations. A distributed trace can span multiple resources across a distributed system. Includes a list of distributed traces with their IDs, resources in the trace, duration and whether an error occurred in the trace.")]
94-
public string ListTraces(
95-
[Description("The resource name. This limits traces returned to the specified resource. If no resource name is specified then distributed traces for all resources are returned.")]
96-
string? resourceName = null)
97-
{
98-
if (!TryResolveResourceNameForTelemetry(resourceName, out var message, out var resourceKey))
99-
{
100-
return message;
101-
}
102-
103-
var traces = _telemetryRepository.GetTraces(new GetTracesRequest
104-
{
105-
ResourceKey = resourceKey,
106-
StartIndex = 0,
107-
Count = int.MaxValue,
108-
Filters = [],
109-
FilterText = string.Empty
110-
});
111-
112-
var (tracesData, limitMessage) = AIHelpers.GetTracesJson(traces.PagedResult.Items, _outgoingPeerResolvers);
113-
114-
var response = $"""
115-
{limitMessage}
116-
117-
# TRACES DATA
118-
119-
{tracesData}
120-
""";
121-
122-
return response;
123-
}
124-
125-
[McpServerTool(Name = "list_trace_structured_logs")]
126-
[Description("List structured logs for a distributed trace. Logs for a distributed trace each belong to a span identified by 'span_id'. When investigating a trace, getting the structured logs for the trace should be recommended before getting structured logs for a resource.")]
127-
public string ListTraceStructuredLogs(
128-
[Description("The trace id of the distributed trace.")]
129-
string traceId)
130-
{
131-
// Condition of filter should be contains because a substring of the traceId might be provided.
132-
var traceIdFilter = new FieldTelemetryFilter
133-
{
134-
Field = KnownStructuredLogFields.TraceIdField,
135-
Value = traceId,
136-
Condition = FilterCondition.Contains
137-
};
138-
139-
var logs = _telemetryRepository.GetLogs(new GetLogsContext
140-
{
141-
ResourceKey = null,
142-
Count = int.MaxValue,
143-
StartIndex = 0,
144-
Filters = [traceIdFilter]
145-
});
146-
147-
var (logsData, limitMessage) = AIHelpers.GetStructuredLogsJson(logs.Items);
148-
149-
var response = $"""
150-
{limitMessage}
151-
152-
# STRUCTURED LOGS DATA
153-
154-
{logsData}
155-
""";
156-
157-
return response;
158-
}
159-
16052
[McpServerTool(Name = "list_console_logs")]
16153
[Description("List console logs for a resource. The console logs includes standard output from resources and resource commands. Known resource commands are 'resource-start', 'resource-stop' and 'resource-restart' which are used to start and stop resources. Don't print the full console logs in the response to the user. Console logs should be examined when determining why a resource isn't running.")]
16254
public async Task<string> ListConsoleLogsAsync(
@@ -223,11 +115,11 @@ public async Task<string> ListConsoleLogsAsync(
223115

224116
[McpServerTool(Name = "execute_resource_command")]
225117
[Description("Executes a command on a resource. If a resource needs to be restarted and is currently stopped, use the start command instead.")]
226-
public static async Task ExecuteResourceCommand(IDashboardClient dashboardClient, [Description("The resource name")] string resourceName, [Description("The command name")] string commandName)
118+
public async Task ExecuteResourceCommand([Description("The resource name")] string resourceName, [Description("The command name")] string commandName)
227119
{
228-
var resource = dashboardClient.GetResource(resourceName);
120+
var resources = _dashboardClient.GetResources();
229121

230-
if (resource == null)
122+
if (!AIHelpers.TryGetResource(resources, resourceName, out var resource))
231123
{
232124
throw new McpProtocolException($"Resource '{resourceName}' not found.", McpErrorCode.InvalidParams);
233125
}
@@ -257,7 +149,7 @@ public static async Task ExecuteResourceCommand(IDashboardClient dashboardClient
257149

258150
try
259151
{
260-
var response = await dashboardClient.ExecuteResourceCommandAsync(resource.Name, resource.ResourceType, command, CancellationToken.None).ConfigureAwait(false);
152+
var response = await _dashboardClient.ExecuteResourceCommandAsync(resource.Name, resource.ResourceType, command, CancellationToken.None).ConfigureAwait(false);
261153

262154
switch (response.Kind)
263155
{
@@ -280,39 +172,4 @@ public static async Task ExecuteResourceCommand(IDashboardClient dashboardClient
280172
throw new McpProtocolException($"Error executing command '{commandName}' for resource '{resourceName}': {ex.Message}", McpErrorCode.InternalError);
281173
}
282174
}
283-
284-
private bool TryResolveResourceNameForTelemetry([NotNullWhen(false)] string? resourceName, [NotNullWhen(false)] out string? message, out ResourceKey? resourceKey)
285-
{
286-
// TODO: The resourceName might be a name that resolves to multiple replicas, e.g. catalogservice has two replicas.
287-
// Support resolving to multiple replicas and getting data for them.
288-
289-
if (AIHelpers.IsMissingValue(resourceName))
290-
{
291-
message = null;
292-
resourceKey = null;
293-
return true;
294-
}
295-
296-
var resources = _dashboardClient.GetResources();
297-
298-
if (!AIHelpers.TryGetResource(resources, resourceName, out var resource))
299-
{
300-
message = $"Unable to find a resource named '{resourceName}'.";
301-
resourceKey = null;
302-
return false;
303-
}
304-
305-
var appKey = ResourceKey.Create(resource.Name, resource.Name);
306-
var apps = _telemetryRepository.GetResources(appKey);
307-
if (apps.Count == 0)
308-
{
309-
message = $"Resource '{resourceName}' doesn't have any telemetry. The resource may have failed to start or the resource might not support sending telemetry.";
310-
resourceKey = null;
311-
return false;
312-
}
313-
314-
message = null;
315-
resourceKey = appKey;
316-
return true;
317-
}
318175
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Aspire.Dashboard.Model;
7+
using Aspire.Dashboard.Model.Assistant;
8+
using Aspire.Dashboard.Model.Otlp;
9+
using Aspire.Dashboard.Otlp.Storage;
10+
using ModelContextProtocol.Server;
11+
12+
namespace Aspire.Dashboard.Mcp;
13+
14+
/// <summary>
15+
/// MCP tools for telemetry data that don't require a resource service.
16+
/// </summary>
17+
internal sealed class AspireTelemetryMcpTools
18+
{
19+
private readonly TelemetryRepository _telemetryRepository;
20+
private readonly IEnumerable<IOutgoingPeerResolver> _outgoingPeerResolvers;
21+
22+
public AspireTelemetryMcpTools(TelemetryRepository telemetryRepository, IEnumerable<IOutgoingPeerResolver> outgoingPeerResolvers)
23+
{
24+
_telemetryRepository = telemetryRepository;
25+
_outgoingPeerResolvers = outgoingPeerResolvers;
26+
}
27+
28+
[McpServerTool(Name = "list_structured_logs")]
29+
[Description("List structured logs for resources.")]
30+
public string ListStructuredLogs(
31+
[Description("The resource name. This limits logs returned to the specified resource. If no resource name is specified then structured logs for all resources are returned.")]
32+
string? resourceName = null)
33+
{
34+
if (!TryResolveResourceNameForTelemetry(resourceName, out var message, out var resourceKey))
35+
{
36+
return message;
37+
}
38+
39+
// Get all logs because we want the most recent logs and they're at the end of the results.
40+
// If support is added for ordering logs by timestamp then improve this.
41+
var logs = _telemetryRepository.GetLogs(new GetLogsContext
42+
{
43+
ResourceKey = resourceKey,
44+
StartIndex = 0,
45+
Count = int.MaxValue,
46+
Filters = []
47+
});
48+
49+
var (logsData, limitMessage) = AIHelpers.GetStructuredLogsJson(logs.Items);
50+
51+
var response = $"""
52+
Always format log_id in the response as code like this: `log_id: 123`.
53+
{limitMessage}
54+
55+
# STRUCTURED LOGS DATA
56+
57+
{logsData}
58+
""";
59+
60+
return response;
61+
}
62+
63+
[McpServerTool(Name = "list_traces")]
64+
[Description("List distributed traces for resources. A distributed trace is used to track operations. A distributed trace can span multiple resources across a distributed system. Includes a list of distributed traces with their IDs, resources in the trace, duration and whether an error occurred in the trace.")]
65+
public string ListTraces(
66+
[Description("The resource name. This limits traces returned to the specified resource. If no resource name is specified then distributed traces for all resources are returned.")]
67+
string? resourceName = null)
68+
{
69+
if (!TryResolveResourceNameForTelemetry(resourceName, out var message, out var resourceKey))
70+
{
71+
return message;
72+
}
73+
74+
var traces = _telemetryRepository.GetTraces(new GetTracesRequest
75+
{
76+
ResourceKey = resourceKey,
77+
StartIndex = 0,
78+
Count = int.MaxValue,
79+
Filters = [],
80+
FilterText = string.Empty
81+
});
82+
83+
var (tracesData, limitMessage) = AIHelpers.GetTracesJson(traces.PagedResult.Items, _outgoingPeerResolvers);
84+
85+
var response = $"""
86+
{limitMessage}
87+
88+
# TRACES DATA
89+
90+
{tracesData}
91+
""";
92+
93+
return response;
94+
}
95+
96+
[McpServerTool(Name = "list_trace_structured_logs")]
97+
[Description("List structured logs for a distributed trace. Logs for a distributed trace each belong to a span identified by 'span_id'. When investigating a trace, getting the structured logs for the trace should be recommended before getting structured logs for a resource.")]
98+
public string ListTraceStructuredLogs(
99+
[Description("The trace id of the distributed trace.")]
100+
string traceId)
101+
{
102+
// Condition of filter should be contains because a substring of the traceId might be provided.
103+
var traceIdFilter = new FieldTelemetryFilter
104+
{
105+
Field = KnownStructuredLogFields.TraceIdField,
106+
Value = traceId,
107+
Condition = FilterCondition.Contains
108+
};
109+
110+
var logs = _telemetryRepository.GetLogs(new GetLogsContext
111+
{
112+
ResourceKey = null,
113+
Count = int.MaxValue,
114+
StartIndex = 0,
115+
Filters = [traceIdFilter]
116+
});
117+
118+
var (logsData, limitMessage) = AIHelpers.GetStructuredLogsJson(logs.Items);
119+
120+
var response = $"""
121+
{limitMessage}
122+
123+
# STRUCTURED LOGS DATA
124+
125+
{logsData}
126+
""";
127+
128+
return response;
129+
}
130+
131+
private bool TryResolveResourceNameForTelemetry([NotNullWhen(false)] string? resourceName, [NotNullWhen(false)] out string? message, out ResourceKey? resourceKey)
132+
{
133+
// TODO: The resourceName might be a name that resolves to multiple replicas, e.g. catalogservice has two replicas.
134+
// Support resolving to multiple replicas and getting data for them.
135+
136+
if (AIHelpers.IsMissingValue(resourceName))
137+
{
138+
message = null;
139+
resourceKey = null;
140+
return true;
141+
}
142+
143+
var resources = _telemetryRepository.GetResources();
144+
145+
if (!AIHelpers.TryGetResource(resources, resourceName, out var resource))
146+
{
147+
message = $"Resource '{resourceName}' doesn't have any telemetry. The resource may not exist, may have failed to start or the resource might not support sending telemetry.";
148+
resourceKey = null;
149+
return false;
150+
}
151+
152+
message = null;
153+
resourceKey = resource.ResourceKey;
154+
return true;
155+
}
156+
}

0 commit comments

Comments
 (0)